OpenCV 5 C++ Object Detection with YOLO26

In the first part of this series we ran a YOLO26 detector on World Cup footage using nothing but the OpenCV 5 DNN module and a CPU, all in Python. This second part keeps the same engine but changes two things at once. First, it moves the whole pipeline into C++, built against OpenCV 5 from source. Second, it goes well beyond drawing boxes.

Specifically, we use the segmentation and pose variants of the model to turn the same NMS-free pipeline into an instance segmenter that cuts each player out of the pitch and a pose estimator that draws a full skeleton on every player. Everything still runs on the CPU, and every model is a plain ONNX file.

Better still, every annotated image and video in this post is reproducible from the source media with a single script. The takeaway is simple. Once a model is exported to ONNX, OpenCV 5 in C++ becomes a complete, self contained runtime for detection, pose and segmentation, with no PyTorch and no Python anywhere in the deployed program.

Every player in a team lineup detected by YOLO26 through the OpenCV 5 C++ DNN module, each box labelled "player"
Every player in a team lineup detected by YOLO26 through the OpenCV 5 C++ DNN module, each box labelled “player”

Table of Contents

1. What Carries Over From Part 1

Notably, the core idea from Part 1 is what makes all of this short. YOLO26 is an end to end, NMS-free detector. Exported to ONNX with a plain head, its detection output is a single fixed tensor of shape [1, 300, 6], where each of the 300 rows is one finalized detection in the form [x1, y1, x2, y2, score, class_id].

Because the model already deduplicates its own predictions, the whole classic post-processing stack collapses into a score threshold and a coordinate rescale. Gone are the transpose from [1, 84, 8400], the objectness handling and the per-class Non-Maximum Suppression. That property is exactly why the C++ code stays small, and it carries straight over to the pose and segmentation variants, whose outputs are the same six values plus a little extra per detection.

In particular, we use the nano weights throughout, because the CPU is our target and nano is the fastest variant. Three models are exported to ONNX with a static input shape and opset 12, which is what the OpenCV 5 DNN importer expects:

  • yolo26n.pt for detection, exported at two sizes, yolo26n_640.onnx and yolo26n_1280.onnx
  • yolo26n-seg.pt for instance segmentation, exported as yolo26n_seg_640.onnx
  • yolo26n-pose.pt for pose estimation, exported as yolo26n_pose_640.onnx

In practice, nothing about the export is language specific, so a model exported once from Python loads identically from C++.

2. Building OpenCV 5 for C++

In fact, there is one real difference between the Python and the C++ path, and it shows up before we write a line of code. On the Python side, a plain pip install gives you a prebuilt wheel. For C++, however, there is no equivalent shortcut for OpenCV 5 at the time of writing. As a result, the development files come from building the library yourself from the 5.0.0 source branch with a C++17 compiler. Here, we build on Windows with the MSYS2 MinGW-w64 toolchain, driven by CMake and Ninja. Nothing in the CMake configuration is Windows-specific, so the same flags should carry over to Linux and macOS with their native compilers.

git clone --depth 1 --branch 5.0.0  opencv5_src

cmake -G Ninja -B opencv5_build \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX=opencv5_install \
  -DBUILD_LIST=core,imgproc,imgcodecs,videoio,highgui,dnn,video \
  -DWITH_CUDA=OFF -DWITH_FFMPEG=ON \
  opencv5_src

cmake --build opencv5_build
cmake --install opencv5_build

Two details matter here in particular. First, WITH_FFMPEG=ON is what lets VideoCapture and VideoWriter handle MP4, and on a MinGW build you need the FFmpeg development libraries installed in the toolchain first. Therefore, it is worth confirming the configuration summary reads FFMPEG: YES before the long compile starts. Second, WITH_CUDA=OFF is intentional, since our whole premise is CPU inference and a CUDA target forces the classic engine anyway. Finally, the full build script is in the companion repository.

3. The Shared Detector in C++

In practice, everything the demos share lives in a single translation unit, yolo26_dnn.cpp. The engine selection and network load mirror the Python binding exactly, because in OpenCV 5 readNetFromONNX takes the engine as a second argument.

cv::dnn::Net build_net(const std::string& onnx_path, const std::string& engine) {
    int eng = cv::dnn::ENGINE_AUTO;
    if      (engine == "new")     eng = cv::dnn::ENGINE_NEW;
    else if (engine == "classic") eng = cv::dnn::ENGINE_CLASSIC;
    return cv::dnn::readNetFromONNX(onnx_path, eng);
}

Next, the detection function runs the three OpenCV calls and walks the finished rows. Notably, there is no transpose and no NMSBoxes, only a score threshold and the inverse of the letterbox.

