7.14. Průchod YOLOv8#

Post-procesor YOLOv8 je dostatečně malý na to, aby se dal projít krok za krokem od vstupního tenzoru po vrácený seznam. Jedno přečtení ukazuje, co dělá každý jiný post-procesor v katalogu: levně prahovat kvantizovaná skóre, vzít to, co přežije, dekvantizovat, dekódovat geometrii, protlačit do NMS, vrátit seznamy podle tříd.

7.14.1. Výchozí tenzor#

Model YOLOv8 vypouští jediný výstupní tenzor, jehož tvar je (1, C, A) – jeden snímek, C kanálů, A predikcí kotev. První čtyři kanály jsou geometrie rámečku – cx, cy, w, h – normalizovaná na [0, 1] vstupních rozměrů sítě. Zbývajících C - 4 kanálů jsou skóre podle tříd, u trénovaného modelu už v rozsahu [0, 1]. Každá kotva je sloupcem napříč kanály.

A grid laid on its side: rows labelled cx, cy, w, h, score_0, score_1, ..., score_(N-1); columns labelled anchor 0 through anchor A-1. One column is outlined to show that a single anchor's prediction is one column down the channels.

Jedna kotva YOLOv8 je jeden sloupec napříč kanály: čtyři čísla rámečku a N skóre tříd.#

Dodávaný yolov8n_192.tflite je jednotřídový detektor osob, takže C = 5 a A je v tisících; vlastní model trénovaný na plné 80třídové sadě COCO má C = 84 při stejném A. Níže uvedené dekódování platí pro libovolný počet tříd.

7.14.2. Prahování před dekvantizací#

Nejprve levný krok. Výstupní tenzor je v kvantizovaném celočíselném dtype modelu a dekvantizace každé hodnoty by se dotkla každého prvku tenzoru s tisíci položkami – z nichž většina je pod prahem skóre a nakonec se zahodí. Post-procesor místo toho kvantizuje práh jednou a porovnává v hrubém kvantizovaném prostoru:

from ml.utils import quantize, threshold, dequantize, NMS
from ulab import numpy as np

oh, ow, oc = model.output_shape[0]
scale = model.output_scale[0]
t = quantize(model, self.threshold)

column_outputs = outputs[0].reshape((oh * ow, oc))

score_block = column_outputs[4:, :]
score_indices = threshold(score_block, t, scale,
                          find_max=True,
                          find_max_axis=0)
if not len(score_indices):
    return ()

column_outputs je tenzor přetvarovaný na (C, A), takže kanály jsou řádky a kotvy jsou sloupce. score_block je dílčí tenzor skóre tříd – vše od řádku 4 dolů. ml.utils.threshold() redukuje tento blok podél osy 0 (find_max=True, find_max_axis=0) na maximální skóre na kotvu a poté vrací indexy kotev, jejichž maximum projde kvantizovaným prahem. Celý tenzor nebyl nikdy dekvantizován; jen maximální redukce po sloupcích v kvantizovaném celočíselném prostoru.

Pokud neprojde žádná kotva, post-procesor vrátí prázdnou n-tici, kterou predict() interpretuje jako žádnou detekci.

Dvě rozhodnutí v tomto kódu dělají rozdíl mezi spustitelným post-procesorem a nepoužitelně pomalým. Prvním je provádění aritmetiky přes numpy: výstupní tenzor má tisíce prvků a jeho iterace v čistém Pythonu trvá celé sekundy na inferenci, kde stejná aritmetika vektorizovaná přes numpy běží v milisekundách. Druhým je dekvantizace po prahovém filtru, nikoli před ním. Dekvantizace nejprve by alokovala plovoucí tenzor čtyřnásobné velikosti oproti kvantizovanému a prošla by každý prvek, než téměř všechny zahodí; dekvantizace pouze přeživších sloupců se dotkne nanejvýš hrstky hodnot a ušetří jak čas, tak RAM, kterou by úplný převod spotřeboval.

