←back to Blog

OpenCV DNN Module: Deep Learning Inference in OpenCV 5

OpenCV’s DNN module lets you run a trained neural network inside the same application that reads images, processes video, and draws results. You can export a model to ONNX, load it with OpenCV, and use its predictions without loading the training framework in your application.

OpenCV 5 expands that workflow with a new inference engine built for a wider range of modern ONNX models. In this tutorial, we will use it for two tasks: object detection with YOLO26 and instance segmentation with RF-DETR. The first produces a box around each detected object. The second adds a mask showing which pixels belong to each instance.

Both examples run on the CPU and save their results as images. After exporting the models, their inference code needs only OpenCV, NumPy, and Python’s standard library.

Version: The examples were executed with OpenCV 5.0.0. The current 5.x development API was checked separately on September 20, 2026: it calls the built-in engine ENGINE_OPENCV, while 5.0.0 calls it ENGINE_NEW. The code below handles both names. For the previous tutorial, see OpenCV 4 DNN Module.

What does the DNN module do?

A typical deployment has four steps: prepare an image, run the network, interpret its outputs, and display or use the predictions. OpenCV provides the image processing functions and the inference API; your application supplies the preprocessing and output decoding that the model expects.

Training remains a separate step. In these examples, the original model packages download pretrained weights and export ONNX files. The application then loads the .onnx files with cv2.dnn.readNetFromONNX(). It does not load the original PyTorch checkpoint files.

That separation is useful when your application already uses OpenCV. The same frame can pass through resizing, inference, drawing, and video output without requiring the training framework at deployment time.

What changes in OpenCV 5?

The largest change is a rewritten DNN inference engine with improved support for dynamic shapes, subgraphs, and modern ONNX features. OpenCV 5 also adds optional ONNX Runtime integration, and its examples cover a broader range of networks, including vision-language models. See the OpenCV 5 release overview.

Engine selection happens when a model is loaded. The current 5.x development API defines these choices:

Engine Behavior
cv2.dnn.ENGINE_AUTO Currently resolves to ENGINE_OPENCV.
cv2.dnn.ENGINE_OPENCV Selects OpenCV’s built-in DNN engine, currently CPU-only.
cv2.dnn.ENGINE_ORT Selects ONNX Runtime integration, when OpenCV was built with it.

For a build exposing the current API, explicit selection looks like this:

net = cv2.dnn.readNetFromONNX("model.onnx", cv2.dnn.ENGINE_OPENCV)

The current API no longer exposes ENGINE_NEW or ENGINE_CLASSIC. DNN_BACKEND_OPENCV is a separate backend constant and is not the engine argument shown above. These definitions are verified in the current API source, pinned to the inspected commit.

The released 5.0.0 package used for the tests has the older selection API: ENGINE_NEW explicitly selects the rewritten engine, and ENGINE_AUTO tries it before falling back to the classic engine during loading. See the 5.0.0 API definition. The complete examples prefer ENGINE_OPENCV when available and otherwise use ENGINE_NEW.

Use the named constants: their numeric values changed between these APIs. The scripts require OPENCV_FORCE_DNN_ENGINE to be unset so an old numeric override cannot select a different engine. No classic fallback is assumed for the current 5.x API.

How fast is the new engine?

The OpenCV project publishes CPU inference measurements against ONNX Runtime 1.25.1. Here are the YOLO26 Nano results, retaining the hardware labels used in the source:

Hardware label OpenCV 5 DNN (ms) ONNX Runtime 1.25.1 (ms)
Intel i9 9.9 9.6
Intel i7 24.9 19.0
Apple M1 55.6 63.4
Apple M5 22.7 21.6
AMD 13.6 30.7

Lower is better. OpenCV is faster in the reported Apple M1 and AMD configurations; ONNX Runtime is faster in the other three. These are comparisons between runtimes on the listed systems, not measurements of the improvement from OpenCV 4 to OpenCV 5. Source: complete OpenCV 5 DNN benchmarks.

The published tables do not specify enough detail about model exports, input shapes, exact CPU models, threading, or timing procedure to reproduce every number. Use them to identify configurations worth testing, then measure your own model and application. The tutorial below uses independently exported models; its output images demonstrate inference, not a reproduction of those benchmarks.

Install OpenCV 5

Use Python 3.11 or newer; these examples were tested with Python 3.14.3. Start in a fresh virtual environment. The examples save files and do not open GUI windows, so the headless package is sufficient:

python -m venv .venv-dnn
# macOS / Linux:
source .venv-dnn/bin/activate
# Windows PowerShell: .venv-dnnScriptsActivate.ps1

