Skip to content

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:

pip install opencv-python numpy matplotlib

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:

\[ \text{Edge strength} = \left| \frac{dI}{dx} \right| \]

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:

\[ |\nabla I| = \sqrt{ \left(\frac{\partial I}{\partial x}\right)^2 + \left(\frac{\partial I}{\partial y}\right)^2 } \]
\[ \theta = \arctan\!\left(\frac{\partial I / \partial y}{\partial I / \partial x}\right) \]

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:

\[ G_x = \begin{bmatrix} -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1 \end{bmatrix} * I, \qquad G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ +1 & +2 & +1 \end{bmatrix} * I \]

The gradient magnitude and direction are:

\[ G = \sqrt{G_x^2 + G_y^2}, \qquad \theta = \arctan\!\left(\frac{G_y}{G_x}\right) \]
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:

\[ G_x = \begin{bmatrix} -3 & 0 & +3 \\ -10 & 0 & +10 \\ -3 & 0 & +3 \end{bmatrix} * I, \qquad G_y = \begin{bmatrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ +3 & +10 & +3 \end{bmatrix} * I \]
# 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:

\[ \nabla^2 I = \frac{\partial^2 I}{\partial x^2} + \frac{\partial^2 I}{\partial y^2} \]

Because the Laplacian is very sensitive to noise, we first smooth the image with a Gaussian filter, yielding the Laplacian of Gaussian (LoG):

\[ \text{LoG}(x, y) = \nabla^2 \left[ G_\sigma(x, y) * I(x, y) \right] \]

where \(G_\sigma\) is a 2-D Gaussian with standard deviation \(\sigma\). A common approximation is the \(5 \times 5\) kernel:

\[ \text{LoG}_{5\times5} = \begin{bmatrix} 0 & 0 & -1 & 0 & 0 \\ 0 & -1 & -2 & -1 & 0 \\ -1 & -2 & 16 & -2 & -1 \\ 0 & -1 & -2 & -1 & 0 \\ 0 & 0 & -1 & 0 & 0 \end{bmatrix} \]
# 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:

\[ I_{\text{smooth}} = G_\sigma * I \]

Step 2: Gradient Computation

Compute gradient magnitude and direction (typically using Sobel):

\[ G = \sqrt{G_x^2 + G_y^2}, \qquad \theta = \arctan(G_y / G_x) \]

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:

\[ \rho = x \cos\theta + y \sin\theta \]

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:

\[ (x - a)^2 + (y - b)^2 = r^2 \]

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:

  1. Apply Sobel with ksize = 3, 5, 7 and compare edge thickness
  2. Apply LoG with different Gaussian \(\sigma\) values (0.5, 1.0, 2.0)
  3. Apply Canny with threshold pairs: (20, 60), (50, 150), (100, 200)
  4. 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:

  1. Create trackbars for T_low, T_high, and blur ksize
  2. Display the original image and Canny edges side by side
  3. Add an option to use the automatic Canny method
  4. 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:

  1. Apply a trapezoidal ROI mask to focus on the road
  2. Use Canny + HoughLinesP to detect lane lines
  3. Filter lines by slope: left lanes have negative slope, right lanes positive
  4. Draw detected lanes on the original image in different colors (left=blue, right=red)
  5. 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:

  1. Preprocess: grayscale → blur → threshold → find contours
  2. For each contour: compute approxPolyDP with \(\epsilon = 0.02 \times \text{perimeter}\)
  3. Classify based on number of vertices: triangle (3), square/rectangle (4), pentagon (5), circle (> 6)
  4. Draw each contour with the shape name as label
  5. 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