
.. _program_listing_file_Test_commit_yolo_detector_test.cpp:

Program Listing for File yolo_detector_test.cpp
===============================================

|exhale_lsh| :ref:`Return to documentation for file <file_Test_commit_yolo_detector_test.cpp>` (``Test/commit/yolo_detector_test.cpp``)

.. |exhale_lsh| unicode:: U+021B0 .. UPWARDS ARROW WITH TIP LEFTWARDS

.. code-block:: cpp

   #include "kataglyphis_test_support.h"
   
   #include <gtest/gtest.h>
   
   #include <algorithm>
   #include <chrono>
   #include <climits>
   #include <condition_variable>
   #include <cstddef>
   #include <cstdint>
   #include <cstring>
   #include <initializer_list>
   #include <limits>
   #include <mutex>
   #include <string>
   #include <string_view>
   #include <utility>
   #include <vector>
   
   #include "kInferenceContract.inc"
   #include "kShippedInferenceConfig.inc"
   
   import nlohmann.json;
   import kataglyphis.gstreamer_pipeline;
   import kataglyphis.toml_config;
   import kataglyphis.yolo_detector;
   
   #include "gstreamer_test_support.h"
   
   namespace detection = kataglyphis::detection;
   namespace gstreamer = kataglyphis::gstreamer;
   using kataglyphis::test::TempFile;
   using kataglyphis::test::YoloReshapeGeometry;
   
   namespace {
   
   // Two classes, so a row is cx, cy, w, h, objectness, person, bicycle: six rows of seven values.
   constexpr YoloReshapeGeometry kTwoClasses{ 2, 7, 6, 7 };
   
   struct Row
   { float cx, cy, w, h, objectness, person, bicycle; };
   
   auto flatten(std::initializer_list<Row> rows) -> std::vector<float>
   {
       std::vector<float> data;
       for (const auto &row : rows) {
           data.insert(data.end(), { row.cx, row.cy, row.w, row.h, row.objectness, row.person, row.bicycle });
       }
       return data;
   }
   
   struct Corner
   { float x1, y1, x2, y2, score, class_index; };
   
   auto flatten_corners(std::initializer_list<Corner> rows) -> std::vector<float>
   {
       std::vector<float> data;
       for (const auto &row : rows) {
           data.insert(data.end(), { row.x1, row.y1, row.x2, row.y2, row.score, row.class_index });
       }
       return data;
   }
   
   auto two_class_config(const TempFile &model) -> detection::YoloConfig
   {
       detection::YoloConfig config;
       config.model_path = model.path();
       config.input_width = static_cast<std::uint32_t>(kTwoClasses.width);
       config.input_height = static_cast<std::uint32_t>(kTwoClasses.height);
       config.num_classes = 2;
       config.confidence_threshold = 0.5F;
       config.nms_threshold = 0.45F;
       return config;
   }
   
   }// namespace
   
   TEST(YoloDetector, CocoClassNamesCoverTheEightyClasses)
   {
       EXPECT_EQ(detection::YoloDetector::get_coco_class_name(0), "person");
       EXPECT_EQ(detection::YoloDetector::get_coco_class_name(15), "cat");
       EXPECT_EQ(detection::YoloDetector::get_coco_class_name(16), "dog");
       EXPECT_EQ(detection::YoloDetector::get_coco_class_name(79), "toothbrush");
       for (const int outside : { -1, 80, 1000, INT_MIN, INT_MAX }) {
           EXPECT_EQ(detection::YoloDetector::get_coco_class_name(outside), "unknown") << outside;
       }
   }
   
   TEST(YoloDetector, TheShippedConfigLabelsMatchTheClassTable)
   {
       const auto config = kataglyphis::config::parse_inference_config(std::string(kShippedInferenceConfig));
       ASSERT_TRUE(config.has_value());
       ASSERT_EQ(config->model.labels.size(), 80U);
       for (std::size_t i = 0; i < config->model.labels.size(); ++i) {
           EXPECT_EQ(config->model.labels[i], detection::YoloDetector::get_coco_class_name(static_cast<int>(i))) << i;
       }
   }
   
   TEST(YoloDetector, DetectBeforeInitializeFails)
   {
       detection::YoloDetector detector;
       EXPECT_FALSE(detector.is_initialized());
       const std::vector<float> image(3 * 640 * 640, 0.0F);
       const auto result = detector.detect(image, 640, 640);
       ASSERT_FALSE(result.has_value());
       EXPECT_EQ(result.error(), detection::OnnxError::SessionNotInitialized);
   }
   
   TEST(YoloDetector, AMissingModelIsModelLoadFailed)
   {
       detection::YoloDetector detector;
       detection::YoloConfig config;
       config.model_path = kataglyphis::test::missing_path("yolo.onnx");
       const auto result = detector.initialize(config);
       ASSERT_FALSE(result.has_value());
       EXPECT_EQ(result.error(), detection::OnnxError::ModelLoadFailed);
       EXPECT_FALSE(detector.is_initialized());
   }
   
   TEST(YoloDetector, DecodesThresholdsClassesNmsAndScale)
   {
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::YoloDetector detector;
       ASSERT_TRUE(detector.initialize(two_class_config(model)).has_value());
       ASSERT_TRUE(detector.is_initialized());
   
       const auto rows = flatten({
         { 10, 10, 4, 4, 0.9F, 0.9F, 0.1F },// person 0.81: kept
         { 10.5F, 10, 4, 4, 0.8F, 0.9F, 0.1F },// person 0.72 overlapping the first: suppressed
         { 10, 10, 4, 4, 0.9F, 0.2F, 0.8F },// bicycle 0.72 on the same box: kept, another class
         { 40, 30, 2, 2, 0.4F, 1.0F, 0.0F },// objectness under the threshold: dropped
         { 40, 30, 2, 2, 0.9F, 0.3F, 0.2F },// best class too weak, 0.27: dropped
         { 30, 5, 6, 2, 0.7F, 0.95F, 0.0F },// person 0.665, disjoint: kept
       });
   
       // The frame is twice as wide and three times as tall as the model input.
       const auto result = detector.detect(rows, 14, 6);
       ASSERT_TRUE(result.has_value());
       EXPECT_EQ(result->original_width, 14U);
       EXPECT_EQ(result->original_height, 6U);
       ASSERT_EQ(result->boxes.size(), 3U);
   
       const auto &first = result->boxes[0];
       EXPECT_EQ(first.class_id, 0);
       EXPECT_EQ(first.class_name, "person");
       EXPECT_NEAR(first.confidence, 0.81F, 1e-5F);
       EXPECT_FLOAT_EQ(first.x, (10.0F - 2.0F) * 2.0F);
       EXPECT_FLOAT_EQ(first.y, (10.0F - 2.0F) * 3.0F);
       EXPECT_FLOAT_EQ(first.width, 8.0F);
       EXPECT_FLOAT_EQ(first.height, 12.0F);
   
       EXPECT_EQ(result->boxes[1].class_name, "bicycle");
       EXPECT_NEAR(result->boxes[1].confidence, 0.72F, 1e-5F);
       EXPECT_EQ(result->boxes[2].class_name, "person");
       EXPECT_NEAR(result->boxes[2].confidence, 0.665F, 1e-5F);
       EXPECT_FLOAT_EQ(result->boxes[2].x, (30.0F - 3.0F) * 2.0F);
   }
   
   TEST(YoloDetector, BoxesComeBackSortedByConfidence)
   {
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::YoloDetector detector;
       ASSERT_TRUE(detector.initialize(two_class_config(model)).has_value());
       const auto rows = flatten({
         { 1, 1, 1, 1, 0.6F, 0.9F, 0 },
         { 10, 1, 1, 1, 0.9F, 0.9F, 0 },
         { 20, 1, 1, 1, 0.7F, 0.9F, 0 },
         { 30, 1, 1, 1, 1.0F, 0.9F, 0 },
         { 40, 1, 1, 1, 0.8F, 0.9F, 0 },
         { 50, 1, 1, 1, 0.65F, 0.9F, 0 },
       });
       const auto result = detector.detect(rows, 7, 2);
       ASSERT_TRUE(result.has_value());
       ASSERT_EQ(result->boxes.size(), 6U);
       for (std::size_t i = 1; i < result->boxes.size(); ++i) {
           EXPECT_GE(result->boxes[i - 1].confidence, result->boxes[i].confidence);
       }
   }
   
   TEST(YoloDetector, FiveValueRowsUseTheObjectnessAsConfidence)
   {
       constexpr YoloReshapeGeometry kNoClasses{ 2, 5, 6, 5 };
       const TempFile model("kataglyphis-yolo5", ".onnx", kataglyphis::test::yolo_reshape_model(kNoClasses));
       detection::YoloDetector detector;
       detection::YoloConfig config;
       config.model_path = model.path();
       config.input_width = 5;
       config.input_height = 2;
       config.confidence_threshold = 0.5F;
       ASSERT_TRUE(detector.initialize(config).has_value());
   
       std::vector<float> rows(kNoClasses.elements(), 0.0F);
       const std::vector<float> only_row{ 2, 2, 2, 2, 0.6F };
       std::copy(only_row.begin(), only_row.end(), rows.begin());
       const auto result = detector.detect(rows, 5, 2);
       ASSERT_TRUE(result.has_value());
       ASSERT_EQ(result->boxes.size(), 1U);
       EXPECT_FLOAT_EQ(result->boxes[0].confidence, 0.6F);
       EXPECT_EQ(result->boxes[0].class_id, 0);
   }
   
   TEST(YoloDetector, CornerRowsDecodeScoreClassAndScale)
   {
       // x1, y1, x2, y2, score, class: the DEIM and YOLOv10 layout, six rows of six values.
       constexpr YoloReshapeGeometry kCorners{ 2, 6, 6, 6 };
       const TempFile model("kataglyphis-corners", ".onnx", kataglyphis::test::yolo_reshape_model(kCorners));
       detection::YoloDetector detector;
       detection::YoloConfig config;
       config.model_path = model.path();
       config.input_width = 6;
       config.input_height = 2;
       config.confidence_threshold = 0.5F;
       config.output_layout = detection::YoloOutputLayout::CornersScoreClass;
       ASSERT_TRUE(detector.initialize(config).has_value());
   
       const auto rows = flatten_corners({
         { 1, 0.5F, 3, 1.5F, 0.9F, 15 },// cat: kept
         { 1, 0.5F, 3, 1.5F, 0.3F, 15 },// under the threshold: dropped
         { 4, 0, 5, 1, 0.7F, 0 },// person: kept
         { 4, 0, 5, 1, 0.6F, 0 },// the same person again: suppressed
         { 0, 0, 1, 1, 0.8F, 80 },// no such class: dropped
         { 2, 0, 1, 1, 0.8F, 2 },// x2 before x1: dropped
       });
       // The frame is twice the input's size each way.
       const auto result = detector.detect(rows, 12, 4);
       ASSERT_TRUE(result.has_value());
       ASSERT_EQ(result->boxes.size(), 2U);
       const auto &cat = result->boxes[0];
       EXPECT_EQ(cat.class_id, 15);
       EXPECT_EQ(cat.class_name, "cat");
       EXPECT_FLOAT_EQ(cat.confidence, 0.9F);
       EXPECT_FLOAT_EQ(cat.x, 2.0F);
       EXPECT_FLOAT_EQ(cat.y, 1.0F);
       EXPECT_FLOAT_EQ(cat.width, 4.0F);
       EXPECT_FLOAT_EQ(cat.height, 2.0F);
       EXPECT_EQ(result->boxes[1].class_name, "person");
       EXPECT_FLOAT_EQ(result->boxes[1].confidence, 0.7F);
   }
   
   TEST(YoloDetector, ShortRowsNeverReadTheNextRowsValues)
   {
       // One class score per row while the detector expects eighty: the next row's cx must not become class 1.
       constexpr YoloReshapeGeometry kOneScore{ 2, 6, 6, 6 };
       const TempFile model("kataglyphis-yolo6", ".onnx", kataglyphis::test::yolo_reshape_model(kOneScore));
       detection::YoloDetector detector;
       detection::YoloConfig config;
       config.model_path = model.path();
       config.input_width = 6;
       config.input_height = 2;
       config.confidence_threshold = 0.4F;
       ASSERT_TRUE(detector.initialize(config).has_value());
   
       std::vector<float> rows(kOneScore.elements(), 0.0F);
       const std::vector<float> first_two{ 3, 3, 2, 2, 0.9F, 0.5F, 0.99F, 0, 0, 0, 0, 0 };
       std::copy(first_two.begin(), first_two.end(), rows.begin());
       const auto result = detector.detect(rows, 6, 2);
       ASSERT_TRUE(result.has_value());
       ASSERT_EQ(result->boxes.size(), 1U);
       EXPECT_EQ(result->boxes[0].class_id, 0);
       EXPECT_NEAR(result->boxes[0].confidence, 0.45F, 1e-5F);
   }
   
   TEST(YoloDetector, NonFiniteValuesNeverBecomeBoxes)
   {
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::YoloDetector detector;
       ASSERT_TRUE(detector.initialize(two_class_config(model)).has_value());
       constexpr float kNaN = std::numeric_limits<float>::quiet_NaN();
       constexpr float kInf = std::numeric_limits<float>::infinity();
       const auto rows = flatten({
         { 1, 1, 1, 1, kNaN, 0.9F, 0 },
         { 1, 1, 1, 1, kInf, 0.9F, 0 },
         { kNaN, 1, 1, 1, 0.9F, 0.9F, 0 },
         { 1, 1, kInf, 1, 0.9F, 0.9F, 0 },
         { 1, 1, 1, 1, 0.9F, kNaN, kNaN },
         { 1, 1, 1, -kInf, 0.9F, 0.9F, 0 },
       });
       const auto result = detector.detect(rows, 7, 2);
       ASSERT_TRUE(result.has_value());
       EXPECT_TRUE(result->boxes.empty());
   }
   
   TEST(YoloDetector, AWrongSizedImageIsInvalidInputShape)
   {
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::YoloDetector detector;
       ASSERT_TRUE(detector.initialize(two_class_config(model)).has_value());
       const std::vector<float> rows(kTwoClasses.elements() - 1, 0.0F);
       const auto result = detector.detect(rows, 7, 2);
       ASSERT_FALSE(result.has_value());
       EXPECT_EQ(result.error(), detection::OnnxError::InvalidInputShape);
   }
   
   TEST(YoloDetector, AMovedDetectorStillDetects)
   {
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::YoloDetector detector;
       ASSERT_TRUE(detector.initialize(two_class_config(model)).has_value());
       detection::YoloDetector moved(std::move(detector));
       const auto rows = flatten({ { 3, 1, 2, 2, 0.9F, 0.9F, 0 }, {}, {}, {}, {}, {} });
       const auto result = moved.detect(rows, 7, 2);
       ASSERT_TRUE(result.has_value());
       EXPECT_EQ(result->boxes.size(), 1U);
   }
   
   TEST(YoloDetectorGStreamer, DecodesAFramePushedThroughAppsrc)
   {
       KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("appsrc", "appsink");
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::YoloDetector detector;
       ASSERT_TRUE(detector.initialize(two_class_config(model)).has_value());
   
       gstreamer::GStreamerPipeline pipeline;
       ASSERT_TRUE(pipeline
           .create_pipeline_from_string("appsrc name=src format=time "
                                        "caps=video/x-raw,format=RGB,width=14,height=6,framerate=0/1 "
                                        "! appsink name=sink sync=false")
           .has_value());
       ASSERT_TRUE(pipeline.start().has_value());
   
       // RGB rows align to four bytes; white normalises to one in every NCHW channel.
       std::vector<std::uint8_t> pixels(44 * 6, 255);
       gstreamer::FrameMetadata metadata;
       metadata.timestamp_ns = 0;
       metadata.duration_ns = 1000;
       ASSERT_TRUE(pipeline.push_buffer(pixels.data(), pixels.size(), metadata).has_value());
   
       const auto result = detector.detect_from_gstreamer(pipeline, 10000);
       ASSERT_TRUE(result.has_value());
       EXPECT_EQ(result->original_width, 14U);
       EXPECT_EQ(result->original_height, 6U);
       ASSERT_EQ(result->boxes.size(), 1U);
       EXPECT_EQ(result->boxes[0].class_name, "person");
       EXPECT_FLOAT_EQ(result->boxes[0].width, 2.0F);
       EXPECT_FLOAT_EQ(result->boxes[0].height, 3.0F);
       EXPECT_TRUE(pipeline.stop().has_value());
   }
   
   TEST(YoloDetectorGStreamer, RejectsFramesWithoutVideoGeometry)
   {
       KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("appsrc", "appsink");
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::YoloDetector detector;
       ASSERT_TRUE(detector.initialize(two_class_config(model)).has_value());
   
       gstreamer::GStreamerPipeline pipeline;
       ASSERT_TRUE(
         pipeline.create_pipeline_from_string("appsrc name=src format=time ! appsink name=sink sync=false").has_value());
       ASSERT_TRUE(pipeline.start().has_value());
   
       gstreamer::FrameMetadata metadata;
       std::vector<float> too_short(kTwoClasses.elements() - 1, 0.0F);
       ASSERT_TRUE(pipeline.push_buffer(too_short.data(), too_short.size() * sizeof(float), metadata).has_value());
       const auto short_frame = detector.detect_from_gstreamer(pipeline, 10000);
       ASSERT_FALSE(short_frame.has_value());
       EXPECT_EQ(short_frame.error(), detection::OnnxError::InvalidInputShape);
   
       std::vector<std::uint8_t> odd_bytes(7, 1);
       ASSERT_TRUE(pipeline.push_buffer(odd_bytes.data(), odd_bytes.size(), metadata).has_value());
       const auto odd_frame = detector.detect_from_gstreamer(pipeline, 10000);
       ASSERT_FALSE(odd_frame.has_value());
       EXPECT_EQ(odd_frame.error(), detection::OnnxError::InvalidInputShape);
       EXPECT_TRUE(pipeline.stop().has_value());
   }
   
   TEST(YoloDetectorGStreamer, APipelineWithoutAppsinkGivesNoInput)
   {
       KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("fakesrc", "fakesink");
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::YoloDetector detector;
       ASSERT_TRUE(detector.initialize(two_class_config(model)).has_value());
       gstreamer::GStreamerPipeline pipeline;
       ASSERT_TRUE(pipeline.create_pipeline_from_string("fakesrc num-buffers=1 ! fakesink").has_value());
       const auto result = detector.detect_from_gstreamer(pipeline, 100);
       ASSERT_FALSE(result.has_value());
       EXPECT_EQ(result.error(), detection::OnnxError::InputAllocationFailed);
   }
   
   TEST(VideoDetectorPipeline, RunsTheDetectorOnEveryFrame)
   {
       KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("videotestsrc", "capsfilter", "appsink");
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
   
       detection::VideoDetectionConfig config;
       config.yolo_config = two_class_config(model);
       config.gstreamer_config.pipeline_description =
         "videotestsrc num-buffers=3 pattern=black ! video/x-raw,format=RGB,width=640,height=480,framerate=30/1 "
         "! appsink name=sink sync=false";
   
       detection::VideoDetectorPipeline pipeline;
       ASSERT_TRUE(pipeline.initialize(config).has_value());
       EXPECT_FALSE(pipeline.is_running());
   
       kataglyphis::test::EventCounter frames;
       kataglyphis::test::EventCounter detections;
       std::mutex sizes_mutex;
       std::vector<std::uint32_t> widths;
       pipeline.set_frame_callback([&](const gstreamer::BufferInfo &) { frames.add(); });
       pipeline.set_detection_callback([&](const detection::DetectionResult &result, const gstreamer::BufferInfo &) {
           {
               std::scoped_lock lock(sizes_mutex);
               widths.push_back(result.original_width);
           }
           detections.add();
       });
   
       ASSERT_TRUE(pipeline.start().has_value());
       EXPECT_TRUE(pipeline.is_running());
       EXPECT_TRUE(frames.wait_for(3, kataglyphis::test::kStreamTimeout));
       EXPECT_TRUE(detections.wait_for(3, kataglyphis::test::kStreamTimeout));
       EXPECT_TRUE(pipeline.stop().has_value());
       EXPECT_FALSE(pipeline.is_running());
   
       EXPECT_EQ(frames.count(), 3);
       EXPECT_EQ(detections.count(), 3);
       std::scoped_lock lock(sizes_mutex);
       EXPECT_EQ(widths, (std::vector<std::uint32_t>{ 640, 640, 640 }));
   }
   
   TEST(VideoDetectorPipeline, ReportsUnsupportedFramesThroughTheErrorCallback)
   {
       KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("videotestsrc", "capsfilter", "appsink");
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::VideoDetectionConfig config;
       config.yolo_config = two_class_config(model);
       config.gstreamer_config.pipeline_description =
         "videotestsrc num-buffers=2 pattern=black ! video/x-raw,format=GRAY8,width=8,height=4 ! appsink sync=false";
       detection::VideoDetectorPipeline pipeline;
       ASSERT_TRUE(pipeline.initialize(config).has_value());
       kataglyphis::test::EventCounter errors;
       pipeline.set_error_callback([&](detection::OnnxError error) {
           EXPECT_EQ(error, detection::OnnxError::InvalidInputType);
           errors.add();
       });
       ASSERT_TRUE(pipeline.start().has_value());
       EXPECT_TRUE(errors.wait_for(2, kataglyphis::test::kStreamTimeout));
       EXPECT_TRUE(pipeline.stop().has_value());
       EXPECT_EQ(pipeline.last_error(), detection::OnnxError::InvalidInputType);
   }
   
   TEST(FramePreprocessing, ReadsPaddedRgbRowsAndNormalisesIntoSeparateChannels)
   {
       gstreamer::BufferInfo frame;
       frame.metadata.width = 2;
       frame.metadata.height = 2;
       frame.metadata.format = "RGB";
       frame.metadata.row_stride_bytes = 8;
       frame.metadata.plane_offset_bytes = 3;
       const std::vector<unsigned char> bytes{ 9, 9, 9, 255, 0, 0, 0, 255, 0, 9, 9, 0, 0, 255, 255, 255, 255 };
       for (const auto value : bytes) { frame.data.push_back(static_cast<std::byte>(value)); }
       frame.size = frame.data.size();
       const auto result = detection::preprocess_frame(frame, 2, 2);
       ASSERT_TRUE(result.has_value());
       EXPECT_EQ(result->data, (std::vector<float>{ 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1 }));
       frame.metadata.row_stride_bytes = 5;
       EXPECT_EQ(
         kataglyphis::test::error_of(detection::preprocess_frame(frame, 2, 2)), detection::OnnxError::InvalidInputShape);
       frame.metadata.row_stride_bytes = 8;
       frame.size--;
       EXPECT_EQ(
         kataglyphis::test::error_of(detection::preprocess_frame(frame, 2, 2)), detection::OnnxError::InvalidInputShape);
   }
   
   TEST(FramePreprocessing, MatchesTheSharedModelAntContract)
   {
       const auto contract = nlohmann::json::parse(kInferenceContract, nullptr, false);
       ASSERT_FALSE(contract.is_discarded());
       ASSERT_EQ(contract["schema_version"], 1);
       for (const auto &item : contract["cases"]) {
           SCOPED_TRACE(item["name"].get<std::string>());
           gstreamer::BufferInfo frame;
           frame.metadata.width = item["width"].get<std::uint32_t>();
           frame.metadata.height = item["height"].get<std::uint32_t>();
           frame.metadata.format = item["pixel_format"].get<std::string>();
           frame.metadata.row_stride_bytes = item["row_stride_bytes"].get<std::int64_t>();
           frame.metadata.plane_offset_bytes = item["plane_offset_bytes"].get<std::size_t>();
           for (const auto value : item["data"].get<std::vector<unsigned char>>()) {
               frame.data.push_back(static_cast<std::byte>(value));
           }
           frame.size = frame.data.size();
           const auto mode = item["mode"] == "letterbox" ? detection::FramePreprocessMode::Letterbox
                                                         : detection::FramePreprocessMode::Stretch;
           const auto result = detection::preprocess_frame(
             frame, item["input_width"].get<std::uint32_t>(), item["input_height"].get<std::uint32_t>(), mode);
           ASSERT_TRUE(result.has_value());
           const auto expected = item["expected_nchw"].get<std::vector<float>>();
           ASSERT_EQ(result->data.size(), expected.size());
           for (std::size_t i = 0; i < expected.size(); ++i) { EXPECT_NEAR(result->data[i], expected[i], 1e-6F) << i; }
           EXPECT_NEAR(result->mapping.scale_x, item["mapping"]["scale_x"].get<float>(), 1e-6F);
           EXPECT_NEAR(result->mapping.scale_y, item["mapping"]["scale_y"].get<float>(), 1e-6F);
           EXPECT_FLOAT_EQ(result->mapping.pad_x, item["mapping"]["pad_x"].get<float>());
           EXPECT_FLOAT_EQ(result->mapping.pad_y, item["mapping"]["pad_y"].get<float>());
       }
   }
   
   TEST(FramePreprocessing, LetterboxesRgbaAndReturnsTheActualPixelMapping)
   {
       gstreamer::BufferInfo frame;
       frame.metadata.width = 2;
       frame.metadata.height = 1;
       frame.metadata.format = "RGBA";
       const std::vector<unsigned char> bytes{ 255, 0, 0, 0, 0, 255, 0, 17 };
       for (const auto value : bytes) { frame.data.push_back(static_cast<std::byte>(value)); }
       frame.size = frame.data.size();
       const auto result = detection::preprocess_frame(frame, 4, 4, detection::FramePreprocessMode::Letterbox);
       ASSERT_TRUE(result.has_value());
       EXPECT_FLOAT_EQ(result->mapping.scale_x, 2.0F);
       EXPECT_FLOAT_EQ(result->mapping.scale_y, 2.0F);
       EXPECT_FLOAT_EQ(result->mapping.pad_y, 1.0F);
       EXPECT_FLOAT_EQ(result->data[0], 114.0F / 255.0F);
       EXPECT_FLOAT_EQ(result->data[4], 1.0F);
       EXPECT_FLOAT_EQ(result->data[5], 1.0F);
       EXPECT_FLOAT_EQ(result->data[6], 0.0F);
       EXPECT_FLOAT_EQ(result->data[16 + 6], 1.0F);
       EXPECT_FLOAT_EQ(result->data[32 + 4], 0.0F);
       frame.metadata.format = "GRAY8";
       EXPECT_EQ(
         kataglyphis::test::error_of(detection::preprocess_frame(frame, 4, 4)), detection::OnnxError::InvalidInputType);
   }
   
   TEST(VideoDetectorPipeline, AMovedPipelineStillRunsTheDetector)
   {
       KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("videotestsrc", "capsfilter", "appsink");
       const TempFile model("kataglyphis-yolo", ".onnx", kataglyphis::test::yolo_reshape_model(kTwoClasses));
       detection::VideoDetectionConfig config;
       config.yolo_config = two_class_config(model);
       config.gstreamer_config.pipeline_description =
         "videotestsrc num-buffers=2 pattern=black ! video/x-raw,format=RGBA,width=6,height=7,framerate=30/1 "
         "! appsink name=sink sync=false";
   
       // The factories hand their pipeline out by move, so the frame path must not hold on to the original object.
       detection::VideoDetectorPipeline original;
       ASSERT_TRUE(original.initialize(config).has_value());
       detection::VideoDetectorPipeline moved(std::move(original));
   
       kataglyphis::test::EventCounter detections;
       moved.set_detection_callback(
         [&](const detection::DetectionResult &, const gstreamer::BufferInfo &) { detections.add(); });
       ASSERT_TRUE(moved.start().has_value());
       EXPECT_TRUE(detections.wait_for(2, kataglyphis::test::kStreamTimeout));
       EXPECT_TRUE(moved.stop().has_value());
   }
   
   TEST(VideoDetectorPipeline, FactoriesFailOnAMissingModel)
   {
       ASSERT_TRUE(gstreamer::GStreamerPipeline::initialize_gstreamer().has_value());
       const auto missing = kataglyphis::test::missing_path("yolo.onnx");
   
       const auto video = detection::create_video_detection_pipeline("clip.mp4", missing, 64, 48);
       ASSERT_FALSE(video.has_value());
       EXPECT_EQ(video.error(), detection::OnnxError::ModelLoadFailed);
   
       const auto camera = detection::create_camera_detection_pipeline("/dev/video0", missing, 64, 48, 30);
       ASSERT_FALSE(camera.has_value());
       EXPECT_EQ(camera.error(), detection::OnnxError::ModelLoadFailed);
   }