python -m pip install opencv-python-headless==5.0.0.93 numpy==2.3.5
python -c "import cv2; print(cv2.__version__)"

The version check should print 5.0.0. These package pins were used for the example validation; use a Python version supported by both packages. Do not install multiple OpenCV wheel variants in this environment, because they share the cv2 namespace. If your application needs OpenCV GUI windows, choose opencv-python instead of the headless variant. See the OpenCV Python package instructions.

The export step has additional dependencies. Keep those in a separate environment so that the model packages cannot replace the OpenCV installation used for inference. Once exported, the ONNX files and their label files can be copied into the inference application.

Example 1: Object detection with YOLO26

We will use YOLO26 Nano, exported at a fixed input size of 640 × 640. This example uses its end-to-end detection output: each prediction already contains a bounding box, confidence score, and class ID. There is no separate non-maximum suppression (NMS) step in our inference code. See the Ultralytics YOLO26 documentation for the model family and available tasks.

Export the model once

Download the example code and open the yolo26 folder. The lightweight download includes scripts, sample images, labels, and validation records; run the exporters to obtain the models. Create a separate export environment and install the pinned versions:

python -m venv .venv-export-yolo
.venv-export-yolo/bin/python -m pip install -r requirements-export.txt
.venv-export-yolo/bin/python export_yolo26.py

On Windows, replace .venv-export-yolo/bin/python with .venv-export-yoloScriptspython.exe. Using that interpreter directly keeps the export installation separate from the active inference environment.

The exporter downloads the pretrained yolo26n.pt checkpoint and the sample bus.jpg, writes coco80.json from the model’s class names, and produces yolo26n.onnx. Its key settings are:

model.export(
    format="onnx", imgsz=640, batch=1, dynamic=False, half=False,
    opset=17, simplify=False, end2end=True, nms=False, device="cpu",
)

This recipe is pinned to Ultralytics 8.4.27. In that version, end2end=True selects the end-to-end head and nms=False avoids adding a separate NMS wrapper. Export options can change between package versions, so keep the supplied requirements and exporter together. The tested graph accepts [1, 3, 640, 640] and returns [1, 300, 6]; each row is [x1, y1, x2, y2, confidence, class_id]. Coordinates are pixels in the 640 × 640 input image. These details were checked against the exported graph and the version-pinned Ultralytics detection head.

Prepare the image and run inference

YOLO expects RGB pixels scaled to the range 0–1. We resize while preserving the image’s aspect ratio and pad the remaining area with the value 114. This is called letterboxing. To draw the predictions on the original image, we subtract the padding and divide by the resize scale.

Notice that the code divides leftover padding between opposite edges. If the padding is an odd number of pixels, one edge gets the extra pixel, keeping the input exactly 640 × 640.

Save the following as tutorial_yolo26.py, or use that file from the download. Run it from the yolo26 folder using the OpenCV inference environment:

python tutorial_yolo26.py
"""Article example: run after export_yolo26.py, from this directory."""
import json
import os

# Check before importing OpenCV: this variable overrides the named engine.
if "OPENCV_FORCE_DNN_ENGINE" in os.environ:
    raise RuntimeError("Unset OPENCV_FORCE_DNN_ENGINE before running this script.")
import cv2
import numpy as np

# Current 5.x calls this ENGINE_OPENCV; the tested 5.0.0 wheel uses ENGINE_NEW.
if hasattr(cv2.dnn, "ENGINE_OPENCV"):
    DNN_ENGINE = cv2.dnn.ENGINE_OPENCV
elif hasattr(cv2.dnn, "ENGINE_NEW"):
    DNN_ENGINE = cv2.dnn.ENGINE_NEW
else:
    raise RuntimeError("This example requires OpenCV 5's built-in DNN engine.")

image = cv2.imread("bus.jpg")
if image is None:
    raise FileNotFoundError("Run export_yolo26.py to download bus.jpg first.")
with open("coco80.json") as stream:
    classes = json.load(stream)

# Preserve aspect ratio, then pad to the exported model's fixed input size.
height, width = image.shape[:2]
size = 640
gain = min(size / width, size / height)
new_width, new_height = round(width * gain), round(height * gain)
resized = cv2.resize(image, (new_width, new_height))
left, top = (size - new_width) // 2, (size - new_height) // 2
padded = cv2.copyMakeBorder(
    resized, top, size - new_height - top, left, size - new_width - left,
    cv2.BORDER_CONSTANT, value=(114, 114, 114),
)

