← Back to list

Computer Vision — Oriented Bounding Boxes Object Detection (YOLO26)

Computer Vision

Marian Todorov · 2026-07-15 12:01 · 0 claps · 4.1 min read
#python #computer-vision #yolo26
Open on Medium ↗
Wiki topics: MM · Multimodal & Generative Media

Computer Vision — Oriented Bounding Boxes Object Detection (YOLO26)

This article provides an example of using Ultralytics YOLO26 for Oriented Bounding Boxes Object Detection using Python and OpenCV on Linux.

Oriented Bounding Boxes Object Detection is a Computer Vision task for identifying specific objects within an image. The detections consist of object class, confidence score, bounding box coordinates and rotation angle.

Prerequisites

The sample code in this article has the following software prerequisites:

  • Python 3.13.5
  • OpenCV 4.13.0.92
  • ONNX Runtime 1.24.4
  • Ultralytics Python API 8.4.32 (required for ONNX model conversion only)

We use the following to install the software prerequisites:

sudo apt-get update
sudo apt-get install python3 python3-venv -y
python3 -m venv ~/Python/.venv
source ~/Python/.venv/bin/activate
pip3 install --upgrade pip wheel
pip3 install pillow opencv-python onnxruntime ultralytics

The Oriented Bounding Boxes Object Detection model used is the Ultralytics YOLO26 Nano (yolo26n-obb). To convert the model from the original PyTorch format (.pt) to ONNX format (.onnx) we use the following:

yolo export model=yolo26n-obb.pt format=onnx && rm yolo26n-obb.pt

You can also select different YOLO26 variants (e.g. Small (yolo26s-obb), Medium (yolo26m-obb), Large (yolo26l-obb) or Extra Large (yolo26x-obb)).

The YOLO26 model is trained on DOTA Dataset. This example uses a text version of the DOTA classes. You can download the list using the following:

wget https://raw.githubusercontent.com/ultralytics/ultralytics/refs/heads/main/ultralytics/cfg/datasets/DOTAv1.yaml

Oriented Bounding Boxes Object Detection

To run Oriented Bounding Boxes Object Detection inference using the YOLO26 model, we create the following Python files:

Program.py:

#!/usr/bin/python3

import cv2
import numpy
import sys
from PIL import Image
from YOLOModel import YOLOModel

# Display image
def DisplayImage(title: str, image: numpy.ndarray):
    cv2.namedWindow(title, cv2.WINDOW_AUTOSIZE)
    while True:
        cv2.imshow(title, image)
        key = cv2.waitKey(1) & 0xFF
        if key == 27 or key == ord("q"):
            break
    cv2.destroyAllWindows()

if __name__ == "__main__":
    fileName = sys.argv[1]
    model = YOLOModel(detectionThreshold=0.3)
    image = numpy.array(Image.open(fileName).convert("RGB"))
    image = model.Execute(image)
    DisplayImage(fileName, cv2.cvtColor(image, cv2.COLOR_RGB2BGR))

The Program.py file is the main Python application file. It contains the application entry point (if __name__ == "__main__":), which initializes the YOLO26 model class (model = YOLOModel(detectionThreshold=0.3)). Once the model is initialized, the application loads the input image specified as the first command line argument (sys.argv[1]), runs model inference (model.Execute(image)) and displays the results (DisplayImage()) in an OpenCV window.

YOLOModel.py:

#!/usr/bin/python3

import cv2
import numpy
import onnxruntime

