7.14. Розбір YOLOv8¶
Постпроцесор YOLOv8 достатньо малий, щоб пройти від вхідного тензора до поверненого списку. Прочитавши його один раз, стає зрозумілим, що робить кожен інший постпроцесор у каталозі: дешево порогує квантовані оцінки, бере те, що виживає, деквантизує, декодує геометрію, передає до NMS, повертає списки по класах.
7.14.1. Початковий тензор¶
Модель YOLOv8 видає єдиний вихідний тензор з формою (1, C, A) – один кадр, C каналів, A передбачень якорів. Перші чотири канали – геометрія рамки: cx, cy, w, h – нормалізовані до [0, 1] від вхідних розмірів мережі. Решта каналів C - 4 – оцінки по класах, вже в [0, 1] для навченої моделі. Кожен якір є стовпцем вздовж каналів.
Один якір YOLOv8 – це один стовпець вздовж каналів: чотири числа рамки та N оцінок класів.¶
Вбудована yolov8n_192.tflite – це однокласний детектор людей, тому C = 5, а A становить тисячі; власна модель, навчена на повному наборі COCO з 80 класами, має C = 84 при тому самому A. Наведений нижче декод працює для будь-якої кількості класів.
7.14.2. Порогування перед деквантизацією¶
Спочатку дешевий крок. Вихідний тензор знаходиться у квантованому цілочисельному dtype моделі, і деквантизація кожного значення торкнулася б кожного елемента тензора з тисячами записів – більшість яких нижче порогу оцінки і врешті відкидаються. Натомість постпроцесор квантує поріг один раз і порівнює у сирому квантованому просторі:
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 – це тензор, перетворений до форми (C, A), щоб канали були рядками, а якорі – стовпцями. score_block – це підтензор оцінок класів – усе від рядка 4 вниз. ml.utils.threshold() зводить цей блок вздовж осі 0 (find_max=True, find_max_axis=0) до максимальної оцінки по якорю, потім повертає індекси якорів, максимум яких проходить квантований поріг. Весь тензор так і не був деквантизований; лише max-редукція по стовпцях у квантованому цілочисельному просторі.
Якщо жоден якір не проходить, постпроцесор повертає порожній кортеж, який predict() інтерпретує як відсутність виявлень.
Два рішення в цьому коді визначають різницю між запускаємим постпроцесором та непрацездатно повільним. Перше – це виконання арифметики через numpy: вихідний тензор має тисячі елементів, і ітерація по ньому в чистому Python займає цілі секунди на інференс, тоді як та сама арифметика, векторизована через numpy, виконується за мілісекунди. Друге – деквантизація після порогового фільтра, а не до нього. Деквантизація спочатку виділила б тензор з плаваючою комою вчетверо більший за квантований і обійшла б кожен елемент перед відкиданням майже всіх; деквантизація лише відібраних стовпців торкається лише невеликої кількості значень і заощаджує як час, так і оперативну пам’ять, яку зайняло б повне перетворення.
7.14.3. Деквантизація відібраних значень¶
Лише відібрані якорі потребують декодування геометрії. numpy.take() витягує ці стовпці, а один виклик ml.utils.dequantize() перетворює їх у числа з плаваючою комою:
bb = dequantize(model,
np.take(column_outputs, score_indices, axis=1))
bb тепер має форму (C, K), де K – кількість відібраних якорів – зазвичай небагато, навіть якщо A сягало тисяч.
7.14.4. Читання геометрії¶
Чотири канали рамки та оцінки по класах витягуються безпосередньо:
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 – найкраща оцінка класу для кожного відібраного якоря; bb_classes – індекс класу, що забезпечив цю оцінку. Геометрія рамки все ще нормалізована в [0, 1] від вхідних розмірів мережі, тому наступний крок масштабує її до пікселів:
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
Після цього рамки знаходяться у пікселях вхідного простору мережі – просторі координат, який NMS очікує на вході.
7.14.5. Немаксимальне пригнічення¶
Відібрані значення проходять через NMS і повертаються як списки по класах:
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 читає inputs[0].roi, тому повернені рамки знаходяться у просторі координат оригінального зображення, а не мережі – застосунок малює їх безпосередньо на захопленому кадрі без додаткового перепроєктування.
7.14.6. Що отримує скрипт¶
Повернуте значення – це список списків по класах, індексованих за класом. Приклад з трьома класами може виглядати так:
[
[((23, 41, 95, 142), 0.92), ((180, 60, 88, 130), 0.71)],
[],
[((310, 95, 55, 70), 0.85)],
]
Кожен запис – це кортеж ((x, y, w, h), score): (x, y) – верхній лівий кут обмежувального прямокутника у пікселях оригінального зображення, w та h – його ширина та висота в пікселях, а score – впевненість, яку мережа присвоїла виявленню. Таким чином, ((180, 60, 88, 130), 0.71) читається як рамка, верхній лівий кут якої знаходиться у пікселі (180, 60), розширюється на 88 пікселів вправо та 130 пікселів вниз, і була повідомлена з впевненістю 0.71.
Зовнішній список показує дві рамки, що вижили, для класу 0, нічого для класу 1, одну для класу 2. Порожній список для класу 1 зберігається на місці, щоб зовнішній індекс завжди збігався з індексом класу. Для вбудованого детектора людей зовнішній список має один елемент, внутрішній список якого містить рамки людей, що вижили. Для моделі з 80 класами він має 80 внутрішніх списків, більшість порожні на будь-якому даному кадрі, а непорожні записи містять рамки для класів, які спрацювали. Застосунок читає результат за допомогою enumerate(boxes), щоб обходити індекси класів разом зі списками рамок – та сама форма, на яку орієнтуються постпроцесори виявлень у каталозі.