본문으로 건너뛰기

MIPI 카메라 모델 실행

필드
범주카메라 및 스트리밍
난이도중급
예상 소요 시간10-15 minutes
레이블mipi, camera, live-input, model, ev74

이 장에서는 카메라가 이미 보드 오버레이 및 libcamera를 통해 작동한다고 가정합니다. Neat는 .dtbo 파일을 선택하거나 ISP를 조정하지 않습니다. 대신 libcamerasrc가 프레임을 생성할 수 있게 되면 해당 프레임을 사용합니다. 튜토리얼을 실행하기 전에 하드웨어 MIPI 가이드 및 GStreamer 캡 검사를 사용하여 카메라를 확인합니다.

이 튜토리얼을 2단계로 생각하십시오. 1단계는 카메라 설정입니다. 여기에는 오버레이, 드라이버, libcamera, ISP 및 정확한 캡 설정이 포함됩니다. 2단계는 Neat 그래프입니다. 여기에는 카메라 프레임을 CVU 전처리, MLA 추론, 선택적 EV74 BoxDecode로 전달하고 출력을 가져오는 작업이 포함됩니다.

둘러보기

카메라 소스 구성

CameraInputOptions는 Neat가 libcamerasrc에서 요청하는 소스 캡(해상도, 프레임 속도, 형식 및 선택적 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 전에 모델 출력이 앱에 도달하지 못했음을 의미합니다. 카메라가 중지되었거나, 캡이 협상되지 않았거나, 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 /경로/model.tar.gz --frames 5 --decode none

지원되는 BoxDecode 경로를 사용하는 YOLO 스타일 모델의 경우, yolov8 또는 yolov9seg와 같은 디코딩 토큰을 선택하십시오.

``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 /경로/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를 사용하십시오. 프로덕션 경로에는 폴백이 활성화된 경우 libcamerasrc, neatcamerabridge, neatprocesscvu, neatprocessmla, 선택 사항인 EV74 후처리, 그리고 appsink가 포함되어야 합니다. 의도적으로 디버그 전용 경로를 추가하지 않은 한 appsrc, ostosima, videoconvert 또는 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;
}
}

소스