public
Last active

Сегодня я вам покажу, как написать маленький скриптик, который умеет искать похожие фотографии. Зачем? Ну, если честно - совсем без причины.

  • Download Gist
img.py
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Video of this screencast: https://vimeo.com/57296525
#
#
from __future__ import print_function, division, absolute_import
from PIL import Image as pImage
import numpy
 
import os
import random
 
class Image:
"""Take an information from image file"""
 
BLOCK_SIZE = 20
TRESHOLD = 60
 
def __init__(self, filename):
self.filename = filename
 
def load(self):
img = pImage.open(self.filename)
small = img.resize( (Image.BLOCK_SIZE, Image.BLOCK_SIZE),
pImage.BILINEAR )
self.t_data = numpy.array(
[sum(list(x)) for x in small.getdata()]
)
del img, small
return self
 
def __repr__(self):
return self.filename
 
def __mul__(self, other):
return sum(1 for x in self.t_data - other.t_data if abs(x) > Image.TRESHOLD)
 
class ImageList:
"""List of images information, built from directory.
All files must be *.jpg"""
def __init__(self, dirname):
self.dirname = dirname
self.load()
 
def load(self):
self.images = \
[Image(os.path.join(self.dirname, filename)).load() \
for filename in os.listdir(self.dirname)
if filename.endswith('.jpg')]
random.shuffle(self.images)
return self
 
def __repr__(self):
return '\n'.join( ( x.filename for x in self.images ) )
 
def html(self):
res = ['<html><body>']
for img in self.images:
distances = sorted([ (img * x, x) for x in self.images ])
res += [
'<img src="' + os.path.basename(x.filename) + '" width="200"/>' + str(dist)
for dist, x in distances if dist < 220]
res += ['<hr/>']
res += ['</body></html>']
 
return '\n'.join(res)
 
if __name__ == '__main__':
il = ImageList('/Users/bobuk/,misc/wm')
print(il.html())

всетаки в функции html (img * x, x) происходит не только сравнения №1 и №2 картинки, но и №2 и №1 после, но вы про это сказали. Но, пожалуй, страшнее то, что №1 и №1 тоже, что слегка странно) Лучше, наверное:

for imgNum in xrange( len(self.images) ):
    distances = sorted([ (self.images[imgNum] * x, x) for x in xrange( imgNum + 1, \
                                                       len(self.images) ) ])
    ...

Но это мелочи, спасибо за скринкаст)

Да и еще, нет резона превьюшки в памяти хранить. Функцию Image.load() неплохо былобы вызывать в Image.mul() и там же удалять привьюшки. Потерь производительности не будет(если учесть предыдущий коммент), а прирост свободной памяти обеспечен.

Проверьте на реальных данных. Моя практика показывает что "сначала загрузить, потом посчитать" работает быстрее, а памяти на превью уходит 400*8+8 = 3208 байт. На 40 картинок - 128 килобайт. Чота я не уверен что надо экономить.

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

Ну вы же, наверное, хотите не только с 40 картинками работать, а по всей галерее бегать. Смысл хранить в памяти то, что не нужно ;)

Возможно я еще "малый и глупый", но ничего не могу поделать с ошибкой:

raise IOError("decoder %s not available" % decoder_name)
IOError: decoder jpeg not available

Стоит Python2.7.

Мой вариант на java, правда работает менее точно.

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import java.io.*;
import java.util.*;


public class FindClone {
    static final int SIZE = 20;
    BufferedImage changeSize(String file_name) throws IOException{
        BufferedImage originalImage = ImageIO.read(new File(file_name));        
        BufferedImage resizedImage = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_RGB );
        Graphics2D g = resizedImage.createGraphics();
        g.drawImage(originalImage, 0, 0, SIZE, SIZE, null);
        g.dispose();
        return resizedImage;
    }
    int getImgSum(BufferedImage img){
        int sum = 0;
        for(int i = 0; i < SIZE; i++){
            for(int j = 0; j < SIZE; j++){
                int rgb = img.getRGB(j, i);
                sum += ((rgb & 0xff) + ((rgb >> 8) & 0xff) + ((rgb >> 16) & 0xff))/3;
            }
        }
        return sum;
    }
    TreeMap<Integer,String> genSumArray(String path_name) throws IOException{
        TreeMap<Integer,String> sum_list = new TreeMap<Integer, String>();
        File path = new File(path_name);
        if(path.isDirectory()){
            String path_list[] = path.list();               
            for(String s: path_list){
                if(s.matches(".*jpg")){
                    String name = path_name+"\\"+s;
                    int sum = getImgSum(changeSize(name));              
                    sum_list.put(sum, name);
                }
            }
        }
        return sum_list;
    }
    public static void main(String[] args) {
        try{
            System.out.println("<html><body>");
            FindClone fc = new FindClone();
            TreeMap<Integer,String> tm = fc.genSumArray(args[0]);
            int tmp = 0;
            for(Integer i:tm.keySet()){
                if(i - tmp > 200) System.out.println("<hr>");
                tmp = i;
                System.out.println("<img src=\""+tm.get(i)+"\" width=200 />"+i);
            }
            System.out.println("</body></html>");
        }
        catch(Exception e){
            e.printStackTrace();
        }       
    }
}

