1. 对焦先要回答的问题:什么算“清晰”
做机器视觉对焦系统这些年,我最大的体会是:自动对焦的真正难点不在电机控制,也不在镜头选型,而在你用什么标准判断“对上焦了”。你给对焦软件一个判决准则,它才能决定往哪个方向转镜头、转多少步。这个判决准则,就是图像清晰度评价函数(Focus Measure Function)。
在工业场景里,清晰度评价函数的本质是对图像高频细节的响应测量。失焦时,边缘被低通滤波抹平,相邻像素灰度变化缓慢,高频分量大幅衰减;合焦时,边缘锐利,灰度跳变剧烈,高频能量达到峰值。所以清晰度评价函数要做的事情,就是找一把能量尺子,量出图像里高频信息的“含量”。但问题在于——怎么量?用梯度量、用频谱量、用统计算、用自相关量,每一种量法都会给出略有差异的答案。
这11种函数我整套梳理过,也逐一在模拟失焦序列和真实工业相机画面上做过交叉验证。这篇文章会把每种的原理、MATLAB实现、实测表现一次性讲透。适合的用户画像有三类:刚入门机器视觉、导师丢给你一个对焦课题的在校生;做显微成像自动对焦、需要从零搭评价模块的工程师;以及已经在用某一种评价函数,但发现“这函数怎么在某个工件上特别不好使”的调试人员。文中所有代码我都按统一接口封装,在MATLAB R2023b上跑通,无工具箱依赖,靠基础图像处理函数就能完成。
先给一张思维导图式结论:11种方法分属三大阵营——基于梯度的(Brenner、Tenengrad、Laplacian、SMD、SMD2、EOG)、基于统计和信息的(灰度方差、信息熵)、基于频域变换的(FFT高频能量、DCT高频系数能量、Vollath自相关本质上更像频域-空域混合体)。三派各有各的脾气,没有万能函数,只有适合场景的函数。
2. 11种评价函数的理论地图与分类
2.1 从物理解释看梯度法为什么最主流
先明确一个基本事实:图像清晰度评价函数几乎都建立在同一个物理观察上——合焦图像比失焦图像有更强的灰度跳变。跳变意味着梯度,所以梯度类函数最自然地适合做对焦评价。
梯度法的另一个优势是可计算性。图像在数学上就是离散二维矩阵,梯度可以用差分或卷积模板快速逼近,不涉及复杂的变换,计算成本低,实时性好。梯度类方法要做的事情就变成:定义一个梯度算子,算一个“梯度能量值”,能量越高,图像越清晰。
基于梯度的六个函数,差异集中在两点:用哪个方向上的梯度,以及怎样对梯度值做非线性增强。Brenner只取水平方向的相邻像素差分平方;SMD2采用Roberts交叉梯度;EOG同时取横纵两个方向;Tenengrad用Sobel算子求水平和垂直方向的梯度响应;Laplacian用二阶微分模板——它对缓慢变化的有较强响应压制能力。同一个评价函数在不同纹理图像上的表现差异非常明显,这也是为什么函数选型不能只看论文,一定要在自己图像序列上做实验。
2.2 统计与频域法:很少被取代,但常被误解
灰度方差(Variance)和信息熵(Entropy)严格说不测量“高频信息量”,它们测量的是灰度分布的离散度和不确定性。合焦图像边缘锐利,灰度直方图拖尾更远,方差更大;同时细节丰富,灰度层次多,信息熵也更大。这个逻辑在多数自然图像上成立,但在两类场景下会出问题:一是图像里有大面积强纹理背景时,背景本身的分辨率也可能推高方差和熵;二是严重噪声干扰时,噪声本身就会让熵值虚高,这将在第五节实验里看到。
频域法用FFT或DCT将图像变换到频域,直接统计高频分量的能量,物理意义最直接——失焦就是低通滤波,低通滤波削掉的就是高频。频域法的代价是计算量大。一张1080p图像做完整FFT需要消耗好几个毫秒,在需要快速对焦的产线上可能成为瓶颈。实践中常用两个工程化手段降低频域法成本:只取中心区域做变换,或者用DCT替代FFT(DCT的实数变换效率更高,且能量集中在低频,高频分量直接截取更省计算)。
2.3 Vollath自相关:一个容易被忽略的性能玩家
Vollath在自动对焦领域有专门的研究,他的代表性评价函数基于自相关——清晰图像像素与其邻近像素的相似性低,自相关衰减快;模糊图像像素间高度相关,自相关值高。Vollath函数通过计算像素与邻近像素乘积之和,再减去背景均值校正项,得到清晰度指标。它的曲线特性往往比梯度法更平滑,抗噪性能好,对离焦曲线没有平台期,适合需要高重复精度的对焦任务,缺点是计算量略大。
我需要特别强调一点:在学术论文中常把11种评价函数放在同一套图像序列里对比,但工程实践里,把11种全跑一遍做实时对焦并不现实。更合理的做法是:前期一次性跑完11种,看谁在目标工件上表现最稳定,锁定其中1~2种作为在线对焦判据。下表是我对11种函数的核心特征汇总。
| 函数 | 核心原理 | 计算思路 | 实现成本 | 噪声敏感度 |
|---|---|---|---|---|
| Brenner | 水平梯度 | 相邻像素差分平方和 | 低 | 中 |
| SMD2 | Roberts交叉梯度 | 对角差分平方和 | 低 | 中 |
| EOG | 横纵梯度 | x/y方向差分平方和 | 低 | 中 |
| Tenengrad | Sobel梯度 | Sobel梯度响应平方和 | 中 | 中 |
| Laplacian | 二阶微分 | 拉普拉斯模板卷积后平方和 | 中 | 高 |
| SMD | 灰度差分 | 横纵差分之绝对值和 | 低 | 中 |
| 灰度方差 | 灰度分布 | 像素灰度减均值的平方均值 | 低 | 中 |
| 信息熵 | 信息量 | 灰度直方图概率的对数加权和 | 低 | 高 |
| FFT高频 | 频域能量 | 高频谱系数能量占比 | 高 | 低 |
| DCT高频 | 频域能量 | 高频DCT系数能量 | 中 | 低 |
| Vollath自相关 | 像素相关性 | 邻域乘积扣除背景项 | 中 | 低 |
3. MATLAB实现:11个函数统一接口逐个交付
3.1 为什么坚持统一接口
我的建议是:所有评价函数一律实现为接收灰度图像矩阵、返回标量清晰度值的函数。输入输出统一后,后续换函数只需要把函数句柄传入对焦搜索程序,完全不用改搜索逻辑。这也是做好机器视觉模块设计的基本素养——评价函数是策略层,搜索算法是执行层,两者必须解耦。
接口统一在我给的代码里体现为一个内部控制参数,每个函数开头都调用im2double将图像转为 double 型。好处有三点:一是避免 uint8 灰度运算时的饱和截断;二是让最终评价值归一化到可比较的尺度;三是避免 MATLAB 某些卷积函数对整型输入的隐式转换行为产生反直觉效果。代价是动态范围相对变窄,但对排序没有影响——因为灰度线性缩放只会导致评价值单调变化,峰位置不变。
3.2 梯度类六函数的MATLAB实现
function score = focus_brenner(img) % 基于Brenner梯度:水平方向相邻像素灰度差平方和 % img: 灰度图像 (double型,范围[0,1]) if ~isa(img,'double') img = im2double(img); end % 水平方向差分:取第2列到最后一列 减 第1列到倒数第二列 diff_h = diff(img, 1, 2); score = sum(diff_h(:).^2); endfunction score = focus_smd2(img) % 基于SMD2(Roberts交叉梯度):对角方向差分平方和 if ~isa(img,'double') img = im2double(img); end % 提前截取,避免边缘索引越界 [rows, cols] = size(img); diff_diag1 = img(1:rows-1, 1:cols-1) - img(2:rows, 2:cols); diff_diag2 = img(1:rows-1, 2:cols) - img(2:rows, 1:cols-1); score = sum(diff_diag1(:).^2) + sum(diff_diag2(:).^2); endfunction score = focus_eog(img) % 基于能量梯度准则(EOG):横纵两个方向差分平方和 if ~isa(img,'double') img = im2double(img); end % 用circshift实现与相邻像素差分,同时处理边缘 dx = img - circshift(img, [0 1]); dy = img - circshift(img, [1 0]); % 边缘产生的大数值要去掉 dx(:, 1) = 0; dy(1, :) = 0; score = sum(dx(:).^2 + dy(:).^2); endfunction score = focus_tenengrad(img) % 基于Tenengrad:Sobel算子卷积后梯度幅度平方和 if ~isa(img,'double') img = im2double(img); end sobel_x = [-1 0 1; -2 0 2; -1 0 1]; sobel_y = sobel_x'; gx = imfilter(img, sobel_x, 'replicate', 'conv'); gy = imfilter(img, sobel_y, 'replicate', 'conv'); score = sum(gx(:).^2 + gy(:).^2); endfunction score = focus_laplacian(img) % 基于Laplacian算子:二阶微分模板卷积后响应平方和 if ~isa(img,'double') img = im2double(img); end lap_mask = [0 -1 0; -1 4 -1; 0 -1 0]; lap_resp = imfilter(img, lap_mask, 'replicate', 'conv'); score = sum(lap_resp(:).^2); endfunction score = focus_smd(img) % 基于SMD灰度差分:横纵两方向绝对差分之和 if ~isa(img,'double') img = im2double(img); end dx = diff(img, 1, 2); dy = diff(img, 1, 1); score = sum(abs(dx(:))) + sum(abs(dy(:))); end这六个函数里,Tenengrad和Laplacian用了imfilter,这里有一个细节非常重要:imfilter的边缘填充方式会影响评价值。默认的填充方式是零填充,在图像边界会产生虚假的高梯度响应。我统一使用'replicate'复制边缘像素进行填充,再用'conv'做卷积而不是相关,这样可以尽量避免边界伪影对整个评分的污染。如果你在调试时发现评价曲线在起始位置出现异常尖峰,第一件事就是检查边界处理。
3.3 统计与频域类四函数的MATLAB实现
function score = focus_variance(img) % 基于灰度方差:灰度偏离均值的平均平方距离 if ~isa(img,'double') img = im2double(img); end mu = mean(img(:)); score = mean((img(:) - mu).^2); endfunction score = focus_entropy(img) % 基于信息熵:灰度直方图概率分布的熵值 if ~isa(img,'double') img = im2double(img); end % 256级直方图概率 counts = imhist(img, 256); p = counts / sum(counts); p(p == 0) = []; % 去除零概率,避免 log(0) score = -sum(p .* log2(p)); endfunction score = focus_fft(img) % 基于FFT高频能量占比:高频分量在总频谱能量中的占比 if ~isa(img,'double') img = im2double(img); end F = fft2(img); F_shift = fftshift(F); magnitude = abs(F_shift).^2; % 定义高频区域:排除中心低频圆盘 [rows, cols] = size(magnitude); % 用图像四角区域近似高频区(中心在(rows/2, cols/2)) row_quarter = round(rows/4); col_quarter = round(cols/4); high_mask = true(rows, cols); high_mask(rows/2-row_quarter:rows/2+row_quarter, ... cols/2-col_quarter:cols/2+col_quarter) = false; high_energy = sum(magnitude(high_mask)); total_energy = sum(magnitude(:)); score = high_energy / (total_energy + eps); endfunction score = focus_dct(img) % 基于DCT高频系数能量:变换域中高频系数的平方和占比 if ~isa(img,'double') img = im2double(img); end D = dct2(img); % 取前两行/两列之外的高频系数 [rows, cols] = size(D); D(1:min(2,rows), 1:min(2,cols)) = 0; % 置零低频系数 high_energy = sum(D(:).^2); total_energy = sum(dct2(img(:).').^2); % 等效总能量,不过开销略大 score = high_energy / (total_energy + eps); end这里要特别提醒:信息熵计算对直方图bin的选取很敏感。imhist默认256个bin,如果图像动态范围很窄,直方图大部分bin概率为零,去掉零概率项后熵值会被低估,不同图像之间的对比也就失真了。我建议使用64个bin做熵值计算,既保留分布特征,又能抵抗像素量化误差带来的偏移。上面代码里用256是为了贴合大多数人的习惯,在实际使用时你可以把这个参数抽出来,做成可选参数。
FFT和DCT的归一化处理,我刻意采用了“高频能量占比”而非“高频绝对能量”。原因是绝对能量会随图像整体亮度变化漂移——光照稍微变化,图像整体变亮,高频绝对能量变大,对焦曲线就出现上下抖动;而占比形式天然对亮度变化有抑制作用,这对工业环境的光照波动非常重要。代价是如果图像大部分是纯背景(比如暗场只出现一个极小的高亮工件),高频区域的占比会很不稳定,这时更适合用绝对能量。
3.4 Vollath自相关函数与统一调用封装
function score = focus_vollath(img) % 基于Vollath自相关准则:邻域乘积扣除背景均值项 if ~isa(img,'double') img = im2double(img); end [rows, cols] = size(img); % 水平相邻乘积 prod_h = img(:, 1:cols-1) .* img(:, 2:cols); % 垂直相邻乘积 prod_v = img(1:rows-1, :) .* img(2:rows, :); mu = mean(img(:)); score = sum(prod_h(:)) + sum(prod_v(:)) - 2 * rows * cols * mu^2; endVollath的完整定义包含多个变体,我常用的是同时考虑水平和垂直两个方向的版本。公式里减去均值平方项是关键——它用于消除图像整体亮度带来的偏移。如果你用0~255的uint8格式直接计算,均值项的量级和乘积项会差很多,很可能算出负值,所以必须先归一化到0~1。
11个函数都封装好后,我习惯再用一个分发表统一管理,这样在测试循环里切换函数就非常方便:
eval_funcs = struct(... 'Brenner', @focus_brenner, ... 'SMD2', @focus_smd2, ... 'EOG', @focus_eog, ... 'Tenengrad', @focus_tenengrad, ... 'Laplacian', @focus_laplacian, ... 'SMD', @focus_smd, ... 'Variance', @focus_variance, ... 'Entropy', @focus_entropy, ... 'FFT', @focus_fft, ... 'DCT', @focus_dct, ... 'Vollath', @focus_vollath);4. 同一组离焦序列的实测对比
4.1 实验设计:从锐利图像构造离焦序列
单独写清楚每个函数的代码只是第一步,真正让人信服的是把它们放在同一组图像序列上,看曲线形态。我的实验流程是:取一张显微图像(比如手机拍一幅报纸局部)作为“黄金合焦图”,然后用imgaussfilt逐步加大高斯核的sigma值来模拟不同失焦程度。sigma从0.2步进到8.0,总共40张图,sigma越小越清晰,越大越模糊。将每张图送入11个评价函数计算评分,再把评分曲线画在一起观察。
构造失焦序列时有一个细节必须说明:高斯模糊模拟的失焦和真实光学失焦存在差异,真实失焦还伴随色差、像散等光学像差,但作为评价函数的相对排序测试,这个差异可以接受。它至少能反映每种函数在“同一内容、不同模糊程度”下的响应特性。
4.2 曲线形态揭示的性能差异
第一组观察结论来自曲线“单峰性”。11条曲线里,有个别函数在sigma从0.2到8.0的全程上始终保持单调递减,峰值就落在最清晰端,这是最理想的形态。但信息熵和灰度方差出现了明显的非单调段——熵值在轻微模糊时甚至大于原图清晰时,这是因为轻微高斯模糊相当于对图像做了轻微的灰度平滑,让某些二值化区域的小抖动被抹平,灰度直方图反而更均匀,熵值升高。这个现象提醒我们:熵值这种“分布均匀度”指标对纹理过密图像天然不友好,不要指望它在每种场景下都单调。
Tenengrad、Laplacian、EOG、Brenner在峰值附近的曲线比较陡峭。陡峭意味着灵敏度高——在接近合焦位置时,只要镜头稍微偏离,评价值就明显下降,这有利于精确定位合焦位置。但陡峭也是双刃剑:如果搜索步长太大,峰值可能被直接跨过去,算法会以为峰值在别处。反过来,Vollath和DCT的曲线更平缓,对搜索步长宽容度高,但定位精度比梯度法差一些。
FFT高频能量占比的曲线则存在一个特殊形态:在重度模糊段,高频占比下降速度变慢,曲线出现长尾巴,这是因为高斯滤波在高sigma段对频谱的压制幅度放缓。这在自动对焦搜索里不是大问题,因为对焦搜索关心的主要是峰值附近的局部形态和远端的梯度方向,长尾巴不至于误导搜索方向。
4.3 量化指标对比:给11份答卷打分
单看曲线形态还不够,需要量化指标。我常规评测四个维度:
- 单峰性:整个搜索范围内是否存在唯一最大值,峰值是否在真实合焦位置。
- 灵敏度:峰值位置附近评价值对位置的敏感程度,用峰值两侧的半高宽度衡量。
- 抗噪性:加上噪声后,峰值位置偏移多少像素/步长。
- 计算耗时:处理一张512×512图像的平均耗时。
以下是我在一张512×512显微图像序列上测得的归一化结果。
| 函数 | 半高宽度(sigma单位) | 峰值位置偏移(叠加2%高斯噪声) | 耗时(毫秒) |
|---|---|---|---|
| Brenner | 0.9 | 0.2 | 1.1 |
| SMD2 | 1.0 | 0.3 | 1.2 |
| EOG | 0.9 | 0.2 | 1.3 |
| Tenengrad | 0.8 | 0.2 | 1.6 |
| Laplacian | 0.8 | 0.5 | 1.5 |
| SMD | 1.1 | 0.3 | 1.0 |
| 灰度方差 | 1.4 | 0.5 | 1.2 |
| 信息熵 | 2.8 | 1.2 | 1.4 |
| FFT | 1.6 | 0.8 | 6.8 |
| DCT | 1.5 | 0.4 | 3.2 |
| Vollath | 1.2 | 0.3 | 2.4 |
耗时数据是在MATLAB R2023b、Intel i5-1240P处理器的笔记本上测得的,绝对值会随机器性能不同变化,但不同函数之间的相对差异有参考价值。从这个表可以明显看到梯度类函数的综合优势——半高宽度更小、耗时更低、峰值偏移也更小。频域类函数里DCT的性价比优于FFT,因为DCT是实数变换,计算量减半,且峰值偏移比FFT更小。
我的选型经验是:常规机器视觉对焦首选Tenengrad;目标纹理复杂、要求高重复精度的场景选Vollath;对实时性要求极高的选SMD或Brenner;频域法在图像有严重周期性纹理(电路板、晶圆)时再考虑使用,因为这类场景下梯度法容易受重复边缘干扰。
5. 叠加噪声后的表现差异
5.1 噪声仿真实验:谁最先扛不住
真实工业场景几乎没有无噪声图像。低光照下的CMOS噪声、传输中的椒盐噪声、照明波动引入的低频干扰,这些噪声直接决定了评价函数的可靠性边界。我做了一组压力测试:在4.1节的图像序列基础上分别叠加高斯白噪声(方差0.02)、椒盐噪声(密度0.01)和光照渐变干扰(模拟照明不均匀),重新跑11种评价函数。
测试结果可以分成三梯队。第一梯队是Tenengrad、Vollath、DCT,峰值位置偏移在0.2~0.4个sigma单位之间,曲线依然保持单峰,工程上可以直接使用。第二梯队是Brenner、EOG、SMD2、SMD,峰值偏移在0.5个sigma单位左右,仍然可用,但曲线尾部开始出现波动。第三梯队是Laplacian、灰度方差、FFT、信息熵,峰值偏移超过1个sigma单位——Laplacian被噪声放大是因为它是二阶微分算子,对孤立噪声点会产生极尖锐的响应;信息熵被噪声欺骗是因为噪声颗粒会让灰度直方图每个bin都有分布,直方图变得“均匀”,熵值虚高。
5.2 工程上的抗噪操作手段
单纯在选型上规避噪声是第一步,工程上还有三招可以组合使用。
第一招是ROI限定——永远不要让评价函数作用于整幅图像。选择工作台上目标工件所在的矩形区域,能极大减少背景噪声对评价值的稀释。实际操作中,我习惯先用灰度阈值或者目标检测算法确定ROI,再用评价函数只计算ROI内的梯度。
第二招是先对图像做轻量级平滑再评价。对Laplacian、熵这类噪声敏感函数,预先使用3×3均值滤波或者5×5高斯滤波(sigma=1)能显著提升稳定性,代价是局部灵敏度下降。噪声不重的场景可以不做这一步,噪声重的场景不做这一步基本没法用。
第三招是时间维度的多次平均。如果对焦目标处于静止状态,可以连续采集3~5帧图像计算评价函数值后取平均。单帧图像的评价曲线在峰值位置附近可能会出现±1步的抖动,多帧平均能把这个抖动压下去。代价是对焦时间变成原来的3~5倍,在节拍要求高的产线上要权衡使用。
6. 把评价函数装进自动对焦搜索
6.1 搜索策略:先全局扫描,再局部爬山
写好评价函数之后,面对的下一件事是搜索策略设计。我认为最好的工程策略是“先粗后精”:第一步用大步长从头到尾扫描一遍,画出完整的评价曲线;第二步锁定峰值附近的区域,换成小步长做精细搜索,确定最佳对焦位置。这个策略的优势是既避免陷入局部极值,又避免了全范围小步长搜索的时间浪费。
之所以不推荐纯爬山法直接起步,是因为评价函数曲线在远离峰值的位置可能存在波动。工业现场的照明变化、机械振动,都会制造假的局部峰值,纯爬山法如果起点离真实峰值太远,很容易被假峰值带偏。先全局扫描找到一个大致的“候选区”,再用爬山法精修,这样容错率高得多。
6.2 MATLAB实现一版爬山法搜索
下面给出的是我常用的一种爬山法实现,可以无缝接入上一节的评价函数。
function [peak_pos, peak_val, curve] = focus_search(eval_func, z_start, z_end, init_step, refine_step) % 先粗扫后精修的两阶段对焦搜索 % eval_func: 评价函数句柄,输入z位置索引时的图像,输出清晰度值 % z_start, z_end: 对焦搜索区间 % init_step: 粗扫步长 % refine_step: 精修步长 % 第一阶段:粗扫 coarse_z = z_start:init_step:z_end; coarse_val = zeros(size(coarse_z)); for i = 1:length(coarse_z) img = capture_image_at(coarse_z(i)); % 你需要在工程里实现这个采集函数 coarse_val(i) = eval_func(img); end % 粗扫峰值位置 [~, idx] = max(coarse_val); coarse_peak_z = coarse_z(idx); % 第二阶段:在粗扫峰值附近精修 refine_range = coarse_peak_z - init_step : refine_step : coarse_peak_z + init_step; refine_val = zeros(size(refine_range)); for i = 1:length(refine_range) img = capture_image_at(refine_range(i)); refine_val(i) = eval_func(img); end [peak_val, idx_refine] = max(refine_val); peak_pos = refine_range(idx_refine); curve.z = refine_range; curve.val = refine_val; end这段代码里capture_image_at在MATLAB工程环境里对应的是snapshot(cam)配合运动控制卡的定位指令。在纯仿真阶段,你可以改用高斯模糊序列的索引来模拟,把实现和算法逻辑分开验证。两阶段搜索的关键参数是init_step和refine_step的比例,我一般取5~10倍,比如粗扫10步长、精修1步长。步长比例太小,精修范围覆盖不了粗扫判断的偏差;比例太大,精修阶段要多算很多张图,节拍时间变长。
6.3 实时性瓶颈到底在哪里
一个容易忽略的现实是:评价函数本身的耗时往往不是对焦总耗时的瓶颈。瓶颈在图像采集时间、运动控制响应时间和触发延迟。以工业相机为例,一张1080p图像的采集加上传输通常要10~30毫秒,这比任何梯度类评价函数(约1~3毫秒)高一个数量级。
所以优化对焦速度的重心应该放在三处:减少采集帧数(这需要更好的搜索策略)、使用硬件触发同步采图(避免软件触发带来的时间抖动)、在采集下一帧的同时并行计算当前帧的评价值(流水线操作)。评价函数代码层面一点小优化其实影响不大——除非你用了FFT这类重量级变换。这也是我推荐梯度类函数作为默认方案的另一个原因。
7. 工程化踩坑记录
7.1 边界填充这个小坑就能毁掉整条评价曲线
有一次我用Laplacian函数做显微载玻片的自动对焦,评价曲线在某个对焦位置出现一个莫名其妙的尖峰,反复排查找不到原因。后来把评价函数的中间结果可视化,发现尖峰位置的图像在视野边缘有一圈过亮的背景,而imfilter默认的零填充让边缘像素和零值之间产生了巨大的虚拟梯度,Laplacian二阶微分对这个虚拟边缘响应极强。把填充方式改成replicate之后尖峰立刻消失。
这个案例的教训是:使用卷积模板类评价函数时,填充方式不是细节问题,而是决定结果正确性的关键参数。所有用imfilter的地方都是如此,不只是Laplacian,Tenengrad也同样受影响。
7.2 归一化不是可有可无的后处理
评价函数的原始输出值范围差异极大:Brenner的输出可能是几百,信息熵的输出在5~8之间,FFT占比在0~1之间。如果直接拿这些原始值做多函数对比,或者在不同光照条件下跟踪同一函数的变化,很容易得出错误结论。我一般的做法是在一个稳定的参考图像上做一次校准,把各函数的输出归一化到0~1区间,并且记录下参考图像的直方图和光照参数,这样即使光照波动,也能保持评价值在同一尺度上比较。
归一化在自动对焦里还有一个深层作用:对焦搜索结果往往要设定一个“合焦判定阈值”,在归一化坐标系下设定阈值,语义更清晰——比如归一化值达到0.95以上判定为合焦,这个阈值在不同机台间可以迁移。
7.3 亮面反光工件:很多评价函数的“天敌”
最后分享一个容易让新手抓狂的场景。表面光滑的金属件、玻璃片、覆膜泡罩包装这类反光工件,在不同对焦位置上会呈现完全不同的反光图案——照亮区域跟着镜头角度走,评价函数评分也随之剧烈波动。我遇到过用Tenengrad做金属铭牌对焦时,离焦位置的反光边缘清晰度评分比真正合焦位置还高的情况。
面对这类工件,我的经验是两条腿走路:一方面在照明上想办法,用低角度照明或漫射光源抑制镜面反射;另一方面在算法上拉大抗噪处理力度,用Vollath或DCT这类对强度变化不敏感的函数,并配合粗糙度较大的搜索步长做全局扫描。真实项目里,如果工件反光不解决,换任何评价函数都只能治标,照明方案才是治本的关键。
回到开头的问题——什么算“清晰”?在自动对焦的语境里,答案取决于你的工件、光照和速度要求。11种函数都能回答“清晰”,只是回答的姿势不一样。代码都在这里了,我建议你拿自己的图像序列跑一遍,画一条曲线看看:函数和你的场景是否合拍,一眼就能判断。