跳转至

边缘检测(Edge Detection)

边缘是机器人视觉中最基本的特征之一。边缘标记了图像中两个区域之间的边界,此处强度发生急剧变化——对应于物体边界、表面标记、阴影或纹理过渡。边缘检测可以大幅减少机器人需要处理的数据量,同时保留车道跟随、物体识别、抓取和导航等任务所需的结构信息。

本教程涵盖边缘检测的理论与实践,包括从简单的梯度算子到广泛使用的 Canny 检测器、用于几何形状提取的霍夫变换(Hough Transform),以及用于下游机器人应用的轮廓分析。


学习目标

完成本教程后,你将能够:

  • 解释什么是边缘以及它在机器人感知中的重要性
  • 使用 Sobel、Scharr 和高斯拉普拉斯算子计算图像梯度
  • 应用 Canny 边缘检测器并针对不同场景调整参数
  • 使用霍夫变换检测直线和圆
  • 查找、分析和逼近轮廓以进行物体识别
  • 将边缘和轮廓技术应用于实际机器人任务(车道检测、抓取 ROI)

前置要求

要求 详情
Python 3.8+
opencv-pythonnumpymatplotlib
先修知识 基础 Python、NumPy 数组、使用 OpenCV 加载图像

安装依赖(如需要):

pip install opencv-python numpy matplotlib

1. 什么是边缘?

**边缘**是图像中强度快速变化的位置。从数学上看,边缘对应于图像强度函数一阶导数(梯度)的局部极大值。

1.1 强度不连续性

考虑图像中单行的一维强度分布 \(I(x)\)。导数 \(\frac{dI}{dx}\) 告诉我们强度变化的速度:

\[ \text{边缘强度} = \left| \frac{dI}{dx} \right| \]

强边缘对应于 \(|dI/dx|\) 的大值。

1.2 边缘类型

类型 描述 强度分布 导数
阶跃边缘 突然的强度变化(如物体边界) 急剧跳变 单个尖峰
斜坡边缘 渐进的强度变化(如模糊边界) 倾斜过渡 宽峰
脊/屋顶边缘 亮或暗的细线 窄峰或窄谷 双尖峰(正+负)

在二维图像中,边缘由梯度的**幅值**和**方向**共同描述:

\[ |\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) \]

其中 \(\frac{\partial I}{\partial x}\)\(\frac{\partial I}{\partial y}\) 分别是水平和垂直方向的梯度。

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 以灰度模式加载图像
img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)

# 显示水平强度剖面
row = img.shape[0] // 2  # 中间行
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("灰度图像")
axes[0].axis("off")

axes[1].plot(profile)
axes[1].set_xlabel("列 (x)")
axes[1].set_ylabel("强度")
axes[1].set_title(f"第 {row} 行的强度剖面")
plt.tight_layout()
plt.show()

2. 基于梯度的方法

基于梯度的方法通过计算图像的一阶或二阶导数来定位边缘。它们使用应用于每个像素的小卷积核(滤波器)。

2.1 Sobel 算子

**Sobel 算子**使用 \(3 \times 3\) 核来估计水平和垂直梯度:

\[ 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 \]

梯度幅值和方向为:

\[ 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)

# 计算 x 和 y 方向的 Sobel 梯度
# cv2.Sobel(src, ddepth, dx, dy, ksize)
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)  # 水平边缘
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)  # 垂直边缘

# 梯度幅值
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(垂直边缘)")
axes[0].axis("off")

axes[1].imshow(np.abs(sobel_y), cmap="gray")
axes[1].set_title("Sobel Y(水平边缘)")
axes[1].axis("off")

axes[2].imshow(magnitude, cmap="gray")
axes[2].set_title("梯度幅值")
axes[2].axis("off")

plt.tight_layout()
plt.show()

为什么要用 cv2.CV_64F

使用 64 位浮点输出可以防止负梯度值被截断。如果使用 cv2.CV_8U,负值会被截断为 0,从而丢失一半的梯度信息。

2.2 Scharr 算子

\(ksize=3\) 的 Sobel 算子在小核时可能不够精确。**Scharr 算子**提供了更精确的 \(3 \times 3\) 梯度估计,使用更大的权重:

