LINUX.ORG.RU

Поиск похожих картинок

 


3

4

Недавно всплывал вопрос о поиске одинаковых картинок. Я хотел применить ImageMagick, но сломал голову его инструкцией и решил начать с решения попримитивнее. Питон 3.10 с установленными пакетами NumPy, Pillow и python-magic. Каждая картинка конвертируется в 24-битную 20x20, затем считается евклидово расстояние между каждой парой картинок (как корень из суммы квадратов разностей для каждого байта). Дефолтное MAX_IMAGE_PIXELS оказалось недостаточным для крупных сканов. Для простоты ограничился типами PNG, JPEG и GIF и файлами только в текущей директории — если нужно что-то сложнее, os.listdir() нужно заменить на соответствующий список или генератор, например, [os.path.join(root, file) for root, dirs, files in os.walk(os.getcwd()) for file in files] или [r + '/' + f for r, _, files in os.walk('.') for f in files].

import os, magic
import numpy as np
from PIL import Image

thumb_size = 20
max_distance = ( 256**2 * thumb_size**2 * 3 )**0.5  
allow_magic = {'PNG image ', 'JPEG image', 'GIF image '}
Image.MAX_IMAGE_PIXELS = 400_000_000

names = sorted( name for name in os.listdir() if os.path.isfile(name) and magic.from_file(name)[:10] in allow_magic )
thumbs = [ Image.open(name).convert(mode='RGB').resize((thumb_size, thumb_size)) for name in names ]
td = [ np.frombuffer(thumb.tobytes(), dtype=np.int8) for thumb in thumbs ]

table = np.full((len(names), len(names)), max_distance, dtype=np.float64)
for nout, hout in enumerate(td):
    for nin, hin in enumerate(td[nout+1:]): 
        table[nout, nin + nout + 1] = np.linalg.norm(hout - hin)

Для идентичных картинок расстояние равно нулю. Для отличающихся размером и артефактами сжатия — существенно меньше 100. Пока ни разу не видел расстояния больше 3500.

Дальше нужно просматривать похожие пары. Дефолтный просмотрщик меня не устроил, поэтому nomacs. Для нулей я вызывал

m = table.min(); r = np.where(table == m); print(m, r);
for x, y in zip(r[0], r[1]): print(names[x], names[y]); os.system( f'nomacs "{names[x]}" & nomacs "{names[y]}"' )

А разобравшись с файлами, заменил все нули на недостижимо большую величину:

table[r] = max_distance

Для расстояний больше 0 вручную повторял однострочник

