7.14. Panduan YOLOv8

Post-processor YOLOv8 cukup kecil untuk ditelusuri dari tensor input hingga daftar yang dikembalikan. Membacanya sekali menunjukkan apa yang dilakukan setiap post-processor lain dalam katalog: threshold skor terkuantisasi dengan murah, ambil yang tersisa, dequantize, dekode geometri, dorong ke NMS, kembalikan daftar per-kelas.

7.14.1. Tensor awal

Model YOLOv8 memancarkan satu tensor output dengan bentuk (1, C, A) -- satu bingkai, C saluran, A prediksi anchor. Empat saluran pertama adalah geometri kotak -- cx, cy, w, h -- dinormalisasi ke [0, 1] dari dimensi input jaringan. Sisa C - 4 saluran adalah skor per-kelas, sudah dalam [0, 1] untuk model yang terlatih. Setiap anchor adalah kolom di sepanjang saluran.

A grid laid on its side: rows labelled cx, cy, w, h, score_0, score_1, ..., score_(N-1); columns labelled anchor 0 through anchor A-1. One column is outlined to show that a single anchor's prediction is one column down the channels.

Satu anchor YOLOv8 adalah satu kolom di sepanjang saluran: empat angka kotak dan N skor kelas.

yolov8n_192.tflite yang tersedia adalah detektor orang satu-kelas, sehingga C = 5 dan A ada dalam ribuan; model kustom yang dilatih pada set COCO 80-kelas penuh memiliki C = 84 terhadap A yang sama. Dekode di bawah ini berlaku untuk jumlah kelas berapa pun.

7.14.2. Thresholding sebelum dequantizing

Langkah murah terlebih dahulu. Tensor output ada dalam dtype integer terkuantisasi model, dan men-dequantize setiap nilai akan menyentuh setiap elemen dari tensor dengan ribuan entri -- yang sebagian besar berada di bawah ambang batas skor dan akhirnya dibuang. Post-processor sebaliknya mengkuantisasi ambang batas sekali dan membandingkan dalam ruang terkuantisasi mentah:

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 adalah tensor yang diubah bentuknya menjadi (C, A) sehingga saluran adalah baris dan anchor adalah kolom. score_block adalah sub-tensor skor kelas -- semua dari baris 4 ke bawah. ml.utils.threshold() mereduksi blok tersebut di sepanjang sumbu 0 (find_max=True, find_max_axis=0) ke skor maksimum per-anchor, lalu mengembalikan indeks anchor yang maksimumnya melewati ambang batas terkuantisasi. Seluruh tensor tidak pernah di-dequantize; hanya reduksi max-per-kolom dalam ruang integer terkuantisasi.

Jika tidak ada anchor yang lolos, post-processor mengembalikan tuple kosong yang ditafsirkan predict() sebagai tidak ada deteksi.

Dua keputusan dalam kode ini membuat perbedaan antara post-processor yang dapat dijalankan dan yang tidak berguna karena lambat. Yang pertama adalah menggerakkan aritmatika melalui numpy: tensor output memiliki ribuan elemen, dan melakukan iterasi dalam Python mentah membutuhkan seluruh detik per inferensi, di mana aritmatika yang sama yang divektorisasi melalui numpy berjalan dalam milidetik. Yang kedua adalah men-dequantize setelah filter threshold daripada sebelumnya. Men-dequantize terlebih dahulu akan mengalokasikan tensor float empat kali ukuran tensor terkuantisasi dan menelusuri setiap elemen sebelum membuang hampir semuanya; men-dequantize hanya pada kolom yang tersisa menyentuh segelintir nilai paling banyak dan menghemat waktu dan RAM yang seharusnya dikonsumsi oleh konversi penuh.

7.14.3. Men-dequantize para penyintas

Hanya anchor yang tersisa yang perlu geometrinya didekode. numpy.take() menarik kolom-kolom tersebut keluar dan satu panggilan ml.utils.dequantize() mengkonversinya ke float:

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

bb sekarang adalah (C, K) di mana K adalah jumlah anchor yang tersisa -- biasanya segelintir bahkan ketika A ada dalam ribuan.

7.14.4. Membaca geometri

Empat saluran kotak dan skor per-kelas ditarik langsung:

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 adalah skor kelas terbaik per anchor yang tersisa; bb_classes adalah indeks kelas yang menghasilkan skor tersebut. Geometri kotak masih dalam [0, 1] ternormalisasi dari dimensi input jaringan, sehingga langkah berikutnya menskalakannnya ke piksel:

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

Setelah ini kotak berada dalam ruang piksel input jaringan -- ruang koordinat yang diharapkan NMS sebagai input.

7.14.5. Non-max suppression

Para penyintas melewati NMS dan dikembalikan sebagai daftar per-kelas:

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 membaca inputs[0].roi sehingga kotak yang dikembalikan berada dalam ruang koordinat citra asli, bukan ruang jaringan -- aplikasi menggambarnya langsung ke bingkai yang ditangkap tanpa pemetaan ulang lebih lanjut.

7.14.6. Apa yang diterima skrip kembali

Nilai kembalian adalah daftar daftar per-kelas yang diindeks oleh kelas. Contoh tiga kelas mungkin terlihat seperti:

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

Setiap entri adalah tuple ((x, y, w, h), score): (x, y) adalah sudut kiri atas kotak pembatas dalam koordinat piksel citra asli, w dan h adalah lebar dan tingginya dalam piksel, dan score adalah keyakinan yang ditetapkan jaringan untuk deteksi tersebut. Jadi ((180, 60, 88, 130), 0.71) dibaca sebagai kotak yang sudut kiri atasnya berada di piksel (180, 60), memanjang 88 piksel ke kanan dan 130 piksel ke bawah, dan dilaporkan dengan keyakinan 0.71.

Daftar luar menunjukkan dua kotak yang tersisa untuk kelas 0, tidak ada untuk kelas 1, satu untuk kelas 2. Daftar kosong untuk kelas 1 dipertahankan di tempatnya sehingga indeks luar selalu sesuai dengan indeks kelas. Untuk detektor orang yang tersedia, daftar luar memiliki satu elemen yang daftar dalamnya berisi kotak orang yang tersisa. Untuk model 80-kelas, ia memiliki 80 daftar dalam, sebagian besar kosong pada bingkai mana pun, dengan entri yang tidak kosong menyimpan kotak untuk kelas yang aktif. Aplikasi membaca hasil dengan enumerate(boxes) untuk menelusuri indeks kelas bersama daftar kotak -- bentuk yang sama yang ditargetkan post-processor deteksi di seluruh katalog.