\[ 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 算子 —— 比 ksize=3 的 Sobel 更精确
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 高斯拉普拉斯(LoG)

**拉普拉斯算子**是二阶导数算子,通过检测二阶导数的零交叉点来定位边缘:

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

由于拉普拉斯算子对噪声非常敏感,我们首先用高斯滤波器对图像进行平滑,得到**高斯拉普拉斯(LoG)**:

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

其中 \(G_\sigma\) 是标准差为 \(\sigma\) 的二维高斯函数。常用的 \(5 \times 5\) 近似核为:

\[ \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} \]
# 高斯拉普拉斯
# 先进行高斯模糊,再计算拉普拉斯
blurred = cv2.GaussianBlur(img, (5, 5), 1.4)
log = cv2.Laplacian(blurred, cv2.CV_64F, ksize=3)

# 显示绝对值(边缘强度)
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("原始图像")
axes[0].axis("off")

axes[1].imshow(log_abs, cmap="gray")
axes[1].set_title("高斯拉普拉斯")
axes[1].axis("off")

plt.tight_layout()
plt.show()

方法对比

方法 导数阶数 噪声敏感度 边缘定位 速度
Sobel 一阶 中等
Scharr 一阶 中低 优于 Sobel
Laplacian 二阶 高(需平滑) 好(零交叉)
LoG (Marr-Hildreth) 二阶 低(内置高斯) 优秀 中等

3. Canny 边缘检测

Canny 边缘检测器(1986 年)是计算机视觉和机器人领域中使用最广泛的边缘检测算法。它被设计为满足三个准则:良好的检测性(低错误率)、良好的定位性(边缘接近真实位置)和**最小响应**(每条边缘只产生一次检测)。

3.1 算法步骤

Canny 算法包含四个主要步骤:

步骤 1:高斯平滑

用高斯滤波器去除噪声,防止产生虚假边缘检测:

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

步骤 2:梯度计算

计算梯度幅值和方向(通常使用 Sobel):

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

步骤 3:非极大值抑制(NMS)

将边缘细化到单像素宽度。对于每个像素,检查其梯度幅值是否为梯度方向上的局部最大值:

对于每个像素 (i, j):
    1. 将 θ 量化为四个方向之一:0°、45°、90°、135°
    2. 沿该方向比较 G(i,j) 与其两个邻域像素
    3. 如果 G(i,j) 不是最大值 → 抑制(设为 0)

步骤 4:双阈值滞后处理

使用两个阈值来分类边缘:

  • \(T_{\text{high}}\):高于此值的像素为**强**边缘(一定保留)
  • \(T_{\text{low}}\):低于此值的像素为**非边缘**(丢弃)
  • 介于 \(T_{\text{low}}\)\(T_{\text{high}}\) 之间的像素为**弱**边缘——仅在与强边缘相连时才保留
import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)

# Canny 边缘检测
# 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("原始图像")
axes[0].axis("off")

axes[1].imshow(edges, cmap="gray")
axes[1].set_title("Canny 边缘 (50, 150)")
axes[1].axis("off")

plt.tight_layout()
plt.show()

3.2 参数调优

选择合适的阈值至关重要。**滑块演示**可以让你交互式地探索参数的影响:

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)  # 确保核大小为奇数

    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 退出
        break

cv2.destroyAllWindows()

阈值选择指南:

场景 \(T_{\text{low}}\) \(T_{\text{high}}\) 备注
干净的室内场景 50 150 默认起始值
噪声图像(室外) 80 200 较高阈值以抑制噪声
需要精细细节 20 80 较低阈值捕获弱边缘
比例经验法则 1 : 2 或 1 : 3 \(T_{\text{high}} = 2\text{–}3 \times T_{\text{low}}\)

自动阈值

使用 cv2.threshold 的 Otsu 方法或计算中位数:设置 \(T_{\text{high}} = \text{median} \times 1.33\)\(T_{\text{low}} = \text{median} \times 0.66\)

# 基于中位数的自动 Canny
def auto_canny(image, sigma=0.33):
    """基于中位数自动计算 Canny 阈值。"""
    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)

在检测到边缘像素后,我们通常希望从分散的边缘点中提取**几何结构**(直线、圆)。**霍夫变换**将边缘点从图像空间映射到参数空间,其中几何形状变为峰值。

4.1 直线检测

图像空间中的直线可以用**极坐标**参数化为:

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

