За 20 лет у меня скопилось несколько тысяч фотографий: праздники, свадьбы, рождение детей, и прочее, прочее... Понятно что снималось всё это на разные цифровики, присылалось почтой, сливалось через ICloud и GDrive, FTP, самба и т.п. По итогу всё это превратилось в дикий хаос папок и что-то найти в архиве можно было только с большим трудом.

В какой-то момент мне нечем было заняться это надоело и я за пару дней накидал скрипт, который всё это безумие раскидал по годам->месяцам->дням. Понятно, что и эта задача не такая простая как кажется на первый взгляд, что например делать с фото, у которых дата создания 1970? Но в этой статье я хотел бы рассказать о другом.

Так вот, во многих папках у меня получилось по 2 и более копий одной и той же фотографий. Как так получилось? Я могу привести кучу вариантов ответа на этот вопрос, но копии от этого сами никуда не рассосутся.

Быстренько погуглив, нашел несколько методов для сравнения фото на идентичность.

Кроме тех, что вы найдете по ссылке, приведу еще пару примеров:

библиотека GD::Image

  my $im1 = GD::Image->new( $image1 );
  my $im2 = GD::Image->new( $image2 );

  my $raw1 = $im1->gd;
  my $raw2 = $im2->gd;
  my $xored = $raw1 ^ $raw2;
  my( $all, $diff ) = (0)x2;

  $all += 255, $diff += ord substr $xored, $_, 1 for 0 .. length( $xored ) - 1;

  my $pr = ( $all - $diff ) / $all * 100;
  print $all, ' ', $diff;
  printf "The simlarity is %.3f%%\n", ( $all - $diff ) / $all * 100;

В данном случае используется сравнение, что называется "в лоб", рассчитывается процент исключенных побитово операндов в выражении $xored = $raw1 ^ $raw2 .

библиотека Image::Compare

use Image::Compare;

my $file1 = shift; #some jpegs, or png
my $file2 = shift;

my ( $cmp ) = Image::Compare->new();

$cmp->set_image1(
   img  => $file1,
   type => 'png',
);

$cmp->set_image2( 
   img => $file2, 
   type => 'png' );

$cmp->set_method(
   method => &Image::Compare::THRESHOLD,
   args   => 25,
);

#$cmp->set_method(
#   method => &Image::Compare::EXACT,
#    );

if ( $cmp->compare() ) {
  # The images are the same, within the threshold
  print "same\n";
}
else {
# The images differ beyond the threshold
  print "not same\n";
}

Image::Compare написана как раз для сравнения изображений, использует разные методы и имеет кучу параметров. С нужной мне задачей она прекрасно справляется, однако функционал библиотеки гораздо шире. В "недра" библиотеки я не заглядывал, по описанию используется анализ pixel-by-pixel.

Итак, у меня есть рабочие методы сравнения и я даже успел протестировать какой из них более производительный. Но... Вот именно производительность меня и не устраивает, ведь даже самое быстрое сравнение только двух фото занимает 2-3 секунды в зависимости от разрешения сравниваемых фотографий. А если изображений несколько тысяч?

Естественно, видя, как работает поиск по изображениям в яндекс и гугл, я ожидал совсем другого. Не унываем, ищем дальше.

библиотека Image::Hash

Итак, что умеет эта библиотека. Цитирую "calculate the average hash, difference hash and perception hash an image". Кроме того, она поддерживает несколько модулей для работы с изображениями: GD, Image::Magick и Imager. Ага, уже интересно, многие советуют для сравнения использовать ImageMagick, а мне как раз его ставить совсем лениво, уж больно много он тянет за собой зависимостей(я поэтому и не стал приводить с ним примеры), а тут замечательные альтернативы. Попробуем:

use Image::Hash;
use GD;
 
my $ihash = Image::Hash->new('/spool/Photo/2015/01/06/00_17_50.jpeg');
 
# Возьмём только ahash
my $a = $ihash->ahash();
 
print "1 Ph: $a\n";

#Второе изображение точная копия первого
$ihash = Image::Hash->new('/spool/Photo/2015/01/06/00_17_50_1082302.jpeg');
$a = $ihash->ahash();
print "2 Ph: $a\n";

#Ну и для чистоты эксперемента...
$ihash = Image::Hash->new('/spool/Photo/2015/01/06/00_17_55.jpeg');
$a = $ihash->ahash();
print "3 Ph: $a\n";

смотрим результат:

1 Ph: C5D74745060CFFFF
2 Ph: C5D74745060CFFFF
3 Ph: 036C040C0878FFFF

Совсем другое дело! Ну и для закрепления быстренько напишем скрипт, который будет проверять файлы в папке на "одинаковость":

use File::stat; #Добавим проверку на размер файла
use Image::Hash;
use GD;

