Перейти до основного вмісту

Запустіть модель камери MIPI

ПолеЗначення
КатегоріяКамери та потокове передавання
СкладністьСередній
Орієнтовний час читання10-15 minutes
Міткиmipi, camera, live-input, model, ev74

У цьому розділі передбачається, що камера вже працює через накладення на плату та libcamera. Neat не вибирає файли .dtbo і не налаштовує ISP; він обробляє кадри після того, як libcamerasrc їх генерує. Перед запуском навчального посібника перевірте роботу камери за допомогою інструкції з апаратного забезпечення MIPI і перевірки параметрів GStreamer.

Уявіть собі, що цей навчальний посібник – це другий етап. Перший етап – це налаштування камери: накладення, драйвер, libcamera, ISP і точні параметри. Другий етап – це Neat graph: кадри з камери передаються на попередню обробку CVU, для виконання висновків MLA, необов’язковий EV74 BoxDecode і виведення результатів.

Покроковий огляд

Налаштуйте джерело камери

CameraInputOptions описує параметри джерела, які Neat запитує від libcamerasrc: роздільна здатність, частота кадрів, формат і необов’язкове ім’я камери libcamera. Встановіть allow_cpu_fallback = true для поточних конфігурацій камери, які ще не підтримують буфери SiMaAI із нульовим копіюванням. Суворе нульове копіювання все ще доступне через --strict-zero-copy, якщо ваш libcamerasrc це підтримує.

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, щоб у робочому графі не відбулося непомітного перетворення на конвеєр обробки зображень на ЦП. За допомогою --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";
}

Запуск

Запустіть цей посібник безпосередньо на Modalix DevKit з налаштованою камерою MIPI. Запустіть попередньо створені команди з кореневої директорії встановлення Neat; запускайте команди для збірки з вихідного коду з кореневої директорії репозиторію. Архів моделі має відповідати попередній обробці та обраному режиму --decode.

За замовчуванням час очікування завантаження становить 15 секунд. Збільште значення --pull-timeout-ms, коли ви збираєте дані для первинної діагностики на холодній платі.

C++ (prebuilt):

./lib/sima-neat/tutorials/tutorial_023_run_mipi_camera_model --model /шлях/до/моделі.tar.gz --frames 5 --decode none

Для моделей у стилі YOLO, які підтримують маршрут BoxDecode, виберіть токен декодування, наприклад, yolov8 або yolov9seg:

python3 share/sima-neat/tutorials/023_run_mipi_camera_model/run_mipi_camera_model.py \  --model /шлях/до/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 /шлях/до/моделі.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 і порогові значення відповідають моделі.

На практиці

Використовуйте --print-backend, коли потрібно перевірити згенерований шлях GStreamer. Шлях виробництва повинен містити 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;
}
}

Джерело