# YOLO model (Type: Oriented Bounding Boxes Object Detection, Resolution: 1024×1024×3, Dataset: DOTAv1, Framework: ONNX)
class YOLOModel:

    # Initialization
    def __init__(self, detectionThreshold: float):
        super().__init__()
        self.__classNames = self.LoadClassNames("./DOTA_v1.txt")
        self.__session = onnxruntime.InferenceSession("./yolo26n-obb.onnx", providers=self.GetProviders())
        self.__inputWidth = self.__session.get_inputs()[0].shape[3]
        self.__inputHeight = self.__session.get_inputs()[0].shape[2]
        self.__detectionThreshold = detectionThreshold
        self.__textBackgroundColor = (255, 255, 255)

    # Load classes
    def LoadClassNames(self, fileName):
        with open(fileName, "r") as file:
            return [line.title() for line in file.read().split("\n")]

    # Return ONNX Runtime providers
    def GetProviders(self) -> list[str]:
        return [provider for provider in ("CUDAExecutionProvider", "CPUExecutionProvider") if provider in onnxruntime.get_available_providers()]

    # Execute model
    def Execute(self, image: numpy.ndarray) -> numpy.ndarray:
        (input, padding) = self.GetInput(image)
        output = self.__session.run(None, {self.__session.get_inputs()[0].name: input})
        return self.ProcessOutput(image, output, padding)

    # Return model input
    def GetInput(self, image: numpy.ndarray) -> tuple[numpy.ndarray, tuple[int, int]]:
        (image, padding) = self.LetterBoxInputImage(image, self.__inputWidth, self.__inputHeight)
        image = numpy.array(image) / 255.0
        image = numpy.transpose(image, (2, 0, 1))
        image = numpy.expand_dims(image, axis=0)
        image = image.astype(numpy.float32)
        return (image, padding)

    # Resize and reshape image while maintaining aspect ratio by adding padding
    def LetterBoxInputImage(self, image: numpy.ndarray, targetWidth: int, targetHeight: int) -> tuple[numpy.ndarray, tuple[int, int]]:
        shape = image.shape[:2]
        (imageHeight, imageWidth) = shape
        scaleRatio = min(targetHeight / imageHeight, targetWidth / imageWidth)
        padding = round(imageWidth * scaleRatio), round(imageHeight * scaleRatio)
        (paddingWidth, paddingHeight) = padding
        if shape[::-1] != padding:
            image = cv2.resize(image, padding, interpolation=cv2.INTER_LINEAR)
        (deltaWidth, deltaHeight) = (targetWidth - paddingWidth) / 2, (targetHeight - paddingHeight) / 2
        (top, bottom) = round(deltaHeight - 0.1), round(deltaHeight + 0.1)
        (left, right) = round(deltaWidth - 0.1), round(deltaWidth + 0.1)
        image = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114, 114, 114))
        return image, (top, left)

    # Process model output (YOLOv10+)
    def ProcessOutput(self, image: numpy.ndarray, output, padding: tuple[int, int]) -> numpy.ndarray:
        predictions = output[0].squeeze(0)
        predictions = predictions[predictions[:, 4] >= self.__detectionThreshold, :]
        if len(predictions) > 0:
            boundingBoxes = predictions[:, 0:4]
            (imageHeight, imageWidth) = image.shape[:2]
            scale = min(self.__inputHeight / imageHeight, self.__inputWidth / imageWidth)
            boundingBoxes[:, 0] = (boundingBoxes[:, 0] - padding[1]) / scale
            boundingBoxes[:, 1] = (boundingBoxes[:, 1] - padding[0]) / scale
            boundingBoxes[:, 2] = boundingBoxes[:, 2] / scale
            boundingBoxes[:, 3] = boundingBoxes[:, 3] / scale
            classScores = predictions[:, 4]
            classIndices = predictions[:, 5]
            boundingBoxAngles = numpy.rad2deg(predictions[:, 6])
            for index, score, box, angle in zip(classIndices.astype(int), classScores, boundingBoxes, boundingBoxAngles):
                (x, y, width, height) = box
                image = self.DrawBoundingBoxRotated(image, f"{self.__classNames[index]}: {score:.0%}", x, y, width, height, angle)
        return image

    # Draw bounding box (rotated)
    def DrawBoundingBoxRotated(self, image: numpy.ndarray, title: str, x: float, y: float, width: float, height: float, angle: float) -> numpy.ndarray:
        box = cv2.boxPoints(((x, y), (width, height), angle)).astype(int)
        cv2.drawContours(image, [box], 0, self.__textBackgroundColor, thickness=1)
        return self.DrawTextRotated(image, title, x, y, -angle)

    # Draw text (top left aligned)
    def DrawTextRotated(self, image: numpy.ndarray, text: str, x: float, y: float, angle: float) -> numpy.ndarray:
        fontScale = 0.5
        thickness = 1
        (textWidth, _), baseline = cv2.getTextSize(text, fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=fontScale, thickness=thickness)
        textImage = numpy.zeros_like(image)
        cv2.putText(textImage, text, (int(x - textWidth / 2), int(y + baseline)), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=fontScale, color=self.__textBackgroundColor, thickness=thickness, lineType=cv2.LINE_AA)
        rotationMatrix = cv2.getRotationMatrix2D((x, y), angle, 1)
        rotatedImage = cv2.warpAffine(textImage, rotationMatrix, (image.shape[1], image.shape[0]))
        return cv2.add(image, rotatedImage)

The YOLOModel class handles the following stages of the Oriented Bounding Boxes Object Detection task:

  • Initialization: The __init__() method loads the ONNX model (yolo26n-obb.onnx), creates an ONNX Inference Session (self.__session = onnxruntime.InferenceSession()) and loads the DOTA classes (LoadClassNames()).
  • Image Pre-processing: The image is transformed into the model input shape (e.g. (1, 3, 1024, 1024) BCHW) using the GetInput() method. The image is resized and padded by maintaining the aspect ratio using the LetterBoxInputImage() method.
  • Model Inference: The model inference is performed in the Execute() method by calling self.__session.run().
  • Image Post-processing: The model output is decoded in the ProcessOutput() method. The detections are filtered based on the specified confidence score threshold (e.g. predictions[:, 4] >= self.__detectionThreshold) and then displayed on the original image using the DrawBoundingBoxRotated() method. The DrawBoundingBoxRotated() method draws the rotated detection bounding boxes and the DrawTextRotated() method draws the detected object class name next to the rotated bounding box.

BCHW stands for Batch, Channel, Height, Width.

To run the application we use the following (./Image.jpg is the path to an input image):

python3 Program.py ./Image.jpg

Model Output

Model Output

References


메타데이터
post_id
ffc1cee43d4c
slug
computer-vision-oriented-bounding-boxes-object-detection-yolo26-ffc1cee43d4c
url
https://medium.com/@meriffa/computer-vision-oriented-bounding-boxes-object-detection-yolo26-ffc1cee43d4c
canonical_url
https://medium.com/@meriffa/computer-vision-oriented-bounding-boxes-object-detection-yolo26-ffc1cee43d4c
author_url
https://medium.com/@meriffa
status
ok
fetched_at
2026-07-15 22:45:01