7.14. YOLOv8 áttekintés

A YOLOv8 utófeldolgozó elég kicsi ahhoz, hogy a bemeneti tenzortól a visszaadott listáig végiglépkedjünk rajta. Egyszeri elolvasása megmutatja, mit csinál a katalógus összes többi utófeldolgozója: olcsón küszöböli a kvantált pontszámokat, veszi a túlélőket, dekvantál, dekódolja a geometriát, az NMS-be tolja, és osztályonkénti listákat ad vissza.

7.14.1. A kiinduló tenzor

Egy YOLOv8 modell egyetlen kimeneti tenzort bocsát ki, amelynek alakja (1, C, A) – egy képkocka, C csatorna, A horgony-predikció. Az első négy csatorna a doboz geometriája – cx, cy, w, h –, a hálózat bemeneti dimenzióinak [0, 1] tartományára normalizálva. A maradék C - 4 csatorna osztályonkénti pontszám, egy betanított modellnél már [0, 1] tartományban. Minden horgony egy oszlop a csatornák mentén lefelé.

Egy oldalára fektetett rács: a sorok cx, cy, w, h, score_0, score_1, ..., score_(N-1) címkével; az oszlopok a 0-tól az A-1-ig terjedő horgonyokkal címkézve. Egy oszlop ki van emelve, hogy megmutassa: egyetlen horgony predikciója egy oszlop a csatornák mentén lefelé.

Egy YOLOv8 horgony egy oszlop a csatornák mentén lefelé: négy doboz-szám és N osztály-pontszám.

A szállított yolov8n_192.tflite egy egyosztályos személydetektor, így C = 5 és A ezres nagyságrendű; egy egyéni, a teljes 80-osztályos COCO halmazon betanított modellnél C = 84 ugyanazon A mellett. Az alábbi dekódolás bármely osztályszámra érvényes.

7.14.2. Küszöbölés a dekvantálás előtt

Az olcsó lépés először. A kimeneti tenzor a modell kvantált egész dtype-jában van, és minden érték dekvantálása egy több ezer bejegyzéssel rendelkező tenzor minden elemét érintené – amelyek többsége a pontszámküszöb alatt van, és végül eldobásra kerül. Az utófeldolgozó ehelyett egyszer kvantálja a küszöbértéket, és nyers kvantált térben hasonlít össze:

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 ()

A column_outputs a tenzor (C, A) alakzatra átalakítva, így a csatornák a sorok, a horgonyok pedig az oszlopok. A score_block az osztály-pontszám altenzor – minden a 4 sortól lefelé. Az ml.utils.threshold() ezt a blokkot a 0 tengely mentén redukálja (find_max=True, find_max_axis=0) a horgonyonkénti maximális pontszámra, majd visszaadja azon horgonyok indexeit, amelyek maximuma átmegy a kvantált küszöbértéken. A teljes tenzort soha nem dekvantáltuk; csak egy oszloponkénti max-redukció történt kvantált egész térben.

Ha egyetlen horgony sem megy át, az utófeldolgozó visszaadja az üres rendezett ennest, amelyet a predict() nincs észlelésként értelmez.

Ebben a kódban két döntés teszi a különbséget egy futtatható utófeldolgozó és egy használhatatlanul lassú között. Az első az aritmetika numpy modulon keresztüli vezetése: a kimeneti tenzornak több ezer eleme van, és nyers Pythonban iterálva ez egész másodperceket vesz igénybe következtetésenként, míg ugyanez az aritmetika a numpy modulon keresztül vektorizálva ezredmásodpercek alatt fut. A második a küszöbszűrés után történő dekvantálás, nem előtte. Az előbb dekvantálás egy négyszer akkora lebegőpontos tenzort foglalna le, mint a kvantált, és minden elemet bejárna, mielőtt szinte mindet eldobja; csak a túlélő oszlopok dekvantálása legfeljebb egy maréknyi értéket érint, és megspórolja mind az időt, mind a RAM-ot, amelyet a teljes átalakítás felemésztett volna.

7.14.3. A túlélők dekvantálása

Csak a túlélő horgonyok geometriáját kell dekódolni. A numpy.take() kihúzza ezeket az oszlopokat, és egyetlen ml.utils.dequantize() hívás lebegőpontossá alakítja őket:

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

A bb most (C, K), ahol K a túlélő horgonyok száma – jellemzően egy maréknyi, még akkor is, ha A ezres nagyságrendű volt.

7.14.4. A geometria kiolvasása

A négy doboz-csatornát és az osztályonkénti pontszámokat közvetlenül kihúzzuk:

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

A bb_scores a túlélő horgonyonkénti legjobb osztály-pontszám; a bb_classes az az osztályindex, amely azt a pontszámot adta. A doboz geometriája még mindig a hálózati bemeneti dimenziók normalizált [0, 1] tartományában van, így a következő lépés képpontokra skálázza:

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

Ezután a dobozok a hálózati bemeneti képpont-térben vannak – abban a koordinátatérben, amelyet a NMS a bemeneten vár.

7.14.5. Non-max suppression

A túlélők átmennek az NMS-en, és osztályonkénti listákként térnek vissza:

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)

A NMS az inputs[0].roi értéket olvassa, így a visszaadott dobozok az eredeti kép koordinátaterében vannak, nem a hálózatéban – az alkalmazás közvetlenül a rögzített képkockára rajzolja őket további visszaképezés nélkül.

7.14.6. Mit kap vissza a szkript

A visszatérési érték osztályonkénti listák listája, osztály szerint indexelve. Egy háromosztályos példa így nézhet ki:

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

Minden bejegyzés egy ((x, y, w, h), score) rendezett ennes: az (x, y) a határoló doboz bal felső sarka az eredeti kép képpont-koordinátáiban, a w és a h a szélessége és magassága képpontban, a score pedig a megbízhatóság, amelyet a hálózat az észleléshez rendelt. Tehát a ((180, 60, 88, 130), 0.71) úgy olvasandó, mint egy doboz, amelynek bal felső sarka a (180, 60) képponton ül, 88 képpontot terjed jobbra és 130 képpontot lefelé, és 0.71 megbízhatósággal jelentették.

A külső lista két túlélő dobozt mutat a 0 osztályhoz, semmit az 1 osztályhoz, egyet a 2 osztályhoz. Az 1 osztály üres listája a helyén marad, hogy a külső index mindig megfeleljen az osztályindexnek. A szállított személydetektornál a külső listának egyetlen eleme van, amelynek belső listája a túlélő személy-dobozokat tartalmazza. Egy 80-osztályos modellnél 80 belső listája van, amelyek többsége bármely adott képkockán üres, a nem üres bejegyzések pedig az aktiválódott osztályok dobozait tartalmazzák. Az alkalmazás az enumerate(boxes) segítségével olvassa az eredményt, hogy az osztályindexeket a doboz-listákkal együtt járja be – ugyanaz az alakzat, amelyet az észlelő utófeldolgozók a katalógusban megcéloznak.