Why can a rectangular object detector box include so much empty background? When a ship appears at an angle, a rectangle aligned with the image edges may be much larger than a rotated rectangle around the same prediction. In this hands-on Python example, we use a YOLO oriented bounding box (OBB) model, inspect its four corner points and compare two ways of drawing that same prediction.
Important: this is a comparison of box geometry, not a test of two different detectors or a measurement of detection accuracy. A predicted box may miss part of an object, identify the wrong object, or overlap another prediction.
Watch the tutorial
Watch the full YouTube tutorial for the prediction, the geometry and its limitations.
Prefer a quick demonstration? Watch the YouTube Short.
See the difference

For this selected prediction, the orange rectangle has about 3.75 times the area of the green one. This ratio is calculated from the predicted coordinates and describes this single example. It does not say how accurately either box matches the true ship.
What is an oriented bounding box?
An axis-aligned bounding box is parallel to the image edges. An OBB can rotate to follow an object's orientation. Ultralytics exposes the four corners through result.obb.xyxyxyxy and a center, width, height and rotation representation through result.obb.xywhr. The angles in the latter are in radians. An OBB has no inherent front or back direction: rotating a rectangle by 180 degrees produces the same rectangle. See the Ultralytics OBB documentation; online documentation may describe versions newer than the one tested here.
Neither a standard box nor an OBB is a segmentation mask. Both are rectangles and can contain background. A smaller rectangle is not automatically a better or more accurate detection; that requires comparison with appropriate ground-truth annotations.
The image and model
The input below is a synthetic harbor image from the course. It is shown so you can see exactly what the program receives. You can save the image for this demonstration or use your own permitted image; other inputs may yield different results.

