Overview
The Ultralytcis ecosystem ships with a lightweight Annotator utility that can overlay detection masks, bounding boxes, oriented boxes, and keypoints on any image or video stream. The snippets below demonstrate typical use-cases.
Interactive sweep counter on a video
The following example tracks every object that crosses a user-draggable veritcal line and continuously prints the running count. The line positino is updated through mouse events.
import cv2
import numpy as np
from ultralytics import YOLO
from ultralytics.utils.plotting import Annotator, colors
video_path = "demo.mp4"
model_path = "yolo11s-seg.pt"
capture = cv2.VideoCapture(video_path)
model = YOLO(model_path)
if not capture.isOpened():
raise RuntimeError("Cannot open video file")
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(capture.get(cv2.CAP_PROP_FPS))
writer = cv2.VideoWriter("annotated.avi",
cv2.VideoWriter_fourcc(*"mp4v"),
fps, (width, height))
line_x = width # vertical line starts at the right border
drag = False
frame_id = 0
def on_mouse(event, mx, my, flags, _):
global line_x, drag
if event == cv2.EVENT_LBUTTONDOWN:
drag = True
if drag and (flags & cv2.EVENT_FLAG_LBUTTON):
line_x = max(0, min(mx, width))
cv2.namedWindow("Sweep")
cv2.setMouseCallback("Sweep", on_mouse)
while capture.isOpened():
ok, img = capture.read()
if not ok:
break
frame_id += 1
annot = Annotator(img)
preds = model.track(img, persist=True)[0]
counter = 0
if preds.boxes.id is not None:
ids = preds.boxes.id.int().cpu().tolist()
cls = preds.boxes.cls.cpu().tolist()
boxes = preds.boxes.xyxy.cpu().numpy()
masks = preds.masks.xy if preds.masks else [None] * len(boxes)
for m, b, c, tid in zip(masks, boxes, cls, ids):
col = colors(tid, True)
if b[0] > line_x:
counter += 1
if m is not None and len(m):
annot.seg_bbox(mask=m, mask_color=col, label=str(model.names[int(c)]))
else:
annot.box_label(box=b, color=col, label=str(model.names[int(c)]))
annot.sweep_annotator(line_x=line_x, line_y=height, label=f"Count:{counter}")
cv2.imshow("Sweep", img)
writer.write(img)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
capture.release()
writer.release()
cv2.destroyAllWindows()
Horizontal bounding boxes on a static image
Quickly draw classic axis-aligned boxes with custom labels.
import cv2
import numpy as np
from ultralytics.utils.plotting import Annotator, colors
labels = {0: "person", 5: "bus", 11: "stop sign"}
img = cv2.imread("bus.jpg")
ann = Annotator(img,_width=None, font_size=None, font="Arial.ttf", pil=False)
boxes = np.array([
[5, 22.9, 231.3, 805.0, 756.8],
[0, 48.6, 398.6, 245.4, 902.7],
[0, 669.5, 392.2, 809.7, 877.0],
[0, 221.5, 405.8, 345.0, 857.5],
[0, 0.0, 550.5, 63.0, 873.4],
[11, 0.1, 254.5, 32.6, 324.9],
])
for row in boxes:
cid, *xyxy = row
ann.box_label(xyxy, label=f"{int(cid):02d}:{labels[int(cid)]}",
color=colors(int(cid), bgr=True))
cv2.imwrite("bus_bboxes.jpg", ann.result())
Oriented bounding boxes (OBB)
For datasets such as DOTA, you can render rotated rectangles by passing the four corner points.
import cv2
import numpy as np
from ultralytics.utils.plotting import Annotator, colors
names = {0: "small vehicle"}
img = cv2.imread("P1142__1024__0___824.jpg")
obb = np.array([
[0, 635, 560, 919, 719, 1087, 420, 803, 261],
[0, 331, 19, 493, 260, 776, 70, 613, -171],
])
ann = Annotator(img, line_width=None, font_size=None, font="Arial.ttf", pil=False)
for row in obb:
cid, *pts = row
pts = np.array(pts).reshape(4, 2)
ann.box_label(pts, label=names[int(cid)],
color=colors(int(cid), True), rotated=True)
cv2.imwrite("obb_demo.jpg", ann.result())