3、基于梯度的评价函数(一):Tenengrad函数、Sobel算子实现、阈值选择策略
聊到自动对焦,最经典、最常用的方法就是基于梯度的评价函数。说白了,就是判断图像清不清晰,看边缘锐不锐利。边缘越锐利,梯度值越大,图像就越“合焦”。
这一章我们重点讲三个东西:Tenengrad函数、Sobel算子怎么落地、以及阈值怎么选。嗯,都是硬核干货,我尽量讲得接地气一点。
3.1 Tenengrad函数:梯度评价的“老大哥”
Tenengrad函数,其实就是一个基于Sobel算子的梯度能量计算。它的核心思想很简单:对图像每个像素求梯度幅值,然后累加求和。
公式长这样:
F(I) = Σ_x Σ_y [ G_x(x,y)² + G_y(x,y)² ]
其中 G_x 和 G_y 分别是Sobel水平方向和垂直方向的梯度。注意,这里用的是平方和,不是值。为什么?
我个人习惯用平方和,因为这样能放大边缘响应,让合焦和离焦的差距更明显。你想想看,如果边缘很弱,平方后更弱;边缘很强,平方后更强——对比度一下就拉开了。
关键点: Tenengrad函数对噪声敏感度适中,对纹理丰富的场景表现很好。但如果是纯色区域(比如白墙),梯度值接近0,评价函数会失效。
3.2 Sobel算子实现:别小看这3x3
Sobel算子,本质上是一个离散微分算子。它结合了高斯平滑和差分,所以对噪声有一定的抑制能力。
标准的3x3 Sobel核:
| 方向 | 核矩阵 | 作用 |
|---|---|---|
| 水平 (Gx) |
|
检测垂直边缘 |
| 垂直 (Gy) |
|
检测水平边缘 |
实现的时候,我建议直接用卷积操作。在嵌入式平台上,如果CPU资源紧张,可以只用Gx或Gy,但精度会下降。我在项目中遇到过这种情况:为了省算力只算Gx,结果对水平条纹的场景完全失效。后来老老实实两个方向都算。
下面是一个C语言风格的伪代码实现,适合嵌入式环境:
// Sobel梯度计算(单通道灰度图)
void sobel_gradient(uint8_t *src, int16_t *grad, int w, int h) {
for (int y = 1; y < h-1; y++) {
for (int x = 1; x < w-1; x++) {
int gx = -src[(y-1)*w + (x-1)] + src[(y-1)*w + (x+1)]
-2*src[y*w + (x-1)] + 2*src[y*w + (x+1)]
-src[(y+1)*w + (x-1)] + src[(y+1)*w + (x+1)];
int gy = -src[(y-1)*w + (x-1)] -2*src[(y-1)*w + x] - src[(y-1)*w + (x+1)]
+ src[(y+1)*w + (x-1)] +2*src[(y+1)*w + x] + src[(y+1)*w + (x+1)];
grad[y*w + x] = abs(gx) + abs(gy); // 也可以用平方和
}
}
}
小技巧: 在ARM Cortex-M系列上,用查表法代替乘法可以提速30%以上。如果内存够,提前算好Sobel核的权重表。
3.3 阈值选择策略:别让噪声毁了你的对焦
直接累加所有像素的梯度值,会有一个问题:噪声也被算进去了。尤其是低光照环境下,传感器噪声会导致梯度值虚高,让系统误以为已经合焦。
解决办法就是加一个阈值。只有梯度值超过阈值的像素才参与累加。公式变成:
F(I) = Σ_x Σ_y [ G(x,y) , if G(x,y) > T ]
阈值T怎么选?我总结了几种策略:
| 策略 | 方法 | 适用场景 | 缺点 |
|---|---|---|---|
| 固定阈值 | 根据经验设定一个常数,比如T=30 | 光照稳定、噪声已知的环境 | 适应性差,换场景就得调参 |
| 自适应阈值 | 取全图梯度均值的1.5~2倍作为阈值 | 光照变化较大的场景 | 计算量稍大 |
| 百分比阈值 | 取梯度直方图的前10%分位点 | 纹理稀疏的场景 | 需要排序,实时性差 |
| 动态阈值 | 根据上一帧的噪声水平动态调整 | 视频连续对焦 | 实现复杂 |
我曾经在一个安防摄像头项目里踩过坑:固定阈值设得太低,晚上噪点全被当成边缘,对焦马达来回乱跑。后来改成自适应阈值,用梯度均值的1.8倍,问题就解决了。
注意: 阈值设得太高,会丢失弱边缘信息,导致对焦精度下降。设得太低,噪声干扰严重。建议在实际场景中采集一组数据,画一条“阈值-评价函数值”曲线,找到拐点。
3.4 整体流程:从图像到评价分数
把上面这些串起来,一个完整的基于梯度的自动对焦评价流程是这样的:
- 采集图像:从传感器获取一帧RAW图,转成灰度。
- ROI选取:只对画面中心区域(或指定区域)计算梯度,减少计算量。
- Sobel卷积:分别计算Gx和Gy。
- 梯度幅值计算:用值或平方和得到每个像素的梯度值。
- 阈值过滤:只保留超过阈值的梯度值。
- 累加求和:得到最终的清晰度评价分数。
- 搜索策略:根据分数驱动马达,寻找峰值位置。
下面这张图展示了整个流程:
3.5 实战建议:别盲目追求高精度
最后说点实在的。很多初学者一上来就搞复杂的梯度算子,什么Scharr、Laplacian、甚至Canny。其实在自动对焦这个场景里,Sobel + 合适的阈值已经能覆盖90%以上的需求。
我个人的经验是:先跑通最简单的Tenengrad,把阈值调好,再考虑优化。如果连基础版本都跑不稳,换再高级的算子也没用。
核心总结:
- Tenengrad = Sobel梯度平方和累加
- Sobel实现注意边界处理和计算效率
- 阈值是区分边缘和噪声的关键,推荐自适应阈值
- 先跑通,再优化,别一步登天