4、经典边缘检测算法(上):Canny边缘检测算法详解
各位同学,今天我们来啃一块硬骨头——Canny边缘检测。说实话,我在做嵌入式视觉的头两年,一直觉得Canny就是个黑盒子,调调阈值就完事了。直到有一次在工业相机项目里,因为边缘检测不准导致定位偏差,我才老老实实把每一行代码都捋了一遍。
Canny算法之所以经典,是因为它把边缘检测这件事拆成了四个清晰的步骤。每一步都有它的道理,少一步都不行。咱们今天就把这四步掰开揉碎了讲。
第一步:去噪——高斯滤波
为什么要先做去噪?你想想看,边缘检测本质上是在找图像中像素值剧烈变化的地方。但噪声也会引起像素值突变,如果不先滤掉噪声,算法会把噪声当成边缘。
Canny用的是高斯滤波。说白了就是用周围像素的加权平均值来代替当前像素值,权重由高斯函数决定。离中心越近的像素权重越大,越远的权重越小。
我记得第一次在STM32上实现时,直接用了5x5的高斯核,结果处理一帧640x480的图像要200多毫秒。后来改成3x3核,速度翻了一倍,效果也还能接受。这里给大家一个经验:嵌入式平台上,3x3高斯核是性价比最高的选择。
核心要点:高斯滤波的核大小直接影响计算量和平滑效果。核越大,去噪越强,但边缘也会越模糊。嵌入式场景下,3x3或5x5就够用了。
第二步:梯度计算——Sobel算子
去噪之后,我们要找到图像中像素变化最剧烈的方向。Canny用的是Sobel算子,它有两个3x3的卷积核:一个检测水平方向变化(Gx),一个检测垂直方向变化(Gy)。
具体怎么算?我直接上代码,这是我在项目里用过的简化版:
// 3x3 Sobel 水平梯度计算
int sobel_x(int img[3][3]) {
return (img[0][2] + 2*img[1][2] + img[2][2])
- (img[0][0] + 2*img[1][0] + img[2][0]);
}
// 3x3 Sobel 垂直梯度计算
int sobel_y(int img[3][3]) {
return (img[2][0] + 2*img[2][1] + img[2][2])
- (img[0][0] + 2*img[0][1] + img[0][2]);
}
有了Gx和Gy,我们就可以算出每个像素的梯度幅值和方向:
- 梯度幅值:G = sqrt(Gx² + Gy²)。嵌入式里为了省计算,常用 |Gx| + |Gy| 近似。
- 梯度方向:θ = atan2(Gy, Gx)。这个反正切函数在MCU上很慢,我一般用查表法。
我的小技巧:在Cortex-M4上,用DSP库的arm_sqrt_f32()算平方根,比手动实现快3倍以上。别自己写开方函数,除非你想体验CPU满载的感觉。
第三步:非极大值抑制——让边缘变细
这一步是Canny的精髓。你想想,经过梯度计算后,一个边缘区域会有很多像素都算出了较大的梯度值。但真正的边缘应该只有一条线,而不是一片模糊的带子。
非极大值抑制的做法是:对于每个像素,沿着它的梯度方向,检查它是不是局部最大值。如果是,保留;如果不是,置为0。
嗯,这里要注意一个细节:梯度方向是连续的,但像素是离散的。我们需要把梯度方向量化到四个方向:0°、45°、90°、135°。然后比较当前像素和它相邻两个像素的梯度值。
我曾经在移植时犯过一个低级错误:忘记处理边界像素。结果图像边缘出现了一圈假边缘,排查了半天才发现是数组越界导致的。所以大家写代码时,边界处理一定要单独写逻辑。
避坑指南:非极大值抑制后,梯度幅值矩阵中大部分像素都会变成0。如果你发现结果图全是黑的,先检查梯度方向量化是否正确。我曾经在这个问题上卡了整整一个下午。
第四步:双阈值检测——区分强弱边缘
经过非极大值抑制,我们得到了一堆候选边缘点。但哪些是真正的边缘,哪些是噪声残留?Canny用双阈值来解决这个问题。
具体做法是设置两个阈值:
- 高阈值(T_high):梯度值高于此阈值的,确定为强边缘。
- 低阈值(T_low):梯度值低于此阈值的,直接丢弃。
- 中间区域:介于两者之间的,标记为弱边缘。如果弱边缘与强边缘相连,则保留;否则丢弃。
这个「滞后阈值跟踪」的逻辑,说白了就是:强边缘是确定的,弱边缘只有跟强边缘沾亲带故才算数。
阈值怎么选?我个人的习惯是:高阈值取梯度最大值的70%-80%,低阈值取高阈值的1/3到1/2。但这个比例不是固定的,得根据你的图像场景来调。比如在光照均匀的室内场景,阈值可以设得低一些;在户外强光下,阈值就得提高。
| 场景 | 高阈值建议 | 低阈值建议 |
|---|---|---|
| 室内均匀光照 | 梯度最大值的70% | 高阈值的1/3 |
| 户外强光 | 梯度最大值的85% | 高阈值的1/2 |
| 低对比度场景 | 梯度最大值的60% | 高阈值的1/4 |
总结一下:Canny算法的四个步骤环环相扣。去噪是基础,梯度计算是手段,非极大值抑制是精炼,双阈值检测是决策。少一步,结果都不对。
最后说一句,Canny在PC上跑得很欢,但移植到嵌入式平台时,每一步都可能成为性能瓶颈。我的建议是:先保证功能正确,再逐步骤优化。别一上来就想着用汇编优化,先把算法跑通再说。
下一章我们会讲另一个经典算法——Sobel和Laplacian,它们比Canny简单,但各有各的妙用。咱们到时候接着聊。