Мне вот такой стиль нравится:

self.images = [
    Image(os.path.join(self.dirname, filename)).load()
    for filename in os.listdir(self.dirname)
    if filename.endswith('.jpg')
]

Начал его использовать под впечатлением от http://en.wikipedia.org/wiki/FLWOR

Спасибо за код и скринкаст - очень понятно и подробно. Возможно даже применю его в скором времени в реальном проекте, если получится. Именно поэтому полез его тестить. Нашел один баг (поправьте меня, если я ошибаюсь). Функция getdata() возвращает список из кортежей, каждый из которых содержит 3 целых числа в диапазоне от 0 до 255 - т.е. каждый пиксел идентифицируется RGB-вектором. Каждому такому кортежу сопоставляется число - сумма элементов. Например, если кортеж будет такой (1, 2, 3), то ему сопоставится число 6, а если такой (3, 2, 1) или такой (2, 2, 2), то тоже 6.

[sum(x) for x in img.getdata()]

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

Контрпример:

флаг 3х цветов (1, 2, 3), (2, 2, 2), (3, 2, 1) и флаг двух цветов (1, 2, 3), (2, 2, 2) - это два разных флага, но алгоритм, переводящий все в серый, будет их считать полностью идентичными изображениями.

Поэтому я немного модифицировал код https://gist.github.com/4579268 (взял сразу отрефакторенный, хорошо написан, спасибо). Число вектору не сопоставлял, а оставил вектор как есть.
[x for x in img.getdata()]

А после вычисления разности двух матриц посчитал евклидову норму разницы векторов (расстояние между ними в евклидовом пространстве RGB): sqrt(R^2 + G^2 + B^2).

sum(1 for x in data1 - data2 if math.sqrt(x[0]*x[0] + x[1]*x[1] + x[2]*x[2]) > THRESHOLD)

На моих тестах получилось, что получше группирует, чем исходный вариант программы. Кое-какие файлы исходная программа группировала лучше, но таких всего пара была, а измененный вариант работал лучше на большем числе файлов (т.е. объединял их в группы, а исходный вариант этого не делал). Вобщем, ошибка накапливается. И вообще, идея переводить все в серый мне кажется не очень хорошей, так как, во-первых, теряется много ценной информации - цвет - а также, способов преобразования в серый существует, насколько я понял, больше одного. Поэтому я бы так делать не стал. Это все равно что сравнивать фото, отснятые ночью.

И еще интересно, как этот скрипт можно адаптировать на большой объем картинок в базе (на С, pypy переписывать?). Работает он довольно долго.

Upd: преобразование из цветного изображения в черно-белое корректно делается не через среднее арифметическое цветов (но так, вроде бы тоже можно), а вот так (цитата отсюда guildalfa.ru/alsha/node/14):

"Хорошо известна формула для вычисления яркости цветного изображения по интенсивности цветовых составляющих:

Lum := Red * 0.299 + Green * 0.587 + Blue * 0.114;

Новое значение цвета пикселя в градациях серого получается при помощи задания одинаковой интенсивности всех цветовых составляющих:

Color := Lum * $00010101;"

Но при таком преобразовании, если, например, нужно сравнить два рисунка с одинаковыми котами в черной комнате, то, если один из котов будет светло-розовый, а другой - темно-синий (т.е. яркость котов будет разная), и это даже не говоря о том, что кто-то раскрасит, к примеру, котов пятнами разного цвета и формы, оставив сами рисунки одинаковым, то оттенки серого в преобразованных рисунках будут различны, поэтому сравнивать корректно алгоритм их не будет. Для более корректного сравнения необходимо оставить на картинке только пикселы двух цветов - черного и белого - причислив каждый пиксел исходной картинки к какой-то из этих 2х групп, но от бинарности точность может и упасть. Более сильным критерием при сравнении все таки является не сама яркость в точке, а характер ее изменения в ней, что и используется в методах сравнения изображений через рассчет характера изменений яркости в ключевых точках двух изображений через рассчет некоторого хеша в точке и сравнения этих хешей (в качестве хешей берутся производные различного типа по яркости) в этих точках (2 ссылка):

http://stackoverflow.com/questions/1005115/what-algorithm-could-be-used-to-identify-if-images-are-the-same-or-similar-re
http://stackoverflow.com/questions/10984313/opencv-2-4-1-computing-surf-descriptors-in-python

IOError: decoder jpeg not available

pip remove PIL
sudo apt-get install python-image

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.