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.
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.