Поиск похожих фотографий

поиск дубликатов при загрузке фотографий в Буга Архивариус

При загрузке архива в Буга Архивариус  задумал создать опцию  отслеживания  похожих фотографий. Так бывает, на моем фото часто сбивается настройка переключателя и он начинает щелкать сериями вместо одно снимка получается сразу 5-6 фото.  Ничего страшного но захотелось мне сделать инструмент проверки таких похожих фото.  Просмотрев сеть на глаза попалось статья «Выглядит похоже»   там предлагается сжать картинку до размера 8x8  и  создать из нее битовую маску ну типа такого 0101110101000111010101010101110100111111011110101010110110101010.   Такая разность называется расстоянием Хемминга.  

В качестве реализации нашел на просторах gitHab phash  готовый класс вот его запустил на тестирование массива фотографий. Задача была найти очень похожие фотографии они должны быть практически идентичные . Но на практике получился большой облом  привожу пример 3 фотографий которые давали расстояние Хемминга равное 0.  

На этот момент в архиве было загружено всего около 3 тысяч фотографий.  Далее ложные срабатывание пошли еще чаще, пришлось все удалять и переделывать код создание для pHash.   

И первое, что приходи в голову это увеличить размер матрицы хранить не 8х8  а в 2 раза больше 16x16 .   Со временем выяснилось, что просто сжимать в квадрат нельзя, надо сначала обрезать картинку квадратом  и потом уже сжимать до 16х16, иначе происходит смещение матрицы и пропорционально сжатые картинки не находит как похожие.  

Второе, что я выяснил методом втыка -  лучше брать не сумму цветов из пикселя, а квадрат разности всех 3 цветов. Если брать сумму цветов то мы приводим картинку к черно- белому изображению, и тогда если для фото изменили баланс цвета оно будет также считаться похожим с оригиналом, это не совсем то что надо. 

 

Общи алгоритм примерно такой:

  • Обрезаем картинку берем квадрат по центру
  • Сжимаем картинку до указанного размера
  • запускаем цикл и проходим по пикселям
  • берем в точке цвет, получаем 3 составляющих красный = 123, зеленый = 23 и синий  = 233 
  • вычисляем итоговое значение Zxy = ( красный-зеленый - синий) 2     полученное число заносим в массив 
  • записываем значения для всех точек в массив
  • вычисляем среднее average   для всего массива
  • проходим в цикле по массиву сравниваем каждое Zxy   со средним значение если оно больше то в битовую маску пишем 1 если меньше пишем 0
  • получаем битовую маску 0101010101010101010101011111111
  • далее битовую маску можно получить в виде x16 кода, битовой строки или массива

Еще один момент на который хочу обратить  внимание - в MySQl максимальный размер BIT поля = 64  бита, а это соответствует матрице 8x8, что нам категорически не подходит. Для того, чтобы увеличить длину  хранимой маски  в 4 раза  я сделал в таблицы MySQL 4 поля по 64 бит.   Выкладываю свой  класс где  возможность задавать размер матрицы до которой сжимается оригинальное фото.  Для  работы требуется библиотека  imagick.

<?php
/*
Stur Image Phash code class
*/

class ImgBitHash{

    var $arrHex = [
        '0000' => '0', '0001' => '1', '0010' => '2', '0011' => '3',
        '0100' => '4', '0101' => '5', '0110' => '6', '0111' => '7',
        '1000' => '8', '1001' => '9', '1010' => 'a', '1011' => 'b',
        '1100' => 'c', '1101' => 'd', '1110' => 'e', '1111' => 'f',
    ];
/*
$path - путь к картинке
$scale - размер по квадрата

$type   - как вернуть результат hex- строка, bit - строка, array - массив из  0 и 1
*/
    function getHash( $path, $scale = 16,  $type='hex' )
    {
        try{
           if( !$im = new imagick( $path ) ) return false;
        } catch (Exception $e){
            return false;
        }

        // обрезаем квадрат
        $imageprops = $im->getImageGeometry();
        $width = $imageprops['width'];
        $height = $imageprops['height'];
        if($height != $width){  // если img не квадратный надо обрезать в квадрат
            $x = $y = 0;
            if ($width > $height) {
                $x = ceil(($width - $height) / 2);
                $width = $height;
            } elseif ($height > $width) {
                    $y = ceil(($height - $width) / 2);
                    $height = $width;
            }
            $im->cropImage ( $width , $height ,  $x , $y );
        }

        // сжимаем изображение
        $im->resizeImage($scale, $scale, imagick::FILTER_TRIANGLE, 1);

        // проходим по матрице получаем цвет пикселей
        for ($x = 0; $x < $scale; $x++) {
            for ($y = 0; $y < $scale; $y++) {
                $pix = $im->getImagePixelColor($x, $y);
                $colors =  $pix->getColor();
                /*echo '<pre>'; print_r($colors); echo '</pre>';
                return ;*/
                // если цвета равны значит это черно-белоя картинка берем любой цвет
                if( $colors['r'] == $colors['g'] AND $colors['r'] == $colors['b'] )
                    $matrix[] = $colors['r'];
                else {// вычисляем сумму разностей цветов так лучше всего !!!!
                    $matrix[] =pow( ($colors['r'] - $colors['g']  - $colors['b']),  2 );

                }
            }
        }
        $im->destroy();
        //echo '<pre>'; print_r($matrix); echo '</pre>';
        // вычисляем среднее значение
        $average =  array_sum($matrix) / sizeof($matrix);
        // проходим по массиву создаем битовую маску
        foreach ($matrix as $val) {
            if( $val > $average )
                $bit[] = 1;
            else
                $bit[] = 0;
        }
        $stBit = implode('', $bit);

        // вернем результат как заказывали
        if($type == 'hex'){
            $arr = str_split($stBit, 4);
            foreach ($arr as $key => $value) {
                $hex .= $this->arrHex[$value];
            }
            return $hex;
        }elseif($type == 'bit'){
            return $stBit;
        }elseif($type == 'array'){
            return $bit;
        }
    }
/*
конвертирует из строки в hex представлении в строку бит 0 и 1
*/
    static function convertHexToBit( $hash ){
        $hash = str_split($hash, 4);
        foreach ($hash as $key => $value) {
            $b =  base_convert ($value, 16, 2);
            $arr[] = str_pad($b, 16, '0', STR_PAD_LEFT );
        }
        return implode('', $arr);
    }

/*
сравнивает 2 строки в hex формате и
выдает количество не совпаших бит
*/
    static function compareHex($h1, $h2){
        $a1 = str_split( self::convertHexToBit($h1) );
        $a2 = str_split( self::convertHexToBit($h2) );
        $dif = 0;
        foreach ($a1 as $key => $b1) {
            $b2 = $a2[$key];
            if($b1 != $b2) $dif++;
        }
        return $dif;
    }
}