В любом проекте человеческий фактор никто не отменял, и если пользователи самостоятельно грузят картинки на сайт – появления дубликатов не избежать. Когда доходит до тысяч файлов, глазами всего не пересмотреть, а повторяющиеся картинки мало того, что никому не нужны, так еще и занимают место, тратят ресурс и в конце концов тормозят работу.



Потому рано или поздно встает вопрос автоматизации процесса поиска повторов, и тут мы рассмотрим основные, а также попробуем в деле.

Сравнение файлов через функцию hash


Одним из способов определения дубликатов является сравнение файлов путем генерации хеш-значения из содержимого заданного файла.

Простой пример вычисления хеша изображения:

<?php
imagecreatefrompng('image.png');
echo hash_file('md5', 'image.png');
?>

Результат выглядит примерно так: bff8b4bc8b5c1c1d5b3211dfb21d1e76

Если хеши двух изображений совпадают – изображения одинаковые.
Метод далеко не самый точный, так как работает только для идентичных картинок, при малейшем различии — толку ноль.



ImageMagick


Функция обработки изображений Imagick::compareImages возвращает массив, который содержит восстановленное изображение и разницу между изображениями.

Пример использования при сравнении двух изображений:

<?php
header("Content-Type: image/png");
$image1 = new imagick("image1.png");
$image2 = new imagick("image2.png");
$result = $image1->compareImages($image2, Imagick::METRIC_MEANSQUAREERROR);
$result[0]->setImageFormat("png");

echo $result[0];
?>

В итоге две сравниваемые картинки лепятся в одну, на которой видны отличия.
Также можно получить числовое выражение отличий по каждому параметру (пример с оф.сайта):

-> compare -verbose -metric mae rose.jpg reconstruct.jpg difference.png
Image: rose.jpg
 Channel distortion: MAE
  red: 2282.91 (0.034835)
  green: 1853.99 (0.0282901)
  blue: 2008.67 (0.0306503)
  all: 1536.39 (0.0234439)


gd2 и libpuzzle


Для быстрого поиска дубликатов необходимо установить библиотеки gd2 и libpuzzle.

Установка:

apt-get install libpuzzle-php php5-gd

Libpuzzle создана для быстрого поиска визуального сходства изображений (GIF, PNG, JPEG). Сначала растровая картинка разбивается на блоки — автоматически отбрасываются рамки, не несущие особо значимой информации. Разница между смежными блоками формирует вектор — это так называемая подпись картинки. Похожесть картинок определяется расстоянием между двумя такими векторами. Потому обычно изменение цвета, ресайз или сжатие не влияют на результаты, выдаваемые libpuzzle.



Libpuzzle довольно проста в использовании. Вычисление подписи для двух изображений:

$cvec1 = puzzle_fill_cvec_from_file('img1.jpg');
$cvec2 = puzzle_fill_cvec_from_file('img2.jpg');

Вычисление расстояния между подписями:

$d = puzzle_vector_normalized_distance($cvec1, $cvec2);

Проверка изображений на схожесть:

if ($d < PUZZLE_CVEC_SIMILARITY_LOWER_THRESHOLD) {
  echo "Pictures are looking similar\n";
} else {
  echo "Pictures are different, distance=$d\n";
}

Сжатие подписей для хранения в базе данных:

$compress_cvec1 = puzzle_compress_cvec($cvec1);
$compress_cvec2 = puzzle_compress_cvec($cvec2);


Перцептивный хеш


Вероятнее всего, самый точный способ нахождения дубликатов — сравнение файлов через перцептивный хеш. Проверка на схожесть проводится путем подсчета количества отличающихся позиций между двумя хешами, это расстояние Хэмминга. Чем расстояние меньше — тем больше совпадение.



Отличается от первого способа тем, что указывает не только на одинаковость/неодинаковость, но и на степень различия. Подробнее об этом принципе можно прочитать в неплохом переводе.

Установка для UNIX платформ выглядит так:

$ ./phpize
$ ./configure [--with-pHash=...] 
$ make
$ make test
$ [sudo] make install


Попробовать на деле можно через i.onthe.io/phash. Загрузка изображений через интерфейс и на выходе показатель «одинаковости».

Как это работает


Получаем хеш первого изображения:

$phash1 = ph_dct_imagehash($file1);

Получаем хеш второго изображения:

$phash2 = ph_dct_imagehash($file2);

Получаем расстояние Хэмминга между двумя изображениями:

$dist = ph_image_dist($phash1,$phash2);


Мы проделали почти все возможные манипуляции с одной и той же фотографией, чтобы проверить — какие изменения мешают определять дубликаты через pHash, а какие — нет.

Например, при зеркальном отражении — картинка остается неузнанной.
Зато с цветами можно играться сколько угодно — на результат сравнения это не повлияет.
Чего нельзя сказать о манипуляциях с RGB-каналами, Джона опять не узнали, хоть и расстояние Хэмминга для такого случая гораздо меньше.

