3、反差对焦原理:图像清晰度评价函数与爬山算法
反差对焦,说白了就是让相机自己判断“哪张照片最清楚”。它不像相位对焦那样靠物理测距,而是直接在图像上算——哪个画面的边缘最锐利、细节最丰富,哪个就是对焦成功的。
我刚开始接触这个模块时,总觉得它很简单:不就是算个梯度嘛。后来在项目里踩了坑才发现,评价函数选不对、爬山步长设不好,画面就会来回震荡,死活合不上焦。嗯,今天咱们就把这块彻底讲透。
3.1 图像清晰度评价函数
评价函数是反差对焦的“眼睛”。它要回答一个问题:当前这幅图像,到底有多清晰?
清晰度的本质是图像中高频信息的多少。一张模糊的照片,边缘是平滑过渡的;一张清晰的照片,边缘是陡峭跳变的。所以,所有评价函数都在做同一件事——提取图像的高频分量。
常用的评价函数有三种:Tenengrad、Laplacian 和 Brenner。我按个人习惯把它们分成两类:基于一阶导数的(Tenengrad、Brenner)和基于二阶导数的(Laplacian)。
3.1.1 Tenengrad 函数
Tenengrad 用的是 Sobel 算子。它分别计算图像在水平方向和垂直方向的梯度,然后取平方和或值之和。
公式长这样:
Gx = Sobel_x * Image
Gy = Sobel_y * Image
Tenengrad = Σ( Gx² + Gy² )
我在项目中遇到过一个问题:Tenengrad 对噪声比较敏感。如果 sensor 的读出噪声偏大,Sobel 算出来的梯度里会混进很多假边缘,导致评价曲线不平滑。后来我加了一个 3×3 的中值滤波做预处理,效果好了不少。
3.1.2 Laplacian 函数
Laplacian 用的是二阶导数。它不区分方向,直接检测图像中的“突变点”。
标准 Laplacian 核:
Laplacian_kernel = [[0, 1, 0],
[1, -4, 1],
[0, 1, 0]]
评价值为:
Laplacian_value = Σ |Laplacian(Image)|
你想想看,二阶导数对边缘的响应比一阶导数更尖锐。所以 Laplacian 的峰值更陡,对焦精度理论上更高。
但代价是什么?它更容易受噪声干扰。我曾经在一个低照度项目里试过 Laplacian,画面稍微暗一点,评价曲线就全是毛刺,爬山算法根本没法收敛。后来我换回了 Tenengrad,虽然峰值没那么尖,但稳定多了。
3.1.3 Brenner 函数
Brenner 是最简单的评价函数之一。它只计算水平方向相隔两个像素的灰度差的平方。
Brenner_value = Σ (I(x+2, y) - I(x, y))²
为什么是相隔两个像素?因为自然图像中相邻像素的相关性很高,相隔两个像素能更好地捕获边缘信息,同时抑制一部分噪声。
Brenner 的优点是计算量极小,适合嵌入式平台。我在一个用 ARM Cortex-M4 的相机项目里就用它,每帧只需要几十微秒就能算出评价值。
但它的缺点也很明显:只考虑水平方向,对垂直纹理不敏感。如果场景里全是横条纹,Brenner 的评价曲线会非常平,爬山算法容易陷入局部极值。
3.1.4 三种函数对比
| 评价函数 | 原理 | 计算量 | 抗噪性 | 峰值锐度 | 适用场景 |
|---|---|---|---|---|---|
| Tenengrad | Sobel 一阶梯度 | 中 | 中 | 中 | 通用场景,推荐首选 |
| Laplacian | 二阶差分 | 低 | 弱 | 高 | 高 SNR、高精度需求 |
| Brenner | 水平像素差平方 | 极低 | 较强 | 低 | 嵌入式、低算力平台 |
3.2 爬山算法
有了评价函数,我们就能给每一帧图像打一个“清晰度分”。接下来要解决的问题是:怎么通过移动镜头,找到分数最高的那个位置?
爬山算法就是干这个的。它模拟人爬山的过程:往高处走,直到山顶。
3.2.1 算法流程
标准的爬山算法分三步:
- 确定初始方向:先往一个方向移动一步,计算评价值。如果变大了,说明方向对了;如果变小了,就反向。
- 持续爬升:沿着当前方向继续移动,每步都计算评价值。只要评价值在上升,就继续走。
- 检测峰值:当评价值开始下降时,说明已经越过了山顶。回退一步,或者用插值找到精确峰值位置。
伪代码:
def hill_climbing():
pos = current_position
step = initial_step_size
direction = +1
# 确定方向
score0 = evaluate(pos)
score1 = evaluate(pos + step)
if score1 < score0:
direction = -1
# 爬升
while True:
pos += direction * step
score_new = evaluate(pos)
if score_new < score_prev:
# 越过峰值,回退
pos -= direction * step
break
score_prev = score_new
return pos
3.2.2 步长策略
步长是爬山算法的灵魂。步长太大,容易跳过峰值;步长太小,对焦速度慢得像蜗牛。
我常用的策略是变步长:
- 粗搜阶段:用大步长(比如镜头全行程的 1/10),快速找到峰值附近区域。
- 精搜阶段:换小步长(比如 1/50),精确锁定峰值位置。
举个例子,一个镜头行程是 1000 步(步进电机单位),粗搜步长设 100 步,精搜步长设 20 步。这样粗搜最多走 10 步就能到峰值附近,精搜再走 5-10 步就能收敛。总步数控制在 20 步以内,对焦时间可以做到 100ms 以内。
3.2.3 局部极值与全局峰值
爬山算法最大的坑是局部极值。如果场景里有多个清晰区域(比如前景和背景都有纹理),评价曲线会出现多个峰值。爬山算法只能爬到最近的那个山顶,不一定是全局最高峰。
怎么解决?我分享两个经验:
- 多起点爬山:在镜头行程上均匀选 3-5 个起点,分别爬山,取评价值最高的那个结果。代价是计算量翻倍,但对焦可靠性大幅提升。
- 结合场景先验:如果是人脸对焦,可以优先搜索画面中央区域对应的镜头位置。我在做手机摄像头时,就利用人脸检测框的位置来缩小搜索范围,既快又准。
3.3 评价曲线与对焦窗口
评价函数算的是整幅图像吗?不是。通常我们只取画面中的对焦窗口(Focus Window)来计算。
为什么?因为整幅图算的话,背景的纹理可能会干扰对焦。比如拍人像时,背景的树叶比人脸更清晰,评价函数就会把镜头拉到背景上。
对焦窗口的选择原则:
- 大小适中:窗口太大,容易引入干扰;窗口太小,可能没有足够纹理。我一般取画面面积的 10%-20%。
- 位置灵活:可以放在画面中央,也可以跟随人脸/物体检测框移动。
- 多窗口融合:有些方案用多个小窗口分别计算,然后加权平均。权重可以根据窗口内的纹理丰富度动态调整。
我曾经在一个项目里遇到过这样的问题:对焦窗口刚好落在纯色区域(比如白墙),评价函数输出几乎为 0,爬山算法直接“失明”。后来我加了一个纹理检测逻辑——如果窗口内的方差小于阈值,就自动扩大窗口或者切换到相邻区域。
3.4 本章知识结构图
下面这张图总结了反差对焦的核心逻辑:
3.5 工程实践要点
最后,我总结几个在实际调试中容易忽略的点:
- 评价函数的归一化:不同亮度下,评价值的值会变化。建议做归一化处理,或者用相对变化量来判断峰值。
- 死区处理:当评价值变化小于某个阈值时,认为已经合焦,停止搜索。避免镜头在峰值附近来回震荡。
- 超时保护:爬山算法一定要设最大步数限制。我曾经遇到过场景纹理太少,算法一直找不到峰值,镜头走到物理限位还在跑,差点烧了电机。
- 与相位对焦的配合:在混合对焦系统中,反差对焦通常作为精调阶段使用。相位对焦先快速拉到大致位置,反差对焦再做微调。这个配合逻辑,咱们后面章节会详细讲。