본문으로 건너뛰기

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):

devkit$./lib/sima-neat/tutorials/tutorial_023_run_mipi_camera_model --model /경로/model.tar.gz --frames 5 --decode none

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

devkit$``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``
devkit$./lib/sima-neat/tutorials/tutorial_023_run_mipi_camera_model \  --model /경로/yolo.tar.gz --frames 5 --decode yolov8

C++ (build from source):

devkit$./build.sh --target tutorial_023_run_mipi_camera_model

devkit$./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;
}
}

소스​