-
-
Save bobuk/4522091 to your computer and use it in GitHub Desktop.
#!/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()) |
ну на жестком диске разные файлы хранятся фрагментированно, так что головка жесткого диска бегать будет все равно много. Быстрее было бы, если б была одна большая картинка, имхо.
Ну вы же, наверное, хотите не только с 40 картинками работать, а по всей галерее бегать. Смысл хранить в памяти то, что не нужно ;)
Мой вариант на 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
Проверьте на реальных данных. Моя практика показывает что "сначала загрузить, потом посчитать" работает быстрее, а памяти на превью уходит 400*8+8 = 3208 байт. На 40 картинок - 128 килобайт. Чота я не уверен что надо экономить.