Python. Как сравнить фотографии?

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

Совсем недавно у меня появилась интересная задача – необходимо было найти одинаковые фотографии на разных объектах недвижимости. Т.е. к объектам недвижимости расположенных с разным местоположением крепилась одна и та же фотография, может ошибочно, может специально, но такие объекты надо было найти. И я хотел бы поделиться тем, как я решал эту задачу. Для примера у Вас может быть домашняя фототека.

Инструменты

Посмотрев просторы интернета, первым делом на глаза мне попалась библиотека OpenCV, эта библиотека имеет интерфейсы на различных языках, среди которых Python, Java, C++ и Matlab. Мне стало интересно, есть ли у Python стандартная библиотека для работы с изображениями и вот она – Pillow. Это форк PIL, которая успешно развивается и был принят в качестве замены оригинальной библиотеки. Свой выбор я остановил на ней.

Решение задачи

Начнем работу с библиотекой, и попробуем открыть файл и показать его.

from PIL import Image #указываем необходимое имя файла im=Image.open('cbcf449ffc010b9f958d611e787fa48092ac31841.jpg') # Покажет нам изображение. im.show()

Данный скрипт откроет нам изображение. Почитав документацию, я нашел функцию, которая по пикселям сравнивает два изображения и выдает разницу. Функция называется difference и находится в модуле ImageChops. Что бы показать принцип работы функции, для примера возьмем фотографию и добавим на нее какой-нибудь текст:

Python. Как сравнить фотографии?
Python. Как сравнить фотографии?
from PIL import Image, ImageChops image_1=Image.open('06ebe74e5dfc3bd7f5e480cf611147bac45c33d2.jpg') image_2=Image.open('06ebe74e5dfc3bd7f5e480cf611147bac45c33d2_text.jpg') result=ImageChops.difference(image_1, image_2) result.show() #Вычисляет ограничивающую рамку ненулевых областей на изображении. print(result.getbbox()) # result.getbbox() в данном случае вернет (0, 0, 888, 666) result.save('result.jpg')

result.show() вернет разницу в пикселях. Так же прошу обратить внимание на result.getbbox(), функция либо вернет рамку где расходятся пиксели, либо вернет None если картинки идентичны. Если мы сравним первую картинку саму с собой, то получим полностью черное изображение.

Python. Как сравнить фотографии?

Напишем простенькую функцию по сравнению двух картинок:

def difference_images(img1, img2): image_1 = Image.open(img1) image_2 = Image.open(img2) result=ImageChops.difference(image_1, image_2).getbbox() if result==None: print(img1,img2,'matches') return

Теперь необходимо подумать над алгоритмом перебора имеющихся изображений.

path='images/' #Путь к папке где лежат файлы для сравнения imgs=os.listdir(path) check_file=0 #Проверяемый файл current_file=0 #Текущий файл while check_file<len(imgs): if current_file==check_file: current_file+=1 continue difference_images(path+imgs[current_file], path+imgs[check_file]) current_file+=1 if current_file==len(imgs): check_file+=1 current_file=check_file

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

А если файлов для сравнения очень много и их обработка очень долгая? Можно пойти двумя способами:

  • Создать миниатюры и работать с ними.
  • Запустить нашу обработку в несколько потоков.

Первый способ простой, в нашу функцию difference_images добавляем несколько строк:

def difference_images(img1, img2): image_1 = Image.open(img1) image_2 = Image.open(img2) size = [400,300] #размер в пикселях image_1.thumbnail(size) #уменьшаем первое изображение image_2.thumbnail(size) #уменьшаем второе изображение #сравниваем уменьшенные изображения result=ImageChops.difference(image_1, image_2).getbbox() if result==None: print(img1,img2,'matches') return

Второй способ уже сложнее и более интересный, потому что нужно будет управлять и потоками, и очередями, так же нужно будет переписать часть кода. Для этого нам понадобятся следующие библиотеки threading и Queue (подробней можно почитать в интернете), ниже приведен готовый код с внесенными изменениями, я постарался прокомментировать все действия что бы было понятно:

class diff_image(threading.Thread): #класс по сравнению картинок. """Потоковый обработчик""" def __init__(self, queue): """Инициализация потока""" threading.Thread.__init__(self) self.queue = queue def run(self): """Запуск потока""" while True: # Получаем пару путей из очереди files = self.queue.get() # Делим и сравниваем self.difference_images(files.split(':')[0],files.split(':')[1]) # Отправляем сигнал о том, что задача завершена self.queue.task_done() def difference_images(self, img1, img2): image_1 = Image.open(img1) image_2 = Image.open(img2) size = [400,300] #размер в пикселях image_1.thumbnail(size) #уменьшаем первое изображение image_2.thumbnail(size) #уменьшаем второе изображение result=ImageChops.difference(image_1, image_2).getbbox() if result==None: print(img1,img2,'matches') return def main(path): imgs=os.listdir(path) #Получаем список картинок queue = Queue() # Запускаем поток и очередь for i in range(4): # 4 - кол-во одновременных потоков t = diff_image(queue) t.setDaemon(True) t.start() # Даем очереди нужные пары файлов для проверки check_file=0 current_file=0 while check_file<len(imgs): if current_file==check_file: current_file+=1 continue queue.put(path+imgs[current_file]+':'+path+imgs[check_file]) current_file+=1 if current_file==len(imgs): check_file+=1 current_file=check_file # Ждем завершения работы очереди queue.join() if __name__ == "__main__": path='images/' main(path)

Резюме

В результате мы получили готовый алгоритм для поиска одинаковых картинок, а так же постарались ускорить обработку файлов двумя способами. Завершив свою задачу, я обнаружил 1227 совпадений в выборке из 6616 картинок.

Надеюсь, моя статья была полезна. Спасибо за внимание.

2020
14 комментариев

Достаточно ли оптимально?
Может, для каждой картинки сперва посчитать хэши, а потом уже хэши сравнивать? По ощущениям, должно быть быстрее.

2

100% быстрее. 
Из простых решений это phash.org, для похожести можно еще расстояние Хэмминга использовать. Загнать в постгрес, индексы которого помогут быстрее матчинг проводить, например, можно на это посмотреть https://github.com/fake-name/pg-spgist_hamming

2

Можно для начала сравнить размеры в байтах

Хорошая статейка, спасибо!

1

вместо того, чтобы проверять картинку на соответствие каждой следующей, лучше просто сделать словарь (размер : расположение) и если размер картинки будет в словаре, то сравнивать их контент. В случае совпадения будет выводиться сообщение с расположением этих одинаковых картинок

вот мой код, надеюсь он вам поможет:

import os
from PIL import Image, ImageChops

print('Введите ПОЛНЫЙ путь к папке: ', end = '')
directory_in_str = input()

directory = os.fsencode(directory_in_str)
imgs = os.listdir(directory_in_str)
dublicates = {}
try:
    for file in os.listdir(directory):
        filename = os.fsdecode(file)
        last_name = directory_in_str
        last_name += '\\' + filename
        f_info = os.stat(last_name)
        if f_info.st_size in dublicates:
            image_1 = Image.open(last_name)
            image_2 = Image.open(dublicates[f_info.st_size])
            result = ImageChops.difference(image_1, image_2)
            result = result.getbbox()
                if result == None:
                    couter += 1
                    print(f'Найден дубликат: [{last_name} и {dublicates[f_info.st_size]}]')
                else:
                    dublicates[f_info.st_size] = last_name
except:
    print('Ошибка поиска директории!')

print(f'Программа завершила работу, файлов обработано [{couter}].')

1
Автор

Спасибо за готовое решение!