-
-
Save isagalaev/4554182 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()) |
Да, так короче и лучше, спасибо. Хотя, @bobuk говорит, что он померил, и numpy там вообще выигрыша по скорости не даёт, большую часть времени съедает загрузка и уменьшение картинок.
В 42 строке лучше вместо списка генерить итератор
В 43 строке в sorted тоже можно передать итератор
Есть ещё один вариант реализации distance():
def distance(data1, data2):
...
return sum(abs(x) > THRESHOLD for x in data1 - data2)Впрочем, он аналогичен варианту @reclosedev, только без numpy.
В 43 строке в sorted тоже можно передать итератор
Хм… Я всегда считал, что sorted требует объекта с длиной, а потому открытый генератор ему не подходит. Впрочем, может так и было в старых Питонах… Спасибо!
В 42 строке лучше вместо списка генерить итератор
Там, в общем-то это совсем неважно, потому что список файлов вряд ли будет достаточно большим, чтобы это оправдать. Но можно, да.
Первым делом я бы добавил argv
В images можно избавиться от f (как вы и пытались в скринкасте), а в 42 строке вместо images итерировать по zip(files, images). Тогда переменная filename не нужна.
upd: пардон, не получится из-за random.shuffle(images)
Как это numpy выигрыша не дает? Вот пара тестов на коленке ("загрузка" включает в себя ресайз):
387 картинок, 272 МБ 860 картинок, 587 МБ
numpy | нет | есть numpy | нет | есть
------------+----------+-------- ------------+----------+--------
Загрузка | 61.43 s | 60.63 s Загрузка | 131.14 s | 149.80 s
Сравнение | 121.11 s | 6.11 s Сравнение | 605.53 s | 38.49 s
------------+----------+-------- ------------+----------+--------
Итого | 182.55 s | 66.74 s Итого | 736.67 s | 188.29 s
Ускорение | x1 | x2.7 Ускорение | x1 | x3.9
(Python 2.7.2 x86, Windows 7)
Оба этапа упираются в процессор, скорость диска особо не влияет.
Загрузка/ресайз идет за O(n), сравнение за O(n^2), можно легко оценить выигрыш от быстрого сравнения, подставив две константы...
В 27й строчке наверно имеет смысл в комментарии сослаться не на число 400, а на BLOCK_SIZE^2. А то если вдруг поменяется значение BLOCK_SIZE, этот комментарий потеряет актуальность
@nnemkin
Интересные результаты. При большем BLOCK_SIZE разница была бы еще заметнее.
Можно еще загрузку немного ускорить:
def image_data(filename):
"""
Get data from image ready for comparison
"""
img = Image.open(filename).resize((BLOCK_SIZE, BLOCK_SIZE), Image.BILINEAR)
return numpy.asarray(img).sum(axis=2).ravel()В 27й строчке наверно имеет смысл в комментарии сослаться не на число 400, а на BLOCK_SIZE^2.
Верно :-). Я о чём-то параллельно думал в тот момент, поэтому в итоге забыл исправить.
@reclosedev именно так я и делаю (только c dtype=np.int16 и без ravel). Но на фоне Image.resize разница не заметна.
Кстати если еще переписать оба главных цикла с использованием parallel_map = multiprocessing.Pool().map (буквально), то на 4х ядерном процессоре ускорение на обоих этапах ~3.3.
В 42 строке лучше вместо списка генерить итератор
В 43 строке в sorted тоже можно передать итератор
В 23 видимо тоже генераторное выражение к месту.
Поддержу противников однобуквенных переменных :) При чтении кода строка 42
for f, d in images
совсем не понятна. Что такое f? Что такое d? ИМХО лучше было бы написать
for filename, data in images
так длиннее и многословнее, зато понятнее при просмотре.
def html_group(group):
return ''.join(
'<img src="%s" width="%s"/>%s' % (os.path.basename(f), WIDTH, dist)
for dist, f in group
)
Я думаю можно заменить на
def html_group(group):
tmpl = '<img src="%s" width="%s"/>%s'
return ''.join(tmpl % (os.path.basename(f), WIDTH, dist) for dist, f in group)
Что улучшит читаемость и сократит код на 2 строки.
Хорошо получилось.
distance()можно и numpy доверить: