Урок курса

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 — вниз: так пиксели лежат в памяти, строка за строкой сверху вниз. Отсюда непривычное после школьной геометрии — «выше» на кадре означает меньшее число, а нижняя граница объекта — большее.

Саму рамку в этих осях записывают по-разному, и в файлах разметки встречаются все варианты.

Оси координат с началом в левом верхнем углу: X вправо, Y вниз. Слева рамка с отмеченными углами x1 y1 и x2 y2, справа та же рамка с отмеченным центром cx cy и стрелками ширины и высоты

Слева 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:

Снимок консоли: координаты двух рамок, площадь пересечения 45325 пикселей, объединение 50218, IoU 0.9026, и три одинаковых значения от своей функции, 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 не выходил на нужную ступень. Зелёная рамка остаётся на месте, синяя сдвигается:

Шесть кадров одной улицы: зелёная рамка на машине и синяя сдвинутая копия, подписи IoU от 0.90 до 0.00

Подобрать сдвиг можно не перебором. Если рамка ширины едет только по горизонтали на расстояние , пересечение теряет полоску шириной , а объединение ровно на столько же прибавляет, и всё сводится к одной дроби:

Отсюда видно главное, что стоит унести из этой картинки. Порог 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 штрафует за пустоту. Вокруг предсказания и разметки описывают одну общую минимальную рамку и смотрят, какая её часть не занята ни той, ни другой:

Зелёная и синяя рамки не пересекаются, вокруг них пунктирная объемлющая рамка C, свободная площадь внутри неё заштрихована

Здесь A и B — сами рамки, C — та самая объемлющая, а вертикальные чёрточки означают площадь: из площади объемлющей рамки вычли площадь, занятую рамками, — это и есть пустота. Пока рамки далеко друг от друга, пустой оказывается почти вся C, штраф близок к единице, и значение уходит в минус: GIoU меняется от −1 до 1. Сближаются рамки — пустота тает. Слабое место видно сразу: если предсказание целиком внутри размеченной рамки, C совпадает с внешней, пустота одна и та же при любом положении внутренней, и GIoU снова становится обычным IoU.

DIoU считает штраф не по площади, а по расстоянию между центрами рамок:

Те же две рамки: стрелка ρ между их центрами и пунктирная диагональ c объемлющей рамки

Здесь ρ — расстояние между центрами, а 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:

Два одинаковых уличных кадра: сверху при пороге 0.25 шесть зелёных рамок TP и шесть синих FP, снизу при пороге 0.80 четыре TP и две красные рамки FN на машинах

Сверху детектор не пропустил ничего — все шесть размеченных объектов найдены. Но вместе с ними прибавилось шесть лишних рамок: обломки машин у левого края, куски одного автомобиля, посчитанные за отдельные. У лучшего из этих шести 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, нулевое расхождение с двойным циклом и 2,4e-07 с torchvision, лучшие совпадения по каждой размеченной рамке, замер 33 мс против 3073 мс

На нашем кадре матрица получилась 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] прочитана как углы: на таких парах ответ съезжает;
  • расхождение только по одной оси — без обрезания нулём вышло бы отрицательное пересечение;
  • рамки, касающиеся краями;
  • нулевая ширина и нулевая высота рядом с обычной рамкой;
  • дробные координаты;
  • рамки разного масштаба с одинаковым относительным сдвигом;
  • порядок аргументов на результат не влияет;
  • результат всегда лежит между нулём и единицей.
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 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 ровно на пороге засчитывается, а на сотую выше порога — уже нет;
  • разреженный набор: пары идут по возрастанию сначала первого номера, потом второго;
  • кадр на восемьсот рамок — триста двадцать тысяч пар за отведённое время.
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 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, а не по первому подходящему в разметке;
  • несколько объектов кадра разбираются независимо друг от друга.
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку