Поиск похожих фотографий
поиск дубликатов при загрузке фотографий в Буга Архивариус
При загрузке архива в Буга Архивариус задумал создать опцию отслеживания похожих фотографий. Так бывает, на моем фото часто сбивается настройка переключателя и он начинает щелкать сериями вместо одно снимка получается сразу 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;
}
}