std::vector detect(cv::dnn::Net& net, const cv::Mat& img,
                              int size, float conf_thres) {
    float r; int dw, dh;
    cv::Mat padded = letterbox(img, size, r, dw, dh);
    cv::Mat blob = cv::dnn::blobFromImage(padded, 1.0/255.0, {size, size},
                                          cv::Scalar(), true, false);
    net.setInput(blob);
    cv::Mat out = net.forward();          // [1, 300, 6]

    const int rows = out.size[out.dims - 2], cols = out.size[out.dims - 1];
    const float* p = reinterpret_cast(out.data);
    std::vector dets;
    for (int i = 0; i < rows; ++i) {
        const float* row = p + (size_t)i * cols;
        if (row[4] < conf_thres) continue;
        Detection d;
        d.box   = cv::Rect2f((row[0]-dw)/r, (row[1]-dh)/r,
                             (row[2]-row[0])/r, (row[3]-row[1])/r);
        d.score = row[4];
        d.cls   = (int)row[5];
        dets.push_back(d);
    }
    return dets;
}

Finally, one small football-specific touch runs through all the demos. Although COCO calls the class “person” and the ball “sports ball”, on match footage “player” and “ball” read better. Therefore, a one-line display-name mapping relabels just the captions. The underlying COCO classes, of course, stay untouched.

4. Object Detection

Detection on an Image

With the helper in place, single-image detection is a load, a warm-up pass, a timed pass and a draw. As a result, there is very little glue code to write. On a team lineup, the 1280-pixel input model boxes every player cleanly, each labelled “player”, even against a busy crowd.

Meanwhile, the same code handles a crowded broadcast frame, where it holds boxes on the outfield players, the goalkeeper and the substitutes in the background at once.

A crowded broadcast frame with the outfield players goalkeeper and background substitutes all detected at once

Detection on Video

Similarly, video is the same call in a loop, reading frames, detecting, drawing and writing an annotated MP4 with a live frame-rate read-out. Pointed at a broadcast clip, the detector tracks the players through the run of play on the CPU alone.

Where a Generic Detector Slips

Of course, it is worth being honest about the failure cases too. In a tight duel, for example, the model catches both players and the real ball at high confidence, yet it also fires a second, low-confidence ball on the crest printed on a shirt. That kind of false positive is normal for any detector on busy textures. Fortunately, it is exactly what a confidence threshold and, if you need it, a class filter are for.

5. Detection Under Blur

However, real footage is not always sharp, so it is fair to ask how the pipeline behaves when the picture is soft. Pointed at a heavily out-of-focus clip of a stadium, the same OpenCV 5 DNN pass on the CPU still finds the players whose shapes survive the blur. The goalkeeper in the bright kit comes through most confidently, while the faint, low-contrast figures in the stands drop below threshold.

Players detected in a heavily blurred stadium clip, with lower confidences
Players detected in a heavily blurred stadium clip, with lower confidences

Of course, the confidences are lower than on a sharp frame. That is the honest and expected result. After all, blur removes exactly the high-frequency detail a detector leans on, so recall falls off gracefully rather than collapsing.

6. Pose Estimation With YOLO26-pose

Likewise, swapping the detection model for yolo26n-pose gives us a skeleton on every player with almost no new code. The pose ONNX output is [1, 300, 57], which is the same six detection values followed by 17 COCO keypoints, each with an x, a y and a confidence. We parse it exactly like the detector, undo the letterbox on the keypoint coordinates too, and draw the 17-point skeleton with its limb connections.

For example, on an aerial header, the skeletons capture both jumping players and the way their bodies twist for the ball.

Two players contesting an aerial header, each drawn with a 17-point skeleton
Two players contesting an aerial header, each drawn with a 17-point skeleton

Because it is the same per-frame call, pose runs on video just as easily. On the halftime performance, the estimator tracks every dancer’s pose through the choreography, which is a good stress test of keeping several skeletons apart in a tight group.

Several dancers, each tracked with a distinct coloured skeleton
Several dancers, each tracked with a distinct coloured skeleton

For a fun finale to the pose section, here is the same estimator running on Shakira and her dancers during a World Cup halftime performance. There is no sound on this clip, but watch how the OpenCV 5 pipeline keeps a clean skeleton on every performer as they spin and overlap.

7. Instance Segmentation With YOLO26-seg

From Coefficients to Masks

Ultimately, segmentation is where the NMS-free format pays off most. For the model side in more depth, our YOLO26 instance segmentation guide covers how the masks are learned and predicted. Here, by contrast, we focus on running them in C++. The yolo26n-seg model has two outputs: output0 of shape [1, 300, 38], which is the six detection values plus 32 mask coefficients per object, and output1 of shape [1, 32, 160, 160], a stack of 32 mask prototypes.

From there, each object’s mask is the sigmoid of its coefficients multiplied by the prototypes, a single small matrix multiply. We then rescale it from the 160 by 160 prototype space back through the letterbox to the full frame and threshold. All of that is a dozen lines of OpenCV.

cv::Mat m = coeff * protoMat;          // (1x32) * (32 x 160*160)
m = m.reshape(1, mh);                   // 160 x 160 logits
cv::exp(-m, prob); prob = 1.0/(1.0+prob);   // sigmoid
// upsample -> remove letterbox pad -> resize to frame -> threshold

