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

Зчитування меж виявлених об’єктів з вихідних даних моделі

ПолеЗначення
КатегоріяМоделі та інференс
СкладністьСередній
Орієнтовний час читання15-20 minutes
Міткиpostprocessing, boxdecode, detection

Детектор не повертає прямокутні області безпосередньо. Його вихідні дані – це набір карт ознак, які все ще потребують застосування порогу, придушення локальних максимумів і відображення координат, перш ніж їх можна буде інтерпретувати. SimaBoxDecode – це етап постобробки, який виконує всі ці три операції за один оптимізований крок, перетворюючи тензори, отримані в процесі виведення, на остаточні результати у вигляді пікселів вихідного зображення.

У цьому розділі налаштовується процес декодування: вибирається сімейство моделей за допомогою decode_type, встановлюється поріг впевненості, придушуються перекриття за допомогою порогу NMS IoU, а також обмежується вивід за допомогою top_k. Після цього запускається модель і зчитується кількість виявлених об’єктів. У результаті ви отримаєте налаштований конвеєр детектора та кількість виявлень, отриману з його виводу, а також (у розділі «На практиці» нижче) повний формат даних, щоб ви могли самостійно обробляти межі об’єктів у будь-якому середовищі виконання.

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

Налаштування декодування

Ці параметри визначають як вхідний контракт, так і поведінку постобробки. decode_type (YoloV8 у цьому випадку) визначає шлях декодування для певної родини моделей. Поріг впевненості відкидає слабкі кандидати перед застосуванням NMS; поріг IoU для NMS контролює, наскільки агресивно об’єднуються перекривні рамки; top_k обмежує кінцеву кількість для детермінованого обчислення наступних етапів; а boxdecode_original_width/boxdecode_original_height відображають декодовані координати назад у пікселі вихідного зображення. Рекомендації щодо налаштування кожного з цих параметрів наведено в розділі «На практиці» нижче.

decode_type приймає перелік BoxDecodeType::YoloV8. Значення порогу/NMS/top_k передаються пізніше через stages::BoxDecodeOptions, а не через Model::Options.

tutorials/007_read_detection_boxes/read_detection_boxes.cpp
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 з архіву разом із параметрами прив’язує конфігурацію декодування до моделі, щоб етапи виведення та постобробки, отримані з неї, використовували наведені вище налаштування.

tutorials/007_read_detection_boxes/read_detection_boxes.cpp
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) налаштовує декодування, яке виконується наступним.

tutorials/007_read_detection_boxes/read_detection_boxes.cpp
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() є кількістю виявлених об’єктів.

tutorials/007_read_detection_boxes/read_detection_boxes.cpp
// 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=..., або raw_output_heads=..., якщо в середовищі виконання не налаштовано з’єднання BoxDecode з model.run). Щоб інтегрувати вихідний код C++ з цього розділу у власний проєкт за допомогою спеціального файлу CMakeLists.txt (додаткова тека не потрібна), див. розділ Як запускати навчальні матеріали на головній сторінці.

На практиці

SimaBoxDecode генерує єдиний вихідний тензор, позначений як BBOX. Цей тензор містить стиснутий буфер байтів, який парсер у середовищі виконання інтерпретує як результати детектування у форматі чисел з плаваючою комою. Розуміння дворівневої структури (буфер у форматі, придатного для передачі, проти розібраних Box записів) є ключем до зчитування вихідних даних як з Python, так і з C++.

Тензор BBOX

На етапі декодування для кожного вхідного кадру генерується один тензор BBOX з такими характеристиками:

ПолеЗначення
semantic.detection.format"BBOX"
dtypeUInt8
shapeранг-1: [N_bytes], де N_bytes – це обсяг буфера, стиснутого в архіві моделі (наприклад, [20160] для стандартного пакету YOLOv8)

Форма тензора – це кількість байтів, а не кількість виявлених об’єктів. Запаковані байти містять невеликий заголовок і суміжний масив записів про обмежувальні рамки фіксованого розміру. N_bytes визначається полем buffers.input[0].size в архіві моделі (у файлі конфігурації JSON етапу boxdecode) і обмежує максимальну кількість об’єктів, які декодер може вивести в одному кадрі (див. розділ «Перевизначення контракту» нижче, щоб дізнатися, як розміри в середовищі виконання взаємодіють із запакованими значеннями).

Запакований формат даних

Буфер uint8 розташовано в порядку little-endian:

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 байти:

Зміщення в записіРозмірТипПолеЗначення
04int32xкоордината x верхнього лівого кута, у пікселях вихідного зображення
44int32yкоордината y верхнього лівого кута, у пікселях вихідного зображення
84int32wширина, у пікселях вихідного зображення
124int32hвисота, у пікселях вихідного зображення
164float32scoreвпевненість виявлення після застосування NMS для [0.0, 1.0] (значення, на основі якого відбувається фільтрація за допомогою detection_threshold)
204int32class_idпередбачений ідентифікатор класу (визначається моделлю; індексація починається з 0; відображення імен класів зберігається в метаданих архіву моделі)

