强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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 参数调优指南

场景threshold1threshold2说明
通用检测501503:1 比例
精细边缘80200只保留强边缘
宽松检测2080保留弱边缘
工业检测100200减少噪声干扰
文档/文字50150标准设置

经验法则: 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圆心距离根据场景设置,太小会检测重复圆
param1Canny 高阈值越大,边缘越少
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 三大边缘检测算法的原理和用法,学会了霍夫变换检测直线和圆,并通过车道线检测实战展示了边缘检测的实际应用。