We used the official YOLOv8n-OBB checkpoint locally. It is an aerial-image-oriented model, so the example uses ships. The tested model's SHA-256 is fa6e4cd2691f132875c143135affaa66b5d89394ebb1d07d19770a9b6382c1b8. Choose a model appropriate to the objects you want to detect.
Set up the example
The actual run used Windows CPU, Python 3.13.9, Ultralytics 8.4.117, OpenCV 5.0.0, NumPy 2.5.1 and PyTorch 2.14.1+cpu. The code below was run in that existing environment; a fresh installation has not been independently verified. On Windows, create an environment and install the direct package:
py -3.13 -m venv .venv
.venv\Scripts\python.exe -m pip install ultralytics==8.4.117
Installing packages and obtaining the checkpoint require internet access unless they are already available locally. The command pins the direct package but not every dependency. Place the image in your project folder as input.png, the downloaded checkpoint as yolov8n-obb.pt, and the complete example below as obb_demo.py.
Complete Python example
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 ARTSCI - Author of the AI/ML/CV course materials.
"""Run one oriented detector and draw its axis-aligned enclosing rectangles.
The horizontal boxes are computed from the *same* oriented predictions; they
are not predictions from a second model. Model detections are not ground truth.
"""
import argparse
import hashlib
import json
from pathlib import Path
import cv2
import numpy as np
from ultralytics import YOLO
def sha256(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--image', type=Path, required=True)
parser.add_argument('--weights', type=Path, required=True)
parser.add_argument('--output', type=Path, default=Path('output'))
args = parser.parse_args()
for path in (args.image, args.weights):
if not path.is_file():
raise FileNotFoundError(path)
image = cv2.imread(str(args.image))
if image is None:
raise ValueError(f'Cannot read image: {args.image}')
args.output.mkdir(parents=True, exist_ok=True)
model = YOLO(str(args.weights))
result = model.predict(source=str(args.image), conf=0.25, imgsz=1024,
device='cpu', verbose=False)[0]
if result.obb is None or len(result.obb) == 0:
raise ValueError('No oriented detections in this image')
corners = result.obb.xyxyxyxy.cpu().numpy()
xywhr = result.obb.xywhr.cpu().numpy()
scores = result.obb.conf.cpu().numpy()
classes = result.obb.cls.cpu().numpy().astype(int)
lower = corners.min(axis=1)
upper = corners.max(axis=1)
obb_area = xywhr[:, 2] * xywhr[:, 3]
span = upper - lower
axis_area = span[:, 0] * span[:, 1]
if np.any(obb_area <= 0):
raise ValueError('Invalid oriented box area')
ratios = axis_area / obb_area
if np.any(ratios < 0.999):
raise ValueError('Axis-aligned box should enclose its oriented box')
rotated = image.copy()
aligned = image.copy()
rows = []
for i, points in enumerate(corners):
poly = np.rint(points).astype(np.int32)
lo = tuple(np.floor(lower[i]).astype(int))
hi = tuple(np.ceil(upper[i]).astype(int))
cv2.polylines(rotated, [poly], True, (34, 220, 66), 3, cv2.LINE_AA)
cv2.rectangle(aligned, lo, hi, (0, 192, 255), 3, cv2.LINE_AA)
rows.append({'id': i + 1, 'class': result.names[int(classes[i])],
'confidence': float(scores[i]), 'corners': points.tolist(),
'axis_box_xyxy': [*lower[i].tolist(), *upper[i].tolist()],
'obb_area': float(obb_area[i]), 'axis_area': float(axis_area[i]),
'axis_over_obb': float(ratios[i]),
'angle_deg': float(np.degrees(xywhr[i, 4]))})
def save(path, img):
if not cv2.imwrite(str(path), img):
raise RuntimeError(f'Failed to write {path}')
save(args.output / 'annotated_obb.png', rotated)
save(args.output / 'annotated_axis.png', aligned)
# Highest-scoring detection is used as an example, not as ground truth.
best = int(np.argmax(scores))
x1, y1 = np.floor(lower[best] - 50).astype(int)
x2, y2 = np.ceil(upper[best] + 50).astype(int)
h, w = image.shape[:2]
x1, y1, x2, y2 = max(0, x1), max(0, y1), min(w, x2), min(h, y2)
if x1 >= x2 or y1 >= y2:
raise ValueError('Could not crop selected detection')
save(args.output / 'crop_original.png', image[y1:y2, x1:x2])
# Draw only selected detection on each crop so nearby boxes do not mislead.
a = image.copy()
b = image.copy()
cv2.rectangle(a, tuple(np.floor(lower[best]).astype(int)),
tuple(np.ceil(upper[best]).astype(int)), (0, 192, 255), 4, cv2.LINE_AA)
cv2.polylines(b, [np.rint(corners[best]).astype(np.int32)], True,
(34, 220, 66), 4, cv2.LINE_AA)
save(args.output / 'crop_axis.png', a[y1:y2, x1:x2])
save(args.output / 'crop_obb.png', b[y1:y2, x1:x2])
save(args.output / 'comparison.png', np.hstack((a[y1:y2, x1:x2], b[y1:y2, x1:x2])))
report = {'image_sha256': sha256(args.image), 'weights_sha256': sha256(args.weights),
'image_shape_hw': [h, w], 'model': 'yolov8n-obb', 'device': 'cpu',
'confidence_threshold': 0.25, 'imgsz': 1024,
'count': len(rows), 'classes': sorted(set(x['class'] for x in rows)),
'mean_axis_over_obb': float(ratios.mean()),
'selected_id': best + 1, 'selected_axis_over_obb': float(ratios[best]),
'selected_crop_xyxy': [int(x1), int(y1), int(x2), int(y2)],
'detections': rows,
'note': 'Axis boxes computed from the same predicted OBB corners, not a second detector. No ground truth assessed.'}
(args.output / 'result.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps({k: v for k, v in report.items() if k != 'detections'}, indent=2))
if __name__ == '__main__':
main()
Run the example from that directory:
.venv\Scripts\python.exe obb_demo.py --image input.png --weights yolov8n-obb.pt --output output
The script saves the original crop, an orange horizontal-box view, a green OBB view, a side-by-side comparison and the coordinates and ratios in output/result.json. It explicitly checks that the image and weights exist, then runs a single CPU prediction at confidence 0.25 and image size 1024.
How the comparison is calculated
- The model returns four corner points for each predicted oriented box.
- The code takes the minimum and maximum x and y values of those same points to obtain an enclosing axis-aligned rectangle.
- It divides the area of that horizontal rectangle by the area of the oriented box.
This calculation compares the enclosing geometry of an OBB prediction, not the output of a conventional object-detection model. Rounding the outline for drawing can make rendered line positions differ slightly from the floating-point coordinates used for areas.
What this run actually found
On this one synthetic image, the model returned 9 boxes labelled ship. The mean ratio of axis-aligned enclosing area to OBB area was about 2.73 for those predictions. The numbers are not a count of verified ships, a recall score, an accuracy benchmark or a prediction for other images.

The overlapping boxes on the upper-right ship illustrate a real limitation: even if the boxes are rotated, predictions can be duplicated or incomplete. Confidence is a model score, not proof that a box is correct. Inspect the image and evaluate against labelled data before relying on a model for an application.
Where can this help?
Rotated boxes can be useful for aerial images, rotated packages, vehicles and other elongated objects. Suitability depends on the model and its training data; this specific example does not establish that the supplied checkpoint can detect every object or domain. Try your own permitted image and compare the resulting boxes with the actual objects.
Explore the course lesson on oriented boxes and other practical computer vision projects: see the current promotional offer. Coupon terms and availability may change. You can also use the course link.
Code and component licenses
The complete example script above is © 2026 ARTSCI - Author of the AI/ML/CV course materials, licensed under GNU AGPL-3.0 only, without warranty. Preserve applicable notices and provide corresponding source when sharing covered modifications. This code notice does not automatically license the harbor image, videos or other course media.
The Ultralytics software and pretrained weights retain their own license terms; see the assets license, Ultralytics source and licensing information for your intended use. Other dependencies have their own licenses.