# The model expects RGB float32 pixels in [0, 1], in NCHW order.
blob = cv2.dnn.blobFromImage(padded, 1 / 255.0, swapRB=True, crop=False)
net = cv2.dnn.readNetFromONNX("yolo26n.onnx", DNN_ENGINE)
# The selected built-in OpenCV engine currently runs on CPU.
net.setInput(blob)
output = net.forward()
if output.shape != (1, 300, 6):
    raise ValueError(f"Use the accompanying end-to-end export; got {output.shape}")

# Each row is x1, y1, x2, y2, confidence, class_id. No NMS is needed.
detections = output[0][output[0, :, 4] >= 0.35].copy()
detections[:, [0, 2]] = (detections[:, [0, 2]] - left) / gain
detections[:, [1, 3]] = (detections[:, [1, 3]] - top) / gain
detections[:, [0, 2]] = detections[:, [0, 2]].clip(0, width)
detections[:, [1, 3]] = detections[:, [1, 3]].clip(0, height)

for x1, y1, x2, y2, score, class_id in detections:
    if x2 <= x1 or y2 <= y1:
        continue
    p1 = (int(round(x1)), int(round(y1)))
    p2 = (int(round(x2)), int(round(y2)))
    label = f"{classes[int(class_id)]} {score:.2f}"
    cv2.rectangle(image, p1, p2, (40, 220, 40), 2)
    cv2.putText(image, label, (p1[0], max(p1[1] - 8, 22)),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, (40, 220, 40), 2, cv2.LINE_AA)
    print(label)

if not cv2.imwrite("yolo26_tutorial_result.jpg", image):
    raise OSError("Could not write output image.")

Actual output from the supplied code: one bus and four people at a confidence threshold of 0.35. Source image: Ultralytics bus sample. The partially visible people at the left and right edges receive their own boxes.

For your own image, use the more flexible detect_yolo26.py script included in the download:

python detect_yolo26.py --image your_photo.jpg --output your_result.jpg

Example 2: Instance segmentation with RF-DETR

A bounding box tells us where an object is. An instance mask adds its visible outline, separating the object’s pixels from nearby objects and the background. This is useful when an application needs more than a rectangular region, such as isolating objects before measuring their visible area.

We will use RF-DETR Seg Nano, the instance segmentation variant, with its 312 × 312 input. The RF-DETR segmentation documentation describes the model family. Detection-only RF-DETR checkpoints do not provide the mask outputs needed here.

Export the segmentation model

Open the download’s rfdetr folder and create its export environment:

python -m venv .venv-export-rfdetr
.venv-export-rfdetr/bin/python -m pip install -r requirements-export.txt
.venv-export-rfdetr/bin/python export_rfdetr.py --output-dir models

On Windows, replace .venv-export-rfdetr/bin/python with .venv-export-rfdetrScriptspython.exe.

The requirements pin RF-DETR 1.6.3, PyTorch 2.10.0, and compatible export dependencies. The exporter loads the pretrained segmentation checkpoint and writes models/inference_model.onnx with batch size 1, a fixed 312 × 312 input, and ONNX opset 17. See RF-DETR’s export documentation for the general export workflow; use the supplied version pins to reproduce this particular graph.

The exported model has three outputs:

Output name Tested shape Meaning
dets [1, 100, 4] Normalized center coordinates, width, and height for 100 object queries.
labels [1, 100, 91] Class logits for each query.
masks [1, 100, 78, 78] A grid of mask logits for each query.

An object query is a candidate object produced by the network. Its box, class scores, and mask must stay associated with the same query index throughout postprocessing.

Use RF-DETR’s preprocessing and output decoding

The input is RGB, directly resized to 312 × 312, scaled to 0–1, and normalized using the mean [0.485, 0.456, 0.406] and standard deviation [0.229, 0.224, 0.225]. This example uses OpenCV’s bilinear image resize. That is a specific preprocessing choice; it is not a claim of pixel-for-pixel equivalence with every training-framework resize implementation.

To decode the outputs, apply a sigmoid to the class logits and select the top 100 query/class pairs before filtering by confidence. Preserve each selected query index when retrieving its mask. The checkpoint uses COCO’s category IDs with gaps, so its 91 score columns cannot be labeled with YOLO’s contiguous 80-class list.

For the masks, resize the floating-point logits to the original image size before applying a threshold of zero. A logit greater than zero corresponds to a sigmoid probability greater than 0.5. Thresholding the small mask first and then enlarging it would produce a different boundary. This ordering follows the RF-DETR 1.6.3 postprocessing implementation.

The following complete script is included as rfdetr_segment.py. Switch back to the OpenCV inference environment and run:

python rfdetr_segment.py --model models/inference_model.onnx 
    --image assets/bus.jpg --output rfdetr_result.jpg

