{"id":14262,"url":"\/distributions\/14262\/click?bit=1&hash=8ff33b918bfe3f5206b0198c93dd25bdafcdc76b2eaa61d9664863bd76247e56","title":"\u041f\u0440\u0435\u0434\u043b\u043e\u0436\u0438\u0442\u0435 \u041c\u043e\u0441\u043a\u0432\u0435 \u0438\u043d\u043d\u043e\u0432\u0430\u0446\u0438\u044e \u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u0434\u043e 1,5 \u043c\u043b\u043d \u0440\u0443\u0431\u043b\u0435\u0439","buttonText":"\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435","imageUuid":"726c984a-5b07-5c75-81f7-6664571134e6"}

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

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

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

Инструменты

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

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

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

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

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

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 если картинки идентичны. Если мы сравним первую картинку саму с собой, то получим полностью черное изображение.

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

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 картинок.

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

0
14 комментариев
Написать комментарий...
Sergey Ilyin

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

Ответить
Развернуть ветку
Eugene Myazin

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

Ответить
Развернуть ветку
Sergey Ilyin

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

Ответить
Развернуть ветку
Ales Sharaev

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

Ответить
Развернуть ветку
Sergey Ilyin

Хм. Сначала сравнить размеры в байтах, а потом - сами картинки при совпадении размеров? Не уверен, если честно, что это быстрее сравнений хэшей.

Ответить
Развернуть ветку
Alexey Baranov

Такое сравнение можно использовать в качестве пре-проверки, чтобы уменьшить размер списка сравнения.
Пример - есть список картинок "a b c A D e B a c f D g a", где:
- a и а - картинки, одинаковые и по содержанию, и по весу
- а и А - картинки, одинаковые по содержанию, но разные по весу
Если на первом шаге прогнать сравнение только по весу картинки, то будут найдены одинаковые группы "a a а", "с с" и "D D".
Убрав дубликаты, мы получим новый список - "a b c A D e B f g", который, очевидно, короче начального и для которого перебор "все со всеми" будет проходить быстрее.
Открытый вопрос - при каком количеству дубликатов в начальном списке, добавление такой пре-проверки оказывается выигрышным по времени?

Ответить
Развернуть ветку
Александр Соколов

Чел, но ведь две разные картинки могут иметь одинаковый размер, ты оптимизируешь не в ту сторону...

Ответить
Развернуть ветку
Alexey Baranov

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

Ответить
Развернуть ветку
Дмитрий Державин

Очень не оптимально полагать на малую вероятность в решениях задач

Ответить
Развернуть ветку
Alexey Baranov

Зависит от контекста.
Я предложил использовать этот подходит в качестве начального фильтра, чтобы уменьшить общую вычислительную сложность алгоритма, и, ИМХО, такой подход имеет право на жизнь.
Использовать же его для определения конечной оценки не советую, потому-что он повышает вероятность допустить ошибку, что непозволительно.

Ответить
Развернуть ветку
Azret Shaulukhov

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

Ответить
Развернуть ветку
KreO

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

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

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}].')

Ответить
Развернуть ветку
NTA
Автор

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

Ответить
Развернуть ветку
Oleg Run

Вот сервис похожий https://jarjad.ru/compare-images/

Ответить
Развернуть ветку
11 комментариев
Раскрывать всегда