table[r] = max_distance; m = table.min(); r = np.where(table == m); print(m, r); x, y = r[0]; names[x], names[y]; os.system( f'nomacs "{names[x]}" & nomacs "{names[y]}"'

пока не надоело. В принципе, в таблице могут найтись одинаковые расстояния, поэтому вручную контролировал, что в r вернуло только 1 пару номеров, но этого не произошло.

Для 20 000 картинок, из которых большинство размером 100-1000 пикселов, и которые прочлись в кеш, время вычисления на 1,8 ГГц ядре:
name (определяется magic.from_file) — 10 с,
thumbs (Image.open + Image.convert + Image.resize) — 272 c,
td (np.frombuffer + thumb.tobytes) — 0,3 с,
table — 3276 c.

Как-то улучшить можно? Ускорить?

★★★★★

Последнее исправление: question4 (всего исправлений: 5)

Ответ на: комментарий от DrBrown

код принадлежит автору, любое использование кода без разрешения автора недопустимо.

vvn_black ★★★★★
()

Алго норм. Можно улучшить наверное если уменьшить кол-во сравниваемых и добавлением эвристик.

  1. Если картинки изначально должны быть одного размера - сравнить width, height при несовпадении не продолжать
  2. Вместо RGB сделать оттенки серого и уменьшить thumb_size
  3. Уже для совпадений делать проверку по RGB и увеличенному thumb_size.

Еще можно попробовать это все распараллелить. Например если у тебя есть 100 картинок, то ты можешь не поочереди все обрабатывать, а, например, одновременно искать пары к первой и второй, а результаты смерджить. Запусти N воркеров через multiprocessing и распредели между ними картики.

Ну и последнее: это очевидно числодробилка, а значит лучше делать ее не на python. Возьми раст там или плюсы. Улучшение будет заметное. Да, numpy достаточно быстрый, но вот

Image.open(name).convert(mode='RGB').resize((thumb_size, thumb_size)).tobytes()

Это питон код и довольно медленный

Aswed ★★★★★
()
Ответ на: комментарий от DrBrown

Какая лицензия у этого кода?

CC0.

question4 ★★★★★
() автор топика
Ответ на: комментарий от Aswed

Еще можно попробовать это все распараллелить.

Всё верно. Всё, что после вызова os.listdir, можно и нужно распараллелить.

multiprocessing

Спасибо за название.

Если картинки изначально должны быть одного размера - сравнить width, height при несовпадении не продолжать

Cкрипт в первую очередь сделан именно для отличающихся размером.

Если померить, для 1007 файлов таблица 1007*1007/2 считается 8,12 секунд, 1007 размеров берутся 0,0016 с, а сравниваются с заданным 0,0032 с. То есть вычисление нормы 1.6e-05 с, получение размеров через PIL 1.6e-06 с, а со сравнением 3.2e-06 с.

это очевидно числодробилка, а значит лучше делать ее не на python. Возьми раст там или плюсы.

Питон — ради интерактивности шелла. И PIL в значительной степени написан на Си. Кроме того, самая долгая часть, возрастающая квадратично — составление таблицы сравнений. Для малого числа картинок нет необходимости оптимизировать вычисление тамбнейлов, для большого таблица всё равно многократно больше.

Вместо RGB сделать оттенки серого и уменьшить thumb_size Уже для совпадений делать проверку по RGB и увеличенному thumb_size.

Попробовал. Серые тамбнейлы размером 8, 16 и 20 считаются 19-20 секунд, это 0,19-0,20 с каждый. RGB — 0,25 c. Таблицы сравнения считаются 6,2, 6,6 и 6,8 с соответственно, это 1.22e-05, 1.29e-05 и 1.34e-05 на каждую пару. Против 1.60e-05 для RGB. А значит, да, Питон существенно замедляет, иначе был бы выигрыш в 19, 4,7 и 3 раза.

Результат поиска минимумов интересный. Для серых 8x8 последовательность «расстояний» оказалась ближе всего к цветным 20x20. Но нормально отработали все.

Ещё давно хотел сравнить enumerate и слайсы с тупым подходом через range и len:

ln = len(names)
for nout in range(ln):
    for nin in range(nout+1,ln): table[nout, nin] = np.linalg.norm(td[nout] - td[nin])

На RGB ушло 8,15 с, это те же 1.6e-5 с на пару.

Получается, только распараллеливание.

И будет полезна какая-то замена PIL.

question4 ★★★★★
() автор топика
Ответ на: комментарий от ei-grad

поставить pillow-simd вместо pillow

Поставил pillow-simd, удалил pillow. import PIL теперь ничего не находит.

P.S. Удалил pillow-simd, поставил повторно. Нашло.

P.P.S. Image.open(name).convert(mode='RGB').resize((thumb_size, thumb_size)) падает от недопустипой инструкции.

question4 ★★★★★
() автор топика
Последнее исправление: question4 (всего исправлений: 2)
Ответ на: комментарий от ei-grad

pillow-simd

Как его правильно пересобрать под AMD K10?

$ grep flags /proc/cpuinfo | uniq
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm 3dnowext 3dnow constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf pni monitor cx16 popcnt lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ibs skinit wdt nodeid_msr cpb hw_pstate vmmcall npt lbrv svm_lock nrip_save pausefilter
question4 ★★★★★
() автор топика

А нельзя ли найти несколько эталонных картинок, разбить на группы и сравнивать попарно внутри групп?

Вопрос только где взять эти эталоны.

olegd ★★★
()

Хочу поиграться, для чистоты эксперимента скинь пожалста 20 000 картинок своих ))).

Или список размеров всех картинок, что-бы я сгенерировал их.

LINUX-ORG-RU ★★★★★
()

Вместо попарного перебора можно собрать индекс через nmslib, и искать потом ближайших соседей по нему.

ei-grad ★★★★★
()
Ответ на: комментарий от question4

Гм, даже если получится - не сильно поможет, там вручную оптимизации, будет примерно то же самое что сам pillow пересобрать.

С таким процессором я бы посоветовал перенести обработку изображений на GPU, но наверное будет странно рассчитывать что в такой системе может оказаться в наличии видеокарта поддерживаемая современными версиями CUDA?

ei-grad ★★★★★
()
Ответ на: комментарий от ei-grad

Вместо попарного перебора можно собрать индекс через nmslib, и искать потом ближайших соседей по нему.

Сходу вопрос: индекс можно составлять по полным картинкам? Или они все должны быть одинаковой длины?

question4 ★★★★★
() автор топика
Ответ на: комментарий от ei-grad

будет странно рассчитывать что в такой системе может оказаться в наличии видеокарта поддерживаемая современными версиями CUDA

Radeon HD 6950.

Гм, даже если получится - не сильно поможет, там вручную оптимизации, будет примерно то же самое что сам pillow пересобрать.

Какой-то SSE4 есть же. Но каких-то инструкций не хватает. В чём дело? Существует несколько несовместимых SSE4?

Или текст выше означает только голый набор SSE4a без всех остальных инструкций SSE4?

question4 ★★★★★
() автор топика
Последнее исправление: question4 (всего исправлений: 2)
Ответ на: комментарий от question4

Спасибо, поиграюсь =)

