OpenCV 计算机视觉完全教程 / 第 06 章 — 边缘检测
第 06 章 — 边缘检测
6.1 边缘检测概述
边缘是图像中像素值发生剧烈变化的区域,通常对应物体边界、纹理变化或深度不连续处。
边缘检测方法分类
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Sobel | 一阶导数梯度 | 简单快速 | 对噪声敏感 |
| Scharr | 改进 Sobel | 更精确的梯度 | 同 Sobel |
| Laplacian | 二阶导数 | 各向同性 | 对噪声非常敏感 |
| Canny | 多阶段算法 | 最优边缘检测 | 参数需调优 |
| Roberts | 交叉差分 | 极简实现 | 精度低 |
6.2 Sobel 算子
Sobel 计算图像在 X 和 Y 方向的梯度(一阶导数)。
原理
Sobel-X 核 (3×3): Sobel-Y 核 (3×3):
┌────────────┐ ┌────────────┐
│ -1 0 +1 │ │ -1 -2 -1 │
│ -2 0 +2 │ │ 0 0 0 │
│ -1 0 +1 │ │ +1 +2 +1 │
└────────────┘ └────────────┘
梯度幅值: G = √(Gx² + Gy²) 或近似: G = |Gx| + |Gy|
梯度方向: θ = arctan(Gy / Gx)
代码实现
import cv2
import numpy as np
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# Sobel 梯度(X 和 Y 方向)
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3) # X 方向梯度
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3) # Y 方向梯度
# 转换为绝对值(处理负梯度)
abs_x = cv2.convertScaleAbs(sobel_x)
abs_y = cv2.convertScaleAbs(sobel_y)
# 合成梯度幅值(近似)
gradient = cv2.addWeighted(abs_x, 0.5, abs_y, 0.5, 0)
# 精确梯度幅值和方向
magnitude = np.sqrt(sobel_x**2 + sobel_y**2)
direction = np.arctan2(sobel_y, sobel_x)
# 不同核大小对比
for ksize in [3, 5, 7]:
sx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=ksize)
sy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=ksize)
result = cv2.addWeighted(cv2.convertScaleAbs(sx), 0.5,
cv2.convertScaleAbs(sy), 0.5, 0)
// C++ Sobel
cv::Mat img = cv::imread("photo.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat grad_x, grad_y;
cv::Sobel(img, grad_x, CV_64F, 1, 0, 3);
cv::Sobel(img, grad_y, CV_64F, 0, 1, 3);
cv::Mat abs_x, abs_y, gradient;
cv::convertScaleAbs(grad_x, abs_x);
cv::convertScaleAbs(grad_y, abs_y);
cv::addWeighted(abs_x, 0.5, abs_y, 0.5, 0, gradient);
注意: Sobel 必须使用
CV_64F(或CV_32F)输出类型,因为梯度可能为负值。然后用convertScaleAbs转回uint8。
6.3 Scharr 算子
Scharr 是 Sobel 的改进版本,在 3×3 核下精度更高:
# Scharr 梯度
scharr_x = cv2.Scharr(img, cv2.CV_64F, 1, 0)
scharr_y = cv2.Scharr(img, cv2.CV_64F, 0, 1)
# Scharr 核
# Scharr-X: [[-3,0,3], [-10,0,10], [-3,0,3]]
# Scharr-Y: [[-3,-10,-3], [0,0,0], [3,10,3]]
6.4 Laplacian 算子
Laplacian 计算图像的二阶导数:
import cv2
import numpy as np
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# Laplacian
laplacian = cv2.Laplacian(img, cv2.CV_64F, ksize=3)
laplacian_abs = cv2.convertScaleAbs(laplacian)
# LoG(Laplacian of Gaussian)— 先高斯平滑再 Laplacian
blurred = cv2.GaussianBlur(img, (5, 5), 1.0)
log_result = cv2.Laplacian(blurred, cv2.CV_64F, ksize=3)
log_abs = cv2.convertScaleAbs(log_result)
Laplacian 核
标准 3×3: 带对角线:
┌────────────┐ ┌────────────┐
│ 0 -1 0 │ │ -1 -1 -1 │
│ -1 4 -1 │ │ -1 8 -1 │
│ 0 -1 0 │ │ -1 -1 -1 │
└────────────┘ └────────────┘
6.5 Canny 边缘检测
Canny 是最经典的边缘检测算法,由 John F. Canny 于 1986 年提出。
算法流程
原始图像
↓
① 高斯平滑(降噪)
↓
② 计算梯度幅值和方向(Sobel)
↓
③ 非极大值抑制(NMS)— 只保留梯度方向上的局部最大值
↓
④ 双阈值检测 — 分为强边缘和弱边缘
↓
⑤ 边缘跟踪(滞后阈值)— 弱边缘连接到强边缘则保留
↓
最终边缘图
代码实现
import cv2
import numpy as np
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# 基本 Canny
edges = cv2.Canny(img, threshold1=50, threshold2=150)
# 不同阈值效果
edges_loose = cv2.Canny(img, 30, 100) # 低阈值,检测更多边缘
edges_strict = cv2.Canny(img, 100, 200) # 高阈值,只保留强边缘
# 推荐:先降噪再检测
blurred = cv2.GaussianBlur(img, (5, 5), 1.0)
edges_denoised = cv2.Canny(blurred, 50, 150)
# 自动阈值(基于中值)
median_val = np.median(img)
lower = int(max(0, 0.67 * median_val))
upper = int(min(255, 1.33 * median_val))
edges_auto = cv2.Canny(img, lower, upper)
print(f"自动阈值: lower={lower}, upper={upper}")
// C++ Canny
cv::Mat img = cv::imread("photo.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat blurred, edges;
cv::GaussianBlur(img, blurred, cv::Size(5, 5), 1.0);
cv::Canny(blurred, edges, 50, 150);
Canny 参数调优指南
| 场景 | threshold1 | threshold2 | 说明 |
|---|---|---|---|
| 通用检测 | 50 | 150 | 3:1 比例 |
| 精细边缘 | 80 | 200 | 只保留强边缘 |
| 宽松检测 | 20 | 80 | 保留弱边缘 |
| 工业检测 | 100 | 200 | 减少噪声干扰 |
| 文档/文字 | 50 | 150 | 标准设置 |
经验法则: threshold2 ≈ 2~3 × threshold1
6.6 轮廓检测(预览)
import cv2
img = cv2.imread("photo.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150)
# 从边缘图中查找轮廓
contours, hierarchy = cv2.findContours(
edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)
# 绘制轮廓
result = img.copy()
cv2.drawContours(result, contours, -1, (0, 255, 0), 2)
print(f"找到 {len(contours)} 个轮廓")
详细内容: 轮廓的完整操作将在 第 08 章 — 轮廓分析 中深入讲解。
6.7 霍夫变换(Hough Transform)
6.7.1 霍夫线变换
霍夫变换用于在边缘图中检测直线。
import cv2
import numpy as np
img = cv2.imread("road.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150)
# 标准霍夫线变换
# rho: 距离分辨率(像素)
# theta: 角度分辨率(弧度)
# threshold: 累加器阈值
lines = cv2.HoughLines(edges, rho=1, theta=np.pi/180, threshold=150)
if lines is not None:
result = img.copy()
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(result, (x1, y1), (x2, y2), (0, 0, 255), 2)
# 概率霍夫线变换(推荐 — 返回线段端点)
lines_p = cv2.HoughLinesP(
edges,
rho=1,
theta=np.pi/180,
threshold=80,
minLineLength=50, # 最短线段长度
maxLineGap=10 # 最大线段间隔
)
result_p = img.copy()
if lines_p is not None:
for line in lines_p:
x1, y1, x2, y2 = line[0]
cv2.line(result_p, (x1, y1), (x2, y2), (0, 255, 0), 2)
print(f"检测到 {len(lines_p)} 条线段")
6.7.2 霍夫圆变换
import cv2
import numpy as np
img = cv2.imread("coins.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.medianBlur(gray, 5) # 中值滤波去噪
# 霍夫圆检测
circles = cv2.HoughCircles(
gray,
cv2.HOUGH_GRADIENT, # 检测方法(唯一选项)
dp=1, # 累加器分辨率(1 = 与原图相同)
minDist=50, # 圆心最小距离
param1=100, # Canny 边缘高阈值
param2=50, # 累加器阈值
minRadius=20, # 最小半径
maxRadius=100 # 最大半径
)
result = img.copy()
if circles is not None:
circles = np.uint16(np.around(circles))
for c in circles[0, :]:
center = (c[0], c[1])
radius = c[2]
cv2.circle(result, center, radius, (0, 255, 0), 2)
cv2.circle(result, center, 2, (0, 0, 255), 3)
print(f"圆: 中心=({c[0]},{c[1]}), 半径={c[2]}")
霍夫圆参数调优
| 参数 | 影响 | 调优建议 |
|---|---|---|
| dp | 累加器精度 | 1 或 1.5,越大越快但越不精确 |
| minDist | 圆心距离 | 根据场景设置,太小会检测重复圆 |
| param1 | Canny 高阈值 | 越大,边缘越少 |
| param2 | 累加器阈值 | 越小,检测到的圆越多 |
| minRadius | 最小半径 | 根据目标物体设置 |
| maxRadius | 最大半径 | 根据目标物体设置 |
6.8 方向梯度直方图(HOG)预览
import cv2
import numpy as np
img = cv2.imread("person.jpg", cv2.IMREAD_GRAYSCALE)
# HOG 特征提取器(用于行人检测)
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
# 检测行人
locations, weights = hog.detectMultiScale(
img,
winStride=(8, 8),
padding=(4, 4),
scale=1.05
)
# 绘制检测结果
result = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
for (x, y, w, h), weight in zip(locations, weights):
if weight > 0.5:
cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.putText(result, f"{weight[0]:.2f}", (x, y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
6.9 实战:车道线检测
"""
lane_detection.py — 简易车道线检测
"""
import cv2
import numpy as np
def region_of_interest(img, vertices):
"""只保留感兴趣区域"""
mask = np.zeros_like(img)
cv2.fillPoly(mask, vertices, 255)
return cv2.bitwise_and(img, mask)
def detect_lane(image_path):
img = cv2.imread(image_path)
h, w = img.shape[:2]
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 1. 高斯模糊
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 2. Canny 边缘检测
edges = cv2.Canny(blurred, 50, 150)
# 3. 定义感兴趣区域(梯形)
roi_vertices = np.array([[
(0, h),
(w // 2 - 50, h // 2 + 50),
(w // 2 + 50, h // 2 + 50),
(w, h)
]], dtype=np.int32)
cropped = region_of_interest(edges, roi_vertices)
# 4. 霍夫线检测
lines = cv2.HoughLinesP(
cropped, 1, np.pi / 180, 50,
minLineLength=100, maxLineGap=50
)
# 5. 绘制车道线
result = img.copy()
if lines is not None:
for line in lines:
x1, y1, x2, y2 = line[0]
slope = (y2 - y1) / (x2 - x1 + 1e-6)
if abs(slope) > 0.5: # 过滤接近水平的线
color = (0, 0, 255) if slope < 0 else (0, 255, 0)
cv2.line(result, (x1, y1), (x2, y2), color, 3)
return result
# 使用
# result = detect_lane("road.jpg")
# cv2.imwrite("lane_result.jpg", result)
6.10 边缘检测方法对比
| 算法 | 速度 | 噪声鲁棒性 | 边缘连续性 | 双边缘 | 参数数量 |
|---|---|---|---|---|---|
| Sobel | ★★★★★ | ★★☆☆☆ | ★★☆☆☆ | 无 | 1 (ksize) |
| Scharr | ★★★★★ | ★★☆☆☆ | ★★☆☆☆ | 无 | 0 |
| Laplacian | ★★★★★ | ★☆☆☆☆ | ★★☆☆☆ | 有 | 1 (ksize) |
| LoG | ★★★★☆ | ★★★☆☆ | ★★★☆☆ | 有 | 2 |
| Canny | ★★★★☆ | ★★★★☆ | ★★★★★ | 无 | 2-4 |
6.11 扩展阅读
| 资源 | 链接 | 说明 |
|---|---|---|
| Canny 原始论文 | “A Computational Approach to Edge Detection” (1986) | 算法原理 |
| OpenCV 边缘检测教程 | docs.opencv.org/4.x/da/d22/tutorial_py_canny | 官方教程 |
| 霍夫变换文档 | docs.opencv.org/4.x/d9/db0/tutorial_hough_lines | 霍夫变换详解 |
| 下一章 | 第 07 章 — 阈值处理与形态学 | 二值化/腐蚀/膨胀 |
本章小结: 掌握了 Sobel、Canny、Laplacian 三大边缘检测算法的原理和用法,学会了霍夫变换检测直线和圆,并通过车道线检测实战展示了边缘检测的实际应用。