7.13. Немаксимальне пригнічення¶
Мережа виявлення зазвичай створює кілька накладних кандидатних рамок навколо одного реального об’єкта: кожен якір поблизу об’єкта спрацьовує з подібною оцінкою, і постпроцесор бачить їх усі. Немаксимальне пригнічення (NMS) — це крок, який перетворює цей кластер на одну рамку.
Алгоритм короткий: відсортувати кандидатні рамки за оцінкою, взяти ту, що має найвищу оцінку, пригнітити всі інші рамки, що перекриваються з нею більше обраного порогу, потім взяти наступну найвищу з тих, що залишилися, і повторити. Метрика перекриття — перетин-до-об’єднання (IoU) – спільна площа двох рамок, поділена на їхню сумарну площу, значення між 0 (без перекриття) і 1 (ідентичні рамки). Аргумент конструктора nms_threshold у кожному вбудованому постпроцесорі — це поріг, вище якого рамки вважаються дублікатами вже збереженої рамки.
NMS згортає кластер накладних виявлень до того, що має найвищу оцінку.¶
7.13.1. Soft-NMS¶
Вбудований клас ml.utils.NMS реалізує Soft-NMS — вдосконалення, яке знижує оцінку накладної рамки на величину, що залежить від ступеня перекриття, а не відкидає рамку повністю. Якщо знижена оцінка падає нижче порогу, рамку відкидають; інакше вона залишається зі зниженою оцінкою та бере участь у наступному раунді.
Параметр nms_sigma контролює агресивність спаду. При малому nms_sigma (стандартне значення 0.1) спад крутий: сильно накладна рамка отримує оцінку, наближену до нуля, і Soft-NMS зводиться до класичного NMS. При більшому nms_sigma спад плавний, і накладні рамки різних об’єктів частіше виживають — це важливо, коли реальні об’єкти одного класу справді перекриваються (натовп облич, кластер долонь).
Встановлення nms_sigma у значення <= 0 повністю вимикає спад: накладні рамки проходять з оригінальними оцінками, і лише поріг оцінки їх фільтрує.
7.13.2. Пряме створення¶
Кожен вбудований постпроцесор створює новий NMS на кожен інференс, додає до нього кожного кандидата та викликає get_bounding_boxes() наприкінці. Власний постпроцесор слідує тому самому шаблону:
from ml.utils import NMS
iw = model.input_shape[0][2]
ih = model.input_shape[0][1]
nms = NMS(iw, ih, inputs[0].roi)
for box, score, class_idx in candidates:
nms.add_bounding_box(box.xmin, box.ymin,
box.xmax, box.ymax,
score, class_idx)
result = nms.get_bounding_boxes(threshold=nms_threshold,
sigma=nms_sigma)
Конструктор приймає ширину і висоту вхідних даних мережі в пікселях та ROI оригінального зображення, проти якого запускалася модель; add_bounding_box() приймає координати рамки у просторі пікселів цього вхідного зображення, а get_bounding_boxes() перепроєктовує відібрані рамки назад у координати зображення з використанням ROI. Перепроєктування автоматично враховує розтяжку нормалізації — той самий ROI, який бачив предиктор, використовується для зворотного проєктування рамок — тому повернені рамки готові до малювання на захопленому кадрі.
Форма повернутого значення — список списків за класами, індексованих за label_index, переданим до add_bounding_box. Порожні списки класів зберігаються, щоб індекс відповідав індексу класу моделі; enumerate(result) обходить класи разом з їхніми виявленнями.