Одинаковые фото под разными именами: как искать кандидатов на дубли датасета
В датасете товаров два одинаковых снимка называются photo_17.jpg и supplier_new_884.png. Один немного пересжат и стал светлее. По имени файлы разные, контрольные суммы тоже. При случайном разбиении они попали в обучение и проверку. Модель видит почти знакомое изображение в отложенной части, и отчёт выглядит лучше, чем работа на новых фотографиях. Поиск только точных совпадений байтов такой случай пропускает.
Для поиска похожих изображений можно вычислять короткое описание внешнего вида. Его часто называют перцептивным хешем. Сначала приводят снимок к небольшому общему размеру и цветовому представлению, затем кодируют грубую структуру. Малое расстояние между кодами указывает на кандидатов для проверки. Оно не доказывает, что на фотографиях один и тот же товар или что одну из них можно удалить.
Начнём с простого average hash
Возьмём серое изображение 8×8 и сравним каждый пиксель со средним значением. Получится 64 бита: светлее среднего или нет. Расстояние Хэмминга равно числу отличающихся битов. Этот метод сохраняет некоторые изменения яркости, но теряет много информации. Простота полезна для понимания механизма и первичного поиска, а порог нужно выбирать на реальных парах вашего источника.
import numpy as np
from PIL import Image
from hashlib import sha256
def ahash(image):
small = np.asarray(image.convert("L").resize((8, 8), Image.Resampling.LANCZOS), dtype=float)
return small > small.mean()
def distance(a, b):
return int(np.count_nonzero(ahash(a) != ahash(b)))
base = np.tile(np.arange(8, dtype=np.uint8)*20, (8, 1))
a = Image.fromarray(base)
b = Image.fromarray(base+10)
c = Image.fromarray(np.fliplr(base).copy())
print("bytes_equal", sha256(a.tobytes()).digest() == sha256(b.tobytes()).digest())
print("A-B", distance(a, b), "A-C", distance(a, c))
assert a.tobytes() != b.tobytes()
assert distance(a, b) == 0 and distance(a, c) == 64
flat1 = Image.fromarray(np.full((8, 8), 20, dtype=np.uint8))
flat2 = Image.fromarray(np.full((8, 8), 220, dtype=np.uint8))
print("flat_collision", distance(flat1, flat2))
assert distance(flat1, flat2) == 0A и B отличаются по пикселям, но дают одинаковый код после равномерного изменения яркости. Зеркально изменённый градиент даёт 64 различающихся бита. При этом два однотонных изображения разной яркости тоже совпали по хешу. Последний случай показывает реальную коллизию представления: одинаковый код не равен одинаковому содержимому.
Как превратить кандидатов в решение
Сначала найдите точные дубли байтов, затем похожие пары по выбранному описанию. Для кандидатов сформируйте контактный лист с именами, источниками и метками класса. Проверьте, совпадает ли сцена, отличается ли ракурс и не скрывает ли один снимок важную деталь дефекта. Два разных товара на одинаковом фоне могут иметь очень близкое грубое описание. Автоматическое удаление таких пар уменьшит разнообразие данных.
Для большого набора не обязательно сравнивать каждую пару полных изображений. Сохраните вычисленные коды и ищите соседей по расстоянию в подходящем индексе либо группируйте коды для первичного отбора. Кандидатов всё равно нужно проверять более точным сравнением или человеком. Если важны повороты, обрезки и значительные изменения перспективы, average hash может пропускать похожие сцены; потребуется другое представление и отдельная оценка его ошибок.
Группы похожих снимков и разбиение данных
Подтверждённые дубли объединяйте в группы и назначайте одну часть выборки всей группе. Но связь «расстояние меньше порога» не означает, что любые два снимка цепочки одинаковы. Один промежуточный кадр может соединить разные сцены. Проверяйте крупные и неоднородные группы, особенно если они построены как связные компоненты графа похожести. Сохраните основание каждого объединения.
У повторных фотографий могут различаться метки. Это полезный сигнал для проверки разметки, а не повод выбрать метку большинства автоматически. Возможно, одна версия обрезана так, что дефект исчез, либо метка относится к другому уровню объекта. Сравнивайте полный исходник и правило аннотации. После очистки пересоберите разбиение и повторно измерьте качество на действительно независимой части.
В отчёте сохраните число точных дублей, кандидатов, подтверждённых групп и спорных пар. Не заменяйте их одним процентом удалённых файлов. Такой след позволит повторить подготовку датасета и объяснить изменение метрики после очистки. Исходные файлы лучше переносить в карантин с манифестом, чтобы решение можно было пересмотреть.
Порог похожести полезно проверять на двух наборах пар: известных дублях и похожих, но разных сценах. Первый показывает пропуски, второй — ложные объединения. Выбор порога по одним подтверждённым дублям не оценивает риск удалить важные независимые фотографии.
В курсе вы готовите и проверяете CV-датасеты, оцениваете модели и собираете воспроизводимый проект. Следующая задача — встроить поиск дублей и групповое разбиение в подготовку данных, затем сравнить качество на независимых изображениях с сохранённым отчётом проверки. Откройте программу платного курса «Computer Vision Engineer: от детекции и Doc-AI до продакшна».
Продолжить на практике
- Computer Vision Engineer: от детекции и Doc-AI до продакшна ↗
Практический курс по Computer Vision. Соберёте продакшн-сервис: датасет → обучение (YOLO/RT-DETR, SAM), трекинг, OCR/Doc-AI, веб-приложение (UI+API) и мониторинг. Экспорт в ONNX/TensorRT/OpenVINO, батчинг/квантование под CPU/GPU. Плюс профессиональные блоки: mAP/IoU/Dice и калибровка доверия, QA разметки и аугментации, p95/стоимость, дрейф данных и алерты, guardrails (NMS/правила), A/B-тесты, Triton/gRPC/FastAPI.
