メインコンテンツまでスキップ

MIPIカメラモデルを実行する

項目
カテゴリカメラとストリーミング
難易度中級
推定所要時間10-15 minutes
ラベルmipi, camera, live-input, model, ev74

この章では、カメラがすでにボードのオーバーレイと libcamera を通じて動作していることを前提としています。Neat は、.dtbo ファイルを選択したり、ISP を調整したりしません。代わりに、libcamerasrc がフレームを生成できるようになった時点で、それらのフレームを使用します。チュートリアルを実行する前に、ハードウェア MIPI ガイド と GStreamer の caps チェックを使用して、カメラを検証してください。

このチュートリアルをゲート 2 と考えてください。ゲート 1 は、カメラの初期設定です。具体的には、オーバーレイ、ドライバー、libcamera、ISP、および正確な caps の設定を行います。ゲート 2 は、Neat グラフです。カメラフレームを CVU プリプロセス、MLA 推論、オプションの EV74 BoxDecode に渡し、最後に結果を出力します。

ウォークスルー

カメラソースを構成する

CameraInputOptions は、Neat が libcamerasrc から要求するソース caps を記述します。これには、解像度、フレームレート、フォーマット、およびオプションの libcamera カメラ名が含まれます。SiMaAI ゼロコピーバッファーがまだ公開されていない現在のカメラスタックでは、allow_cpu_fallback = true を設定します。libcamerasrc がサポートしている場合は、--strict-zero-copy を使用して、厳密なゼロコピーを有効にすることもできます。

tutorials/023_run_mipi_camera_model/run_mipi_camera_model.cpp
neat::CameraInputOptions camera;
camera.width = static_cast<std::uint32_t>(int_arg(argc, argv, "--width", 1920));
camera.height = static_cast<std::uint32_t>(int_arg(argc, argv, "--height", 1080));
camera.framerate_num = static_cast<std::uint32_t>(int_arg(argc, argv, "--fps", 30));
camera.framerate_den = 1;
camera.format = "NV12";
camera.buffer_name = "camera0";
camera.allow_cpu_fallback = !has_flag(argc, argv, "--strict-zero-copy");
std::string camera_name;
if (get_arg(argc, argv, "--camera-name", camera_name)) {
camera.camera_name = camera_name;
}

モデルのルートを構成する

モデルは、カメラフレームを NV12 形式の画像として認識します。カラー変換、リサイズ、正規化、量子化、およびテッセレーションのために、モデル管理のプリプロセスを設定します。例では、モデル管理の CVU プリプロセスを EV74 に固定しています。これにより、本番環境のグラフが静かに CPU イメージパイプラインに変わるのを防ぎます。--decode none を使用すると、ルートは MLA で終了し、生のモデルテンソルが返されます。YOLO --decode トークンを使用すると、BoxDecode がモデル管理の EV74 ポストプロセスステージとして実行されます。

tutorials/023_run_mipi_camera_model/run_mipi_camera_model.cpp
const neat::BoxDecodeType decode_type = decode_type_from_token(decode_token);
neat::Model model(model_path, model_options_for_camera(camera, decode_type));

neat::Model::RouteOptions route;
route.include_input = false;
route.include_output = true;
route.upstream_name = camera.buffer_name;
route.buffer_name = camera.buffer_name;
route.name_suffix = "_camera0";
route.advanced_execution.preprocess_target = "EV74";
if (decode_type != neat::BoxDecodeType::Unspecified) {
route.advanced_execution.postprocess_target = "EV74";
}

ソースが所有するグラフを構成する

最初に CameraInput を追加し、次に include_input = false を使用してモデルのルートを追加します。フレームは実行中のパイプライン内で生成されるため、パブリックな Input ノードはありません。include_output = true を使用すると、検出結果またはテンソル用のプルエンドポイントを保持できます。

tutorials/023_run_mipi_camera_model/run_mipi_camera_model.cpp
neat::Graph graph("mipi_camera_model");
graph.add(neat::nodes::CameraInputWithCaptureBuffers(camera, 32));
graph.add(model.graph(route));

if (has_flag(argc, argv, "--print-backend")) {
std::cout << graph.describe_backend(false) << "\n";
}

neat::Run run = graph.build();

出力をプルする

グラフを構築し、固定数の出力をプルします。タイムアウトが発生した場合、--pull-timeout-ms の時間内にモデル出力がアプリケーションに到達しなかったことを意味します。これは、カメラが停止したか、caps がネゴシエートされなかったか、または BoxDecode などの下流ステージでバックプレッシャーが発生した可能性があります。テンソルの数と最初のテンソルの形状を出力して、アプリケーションロジックを追加する前にデータが移動していることを確認します。

tutorials/023_run_mipi_camera_model/run_mipi_camera_model.cpp
for (int i = 0; i < frames; ++i) {
std::optional<neat::Sample> sample = run.pull(/*timeout_ms=*/pull_timeout_ms);
if (!sample.has_value()) {
std::cout << "frame=" << i << " output_timeout timeout_ms=" << pull_timeout_ms;
const std::string last_error = run.last_error();
if (!last_error.empty())
std::cout << " last_error=" << last_error;
std::cout << "\n";
return 2;
}
const neat::TensorList tensors = neat::tensors_from_sample(*sample, true);
std::cout << "frame=" << i << " tensors=" << tensors.size();
if (!tensors.empty())
std::cout << " first_shape=" << shape_string(tensors.front().shape);
std::cout << "\n";
}