my %args;
foreach my $vl (@ARGV) {
        my ($key,$value) = split(/=/, $vl);
        $args{$key} = $value;
}

my $dir = $args{DIR} or die "Please specify the folder\n";
my @files = glob( $dir . '/*.jpeg' );
my %fhash;
my @results;

for my $val (@files){
    my $ihash = Image::Hash->new($val);
    # Получаем размер проверяемого файла
    my $fsize = stat($val)->size;
    # Получаем хеш файла
    my $a = $ihash->ahash();

#Тут немного распишу:
# 1.проверяем, если такого хеша ранее не было 
# 2.проверяем, если был хеш, то сравниваем размеры файла,
# выигрывает файл большего размера
# По итогу проверки файл с уникальным хешем попадает в %fhash,
# а дубликат в массив на удаление 
if ( !exists $fhash{$a} || (exists $fhash{$a} && $fhash{$a}[1] <=  $fsize) ) {
        $fhash{$a} = [ $val, $fsize];
    }
    else {
        push @results, $val;
    }
}

#Ну и удаляем дубликаты с запросом, будет выведен список файлов на удаление.
if ( @results > 0 ) {
    print 'Total ' . scalar(@results)  . " double files:\n" . join("\n", sort @results) . "\n";
    print "Delete files(Y/N): ";
    my $ans = <STDIN>;
    chomp $ans;
    if ( $ans eq 'Y' ) {
        foreach my $dfile ( @results ) {
            unlink $dfile;
        }
        print 'Check complete! Result: ' . scalar(@results) . " img removed\n";
    }
}
else {
    print "Goodbye!\n"
}

Погоняем по тестовой папке... Ну вот. За 10 секунд "прочесало" 60 изображений, и безошибочно нашло все дубликаты.

Пока дебажил скрипт, замечаю ещё одну полезную вещь, если изображения не являются полной копией, а ОЧЕНЬ похожи, то их хеши различаются в 1-2 разряда. А таких у меня тоже немало, я так понимаю снимали в многокадровом режиме, но почему-то сохранились все исходники.

Тут же возникает идея слить хеши в БД и организовать поиск по фото. Для понимания привожу пример сравнения двух изображений:

1 Ph: ECFCF4F4E4C4C4C4
2 Ph: E4FCFCFCC4D4C4C4

То есть, даже несмотря на огромные отличия, в том числе и разное сотношение сторон, хеши очень похожи. Но это уже история для отдельной статьи.

В заключение

По итогу простая сортировка фотографий вылилась у меня в написание полноценной фотогалерии, с бекендом на Perl и фронтом на JS, обработчиком добавления новых фото с автосортировкой и проверкой на дубликаты.

Если Вас зантересовал этот метод, но Вы не являетесь фанатом Perl, то спешу обрадовать, есть подобные библиотекии для других ЯП: Python и класс для PHP.