Masks on Images and Video

In practice, the masks are tight. On a close duel, for instance, it cuts each player out of the pitch and still picks up the ball as its own instance.

Two players and the ball, each with a tight coloured segmentation mask
Two players and the ball, each with a tight coloured segmentation mask

Likewise, on the celebration, five players each get their own coloured silhouette.

A group of players after a goal, each with a per-player segmentation mask
A group of players after a goal, each with a per-player segmentation mask

As before, segmentation runs on video the same way, one mask pass per frame with no extra machinery.

Dancers with per-person segmentation masks tracing their outlines
Dancers with per-person segmentation masks tracing their outlines

We already ran pose estimation on this same halftime dance clip. Now the segmentation model runs on the very same video, swapping the skeletons for full silhouettes. Even as the dancers spin and cross in front of one another, each one keeps its own coloured mask, so the outlines never merge.

Meanwhile, the foot-and-ball close-up is a tighter test at short range. Even here the mask peels the player’s boot away from the ball as separate instances, holding a clean edge between the two as the camera moves in close around them.

8. Choosing the Engine, and a Benchmark

Because OpenCV 5 exposes an engine selector, our helper makes it a first-class option, and a small benchmark times the same model and image through each engine after a warm-up. On our CPU, at the 640 input size, the new engine is clearly ahead of the classic one.

Engine Mean (ms) FPS
ENGINE_NEW 91.9 10.9
ENGINE_CLASSIC 141.9 7.0
ENGINE_AUTO 102.8 9.7

Overall, the new graph engine is meaningfully quicker here, roughly a third less time per frame than the classic one, which works out to about fifty percent more frames per second, through its shape inference, constant folding and operator fusion. The practical advice is simple: for CPU inference use auto or new, and reach for classic only when you need a backend the new engine does not support yet, most notably CUDA.

9. Reproducing Everything

In fact, every annotated image and video above is regenerated from the source media by one script. Specifically, the repository ships an assets/ folder with the exact input images and clips, the model export scripts, and a reproduce.sh that runs each demo and writes the results into outputs/.

./build_opencv5.sh                                   # build OpenCV 5 from source
python scripts/export_yolo26_onnx.py --imgsz 640     # detection model
python scripts/export_yolo26_onnx.py --imgsz 1280
python scripts/export_variants.py                    # seg + pose models
cmake -G Ninja -B build -DOpenCV_DIR=.../opencv5_install/lib/cmake/opencv5
cmake --build build
./reproduce.sh                                       # regenerate every output

As a result, the outputs you get should match the ones in this post, because the inputs and the models are fixed.

10. Migration Notes and Gotchas

  • The C++ side is a from-source build. There is no pip-style shortcut for the OpenCV 5 C++ files yet, so plan for a CMake build against the 5.0.0 branch with a C++17 compiler.
  • Confirm FFmpeg before you build. If the CMake summary says FFMPEG: NO, video read and write will not work. On MinGW you need the FFmpeg development libraries in the toolchain first.
  • Read every output tensor by its shape. Detection is [1,300,6], pose is [1,300,57], segmentation is [1,300,38] plus a [1,32,160,160] prototype tensor. Reading the row and column counts from the tensor keeps the parser robust across models.
  • A cv::dnn::Net is not thread-safe. If you run inference on several threads, give each thread its own network.
  • On MinGW the vendored MLAS kernels do not build. They assume the System V ABI, so OpenCV falls back to its built-in SGEMM. Detection is still correct, just slightly slower than an MSVC build.

11. Conclusion

In conclusion, Part 1 showed that OpenCV 5 plus YOLO26 turns OpenCV into a serious, self contained inference runtime. This part, in turn, cashed that out in C++ and pushed it past detection.

With three ONNX files and one small helper, the same CPU pipeline did a great deal. First, it detected the players. Then it drew a skeleton on each one, and finally it cut each player out of the pitch with an instance mask. Moreover, it did all of that on real football and World Cup footage, and it still held up honestly on a heavily blurred clip.

Crucially, none of it needed PyTorch or Python in the deployed program. Better still, all of it is reproducible from the assets in the repository. That combination, a small dependency footprint, one code path from prototype to production, and detection, pose and segmentation from the same runtime, is exactly what makes OpenCV 5 worth building for C++.

From idea to working model to real-time deployment

Big Vision takes computer vision projects through the full journey, not just the easy parts.

12. References

  1. OpenCV 5 overview
  2. OpenCV 5.0.0 release
  3. OpenCV 5.0.0 source tree
  4. OpenCV 4 to 5 migration guide
  5. OpenCV 5 announcement, OpenCV.org
  6. OpenCV 5 configuration options reference
  7. YOLO26: An Analysis of NMS-Free End to End Framework for Real-Time Object Detection (arXiv)
  8. Ultralytics YOLO documentation
  9. OpenCV 5.0.0 root CMake configuration
  10. OpenCV 5 DNN module build options
  11. OpenCV DNN module reference

Post Views: 618

Similar Posts

Leave a Reply