├── .gitignore ├── README.md ├── composer.json └── src ├── FeatureDescriptors └── Hog.php └── ImageReaders └── Pgm.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Computer Vision (Work in Progress) 2 | 3 | PHP Computer Vision Library. 4 | 5 | ## TO-DO 6 | 7 | The "Issues" page from this repository is being used for TO-DO management, just search for the "to-do" tag. 8 | 9 | ## Credits 10 | 11 | [@gabrielrcouto](http://www.twitter.com/gabrielrcouto) 12 | 13 | ## License 14 | 15 | [MIT License](http://gabrielrcouto.mit-license.org/) 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gabrielrcouto/php-computer-vision", 3 | "description": "PHP Computer Vision", 4 | "keywords": ["php","computer","vision","artificial"], 5 | "homepage": "http://github.com/gabrielrcouto/php-computer-vision", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Gabriel Rodrigues Couto", 10 | "email": "gabrielrcouto@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.0", 15 | "php-ai/php-ml": "^0.4.0" 16 | }, 17 | "require-dev" : { 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "ComputerVision\\": "src/" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/FeatureDescriptors/Hog.php: -------------------------------------------------------------------------------- 1 | image = $image; 38 | } 39 | 40 | if (gettype($image) === 'string') { 41 | $pathinfo = pathinfo($image); 42 | 43 | if ($pathinfo['extension'] === 'png') { 44 | $this->image = imagecreatefrompng($image); 45 | } 46 | 47 | if ($pathinfo['extension'] === 'jpg') { 48 | $this->image = imagecreatefromjpeg($image); 49 | } 50 | 51 | if ($pathinfo['extension'] === 'pgm') { 52 | $this->image = Pgm::loadPGM($image); 53 | } 54 | } 55 | 56 | if (gettype($this->image) !== 'resource') { 57 | throw new \Exception('The image is not a resource or a valid file'); 58 | } 59 | 60 | $this->height = imagesy($this->image); 61 | $this->width = imagesx($this->image); 62 | 63 | $this->cellHeight = $cellHeight; 64 | $this->cellWidth = $cellWidth; 65 | $this->binsNumber = $binsNumber; 66 | $this->blockHeight = $blockHeight; 67 | $this->blockWidth = $blockWidth; 68 | $this->blockStride = $blockStride; 69 | } 70 | 71 | protected function convert2dToImage($matrix) 72 | { 73 | $image = imagecreatetruecolor($this->width, $this->height); 74 | 75 | foreach ($matrix as $y => $yValue) { 76 | foreach ($yValue as $x => $xValue) { 77 | $intensity = floor($matrix[$y][$x] * 255); 78 | $color = imagecolorallocate($image, $intensity, $intensity, $intensity); 79 | imagesetpixel($image, $x, $y, $color); 80 | } 81 | } 82 | 83 | return $image; 84 | } 85 | 86 | protected function convertToGrayscale() 87 | { 88 | imagefilter($this->image, IMG_FILTER_GRAYSCALE); 89 | } 90 | 91 | protected function convertToGradient() 92 | { 93 | $matrix = $this->get2dMatrix(); 94 | $kernel = [-1, 0, 1]; 95 | 96 | $gradient = array_fill(0, $this->height, array_fill(0, $this->width, [])); 97 | 98 | foreach ($matrix as $y => $yValue) { 99 | foreach ($yValue as $x => $xValue) { 100 | $prevX = ($x == 0) ? $matrix[$y][$x] : $matrix[$y][$x - 1]; 101 | $nextX = ($x == $this->width - 1) ? $matrix[$y][$x] : $matrix[$y][$x + 1]; 102 | $prevY = ($y == 0) ? $matrix[$y][$x] : $matrix[$y - 1][$x]; 103 | $nextY = ($y == $this->height - 1) ? $matrix[$y][$x] : $matrix[$y + 1][$x]; 104 | 105 | // kernel [-1, 0 , 1] 106 | $horizontal = -$prevX + $nextX; 107 | $gradient[$y][$x]['horizontal'] = $horizontal; 108 | // kernel [-1, 0 , 1]T 109 | $vertical = -$prevY + $nextY; 110 | $gradient[$y][$x]['vertical'] = $vertical; 111 | 112 | // magnitude = sqrt(sx^2 + sy^2) 113 | $gradient[$y][$x]['magnitude'] = sqrt(pow($horizontal, 2) + pow($vertical, 2)); 114 | 115 | // orientation = arctan(sy/sx) 116 | $gradient[$y][$x]['orientation'] = rad2deg(atan2($vertical, $horizontal)); 117 | 118 | // Normalize orientation to 0 - 180 119 | // atan2 returns -PI to PI 120 | if ($gradient[$y][$x]['orientation'] < 0) { 121 | $gradient[$y][$x]['orientation'] += 180; 122 | } 123 | } 124 | } 125 | 126 | return $gradient; 127 | } 128 | 129 | protected function get2dMatrix() 130 | { 131 | $matrix = array_fill(0, $this->height, array_fill(0, $this->width, 0)); 132 | 133 | foreach ($matrix as $y => $yValue) { 134 | foreach ($yValue as $x => $xValue) { 135 | $pixelRgb = imagecolorat($this->image, $x, $y); 136 | 137 | $r = ($pixelRgb >> 16) & 0xFF; 138 | $g = ($pixelRgb >> 8) & 0xFF; 139 | $b = $pixelRgb & 0xFF; 140 | 141 | if ($r > $g && $r > $b) { 142 | // Unsigned representation of pixel intesity (0 - 1) 143 | $matrix[$y][$x] = $r / 255; 144 | continue; 145 | } 146 | 147 | if ($g > $b) { 148 | // Unsigned representation of pixel intesity (0 - 1) 149 | $matrix[$y][$x] = $g / 255; 150 | continue; 151 | } 152 | 153 | $matrix[$y][$x] = $b / 255; 154 | } 155 | } 156 | 157 | return $matrix; 158 | } 159 | 160 | protected function getDescriptorBlock($cells, $fromX, $fromY, $width, $height) 161 | { 162 | $blockCells = []; 163 | 164 | for ($y = $fromY; $y < $fromY + $height; $y++) { 165 | for ($x = $fromX; $x < $fromX + $width; $x++) { 166 | if (array_key_exists($y, $cells) && array_key_exists($x, $cells[$y])) { 167 | $blockCells[] = $cells[$y][$x]; 168 | } 169 | } 170 | } 171 | 172 | // Block normalization 173 | // In their experiments, Dalal and Triggs found the L2-hys, L2-norm, and 174 | // L1-sqrt schemes provide similar performance, while the L1-norm provides 175 | // slightly less reliable performance; 176 | 177 | // Default PHP-ML Normalizer is L2-norm 178 | $normalizer = new Normalizer(Normalizer::NORM_L1); 179 | $normalizer->transform($blockCells); 180 | 181 | $block = []; 182 | 183 | // Merge all the cells in one vector 184 | foreach ($blockCells as $cell) { 185 | $block = array_merge($block, $cell); 186 | } 187 | 188 | return $block; 189 | } 190 | 191 | protected function getDescriptorBlocks($cells) 192 | { 193 | $blocks = []; 194 | 195 | // Next block can overlap the previous block 196 | // Greater block Stride values, less overlap area 197 | for ($y = 0; $y < count($cells); $y += $this->blockStride) { 198 | for ($x = 0; $x < count($cells[$y]); $x += $this->blockStride) { 199 | $blocks[] = $this->getDescriptorBlock( 200 | $cells, 201 | $x, 202 | $y, 203 | $this->blockWidth, 204 | $this->blockHeight 205 | ); 206 | } 207 | } 208 | 209 | // The final descriptor is then the vector of all components of the 210 | // normalized cell responses from all of the blocks in the detection window 211 | $descriptor = []; 212 | 213 | foreach ($blocks as $block) { 214 | $descriptor = array_merge($descriptor, $block); 215 | } 216 | 217 | return $descriptor; 218 | } 219 | 220 | public function getImage() 221 | { 222 | return $this->image; 223 | } 224 | 225 | protected function getOrientationCell($gradient, $fromX, $fromY, $width, $height, $binsNumber) 226 | { 227 | // We are using unsigned gradient 228 | // Bins are divided from 0 - 180 degrees 229 | $bins = array_fill(0, $binsNumber, 0); 230 | $binDegrees = 180 / $binsNumber; 231 | 232 | for ($y = $fromY; $y < $fromY + $height; $y++) { 233 | for ($x = $fromX; $x < $fromX + $width; $x++) { 234 | if (array_key_exists($y, $gradient) && array_key_exists($x, $gradient[$y])) { 235 | // As for the vote weight, pixel contribution can either be the gradient magnitude itself, 236 | // or some function of the magnitude 237 | 238 | // Weight distribution: 239 | // Interpolate votes linearly between neighboring bin centers 240 | // The closest bin receives the biggest score 241 | 242 | $orientation = $gradient[$y][$x]['orientation']; 243 | $magnitude = $gradient[$y][$x]['magnitude']; 244 | 245 | $orientationDivision = $orientation / $binDegrees; 246 | $binIndex = ceil($orientationDivision); 247 | 248 | if ($binIndex == $binsNumber) { 249 | $binIndex = 0; 250 | } 251 | 252 | $bins[$binIndex] += $magnitude; 253 | } 254 | } 255 | } 256 | 257 | array_map(function ($n) use ($width, $height) { 258 | return $n / ($width * $height); 259 | }, $bins); 260 | 261 | return $bins; 262 | } 263 | 264 | protected function getOrientationCells($gradient) 265 | { 266 | // The cells themselves can either be rectangular or radial in shape, 267 | // and the histogram channels are evenly spread over 0 to 180 degrees (unsigned gradient) 268 | // or 0 to 360 degrees (signed gradient) 269 | $cellsPerColumn = ceil($this->height / $this->cellHeight); 270 | $cellsPerRow = ceil($this->width / $this->cellWidth); 271 | 272 | $cells = array_fill(0, $cellsPerColumn, array_fill(0, $cellsPerRow, [])); 273 | 274 | for ($y = 0; $y < $cellsPerColumn; $y++) { 275 | for ($x = 0; $x < $cellsPerRow; $x++) { 276 | $cells[$y][$x] = $this->getOrientationCell( 277 | $gradient, 278 | $x * $this->cellWidth, 279 | $y * $this->cellHeight, 280 | $this->cellWidth, 281 | $this->cellHeight, 282 | $this->binsNumber 283 | ); 284 | } 285 | } 286 | 287 | return $cells; 288 | } 289 | 290 | public function getHog() 291 | { 292 | // Gradient computation 293 | $gradient = $this->convertToGradient(); 294 | 295 | // Orientation binning 296 | $cells = $this->getOrientationCells($gradient); 297 | 298 | // Descriptor blocks 299 | return $this->getDescriptorBlocks($cells); 300 | } 301 | 302 | public function getHogCells() 303 | { 304 | // Gradient computation 305 | $gradient = $this->convertToGradient(); 306 | 307 | // Orientation binning 308 | $cells = $this->getOrientationCells($gradient); 309 | 310 | $blocks = []; 311 | $descriptor = []; 312 | 313 | for ($y = 0; $y < count($cells); $y++) { 314 | for ($x = 0; $x < count($cells[$y]); $x++) { 315 | $blocks[] = $cells[$y][$x]; 316 | } 317 | } 318 | 319 | $normalizer = new Normalizer(); 320 | $normalizer->transform($blocks); 321 | 322 | foreach ($blocks as $block) { 323 | for ($i = 0; $i < count($block); $i++) { 324 | $descriptor[] = $block[$i]; 325 | } 326 | } 327 | 328 | return $descriptor; 329 | } 330 | 331 | public function resize($width, $height) 332 | { 333 | $image = imagecreatetruecolor($width, $height); 334 | imagecopyresampled($image, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height); 335 | 336 | $this->image = $image; 337 | $this->height = $height; 338 | $this->width = $width; 339 | } 340 | 341 | public function saveGradientImage($imagePath) 342 | { 343 | $length = 5; 344 | 345 | $image = imagecreatetruecolor($this->width * $length, $this->height * $length); 346 | 347 | // Gradient computation 348 | $gradient = $this->convertToGradient(); 349 | 350 | $black = imagecolorallocate($image, 0, 0, 0); 351 | 352 | imagefill($image, 0, 0, $black); 353 | 354 | for ($y = 0; $y < $this->height; $y++) { 355 | for ($x = 0; $x < $this->width; $x++) { 356 | $angle = deg2rad($gradient[$y][$x]['orientation']); 357 | $intensity = 50 + ($gradient[$y][$x]['magnitude'] * 155); 358 | 359 | $white = imagecolorallocate($image, $intensity, $intensity, $intensity); 360 | 361 | $endX = ($x * $length) + cos($angle) * $length; 362 | $endY = ($y * $length) - sin($angle) * $length; 363 | 364 | imageline($image, $x * $length, $y * $length, $endX, $endY, $white); 365 | } 366 | } 367 | 368 | imagejpeg($image, $imagePath); 369 | } 370 | 371 | public function saveDescriptorBlocksImage($imagePath) 372 | { 373 | // Gradient computation 374 | $gradient = $this->convertToGradient(); 375 | 376 | // Orientation binning 377 | $cells = $this->getOrientationCells($gradient); 378 | 379 | $width = ceil(count($cells[0]) / ($this->blockWidth - $this->blockStride)); 380 | $height = ceil(count($cells) / ($this->blockHeight - $this->blockStride)); 381 | $length = 6; 382 | 383 | $binDegrees = 180 / $this->binsNumber; 384 | 385 | $image = imagecreatetruecolor($width * $length, $height * $length); 386 | 387 | $y = 0; 388 | 389 | for ($cellY = 0; $cellY < count($cells); $cellY += $this->blockStride) { 390 | $x = 0; 391 | 392 | for ($cellX = 0; $cellX < count($cells[$cellY]); $cellX += $this->blockStride) { 393 | $bins = $this->getDescriptorBlock( 394 | $cells, 395 | $cellX, 396 | $cellY, 397 | $this->blockWidth, 398 | $this->blockHeight 399 | ); 400 | 401 | foreach ($bins as $key => $bin) { 402 | $binIndex = $key % $this->binsNumber; 403 | $angle = deg2rad($binIndex * $binDegrees); 404 | $intensity = $bin * 255; 405 | 406 | $white = imagecolorallocate($image, $intensity, $intensity, $intensity); 407 | 408 | $endX = (($x * $length) + $length / 2) + cos($angle) * ($length / 2); 409 | $endY = (($y * $length) + $length / 2) - sin($angle) * ($length / 2); 410 | 411 | imageline($image, (($x * $length) + $length / 2), (($y * $length) + $length / 2), $endX, $endY, $white); 412 | } 413 | 414 | ++$x; 415 | } 416 | 417 | ++$y; 418 | } 419 | 420 | imagejpeg($image, $imagePath); 421 | } 422 | 423 | public function saveOrientationCellsImage($imagePath) 424 | { 425 | // Gradient computation 426 | $gradient = $this->convertToGradient(); 427 | 428 | // Orientation binning 429 | $cells = $this->getOrientationCells($gradient); 430 | 431 | $length = $this->cellWidth * 6; 432 | $width = count($cells[0]); 433 | $height = count($cells); 434 | $binDegrees = 180 / $this->binsNumber; 435 | 436 | $image = imagecreatetruecolor($width * $length, $height * $length); 437 | 438 | for ($y = 0; $y < $height; $y++) { 439 | for ($x = 0; $x < $width; $x++) { 440 | $bins = $cells[$y][$x]; 441 | 442 | $sum = array_reduce($bins, function ($carry, $item) { 443 | $carry += abs($item); 444 | return $carry; 445 | }); 446 | 447 | if ($sum > 0) { 448 | // Normalize the cell 449 | $bins = array_map(function ($item) use ($sum) { 450 | return $item / $sum; 451 | }, $bins); 452 | } 453 | 454 | foreach ($bins as $binIndex => $intensity) { 455 | $angle = deg2rad($binIndex * $binDegrees); 456 | $white = imagecolorallocate($image, $intensity * 255, $intensity * 255, $intensity * 255); 457 | 458 | $endX = (($x * $length) + $length / 2) + cos($angle) * ($length / 2); 459 | $endY = (($y * $length) + $length / 2) - sin($angle) * ($length / 2); 460 | 461 | imageline($image, (($x * $length) + $length / 2), (($y * $length) + $length / 2), $endX, $endY, $white); 462 | } 463 | } 464 | } 465 | 466 | imagejpeg($image, $imagePath); 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /src/ImageReaders/Pgm.php: -------------------------------------------------------------------------------- 1 | 255) ? 'n*' : 'C*'; 45 | 46 | foreach (unpack($unpackMethod, $line) as $pixel) { 47 | $pixelArray[] = $pixel; 48 | } 49 | } 50 | } 51 | 52 | $image = imagecreatetruecolor($width, $height); 53 | 54 | foreach ($pixelArray as $key => $pixel) { 55 | $color = imagecolorallocate($image, $pixel, $pixel, $pixel); 56 | imagesetpixel($image, $key % $width, floor($key / $width), $color); 57 | } 58 | 59 | fclose($fh); 60 | 61 | return $image; 62 | } 63 | } 64 | --------------------------------------------------------------------------------