How to compare image similarity using php regardless of scale, rotation?

倾然丶 夕夏残阳落幕 提交于 2019-12-09 08:27:41

问题


I want to compare similarity between below images. Acording to my requirements I want to identify all of these images as similar, since it has use the same color, same clip art. The only difference in these images are rotation ,scale and the placement of the clip art. Since all 3 t-shirts has used the same color and clip art I want to identify all 3 images as similar. I tried out the method described in hackerfactor.com. But it doesn't give me correct result acording to my requirements. How to identify all these images as similar?DO you have any suggestions? Please help me.

The below images should be recognized as different from above images.(Even though the tshirts has the same color, clip arts are different. Last tshirt is different from above, because it has use the same clip art, but twice. )


回答1:


Moved to GitHub

Because this question is quite interesting, I moved the whole thing to GitHub where you can find the current implementation: ImageCompare

Original answer

I made a very simple approach, using img-resize and comparing the average color of the resized images.

$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]])));
}

For the used size and color-tolerance I get the expected result:

bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
bool(false)

More advanced implementation

Empty T-Shirt to compare:

$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 \n", $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\n";
foreach($perms as $perm) {
    var_dump(getAvgColor($binEqual[$perm[0]])->compare(getAvgColor($binEqual[$perm[1]])));
}

echo "Different\n";
foreach($perms as $perm) {
    var_dump(getAvgColor($binEqual[$perm[0]])->compare(getAvgColor($binDiff[$perm[1]])));
}

Result:

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)

In this calculation the background is ignored what leads to bigger difference in the avg color.

Final implementation (OOP)

Quite interessting topic. So i tryed to tune it up a liddle bit. This is now a complete OOP implementation. You can now create a new image and subtract some mask of it in order to eliminate a background. Then you can compare one image to another using the compare method. To keep the calculation limited it's better to resize your image first (masks are allways fittet to the current image)

The compare algorythme it self chunks the two images into serveral tiles, then eliminates tiles, that are almost equal to white average color and then compares the average color of all remaining tile-permutations.

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 \n", $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\n";
var_dump(
    $image1->compare($image2),
    $image1->compare($image3),
    $image2->compare($image3)
);

echo "Image 1 to Other\n";
var_dump(
    $image1->compare($other1),
    $image1->compare($other2),
    $image1->compare($other3)
);

echo "Image 2 to Other\n";
var_dump(
    $image2->compare($other1),
    $image2->compare($other2),
    $image2->compare($other3)
);

echo "Image 3 to Other\n";
var_dump(
    $image3->compare($other1),
    $image3->compare($other2),
    $image3->compare($other3)
);

Result:

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)



回答2:


I'm not claiming to really know anything about this topic, which I think generally is termed 'vision'.

What I would do however, is something along these lines:

Flow:

  • Posterise, to minimal number of colors/shades (guess).
  • Remove two largest colors (white + shirt).
  • Compare remaining color-palette, and fail if schemes differ too much.
  • Calculate a coarse polygon around any remaining 'color-blobs' (see https://en.wikipedia.org/wiki/Convex_hull )
  • Compare number of polygons and largest polygon’s number of angles and angle-values (not size), from each image, and fail or pass.

Main problem in such a setup, will be rounding ... as in posterising a color, that is precisely at middelpoint between two colors ... sometimes it gets colorA, sometimes it gets colorB. Same with the polygons, I guess.




回答3:


SIMILAR computes the normalized cross correlation similarity metric between two equal dimensioned images. The normalized cross correlation metric measures how similar two images are, not how different they are.The range of ncc metric values is between 0 (dissimilar) and 1 (similar). If mode=g, then the two images will be converted to grayscale. If mode=rgb, then the two images first will be converted to colorspace=rgb. Next, the ncc similarity metric will be computed for each channel. Finally, they will be combined into an rms value. NOTE: this metric does not work for constant color channels as it produces an ncc metric = 0/0 for that channel. Thus it is not advised to run the script with either image having a totally opaque or totally transparent alpha channel that is enabled.

try this api,

http://www.phpclasses.org/package/8255-PHP-Compare-two-images-to-find-if-they-are-similar.html



回答4:


As someone mentioned, anything else than calculating the histogram of the images and comparing them is not easily achievable. Here is an example that gives the correct result for the images provided in question. The key point here is how to get the right balance between the number of peak color levels and what is acceptable amount of them ( similarity( $histograms, $levels = 30, $enough = 28 ) ).

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
        )

)
*/

This article also helped me to create the histograms.



来源:https://stackoverflow.com/questions/31261456/how-to-compare-image-similarity-using-php-regardless-of-scale-rotation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!