7.14.3. Dekvantizace přeživších#

Geometrii je třeba dekódovat pouze u přeživších kotev. numpy.take() vytáhne tyto sloupce a jediné volání ml.utils.dequantize() je převede na plovoucí čísla:

bb = dequantize(model,
                np.take(column_outputs, score_indices, axis=1))

bb je nyní (C, K), kde K je počet přeživších kotev – typicky hrstka, i když A bylo v tisících.

7.14.4. Čtení geometrie#

Čtyři kanály rámečku a skóre podle tříd se vytáhnou přímo:

bb_scores  = np.max(bb[4:, :],    axis=0)
bb_classes = np.argmax(bb[4:, :], axis=0)

x_center = bb[0, :]
y_center = bb[1, :]
w_half   = bb[2, :] * 0.5
h_half   = bb[3, :] * 0.5

bb_scores je nejlepší skóre třídy na přeživší kotvu; bb_classes je index třídy, která toto skóre dodala. Geometrie rámečku je stále normalizovaná na [0, 1] vstupních rozměrů sítě, takže ji další krok přeškáluje na pixely:

ib, ih, iw, ic = model.input_shape[0]
xmin = (x_center - w_half) * iw
ymin = (y_center - h_half) * ih
xmax = (x_center + w_half) * iw
ymax = (y_center + h_half) * ih

Poté jsou rámečky v pixelovém prostoru vstupu sítě – souřadnicovém prostoru, který NMS očekává na vstupu.

7.14.5. Potlačení nemaximálních hodnot#

Přeživší projdou přes NMS a jsou vráceni jako seznamy podle tříd:

nms = NMS(iw, ih, inputs[0].roi)
for i in range(bb.shape[1]):
    nms.add_bounding_box(xmin[i], ymin[i],
                         xmax[i], ymax[i],
                         bb_scores[i], bb_classes[i])
return nms.get_bounding_boxes(threshold=self.nms_threshold,
                              sigma=self.nms_sigma)

NMS čte inputs[0].roi, takže vrácené rámečky jsou v souřadnicovém prostoru původního obrazu, nikoli sítě – aplikace je kreslí přímo do zachyceného snímku bez dalšího přemapování.

7.14.6. Co skript dostane zpět#

Návratovou hodnotou je seznam seznamů podle tříd indexovaný třídou. Příklad se třemi třídami by mohl vypadat takto:

[
    [((23, 41, 95, 142), 0.92), ((180, 60, 88, 130), 0.71)],
    [],
    [((310, 95, 55, 70), 0.85)],
]

Každá položka je n-tice ((x, y, w, h), score): (x, y) je levý horní roh ohraničujícího rámečku v pixelových souřadnicích původního obrazu, w a h jsou jeho šířka a výška v pixelech a score je spolehlivost, kterou síť detekci přiřadila. Takže ((180, 60, 88, 130), 0.71) se čte jako rámeček, jehož levý horní roh leží na pixelu (180, 60), sahá 88 pixelů doprava a 130 pixelů dolů a byl ohlášen se spolehlivostí 0.71.

Vnější seznam ukazuje dva přeživší rámečky pro třídu 0, nic pro třídu 1, jeden pro třídu 2. Prázdný seznam pro třídu 1 je ponechán na místě, aby vnější index vždy odpovídal indexu třídy. U dodávaného detektoru osob má vnější seznam jediný prvek, jehož vnitřní seznam obsahuje přeživší rámečky osob. U 80třídového modelu má 80 vnitřních seznamů, z nichž je v daném snímku většina prázdná, přičemž neprázdné položky drží rámečky pro třídy, které se aktivovaly. Aplikace čte výsledek pomocí enumerate(boxes), aby procházela indexy tříd spolu se seznamy rámečků – stejný tvar, na který cílí detekční post-procesory napříč katalogem.