This demo runs one image through a YOLOv8 nano model and prints the detected
bounding boxes. ONNX Runtime executes the neural network. The C++ code performs
the image preprocessing and YOLO postprocessing itself.
1. Install the Dependencies
From the repository root, install the compiler, CMake, and the OpenCV parts
used to load and resize images:
The coordinates use the original image's pixels. Classes are numeric COCO
class IDs because this minimal demo does not load label names.
3. Basic Idea
YOLO cannot consume the bytes returned directly by cv::imread(). The image
must first be resized, padded, reordered, and normalized. The model produces
candidate predictions rather than final boxes, so its output must also be
decoded and filtered.
flowchart TD
A[Image file] --> B[OpenCV BGR image]
B --> C[Letterbox resize and padding]
C --> D[RGB FP32 NCHW tensor]
D --> E[ONNX Runtime session.Run]
E --> F[Raw YOLOv8 candidates]
F --> G[Confidence filter]
G --> H[Restore original coordinates]
H --> I[Class-aware NMS]
I --> J[Final bounding boxes]
image variable height x width x 3, BGR uint8
model input [1, 3, 640, 640], RGB float32
model output [1, 84, 8400], float32
final detection class + confidence + x + y + width + height
For COCO YOLOv8, 84 output channels means four box values plus 80 class
scores. 8400 is the number of candidate predictions.
4. Source Code Map
The implementation is intentionally kept in one file:
ort.cpp
├── Detection final class, score, and source-image box
├── Letterbox input tensor plus resize/padding information
├── preprocess() image to model tensor
├── intersection_over_union() overlap measurement used by NMS
├── to_source_box() model coordinates to original coordinates
├── postprocess() decode, filter, and suppress candidates
└── main() load, validate, infer, and print
run_options options for this single call
input_names model node receiving the input
&input tensor passed to that node
input_count number of input names and tensors
output_names model nodes whose values are requested
output_count number of requested outputs
Run() blocks until CPU inference finishes. The returned vector owns the
output tensors allocated by ONNX Runtime.
Steps 10-12: Validate, Postprocess, and Print
The program verifies that the output is three-dimensional, then passes its
float memory to postprocess():
The final loop prints each retained detection in source-image coordinates.
6. Preprocessing Drilldown
Letterbox Resize
Stretching a rectangular image directly to a square changes object shapes.
Letterboxing instead uses one scale for both axes and fills the unused area.
For each candidate, postprocess() finds the largest class score. Candidates
below 0.25 are discarded. Raw YOLOv8 detection output has no separate
objectness row, so the selected class score is used as confidence.
Restore Original Coordinates
YOLO emits a center-based box in letterboxed model coordinates:
The result is clipped to the source image. Empty boxes are discarded.
Class-Aware Non-Maximum Suppression
Many candidates describe the same object. NMS keeps the strongest candidate
and rejects weaker boxes of the same class when their intersection-over-union
is above 0.45.
flowchart TD
A[Sort candidates by confidence] --> B[Take next candidate]
B --> C{Overlaps a kept box<br/>of the same class?}
C -- No --> D[Keep it]
C -- Yes, IoU > 0.45 --> E[Discard it]
D --> F{More candidates?}
E --> F
F -- Yes --> B
F -- No --> G[Final detections]
IoU measures overlap relative to the combined area:
flowchart LR
A[Root CMakeLists.txt] -->|add_subdirectory| B[demos/CMakeLists.txt]
B -->|add_subdirectory| C[ort_cpu_demo/CMakeLists.txt]
C --> D[ort executable]
The demo CMake file finds OpenCV, locates the bundled ONNX Runtime header and
library, and defines the executable:
/* * YOLOv8 + ONNX Runtime pipeline * ================================= * * image file * BGR, original size * | * v * +------------------------+ * | Letterbox preprocessing| * | - keep aspect ratio | * | - resize | * | - pad with value 114 | * +------------------------+ * | * v * +------------------------+ * | Tensor conversion | * | BGR -> RGB | * | uint8 -> float [0, 1] | * | HWC -> NCHW | * +------------------------+ * | * v * input tensor [1, 3, H, W] * | * v * +------------------------+ * | ONNX Runtime | * | Ort::Session::Run() | * +------------------------+ * | * v * output tensor [1, 4 + classes, candidates] * | * v * +------------------------+ * | YOLOv8 postprocessing | * | - decode cx, cy, w, h | * | - choose best class | * | - confidence filter | * | - undo padding/resize | * | - clip to image | * | - class-aware NMS | * +------------------------+ * | * v * class, confidence, x, y, width, height * * Preprocessing records scale and padding so boxes predicted in the model's * letterboxed coordinate system can be mapped back to the original image. * * A raw YOLOv8 detection export (nms=False) stores one candidate per output * column. Its first four rows are center_x, center_y, width, and height. The * remaining rows are class scores. The highest class score is the candidate's * confidence; YOLOv8 has no separate objectness row in this output format. * * NMS (non-maximum suppression) keeps the strongest box and removes weaker, * highly overlapping boxes of the same class. ONNX Runtime only executes the * model; this file performs preprocessing and postprocessing explicitly. */#include<onnxruntime_cxx_api.h>#include<opencv2/imgcodecs.hpp>#include<opencv2/imgproc.hpp>#include<algorithm>#include<cmath>#include<iostream>#include<stdexcept>#include<string>#include<vector>structDetection{intclass_id;floatconfidence;cv::Rectbox;};structLetterbox{std::vector<float>input;floatscale;intpad_x;intpad_y;};staticLetterboxpreprocess(constcv::Mat&image,intinput_width,intinput_height){constfloatscale=std::min(static_cast<float>(input_width)/image.cols,static_cast<float>(input_height)/image.rows);constintresized_width=std::lround(image.cols*scale);constintresized_height=std::lround(image.rows*scale);constintpad_x=(input_width-resized_width)/2;constintpad_y=(input_height-resized_height)/2;cv::Matresized;cv::resize(image,resized,cv::Size(resized_width,resized_height));cv::Matpadded(input_height,input_width,CV_8UC3,cv::Scalar(114,114,114));resized.copyTo(padded(cv::Rect(pad_x,pad_y,resized_width,resized_height)));constsize_tplane_size=static_cast<size_t>(input_width)*input_height;std::vector<float>input(3*plane_size);for(inty=0;y<input_height;++y){for(intx=0;x<input_width;++x){constcv::Vec3bbgr=padded.at<cv::Vec3b>(y,x);constsize_tindex=static_cast<size_t>(y)*input_width+x;input[index]=bgr[2]/255.0F;input[plane_size+index]=bgr[1]/255.0F;input[2*plane_size+index]=bgr[0]/255.0F;}}return{std::move(input),scale,pad_x,pad_y};}staticfloatintersection_over_union(constcv::Rect&left,constcv::Rect&right){constintintersection=(left&right).area();returnstatic_cast<float>(intersection)/static_cast<float>(left.area()+right.area()-intersection);}staticcv::Rectto_source_box(floatcenter_x,floatcenter_y,floatwidth,floatheight,constLetterbox&letterbox,constcv::Size&source_size){floatleft=(center_x-width/2.0F-letterbox.pad_x)/letterbox.scale;floattop=(center_y-height/2.0F-letterbox.pad_y)/letterbox.scale;floatright=(center_x+width/2.0F-letterbox.pad_x)/letterbox.scale;floatbottom=(center_y+height/2.0F-letterbox.pad_y)/letterbox.scale;left=std::clamp(left,0.0F,static_cast<float>(source_size.width));top=std::clamp(top,0.0F,static_cast<float>(source_size.height));right=std::clamp(right,0.0F,static_cast<float>(source_size.width));bottom=std::clamp(bottom,0.0F,static_cast<float>(source_size.height));constintx=static_cast<int>(std::floor(left));constinty=static_cast<int>(std::floor(top));constintbox_width=static_cast<int>(std::ceil(right))-x;constintbox_height=static_cast<int>(std::ceil(bottom))-y;return{x,y,box_width,box_height};}staticstd::vector<Detection>postprocess(constfloat*output,int64_tchannels,int64_tcandidate_count,constLetterbox&letterbox,constcv::Size&source_size,floatconfidence_threshold,floatiou_threshold){constintclass_count=static_cast<int>(channels-4);std::vector<Detection>candidates;for(int64_tcandidate=0;candidate<candidate_count;++candidate){intbest_class=0;floatbest_score=output[4*candidate_count+candidate];for(intclass_id=1;class_id<class_count;++class_id){constfloatscore=output[(4+class_id)*candidate_count+candidate];if(score>best_score){best_score=score;best_class=class_id;}}if(best_score<confidence_threshold){continue;}constcv::Rectbox=to_source_box(output[candidate],output[candidate_count+candidate],output[2*candidate_count+candidate],output[3*candidate_count+candidate],letterbox,source_size);if(box.area()>0){candidates.push_back({best_class,best_score,box});}}std::sort(candidates.begin(),candidates.end(),[](constDetection&left,constDetection&right){returnleft.confidence>right.confidence;});// ponytail: O(n^2) NMS is enough for YOLOv8n; use an optimized NMS only if profiling requires it.std::vector<Detection>detections;for(constDetection&candidate:candidates){constbooloverlaps=std::any_of(detections.begin(),detections.end(),[&](constDetection&kept){returncandidate.class_id==kept.class_id&&intersection_over_union(candidate.box,kept.box)>iou_threshold;});if(!overlaps){detections.push_back(candidate);}}returndetections;}intmain(intargc,char**argv)try{// 1. Read the model and image paths from the command line.conststd::stringprogram_name=argv[0];if(argc!=3){std::cerr<<"usage: "<<program_name<<" <yolov8.onnx> <image>\n";return2;}conststd::stringmodel_path=argv[1];conststd::stringimage_path=argv[2];// 2. Load the source image. OpenCV stores color images in BGR order.constcv::Matimage=cv::imread(image_path);if(image.empty()){throwstd::runtime_error("cannot read image: "+image_path);}// 3. Create a CPU ONNX Runtime session and enable graph optimizations.Ort::Envenvironment(ORT_LOGGING_LEVEL_WARNING,"yolov8-demo");Ort::SessionOptionsoptions;options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);Ort::Sessionsession(environment,model_path.c_str(),options);// 4. This minimal demo supports models with one image input and one output.if(session.GetInputCount()!=1||session.GetOutputCount()!=1){throwstd::runtime_error("expected exactly one model input and one output");}// 5. Inspect the model instead of hard-coding its input height and width.conststd::vector<int64_t>input_shape=session.GetInputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape();constexprsize_tbatch_axis=0;constexprsize_tchannel_axis=1;constexprsize_theight_axis=2;constexprsize_twidth_axis=3;if(input_shape.size()!=4||input_shape[batch_axis]!=1||input_shape[channel_axis]!=3||input_shape[height_axis]<=0||input_shape[width_axis]<=0){throwstd::runtime_error("expected a static float input shaped [1, 3, height, width]");}// 6. Letterbox the image and build the normalized RGB NCHW float buffer.constintinput_height=static_cast<int>(input_shape[height_axis]);constintinput_width=static_cast<int>(input_shape[width_axis]);Letterboxletterbox=preprocess(image,input_width,input_height);// 7. Wrap our float buffer as an ONNX Runtime tensor without copying it.Ort::MemoryInfomemory=Ort::MemoryInfo::CreateCpu(OrtArenaAllocator,OrtMemTypeDefault);Ort::Valueinput=Ort::Value::CreateTensor<float>(memory,letterbox.input.data(),letterbox.input.size(),input_shape.data(),input_shape.size());// 8. Ask the model for its actual input/output node names.Ort::AllocatorWithDefaultOptionsallocator;autoinput_name=session.GetInputNameAllocated(0,allocator);autooutput_name=session.GetOutputNameAllocated(0,allocator);constchar*input_names[]={input_name.get()};constchar*output_names[]={output_name.get()};constOrt::RunOptionsrun_options{nullptr};constexprsize_tinput_count=1;constexprsize_toutput_count=1;// 9. Run synchronous CPU inference. This call blocks until output is ready.// Arguments, in order:// - run_options: use default options for this inference call.// - input_names: names of the model nodes receiving input tensors.// - &input: address of the first input tensor.// - input_count: number of input names and tensors.// - output_names: names of the model output nodes to request.// - output_count: number of requested outputs.// The returned vector owns the output tensors allocated by ONNX Runtime.std::vector<Ort::Value>outputs=session.Run(run_options,input_names,&input,input_count,output_names,output_count);// 10. Validate the raw YOLOv8 output layout before reading its memory.conststd::vector<int64_t>output_shape=outputs[0].GetTensorTypeAndShapeInfo().GetShape();if(output_shape.size()!=3||output_shape[0]!=1||output_shape[1]<5||output_shape[2]<=0){throwstd::runtime_error("expected raw YOLOv8 output [1, 4 + classes, candidates]; export with nms=False");}// 11. Decode candidates, restore source coordinates, and apply NMS.constexprfloatconfidence_threshold=0.25F;constexprfloatiou_threshold=0.45F;conststd::vector<Detection>detections=postprocess(outputs[0].GetTensorData<float>(),output_shape[1],output_shape[2],letterbox,image.size(),confidence_threshold,iou_threshold);// 12. Print the final detections in original-image pixel coordinates.std::cout<<"detections: "<<detections.size()<<'\n';for(constDetection&detection:detections){std::cout<<"class="<<detection.class_id<<" confidence="<<detection.confidence<<" x="<<detection.box.x<<" y="<<detection.box.y<<" width="<<detection.box.width<<" height="<<detection.box.height<<'\n';}return0;}catch(constOrt::Exception&error){std::cerr<<"ONNX Runtime error: "<<error.what()<<'\n';return1;}catch(conststd::exception&error){std::cerr<<"error: "<<error.what()<<'\n';return1;}