7.13. Tłumienie niemaksymalne (NMS)

Sieć wykrywająca obiekty zazwyczaj generuje kilka nakładających się na siebie kandydujących ramek wokół tego samego rzeczywistego obiektu: każda kotwica w pobliżu obiektu uruchamia się z podobnym wynikiem, a post-processor widzi je wszystkie. Tłumienie niemaksymalne (NMS) to krok, który przekształca taki klaster w jedną ramkę.

Algorytm jest krótki: posortuj kandydujące ramki według wyniku, weź tę o najwyższym wyniku, stłum każdą inną ramkę, która nakłada się na nią powyżej wybranego progu, następnie weź kolejną o najwyższym wyniku spośród pozostałych i powtórz. Miarą nakładania się jest przecięcie podzielone przez sumę (IoU) – wspólny obszar dwóch ramek podzielony przez ich łączny obszar, wartość pomiędzy 0 (brak nakładania) a 1 (identyczne ramki). Argument konstruktora nms_threshold w każdym dostarczanym post-processorze to wartość graniczna, powyżej której ramki są traktowane jako duplikaty już zachowanej ramki.

Po lewej trzy nakładające się ramki ograniczające wokół jednego obiektu, oznaczone wynikami 0.92, 0.83 i 0.71. Strzałka wskazuje w prawo, gdzie pozostaje jedna ramka, która przetrwała, z wynikiem 0.92. Dwie ramki o niższych wynikach zostały stłumione przez ramkę o wyższym wyniku, ponieważ ich przecięcie podzielone przez sumę z nią przekracza próg.

NMS redukuje klaster nakładających się wykryć do tego o najwyższym wyniku.

7.13.1. Soft-NMS

Dostarczana klasa ml.utils.NMS implementuje Soft-NMS, udoskonalenie, które zmniejsza wynik nakładającej się ramki o wartość zależną od tego, jak bardzo się ona nakłada, zamiast całkowicie ją odrzucać. Jeśli obniżony wynik spadnie poniżej progu, ramka jest odrzucana; w przeciwnym razie przetrwa ze zredukowanym wynikiem i konkuruje w następnej rundzie.

Parametr nms_sigma kontroluje, jak agresywny jest spadek. Przy małym nms_sigma (dostarczana domyślna wartość 0.1) spadek jest stromy: silnie nakładająca się ramka ma wynik sprowadzany niemal do zera, a Soft-NMS redukuje się do klasycznego NMS. Przy większym nms_sigma spadek jest łagodny, a nakładające się ramki różnych obiektów częściej przetrwają, co ma znaczenie, gdy rzeczywiste obiekty tej samej klasy faktycznie się nakładają (tłum twarzy, skupisko dłoni).

Ustawienie nms_sigma na <= 0 całkowicie wyłącza spadek: nakładające się ramki przechodzą z oryginalnymi wynikami, a filtruje je jedynie próg wyniku.

7.13.2. Tworzenie go bezpośrednio

Każdy dostarczany post-processor tworzy nowy obiekt NMS dla każdego wnioskowania, dodaje do niego każdego kandydata i na końcu wywołuje get_bounding_boxes(). Niestandardowy post-processor stosuje ten sam wzorzec:

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)

Konstruktor przyjmuje szerokość i wysokość wejścia sieci w pikselach oraz ROI oryginalnego obrazu, na którym uruchomiono model; add_bounding_box() przyjmuje współrzędne ramki w tej przestrzeni pikselowej wejścia sieci, a get_bounding_boxes() odwzorowuje ramki, które przetrwały, z powrotem na współrzędne obrazu przy użyciu ROI. Odwzorowanie automatycznie uwzględnia rozciągnięcie normalizacji – to samo ROI, które widział predyktor, jest używane do rzutowania ramek z powrotem – dzięki czemu zwracane ramki są gotowe do narysowania na przechwyconej ramce.

Zwracana struktura to lista list dla poszczególnych klas, indeksowana przez label_index przekazany do add_bounding_box. Puste listy klas są zachowywane, aby indeks odpowiadał indeksowi klasy modelu; enumerate(result) przechodzi przez klasy wraz z ich wykryciami.