Image overlays turn detection coordinates, regions of interest, and visual debug hints into marks that stay with the saved frame. In OpenCV, the same image array that holds pixels can receive labels, boxes, markers, and polylines before the file is written.
Drawing calls such as cv.rectangle(), cv.putText(), cv.circle(), and cv.polylines() modify the target image in place. Work on a copy when the original pixels need to remain available for comparison or later processing.
Color tuples for OpenCV image I/O use blue, green, red channel order. A translucent callout also needs a separate overlay buffer and a blend step, because drawing functions do not alpha-composite directly onto a four-channel image in the way most annotation workflows expect.
Related: How to read and write an image with OpenCV
Related: How to install OpenCV on Ubuntu
Use any image format that cv.imread() can load as a color image. The output examples use a 720 by 480 pixel scene so the coordinates and pixel counts remain easy to compare.
#!/usr/bin/env python3 import argparse from pathlib import Path import cv2 as cv import numpy as np def clamp_alpha(value: str) -> float: alpha = float(value) if not 0.0 <= alpha <= 1.0: raise argparse.ArgumentTypeError("alpha must be between 0.0 and 1.0") return alpha parser = argparse.ArgumentParser(description="Draw text and shape overlays with OpenCV.") parser.add_argument("input_image", type=Path) parser.add_argument("output_image", type=Path) parser.add_argument("--alpha", type=clamp_alpha, default=0.35) args = parser.parse_args() image = cv.imread(str(args.input_image), cv.IMREAD_COLOR) if image is None: raise SystemExit(f"could not read image: {args.input_image}") annotated = image.copy() height, width = annotated.shape[:2] box_start = (int(width * 0.12), int(height * 0.18)) box_end = (int(width * 0.62), int(height * 0.58)) label_origin = (box_start[0] + 18, box_start[1] - 14) center = (int(width * 0.75), int(height * 0.34)) radius = max(18, min(width, height) // 16) polyline = np.array( [ (int(width * 0.18), int(height * 0.76)), (int(width * 0.36), int(height * 0.66)), (int(width * 0.55), int(height * 0.82)), ], dtype=np.int32, ).reshape((-1, 1, 2)) overlay = annotated.copy() cv.rectangle(overlay, box_start, box_end, (0, 180, 255), cv.FILLED) cv.addWeighted(overlay, args.alpha, annotated, 1.0 - args.alpha, 0, annotated) cv.rectangle(annotated, box_start, box_end, (0, 140, 255), 3, cv.LINE_AA) cv.putText( annotated, "inspection zone", label_origin, cv.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 0), 4, cv.LINE_AA, ) cv.putText( annotated, "inspection zone", label_origin, cv.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2, cv.LINE_AA, ) cv.circle(annotated, center, radius, (255, 80, 80), 3, cv.LINE_AA) cv.drawMarker(annotated, center, (255, 255, 255), cv.MARKER_CROSS, radius * 2, 2, cv.LINE_AA) cv.polylines(annotated, [polyline], False, (80, 255, 80), 4, cv.LINE_AA) args.output_image.parent.mkdir(parents=True, exist_ok=True) if not cv.imwrite(str(args.output_image), annotated): raise SystemExit(f"could not write image: {args.output_image}") changed = cv.absdiff(image, annotated) changed_pixels = int(np.count_nonzero(cv.cvtColor(changed, cv.COLOR_BGR2GRAY))) x1, y1 = box_start x2, y2 = box_end roi_changed = cv.absdiff(image[y1:y2, x1:x2], annotated[y1:y2, x1:x2]) roi_pixels = int(np.count_nonzero(cv.cvtColor(roi_changed, cv.COLOR_BGR2GRAY))) print(f"input: {args.input_image}") print(f"alpha: {args.alpha:.2f}") print(f"changed pixels: {changed_pixels}") print(f"inspection zone changed pixels: {roi_pixels}") print(f"output: {args.output_image}")
$ python3 draw_image_overlays.py input/scene.png output/scene-overlays.png input: input/scene.png alpha: 0.35 changed pixels: 79925 inspection zone changed pixels: 69120 output: output/scene-overlays.png
The changed-pixel counts compare the input frame with the saved annotation output. A nonzero count in the inspection zone confirms that the translucent rectangle, border, and label altered the intended region.
cv.rectangle(annotated, box_start, box_end, (0, 140, 255), 3, cv.LINE_AA) cv.circle(annotated, center, radius, (255, 80, 80), 3, cv.LINE_AA) cv.polylines(annotated, [polyline], False, (80, 255, 80), 4, cv.LINE_AA)
cv.LINE_AA draws antialiased edges on 8-bit images. Swap color tuple values only after accounting for OpenCV BGR order.
Related: How to convert image color spaces with OpenCV
overlay = annotated.copy() cv.rectangle(overlay, box_start, box_end, (0, 180, 255), cv.FILLED) cv.addWeighted(overlay, args.alpha, annotated, 1.0 - args.alpha, 0, annotated)
Keep alpha between 0.0 and 1.0. The copied overlay has the same size and type as the image, which keeps cv.addWeighted() on a valid pair of arrays.
$ python3 - <<'PY'
import cv2 as cv
image = cv.imread("output/scene-overlays.png")
if image is None:
raise SystemExit("could not read output/scene-overlays.png")
print(f"overlay output readable: {image.shape[1]}x{image.shape[0]}")
PY
overlay output readable: 720x480
$ rm draw_image_overlays.py