モデルの出力から検出ボックスを読み取る
| 項目 | 値 |
|---|---|
| カテゴリ | モデルと推論 |
| 難易度 | 中級 |
| 推定所要時間 | 15-20 minutes |
| ラベル | postprocessing, boxdecode, detection |
検出器は、直接バウンディングボックスを返しません。その生の出力は、意味のあるものになる前に、しきい値処理、非最大抑制、および座標マッピングが必要な特徴マップのスタックです。SimaBoxDecodeは、これらすべてを1つの最適化されたステップで実行する後処理ステージであり、推論テンソルをソース画像のピクセル単位の最終的な検出に変換します。
この章では、そのデコードを構成します。つまり、decode_typeを使用してモデルファミリーを選択し、スコアしきい値で信頼度を制御し、NMS IoUしきい値で重複を抑制し、top_kで出力を制限します。その後、モデルを実行し、検出された数を読み取ります。最終的には、構成された検出器パイプラインと、その 出力から読み取った検出数のカウントが得られ、さらに(以下に示す「実践」の参照)完全なワイヤ形式も得られるため、任意のランタイムでバウンディングボックスを自分で解析できます。
ウォークスルー
デコードを構成する
これらのオプションは、入力の契約と後処理の動作の両方を設定します。decode_type(ここではYoloV8)は、モデルファミリーのデコードパスを選択します。信頼度しきい値は、NMSの前に弱い候補を削除します。NMS IoUしきい値は、重複するバウンディングボックスの結合をどの程度積極的に行うかを制御します。top_kは、決定的な下流のコストのために最終的なカウントを制限します。そして、boxdecode_original_width/boxdecode_original_heightは、デコードされた座標をソース画像のピクセルにマッピングします。これらの各項目の調整に関するガイダンスは、以下に示す「実践」にあります。
decode_typeは、BoxDecodeType::YoloV8 enumを受け取ります。しきい値/NMS/top_kの値は、Model::Optionsではなく、後でstages::BoxDecodeOptionsを通じて渡されます。
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推論、およびボックスデコーダーを通過し、検出出力が生成されます。
処理フローは段階的に明確に定義されています。stages::Preproc は入力テンソルを生成し、stages::Infer はモデルを実行し、stages::BoxDecodeOptions(detection_threshold = 0.55、nms_iou_threshold = 0.5、top_k = 100 を含む)が、次に実行されるデコードを構成します。
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;
ボックスの読み込み
最後に、デコードの出力を、実際に使用できる形式に変換します。
stages::BoxDecodeResults(...) は BoxDecodeResultList を返し、最初の結果の boxes ベクトルは、すでにソースピクセルにクランプされた {x1, y1, x2, y2, score, class_id} にパースされているため、decoded.boxes.size() が検出の数になります。
// 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 というタグが付けられた単一の出力テンソルを出力します。このテンソルには、ランタイムパーサーが浮動小数点数の検出に解釈する、パックされたバイトバッファが含まれています。この2層の契約(ワイヤバッファとパースされた Box レコード)を理解することが、PythonまたはC++のいずれかから出力を読み取るための鍵となります。
BBOX テンソル
デコードステージは、入力フレームごとに1つの BBOX テンソルを生成します。
| フィールド | 値 |
|---|---|
semantic.detection.format | "BBOX" |
dtype | UInt8 |
shape | ランク1: [N_bytes]。ここで、N_bytes は、モデルアーカイブにパックされたバッファの容量です(たとえば、標準の YOLOv8 パックでは [20160])。 |
テンソルの形状は、バイト数であり、検出の数ではありません。パックされたバイトには、小さなヘッダーと、固定サイズのボックスレコードの連続した配列が含まれています。N_bytes は、モデルアーカイブの buffers.input[0].size フィールド(ボックスデコードステージの構成JSON内)によって決定され、デコーダーが1つのフレームで出力できる最大検出数を制限します(ランタイムの次元がパッケージ化された値とど のように相互作用するかについては、「契約のオーバーライド」を参照)。
パックされたワイヤ形式
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から始まるインデックス、クラス名マップはモデルアーカイブのメタデータに格納) |
1つのレコードに一致する標準的なPython struct形式は"<iiiifi"です(リトルエンディアン、4つの符号付き整数、1つの浮動小数点数、1つの符号付き整数)。
ランタイムの解析ヘルパー(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パック(boxdecode構成内のbuffers.input[0].size = 20160)を使用すると、デコードされた単一のフレームは次のようになります。
out.kind == SampleKind.Tensorout.payload_tag == "BBOX"out.tensor.dtype == UInt8、out.tensor.shape == [20160]- バイト
[0:4]はリトルエンディアンでNを表します。0 <= N <= 100は、top_k = 100のためです。Nが0の場合、「このフレームで閾値を超える検出がない」という意味であり、0回反復して何も出力しません。 - バイト
[4 : 4 + 24 * N]には有効な検出が含まれており、それ以降のすべてのバイトはゼロ/パディングであり、無視する必要があります。
Pythonでボックスを読み取るには、struct.unpack_fromを使用します。
import struct
payload = out.tensor.copy_payload_bytes()
count = struct.unpack_from("<I", payload, 0)[0]
for i in range(count):
x, y, w, h, score, cls = struct.unpack_from("<iiiifi", payload, 4 + 24 * i)
# (x, y, w, h) in source pixels; x2 = x + w, y2 = y + h
C++では、stages::BoxDecodeヘルパー関数は、この処理を済ませたBoxDecodeResultを返します。result.boxes[i]は、(x, y, x+w, y+h)から(x1, y1, x2, y2)がすでに設定され、画像に合わせてクリップされたBoxです。