其中 \(\rho\) 是从原点到直线的垂直距离,\(\theta\) 是法线的角度。每个边缘点 \((x, y)\)\((\rho, \theta)\) 空间中映射为一条正弦曲线。直线在多条曲线**相交**(投票累积)处被找到。

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 步骤 1:边缘检测
edges = cv2.Canny(gray, 50, 150, apertureSize=3)

# 步骤 2:霍夫直线变换(标准)
# cv2.HoughLines(image, rho, theta, threshold)
#   rho   — 距离分辨率(像素),1 = 1 像素
#   theta — 角度分辨率(弧度),np.pi/180 = 1 度
#   threshold — 最小投票数
lines = cv2.HoughLines(edges, rho=1, theta=np.pi / 180, threshold=150)

# 绘制检测到的直线
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
        # 延伸直线到图像边界
        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("原始图像")
axes[0].axis("off")

axes[1].imshow(edges, cmap="gray")
axes[1].set_title("Canny 边缘")
axes[1].axis("off")

axes[2].imshow(cv2.cvtColor(img_lines, cv2.COLOR_BGR2RGB))
axes[2].set_title(f"霍夫直线(找到 {len(lines) if lines is not None else 0} 条)")
axes[2].axis("off")

plt.tight_layout()
plt.show()

概率霍夫变换(HoughLinesP

标准霍夫变换速度较慢,且不返回线段端点。HoughLinesP 直接返回线段:

# 概率霍夫变换 —— 返回线段
# cv2.HoughLinesP(image, rho, theta, threshold,
#                 minLineLength, maxLineGap)
lines_p = cv2.HoughLinesP(
    edges,
    rho=1,
    theta=np.pi / 180,
    threshold=80,
    minLineLength=50,   # 最小线段长度
    maxLineGap=10        # 最大间隙以合并线段
)

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"霍夫线段(找到 {len(lines_p) if lines_p is not None else 0} 条)")
plt.axis("off")
plt.show()
方法 返回值 速度 使用场景
HoughLines \((\rho, \theta)\) 无限直线 较慢 需要完整直线时
HoughLinesP \((x_1, y_1, x_2, y_2)\) 线段 较快 实际机器人任务

4.2 圆检测

圆使用三参数霍夫变换进行检测:

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

其中 \((a, b)\) 是圆心,\(r\) 是半径。cv2.HoughCircles 使用**霍夫梯度法**,比标准方法更快。

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)

# 霍夫圆变换
# cv2.HoughCircles(image, method, dp, minDist,
#                  param1, param2, minRadius, maxRadius)
circles = cv2.HoughCircles(
    gray_blur,
    cv2.HOUGH_GRADIENT,
    dp=1,              # 累加器分辨率比
    minDist=50,        # 圆心之间的最小距离
    param1=100,        # Canny 上阈值
    param2=50,         # 累加器阈值(越低检测到越多圆)
    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"找到 {len(circles[0])} 个圆")

plt.figure(figsize=(8, 6))
plt.imshow(cv2.cvtColor(img_circles, cv2.COLOR_BGR2RGB))
plt.title("霍夫圆")
plt.axis("off")
plt.show()

调优技巧

  • minDist:设置为略小于预期的圆心间最小距离。
  • param2:较低的值检测更多圆(包括误检);提高此值可更严格。
  • minRadius / maxRadius:缩小范围以加速并减少误检。

5. 轮廓检测与分析

**轮廓**是沿具有相同颜色或强度的边界连接连续点的曲线。轮廓是比原始边缘更高层次的表示,对物体检测和形状分析极为有用。

