web-dev-qa-db-ja.com

スケール、回転に関係なく、phpを使用して画像の類似性を比較するにはどうすればよいですか?

以下の画像の類似性を比較したいと思います。私の要件によると、同じ色、同じクリップアートを使用しているため、これらすべての画像を類似しているものとして識別したいと思います。これらの画像の唯一の違いは、回転、スケール、およびクリップアートの配置です。 3つのTシャツはすべて同じ色とクリップアートを使用しているので、3つの画像すべてを類似していると識別したいと思います。 hackerfactor.com で説明されている方法を試してみました。しかし、それは私の要件に従って私に正しい結果を与えません。これらすべての画像を類似していると識別する方法はありますか?何か提案はありますか?私を助けてください。

enter image description hereenter image description hereenter image description here

下の画像は上の画像とは異なるものとして認識される必要があります(Tシャツは同じ色ですが、クリップアートは異なります。最後のTシャツは同じクリップを使用しているため、上とは異なります。アートですが、2回です。)

Image AImage BImage C

20
Tharu

GitHubに移動しました

この質問は非常に興味深いので、現在の実装を見つけることができるGitHubにすべてを移動しました: ImageCompare

元の回答

Img-resizeを使用し、サイズ変更された画像の平均色を比較するという、非常に単純なアプローチを作成しました。

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

使用したサイズと色の許容範囲については、期待どおりの結果が得られます。

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

より高度な実装

比較する空のTシャツ: Plain T-Shirt

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

結果:

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つの画像をサーバータイルに自己チャンクし、白の平均色にほぼ等しいタイルを削除してから、残りのすべてのタイル順列の平均色を比較します。

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)
);

結果:

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)
15
Inceddy

私はこのトピックについて本当に何も知っているとは主張していません。これは一般的に「ビジョン」と呼ばれていると思います。

しかし、私がすることはこれらの線に沿った何かです:

フロー:

  • Posterise、最小数の色/色合い(推測)。
  • 削除2つの最大の色(白+シャツ)。
  • 残りのカラーパレットを比較し、スキームの違いが大きすぎると失敗します。
  • Calculate残りの「color-blobs」の周りの粗いポリゴン( https: //en.wikipedia.org/wiki/Convex_hull
  • 比較ポリゴンの数と最大ポリゴンの角度の数と角度値(サイズではない)、から各画像、および失敗または合格。

このような設定での主な問題は、丸めです...色をポスタライズする場合のように、正確には2つの色の中間にあります...時々colorAを取得し、時にはcolorBを取得します。ポリゴンも同じだと思います。

1
Ole Sauffaus

SIMILARは、2つの等しい次元の画像間の正規化された相互相関類似性メトリックを計算します。正規化された相互相関メトリックは、2つの画像の類似性を測定しますが、それらの違いは測定しません.nccメトリック値の範囲は、0(非類似)から1(類似)の間です。 mode = gの場合、2つの画像はグレースケールに変換されます。 mode = rgbの場合、最初に2つの画像がcolorspace = rgbに変換されます。次に、チャネルごとにncc類似性メトリックが計算されます。最後に、それらはrms値に結合されます。注:このメトリックは、そのチャネルに対してnccメトリック= 0/0を生成するため、一定のカラーチャネルでは機能しません。したがって、完全に不透明または完全に透明なアルファチャネルが有効になっている画像を使用してスクリプトを実行することはお勧めしません。

このAPIを試してください

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

誰かが言ったように、画像のヒストグラムを計算して比較する以外のことは簡単には達成できません。これは、問題の画像に対して正しい結果を与える例です。ここで重要なのは、ピークカラーレベルの数と許容可能な量のバランスを適切にとる方法です(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
        )

)
*/

この記事 ヒストグラムの作成にも役立ちました。

0
Danijel