Skip to content

YOLOs-CPP v1.1.0

Latest

Choose a tag to compare

@Geekgineer Geekgineer released this 02 Aug 11:15
2b3b2f6

Two new tasks, batch inference, in-memory model loading, and two parity fixes that
move output values. Everything in the high-level API is additive — v1.0.0 code
compiles unchanged.

Features

Monocular metric depth estimation (YOLO26)

A new task returning a dense per-pixel depth map in meters, not a normalized
disparity. Unlike the other tasks it takes no labels file and no confidence/IoU
thresholds, because the model outputs one dense map rather than instances to filter.

yolos::depth::YOLODepthEstimator estimator("yolo26n-depth.onnx", /*gpu=*/true);
cv::Mat depth = estimator.estimate(frame);   // CV_32FC1, meters
estimator.drawDepth(frame, depth);
./build/image_depth_inference models/yolo26n-depth.onnx data/dog.jpg

Available for yolo26{n,s,m,l,x}-depth. Parity against Ultralytics on the real
yolo26n-depth export measures mean AbsRel ~1.8e-06, max relative error ~5.2e-06,
with δ1 saturated at 1.0. Image, video and camera examples ship alongside. See
Depth Estimation.

Batch inference

batchDetect / batchSegment / batchClassify push many images through a single
ONNX Runtime call — the throughput win on GPU.

std::vector<cv::Mat> images = {a, b, c};
auto results = detector.batchDetect(images, /*conf=*/0.25f, /*iou=*/0.45f);
// one result vector per input image, in input order

Fixed-batch exports fall back to a per-image loop automatically, so the call works
against any model. The fallback also catches Ort::Exception at run time, because
real Ultralytics static exports bake batch=1 into their DFL Reshape nodes and so
advertise a dynamic batch they cannot actually execute.

In-memory model loading

Every task class gained a (const void* data, size_t size, ...) constructor, for
encrypted stores, network streams and resources embedded in the binary. Class names
are passed as a vector, so no labels file is needed either.

std::vector<uint8_t> bytes = yolos::utils::readFileBytes("yolo11n.onnx");
yolos::det::YOLODetector detector(bytes.data(), bytes.size(), {"person", "car"});
// ONNX Runtime copies the buffer during construction — safe to wipe it now

Grayscale input support

Single-channel models are now detected from the input tensor's channel dimension and
preprocessed accordingly, instead of assuming three channels. Thanks @choyy (#139).

Build as a subdirectory

add_subdirectory(YOLOs-CPP) now propagates the project's own include directories, so
consuming projects build without manual include paths. Thanks @Ferdi0412 (#142).

Documentation site

The docs moved into docs/ and now build with MkDocs and deploy to GitHub Pages, with
the API reference rewritten around worked examples. Thanks @imessam (#133).

YOLOE open-vocabulary detection and segmentation

Landed after v1.0.0 and shipping in a tagged release for the first time. Runs ONNX
exported after set_classes() or from prompt-free *-pf checkpoints, via
scripts/export_yoloe_onnx.py and the image_yoloe_seg / video_yoloe_seg demos.

Behavior changes

These move output values without breaking compilation — if you have thresholds or
golden files tuned against v1.0.0, recheck them.

Classification preprocessing now matches Ultralytics (#145, fixes #137).
Ultralytics resizes classification inputs through PIL, whose bilinear filter is
antialiased; cv::resize(INTER_LINEAR) is not. On a 768×576 image that shifted the
input tensor by up to 76/255 and moved confidences by 0.12–0.22 — enough to flip
yolo26n-cls from husky to Alaskan Malamute. A second, smaller mismatch:
torchvision's CenterCrop rounds the crop offset where this code floored it.

YOLOs-CPP now ports Pillow's resampling (preprocessing::resizeAntialiasBilinear) and
crops with nearbyint. Tensors agree with Pillow to within 1/255, max confidence delta
0.0034 across 11 image shapes × 3 models, zero class flips. Expect your classification
confidences to drop slightly
— the old values were inflated by aliasing.

Detection, segmentation, pose and OBB are deliberately untouched: Ultralytics' LetterBox
really does use cv2.INTER_LINEAR, so those paths already matched.

Letterbox descaling matches the padding actually applied (#147). getScalePad()
returned the unrounded half-difference while letterBoxToBlob() pads by the integer
nearbyint(dw - 0.1). Whenever (target - resized) is odd the two disagree by half a
letterbox pixel, and descaling by the gain magnifies it — up to 3.3 original pixels on
a 1600×2128 image at 320. Tie-rounding now also follows Python's round-half-to-even.
Measured against Ultralytics with YOLOv8n on that shape, x-edge error drops from
3.589 / 3.726 px to 0.589 / 0.726 px; across the detection parity set the worst
coordinate goes from 5 px to exactly 0, over 35 boxes and 7 models.

This shifts box, mask, keypoint and OBB coordinates by up to a few pixels for
detection, segmentation, pose and OBB — toward Ultralytics.

Breaking changes

One low-level signature changed. The task classes (YOLODetector, YOLOSegDetector,
YOLOPoseDetector, YOLOOBBDetector, YOLOClassifier) are unaffected.

// before
preprocessing::letterBoxToBlob(image, blob, targetSize, actualSize, padColor);
// after — targetChannels is required, and comes from the model's input tensor
preprocessing::letterBoxToBlob(image, blob, targetChannels, targetSize, actualSize, padColor);

Both the std::vector<float>& and InferenceBuffer& overloads take the new
parameter. It arrived with grayscale support (#139).

Testing

107 tests across 8 suites — 50 Ultralytics-parity and 57 self-contained. The
self-contained tests run against synthetic ONNX models or fixed reference values: no
downloaded weights and no Ultralytics reference run. 27 of them need nothing but the
compiler; the other 30 use Python only to generate a synthetic model.

Task Parity Self-contained Total
Detection 7 3 10
Segmentation 8 8
Pose 7 7
OBB 7 7
Classification 6 7 13
Depth 7 25 32
YOLOE 8 8
API (batch + in-memory) 22 22
Total 50 57 107

Each suite runs as its own CI job. The classification parity harness previously
re-implemented the C++ preprocessing in Python — comparing YOLOs-CPP against a
transcription of itself, which is why #137 went undetected. It now calls Ultralytics'
classify_transforms() directly, and the tolerance tightened from 0.1 to 0.02. The
detection bbox margin tightened from 50 px to 3 px.

tests/*/results/ is no longer version-controlled. Every suite regenerates it from
scratch on each run, so the committed copy only ever went stale against whatever
Ultralytics version was installed.

Supported models

Version Detection Segmentation Pose OBB Classification Depth
YOLOv5
YOLOv6
YOLOv7
YOLOv8
YOLOv9
YOLOv10
YOLOv11
YOLOv12
YOLO26
YOLOE (open-vocab)

Requirements

Unchanged from v1.0.0 — CMake 3.16+, a C++17 compiler, OpenCV 4.5+, ONNX Runtime 1.16+.

Contributors

@Geekgineer, @imessam, @choyy (#139, first contribution), @Ferdi0412 (#142, first
contribution).

Full changelog: v1.0.0...v1.1.0