從模型輸出讀取檢測框
| 欄位 | 值 |
|---|---|
| 類別 | 模型與推論 |
| 難度 | 中級 |
| 預估閱讀時間 | 15-20 minutes |
| 標籤 | postprocessing, boxdecode, detection |
偵測器不會直接輸出框。它的原始輸出是一堆特徵圖,在產生任何意義之前,仍然需要進行閾值處理、非最大值抑制和座標映射。SimaBoxDecode 是後處理階段,它將這三個步驟整合到一個最佳化的步驟中,將推理張量轉換為源圖像像素中的最終檢測結果。
本章設定了解碼器——選擇模型系列,使用 decode_type,使用分數閾值控制置信度,使用 NMS IoU 閾值抑制重疊,並使用 top_k 限制輸出——然後執行模型並讀取傳回的檢測數量。到最後,您將擁有一個設定好的偵測器管線,以及從其輸出讀取的檢測數量,此外(在下面的「實務參考」中),還包含完整的線格式,以便您可以在任何執行階段自行解析框。
操作指南
設定解碼器
這些選項同時設定輸入合約和後處理行為。decode_type(這裡為 YoloV8)選擇模型系列的解碼路徑。置信度閾值會在 NMS 之前剔除較弱的候選者;NMS IoU 閾值控制重疊框的合併程度;top_k 限制最終數量,以實現確定性的下游成本;boxdecode_original_width/boxdecode_original_height 將解碼後的座標映射回源圖像像素。關於這些參數的調整指南,請參閱下面的「實務參考」。
C++: decode_type 採用 BoxDecodeType::YoloV8 列舉。閾值/NMS/top_k 值稍後通過 stages::BoxDecodeOptions 傳遞,而不是通過 Model::Options。
Python: decode_type 採用 pyneat.BoxDecodeType.YoloV8 列舉,並且 score_threshold、nms_iou_threshold、top_k 和 boxdecode_original_width/boxdecode_original_height 直接設定在 ModelOptions 上。(score_threshold 和 C++ 中的 detection_threshold 名稱相同,控制相同的內容——請參閱「實務參考」中的命名說明。)
simaai::neat::Model::Options opt;
opt.preprocess.color_convert.input_format = simaai::neat::PreprocessColorFormat::BGR;
opt.preprocess.input_max_width = bgr.cols;
opt.preprocess.input_max_height = bgr.rows;
opt.preprocess.input_max_depth = bgr.channels();
opt.decode_type = simaai::neat::BoxDecodeType::YoloV8;
建置模型
從檔案加上選項建置 Model,將解碼設定綁定到模型,以便從中衍生的推理和後處理階段使用上述設定。
simaai::neat::Model model(model_path, opt);
執行預處理、推理和解碼
此時,一個幀將通過預處理、MLA 推理和框解碼器,以產生檢測輸出。
**C++:**路徑以分階段的方式明確定義:stages::Preproc 產生輸入張量,stages::Infer 執行模型,以及一個 stages::BoxDecodeOptions(包含 detection_threshold = 0.55、nms_iou_threshold = 0.5、top_k = 100)設定了解碼,然後執行。
Python:model.run([tensor]) 在單次呼叫中執行整個已設定的路徑,並傳回一個 TensorList。當 BoxDecode 連接到模型路徑時,第一個張量是封裝後的 BBOX 輸出。
simaai::neat::TensorList pre = simaai::neat::stages::Preproc(std::vector<cv::Mat>{bgr}, model);
simaai::neat::Sample infer_samples = simaai::neat::stages::Infer(
simaai::neat::Sample{simaai::neat::sample_from_tensors(pre)}, model);
if (infer_samples.empty())
throw std::runtime_error("infer stage returned no samples");
simaai::neat::Sample infer = infer_samples.front();
simaai::neat::stages::BoxDecodeOptions box(simaai::neat::BoxDecodeType::YoloV8);
(void)box.decode_type;
(void)bgr.cols;
(void)bgr.rows;
box.detection_threshold = 0.55;
box.nms_iou_threshold = 0.5;
box.top_k = 100;
讀取框
最後,將解碼輸出轉換為您可以使用的內容。
C++:stages::BoxDecodeResults(...) 傳回一個 BoxDecodeResultList;前一個結果的 boxes 向量已經解析為 {x1, y1, x2, y2, score, class_id},並限制到來源像素,因此 decoded.boxes.size() 是檢測計數。
**Python:**結果是一個單一的 BBOX uint8 張量,位於 outputs[0] 中。前四個小端字節是檢測計數(struct.unpack_from("<I", buf, 0));完整的記錄佈局在「實際操作」中記錄。如果執行階段未將 BoxDecode 連接到 model.run,則傳回的 TensorList 包含原始的特徵圖標頭。
// BoxDecode parses the "BBOX" tensor into {x1, y1, x2, y2, score, class_id}
// entries clamped to original_width x original_height source pixels.
simaai::neat::BoxDecodeResultList decoded_results =
simaai::neat::stages::BoxDecodeResults(simaai::neat::Sample{infer}, model, box);
if (decoded_results.empty())
throw std::runtime_error("boxdecode result parser returned no results");
const simaai::neat::BoxDecodeResult& decoded = decoded_results.front();
執行
從以下位置執行 Python 和 C++(預先建置) 指令:Neat 安裝根目錄(包含 share/ 以及 lib/);從原始碼開始執行建置指令,指令應從儲存庫的根目錄執行。
C++ (prebuilt):
./lib/sima-neat/tutorials/tutorial_007_read_detection_boxes \
--model /tmp/yolo_v8s.tar.gz --image /path/to/frame.jpg
C++ (build from source):
./build.sh --target tutorial_007_read_detection_boxes
./build/tutorials-standalone/tutorial_007_read_detection_boxes \
--model /tmp/yolo_v8s.tar.gz --image /path/to/frame.jpg
預期輸出(方塊數量取決於框架;合成框架會產生零個方塊):
boxes=0
[OK] 007_read_detection_boxes
(Python 建構會輸出 detections=...,或者在執行階段如果沒有將 BoxDecode 連接到 model.run,則輸出 raw_output_heads=...。)若要將本章的 C++ 原始碼整合到您自己的專案中,並使用自訂的 CMakeLists.txt(無需額外的資料夾),請參閱登陸頁面上的 如何執行教學。
實務應用
SimaBoxDecode 輸出一個帶有標籤 BBOX 的單一輸出張量。該張量攜帶一個封裝的字節緩衝區,執行階段解析器將其解釋為浮點檢測。理解這兩個層級的協定(線路緩衝區與解析後的 Box 記錄)是從 Python 或 C++ 讀取輸出的關鍵。
BBOX 張量
解碼階段為每個輸入幀產生一個 BBOX 張量,具有以下內容:
| 欄位 | 值 |
|---|---|
semantic.detection.format | "BBOX" |
dtype | UInt8 |
shape | rank-1:[N_bytes],其中 N_bytes 是模型封存檔封裝的緩衝區容量(例如,標準 YOLOv8 封裝中的 [20160]) |
張量形狀是一個字節計數,而不是檢測計數。封裝的字節包含一個小標頭和一個連續的固定大小框記錄陣列。N_bytes 由模型封存檔的 buffers.input[0].size 欄位確定(位於框解碼階段的設定 JSON 中),並且限制了解碼器在單個幀中可以輸出的最大檢測數量(有關執行階段維度如何與封裝值交互,請參閱下面的「覆寫協定」)。
封裝的線路格式
uint8 緩衝區以小端格式佈局:
offset size content
------ ---- -------
0 4 uint32 N = number of valid detections in this frame
4 24 RawBox[0]
28 24 RawBox[1]
. . ...
. . RawBox[N-1]
(trailing bytes up to buffer capacity are padding, ignored)
每個 RawBox 記錄長度為 24 位元組:
| 記錄中的位移量 | 大小 | 類型 | 欄位 | 意義 |
|---|---|---|---|---|
| 0 | 4 | int32 | x | 來源影像中,左上角 x 座標(以像素為單位) |
| 4 | 4 | int32 | y | 來源影像中,左上角 y 座標(以像素為單位) |
| 8 | 4 | int32 | w | 寬度,以原始像素為單位 |
| 12 | 4 | int32 | h | 原始影像中的高度(以像素為單位) |
| 16 | 4 | float32 | score | 非最大值抑制 (NMS) 後的檢測置信度 [0.0, 1.0] (數值 detection_threshold 閘門開啟) |
| 20 | 4 | int32 | class_id | 預測的類別 ID(模型定義;從 0 開始編號;類別名稱對應表位於模型封存檔的元資料中) |
標準的 Python struct 格式符合單一記錄的是 "<iiiifi"
(小端序,4 個帶號整數,一個浮點數,一個帶號整數)。
執行階段的解析輔助函式(parse_bbox_bytes /
decode_bbox_tensor 在 include/pipeline/DetectionTypes.h,
tests/unit_testing/unit_detection_types_bbox_test.cpp 將電線合約固定下來)
擴展每個 RawBox 變成 Box 用於後續程式碼的結構:
struct Box {
float x1, y1, x2, y2; // x2 = x + w, y2 = y + h; clamped to [0, img_w|h]
float score;
int class_id;
};
座標空間
從中解碼出的座標為 BBOX 位於「原始影像像素」中,與您傳遞的座標系統相同。 original_width / original_height (或模型封存檔是與之一起封裝的)。它們並未經過標準化處理。 [0, 1],而且它們並未以模型內部矩形輸入空間的形式呈現。語法分析器會限制。 (x1, y1, x2, y2) 到 [0, original_width] / [0, original_height]
因此,呼叫程式碼可以直接在原始框架上繪製這些內容。
實際範例
使用教學的執行階段設定(original_width = 640,
original_height = 640, top_k = 100) 以及預設的 YOLOv8 模型套件buffers.input[0].size = 20160 在「boxdecode config」中,單一解碼後的影格會產生:
out.kind == SampleKind.Tensorout.payload_tag == "BBOX"out.tensor.dtype == UInt8,out.tensor.shape == [20160]- 位元組
[0:4]給N以小端格式;0 <= N <= 100因為top_k = 100