Два детектора нашли один объект: как правильно сопоставить предсказания с разметкой
Детектор выдал две почти одинаковые рамки одного товара. У каждой высокий IoU с разметкой. Самодельная оценка проверяет рамки независимо и засчитывает два верных обнаружения, хотя в кадре один объект. Precision получается идеальным, а количество true positive превышает число размеченных товаров. Ошибка появилась в сопоставлении результатов, ещё до вычисления самой метрики.
Для обычных отдельных объектов одна эталонная рамка может подтвердить только одно предсказание на данном пороге. После успешного сопоставления её нужно пометить занятой. Оставшаяся рамка становится лишним обнаружением, если у неё нет другого свободного подходящего объекта. Это правило не заменяет NMS: оценка должна корректно работать даже с дублирующим выходом модели.
Зададим понятное правило сопоставления
Для учебного случая берём один класс, обычную разметку без crowd и ignore, порог IoU 0,5. Предсказания обрабатываем по убыванию confidence. Каждому выбираем свободную эталонную рамку с максимальным IoU выше порога. Так оценка учитывает, какое обнаружение модель считает самым уверенным. Порядок особенно важен для дальнейшего построения precision-recall кривой.
import numpy as np
gt = np.array([[0, 0, 10, 10], [20, 0, 30, 10]], dtype=float)
pred = np.array([[0, 0, 10, 10], [1, 0, 11, 10], [20, 0, 30, 10]], dtype=float)
scores = np.array([.95, .90, .80])
def iou(a, b):
lo = np.maximum(a[:2], b[:2])
hi = np.minimum(a[2:], b[2:])
inter = np.maximum(hi-lo, 0).prod()
area_a = np.maximum(a[2:]-a[:2], 0).prod()
area_b = np.maximum(b[2:]-b[:2], 0).prod()
union = area_a + area_b - inter
return float(inter / union) if union else 0.0
used, matches = set(), []
for index in np.argsort(-scores, kind="stable"):
options = [(iou(pred[index], box), j) for j, box in enumerate(gt) if j not in used]
overlap, j = max(options, default=(0.0, -1))
ok = overlap >= .5
if ok:
used.add(j)
matches.append((int(index), int(j) if ok else None, bool(ok)))
tp = sum(ok for _, _, ok in matches)
fp, fn = len(pred)-tp, len(gt)-tp
print("matches", matches)
print("TP FP FN", tp, fp, fn, "precision", tp/len(pred), "recall", tp/len(gt))
assert (tp, fp, fn) == (2, 1, 0)Первая рамка занимает объект 0. Вторая хорошо перекрывает тот же объект, но он уже занят, поэтому это FP. Третья подтверждает объект 1. Получаем два TP, один FP и ноль FN: precision равен двум третям, recall — единице. Независимая проверка каждого предсказания дала бы три TP и скрыла лишнее обнаружение.
Чем это отличается от оптимального назначения
В трекинге и других задачах часто решают задачу общего назначения пар, например максимизируют суммарное перекрытие. Для оценки детектора с ранжированием по уверенности это может дать другой результат. Следуйте выбранному протоколу. В COCO есть также специальные правила для crowd, ignore, размеров объектов и максимального числа обнаружений. Маленький пример выше объясняет обычное сопоставление, но не воспроизводит весь COCO evaluator.
Проверяйте классы до выбора пары. Рамка «бутылка» не подтверждает объект «банка», даже если координаты совпадают. Отдельно учитывайте соглашение о координатах: здесь прямоугольники непрерывные, площадь равна разности правой и левой границ без прибавления единицы. Смешение пиксельной включительной записи и непрерывных координат меняет IoU на маленьких объектах особенно заметно.
Как встроить проверку в оценку модели
Сохраните этот мини-кейс как контроль: одна точная рамка, её дубль и второй отдельный объект. Добавьте отсутствие предсказаний, отсутствие разметки, неправильный класс и касание без площади пересечения. Сравните ожидаемые TP, FP и FN с библиотекой, которой считаете итоговый отчёт. Если различие связано со специальными флагами разметки, отразите их в тестовом входе.
При сравнении двух релизов используйте одинаковый набор кадров и один протокол оценки. Порог confidence влияет на число предсказаний, порог IoU — на допустимую геометрическую ошибку. Это разные настройки. Сначала решите, нужен ли вам фиксированный рабочий порог или кривая по confidence. Одно число precision без указания порогов не позволяет воспроизвести выбор.
Для ошибок с дублями сохраните исходные рамки до и после NMS. Тогда можно понять, модель выдаёт слишком много соседних кандидатов или постобработка пропускает их в ответ. Исправление оценки не удаляет дубли из сервиса, но перестаёт награждать модель за повтор одного объекта.
Отдельно сохраните пары matched prediction → ground truth в диагностическом отчёте. Тогда высокий FP можно разобрать по рамкам, а не угадывать его источник по одному числу. Для дубликатов будет видно, какая более уверенная рамка уже заняла объект.
При одинаковой уверенности сохраняйте воспроизводимый порядок предсказаний. Иначе повторный запуск оценки может выбирать другую рамку первой в пограничном случае.
В курсе разбираются детекция, mAP и IoU, подготовка разметки и оценка моделей. Следующий шаг — собрать корректный отчёт по детектору и связать его с рабочим порогом сервиса, чтобы изменения postprocessing были видны и в метрике, и в результате для пользователя. Откройте программу платного курса «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.
