Автоматизация, Программирование

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

Время прочтения: 4 мин.

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

Инструменты

Посмотрев просторы интернета, первым делом на глаза мне попалась библиотека 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

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

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

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

Первый способ простой, в нашу функцию 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 картинок.

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

Советуем почитать