UPD: добавляю еще одну ссылку класса для PHP из комментариев, - тут имеется возможность задавать размер анализруемой матрицы.

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


  1. LynXzp
    20.09.2021 12:46
    +2

    Круто. Но если кому тоже надо просто найти разок дубликаты, то есть готовый инструмент: geeqie: File -> Find duplicates. С сортировкой по похожести и полноценным просмотрщиком изображений. (Для windows похоже его нет.)


    1. qark
      20.09.2021 22:35
      +1

      Для Windows есть прекрасный AntiDupl.NET https://github.com/ermig1979/AntiDupl.


  1. Di-Ger Автор
    20.09.2021 13:07

    На самом деле программ для поиска дубликатов не так чтобы много, но они есть, и изначально я как раз-таки попробовал несколько программ с GUI. Не скажу, что все они были плохи, но у многих опять же всё упирается в производительность, ну и нет у меня столько свободного времени, чтобы сидеть и выбирать, правильно ли они нашли дубликаты. Мне нужен был такой способ, чтобы «сделал и забыл». Сейчас я просто выгружаю фотографии на сервер, а обработчики уже сами выкидывают дубликаты и раскладывают файлы. Ну и отдельная песня — это ОЧЕНЬ похожие изображения, про которые я писал в статье и которые смысла нет хранить, тут у меня анализ сваливается в вебку и вот там уже приходится ручками сортировать.


  1. talbot
    20.09.2021 14:22
    +1

    GUI инструменты трудно автоматизировать, тогда как перл-скрипт, проверяющий дубликаты, можно сделать частью pipeline, и расчищить и раскладывать фотографии в автоматическом режиме.


  1. COKPOWEHEU
    20.09.2021 15:13

    Если нужно просто найти дубликаты, существует утилита fdupes.


    1. Di-Ger Автор
      21.09.2021 09:03
      +1

      К сожалению fdupes совершенно не подходит для работы с изображениями. Как nix-юзер с многолетним стажем, я в первую очередь эту тулзу попробовал. Дело в том, что у неё первоначальный анализ строится на MD5 хешах, а практически все изображения содержат, кроме собственно изображения, — Exif. И тут уже сразу большая часть дубликатов пролетает мимо. Ну а дальше, опять же низкая производительность, так как используется сверка по-байтно.


  1. SergeyDeryabin
    20.09.2021 23:44

    Такой нюанс ещё. Может быть несколько фотографий одинаковых, но оригинал один, а остальные сохранённые из мессенджеров, следовательно ужатые.

    И ещё была прога от Google - picasa, она тоже отлично дубликаты находила.

    В своё время для автоматизации похожих изображений использовал библиотеку phash. Правда она и тогда была уже старая, я ее под php 5.6 ещё собирал )


    1. Di-Ger Автор
      21.09.2021 08:48
      +1

      Совершенно верно, кроме мессенджеров, я у себя нашел слитые папки из IPhoto, там такая же ерунда да еще и превьюшки. Тут я уже использую схожесть хешей, приходится вводить, эм, скажем так «коэффициент достоверности», то есть допуски по расхождению хешей.


  1. stur
    21.09.2021 09:04
    +1

    пару месяцев провозился с этой темой вот что могу сказать

    судя по длине хеш алгоритм сжимает картинку до матрицы 8*8 , на практике этого не достаточно если очень много фото у вас пойдет много ложных срабатываний на первый взгляд разные картинки будут давать полное совпадение хеш

    надо увеличить хеш сжимать картинку до 16*16 у вас на каждую картинку будет 4 таких хеш

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

    • Обрезаем картинку берем квадрат по центру

    • Сжимаем картинку до указанного размера

    • запускаем цикл и проходим по пикселям

    • берем в точке цвет, получаем 3 составляющих красный = 123, зеленый = 23 и синий  = 233 

    • вычисляем итоговое значение Zxy = ( красный-зеленый - синий) 2   квадрат разницы,  полученное число заносим в массив 

    • записываем значения для всех точек в массив

    • вычисляем среднее average   для всего массива

    • проходим в цикле по массиву сравниваем каждое Zxy   со средним значение если оно больше то в битовую маску пишем 1 если меньше пишем 0

    • получаем битовую маску 0101010101010101010101011111111

    • далее битовую маску можно получить в виде x16 кода, битовой строки или массива

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


    1. Di-Ger Автор
      21.09.2021 09:37

      Большое спасибо за комментарий, по ссылке как раз обсуждаются эти моменты, предлагаются даже матрицы большего размера 32х32. Естественно я принимал во внимание возможность ложного срабатывания, поэтому никогда бы не стал обрабатывать весь массив фотографий, — как я писал ранее, они уже рассортированы по периодам. А вот при обработки новых фото, вероятность практически нулевая, так как нет необходимости сравнивать со ВСЕМИ фотографиями, а только с аналогичным периодом. А для поиска в вебке «по фото», такого размера хеша более чем достаточно, ложными срабатываниями можно пренебречь и даже расширить допуски, чтобы можно было находить ПОХОЖИЕ изображения. И да, я попробовал фотографии с Вашего сайта, вот результаты:

      1 Ph: FFFFFF3F00040400
      2 Ph: FFFFFF4000000000
      3 Ph: FFFFEB0000000000


      1. stur
        21.09.2021 14:46

        видимо ваш алгоритм получения матрицы немного отличается от того исходника с которого я начинал


        1. Di-Ger Автор
          21.09.2021 19:23

          Он не мой :) Я вообще только после Вашего комментария решил заглянуть ему "под капот". Еще раз, спасибо за комментарий.


  1. robert_ayrapetyan
    21.09.2021 17:48
    -1

    Почему бы не залить все в гугл фото, где все описанные проблемы будут решены автоматически?


    1. Di-Ger Автор
      21.09.2021 19:06

      Все фото за 20 лет? А вспоминая истории с потерей файлов на cloud.mail.ru, ну его нафиг. Ну и в свете последних событий, можно и вообще доступа лишится к таким сервисам.


      1. robert_ayrapetyan
        21.09.2021 19:39

        У меня обратный опыт - сколько потерянных фото с разных девайсов и умерших дисков - не счесть.


        1. Di-Ger Автор
          22.09.2021 06:05

          Да, тоже потерял кучу фото, в основном по выходу из строя HD. Спасибо Sun Ms за ZFS, а особо, за zfs snapshot, не страшно даже по ошибке удалить что-то. Пользуюсь ZFS на «боевых» серверах начиная с FreeBSD 9, — за все годы ни разу не потерял свои данные.