Canny 边缘检测算法是一种经典的图像边缘检测方法,由 John F. Canny 在 1986 年提出。该算法旨在以最优方式检测图像边缘,其核心目标是实现 低误检率、良好的定位性和最少的响应次数。Canny 算法包含五个主要步骤,下面将详细介绍每个步骤的原理和相关公式推导。
一、算法步骤概述
- 噪声抑制(高斯平滑)
- 计算梯度(使用 Sobel 或类似算子)
- 非极大值抑制(NMS)
- 双阈值检测
- 边缘连接(滞后阈值)
二、详细步骤与公式推导
1. 高斯滤波(去噪)
图像中存在噪声会干扰边缘检测,因此第一步是用高斯滤波器平滑图像。
公式:
高斯核函数(二维):
G ( x , y ) = 1 2 π σ 2 exp ( − x 2 + y 2 2 σ 2 ) G(x, y) = \frac{1}{2\pi \sigma^2} \exp\left(-\frac{x^2 + y^2}{2\sigma^2}\right) G(x,y)=2πσ21exp(−2σ2x2+y2)
将图像 I ( x , y ) I(x, y) I(x,y) 与高斯核进行卷积:
I s ( x , y ) = I ( x , y ) ∗ G ( x , y ) I_s(x, y) = I(x, y) * G(x, y) Is(x,y)=I(x,y)∗G(x,y)
2. 梯度计算(边缘强度与方向)
使用 Sobel 算子或 Prewitt 算子计算图像中每个像素的梯度。
常用 Sobel 算子:
-
水平方向:
G x = [ − 1 0 1 − 2 0 2 − 1 0 1 ] G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} Gx= −1−2−1000121
-
垂直方向:
G y = [ − 1 − 2 − 1 0 0 0 1 2 1 ] G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix} Gy= −101−202−101
梯度幅值和方向:
M ( x , y ) = G x 2 + G y 2 M(x, y) = \sqrt{G_x^2 + G_y^2} M(x,y)=Gx2+Gy2
θ ( x , y ) = arctan ( G y G x ) \theta(x, y) = \arctan\left(\frac{G_y}{G_x}\right) θ(x,y)=arctan(GxGy)
3. 非极大值抑制(Non-Maximum Suppression)
目的是精确定位边缘,仅保留局部最大值点。
Canny 边缘检测算法中的 非极大值抑制(Non-Maximum Suppression, NMS) 是使边缘细化(thin)为单像素宽度的关键步骤,目的是消除在梯度方向上不是真正边缘的像素,从而得到更加精确的边界定位。
3.1. 背景回顾
在使用 Sobel 或其他梯度算子计算出图像每个像素的梯度幅值 M ( x , y ) M(x, y) M(x,y) 和方向 θ ( x , y ) \theta(x, y) θ(x,y) 后,很多像素的梯度虽然大,但并不一定位于真实边缘的“局部最大”处。
非极大值抑制的目标:
只保留梯度方向上“局部最大的像素”,并将其余像素置零。
3.2. 基本原理
对每个像素点:
- 获取其梯度幅值 M ( x , y ) M(x, y) M(x,y) 和方向 θ ( x , y ) \theta(x, y) θ(x,y)
- 沿着 θ \theta θ 方向,找其前后两个邻接像素的幅值
- 如果当前点的幅值不是沿该方向上的最大值,则将其抑制(设为 0)
3.3. 梯度方向量化(4 个主方向)
由于图像是离散的,梯度方向不能按连续角度进行比较,因此通常将方向 θ \theta θ(单位为角度)量化为以下 4 个方向:
方向范围(角度) | 量化方向(近似) | 比较点位置 |
---|---|---|
0° ± 22.5° | 水平 (0°) | 左 / 右 像素 |
45° ± 22.5° | 斜向 (45°) | 左下 / 右上 像素 |
90° ± 22.5° | 垂直 (90°) | 上 / 下 像素 |
135° ± 22.5° | 斜向 (135°) | 左上 / 右下 像素 |
3.4. 伪代码算法步骤
for each pixel (x, y):
compute gradient magnitude M(x, y) and direction θ(x, y)
quantize θ into one of 0°, 45°, 90°, 135°
compare M(x, y) with M1 and M2 (interpolated or neighbor points in θ direction)
if M(x, y) >= M1 and M(x, y) >= M2:
keep it
else:
suppress (set to 0)
其中 M 1 M1 M1 和 M 2 M2 M2 是沿梯度方向的两个邻域像素的幅值。
3.5. 示例图解
↑ θ=90°
|
M1 M0 M2 → 若 M0 ≥ M1 且 M0 ≥ M2,保留 M0,否则设为 0
↖ 斜方向 θ=135°
M1 M2
M0
3.6. 数学描述
设当前像素为 ( x , y ) (x, y) (x,y),其梯度方向为单位向量 d ⃗ = ( cos θ , sin θ ) \vec{d} = (\cos \theta, \sin \theta) d=(cosθ,sinθ),
取两个点:
- M 1 = M ( x + d x , y + d y ) M_1 = M(x + d_x, y + d_y) M1=M(x+dx,y+dy)
- M 2 = M ( x − d x , y − d y ) M_2 = M(x - d_x, y - d_y) M2=M(x−dx,y−dy)
若:
M ( x , y ) ≥ M 1 且 M ( x , y ) ≥ M 2 M(x, y) \geq M_1 \text{ 且 } M(x, y) \geq M_2 M(x,y)≥M1 且 M(x,y)≥M2
则保留,否则抑制。
(注意:M1、M2 可通过双线性插值获取)
3.7. 总结作用
优点 | 描述 |
---|---|
边缘细化 | 保留边缘响应的精确位置,宽边变细边 |
去除虚假边缘 | 剔除非局部最大响应的像素,减少伪边缘 |
为边缘连接打基础 | 只有局部最强点才能触发连接传播 |
4. 双阈值检测(Hysteresis Thresholding)
使用两个阈值:
- 高阈值 T H T_H TH:强边缘
- 低阈值 T L T_L TL:弱边缘
分类:
- M ( x , y ) > T H M(x, y) > T_H M(x,y)>TH:强边缘(保留)
- T L < M ( x , y ) ≤ T H T_L < M(x, y) \le T_H TL<M(x,y)≤TH:弱边缘(待定)
- M ( x , y ) ≤ T L M(x, y) \le T_L M(x,y)≤TL:非边缘(剔除)
5. 边缘连接(滞后处理)
将弱边缘像素与其邻域的强边缘进行连接(8邻域检查),只有与强边缘相连的弱边缘才保留为最终边缘。
Canny 边缘检测算法中的边缘连接(Edge Tracking by Hysteresis,又称滞后阈值处理)是算法中非常关键的一步,它的作用是将图像中真实存在但不够强烈的“弱边缘”正确地识别出来,并排除孤立的噪声响应。
5.1、为什么需要边缘连接?
在非极大值抑制之后,我们得到了细化的边缘,但其中包含两种类型的边缘响应:
- 强边缘点:响应值 > 高阈值( T H T_H TH),这些是可靠的边缘。
- 弱边缘点:介于 T L T_L TL(低阈值)与 T H T_H TH 之间的响应,不确定是否是边缘。
- 非边缘点:响应值 ≤ T L T_L TL,可直接舍弃。
问题是:某些真实边缘由于纹理或光照不均,在某些区域响应值可能较低而被归为“弱边缘”,如果直接丢弃,就会造成边缘断裂。
5.2、边缘连接的核心思想
一个弱边缘点,如果它的 8 邻域中至少有一个强边缘点,那么它被认为是边缘的一部分并保留;否则丢弃。
这是一个 递归或 BFS(广度优先搜索)式 的连接过程。
5.3、算法步骤
-
从所有强边缘像素出发。
-
对每个强边缘点的 8 邻域像素:
-
如果邻域中有弱边缘点,且未被标记为边缘:
- 将它标记为边缘。
- 把这个点加入强边缘集合(继续向外扩张)。
-
-
继续这个过程直到没有更多可以连接的弱边缘点。
-
所有没有连接上的弱边缘点被丢弃。
5.4、图示说明
T_H = 高阈值
T_L = 低阈值
强边缘点(> T_H):立即标记为边缘
弱边缘点(T_L < val <= T_H):待观察
非边缘点(≤ T_L):剔除
连接判断:
强点: o
弱点: x
其他: .
示例局部区域(假设中间是强边缘):
. x .
x o x
. x .
中心 o 是强边缘,x 是弱边缘 → 所有 x 都被连接标记为边缘
5.5、数学抽象
设梯度幅值图为 M ( x , y ) M(x, y) M(x,y),定义集合:
- E s = { ( x , y ) ∣ M ( x , y ) > T H } E_s = \{(x, y) \mid M(x, y) > T_H\} Es={(x,y)∣M(x,y)>TH}:强边缘点集合
- E w = { ( x , y ) ∣ T L < M ( x , y ) ≤ T H } E_w = \{(x, y) \mid T_L < M(x, y) \le T_H\} Ew={(x,y)∣TL<M(x,y)≤TH}:弱边缘点集合
最终保留的边缘点集合 E E E 为:
E = E s ∪ { ( x , y ) ∈ E w ∣ ∃ ( x ′ , y ′ ) ∈ N 8 ( x , y ) , ( x ′ , y ′ ) ∈ E s } E = E_s \cup \left\{(x, y) \in E_w \mid \exists (x', y') \in \mathcal{N}_8(x, y),\ (x', y') \in E_s \right\} E=Es∪{(x,y)∈Ew∣∃(x′,y′)∈N8(x,y), (x′,y′)∈Es}
其中 N 8 ( x , y ) \mathcal{N}_8(x, y) N8(x,y) 表示点 ( x , y ) (x, y) (x,y) 的 8 邻域。
三、Canny 算法的优势
- 抗噪性能强(高斯滤波)
- 定位准确(非极大值抑制)
- 不会重复响应边缘(通过滞后双阈值处理)
四、Canny 算法 C++ 实现(基于 OpenCV)
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 加载图像
cv::Mat src = cv::imread("sample.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty()) {
std::cerr << "无法加载图像!" << std::endl;
return -1;
}
// 1. 高斯滤波去噪
cv::Mat blurred;
cv::GaussianBlur(src, blurred, cv::Size(5, 5), 1.4);
// 2~5. 使用 OpenCV 的 Canny 封装函数
cv::Mat edges;
double low_thresh = 50, high_thresh = 150;
cv::Canny(blurred, edges, low_thresh, high_thresh);
// 显示结果
cv::imshow("Original", src);
cv::imshow("Canny Edges", edges);
cv::waitKey(0);
return 0;
}
参数调节建议
参数 | 说明 |
---|---|
low_thresh | 低阈值:边缘保留灵敏度高 |
high_thresh | 高阈值:边缘更稳定、抗噪强 |
GaussianBlur kernel size | 核越大,越模糊,边缘越干净 |