実行

MIPIカメラが設定されたModalix DevKit上で、このチュートリアルを直接実行します。Neatのインストールルートから、事前に作成されたコマンドを実行します。リポジトリのルートから、ソースコードからビルドするコマンドを実行します。モデルアーカイブは、要求する前処理およびオプションの--decodeモードと一致する必要があります。

デフォルトのプルタイムアウトは15秒です。起動時の診断情報を収集する際に、--pull-timeout-msの値を大きくしてください(特に、起動直後のボードの場合)。

C++ (prebuilt):

./lib/sima-neat/tutorials/tutorial_023_run_mipi_camera_model--model /path/to/model.tar.gz --frames 5 --decode none

サポートされている BoxDecode ルートを持つ YOLO スタイルのモデルの場合、yolov8yolov9seg などのデコードトークンを選択してください。

python3 share/sima-neat/tutorials/023_run_mipi_camera_model/run_mipi_camera_model.py \  --model /path/to/yolo.tar.gz --frames 5 --decode yolov8
./lib/sima-neat/tutorials/tutorial_023_run_mipi_camera_model--model /path/to/yolo.tar.gz --frames 5 --decode yolov8

C++ (build from source):

./build.sh --target tutorial_023_run_mipi_camera_model

./build/tutorials-standalone/tutorial_023_run_mipi_camera_model \  --model /モデルへのパス/model.tar.gz --frames 5 --decode none

期待される出力形状は、モデルとデコード経路によって異なります。生のMLA出力には通常、モデル固有のテンソルが含まれます。

frame=0 tensors=<raw_tensor_count> first_shape=[<model_specific_shape>]
frame=1 tensors=<raw_tensor_count> first_shape=[<model_specific_shape>]
frame=2 tensors=<raw_tensor_count> first_shape=[<model_specific_shape>]
frame=3 tensors=<raw_tensor_count> first_shape=[<model_specific_shape>]
frame=4 tensors=<raw_tensor_count> first_shape=[<model_specific_shape>]
[OK] 023_run_mipi_camera_model

サポートされているBoxDecode経路を使用すると、出力はデコードされた検出またはセグメンテーションテンソルに変わります。テンソルの数と最初の形状を、普遍的な契約としてではなく、移動の確認として使用してください。

output_timeoutが表示された場合は、gst-launch-1.0を使用してカメラを検証し、次に--print-backendを使用して生成されたバックエンドを検査します。BoxDecode経路の場合、モデルアーカイブ、--decodeトークン、および閾値がモデルと一致することを確認してください。

実践

生成された GStreamer パスを調べたい場合は、--print-backend を使用してください。フォールバックが有効になっている場合、実行パスには libcamerasrcneatcamerabridgeneatprocesscvuneatprocessmla、オプションの EV74 ポストプロセス、および appsink が含まれている必要があります。デバッグ専用のパスを意図的に追加した場合を除き、appsrcostosimavideoconvert、または videoscale は含まれてはなりません。

完全なソース

完全なソースプログラムを表示
tutorials/023_run_mipi_camera_model/run_mipi_camera_model.cpp
// Run a model from a MIPI/libcamera camera source.
//
// Usage:
// tutorial_023_run_mipi_camera_model --model /path/to/model.tar.gz [--frames 5]

#include <neat.h>

#include <algorithm>
#include <cctype>
#include <cstdint>
#include <filesystem>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>

namespace neat = simaai::neat;
namespace fs = std::filesystem;