On Windows, enter the command on a single line.

"""RF-DETR Seg Nano (COCO) instance segmentation with OpenCV 5 and NumPy."""
import argparse
import os
from pathlib import Path

# Check before importing OpenCV: this variable overrides the named engine.
if "OPENCV_FORCE_DNN_ENGINE" in os.environ:
    raise RuntimeError("Unset OPENCV_FORCE_DNN_ENGINE before running this script.")
import cv2
import numpy as np

# Current 5.x calls this ENGINE_OPENCV; the tested 5.0.0 wheel uses ENGINE_NEW.
if hasattr(cv2.dnn, "ENGINE_OPENCV"):
    DNN_ENGINE, DNN_ENGINE_NAME = cv2.dnn.ENGINE_OPENCV, "ENGINE_OPENCV"
elif hasattr(cv2.dnn, "ENGINE_NEW"):
    DNN_ENGINE, DNN_ENGINE_NAME = cv2.dnn.ENGINE_NEW, "ENGINE_NEW"
else:
    raise RuntimeError("This example requires OpenCV 5's built-in DNN engine.")

# This checkpoint uses sparse COCO category IDs (1..90), not a dense 0..79 list.
COCO_IDS = [
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20,
    21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,
    59, 60, 61, 62, 63, 64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79,
    80, 81, 82, 84, 85, 86, 87, 88, 89, 90,
]
COCO_NAMES = (
    "person|bicycle|car|motorcycle|airplane|bus|train|truck|boat|traffic light|"
    "fire hydrant|stop sign|parking meter|bench|bird|cat|dog|horse|sheep|cow|"
    "elephant|bear|zebra|giraffe|backpack|umbrella|handbag|tie|suitcase|frisbee|"
    "skis|snowboard|sports ball|kite|baseball bat|baseball glove|skateboard|"
    "surfboard|tennis racket|bottle|wine glass|cup|fork|knife|spoon|bowl|banana|"
    "apple|sandwich|orange|broccoli|carrot|hot dog|pizza|donut|cake|chair|couch|"
    "potted plant|bed|dining table|toilet|tv|laptop|mouse|remote|keyboard|"
    "cell phone|microwave|oven|toaster|sink|refrigerator|book|clock|vase|"
    "scissors|teddy bear|hair drier|toothbrush"
).split("|")
CLASSES = dict(zip(COCO_IDS, COCO_NAMES))


def preprocess(image):
    # Direct square resize: RF-DETR does not use YOLO's letterbox transform.
    rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    rgb = cv2.resize(rgb, (312, 312), interpolation=cv2.INTER_LINEAR)
    rgb = rgb.astype(np.float32) / 255.0
    rgb = (rgb - np.array([0.485, 0.456, 0.406], np.float32)) / np.array(
        [0.229, 0.224, 0.225], np.float32
    )
    return np.ascontiguousarray(rgb.transpose(2, 0, 1)[None])


