Edge Detection¶
Edges are among the most fundamental features in robot vision. An edge marks the boundary between two regions of an image where the intensity changes sharply—corresponding to object boundaries, surface markings, shadows, or texture transitions. Detecting edges reduces the amount of data a robot needs to process while preserving the structural information essential for tasks such as lane following, object recognition, grasping, and navigation.
This tutorial covers the theory and practice of edge detection, from simple gradient operators to the widely-used Canny detector, the Hough Transform for geometric shape extraction, and contour analysis for downstream robotics applications.
Learning Objectives¶
After completing this tutorial you will be able to:
- Explain what edges are and why they matter in robot perception
- Compute image gradients using Sobel, Scharr, and Laplacian of Gaussian operators
- Apply the Canny edge detector and tune its parameters for different scenarios
- Detect lines and circles using the Hough Transform
- Find, analyze, and approximate contours for object recognition
- Apply edge and contour techniques to real robotics tasks (lane detection, pick-and-place ROI)
Prerequisites¶
| Requirement | Details |
|---|---|
| Python | 3.8+ |
| Libraries | opencv-python, numpy, matplotlib |
| Prior knowledge | Basic Python, NumPy arrays, image loading with OpenCV |
Install dependencies if needed:
1. What Are Edges?¶
An edge is a location in an image where the intensity changes rapidly. Mathematically, edges correspond to local maxima in the first derivative (gradient) of the image intensity function.
1.1 Intensity Discontinuities¶
Consider a 1-D intensity profile \(I(x)\) along a single row of an image. The derivative \(\frac{dI}{dx}\) tells us how fast the intensity is changing:
A strong edge corresponds to a large value of \(|dI/dx|\).
1.2 Types of Edges¶
| Type | Description | Intensity Profile | Derivative |
|---|---|---|---|
| Step edge | Abrupt intensity change (e.g., object boundary) | Sharp jump | Single spike |
| Ramp edge | Gradual intensity change (e.g., blurred boundary) | Sloped transition | Broad peak |
| Ridge / Roof edge | Thin bright or dark line | Narrow peak or valley | Two spikes (positive + negative) |
In 2-D images, edges are characterized by both magnitude and direction of the gradient:
where \(\frac{\partial I}{\partial x}\) and \(\frac{\partial I}{\partial y}\) are the horizontal and vertical gradients.
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Load image in grayscale
img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)
# Show a horizontal intensity profile
row = img.shape[0] // 2 # middle row
profile = img[row, :].astype(float)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].imshow(img, cmap="gray")
axes[0].axhline(row, color="red", linewidth=1)
axes[0].set_title("Grayscale Image")
axes[0].axis("off")
axes[1].plot(profile)
axes[1].set_xlabel("Column (x)")
axes[1].set_ylabel("Intensity")
axes[1].set_title(f"Intensity Profile at Row {row}")
plt.tight_layout()
plt.show()
2. Gradient-Based Methods¶
Gradient-based methods compute the first or second derivative of the image to locate edges. They use small convolution kernels (filters) applied to every pixel.
2.1 Sobel Operator¶
The Sobel operator estimates the horizontal and vertical gradients using \(3 \times 3\) kernels:
The gradient magnitude and direction are:
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)
# Compute Sobel gradients in x and y directions
# cv2.Sobel(src, ddepth, dx, dy, ksize)
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) # horizontal edges
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) # vertical edges
# Gradient magnitude
magnitude = np.sqrt(sobel_x**2 + sobel_y**2)
magnitude = np.uint8(np.clip(magnitude, 0, 255))
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].imshow(np.abs(sobel_x), cmap="gray")
axes[0].set_title("Sobel X (Vertical Edges)")
axes[0].axis("off")
axes[1].imshow(np.abs(sobel_y), cmap="gray")
axes[1].set_title("Sobel Y (Horizontal Edges)")
axes[1].axis("off")
axes[2].imshow(magnitude, cmap="gray")
axes[2].set_title("Gradient Magnitude")
axes[2].axis("off")
plt.tight_layout()
plt.show()
Why cv2.CV_64F?
Using 64-bit float output prevents clipping of negative gradient values. If you use cv2.CV_8U, negative values are clipped to 0 and you lose half the gradient information.
2.2 Scharr Operator¶
The Sobel operator with \(ksize=3\) can be inaccurate for small kernels. The Scharr operator provides a more accurate 3×3 gradient estimate with larger weights:
# Scharr operator — more accurate than Sobel with ksize=3
scharr_x = cv2.Scharr(img, cv2.CV_64F, 1, 0)
scharr_y = cv2.Scharr(img, cv2.CV_64F, 0, 1)
magnitude_scharr = np.sqrt(scharr_x**2 + scharr_y**2)
magnitude_scharr = np.uint8(np.clip(magnitude_scharr, 0, 255))
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(magnitude, cmap="gray")
axes[0].set_title("Sobel (ksize=3)")
axes[0].axis("off")
axes[1].imshow(magnitude_scharr, cmap="gray")
axes[1].set_title("Scharr")
axes[1].axis("off")
plt.tight_layout()
plt.show()
2.3 Laplacian of Gaussian (LoG)¶
The Laplacian is a second-order derivative operator that detects edges as zero-crossings of the second derivative:
Because the Laplacian is very sensitive to noise, we first smooth the image with a Gaussian filter, yielding the Laplacian of Gaussian (LoG):
where \(G_\sigma\) is a 2-D Gaussian with standard deviation \(\sigma\). A common approximation is the \(5 \times 5\) kernel:
# Laplacian of Gaussian
# First apply Gaussian blur, then compute Laplacian
blurred = cv2.GaussianBlur(img, (5, 5), 1.4)
log = cv2.Laplacian(blurred, cv2.CV_64F, ksize=3)
# Display absolute values (edge strength)
log_abs = np.uint8(np.clip(np.abs(log), 0, 255))
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(img, cmap="gray")
axes[0].set_title("Original")
axes[0].axis("off")
axes[1].imshow(log_abs, cmap="gray")
axes[1].set_title("Laplacian of Gaussian")
axes[1].axis("off")
plt.tight_layout()
plt.show()
Method Comparison¶
| Method | Derivative Order | Noise Sensitivity | Edge Localization | Speed |
|---|---|---|---|---|
| Sobel | 1st | Medium | Good | Fast |
| Scharr | 1st | Medium-Low | Better than Sobel | Fast |
| Laplacian | 2nd | High (needs smoothing) | Good (zero-crossing) | Fast |
| LoG (Marr-Hildreth) | 2nd | Low (Gaussian built-in) | Excellent | Medium |
3. Canny Edge Detection¶
The Canny edge detector (1986) is the most widely used edge detection algorithm in computer vision and robotics. It was designed to satisfy three criteria: good detection (low error rate), good localization (edges close to true position), and minimal response (one detection per edge).
3.1 Algorithm Steps¶
The Canny algorithm consists of four main steps:
Step 1: Gaussian Smoothing
Remove noise with a Gaussian filter to prevent false edge detections:
Step 2: Gradient Computation
Compute gradient magnitude and direction (typically using Sobel):
Step 3: Non-Maximum Suppression (NMS)
Thin the edges to single-pixel width. For each pixel, check if its gradient magnitude is a local maximum along the gradient direction:
For each pixel (i, j):
1. Quantize θ to one of four directions: 0°, 45°, 90°, 135°
2. Compare G(i,j) with its two neighbors along that direction
3. If G(i,j) is NOT the maximum → suppress (set to 0)
Step 4: Hysteresis Thresholding
Use two thresholds to classify edges:
- \(T_{\text{high}}\): pixels above this are strong edges (definitely keep)
- \(T_{\text{low}}\): pixels below this are non-edges (discard)
- Pixels between \(T_{\text{low}}\) and \(T_{\text{high}}\) are weak edges — kept only if connected to a strong edge
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)
# Canny edge detection
# cv2.Canny(image, threshold1, threshold2)
# threshold1 = T_low, threshold2 = T_high
edges = cv2.Canny(img, threshold1=50, threshold2=150)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].imshow(img, cmap="gray")
axes[0].set_title("Original")
axes[0].axis("off")
axes[1].imshow(edges, cmap="gray")
axes[1].set_title("Canny Edges (50, 150)")
axes[1].axis("off")
plt.tight_layout()
plt.show()
3.2 Parameter Tuning¶
Choosing the right thresholds is critical. A trackbar demo lets you interactively explore the effect of parameters:
import cv2
import numpy as np
def nothing(x):
pass
img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)
cv2.namedWindow("Canny")
cv2.createTrackbar("T_low", "Canny", 50, 255, nothing)
cv2.createTrackbar("T_high", "Canny", 150, 255, nothing)
cv2.createTrackbar("Blur", "Canny", 1, 20, nothing)
while True:
t_low = cv2.getTrackbarPos("T_low", "Canny")
t_high = cv2.getTrackbarPos("T_high", "Canny")
k = cv2.getTrackbarPos("Blur", "Canny")
k = max(1, k * 2 + 1) # ensure odd kernel size
blurred = cv2.GaussianBlur(img, (k, k), 0)
edges = cv2.Canny(blurred, t_low, t_high)
cv2.imshow("Canny", edges)
if cv2.waitKey(30) & 0xFF == 27: # ESC to exit
break
cv2.destroyAllWindows()
Guidelines for threshold selection:
| Scenario | \(T_{\text{low}}\) | \(T_{\text{high}}\) | Notes |
|---|---|---|---|
| Clean indoor scene | 50 | 150 | Default starting point |
| Noisy image (outdoor) | 80 | 200 | Higher thresholds to suppress noise |
| Fine details needed | 20 | 80 | Lower thresholds capture weak edges |
| Ratio rule of thumb | 1 : 2 or 1 : 3 | \(T_{\text{high}} = 2\text{–}3 \times T_{\text{low}}\) |
Automatic Thresholds
Use cv2.threshold with Otsu's method or compute the median: set \(T_{\text{high}} = \text{median} \times 1.33\) and \(T_{\text{low}} = \text{median} \times 0.66\).
# Automatic Canny using median
def auto_canny(image, sigma=0.33):
"""Compute Canny edges with automatic thresholds based on median."""
v = np.median(image)
lower = int(max(0, (1.0 - sigma) * v))
upper = int(min(255, (1.0 + sigma) * v))
return cv2.Canny(image, lower, upper)
edges_auto = auto_canny(img)
4. Hough Transform¶
After detecting edge pixels, we often want to extract geometric structures (lines, circles) from the scattered edge points. The Hough Transform maps edge points from image space to a parameter space where geometric shapes become peaks.
4.1 Line Detection¶
A line in image space can be parameterized in polar coordinates as:
where \(\rho\) is the perpendicular distance from the origin and \(\theta\) is the angle of the normal. Each edge point \((x, y)\) maps to a sinusoidal curve in \((\rho, \theta)\) space. Lines are found where many curves intersect (vote accumulation).
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("robot_workspace.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Step 1: Edge detection
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
# Step 2: Hough Line Transform (standard)
# cv2.HoughLines(image, rho, theta, threshold)
# rho — distance resolution in pixels (1 = 1 pixel)
# theta — angle resolution in radians (np.pi/180 = 1 degree)
# threshold — minimum number of votes
lines = cv2.HoughLines(edges, rho=1, theta=np.pi / 180, threshold=150)
# Draw detected lines
img_lines = img.copy()
if lines is not None:
for line in lines:
rho, theta = line[0]
a, b = np.cos(theta), np.sin(theta)
x0, y0 = a * rho, b * rho
# Extend line to image boundaries
x1 = int(x0 + 1000 * (-b))
y1 = int(y0 + 1000 * (a))
x2 = int(x0 - 1000 * (-b))
y2 = int(y0 - 1000 * (a))
cv2.line(img_lines, (x1, y1), (x2, y2), (0, 0, 255), 2)
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
axes[0].imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
axes[0].set_title("Original")
axes[0].axis("off")
axes[1].imshow(edges, cmap="gray")
axes[1].set_title("Canny Edges")
axes[1].axis("off")
axes[2].imshow(cv2.cvtColor(img_lines, cv2.COLOR_BGR2RGB))
axes[2].set_title(f"Hough Lines ({len(lines) if lines is not None else 0} found)")
axes[2].axis("off")
plt.tight_layout()
plt.show()
Probabilistic Hough Transform (HoughLinesP)¶
The standard Hough Transform can be slow and does not return line segment endpoints. HoughLinesP returns line segments directly:
# Probabilistic Hough Transform — returns line segments
# cv2.HoughLinesP(image, rho, theta, threshold,
# minLineLength, maxLineGap)
lines_p = cv2.HoughLinesP(
edges,
rho=1,
theta=np.pi / 180,
threshold=80,
minLineLength=50, # minimum segment length
maxLineGap=10 # max gap to merge segments
)
img_segments = img.copy()
if lines_p is not None:
for line in lines_p:
x1, y1, x2, y2 = line[0]
cv2.line(img_segments, (x1, y1), (x2, y2), (0, 255, 0), 2)
plt.figure(figsize=(8, 6))
plt.imshow(cv2.cvtColor(img_segments, cv2.COLOR_BGR2RGB))
plt.title(f"Hough Line Segments ({len(lines_p) if lines_p is not None else 0} found)")
plt.axis("off")
plt.show()
| Method | Returns | Speed | Use Case |
|---|---|---|---|
HoughLines |
\((\rho, \theta)\) infinite lines | Slower | When full lines are needed |
HoughLinesP |
\((x_1, y_1, x_2, y_2)\) segments | Faster | Practical robotics tasks |
4.2 Circle Detection¶
Circles are detected using a 3-parameter Hough Transform:
where \((a, b)\) is the center and \(r\) is the radius. cv2.HoughCircles uses the Hough Gradient Method which is faster than the standard approach.
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("coins.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray_blur = cv2.medianBlur(gray, 5)
# Hough Circle Transform
# cv2.HoughCircles(image, method, dp, minDist,
# param1, param2, minRadius, maxRadius)
circles = cv2.HoughCircles(
gray_blur,
cv2.HOUGH_GRADIENT,
dp=1, # accumulator resolution ratio
minDist=50, # min distance between centers
param1=100, # upper Canny threshold
param2=50, # accumulator threshold (lower = more circles)
minRadius=20,
maxRadius=100
)
img_circles = img.copy()
if circles is not None:
circles = np.uint16(np.around(circles))
for c in circles[0, :]:
cv2.circle(img_circles, (c[0], c[1]), c[2], (0, 255, 0), 2)
cv2.circle(img_circles, (c[0], c[1]), 2, (0, 0, 255), 3)
print(f"Found {len(circles[0])} circles")
plt.figure(figsize=(8, 6))
plt.imshow(cv2.cvtColor(img_circles, cv2.COLOR_BGR2RGB))
plt.title("Hough Circles")
plt.axis("off")
plt.show()
Tuning Tips
minDist: Set slightly smaller than the expected minimum distance between circle centers.param2: Lower values detect more circles (including false positives); raise it to be more strict.minRadius/maxRadius: Narrow the range to speed up and reduce false detections.
5. Contour Detection & Analysis¶
Contours are curves joining continuous points along a boundary with the same color or intensity. Contours are a higher-level representation than raw edges and are extremely useful for object detection and shape analysis.
5.1 Finding Contours¶
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Load and threshold
img = cv2.imread("robot_workspace.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Binary threshold (or use Canny edges)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# Find contours
# cv2.findContours(image, mode, method)
# mode: cv2.RETR_EXTERNAL — only outer contours
# cv2.RETR_TREE — all contours with full hierarchy
# method: cv2.CHAIN_APPROX_SIMPLE — compress segments (only endpoints)
contours, hierarchy = cv2.findContours(
binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)
# Draw contours
img_contours = img.copy()
cv2.drawContours(img_contours, contours, -1, (0, 255, 0), 2)
print(f"Found {len(contours)} contours")
plt.figure(figsize=(8, 6))
plt.imshow(cv2.cvtColor(img_contours, cv2.COLOR_BGR2RGB))
plt.title(f"Contours ({len(contours)} found)")
plt.axis("off")
plt.show()
5.2 Contour Features¶
Once contours are found, we can extract rich geometric features:
for i, cnt in enumerate(contours):
# Area (in pixels)
area = cv2.contourArea(cnt)
# Perimeter (arc length)
# True = closed contour
perimeter = cv2.arcLength(cnt, closed=True)
# Moments (for centroid computation)
M = cv2.moments(cnt)
if M["m00"] != 0:
cx = int(M["m10"] / M["m00"]) # centroid x
cy = int(M["m01"] / M["m00"]) # centroid y
else:
cx, cy = 0, 0
# Axis-aligned bounding rectangle
x, y, w, h = cv2.boundingRect(cnt)
# Minimum area bounding rectangle (rotated)
rect = cv2.minAreaRect(cnt)
# rect = ((cx, cy), (width, height), angle)
box = cv2.boxPoints(rect)
box = np.int32(box)
# Minimum enclosing circle
(circle_cx, circle_cy), radius = cv2.minEnclosingCircle(cnt)
# Aspect ratio
aspect_ratio = w / h if h > 0 else 0
# Extent: ratio of contour area to bounding rect area
extent = area / (w * h) if (w * h) > 0 else 0
# Solidity: ratio of contour area to convex hull area
hull = cv2.convexHull(cnt)
hull_area = cv2.contourArea(hull)
solidity = area / hull_area if hull_area > 0 else 0
print(f"Contour {i}: area={area:.0f}, perimeter={perimeter:.1f}, "
f"centroid=({cx},{cy}), aspect={aspect_ratio:.2f}, "
f"extent={extent:.2f}, solidity={solidity:.2f}")
Feature Summary Table:
| Feature | Function | Description |
|---|---|---|
| Area | cv2.contourArea(cnt) |
Number of pixels inside contour |
| Perimeter | cv2.arcLength(cnt, True) |
Total contour length |
| Moments | cv2.moments(cnt) |
Statistical shape descriptors |
| Bounding Rect | cv2.boundingRect(cnt) |
Axis-aligned rectangle \((x, y, w, h)\) |
| Min Area Rect | cv2.minAreaRect(cnt) |
Rotated minimum rectangle |
| Min Enclosing Circle | cv2.minEnclosingCircle(cnt) |
Smallest enclosing circle |
| Aspect Ratio | \(w / h\) | Width-to-height ratio of bounding rect |
| Extent | \(\text{area} / (w \times h)\) | Fill ratio of bounding rect |
| Solidity | \(\text{area} / \text{hull\_area}\) | Fill ratio of convex hull |
5.3 Approximation & Convex Hull¶
Polygon approximation simplifies a contour to a polygon with fewer vertices:
# Approximate contour as polygon
# cv2.approxPolyDP(curve, epsilon, closed)
# epsilon = max distance from contour to approximated polygon
epsilon_fraction = 0.02 # 2% of perimeter
for cnt in contours:
peri = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon_fraction * peri, True)
# Number of vertices indicates shape type
n_vertices = len(approx)
if n_vertices == 3:
shape = "triangle"
elif n_vertices == 4:
shape = "rectangle/square"
elif n_vertices > 6:
shape = "circle"
else:
shape = f"{n_vertices}-gon"
print(f"Shape: {shape} ({n_vertices} vertices)")
Convex hull is the smallest convex polygon enclosing the contour:
# Convex hull and convexity defects
hull = cv2.convexHull(cnt, returnPoints=True)
# Convexity defects (useful for hand/grip detection)
hull_indices = cv2.convexHull(cnt, returnPoints=False)
defects = cv2.convexityDefects(cnt, hull_indices)
if defects is not None:
for d in defects[:, 0]:
start, end, far, depth = d
# depth / 256 is the distance from the farthest point to the hull
if depth / 256 > 10: # filter small defects
far_point = tuple(cnt[far][0])
cv2.circle(img, far_point, 5, (0, 0, 255), -1)
Shape Detection
Combining approxPolyDP with convexity defects enables shape recognition for robotic grasping — e.g., identifying whether an object is a box (4 vertices, high solidity) or an irregular tool (many vertices, low solidity).
6. Applications in Robotics¶
6.1 Lane Detection¶
A classic robotics task — detect lane markings using edges and lines:
import cv2
import numpy as np
def detect_lanes(image):
"""Detect lane lines in a road image."""
h, w = image.shape[:2]
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Define region of interest (trapezoid in front of vehicle)
roi_vertices = np.array([[
(0, h), (w // 2 - 50, h // 2 + 50),
(w // 2 + 50, h // 2 + 50), (w, h)
]], dtype=np.int32)
mask = np.zeros_like(gray)
cv2.fillPoly(mask, roi_vertices, 255)
masked = cv2.bitwise_and(gray, mask)
# Edge detection + Hough Transform
edges = cv2.Canny(masked, 50, 150)
lines = cv2.HoughLinesP(
edges, 1, np.pi / 180, threshold=50,
minLineLength=100, maxLineGap=50
)
# Draw lines on original image
result = image.copy()
if lines is not None:
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(result, (x1, y1), (x2, y2), (0, 255, 0), 3)
return result
# Usage
# frame = cv2.imread("road.jpg")
# lanes = detect_lanes(frame)
# cv2.imshow("Lanes", lanes)
# cv2.waitKey(0)
6.2 Object Boundary Extraction¶
Use edges and contours to isolate objects on a conveyor belt or workspace:
def extract_object_boundary(image):
"""Find the largest contour (the object) in the image."""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, 50, 150)
# Dilate edges to close gaps
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
edges_closed = cv2.dilate(edges, kernel, iterations=2)
edges_closed = cv2.erode(edges_closed, kernel, iterations=1)
contours, _ = cv2.findContours(
edges_closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
if not contours:
return None, image
# Find largest contour
largest = max(contours, key=cv2.contourArea)
result = image.copy()
cv2.drawContours(result, [largest], -1, (0, 255, 0), 2)
# Bounding rectangle for ROI
x, y, w, h = cv2.boundingRect(largest)
cv2.rectangle(result, (x, y), (x + w, y + h), (255, 0, 0), 2)
return largest, result
6.3 Pick-and-Place ROI Detection¶
Detect objects and compute grasp points using contour analysis:
def find_grasp_points(image, min_area=500):
"""Find graspable objects and compute pick points."""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
objects = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < min_area:
continue
M = cv2.moments(cnt)
if M["m00"] == 0:
continue
cx = int(M["m10"] / M["m00"])
cy = int(M["m01"] / M["m00"])
# Minimum area rectangle for oriented bounding box
rect = cv2.minAreaRect(cnt)
angle = rect[2]
objects.append({
"center": (cx, cy),
"area": area,
"angle": angle,
"contour": cnt,
"rect": rect
})
return objects
# Usage:
# objects = find_grasp_points(image)
# for obj in objects:
# print(f"Pick at {obj['center']}, angle={obj['angle']:.1f}°")
7. Exercises¶
Exercise 1: Sobel vs. Canny Comparison¶
Load any image and display Sobel magnitude, LoG result, and Canny edges side by side. Experiment with different Sobel kernel sizes (3, 5, 7) and Canny thresholds.
Tasks:
- Apply Sobel with
ksize= 3, 5, 7 and compare edge thickness - Apply LoG with different Gaussian \(\sigma\) values (0.5, 1.0, 2.0)
- Apply Canny with threshold pairs: (20, 60), (50, 150), (100, 200)
- Create a 3×3 subplot grid comparing all results
Exercise 2: Canny Trackbar Explorer¶
Build an interactive Canny parameter explorer using OpenCV trackbars. Add a trackbar for Gaussian blur kernel size.
Tasks:
- Create trackbars for
T_low,T_high, and blurksize - Display the original image and Canny edges side by side
- Add an option to use the automatic Canny method
- Save the best parameter set to a text file when 's' is pressed
Exercise 3: Hough Transform Lane Finder¶
Write a complete lane detection pipeline for a road image.
Tasks:
- Apply a trapezoidal ROI mask to focus on the road
- Use Canny +
HoughLinesPto detect lane lines - Filter lines by slope: left lanes have negative slope, right lanes positive
- Draw detected lanes on the original image in different colors (left=blue, right=red)
- Display intermediate results (edges, masked edges, raw lines, filtered lines)
Exercise 4: Object Shape Classifier¶
Write a function that classifies objects in an image by shape using contour features.
Tasks:
- Preprocess: grayscale → blur → threshold → find contours
- For each contour: compute
approxPolyDPwith \(\epsilon = 0.02 \times \text{perimeter}\) - Classify based on number of vertices: triangle (3), square/rectangle (4), pentagon (5), circle (> 6)
- Draw each contour with the shape name as label
- Compute and print a table: shape name, area, perimeter, solidity, aspect ratio
Method Comparison Summary¶
| Method | What It Detects | Output | Robustness | Speed | Best For |
|---|---|---|---|---|---|
| Sobel | Gradient edges | Edge magnitude image | Low (noise) | ★★★★★ | Quick gradient visualization |
| Scharr | Gradient edges | Edge magnitude image | Medium | ★★★★★ | Accurate 3×3 gradients |
| LoG | Zero-crossing edges | Edge image | Medium | ★★★★ | Blob and edge detection |
| Canny | Clean edge map | Binary edge image | High | ★★★★ | General-purpose edges |
| HoughLines | Straight lines | \((\rho, \theta)\) | Medium | ★★★ | Lane detection, structure |
| HoughCircles | Circles | \((a, b, r)\) | Medium | ★★★ | Ball/circle detection |
| Contours | Closed boundaries | Point sequences | High | ★★★★★ | Object analysis, grasping |
References¶
- Canny, J., "A Computational Approach to Edge Detection," IEEE TPAMI, 1986 — Original Canny edge detector paper
- OpenCV Edge Detection Tutorial — Official Sobel, Scharr, Laplacian documentation
- OpenCV Canny Edge Detection — Canny tutorial with examples
- OpenCV Hough Transform — Line and circle detection
- OpenCV Contours Tutorial — Contour basics, features, and properties
- Szeliski, R., "Computer Vision: Algorithms and Applications," 2nd ed., Springer, 2022 — Comprehensive reference for edge detection theory
- Gonzalez & Woods, "Digital Image Processing," 4th ed., Pearson, 2018 — Classic textbook with detailed edge detection derivations