namespace {

bool get_arg(int argc, char** argv, const std::string& key, std::string& out) {
for (int i = 1; i + 1 < argc; ++i) {
if (key == argv[i]) {
out = argv[i + 1];
return true;
}
}
return false;
}

bool has_flag(int argc, char** argv, const std::string& key) {
for (int i = 1; i < argc; ++i) {
if (key == argv[i])
return true;
}
return false;
}

int int_arg(int argc, char** argv, const std::string& key, int def) {
std::string value;
if (!get_arg(argc, argv, key, value))
return def;
return std::stoi(value);
}

std::string lower_copy(std::string value) {
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return value;
}

neat::BoxDecodeType decode_type_from_token(const std::string& token) {
const std::string v = lower_copy(token);
if (v.empty() || v == "none" || v == "raw")
return neat::BoxDecodeType::Unspecified;
if (v == "yolo")
return neat::BoxDecodeType::Yolo;
if (v == "yolov5")
return neat::BoxDecodeType::YoloV5;
if (v == "yolov8")
return neat::BoxDecodeType::YoloV8;
if (v == "yolov8seg" || v == "yolov8-seg")
return neat::BoxDecodeType::YoloV8Seg;
if (v == "yolov9")
return neat::BoxDecodeType::YoloV9;
if (v == "yolov9seg" || v == "yolov9-seg")
return neat::BoxDecodeType::YoloV9Seg;
throw std::runtime_error("unsupported --decode token: " + token);
}

template <typename Shape> std::string shape_string(const Shape& shape) {
std::string out = "[";
for (std::size_t i = 0; i < shape.size(); ++i) {
out += std::to_string(shape[i]);
if (i + 1 < shape.size())
out += ",";
}
out += "]";
return out;
}

neat::Model::Options model_options_for_camera(const neat::CameraInputOptions& camera,
neat::BoxDecodeType decode_type) {
neat::Model::Options options;
options.preprocess.kind = neat::InputKind::Image;
options.preprocess.input_max_width = static_cast<int>(camera.width);
options.preprocess.input_max_height = static_cast<int>(camera.height);
options.preprocess.input_max_depth = 3;
options.preprocess.color_convert.input_format = neat::PreprocessColorFormat::NV12;
options.preprocess.color_convert.output_format = neat::PreprocessColorFormat::RGB;
options.preprocess.resize.enable = neat::AutoFlag::On;
options.preprocess.resize.width = 640;
options.preprocess.resize.height = 640;
options.preprocess.resize.mode = neat::ResizeMode::Letterbox;
options.preprocess.resize.pad_value = 114;
options.preprocess.preset = neat::NormalizePreset::COCO_YOLO;
options.advanced_execution.preprocess_target = "EV74";
options.decode_type = decode_type;
if (decode_type == neat::BoxDecodeType::Unspecified) {
options.inference_terminal.mla_only = true;
} else {
options.advanced_execution.postprocess_target = "EV74";
options.score_threshold = 0.25f;
options.nms_iou_threshold = 0.45f;
options.top_k = 100;
}
return options;
}

void usage(const char* argv0) {
std::cerr << "Usage: " << argv0
<< " --model <model.tar.gz> [--frames 5] [--width 1920] [--height 1080] "
"[--fps 30] [--camera-name NAME] [--decode none|yolov8|yolov9seg] "
"[--pull-timeout-ms 15000] [--strict-zero-copy] [--print-backend]\n";
}

} // namespace

int main(int argc, char** argv) {
try {
std::string model_path;
if (!get_arg(argc, argv, "--model", model_path)) {
usage(argv[0]);
return 1;
}
if (!fs::exists(model_path))
throw std::runtime_error("model archive not found: " + model_path);

const int frames = int_arg(argc, argv, "--frames", 5);
if (frames <= 0)
throw std::runtime_error("--frames must be positive");
const int pull_timeout_ms = int_arg(argc, argv, "--pull-timeout-ms", 15000);
if (pull_timeout_ms <= 0)
throw std::runtime_error("--pull-timeout-ms must be positive");

std::string decode_token = "none";
get_arg(argc, argv, "--decode", decode_token);

// CORE LOGIC
neat::CameraInputOptions camera;
camera.width = static_cast<std::uint32_t>(int_arg(argc, argv, "--width", 1920));
camera.height = static_cast<std::uint32_t>(int_arg(argc, argv, "--height", 1080));
camera.framerate_num = static_cast<std::uint32_t>(int_arg(argc, argv, "--fps", 30));
camera.framerate_den = 1;
camera.format = "NV12";
camera.buffer_name = "camera0";
camera.allow_cpu_fallback = !has_flag(argc, argv, "--strict-zero-copy");
std::string camera_name;
if (get_arg(argc, argv, "--camera-name", camera_name)) {
camera.camera_name = camera_name;
}

const neat::BoxDecodeType decode_type = decode_type_from_token(decode_token);
neat::Model model(model_path, model_options_for_camera(camera, decode_type));

neat::Model::RouteOptions route;
route.include_input = false;
route.include_output = true;
route.upstream_name = camera.buffer_name;
route.buffer_name = camera.buffer_name;
route.name_suffix = "_camera0";
route.advanced_execution.preprocess_target = "EV74";
if (decode_type != neat::BoxDecodeType::Unspecified) {
route.advanced_execution.postprocess_target = "EV74";
}

neat::Graph graph("mipi_camera_model");
graph.add(neat::nodes::CameraInputWithCaptureBuffers(camera, 32));
graph.add(model.graph(route));

if (has_flag(argc, argv, "--print-backend")) {
std::cout << graph.describe_backend(false) << "\n";
}

neat::Run run = graph.build();

for (int i = 0; i < frames; ++i) {
std::optional<neat::Sample> sample = run.pull(/*timeout_ms=*/pull_timeout_ms);
if (!sample.has_value()) {
std::cout << "frame=" << i << " output_timeout timeout_ms=" << pull_timeout_ms;
const std::string last_error = run.last_error();
if (!last_error.empty())
std::cout << " last_error=" << last_error;
std::cout << "\n";
return 2;
}
const neat::TensorList tensors = neat::tensors_from_sample(*sample, true);
std::cout << "frame=" << i << " tensors=" << tensors.size();
if (!tensors.empty())
std::cout << " first_shape=" << shape_string(tensors.front().shape);
std::cout << "\n";
}

std::cout << "[OK] 023_run_mipi_camera_model\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "[FAIL] " << e.what() << "\n";
return 1;
}
}

ソース