5.1 查找轮廓

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 加载并二值化
img = cv2.imread("robot_workspace.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二值阈值(或使用 Canny 边缘)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# 查找轮廓
# cv2.findContours(image, mode, method)
#   mode: cv2.RETR_EXTERNAL — 仅外层轮廓
#         cv2.RETR_TREE — 所有轮廓及其层级关系
#   method: cv2.CHAIN_APPROX_SIMPLE — 压缩线段(仅保留端点)
contours, hierarchy = cv2.findContours(
    binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)

# 绘制轮廓
img_contours = img.copy()
cv2.drawContours(img_contours, contours, -1, (0, 255, 0), 2)

print(f"找到 {len(contours)} 个轮廓")

plt.figure(figsize=(8, 6))
plt.imshow(cv2.cvtColor(img_contours, cv2.COLOR_BGR2RGB))
plt.title(f"轮廓(找到 {len(contours)} 个)")
plt.axis("off")
plt.show()

5.2 轮廓特征

找到轮廓后,我们可以提取丰富的几何特征:

for i, cnt in enumerate(contours):
    # 面积(像素数)
    area = cv2.contourArea(cnt)

    # 周长(弧长)
    # True = 闭合轮廓
    perimeter = cv2.arcLength(cnt, closed=True)

    # 矩(用于计算质心)
    M = cv2.moments(cnt)
    if M["m00"] != 0:
        cx = int(M["m10"] / M["m00"])  # 质心 x
        cy = int(M["m01"] / M["m00"])  # 质心 y
    else:
        cx, cy = 0, 0

    # 轴对齐边界矩形
    x, y, w, h = cv2.boundingRect(cnt)

    # 最小面积边界矩形(可旋转)
    rect = cv2.minAreaRect(cnt)
    # rect = ((cx, cy), (width, height), angle)
    box = cv2.boxPoints(rect)
    box = np.int32(box)

    # 最小外接圆
    (circle_cx, circle_cy), radius = cv2.minEnclosingCircle(cnt)

    # 宽高比
    aspect_ratio = w / h if h > 0 else 0

    # 范围率:轮廓面积与边界矩形面积之比
    extent = area / (w * h) if (w * h) > 0 else 0

    # 实心率:轮廓面积与凸包面积之比
    hull = cv2.convexHull(cnt)
    hull_area = cv2.contourArea(hull)
    solidity = area / hull_area if hull_area > 0 else 0

    print(f"轮廓 {i}: 面积={area:.0f}, 周长={perimeter:.1f}, "
          f"质心=({cx},{cy}), 宽高比={aspect_ratio:.2f}, "
          f"范围率={extent:.2f}, 实心率={solidity:.2f}")

特征汇总表:

特征 函数 描述
面积 cv2.contourArea(cnt) 轮廓内部的像素数
周长 cv2.arcLength(cnt, True) 轮廓总长度
cv2.moments(cnt) 统计形状描述符
边界矩形 cv2.boundingRect(cnt) 轴对齐矩形 \((x, y, w, h)\)
最小面积矩形 cv2.minAreaRect(cnt) 旋转的最小矩形
最小外接圆 cv2.minEnclosingCircle(cnt) 最小的外接圆
宽高比 \(w / h\) 边界矩形的宽高比
范围率 \(\text{area} / (w \times h)\) 边界矩形的填充比
实心率 \(\text{area} / \text{hull\_area}\) 凸包的填充比

5.3 多边形逼近与凸包

**多边形逼近**将轮廓简化为具有更少顶点的多边形:

# 将轮廓逼近为多边形
# cv2.approxPolyDP(curve, epsilon, closed)
#   epsilon = 轮廓到逼近多边形的最大距离
epsilon_fraction = 0.02  # 周长的 2%
for cnt in contours:
    peri = cv2.arcLength(cnt, True)
    approx = cv2.approxPolyDP(cnt, epsilon_fraction * peri, True)

    # 顶点数指示形状类型
    n_vertices = len(approx)
    if n_vertices == 3:
        shape = "三角形"
    elif n_vertices == 4:
        shape = "矩形/正方形"
    elif n_vertices > 6:
        shape = "圆形"
    else:
        shape = f"{n_vertices}边形"
    print(f"形状: {shape}{n_vertices} 个顶点)")

**凸包**是包围轮廓的最小凸多边形:

# 凸包和凸性缺陷
hull = cv2.convexHull(cnt, returnPoints=True)

# 凸性缺陷(用于手部/抓取检测)
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 是最远点到凸包的距离
        if depth / 256 > 10:  # 过滤小缺陷
            far_point = tuple(cnt[far][0])
            cv2.circle(img, far_point, 5, (0, 0, 255), -1)

形状识别

结合 approxPolyDP 和凸性缺陷可以实现机器人抓取中的形状识别——例如,判断物体是盒子(4 个顶点、高实心率)还是不规则工具(多个顶点、低实心率)。


6. 机器人应用

6.1 车道检测

经典机器人任务——使用边缘和直线检测车道标线:

import cv2
import numpy as np

def detect_lanes(image):
    """检测道路图像中的车道线。"""
    h, w = image.shape[:2]
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # 定义感兴趣区域(车辆前方的梯形区域)
    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)

    # 边缘检测 + 霍夫变换
    edges = cv2.Canny(masked, 50, 150)
    lines = cv2.HoughLinesP(
        edges, 1, np.pi / 180, threshold=50,
        minLineLength=100, maxLineGap=50
    )

    # 在原始图像上绘制直线
    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

