Урок курса
IoU: как сравнить две рамки
Computer Vision Engineer: от детекции и Doc-AI до продакшнаРамка совпала — это сколько?
Детектор отдаёт список прямоугольников. Разметка — тоже список прямоугольников. Чтобы сказать, работает модель или нет, эти два списка надо сравнить, и вот тут выясняется неприятное: ни одна пара рамок не совпадает пиксель в пиксель. Вот настоящий пример с уличного кадра — размеченная машина и рамка, которую на ней поставил детектор:
В разметке эта машина записана как [488, 119, 772, 294], детектор выдал [511, 118, 770, 295]. Глазами — почти одно и то же. В программе глаз нет, есть четыре числа, и различие в них есть: слева 23 пикселя, по остальным граням — один-два. Вопрос «совпало или нет» здесь не имеет ответа, пока мы не договоримся, насколько должно совпасть.
Считают это через IoU (intersection over union) — базовую меру совпадения рамок, на которой стоят и метрики детекции, и отсев дублей. Берём площадь, которую рамки делят между собой, и делим её на площадь, которую они занимают вдвоём:
Слева — то, что идёт в числитель, справа — то, что в знаменатель. Числитель не бывает больше знаменателя, поэтому IoU всегда лежит между 0 и 1: единица получается только при полном совпадении, ноль — когда общей площади нет вовсе. Порядок рамок роли не играет: от перестановки IoU не меняется.
Главное свойство — деление убирает масштаб. Машина занимает на кадре 50 тысяч пикселей, лицо в толпе — пятьсот, и абсолютное расхождение в двадцать пикселей для них значит совершенно разное. IoU от этой разницы не зависит: 0.9 у машины и 0.9 у лица описывают одинаково точное попадание. Поэтому одним порогом можно мерить и то, и другое, а метрики детекции считаются по разноразмерным объектам без отдельных поправок.
У этой же арифметики есть слепое пятно, о котором стоит знать сразу. Если рамки не пересекаются, IoU равен нулю — и ноль не различает «промахнулся на пиксель» и «попал в другой угол кадра». Две рамки, разошедшиеся на волосок, и две рамки на разных концах улицы дают одно и то же число.
У пары с картинки пересечение занимает почти всю площадь обеих рамок, и IoU выходит 0.90. Число получено не на глаз: рамка — это четыре координаты, а площади считаются вычитанием, так что весь расчёт помещается в десяток строк.
Кадр — Segway gliders (Albert Lugosi), CC BY 2.0; разметка — Open Images V7 (Google), CC BY 4.0; рамки нанесены поверх.
Одна рамка, три способа её записать
Прежде чем делить площади, надо договориться о двух вещах: в каких координатах задан кадр и какими числами записана рамка. Обе договорённости выглядят формальностью ровно до первого расхождения — а расходятся они постоянно, и это первая причина, по которой у людей «не сходится IoU» на совершенно правильном коде.
Начало координат стоит в левом верхнем углу кадра, ось X идёт вправо, а ось Y — вниз: так пиксели лежат в памяти, строка за строкой сверху вниз. Отсюда непривычное после школьной геометрии — «выше» на кадре означает меньшее число, а нижняя граница объекта — большее.
Саму рамку в этих осях записывают по-разному, и в файлах разметки встречаются все варианты.
Слева xyxy — два противоположных угла. Справа xywh — центр и размеры. Одна и та же рамка, четыре числа в обоих случаях, но числа разные. В torchvision у функции box_convert эти форматы названы честнее, чем в разговоре: xyxy — углы, cxcywh — центр с размерами, а имя xywh закреплено за третьим вариантом, левым верхним углом с размерами. Именно так рамка записана в разметке COCO.
Отсюда ловушка, на которой спотыкаются регулярно: под словом «xywh» в двух соседних библиотеках лежат два разных формата — у COCO это угол с размерами, у YOLO центр с размерами. Перепутать их легко, а на кадре ошибка выглядит не как ошибка: все рамки просто чуть съезжают вниз-вправо на половину своего размера.
Отдельно — нормировка. YOLO хранит разметку в долях кадра, а не в пикселях: строка файла выглядит так.
1 0.615020 0.359036 0.277402 0.303614
Это та самая машина с прошлых шагов: класс 1, центр и размеры в долях. Доли удобны тем, что не ломаются при изменении размера кадра, — но перед сравнением их надо развернуть в пиксели того кадра, с которым работаете:
def yolo_to_xyxy(cx, cy, w, h, width, height):
cx, w = cx * width, w * width
cy, h = cy * height, h * height
return [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2]
print(yolo_to_xyxy(0.615020, 0.359036, 0.277402, 0.303614, 1024, 576))
# [487.75, 119.36, 771.81, 294.24]
Получилось [488, 119, 772, 294] с точностью до округления — та самая рамка, которую мы делили на предсказание детектора. Обратный перевод так же короток: центр — полусумма углов, размеры — их разность.
Из трёх записей нам дальше нужна одна. Считать пересечение удобнее всего по углам: общая часть двух прямоугольников находится сравнением их краёв, без единого пересчёта, — поэтому рамка по умолчанию хранится как xyxy в пикселях, а остальное к ней приводится.
Отсюда правило, которым лучше пользоваться механически. Перед любым сравнением приведите оба набора к xyxy в пикселях и проверьте перевод на одной рамке — нарисуйте её поверх кадра. Тридцать секунд на картинку экономят вечер на вопрос «почему модель не учится»: рамки, съехавшие на полразмера, дают ровно ту же картину, что плохо подобранная скорость обучения.
Отличить нормированный формат от пиксельного помогает грубая примета: все четыре числа меньше единицы — доли, есть среди них 488 — пиксели.
Четыре максимума и минимума
Обе рамки приведены к углам в пикселях, и дальше нужна только арифметика. Пересечение двух прямоугольников — тоже прямоугольник, а его углы находятся без всякой геометрии, сравнением координат:
Рамки на схеме названы a и b — теми же именами, что в коде ниже. Левый край общей части — тот, что правее, то есть максимум из двух левых краёв. Верхний — тот, что ниже, снова максимум. Правый и нижний, наоборот, берутся минимумом. Четыре сравнения — и прямоугольник найден.
Одна ловушка тут всё-таки есть. Если рамки не пересекаются, формула всё равно доходит до конца: «правый край левее левого», и ширина получается отрицательной. Перемножив две отрицательные стороны, вы получите положительную площадь пересечения у рамок, которые вообще не касаются друг друга. Поэтому стороны обрезаются нулём до умножения — это и есть то место, где чаще всего ломается своя реализация.
Дальше объединение. Складывать площади нельзя: общая часть войдёт в сумму дважды, поэтому её вычитают один раз.
Здесь — площадь пересечения, и — площади рамок. Весь расчёт целиком:
def iou(a, b):
ax1, ay1, ax2, ay2 = a
bx1, by1, bx2, by2 = b
ix1, iy1 = max(ax1, bx1), max(ay1, by1)
ix2, iy2 = min(ax2, bx2), min(ay2, by2)
inter = max(0.0, ix2 - ix1) * max(0.0, iy2 - iy1)
area_a = (ax2 - ax1) * (ay2 - ay1)
area_b = (bx2 - bx1) * (by2 - by1)
return inter / (area_a + area_b - inter)
Прогоним его на той самой паре рамок с уличного кадра и заодно сверим с двумя библиотечными реализациями — box_iou из torchvision и метрикой из torchmetrics:
Пересечение — прямоугольник [511, 119, 770, 294] размером 259 на 175, то есть 45 325 пикселей. Рамки занимают 49 700 и 45 843, вместе без двойного счёта — 50 218. Деление даёт 0.9026. Три реализации сошлись до четвёртого знака.
Сверка здесь не формальность, а рабочий приём. У IoU нет промежуточного результата, который можно проверить глазами: число 0.9 выглядит правдоподобно и при забытом обрезании нулём, и при перепутанных углах, и при сложении площадей без вычитания пересечения. Своя реализация считается рабочей, когда она совпала с признанной на наборе значений.
Полезно взять в набор проверки не только удачную пару, но и три вырожденные: рамки, не касающиеся друг друга (ждём ровно 0), одинаковые рамки (ровно 1) и вложенные одна в другую, когда пересечение равно меньшей площади. Именно на них вылезают ошибки, которые удачная пара не показывает.
Что означает «порог 0.5»
IoU — число, а решение нужно бинарное: рамка засчитана или нет. Значит, где-то надо провести черту. Чаще всего её проводят на 0.5, хотя ничего обязательного в этом числе нет. Проще всего понять, много это или мало, увидев одну и ту же пару рамок при разных значениях.
Мы взяли размеченную машину с уличного кадра — 294 на 189 пикселей — и сдвигали её копию вдоль улицы, пока IoU не выходил на нужную ступень. Зелёная рамка остаётся на месте, синяя сдвигается:
Подобрать сдвиг можно не перебором. Если рамка ширины едет только по горизонтали на расстояние , пересечение теряет полоску шириной , а объединение ровно на столько же прибавляет, и всё сводится к одной дроби:
Отсюда видно главное, что стоит унести из этой картинки. Порог 0.5 — это рамка, съехавшая на треть своей ширины (подставьте 0.5: получится , на нашей машине 98 пикселей). Не «половина совпала», как читается на слух, а треть промаха. Порог 0.9 — это 15 пикселей, почти пиксель в пиксель. А вот 0.2 — сдвиг на две трети ширины: на картинке рамка стоит уже на соседнем объекте, и засчитывать такое попаданием нельзя.
Дальше выбор порога — вопрос задачи, а не традиции. Если по рамке считают количество объектов, точность границы не важна: пустое место на полке, посчитанное с IoU 0.3, всё равно посчитано. Если по рамке вырезают кроп на распознавание номера или наводят манипулятор, 0.5 не годится — треть номера уедет за край. Строгие пороги 0.75 и 0.9 существуют ровно для таких случаев; отчёты COCO дают среднее сразу по десяти порогам от 0.50 до 0.95, чтобы не выбирать один.
Осталась оговорка про слепое пятно, о котором шла речь в начале урока. На последней ступени IoU равен нулю — и точно такой же ноль получится, если увезти рамку в другой конец кадра. Для отчёта это не мешает: там любой ноль означает «не засчитано». Но при обучении сети такая мера бесполезна: по ней не видно, в какую сторону двигать рамку.
Метрики, из которых собирают функцию потерь
Ошибку за положение рамки детектор считает прямо из метрики совпадения: чем хуже рамки сходятся, тем больше потери, а сеть подкручивает веса так, чтобы их уменьшить. С обычным IoU так не выйдет — у разошедшихся рамок ошибка одинакова, куда предсказание ни сдвинь, и подкручивать нечего. Поэтому в функцию потерь берут не сам IoU, а его надстройки — GIoU, DIoU и CIoU. Устроены все три одинаково: обычный IoU минус штраф, который остаётся осмысленным и тогда, когда рамки не пересеклись вовсе.
GIoU штрафует за пустоту. Вокруг предсказания и разметки описывают одну общую минимальную рамку и смотрят, какая её часть не занята ни той, ни другой:
Здесь A и B — сами рамки, C — та самая объемлющая, а вертикальные чёрточки означают площадь: из площади объемлющей рамки вычли площадь, занятую рамками, — это и есть пустота. Пока рамки далеко друг от друга, пустой оказывается почти вся C, штраф близок к единице, и значение уходит в минус: GIoU меняется от −1 до 1. Сближаются рамки — пустота тает. Слабое место видно сразу: если предсказание целиком внутри размеченной рамки, C совпадает с внешней, пустота одна и та же при любом положении внутренней, и GIoU снова становится обычным IoU.
DIoU считает штраф не по площади, а по расстоянию между центрами рамок:
Здесь ρ — расстояние между центрами, а c — диагональ всё той же объемлющей рамки. Делят на диагональ, чтобы штраф не зависел от размера объекта. Куда двигать предсказание, отсюда видно напрямую, в том числе у вложенных рамок: центры не совпали — штраф есть. За счёт этого обучение доходит до того же качества за меньшее число шагов, чем на GIoU.
CIoU — это DIoU плюс третье слагаемое, за форму. Центры могут сойтись, площадь совпасть, а рамка при этом быть вытянута не в ту сторону: широкая вместо высокой. Для DIoU это положение уже идеальное, штрафовать ему нечем:
Добавка αv и ловит форму. Множитель v сравнивает отношение ширины к высоте у разметки и у предсказания, а α приглушает эту добавку, пока рамки далеко друг от друга — сперва нужно свести их вместе, а форму подгонять потом:
Именно CIoU стоит по умолчанию в обучении детекторов YOLO — строка box_loss в логе считается по ней.
Формулы эти запоминать незачем: выбор между ними — настройка обучения, и в готовых конфигах он уже сделан. В отчётах и в порогах стоит обычный IoU.
Кадр — Segway gliders (Albert Lugosi), CC BY 2.0; разметка — Open Images V7 (Google), CC BY 4.0; рамки нанесены поверх.
TP, FP, FN и два числа, которые из них считают
Порог превращает IoU в решение по одной паре. Но детектор выдаёт не пару, а список, и в разметке список свой. Разложить один на другой — отдельная процедура, и она короче, чем кажется.
Находки сортируются по убыванию уверенности. Каждая по очереди ищет среди ещё свободных размеченных объектов своего класса тот, с которым у неё наибольший IoU. Если он дотягивает до порога — это TP (true positive), попадание, и занятый объект выбывает из списка кандидатов. Если нет — FP (false positive), ложное срабатывание. Размеченные объекты, которых так никто и не занял, — FN (false negative), пропуски.
Буквы читаются с конца: positive или negative — что сказала модель (рамка есть или рамки нет), true или false — угадала она или нет. Отсюда и названия: FP — модель нашла то, чего нет, FN — не нашла того, что есть.
Важна деталь про «выбывает». Если детектор поставил на одну машину две рамки, вторая не найдёт свободного кандидата и станет FP, даже когда лежит на объекте идеально. Так и задумано: дубликаты — это ошибка, и в метрике они засчитываются как ошибка.
Вот тот же уличный кадр, прогнанный YOLO11n. В разметке шесть объектов: три человека и три машины. Сверху — все находки при пороге уверенности 0.25, снизу — только уверенные, от 0.80:
Сверху детектор не пропустил ничего — все шесть размеченных объектов найдены. Но вместе с ними прибавилось шесть лишних рамок: обломки машин у левого края, куски одного автомобиля, посчитанные за отдельные. У лучшего из этих шести IoU с ближайшим размеченным объектом всего 0.34, у худшего 0.02.
Снизу список чистый: четыре рамки, все четыре — попадания. Ценой того, что две машины исчезли вовсе.
Два числа, которыми это описывают, устроены как две дроби с одним и тем же числителем:
Precision отвечает на вопрос «сколько из того, что модель принесла, оказалось правдой». Знаменатель — всё, что модель сказала. Recall отвечает на другой вопрос — «сколько из того, что было, модель нашла», и в знаменателе у него всё, что размечено. Считаются они по картинке в уме:
| Порог уверенности | Находок | TP | FP | FN | precision | recall |
|---|---|---|---|---|---|---|
| 0.25 | 12 | 6 | 6 | 0 | 6/12 = 0.50 | 6/6 = 1.00 |
| 0.80 | 4 | 4 | 0 | 2 | 4/4 = 1.00 | 4/6 = 0.67 |
Модель, кадр и веса одни и те же. Поменялось только число, отсекающее неуверенные находки, — и precision поднялся с 0.50 до единицы, а recall упал с единицы до 0.67. Отсюда правило, которое стоит применять к любому отчёту: precision без recall ничего не значит — как и оба вместе без указанного порога уверенности. «У нас precision 1.0» — это, скорее всего, второй случай: всё, что ниже порога уверенности, в отчёт просто не попало.
Ещё одно, что бросается в глаза в таблице: TN (true negative), четвёртой клетки привычной матрицы ошибок, здесь нет. Правильно не поставленных рамок бесконечно много — на кадре бесконечно много мест, где машины нет и рамка правильно не поставлена. Поэтому в детекции остаются три исхода из четырёх.
Сопоставление — это матрица
Процедура выше требует IoU каждой находки с каждым размеченным объектом. Это матрица: строка — размеченная рамка, столбец — предсказание. Считать её двойным циклом можно, но не нужно — та же арифметика делается сразу для всех пар, если добавить наборам по одной оси:
import numpy as np
def iou_matrix(a, b):
ix1 = np.maximum(a[:, None, 0], b[None, :, 0])
iy1 = np.maximum(a[:, None, 1], b[None, :, 1])
ix2 = np.minimum(a[:, None, 2], b[None, :, 2])
iy2 = np.minimum(a[:, None, 3], b[None, :, 3])
inter = np.clip(ix2 - ix1, 0, None) * np.clip(iy2 - iy1, 0, None)
area_a = (a[:, 2] - a[:, 0]) * (a[:, 3] - a[:, 1])
area_b = (b[:, 2] - b[:, 0]) * (b[:, 3] - b[:, 1])
return inter / (area_a[:, None] + area_b[None, :] - inter)
Вся хитрость в двух срезах. a[:, None, 0] ставит левые края первого набора столбцом, b[None, :, 0] — второго строкой, и np.maximum сравнивает каждую с каждой, возвращая матрицу нужного размера. Тело функции слово в слово повторяет расчёт для пары, только max и min стали np.maximum и np.minimum, а обрезание нулём — np.clip.
На нашем кадре матрица получилась 6 на 12: шесть размеченных объектов, двенадцать находок. Максимум в строке — тот самый лучший кандидат, который ищет процедура сопоставления. Расхождение с двойным циклом нулевое, с box_iou из torchvision — 2,4 · 10⁻⁷, то есть разница в последнем знаке float32.
Скорость на такой матрице роли не играет, а вот на тысяче рамок против тысячи — уже да: 33 миллисекунды векторно против 3073 миллисекунд циклом, в 92 раза (замер на процессоре стенда курса). Такие размеры берутся не из отчёта: та же матрица считается внутри отсева дубликатов, который детектор делает на каждом кадре, и рамок-кандидатов там на порядки больше, чем остаётся в ответе.
Кадр — Segway gliders (Albert Lugosi), CC BY 2.0; разметка — Open Images V7 (Google), CC BY 4.0; рамки нанесены поверх.
Задача 1
Детектор промахнулся ровно на 20 пикселей по каждой грани на двух объектах одного кадра: на машине размером 300 на 180 пикселей и на лице размером 30 на 20. Что покажет IoU?
Задача 2
Сопоставьте каждое название формата с тем, что означают его четыре числа.
Сопоставьте элементы
Задача 3
Разметку читают как cxcywh, а в файле на самом деле лежит формат COCO — левый верхний угол и размеры. Обе записи в пикселях. Как это будет выглядеть на кадре с нарисованными рамками?
Задача 4
Рамка разметки задана как [0, 0, 10, 10], предсказание детектора — [2, 2, 6, 8]. Обе записаны в формате xyxy в пикселях. Чему равен их IoU? Ответ округлите до двух знаков после запятой.
Задача 5
Свою реализацию IoU проверили на одной паре рамок с уличного кадра — пересечение у них занимает почти всю площадь, правильный ответ 0.90. Отметьте все ошибки, которые на такой паре дадут результат, всё ещё похожий на правду. Верных ответов несколько.
Задача 6
Рамку шириной 300 пикселей сдвинули только по горизонтали, и IoU с исходной упал ровно до 0.50. На сколько пикселей она уехала?
Задача 7
Сопоставьте каждую меру совпадения рамок с тем, чем она отличается от остальных трёх.
Сопоставьте элементы
Задача 8
У детектора подняли порог уверенности с 0.25 до 0.80. Веса, кадр, разметка и порог IoU остались прежними. Отметьте все утверждения, верные для такой перемены. Верных ответов несколько.
Задача 9
IoU двух рамок из разметки COCO
Задание
Напишите функцию iou(a, b), которая возвращает IoU двух рамок. Обе рамки приходят в том виде, в каком они лежат в файле разметки COCO: [x, y, w, h] — левый верхний угол, затем ширина и высота в пикселях. Начало координат в левом верхнем углу кадра, ось Y направлена вниз.
Считать пересечение и объединение прямо по этим четырём числам неудобно: общая часть двух прямоугольников находится сравнением их краёв, а краёв в такой записи нет. Приведите обе рамки к углам, и дальше останется арифметика — максимумы и минимумы, обрезание сторон нулём, сумма площадей за вычетом пересечения.
Отдельно два случая. Рамки могут не иметь общей площади — тогда результат ровно 0.0, а не отрицательное число и не единица. И ширина или высота в разметке может оказаться нулевой: такая рамка не ошибка входных данных, IoU с ней тоже 0.0.
Пример
iou([0, 0, 10, 10], [5, 5, 10, 10]) # 0.14285714285714285
iou([0, 0, 10, 10], [2, 2, 4, 6]) # 0.24
iou([0, 0, 10, 10], [20, 20, 10, 10]) # 0.0
iou([0, 0, 10, 0], [0, 0, 10, 0]) # 0.0
В первом примере рамки перекрываются углами: общая часть 5 на 5, объединение 175 пикселей. Во втором вторая рамка целиком лежит внутри первой.
В наборе проверки есть и пара с уличного кадра из урока: в записи COCO это [488, 119, 284, 175] и [511, 118, 259, 177]. На ней ожидается 0.9026 — то самое значение, с которым сошлись torchvision и torchmetrics.
Примечание
Координаты приходят числами int или float. Возвращайте число; печатать ничего не нужно и вызывать функцию самостоятельно тоже — тесты сделают это сами. Значения сравниваются с точностью до девятого знака, так что округлять результат не надо.
Что проверяется
Код проверок скрыт, так что вот полный список того, что в них разбирается:
- формат ответа: возвращается число;
- все четыре вызова из примера: перекрытие углами, рамка внутри рамки, отсутствие общей площади и вырожденная рамка;
- пара с уличного кадра из урока в записи COCO и рамка сама с собой;
- запись
[x, y, w, h]прочитана как углы: на таких парах ответ съезжает; - расхождение только по одной оси — без обрезания нулём вышло бы отрицательное пересечение;
- рамки, касающиеся краями;
- нулевая ширина и нулевая высота рядом с обычной рамкой;
- дробные координаты;
- рамки разного масштаба с одинаковым относительным сдвигом;
- порядок аргументов на результат не влияет;
- результат всегда лежит между нулём и единицей.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 10
Матрица IoU и поиск дублей
Задание
Детектор поставил на кадр список рамок, и часть из них наверняка описывает один и тот же объект. Найдите такие пары: напишите функцию duplicate_pairs(boxes, threshold), которая возвращает все пары рамок с IoU не меньше порога.
boxes — массив numpy формы (N, 4), рамки в формате xyxy в пикселях. Результат — список кортежей (i, j) с номерами строк, причём i < j, отсортированный по возрастанию: сначала по первому номеру, при равенстве — по второму.
В уроке матрица IoU считалась между двумя разными наборами и получалась прямоугольной. Здесь набор один, матрица выходит квадратной — и в ней появляется то, чего в прямоугольной не было. На главной диагонали стоит IoU рамки с самой собой, то есть единица, и в дубли она не годится. Каждая пара встречается в матрице дважды, выше и ниже диагонали, а в ответе должна быть один раз.
Считайте матрицу целиком, срезами и None, как в уроке: в наборе проверки есть кадр на восемьсот рамок, а это триста двадцать тысяч пар — тот размер, ради которого матрицу и собирают сразу для всех пар, а не перебором.
Пример
import numpy as np
boxes = np.array([
[0, 0, 10, 10],
[1, 0, 11, 10],
[0, 0, 10, 10],
[50, 50, 60, 60],
])
duplicate_pairs(boxes, 0.5) # [(0, 1), (0, 2), (1, 2)]
duplicate_pairs(boxes, 0.9) # [(0, 2)]
Первые три рамки стоят на одном объекте: нулевая и вторая совпадают полностью, первая сдвинута на пиксель вправо, и у неё с соседями IoU около 0.82. При пороге 0.9 остаётся только пара точных совпадений. Последняя рамка ни с чем не пересекается и в ответ не попадает.
Примечание
numpy уже импортирован как np. Массив может содержать одну рамку или не содержать ни одной — тогда пар нет и ответ пустой. Номера в кортежах — обычные int, а не numpy-скаляры: tolist() у массива индексов приводит их сам.
Что проверяется
Код проверок скрыт, так что вот полный список того, что в них разбирается:
- формат ответа: список пар
(i, j); - оба вызова из примера — один кадр при пороге 0.5 и 0.9;
- пустой массив, одна рамка и одна рамка при нулевом пороге;
- четыре одинаковые рамки: диагональ в ответ не идёт, каждая пара встречается один раз;
- разнесённые рамки и они же при нулевом пороге, когда в ответ идут все пары;
- IoU ровно на пороге засчитывается, а на сотую выше порога — уже нет;
- разреженный набор: пары идут по возрастанию сначала первого номера, потом второго;
- кадр на восемьсот рамок — триста двадцать тысяч пар за отведённое время.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 11
TP, FP и FN на одном кадре
Задание
Напишите функцию match_detections(gt, preds, scores, iou_threshold), которая раскладывает находки детектора на попадания, ложные срабатывания и пропуски и возвращает кортеж из трёх целых чисел — (TP, FP, FN).
Аргументы: gt — список размеченных рамок, preds — список рамок детектора, scores — уверенность каждой находки (той же длины, что preds), iou_threshold — порог засчитывания. Все рамки в формате xyxy в пикселях, объекты одного класса.
Порядок разбора тот же, что в уроке. Находки перебираются по убыванию уверенности. Для очередной находки среди ещё свободных размеченных объектов ищется тот, с которым IoU наибольший. Если этот IoU не меньше порога — это TP, и объект перестаёт быть свободным. Иначе — FP. Размеченные объекты, оставшиеся свободными до конца перебора, дают FN.
Вторая рамка на уже занятом объекте свободного кандидата не найдёт и станет FP, даже если лежит на нём идеально, — дубликаты считаются ошибкой.
Пример
gt = [[0, 0, 10, 10], [20, 20, 30, 30]]
preds = [[0, 0, 10, 10], [0, 1, 10, 11], [100, 100, 110, 110]]
scores = [0.9, 0.8, 0.7]
match_detections(gt, preds, scores, 0.5) # (1, 2, 1)
Первая находка легла на первый объект точно — TP. Вторая почти повторяет её, но объект уже занят, а со вторым объектом у неё нет общей площади — FP. Третья стоит в стороне от всего — тоже FP. Второй размеченный объект никто не нашёл — FN.
Примечание
Функция iou(a, b) для пары рамок уже объявлена — писать её заново не нужно. Списки gt и preds могут быть пустыми. Возвращайте кортеж из трёх int; печатать ничего не нужно и вызывать функцию самостоятельно тоже — тесты сделают это сами.
Что проверяется
Код проверок скрыт, так что вот полный список того, что в них разбирается:
- формат ответа: кортеж из трёх целых чисел;
- пример из условия;
- три дубликата на одном объекте: попадание одно, остальные — ошибки;
- пустой кадр, пустая разметка и пустой список находок по отдельности;
- IoU ровно на пороге засчитывается, а тот же кадр с порогом выше — уже нет;
- кадр, где ни одна находка до порога не дотягивает;
- перебор идёт по убыванию уверенности, а не в порядке списка;
- кандидат выбирается по наибольшему IoU, а не по первому подходящему в разметке;
- несколько объектов кадра разбираются независимо друг от друга.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
