How to compare image similarity using php regardless of scale, rotation?
我想比较以下图像之间的相似性。 根据我的要求,我想将所有这些图像标识为相似的,因为它使用相同的颜色,相同的剪贴画。 这些图像的唯一区别是旋转,缩放和剪贴画的位置。 由于所有3件T恤都使用相同的颜色和剪贴画,因此我想将所有3张图像标识为相似。 我尝试了hackerfactor.com中描述的方法。 但这并不能根据我的要求给我正确的结果。 如何将所有这些图像识别为相似图像?您有什么建议吗? 请帮我。
下面的图像应被识别为与上面的图像不同。(即使T恤具有相同的颜色,剪贴画也有所不同。最后一件T恤与上面的有所不同,因为它使用的是相同的剪贴画,但是两次。)
移至GitHub
因为这个问题很有趣,所以将整个内容移到了GitHub,您可以在其中找到当前的实现:
图像比较
原始答案
我做了一个非常简单的方法,使用img-resize并比较了调整大小后的图像的平均颜色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | $binEqual = [ file_get_contents('http://i.stack.imgur.com/D8ct1.png'), file_get_contents('http://i.stack.imgur.com/xNZt1.png'), file_get_contents('http://i.stack.imgur.com/kjGjm.png') ]; $binDiff = [ file_get_contents('http://i.stack.imgur.com/WIOHs.png'), file_get_contents('http://i.stack.imgur.com/ljoBT.png'), file_get_contents('http://i.stack.imgur.com/qEKSK.png') ]; function getAvgColor($bin, $size = 10) { $target = imagecreatetruecolor($size, $size); $source = imagecreatefromstring($bin); imagecopyresized($target, $source, 0, 0, 0, 0, $size, $size, imagesx($source), imagesy($source)); $r = $g = $b = 0; foreach(range(0, $size - 1) as $x) { foreach(range(0, $size - 1) as $y) { $rgb = imagecolorat($target, $x, $y); $r += $rgb >> 16; $g += $rgb >> 8 & 255; $b += $rgb & 255; } } unset($source, $target); return (floor($r / $size ** 2) << 16) + (floor($g / $size ** 2) << 8) + floor($b / $size ** 2); } function compAvgColor($c1, $c2, $tolerance = 4) { return abs(($c1 >> 16) - ($c2 >> 16)) <= $tolerance && abs(($c1 >> 8 & 255) - ($c2 >> 8 & 255)) <= $tolerance && abs(($c1 & 255) - ($c2 & 255)) <= $tolerance; } $perms = [[0,1],[0,2],[1,2]]; foreach($perms as $perm) { var_dump(compAvgColor(getAvgColor($binEqual[$perm[0]]), getAvgColor($binEqual[$perm[1]]))); } foreach($perms as $perm) { var_dump(compAvgColor(getAvgColor($binDiff[$perm[0]]), getAvgColor($binDiff[$perm[1]]))); } |
对于使用的尺寸和颜色公差,我得到了预期的结果:
1 2 3 4 5 6 | bool(true) bool(true) bool(true) bool(false) bool(false) bool(false) |
更高级的实施
空T恤进行比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | $binEqual = [ file_get_contents('http://i.stack.imgur.com/D8ct1.png'), file_get_contents('http://i.stack.imgur.com/xNZt1.png'), file_get_contents('http://i.stack.imgur.com/kjGjm.png') ]; $binDiff = [ file_get_contents('http://i.stack.imgur.com/WIOHs.png'), file_get_contents('http://i.stack.imgur.com/ljoBT.png'), file_get_contents('http://i.stack.imgur.com/qEKSK.png') ]; class Color { private $r = 0; private $g = 0; private $b = 0; public function __construct($r = 0, $g = 0, $b = 0) { $this->r = $r; $this->g = $g; $this->b = $b; } public function r() { return $this->r; } public function g() { return $this->g; } public function b() { return $this->b; } public function toInt() { return $this->r << 16 + $this->g << 8 + $this->b; } public function toRgb() { return [$this->r, $this->g, $this->b]; } public function mix(Color $color) { $this->r = round($this->r + $color->r() / 2); $this->g = round($this->g + $color->g() / 2); $this->b = round($this->b + $color->b() / 2); } public function compare(Color $color, $tolerance = 500) { list($r1, $g1, $b1) = $this->toRgb(); list($r2, $g2, $b2) = $color->toRgb(); $diff = round(sqrt(pow($r1 - $r2, 2) + pow($g1 - $g2, 2) + pow($b1 - $b2, 2))); printf("Comp r(%s : %s), g(%s : %s), b(%s : %s) Diff %s ", $r1, $r2, $g1, $g2, $b1, $b2, $diff); return $diff <= $tolerance; } public static function fromInt($int) { return new self($int >> 16, $int >> 8 & 255, $int & 255); } } function getAvgColor($bin, $size = 5) { $target = imagecreatetruecolor($size, $size); $targetTmp = imagecreatetruecolor($size, $size); $sourceTmp = imagecreatefrompng('http://i.stack.imgur.com/gfn5A.png'); $source = imagecreatefromstring($bin); imagecopyresized($target, $source, 0, 0, 0, 0, $size, $size, imagesx($source), imagesy($source)); imagecopyresized($targetTmp, $sourceTmp, 0, 0, 0, 0, $size, $size, imagesx($source), imagesy($source)); $r = $g = $b = $relPx = 0; $baseColor = new Color(); foreach(range(0, $size - 1) as $x) { foreach(range(0, $size - 1) as $y) { if (imagecolorat($target, $x, $y) != imagecolorat($targetTmp, $x, $y)) $baseColor->mix(Color::fromInt(imagecolorat($target, $x, $y))); } } unset($source, $target, $sourceTmp, $targetTmp); return $baseColor; } $perms = [[0,0], [1,0], [2,0], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]]; echo"Equal "; foreach($perms as $perm) { var_dump(getAvgColor($binEqual[$perm[0]])->compare(getAvgColor($binEqual[$perm[1]]))); } echo"Different "; foreach($perms as $perm) { var_dump(getAvgColor($binEqual[$perm[0]])->compare(getAvgColor($binDiff[$perm[1]]))); } |
结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | Equal Comp r(101 : 101), g(46 : 46), b(106 : 106) Diff 0 bool(true) Comp r(121 : 101), g(173 : 46), b(249 : 106) Diff 192 bool(true) Comp r(219 : 101), g(179 : 46), b(268 : 106) Diff 241 bool(true) Comp r(121 : 101), g(173 : 46), b(249 : 106) Diff 192 bool(true) Comp r(121 : 121), g(173 : 173), b(249 : 249) Diff 0 bool(true) Comp r(121 : 219), g(173 : 179), b(249 : 268) Diff 100 bool(true) Comp r(219 : 101), g(179 : 46), b(268 : 106) Diff 241 bool(true) Comp r(219 : 121), g(179 : 173), b(268 : 249) Diff 100 bool(true) Comp r(219 : 219), g(179 : 179), b(268 : 268) Diff 0 bool(true) Different Comp r(101 : 446), g(46 : 865), b(106 : 1242) Diff 1442 bool(false) Comp r(121 : 446), g(173 : 865), b(249 : 1242) Diff 1253 bool(false) Comp r(219 : 446), g(179 : 865), b(268 : 1242) Diff 1213 bool(false) Comp r(121 : 446), g(173 : 865), b(249 : 1242) Diff 1253 bool(false) Comp r(121 : 654), g(173 : 768), b(249 : 1180) Diff 1227 bool(false) Comp r(121 : 708), g(173 : 748), b(249 : 1059) Diff 1154 bool(false) Comp r(219 : 446), g(179 : 865), b(268 : 1242) Diff 1213 bool(false) Comp r(219 : 654), g(179 : 768), b(268 : 1180) Diff 1170 bool(false) Comp r(219 : 708), g(179 : 748), b(268 : 1059) Diff 1090 bool(false) |
在此计算中,背景被忽略,导致平均颜色差异更大。
最终实施(OOP)
很有意思的话题。因此,我尝试将其调优。
现在这是一个完整的OOP实现。现在,您可以创建一个新图像并减去一些蒙版以消除背景。然后,您可以使用compare方法将一个图像与另一个图像进行比较。为了限制计算,最好先调整图像大小(遮罩始终适合当前图像)
比较算法将自己将两个图像分块为多个服务器磁贴,然后消除几乎等于白色平均颜色的磁贴,然后比较所有剩余磁贴排列的平均颜色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | Class Image { const HASH_SIZE = 8; const AVG_SIZE = 10; private $img = null; public function __construct($resource) { $this->img = $resource;; } private function permute(array $a1, array $a2) { $perms = array(); for($i = 0; $i < sizeof($a1); $i++) { for($j = $i; $j < sizeof($a2); $j++) { if ($i != $j) { $perms[] = [$a1[$i], $a2[$j]]; } } } return $perms; } public function compare(Image $comp) { $avgComp = array(); foreach($comp->chunk(25) as $chunk) { $avgComp[] = $chunk->avg(); } $avgOrg = array(); foreach($this->chunk(25) as $chunk) { $avgOrg[] = $chunk->avg(); } $white = Color::fromInt(0xFFFFFF); $avgComp = array_values(array_filter($avgComp, function(Color $color) use ($white){ return $white->compare($color, 1000); })); $avgOrg = array_values(array_filter($avgOrg, function(Color $color) use ($white){ return $white->compare($color, 1000); })); $equal = 0; $pairs = $this->permute($avgOrg, $avgComp); foreach($pairs as $pair) { $equal += $pair[0]->compare($pair[1], 100) ? 1 : 0; } return ($equal / sizeof($pairs)); } public function substract(Image $mask, $tolerance = 50) { $size = $this->size(); if ($mask->size() != $size) { $mask = $mask->resize($size); } for ($x = 0; $x < $size[0]; $x++) { for ($y = 0; $y < $size[1]; $y++) { if ($this->colorat($x, $y)->compare($mask->colorat($x, $y), $tolerance)) imagesetpixel($this->img, $x, $y, 0xFFFFFF); } } return $this; } public function avg($size = 10) { $target = $this->resize([self::AVG_SIZE, self::AVG_SIZE]); $avg = Color::fromInt(0x000000); $white = Color::fromInt(0xFFFFFF); for ($x = 0; $x < self::AVG_SIZE; $x++) { for ($y = 0; $y < self::AVG_SIZE; $y++) { $color = $target->colorat($x, $y); if (!$color->compare($white, 10)) $avg->mix($color); } } return $avg; } public function colorat($x, $y) { return Color::fromInt(imagecolorat($this->img, $x, $y)); } public function chunk($chunkSize = 10) { $collection = new ImageCollection(); $size = $this->size(); for($x = 0; $x < $size[0]; $x += $chunkSize) { for($y = 0; $y < $size[1]; $y += $chunkSize) { switch (true) { case ($x + $chunkSize > $size[0] && $y + $chunkSize > $size[1]): $collection->push($this->slice(['x' => $x, 'y' => $y, 'height' => $size[0] - $x, 'width' => $size[1] - $y])); break; case ($x + $chunkSize > $size[0]): $collection->push($this->slice(['x' => $x, 'y' => $y, 'height' => $size[0] - $x, 'width' => $chunkSize])); break; case ($y + $chunkSize > $size[1]): $collection->push($this->slice(['x' => $x, 'y' => $y, 'height' => $chunkSize, 'width' => $size[1] - $y])); break; default: $collection->push($this->slice(['x' => $x, 'y' => $y, 'height' => $chunkSize, 'width' => $chunkSize])); break; } } } return $collection; } public function slice(array $rect) { return Image::fromResource(imagecrop($this->img, $rect)); } public function size() { return [imagesx($this->img), imagesy($this->img)]; } public function resize(array $size = array(100, 100)) { $target = imagecreatetruecolor($size[0], $size[1]); imagecopyresized($target, $this->img, 0, 0, 0, 0, $size[0], $size[1], imagesx($this->img), imagesy($this->img)); return Image::fromResource($target); } public function show() { header("Content-type: image/png"); imagepng($this->img); die(); } public function save($name = null, $path = '') { if ($name === null) { $name = $this->hash(); } imagepng($this->img, $path . $name . '.png'); return $this; } public function hash() { // Resize the image. $resized = imagecreatetruecolor(self::HASH_SIZE, self::HASH_SIZE); imagecopyresampled($resized, $this->img, 0, 0, 0, 0, self::HASH_SIZE, self::HASH_SIZE, imagesx($this->img), imagesy($this->img)); // Create an array of greyscale pixel values. $pixels = []; for ($y = 0; $y < self::HASH_SIZE; $y++) { for ($x = 0; $x < self::HASH_SIZE; $x++) { $rgb = imagecolorsforindex($resized, imagecolorat($resized, $x, $y)); $pixels[] = floor(($rgb['red'] + $rgb['green'] + $rgb['blue']) / 3); } } // Free up memory. imagedestroy($resized); // Get the average pixel value. $average = floor(array_sum($pixels) / count($pixels)); // Each hash bit is set based on whether the current pixels value is above or below the average. $hash = 0; $one = 1; foreach ($pixels as $pixel) { if ($pixel > $average) $hash |= $one; $one = $one << 1; } return $hash; } public static function fromResource($resource) { return new self($resource); } public static function fromBin($binf) { return new self(imagecreatefromstring($bin)); } public static function fromFile($path) { return new self(imagecreatefromstring(file_get_contents($path))); } } class ImageCollection implements IteratorAggregate { private $images = array(); public function __construct(array $images = array()) { $this->images = $images; } public function push(Image $image) { $this->images[] = $image; return $this; } public function pop() { return array_pop($this->images); } public function save() { foreach($this->images as $image) { $image->save(); } return $this; } public function getIterator() { return new ArrayIterator($this->images); } } class Color { private $r = 0; private $g = 0; private $b = 0; public function __construct($r = 0, $g = 0, $b = 0) { $this->r = $r; $this->g = $g; $this->b = $b; } public function r() { return $this->r; } public function g() { return $this->g; } public function b() { return $this->b; } public function toInt() { return $this->r << 16 + $this->g << 8 + $this->b; } public function toRgb() { return [$this->r, $this->g, $this->b]; } public function mix(Color $color) { $this->r = round($this->r + $color->r() / 2); $this->g = round($this->g + $color->g() / 2); $this->b = round($this->b + $color->b() / 2); } public function compare(Color $color, $tolerance = 500) { list($r1, $g1, $b1) = $this->toRgb(); list($r2, $g2, $b2) = $color->toRgb(); $diff = round(sqrt(pow($r1 - $r2, 2) + pow($g1 - $g2, 2) + pow($b1 - $b2, 2))); //printf("Comp r(%s : %s), g(%s : %s), b(%s : %s) Diff %s ", $r1, $r2, $g1, $g2, $b1, $b2, $diff); return $diff <= $tolerance; } public static function fromInt($int) { return new self($int >> 16, $int >> 8 & 255, $int & 255); } } $mask = Image::fromFile('http://i.stack.imgur.com/gfn5A.png'); $image1 = Image::fromFile('http://i.stack.imgur.com/D8ct1.png')->resize([50, 100])->substract($mask, 100); $image2 = Image::fromFile('http://i.stack.imgur.com/xNZt1.png')->resize([50, 100])->substract($mask, 100); $image3 = Image::fromFile('http://i.stack.imgur.com/kjGjm.png')->resize([50, 100])->substract($mask, 100); $other1 = Image::fromFile('http://i.stack.imgur.com/WIOHs.png')->resize([50, 100])->substract($mask, 100); $other2 = Image::fromFile('http://i.stack.imgur.com/ljoBT.png')->resize([50, 100])->substract($mask, 100); $other3 = Image::fromFile('http://i.stack.imgur.com/qEKSK.png')->resize([50, 100])->substract($mask, 100); echo"Equal "; var_dump( $image1->compare($image2), $image1->compare($image3), $image2->compare($image3) ); echo"Image 1 to Other "; var_dump( $image1->compare($other1), $image1->compare($other2), $image1->compare($other3) ); echo"Image 2 to Other "; var_dump( $image2->compare($other1), $image2->compare($other2), $image2->compare($other3) ); echo"Image 3 to Other "; var_dump( $image3->compare($other1), $image3->compare($other2), $image3->compare($other3) ); |
结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Equal float(0.47619047619048) float(0.53333333333333) float(0.4) Image 1 to Other int(0) int(0) int(0) Image 2 to Other int(0) int(0) int(0) Image 3 to Other int(0) int(0) int(0) |
SIMILAR计算两个等尺寸图像之间的归一化互相关相似度。归一化互相关度量标准衡量的是两幅图像的相似度,而不是它们之间的差异.ncc度量标准值的范围介于0(不相似)和1(相似)之间。如果mode = g,则这两个图像将转换为灰度。如果mode = rgb,则首先将两个图像转换为colorspace = rgb。接下来,将为每个通道计算ncc相似性度量。最后,它们将被组合成一个均方根值。注意:此度量标准不适用于恒定颜色通道,因为它为该通道产生ncc度量标准= 0/0。因此,建议不要使用启用了完全不透明或完全透明的Alpha通道的图像来运行脚本。
试试这个api,
1 | http://www.phpclasses.org/package/8255-PHP-Compare-two-images-to-find-if-they-are-similar.html |
我并不是说对这个话题一无所知,我一般将其称为"视觉"。
但是,我要做的是遵循以下原则:
流:
- 后验,最小化颜色/阴影(猜测)。
- 删除两种最大的颜色(白色+衬衫)。
- 比较剩余的调色板,如果方案相差太大,则失败。
- 计算任何剩余的"颜色斑点"周围的粗多边形(请参阅https://en.wikipedia.org/wiki/Convex_hull)
- 比较每张图片的多边形数量和最大多边形的角度数量和角度值(不是大小),然后判断是否合格。
这种设置的主要问题是四舍五入……就像在绘制颜色时一样,恰好是两种颜色之间的中间点……有时它变为colorA,有时它变为colorB。
我猜多边形也一样。
正如有人提到的那样,除了计算图像的直方图并进行比较之外,其他任何事情都不容易实现。这是一个可以为所提供图像提供正确结果的示例。此处的关键是如何在峰值颜色级别的数量和可接受的峰值数量(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | function histograms( $images ) { foreach( $images as $img ) { $image = imagecreatefrompng( $img ); $width = imagesx( $image ); $height = imagesy( $image ); $num_pixels = $width * $height; $histogram = []; for ( $x = 0; $x < $width; $x++ ) { for ( $y = 0; $y < $height; $y++ ) { $rgb = imagecolorat( $image, $y, $x ); $rgb = [ $rgb >> 16, ( $rgb >> 8 ) & 0xFF, $rgb & 0xFF ]; $histo_v = (int) round( ( $rgb[0] + $rgb[1] + $rgb[02] ) / 3 ); $histogram[ $histo_v ] = array_key_exists( $histo_v, $histogram ) ? $histogram[ $histo_v ] + $histo_v/$num_pixels : $histo_v/$num_pixels; } } $histograms[$img] = $histogram; arsort( $histograms[$img] ); } return $histograms; } function similarity( $histograms, $levels = 30, $enough = 28 ) { $keys = array_keys( $histograms ); $output = []; for ( $x = 0; $x < count( $histograms ) - 1; $x++ ) { for ( $y = $x + 1; $y < count( $histograms ); $y++ ) { $similarity = count( array_intersect_key( array_slice( $histograms[ $keys[$x] ], 0, $levels, true ), array_slice( $histograms[ $keys[$y] ], 0, $levels, true ) ) ); if ( $similarity > $enough ) $output[] = [ $keys[$x], $keys[$y], $similarity ]; } } return $output; } $histograms = histograms( [ 'http://i.stack.imgur.com/D8ct1.png', 'http://i.stack.imgur.com/xNZt1.png', 'http://i.stack.imgur.com/kjGjm.png', 'http://i.stack.imgur.com/WIOHs.png', 'http://i.stack.imgur.com/ljoBT.png', 'http://i.stack.imgur.com/qEKSK.png' ] ); $similarity = similarity( $histograms ); print_r( $similarity ); /* Array ( [0] => Array ( [0] => http://i.stack.imgur.com/D8ct1.png [1] => http://i.stack.imgur.com/xNZt1.png [2] => 30 ) [1] => Array ( [0] => http://i.stack.imgur.com/D8ct1.png [1] => http://i.stack.imgur.com/kjGjm.png [2] => 30 ) [2] => Array ( [0] => http://i.stack.imgur.com/D8ct1.png [1] => http://i.stack.imgur.com/qEKSK.png [2] => 29 ) [3] => Array ( [0] => http://i.stack.imgur.com/xNZt1.png [1] => http://i.stack.imgur.com/kjGjm.png [2] => 30 ) [4] => Array ( [0] => http://i.stack.imgur.com/xNZt1.png [1] => http://i.stack.imgur.com/qEKSK.png [2] => 29 ) [5] => Array ( [0] => http://i.stack.imgur.com/kjGjm.png [1] => http://i.stack.imgur.com/qEKSK.png [2] => 29 ) ) */ |
本文还帮助我创建了直方图。