# 使用示例
# frame = cv2.imread("road.jpg")
# lanes = detect_lanes(frame)
# cv2.imshow("车道线", lanes)
# cv2.waitKey(0)

6.2 物体边界提取

使用边缘和轮廓隔离传送带或工作空间上的物体:

def extract_object_boundary(image):
    """找到图像中最大的轮廓(物体)。"""
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    edges = cv2.Canny(blurred, 50, 150)

    # 膨胀边缘以闭合间隙
    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

    # 找到最大轮廓
    largest = max(contours, key=cv2.contourArea)

    result = image.copy()
    cv2.drawContours(result, [largest], -1, (0, 255, 0), 2)

    # 获取边界矩形作为 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 抓取 ROI 检测

使用轮廓分析检测物体并计算抓取点:

def find_grasp_points(image, min_area=500):
    """找到可抓取物体并计算拾取点。"""
    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"])

        # 最小面积矩形用于有向边界框
        rect = cv2.minAreaRect(cnt)
        angle = rect[2]

        objects.append({
            "center": (cx, cy),
            "area": area,
            "angle": angle,
            "contour": cnt,
            "rect": rect
        })

    return objects

# 使用示例:
# objects = find_grasp_points(image)
# for obj in objects:
#     print(f"拾取点 {obj['center']}, 角度={obj['angle']:.1f}°")

7. 练习

练习 1:Sobel 与 Canny 对比

加载任意图像,并排显示 Sobel 幅值、LoG 结果和 Canny 边缘。尝试不同的 Sobel 核大小(3、5、7)和 Canny 阈值。

任务:

  1. 使用 ksize = 3、5、7 应用 Sobel,比较边缘粗细
  2. 使用不同的高斯 \(\sigma\) 值(0.5、1.0、2.0)应用 LoG
  3. 使用阈值对 (20, 60)、(50, 150)、(100, 200) 应用 Canny
  4. 创建 \(3 \times 3\) 子图网格比较所有结果

练习 2:Canny 滑块探索器

使用 OpenCV 滑块构建交互式 Canny 参数探索器。添加高斯模糊核大小的滑块。

任务:

  1. 创建 T_lowT_high 和模糊 ksize 的滑块
  2. 并排显示原始图像和 Canny 边缘
  3. 添加使用自动 Canny 方法的选项
  4. 按 's' 键时将最佳参数保存到文本文件

练习 3:霍夫变换车道检测器

为道路图像编写完整的车道检测流水线。

任务:

  1. 应用梯形 ROI 掩码聚焦于道路区域
  2. 使用 Canny + HoughLinesP 检测车道线
  3. 按斜率过滤:左车道线斜率为负,右车道线斜率为正
  4. 在原始图像上用不同颜色绘制车道线(左=蓝色,右=红色)
  5. 显示中间结果(边缘、掩码边缘、原始直线、过滤后直线)

练习 4:物体形状分类器

编写一个函数,使用轮廓特征按形状对图像中的物体进行分类。

任务:

  1. 预处理:灰度 → 模糊 → 阈值 → 查找轮廓
  2. 对每个轮廓:使用 \(\epsilon = 0.02 \times \text{perimeter}\) 计算 approxPolyDP
  3. 根据顶点数分类:三角形(3)、正方形/矩形(4)、五边形(5)、圆形(> 6)
  4. 绘制每个轮廓并标注形状名称
  5. 计算并打印表格:形状名称、面积、周长、实心率、宽高比

方法对比总结

方法 检测内容 输出 鲁棒性 速度 最佳用途
Sobel 梯度边缘 边缘幅值图像 低(噪声) ★★★★★ 快速梯度可视化
Scharr 梯度边缘 边缘幅值图像 中等 ★★★★★ 精确的 3×3 梯度
LoG 零交叉边缘 边缘图像 中等 ★★★★ 斑点和边缘检测
Canny 干净的边缘图 二值边缘图像 ★★★★ 通用边缘检测
HoughLines 直线 \((\rho, \theta)\) 中等 ★★★ 车道检测、结构分析
HoughCircles \((a, b, r)\) 中等 ★★★ 球体/圆检测
轮廓 封闭边界 点序列 ★★★★★ 物体分析、抓取

参考资料