Edges mark sharp intensity changes that often become the first boundary signal in a vision pipeline. In OpenCV, Canny edge detection turns a photo or generated scene into a binary edge map before contour finding, shape measurement, or visual inspection.
Canny works on a single-channel image, so the script loads the source as grayscale, applies a small Gaussian blur to reduce isolated noise, and passes two hysteresis thresholds to cv.Canny(). Pixels connected to strong gradients remain white in the output, while background and weak unconnected texture stay black.
The sample path uses input/scene.png and writes output/edges.png. The edge-pixel percentage gives a quick sanity check because a near-zero value usually means the thresholds are too high, while a crowded edge map usually means the thresholds are too low or the image needs more blur.
Related: How to read and write an image with OpenCV
Related: How to blur an image with OpenCV
Related: How to find contours with OpenCV
Use an image with visible object boundaries or scene structure. Canny works on grayscale data, so color is not required unless later project code needs it.
#!/usr/bin/env python3 import argparse from pathlib import Path import cv2 as cv import numpy as np def odd_kernel(value: str) -> int: kernel = int(value) if kernel < 1 or kernel % 2 == 0: raise argparse.ArgumentTypeError("blur kernel must be a positive odd integer") return kernel parser = argparse.ArgumentParser(description="Detect image edges with OpenCV Canny.") parser.add_argument("input_image", type=Path) parser.add_argument("output_image", type=Path) parser.add_argument("--low", type=float, default=80.0) parser.add_argument("--high", type=float, default=160.0) parser.add_argument("--blur", type=odd_kernel, default=5) parser.add_argument("--aperture", type=odd_kernel, default=3) parser.add_argument("--l2-gradient", action="store_true") args = parser.parse_args() if args.aperture not in {3, 5, 7}: raise SystemExit("aperture must be 3, 5, or 7") if args.low >= args.high: raise SystemExit("--low must be lower than --high") gray = cv.imread(str(args.input_image), cv.IMREAD_GRAYSCALE) if gray is None: raise SystemExit(f"could not read image: {args.input_image}") source = cv.GaussianBlur(gray, (args.blur, args.blur), 0) edges = cv.Canny( source, args.low, args.high, apertureSize=args.aperture, L2gradient=args.l2_gradient, ) args.output_image.parent.mkdir(parents=True, exist_ok=True) if not cv.imwrite(str(args.output_image), edges): raise SystemExit(f"could not write image: {args.output_image}") edge_pixels = int(np.count_nonzero(edges)) total_pixels = edges.size edge_percent = edge_pixels / total_pixels * 100 print(f"input: {args.input_image}") print(f"image size: {gray.shape[1]}x{gray.shape[0]}") print(f"thresholds: {args.low:.0f}/{args.high:.0f}") print(f"blur kernel: {args.blur}x{args.blur}") print(f"aperture: {args.aperture}") print(f"edge pixels: {edge_pixels} ({edge_percent:.2f}%)") print(f"output: {args.output_image}")
$ python3 detect_edges.py input/scene.png output/edges.png input: input/scene.png image size: 720x480 thresholds: 80/160 blur kernel: 5x5 aperture: 3 edge pixels: 4968 (1.44%) output: output/edges.png
--low and --high set the hysteresis thresholds. Raise both values when texture becomes edges, or lower them when object boundaries disappear.
$ python3 -c 'import cv2 as cv, numpy as np; edges = cv.imread("output/edges.png", cv.IMREAD_GRAYSCALE); print(edges.shape); print(np.unique(edges))'
(480, 720)
[ 0 255]
The shape confirms the saved image height and width. The two pixel values show a binary edge map where white pixels are retained edges.
$ rm detect_edges.py