IoU сегментации: как пустые маски и усреднение меняют результат
Две реализации оценки сегментации показывают разные IoU на одних масках. В одной системе качество выглядит приемлемым, в другой — почти нулевым. Особенно заметна разница на контроле дефектов: большинство кадров чистые, а повреждение занимает небольшую часть изображения. Прежде чем искать ошибку в весах, стоит восстановить правило, по которому отдельные маски превращаются в итоговое число.
Для одного класса IoU — число пикселей, одновременно отмеченных предсказанием и разметкой, делённое на число пикселей, отмеченных хотя бы в одной маске. Здесь считаем только класс «дефект». Пиксели фона не приносят ему правильных ответов: совпадение двух пустых изображений не создаёт пересечения переднего класса.
Что делать с двумя пустыми масками
Если разметка и предсказание пусты, пересечение и объединение равны нулю. Формула даёт 0/0. Дальше начинается соглашение оценщика: исключить такой случай, назначить ему 1 или сохранить специальное значение отсутствия. Эти правила дают разные средние. Их необходимо выбрать до сравнения моделей и записать в отчёте.
Пустая разметка и непустое предсказание — другой случай. Объединение положительно, пересечение нулевое, IoU равен нулю. Исключать все чистые изображения по условию «в разметке ничего нет» нельзя: тогда пропадёт часть ложных тревог. В примере исключаем только случаи, где пусты обе маски.
Среднее изображений и сумма пикселей
Есть два естественных способа собрать результат по набору. Можно вычислить IoU для каждого кадра и усреднить. Тогда маленький и большой дефект получают одинаковый вес как отдельные изображения. Можно сложить пересечения и объединения всех кадров и разделить суммы. Тогда вклад изображения зависит от размера объединения его масок.
На четырёх маленьких масках различие видно без модели и файлов. Первый кадр чистый с обеих сторон. На втором пропущен дефект площадью четыре пикселя. На третьем точно найден один пиксель. На четвёртом предсказан лишний пиксель на чистом фоне.
import numpy as np
# Четыре изображения 2x2, один передний класс «дефект».
target = np.zeros((4, 2, 2), dtype=bool)
target[1] = True # крупный дефект пропущен
target[2, 0, 0] = True # маленький найден точно
pred = np.zeros_like(target)
pred[2, 0, 0] = True
pred[3, 1, 1] = True # ложный дефект на чистом кадре
def summarize(pred, target):
intersection = np.logical_and(pred, target).sum(axis=(1, 2))
union = np.logical_or(pred, target).sum(axis=(1, 2))
per_image = np.divide(intersection, union,
out=np.full(union.shape, np.nan), where=union > 0)
global_iou = float(intersection.sum() / union.sum()) if union.sum() else None
mean_nonempty = float(np.nanmean(per_image)) if np.any(union > 0) else None
mean_empty_as_one = float(np.where(union == 0, 1.0, per_image).mean())
return global_iou, mean_nonempty, mean_empty_as_one
for name, mask in [("Модель", pred), ("Всегда пусто", np.zeros_like(pred))]:
global_iou, mean_nonempty, mean_one = summarize(mask, target)
print(name, f"global={global_iou:.3f}",
f"mean_nonempty={mean_nonempty:.3f}",
f"mean_empty_as_one={mean_one:.3f}")
np.testing.assert_allclose(summarize(pred, target), (1/6, 1/3, 0.5))
assert summarize(np.zeros_like(pred), target) == (0.0, 0.0, 0.5)
assert summarize(np.zeros((1, 2, 2), bool), np.zeros((1, 2, 2), bool)) == (None, None, 1.0)
Для модели получаются три разных числа: общий foreground IoU — 0.167, среднее по изображениям с ненулевым объединением — 0.333, среднее с оценкой 1 для двух пустых масок — 0.500. Все расчёты используют одни и те же данные. Различается ответ на вопрос, чему дать одинаковый вес и что считать успехом на полностью пустом кадре.
Особенно показателен вариант «всегда пусто». Он пропускает оба настоящих дефекта, поэтому общий IoU и среднее по непустым объединениям равны нулю. Но среднее с единицей за два чистых кадра снова равно 0.500. Если на производстве чистых кадров гораздо больше, этот показатель может скрыть серьёзную проблему с обнаружением дефектов.
Как подписать метрику в отчёте
Не оставляйте рядом с числом только слово mIoU. Укажите, какие классы включены, усредняется ли результат по классам или изображениям, суммируются ли пиксельные счётчики по датасету и что происходит при нулевом объединении. Среднее по классам — ещё одна отдельная операция: её нельзя подменить средним по кадрам.
У готовых библиотек встречаются специальные соглашения. Например, TorchMetrics MeanIoU возвращает -1 для класса, полностью отсутствующего в предсказании и разметке. Если такую величину передать дальше в обычное среднее без учёта контракта, получится уже другая метрика. Приведённый код явно использует NaN для отсутствующего объединения внутри собственного расчёта.
Для приёмки контроля дефектов добавьте отдельный отчёт по кадрам с повреждениями и частоту ложных тревог на чистых деталях. Это позволит связать геометрию маски с последствиями ошибки. Порог бинаризации вероятностей также фиксируйте: изменение порога меняет маски предсказания и сравниваемые числа.
В платном курсе Computer Vision Engineer: от детекции и Doc-AI до продакшна вы разберёте метрики, цену ошибки, обучение сегментации на дефектах и постпроцесс масок. Затем сможете связать оценку качества с мониторингом работающего сервиса. Откройте программу курса и начните с пробного урока «IoU: как сравнить две рамки» — геометрия пересечения пригодится и при работе с масками.