Канонічний формат Python struct, що відповідає одному запису, — "<iiiifi" (little-endian, 4 цілих числа зі знаком, одне число з плаваючою комою, одне ціле число зі знаком).

Допоміжні функції для аналізу в середовищі виконання (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 (buffers.input[0].size = 20160 в конфігурації boxdecode), один декодований кадр дає наступний результат:

  • out.kind == SampleKind.Tensor
  • out.payload_tag == "BBOX"
  • out.tensor.dtype == UInt8, out.tensor.shape == [20160]
  • Байти [0:4] передають N у форматі little-endian; 0 <= N <= 100, тому що top_k = 100. Кількість N дорівнює 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] – це Box, де (x1, y1, x2, y2) вже заповнено значеннями з (x, y, x+w, y+h) і обмежено розмірами зображення.

Перевизначення контракту: розміри в середовищі виконання порівняно зі значеннями за замовчуванням в архіві моделі

SimaBoxDecode створюється на основі навченого архіву моделі, який містить значення за замовчуванням для decode_type, detection_threshold, nms_iou_threshold, top_k, original_width та original_height. Публічний конструктор:

SimaBoxDecode(const Model& model,
const std::string& decode_type = "",
int original_width = 0, int original_height = 0,
double detection_threshold = 0.0,
double nms_iou_threshold = 0.0,
int top_k = 0);

і його аналог на Python pyneat.nodes.sima_box_decode(model, ...) використовують просте правило: «позитивні значення перекривають, нульові/порожні значення зберігаються» для кожного поля.

Примітка щодо іменування. detection_threshold – це назва, яка використовується конструктором SimaBoxDecode. ModelOptions.score_threshold (використовується в навчальному посібнику з Python) передається як той самий аргумент. Обидві назви посилаються на один і той самий базовий елемент керування.

Аргумент середовища виконанняПередане значенняПоведінка
decode_type"" (порожній рядок)зберегти архів моделі / використовувати шлях до моделі для виведення результатів
decode_typeНепорожній рядокперезаписати значення архіву моделі для цього запуску
original_width / original_height0зберегти розмірність архіву моделі
original_width / original_heightДодатне ціле числоперезаписати original_width / original_height в ефективній конфігурації
detection_threshold0.0зберегти поріг, що міститься в архіві моделі
detection_threshold> 0.0перезаписати (також активує попередження YOLOv8, зазначене нижче)
nms_iou_threshold0.0зберегти NMS IoU, що міститься в архіві моделі
nms_iou_threshold> 0.0перезаписати
top_k0зберегти значення top-K, що міститься в архіві моделі
top_k> 0перезаписати

Правило застосовується суворо до кожного поля:

  • Шлях Python — у навчальному посібнику замінюються значення кожного поля, оскільки ModelOptions встановлює додатні значення.
  • Шлях C++read_detection_boxes.cpp передає 0.55f, 0.5f, 100 (тобто detection_threshold, nms_iou_threshold та top_k замінюються), плюс bgr.cols, bgr.rows зі знаком плюс (тобто original_width / original_height також замінюються).

Практичні наслідки:

  • Якщо архів вашої моделі був створений для іншої роздільної здатності, ніж у ваших вихідних кадрах, явно вкажіть значення original_width і original_height, щоб координати відповідали пікселям вихідного зображення.
  • Залишення значень detection_threshold і nms_iou_threshold на рівні 0.0 є найбезпечнішим способом отримання перевірених значень за замовчуванням для архіву моделі; змінюйте їх лише тоді, коли ви навмисно налаштовуєте параметри.
  • Будьте обережні, використовуючи низьке значення detection_threshold. Чим нижче це значення, тим більше кандидатських обмежувальних рамок проходять фільтрацію, і обчислювальні витрати NMS зростають пропорційно квадрату кількості обмежувальних рамок, що пройшли фільтрацію, тому дуже низьке значення може значно збільшити обчислювальні витрати та затримку на етапі постобробки. Зменшуйте його лише до необхідного рівня, щоб виявляти слабкі об’єкти; використовуйте його разом із top_k, щоб обмежити найгірший випадок.

Типи декодування та контракти тензорів

BoxDecodeType — це типізований API (simaai::neat::BoxDecodeType / neat.BoxDecodeType), і його завжди слід явно встановлювати для етапів декодування. Наведений нижче контракт середовища виконання походить від internals/gst_plugins/genericboxdecode_v2/gstneatboxdecode.cpp (infer_num_classes, infer_yolo_decoupled_classes, infer_yolo_packed_classes, compute_required_output_size).