def decode(dets, logits, mask_logits, image_shape, threshold):
    if dets.shape != (1, 100, 4) or logits.shape != (1, 100, 91):
        raise ValueError("Expected the supplied static RF-DETR Seg Nano COCO export")
    if mask_logits.shape != (1, 100, 78, 78):
        raise ValueError(f"Unexpected mask shape: {mask_logits.shape}")
    # Select query/class PAIRS, preserving the query index for each mask.
    scores = 1.0 / (1.0 + np.exp(-np.clip(logits[0], -80, 80)))
    flat = scores.ravel()
    chosen = np.argsort(-flat)[:100]  # Matches this model's num_select=100.
    h, w = image_shape[:2]
    instances = []
    for index in chosen:
        score = float(flat[index])
        if score < threshold:
            continue
        query, category = divmod(int(index), 91)
        if category not in CLASSES:
            continue
        cx, cy, bw, bh = dets[0, query]
        box = np.array([cx - bw / 2, cy - bh / 2, cx + bw / 2, cy + bh / 2])
        box *= np.array([w, h, w, h])
        box = np.clip(box, [0, 0, 0, 0], [w - 1, h - 1, w - 1, h - 1])
        # Resize logits first, THEN threshold. A logit >0 means sigmoid >0.5.
        mask = cv2.resize(mask_logits[0, query], (w, h),
                          interpolation=cv2.INTER_LINEAR) > 0.0
        instances.append((category, score, box, mask))
    return instances  # RF-DETR does not need NMS.


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--model", required=True)
    parser.add_argument("--image", required=True)
    parser.add_argument("--output", default="rfdetr_result.jpg")
    parser.add_argument("--confidence", type=float, default=0.5)
    args = parser.parse_args()
    if not 0 <= args.confidence <= 1:
        parser.error("--confidence must be between 0 and 1")
    image = cv2.imread(args.image)
    if image is None:
        raise FileNotFoundError(f"Cannot read image: {args.image}")
    net = cv2.dnn.readNetFromONNX(args.model, DNN_ENGINE)
    net.setInput(preprocess(image))
    dets, logits, masks = net.forward(["dets", "labels", "masks"])
    instances = decode(dets, logits, masks, image.shape, args.confidence)
    result = image.copy()
    for i, (category, score, box, mask) in enumerate(instances):
        color = ((71 * i + 60) % 256, (137 * i + 120) % 256, (199 * i + 200) % 256)
        result[mask] = (0.55 * result[mask] + 0.45 * np.array(color)).astype(np.uint8)
        contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL,
                                       cv2.CHAIN_APPROX_SIMPLE)
        cv2.drawContours(result, contours, -1, color, 2)
    for category, score, box, _ in instances:
        x1, y1, x2, y2 = np.rint(box).astype(int)
        label = f"{CLASSES[category]} {score:.2f}"
        cv2.rectangle(result, (x1, y1), (x2, y2), (255, 255, 255), 1)
        cv2.putText(result, label, (x1, max(y1 - 5, 18)), cv2.FONT_HERSHEY_SIMPLEX,
                    0.6, (0, 0, 0), 3, cv2.LINE_AA)
        cv2.putText(result, label, (x1, max(y1 - 5, 18)), cv2.FONT_HERSHEY_SIMPLEX,
                    0.6, (255, 255, 255), 1, cv2.LINE_AA)
        print(label)
    Path(args.output).parent.mkdir(parents=True, exist_ok=True)
    if not cv2.imwrite(args.output, result):
        raise OSError(f"Cannot write output: {args.output}")
    print(f"Saved {len(instances)} instances to {args.output} "
          f"(OpenCV {cv2.__version__}, {DNN_ENGINE_NAME}, CPU)")


if __name__ == "__main__":
    main()

Actual output from the supplied code: five instances at a confidence threshold of 0.5. The colored masks follow the visible bus and people. The models run at different input sizes and use different thresholds; these two images illustrate detection and segmentation, rather than a controlled comparison of model accuracy or speed.

What was tested?

Both scripts were run on an Apple M5 Pro using OpenCV 5.0.0’s new CPU engine. They were also rerun in a fresh environment with only opencv-python-headless==5.0.0.93 and numpy==2.3.5 installed as application dependencies. PyTorch, Ultralytics, RF-DETR, and ONNX Runtime were absent from that inference environment.

The YOLO26 output was checked against the source PyTorch model on the same preprocessed input. RF-DETR’s output tensors were checked against ONNX Runtime, and its decoded masks were checked against the original RF-DETR postprocessing on identical tensors. The download includes the validation records and export settings. These checks establish that the supplied examples execute and decode their outputs correctly on the tested setup; they are not a dataset-level accuracy evaluation. The newer ENGINE_OPENCV API was verified against source; this article does not claim execution testing of a build from the current development branch.

GPU inference and migration from OpenCV 4

OpenCV’s built-in engine used here runs on the CPU, and the inspected current 5.x header retains that restriction. Simply setting DNN_TARGET_CUDA on these examples does not move them to the GPU. The classic-engine GPU route described for the original 5.0.0 release applies only to builds that still provide that engine and the required accelerator backend; the current 5.x API no longer offers ENGINE_CLASSIC. Check the API and build you are deploying rather than assuming the original release’s fallback behavior still applies. Current engine API, 5.0.0 migration guidance.

OpenCV’s optional ONNX Runtime integration is another deployment path, including GPU execution providers in appropriately configured builds. It must be compiled into OpenCV. Installing the Python onnxruntime package alone does not add that integration to an existing OpenCV wheel. The OpenCV 5 build overview documents the relevant CMake options.

Also revisit old model-loading code during migration. OpenCV 5 removes the Caffe and Darknet parsers; export or convert those models to ONNX. An ONNX file still needs compatible operators, supported tensor shapes, and the right preprocessing. Keep a record of the checkpoint, export settings, OpenCV build, and a known test image so that future upgrades can be checked against a working reference. The removed formats are documented in the OpenCV 5 model-loading implementation.

To try your own images now, keep the model and preprocessing unchanged and replace the image path. If you switch to a different model size or a custom checkpoint, update the expected input size, output shapes, and class mapping together. Those details are part of the model’s interface, just as much as the ONNX filename.

The post OpenCV DNN Module: Deep Learning Inference in OpenCV 5 appeared first on OpenCV.