Остальные результаты выглядят так:
Не мешают (расстояние Хэмминга = 0) Мешают (расстояние Хэмминга — в скобках)
Измененное имя файла Кроп (34)*
Формат (JPEG, PNG, GIF) Поворот 90° (32)**
Оптимизация Google PageSpeed Зеркальное отражение (36)
Ресайз с сохранением пропорций и без Изменение положения кривых в RGB-каналах (18)
Изменение цветовой гаммы и четкости

*зависит от величины кропнутой области. При отрезании от картинки маленькой рамки толщиной в несколько пикселей, расстояние Хэмминга будет нулевым, следовательно сходство — 100%. Но чем ощутимее кроп — тем больше расстояние — тем меньше шансов обнаружить дубликат. О поиске кропнутых дубликатов через перцептивные хеши можно почитать тут.

**то же самое, что и с кропом. При повороте на пару градусов расстояние незначительное, но чем больше угол наклона — тем сильнее различие.

Конспект


  1. Для сравнения картинок используйте ImageMagick, а для поиска полностью идентичных — сравнение через хеш.
  2. Чтобы находить незначительно измененные изображения — используйте библиотеку libpuzzle.
  3. Сравнение через перцептивный хеш — одно из самых надежных, можно попробовать тут.

Комментарии (21)


  1. gto
    05.06.2015 17:12

    Получается, каждый раз картинку сравнивать со всеми уже имеющимися на сайте? (кроме вариантов с хешем)


    1. nikita2206
      05.06.2015 17:42

      Вот здесь хорошее объяснение как использовать либпазл для индексирования изображений stackoverflow.com/a/9780314/1461092


      1. gto
        05.06.2015 20:39

        Реляционную БД для такой задачи, наверное, не очень правильно использовать, а вот с чем-нибудь наподобие neo4j может интересно получиться.


        1. Komzpa
          06.06.2015 17:40

          У постгреса есть хорошие расширения на тему поиска похожих хешей:
          zdk.github.io/detecting-similar-images-with-phash-and-pg_similarity


  1. stychos
    05.06.2015 17:24
    +1

    Как-то раз уже реализовывал сравнение по Хэммингу, как раз на php, весьма интересная область, с большим пространством для манёвров.


  1. mephistopheies
    05.06.2015 18:09
    -12

    и это во времена дип лернинга


    1. Lisio
      06.06.2015 03:28
      +12

      и очень модного инглиш юзинга


      1. mephistopheies
        07.06.2015 14:31
        -8

        ну лучше уж инглишь юзинг чем пхп юзинг


        1. Acuna
          10.06.2015 22:49

          Не минусовал, однако с Вами не соглашусь. PHP весьма и весьма неплохой язык, с неплохим синтаксисом и логикой, решающий достаточно широкий круг задач, и оставался бы весьма неплохим языком, если бы не школота, клепающая на нем ГС для своих игровых кланов, постоянно портящая ему репутацию. И все это «PHP — говно» заключается как правило в незнании области его применения. Он не хорош и не плох, он просто для другого. Он отлично подходит для веба, однако не так хорош для десктопа (возможно и такое, да). Для каждой задачи хороши свои инструменты, и если какую-то задачу нельзя решить каким,-либо инструментом — это не значит, что этот инструмент плох, просто он используется для неподходящей задачи. А быть может плох пользователь этого инструмента, пытающийся забить гвоздь унитазом, и сетующий на то, что унитаз — это определенно плохо.


          1. Hellsy22
            11.06.2015 01:53

            PHP изначально был перловым фреймворком для быстрого клепания простеньких страничек. Попытка использовать его в серьезных проектах ведет к необходимости отказаться от основных преимуществ PHP, т.е. перейти на шаблонизаторы, разбирать запросы, перенести работу с СУБД на отдельный уровень абстракции, отказаться от передачи переменных через сессию и т.д. Короче, в итоге получается более медленная, прожорливая и примитивная версия Перла. Но именно благодаря тем самым «школьникам, клепающим на нем ГС для своих игровых кланов» на сегодняшний день разработчиков PHP в десятки, если не в сотни раз больше, чем разработчиков на Perl. И это играет важную, а порой и решающую роль в выборе языка проекта — пусть PHP и хуже, но зато под него разработчиков найти проще. Ну, а чем больше проектов стартует на PHP — тем меньше вакансий для разработчиков на других языках. Что и вызывает негодование у тех, кто не хочет принять реальность. По мне, так в мире IT полно разного хлама, который стал популярным по разным причинам, проще его изучить и использовать, чем огораживаться.


            1. Acuna
              12.06.2015 02:40

              Так я и думал, что ответит мне не автор сего сообщения. Весьма предсказуемо. Ну да ладно, не суть в общем-то…

              Полностью с Вами соглашусь. Однако тут есть и проблема иного рода. Я был бы очень рад, если бы такого рода критикой выступали бы люди, проверившие эту точку зрения на собственном опыте, то есть они честно писали на нем и сами убедились в том, что этот язык действительно криво решает ту задачу, для решения которой они, собственно, и пытались использовать данный инструмент. Только это может давать им свободу слова в данном вопросе. К такого рода заявлениям я отношусь уважительно. Однако к моему сожалению, эти высказывания чаще всего я слышу от людей, о которых ясно можно сделать вывод, что это мнение они подхватили извне, даже не пытаясь как-то проверить его истинность на собственном опыте. Не знаю, есть ли Вы в некой соц. сети от одного известного питерского программиста, однако там имеется группа Хабра, дак вы бы видели комментарии там, когда речь заходит о PHP! Она просто притягивает толпы школьников, которые генерируют тонны шуточек, просто пытаясь таким образом быть на одной волне со своими одноклассниками. Не один раз говорил о том, что за такое надо реально банить или хотя-бы закрыть там комменты, чтобы хоть как-то избежать наплыв такого количества спама, однако комменты по прежнему там открыты.

              P. S. mephistopheies не в счет, он вроде как в Мыле работает, очень хочется надеяться, что это так.


  1. nickolaym
    05.06.2015 19:28
    +3

    Ситуация, на которой идея «выбрасывать похожие картинки» даст сбой — это, например, скриншоты: «вот посмотрите: здесь у кнопочки есть рамочка, а здесь её сдвинули на пиксел, и рамочка пропала».
    Картинкохостинг в лучшем случае откажет пользователю в загрузке второй картинки («у меня уже есть такая»), а в худшем тихо выберет из двух картинок одну — и сильно удивит читателя (как же так, товарищ жалуется на разницу, а разницы-то нет)


    1. Acuna
      05.06.2015 20:12

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


      1. nickolaym
        05.06.2015 20:35

        Метод далеко не самый точный, так как работает только для идентичных картинок, при малейшем различии — толку ноль.


        Я просто подчёркиваю, что случаи разные бывают, и принимать решения, опираясь лишь на низкоуровневый факт «картинки практически одинаковы» — недостаточно.

        Разве что, можно собирать похожие картинки в кластеры для дальнейших ручных разгребаний.
        И при добавлении новой картинки спрашивать: «вот есть уже похожая: всё равно добавить?»


        1. stychos
          06.06.2015 02:39

          Ваши слова про кластеры напомнили мне одну простую и уже существующую без всяких кластеров вещь — дедупликацию на уровне файловых систем. Кто-нибудь рассматривал предметно, как обстоят дела с идентичными картинками и дедупликацией?


  1. Andriyevski
    06.06.2015 00:10
    -3

    Мне кажется задержки на обработку, если большой сервис и много онлайна будут очень высоки!
    Как бы не было прискорбно, но по хешу вычислять побыстрее будет, я бы не грузил систему проверками, так как спорю 99% затраченого времени будет идти на подтверждение проверки на уникальность изображения!
    Сравнивать со всеми имеющимися на сайте картинками кажется дурной тратой ресурсов системы, даже если будут несколько практически одинаковых картинок, ведь скажите, если на такой же картинке появиться надпись «Привет Вася!» в низу угла, и текст забрал к примеру 10 пикселей от низа в верх и в длинну пускай 20 пиксов, получаеться уникальная картинка присвоеная кому то, система отступит и увидит что картинка идентична с той что есть в системе и удалит именную картинку…
    Я против такого хода… Не рационально! Хеш-сумма самое оно!
    Если и проверять на копии то нужно писать отдельную проверку в которой все таки человек должен делать окончательный выбор удалить или нет.


    1. stychos
      06.06.2015 02:37

      Всё зависит от задачи. Описанный тут хеш Хэмминга — это не постоянная величина, его и создавать, и применять и сравнивать можно по-разному, а немного разобравшись, в голову сразу лезет куча идей с собственными улучшениями велосипедами, так что, вопрос не в том, напишет ли Вася в углу картинки «Привет Вася!», а в том, что именно необходимо сделать в результате.


  1. AlexSane
    06.06.2015 17:22

    Перцептивный хеш — вероятнее всего, самый точный способ нахождения дубликатов

    image
    С другой стороны на очень похожих картинках даёт большое расхождение


    1. golotyuk
      06.06.2015 19:23

      image


  1. vaniaPooh
    06.06.2015 22:49

    А не будет боевой сервер стреляться с высоким load average, если столько хэшей считать? Кажется, что современные диски дешевле, чем процессоры и память.


  1. betauser
    07.06.2015 17:45

    Если хеши двух изображений совпадают – изображения одинаковые.

    Хеш может быть одинаковый для разных картинок. Нужно полностью сравнить каждую картинку из списка картинок с одинаковым хешем.