7.14. Tutorial YOLOv8¶
O pós-processador YOLOv8 é suficientemente pequeno para percorrer desde o tensor de entrada até à lista retornada. Lê-lo uma vez mostra o que cada outro pós-processador no catálogo está a fazer: limiar de pontuações quantizadas de forma eficiente, tomar o que sobrevive, desquantizar, descodificar geometria, enviar para NMS, retornar listas por classe.
7.14.1. O tensor inicial¶
Um modelo YOLOv8 emite um único tensor de saída com forma (1, C, A) – um fotograma, C canais, A previsões de âncora. Os primeiros quatro canais são geometria de caixa – cx, cy, w, h – normalizados para [0, 1] das dimensões de entrada da rede. Os restantes C - 4 canais 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 incluído é um detetor de pessoas de classe única, pelo que C = 5 e A está na ordem dos milhares; um modelo personalizado treinado no conjunto completo de 80 classes COCO tem C = 84 contra o mesmo A. A descodificação abaixo é válida para qualquer número de classes.
7.14.2. Limiar antes de desquantizar¶
O passo eficiente primeiro. O tensor de saída está no dtype inteiro quantizado do modelo, e desquantizar cada valor tocaria em cada elemento de um tensor com milhares de entradas – a maioria das quais está abaixo do limiar de pontuação e acaba por ser descartada. O pós-processador quantiza o limiar uma única 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 reformatado para (C, A) de modo a que os canais sejam linhas e as âncoras sejam colunas. score_block é o sub-tensor 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, e depois retorna os índices das âncoras cuja pontuação máxima passa o limiar quantizado. O tensor completo nunca foi desquantizado; apenas uma redução máxima por coluna no espaço inteiro quantizado.
Se nenhuma âncora passar, o pós-processador retorna o tuplo vazio que predict() interpreta como sem deteção.
Duas decisões neste código fazem a diferença entre um pós-processador funcional 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 demora segundos inteiros por inferência, enquanto a mesma aritmética vetorizada através do numpy corre em milissegundos. A segunda é desquantizar após o filtro de limiar em vez de antes. Desquantizar primeiro alocaria um tensor float quatro vezes o tamanho do tensor quantizado e percorreria cada elemento antes de descartar quase todos; desquantizar apenas as colunas sobreviventes toca num punhado de valores no máximo e poupa tanto o tempo como a RAM que a conversão completa teria consumido.
7.14.3. Desquantizar os sobreviventes¶
Apenas as âncoras sobreviventes precisam de ter a sua geometria descodificada. numpy.take() extrai essas colunas e uma única chamada a ml.utils.dequantize() converte-as para floats:
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 ordem dos milhares.
7.14.4. Ler 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 obteve essa pontuação. A geometria da caixa ainda está normalizada em [0, 1] das dimensões de entrada da rede, pelo que o passo seguinte a escala para pixéis:
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
Após isto, as caixas estão no espaço de pixéis de entrada da rede – o espaço de coordenadas que NMS espera na entrada.
7.14.5. Supressão de não-máximos¶
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 da rede – a aplicação desenha-as diretamente no fotograma capturado sem remapeamento adicional.
7.14.6. O que o script recebe de volta¶
O valor de retorno é uma lista de listas por classe indexadas por classe. Um exemplo de três classes pode ter este aspeto:
[
[((23, 41, 95, 142), 0.92), ((180, 60, 88, 130), 0.71)],
[],
[((310, 95, 55, 70), 0.85)],
]
Cada entrada é um tuplo ((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 a sua largura e altura em pixéis, e score é a confiança que a rede atribuiu à deteção. Assim, ((180, 60, 88, 130), 0.71) lê-se como uma caixa cujo canto superior esquerdo se situa no pixel (180, 60), se estende 88 pixéis para a direita e 130 pixéis para baixo, e foi reportada com confiança 0.71.
A lista exterior mostra duas caixas sobreviventes para a classe 0, nenhuma para a classe 1, uma para a classe 2. A lista vazia para a classe 1 é mantida no lugar para que o índice exterior corresponda sempre ao índice de classe. Para o detetor de pessoas incluído, a lista exterior tem um único elemento cuja lista interior contém as caixas de pessoas sobreviventes. Para um modelo de 80 classes, tem 80 listas interiores, a maioria vazias em qualquer fotograma, 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 juntamente com as listas de caixas – a mesma forma que os pós-processadores de deteção visam em todo o catálogo.