Основні правила для тензорних операцій:

  • Типи декодування сімейства YOLO (yolo, yolov5*, yolov7*, yolov8*, yolov9*, yolov10*):
    • Роз’єднані голови: глибина класів-голів має бути повторюваною і дорівнювати > 4.
    • Згруповані голови: глибина кожної голови повинна відповідати вимогам depth = 3 * (num_classes + 5) і бути узгодженою між головами.
  • yolo26: роз’єднані згруповані голови з 4-канальними тензорами необроблених координат обмежувальних рамок (l/t/r/b) і повторюваною глибиною класів-голів > 4.
  • detr: канали класів визначаються на основі максимальної глибини між головами і повинні бути > 4.
  • Інші типи декодування, відмінні від YOLO (effdet, rcnn-stage1, centernet): резервний метод визначення класу використовує максимальну глибину і вимагає > 4.
  • Токени декодування для сегментації (*-seg) дозволяють використовувати розміри вихідних даних, подібні до сегментації, у версії v2 (додає корисне навантаження маски для кожної детекції).
Перелік APIТокен бекендуОчікуваний контракт
BoxDecodeType::YoloyoloРозділений або об’єднаний контракт глибини YOLO
BoxDecodeType::YoloV5yolov5Розділений або об’єднаний контракт глибини YOLO
BoxDecodeType::YoloV5Segyolov5-segКонтракт глибини YOLO + шлях сегментації
BoxDecodeType::YoloV7yolov7Розділений або об’єднаний контракт глибини YOLO
BoxDecodeType::YoloV7Segyolov7-segКонтракт глибини YOLO + шлях сегментації
BoxDecodeType::YoloV8yolov8Розділений або об’єднаний контракт глибини YOLO
BoxDecodeType::YoloV8Segyolov8-segКонтракт глибини YOLO + шлях сегментації
BoxDecodeType::YoloV8Poseyolov8-poseРозділений або об’єднаний контракт глибини YOLO
BoxDecodeType::YoloV9yolov9Розділений або об’єднаний контракт глибини YOLO
BoxDecodeType::YoloV9Segyolov9-segКонтракт глибини YOLO + шлях сегментації
BoxDecodeType::YoloV10yolov10Розділений або об’єднаний контракт глибини YOLO
BoxDecodeType::YoloV10Segyolov10-segКонтракт глибини YOLO + шлях сегментації
BoxDecodeType::YoloV26yolo26YOLO26, згруповані необроблені голови обмежувальних рамок l/t/r/b + голови класифікації
BoxDecodeType::Detrdetrnum_classes = max(depth) (має бути > 4)
BoxDecodeType::EffDeteffdetРезервний висновок із максимальною глибиною (> 4)
BoxDecodeType::RcnnStage1rcnn-stage1Резервний висновок із максимальною глибиною (> 4)
BoxDecodeType::CenternetcenternetРезервний висновок із максимальною глибиною (> 4)

Принцип швидкої обробки помилок:

  • stages::BoxDecodeOptions вимагає явного визначення типу декодування під час створення.
  • stages::BoxDecode(...) та nodes::SimaBoxDecode(...) швидко завершують роботу у разі BoxDecodeType::Unspecified.

Явне встановлення типу декодування:

simaai::neat::stages::BoxDecodeOptions opt(simaai::neat::BoxDecodeType::YoloV8);
opt.detection_threshold = 0.25;
opt.nms_iou_threshold = 0.5;
opt.top_k = 100;
opt = neat.ModelOptions()
opt.decode_type = neat.BoxDecodeType.YoloV8

Повний початковий код

Показати повні програми
tutorials/007_read_detection_boxes/read_detection_boxes.cpp
// Decompose model execution into stages: Preproc -> Infer -> BoxDecode.
//
// Usage:
// tutorial_007_read_detection_boxes --model /path/to/yolo_v8s.tar.gz --image /path/to.jpg

#include "neat.h"

#include "pipeline/StageRun.h"

#include <opencv2/imgcodecs.hpp>

#include <iostream>
#include <stdexcept>
#include <string>

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;
}

} // namespace

int main(int argc, char** argv) {
try {
std::string model_path, image;
if (!get_arg(argc, argv, "--model", model_path) || !get_arg(argc, argv, "--image", image)) {
std::cerr << "Usage: tutorial_007_read_detection_boxes --model <path> --image <path>\n";
return 1;
}

cv::Mat bgr = cv::imread(image, cv::IMREAD_COLOR);
if (bgr.empty())
throw std::runtime_error("failed to load image: " + image);

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;

simaai::neat::Model model(model_path, opt);

// CORE LOGIC
// Stage-by-stage: each stages::* call runs one piece of the model pipeline.
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;

// 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();

std::cout << "boxes=" << decoded.boxes.size() << "\n";
std::cout << "[OK] 007_read_detection_boxes\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "[FAIL] " << e.what() << "\n";
return 1;
}
}

Джерело