7.14. Passo a passo do YOLOv8¶
O pós-processador do YOLOv8 é pequeno o suficiente para ser percorrido do tensor de entrada até a lista retornada. Lê-lo uma vez mostra o que todos os outros pós-processadores do catálogo estão fazendo: aplicar limiar às pontuações quantizadas de forma barata, pegar o que sobrevive, desquantizar, decodificar a geometria, empurrar para o NMS, retornar listas por classe.
7.14.1. O tensor inicial¶
Um modelo YOLOv8 emite um único tensor de saída cujo formato é (1, C, A) – um quadro, C canais, A predições de âncora. Os primeiros quatro canais são a geometria da caixa – cx, cy, w, h – normalizados para [0, 1] das dimensões de entrada da rede. Os C - 4 canais restantes são pontuações por classe, já em [0, 1] para um modelo treinado. Cada âncora é uma coluna ao longo dos canais.
Uma âncora YOLOv8 é uma coluna ao longo dos canais: quatro números de caixa e N pontuações de classe.¶
O yolov8n_192.tflite fornecido é um detector de pessoas de classe única, então C = 5 e A está na casa dos milhares; um modelo personalizado treinado no conjunto COCO completo de 80 classes tem C = 84 contra o mesmo A. A decodificação abaixo vale para qualquer número de classes.
7.14.2. Aplicando o limiar antes de desquantizar¶
A etapa barata primeiro. O tensor de saída está no dtype inteiro quantizado do modelo, e desquantizar cada valor tocaria cada elemento de um tensor com milhares de entradas – a maioria das quais está abaixo do limiar de pontuação e acaba descartada. Em vez disso, o pós-processador quantiza o limiar uma vez e compara no espaço quantizado bruto:
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 é o tensor remodelado para (C, A) de modo que os canais são linhas e as âncoras são colunas. score_block é o subtensor de pontuações de classe – tudo a partir da linha 4 para baixo. ml.utils.threshold() reduz esse bloco ao longo do eixo 0 (find_max=True, find_max_axis=0) para a pontuação máxima por âncora, depois retorna os índices das âncoras cujo máximo passa o limiar quantizado. O tensor inteiro nunca foi desquantizado; apenas uma redução de máximo por coluna no espaço inteiro quantizado.
Se nenhuma âncora passar, o pós-processador retorna a tupla vazia que predict() interpreta como nenhuma detecção.
Duas decisões neste código fazem a diferença entre um pós-processador executável e um inutilizavelmente lento. A primeira é conduzir a aritmética através do numpy: o tensor de saída tem milhares de elementos, e iterá-lo em Python puro leva segundos inteiros por inferência, enquanto a mesma aritmética vetorizada através do numpy é executada em milissegundos. A segunda é desquantizar depois do filtro de limiar, em vez de antes. Desquantizar primeiro alocaria um tensor de ponto flutuante quatro vezes maior que o quantizado e percorreria cada elemento antes de descartar quase todos eles; desquantizar apenas as colunas sobreviventes toca, no máximo, um punhado de valores e economiza tanto o tempo quanto a RAM que a conversão completa teria consumido.
7.14.3. Desquantizando os sobreviventes¶
Apenas as âncoras sobreviventes precisam ter sua geometria decodificada. numpy.take() extrai essas colunas e uma única chamada de ml.utils.dequantize() as converte para ponto flutuante:
bb = dequantize(model,
np.take(column_outputs, score_indices, axis=1))
bb agora é (C, K) onde K é o número de âncoras sobreviventes – tipicamente um punhado, mesmo quando A estava na casa dos milhares.
7.14.4. Lendo a geometria¶
Os quatro canais de caixa e as pontuações por classe são extraídos diretamente:
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 é a melhor pontuação de classe por âncora sobrevivente; bb_classes é o índice de classe que entregou essa pontuação. A geometria da caixa ainda está normalizada em [0, 1] das dimensões de entrada da rede, então a próxima etapa a escala para pixels:
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
Depois disso, as caixas estão no espaço de pixels de entrada da rede – o espaço de coordenadas que NMS espera na entrada.
7.14.5. Non-max suppression¶
Os sobreviventes passam pelo NMS e são retornados como listas por classe:
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ê inputs[0].roi para que as caixas retornadas estejam no espaço de coordenadas da imagem original, não no da rede – a aplicação as desenha no quadro capturado diretamente, sem remapeamento adicional.
7.14.6. O que o script recebe de volta¶
O valor de retorno é uma lista de listas por classe indexada por classe. Um exemplo de três classes pode parecer assim:
[
[((23, 41, 95, 142), 0.92), ((180, 60, 88, 130), 0.71)],
[],
[((310, 95, 55, 70), 0.85)],
]
Cada entrada é uma tupla ((x, y, w, h), score): (x, y) é o canto superior esquerdo da caixa delimitadora nas coordenadas de pixel da imagem original, w e h são sua largura e altura em pixels, e score é a confiança que a rede atribuiu à detecção. Então ((180, 60, 88, 130), 0.71) se lê como uma caixa cujo canto superior esquerdo fica no pixel (180, 60), estende-se 88 pixels para a direita e 130 pixels para baixo, e foi reportada com confiança 0.71.
A lista externa mostra duas caixas sobreviventes para a classe 0, nada para a classe 1, uma para a classe 2. A lista vazia para a classe 1 é mantida no lugar para que o índice externo sempre corresponda ao índice de classe. Para o detector de pessoas fornecido, a lista externa tem um único elemento cuja lista interna contém as caixas de pessoas sobreviventes. Para um modelo de 80 classes, ela tem 80 listas internas, a maioria vazia em qualquer quadro dado, com as entradas não vazias contendo as caixas para as classes que dispararam. A aplicação lê o resultado com enumerate(boxes) para percorrer os índices de classe junto com as listas de caixas – o mesmo formato que os pós-processadores de detecção têm como alvo em todo o catálogo.