7.14. YOLOv8-genomgång

YOLOv8-efterbehandlaren är liten nog att gå igenom steg för steg från indatatensor till returnerad lista. Att läsa den en gång visar vad varje annan efterbehandlare i katalogen gör: tröskelfiltrera kvantiserade poäng billigt, ta det som överlever, avkvantisera, avkoda geometri, skicka till NMS, returnera listor per klass.

7.14.1. Starttensorn

En YOLOv8-modell avger en enda utdatatensor vars form är (1, C, A) – en bildruta, C kanaler, A ankarprediktioner. De första fyra kanalerna är rutgeometri – cx, cy, w, h – normaliserade till [0, 1] av nätverkets indatadimensioner. De återstående C - 4 kanalerna är poäng per klass, redan i [0, 1] för en tränad modell. Varje ankare är en kolumn nedåt genom kanalerna.

Ett rutnät lagt på sidan: rader märkta cx, cy, w, h, score_0, score_1, ..., score_(N-1); kolumner märkta anchor 0 till anchor A-1. En kolumn är inramad för att visa att ett enskilt ankares prediktion är en kolumn nedåt genom kanalerna.

Ett YOLOv8-ankare är en kolumn nedåt genom kanalerna: fyra ruttal och N klasspoäng.

Den medföljande yolov8n_192.tflite är en enklass-persondetektor, så C = 5 och A är i tusental; en anpassad modell tränad på hela 80-klassers COCO-uppsättningen har C = 84 mot samma A. Avkodningen nedan håller för vilket klassantal som helst.

7.14.2. Tröskelfiltrering före avkvantisering

Det billiga steget först. Utdatatensorn är i modellens kvantiserade heltals-dtype, och att avkvantisera varje värde skulle vidröra varje element i en tensor med tusentals poster – de flesta av vilka ligger under poängtröskeln och slutar med att förkastas. Efterbehandlaren kvantiserar i stället tröskelvärdet en gång och jämför i rått kvantiserat rum:

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 är tensorn omformad till (C, A) så att kanalerna är rader och ankarna är kolumner. score_block är klasspoäng-deltensorn – allt från rad 4 och nedåt. ml.utils.threshold() reducerar det blocket längs axel 0 (find_max=True, find_max_axis=0) till den maximala poängen per ankare, och returnerar sedan indexen för de ankare vars maximum passerar det kvantiserade tröskelvärdet. Hela tensorn avkvantiserades aldrig; endast en maximumreduktion per kolumn i kvantiserat heltalsrum.

Om inget ankare passerar returnerar efterbehandlaren den tomma tupeln som predict() tolkar som ingen detektering.

Två beslut i denna kod gör skillnaden mellan en körbar efterbehandlare och en oanvändbart långsam sådan. Det första är att driva aritmetiken genom numpy: utdatatensorn har tusentals element, och att iterera den i rå Python tar hela sekunder per inferens, där samma aritmetik vektoriserad genom numpy körs på millisekunder. Det andra är att avkvantisera efter tröskelfiltret snarare än före. Att avkvantisera först skulle allokera en flyttalstensor fyra gånger så stor som den kvantiserade och gå igenom varje element innan nästan alla förkastas; att avkvantisera endast de överlevande kolumnerna vidrör som mest en handfull värden och sparar både tiden och RAM-minnet som den fullständiga konverteringen skulle ha förbrukat.

7.14.3. Avkvantisera de överlevande

Endast de överlevande ankarna behöver sin geometri avkodad. numpy.take() drar ut dessa kolumner och ett enda ml.utils.dequantize()-anrop konverterar dem till flyttal:

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

bb är nu (C, K) där K är antalet överlevande ankare – typiskt en handfull även när A var i tusental.

7.14.4. Läsa geometrin

De fyra rutkanalerna och poängen per klass dras ut direkt:

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 är den bästa klasspoängen per överlevande ankare; bb_classes är klassindexet som levererade den poängen. Rutgeometrin är fortfarande i normaliserade [0, 1] av nätverkets indatadimensioner, så nästa steg skalar den till pixlar:

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

Efter detta är rutorna i nätverkets indatapixelrum – det koordinatrum som NMS förväntar sig som indata.

7.14.5. Icke-maximumundertryckning

De överlevande går genom NMS och returneras som listor per klass:

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 läser inputs[0].roi så att de returnerade rutorna är i originalbildens koordinatrum, inte nätverkets – applikationen ritar dem direkt på den infångade bildrutan utan ytterligare ommappning.

7.14.6. Vad skriptet får tillbaka

Returvärdet är en lista av listor per klass indexerad efter klass. Ett treklass-exempel kan se ut så här:

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

Varje post är en ((x, y, w, h), score)-tupel: (x, y) är begränsningsrutans övre vänstra hörn i originalbildens pixelkoordinater, w och h är dess bredd och höjd i pixlar, och score är konfidensen nätverket tilldelade detekteringen. Så ((180, 60, 88, 130), 0.71) läses som en ruta vars övre vänstra hörn sitter vid pixel (180, 60), sträcker sig 88 pixlar åt höger och 130 pixlar nedåt, och rapporterades med konfidensen 0.71.

Den yttre listan visar två överlevande rutor för klass 0, ingenting för klass 1, en för klass 2. Den tomma listan för klass 1 behålls på plats så att det yttre indexet alltid matchar klassindexet. För den medföljande persondetektorn har den yttre listan ett enda element vars inre lista innehåller de överlevande personrutorna. För en 80-klassers modell har den 80 inre listor, de flesta tomma på en given bildruta, med de icke-tomma posterna som håller rutorna för de klasser som utlöstes. Applikationen läser resultatet med enumerate(boxes) för att gå igenom klassindexen tillsammans med rutlistorna – samma form som detekteringsefterbehandlare riktar in sig på i hela katalogen.