А по поводу оптимизации, ну вместо 100500 форматов картонок все их сконвертировать заранее в 1 формат без сжатия, но с RLE. Я вот буду играться только с TGA, чистая сишка без зависимостей.

Как поиграю выложу, мож пригодится =)

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от question4

Radeon HD 6950.

Расчёты можно провести в шейдере, нужно только будет успевать забивать данные в текстуру =)

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от ei-grad

сам pillow пересобрать.

Заменил pillow с pypi на джентушный пакет. Время на создание тамбнейла уменьшилось на <5%.

question4 ★★★★★
() автор топика
Ответ на: комментарий от vvn_black

Хоть меня и игнорируют, но спрошу, почему не сделать поиск похожих картинок через расчёт дистанции между эмбеддингами, а вектора эти загнать предварительно в базу?

vvn_black ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

вместо 100500 форматов картонок все их сконвертировать заранее в 1 формат без сжатия, но с RLE

Лимитирующая стадия — сравнение. Она единственная возрастает квадратично.

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

Ну эт понятно всё нужно сравнить со всем кроме себя.

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от vvn_black

эмбеддингами

Чем?

До qdrant пока не добрался. Его можно поставить без Докера? Например, через cargo?

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

Чем?

Эмбеддинг - векторное представление какой-либо сущности. Главное, так подобрать это представление, чтобы векторы для схожих картинок были близкими.

qdrant - это просто движок для хранения, обновление и поиска векторов. Ищет дистанцию, по-моему, по скалярному произведению и ещё паре методов. Т.е. как векторы получать надо самому придумать.

Ставится в докер, кубер или из сырцов (cargo build).

vvn_black ★★★★★
()
Последнее исправление: vvn_black (всего исправлений: 1)
Ответ на: комментарий от question4

Radeon HD 6950

Хммм, ну opencl впринципе может чуток что-то ускорить… можно поискать готовые реализации поиска ближайших соседей на pyopencl (можно и самому закодить, но это надо знать opencl), можно закодить (или поискать готовое) на theano (libgpuarray умело в opencl, но фиг знает сколько гемора щас будет это все завести) или chainer+clpy (хз, говорят что работает, но не факт).

ei-grad ★★★★★
()
Ответ на: комментарий от question4

Эмбеддингом например можно назвать твои 20x20 массивчики, только обычно к ним делают .flatten(), будет у тебя эмбеддинг размерности 400. //edit: а, там у тебя RGB еще… ну 1200 :)

ei-grad ★★★★★
()
Последнее исправление: ei-grad (всего исправлений: 1)
Ответ на: комментарий от question4

Суть та же что @vvn_black предлагает, сначала надо сжать в вектора одинаковой длины каким-нибудь более-менее одинаковым способом :)

ei-grad ★★★★★
()

У тебя идея не очень правильная. Я не по обработке картинок специалист, а по обработке текстов, но идея у тебя должна быть похожая, с идеей алгоритма расстояния Дамерау — Левенштейна, с учётом теории цвета и того что ты работаешь с многомерными матрицами, где геометрия двумерная важна. Это если ты что-то крутое и серьёзное делать решил. Если быстро и на тяп-ляп, то пляши от перцептивного хеша. Если всё очень сложно и искать надо похожие по каким-то элементам или признакам картинки, то нейронки.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 1)
Ответ на: комментарий от ei-grad

Вместо попарного перебора можно собрать индекс через nmslib, и искать потом ближайших соседей по нему.

Спасибо, так гораздо быстрее. Для 20 000 картинок индекс построился за десятки секунд.

Теперь нужно придумать, чем лучше заменить 1200 байт RGB, но на эксперименты пока нет времени.

question4 ★★★★★
() автор топика
Ответ на: комментарий от peregrine

с учётом теории цвета

convert(Mode='L') преобразует в оттенки серого с учётом человеческого восприятия, примерно как в JPEG. Начну с этого…

перцептивного хеша

Картинка 20х20 и была примером такого хеша из какой-то обзорно-популярной статьи. Оказалось, что даже это работает, и главной проблемой становится квадратичное сравнение.

Быстрое гугление нашло pHash. Что ещё попробовать помимо него?

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

pHash

Ага, питоновский ImageHash его включает.

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

на эксперименты пока нет времени

взять энкодер от stable diffusion и навесить сверху globalaveragepooling, отличный пример чего можно дешево сделать в качестве эксперимента когда на эксперименты появится время :-)

правда оно не одинаковые картинки будет искать, а картинки на которых изображено что-то что описывается похожими словами … или нет :)

ei-grad ★★★★★
()
Последнее исправление: ei-grad (всего исправлений: 1)

нанимаешь индуса и он тебе похожие фотки находит - можешь использовать сервис яндекс.толока для такого.

xmikex ★★★★
()
Ответ на: комментарий от xmikex

Недостаточно смешно. Повтори попытку.

question4 ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.