边缘检测(Edge Detection)¶
边缘是机器人视觉中最基本的特征之一。边缘标记了图像中两个区域之间的边界,此处强度发生急剧变化——对应于物体边界、表面标记、阴影或纹理过渡。边缘检测可以大幅减少机器人需要处理的数据量,同时保留车道跟随、物体识别、抓取和导航等任务所需的结构信息。
本教程涵盖边缘检测的理论与实践,包括从简单的梯度算子到广泛使用的 Canny 检测器、用于几何形状提取的霍夫变换(Hough Transform),以及用于下游机器人应用的轮廓分析。
学习目标¶
完成本教程后,你将能够:
- 解释什么是边缘以及它在机器人感知中的重要性
- 使用 Sobel、Scharr 和高斯拉普拉斯算子计算图像梯度
- 应用 Canny 边缘检测器并针对不同场景调整参数
- 使用霍夫变换检测直线和圆
- 查找、分析和逼近轮廓以进行物体识别
- 将边缘和轮廓技术应用于实际机器人任务(车道检测、抓取 ROI)
前置要求¶
| 要求 | 详情 |
|---|---|
| Python | 3.8+ |
| 库 | opencv-python、numpy、matplotlib |
| 先修知识 | 基础 Python、NumPy 数组、使用 OpenCV 加载图像 |
安装依赖(如需要):
1. 什么是边缘?¶
**边缘**是图像中强度快速变化的位置。从数学上看,边缘对应于图像强度函数一阶导数(梯度)的局部极大值。
1.1 强度不连续性¶
考虑图像中单行的一维强度分布 \(I(x)\)。导数 \(\frac{dI}{dx}\) 告诉我们强度变化的速度:
强边缘对应于 \(|dI/dx|\) 的大值。
1.2 边缘类型¶
| 类型 | 描述 | 强度分布 | 导数 |
|---|---|---|---|
| 阶跃边缘 | 突然的强度变化(如物体边界) | 急剧跳变 | 单个尖峰 |
| 斜坡边缘 | 渐进的强度变化(如模糊边界) | 倾斜过渡 | 宽峰 |
| 脊/屋顶边缘 | 亮或暗的细线 | 窄峰或窄谷 | 双尖峰(正+负) |
在二维图像中,边缘由梯度的**幅值**和**方向**共同描述:
其中 \(\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\) 核来估计水平和垂直梯度:
梯度幅值和方向为:
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\) 梯度估计,使用更大的权重:
# 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)¶
**拉普拉斯算子**是二阶导数算子,通过检测二阶导数的零交叉点来定位边缘:
由于拉普拉斯算子对噪声非常敏感,我们首先用高斯滤波器对图像进行平滑,得到**高斯拉普拉斯(LoG)**:
其中 \(G_\sigma\) 是标准差为 \(\sigma\) 的二维高斯函数。常用的 \(5 \times 5\) 近似核为:
# 高斯拉普拉斯
# 先进行高斯模糊,再计算拉普拉斯
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:高斯平滑
用高斯滤波器去除噪声,防止产生虚假边缘检测:
步骤 2:梯度计算
计算梯度幅值和方向(通常使用 Sobel):
步骤 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\) 是从原点到直线的垂直距离,\(\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 圆检测¶
圆使用三参数霍夫变换进行检测:
其中 \((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 阈值。
任务:
- 使用
ksize= 3、5、7 应用 Sobel,比较边缘粗细 - 使用不同的高斯 \(\sigma\) 值(0.5、1.0、2.0)应用 LoG
- 使用阈值对 (20, 60)、(50, 150)、(100, 200) 应用 Canny
- 创建 \(3 \times 3\) 子图网格比较所有结果
练习 2:Canny 滑块探索器¶
使用 OpenCV 滑块构建交互式 Canny 参数探索器。添加高斯模糊核大小的滑块。
任务:
- 创建
T_low、T_high和模糊ksize的滑块 - 并排显示原始图像和 Canny 边缘
- 添加使用自动 Canny 方法的选项
- 按 's' 键时将最佳参数保存到文本文件
练习 3:霍夫变换车道检测器¶
为道路图像编写完整的车道检测流水线。
任务:
- 应用梯形 ROI 掩码聚焦于道路区域
- 使用 Canny +
HoughLinesP检测车道线 - 按斜率过滤:左车道线斜率为负,右车道线斜率为正
- 在原始图像上用不同颜色绘制车道线(左=蓝色,右=红色)
- 显示中间结果(边缘、掩码边缘、原始直线、过滤后直线)
练习 4:物体形状分类器¶
编写一个函数,使用轮廓特征按形状对图像中的物体进行分类。
任务:
- 预处理:灰度 → 模糊 → 阈值 → 查找轮廓
- 对每个轮廓:使用 \(\epsilon = 0.02 \times \text{perimeter}\) 计算
approxPolyDP - 根据顶点数分类:三角形(3)、正方形/矩形(4)、五边形(5)、圆形(> 6)
- 绘制每个轮廓并标注形状名称
- 计算并打印表格:形状名称、面积、周长、实心率、宽高比
方法对比总结¶
| 方法 | 检测内容 | 输出 | 鲁棒性 | 速度 | 最佳用途 |
|---|---|---|---|---|---|
| Sobel | 梯度边缘 | 边缘幅值图像 | 低(噪声) | ★★★★★ | 快速梯度可视化 |
| Scharr | 梯度边缘 | 边缘幅值图像 | 中等 | ★★★★★ | 精确的 3×3 梯度 |
| LoG | 零交叉边缘 | 边缘图像 | 中等 | ★★★★ | 斑点和边缘检测 |
| Canny | 干净的边缘图 | 二值边缘图像 | 高 | ★★★★ | 通用边缘检测 |
| HoughLines | 直线 | \((\rho, \theta)\) | 中等 | ★★★ | 车道检测、结构分析 |
| HoughCircles | 圆 | \((a, b, r)\) | 中等 | ★★★ | 球体/圆检测 |
| 轮廓 | 封闭边界 | 点序列 | 高 | ★★★★★ | 物体分析、抓取 |
参考资料¶
- Canny, J., "A Computational Approach to Edge Detection," IEEE TPAMI, 1986 — 原始 Canny 边缘检测器论文
- OpenCV 边缘检测教程 — Sobel、Scharr、Laplacian 官方文档
- OpenCV Canny 边缘检测 — 带示例的 Canny 教程
- OpenCV 霍夫变换 — 直线和圆检测
- OpenCV 轮廓教程 — 轮廓基础、特征和属性
- Szeliski, R., "Computer Vision: Algorithms and Applications," 2nd ed., Springer, 2022 — 边缘检测理论的综合参考
- Gonzalez & Woods, "Digital Image Processing," 4th ed., Pearson, 2018 — 包含详细边缘检测推导的经典教材