
做自动对焦或者机器视觉的朋友应该都遇到过这个需求从一组不同离焦程度的图像里自动判断哪一张最清晰。表面上这是一个“选照片”的问题但往底层拆它其实是一个“如何给图像清晰度打分”的问题——这个打分的函数就是图像清晰度评价函数。它被用在显微镜载物台对焦、工业相机检测、扫描仪调焦甚至手机摄像头的辅助对焦逻辑里。这篇文章我会把我常用的11种评价函数一次性整理出来附上可直接运行的MATLAB代码再用一组离焦序列图实测对比。不需要你有多深的理论基础只要会基本的MATLAB语法照着代码跑一遍就能看到效果。1. 图像清晰度评价函数到底在评什么1.1 为什么不用“边缘检测”直接当评价指标很多人第一次接触清晰度评价第一反应是“检测边缘”。但实际做自动对焦项目时会发现边缘检测的结果是一张二维图像你得想尽办法把它压缩成一个数值而且这个数值要满足“越清晰越大”的特性。于是大家自然想到清晰图像边缘锐利、对比度高那就把这种“锐利程度”累计成一个标量。图像清晰度评价函数的基本思想就是这个——把二维图像里的高频信息含量压缩成一个标量。但问题来了怎样的统计方式才算好不同应用的口味不一样。比如显微镜下的细胞图像背景干净、目标边缘强用梯度类函数就很好工业相机拍摄纹理丰富的金属表面噪声可能很大这时候用熵或者方差这类对噪声不那么敏感的函数反而更稳。所以清晰度评价函数不是越复杂越好而是越贴合你的图像特征越好。1.2 好用的评价函数至少有四个特征第一个特征单峰性。从模糊到清晰再到模糊的序列中评价曲线只能有一个明显的峰值而且峰值正好落在最清晰的位置。第二个特征单调性峰值两侧应该平滑上升、平滑下降不能出现很多局部抖动。第三个特征灵敏度峰值附近越尖锐越好否则对焦搜索算法很难定位准确。第四个特征计算量嵌入式设备上每帧都要跑一遍函数太复杂会拖垮帧率甚至导致电机来回震荡。我的习惯是先看应用场景和计算预算再从这11种里挑1-2种做验证。没有一种函数是万能的11种都摆出来是让你有足够的选择空间而不是让你背公式。2. 11种评价函数逐个拆解原理与MATLAB代码2.1 梯度类函数从Brenner到EOG为什么梯度类是主流因为清晰图像的相邻像素灰度变化剧烈而模糊图像相当于做了一次平滑相邻像素差异变小。用一句话概括梯度函数衡量的是图像里“灰度跳变”的总强度。下面这7个函数全部基于这个思想只是细节不同。Brenner函数Brenner是我个人最喜欢用的一个它相隔两个像素做差分再平方。为什么间隔两个像素而不是一个因为隔一个像素时单像素噪点会被明显放大而隔两个像素能适当抑制部分高频噪声同时对轻微离焦更敏感。代码非常简单function F Brenner(I) I double(I); [M, N] size(I); % 水平方向第1列与第3列、第2列与第4列...做差 diffX I(:, 1:N-2) - I(:, 3:N); % 垂直方向第1行与第3行、第2行与第4行...做差 diffY I(1:M-2, :) - I(3:M, :); F sum(diffX(:).^2) sum(diffY(:).^2); endSMD函数灰度差分SMD的全称是Sum Modulus Difference中文一般翻译成灰度差分函数。它统计的是水平方向和垂直方向相邻像素灰度差的绝对值之和。绝对值的好处是计算快坏处是对噪声比较敏感因为单个噪点就会产生一个很大的差值直接累加进去。在粗对焦阶段用它扫一遍完全够用function F SMD(I) I double(I); diffX diff(I, 1, 2); % 水平相邻差分 diffY diff(I, 1, 1); % 垂直相邻差分 F sum(abs(diffX(:))) sum(abs(diffY(:))); endSMD2函数改进灰度差分SMD2经常会被人误以为是“SMD的平方版”其实在很多文献里它用的是“水平绝对差分 × 垂直绝对差分”的和。为什么用乘法而不是加法因为真正的边缘点通常在水平和垂直两个方向都有梯度变化而孤立的噪点往往只在某一个方向突出。相乘相当于对“真正的边缘”额外加权对孤立噪点有一定抑制作用。实现的时候需要特别注意尺寸对齐水平差分结果是M×(N-1)垂直差分结果是(M-1)×N要做逐像素乘法必须先取公共区域function F SMD2(I) I double(I); gx abs(I(:, 2:end) - I(:, 1:end-1)); % 水平梯度尺寸 M×(N-1) gy abs(I(2:end, :) - I(1:end-1, :)); % 垂直梯度尺寸 (M-1)×N gx gx(1:end-1, :); % 去掉最后一行公共区域 gy gy(:, 1:end-1); % 去掉最后一列公共区域 F sum(gx(:) .* gy(:)); endRoberts函数Roberts用的是2×2交叉差分它关注的是对角线方向的灰度变化。为什么关心对角线因为实际图像里的边缘不一定都是水平或垂直的斜向边缘占了很大比例。Roberts算子的计算量极小只涉及相邻像素的加减适合对计算资源抠得很紧的场景。function F Roberts(I) I double(I); [M, N] size(I); % 两条对角线方向做差分 g1 I(1:M-1, 1:N-1) - I(2:M, 2:N); g2 I(2:M, 1:N-1) - I(1:M-1, 2:N); F sum(g1(:).^2) sum(g2(:).^2); endTenengrad函数Tenengrad是很多工业场景的默认选项它用Sobel算子分别计算水平和垂直梯度然后求平方和。Sobel算子自带一个3×3的平滑权重所以它对噪声的容忍度比SMD、Roberts要好不少。工程上如果不想折腾先试Tenengrad大部分情况下结果都不会太差。function F Tenengrad(I) I double(I); % Sobel水平、垂直模板 Gx conv2(I, [-1 0 1; -2 0 2; -1 0 1], same); Gy conv2(I, [-1 -2 -1; 0 0 0; 1 2 1], same); F sum(Gx(:).^2) sum(Gy(:).^2); endLaplacian函数Laplacian是二阶微分算子。为什么一阶还不够还要上二阶因为一阶微分只能描述梯度大小二阶微分描述的是梯度变化率。模糊图像的边缘过渡带很宽梯度是缓慢变化的二阶响应衰减得更快理论上对离焦更敏感。不过二阶算子很容易放大噪声这是它最大的短板用之前最好先确认你的图像信噪比够不够。function F Laplacian(I) I double(I); % 4邻域拉普拉斯模板 L [0 -1 0; -1 4 -1; 0 -1 0]; Lap conv2(I, L, same); F sum(Lap(:).^2); endEOG函数能量梯度EOG全称Energy of Gradient能量梯度函数。它和SMD2容易搞混但这里必须把两者区分清楚EOG是水平方向差分平方 垂直方向差分平方SMD2是水平差分绝对值 × 垂直差分绝对值。EOG用平方代替绝对值会让大梯度贡献进一步放大图像越清晰边缘处梯度越大平方操作带来的增益就越明显评价曲线会变得更尖锐。function F EOG(I) I double(I); diffX diff(I, 1, 2); diffY diff(I, 1, 1); F sum(diffX(:).^2) sum(diffY(:).^2); end2.2 统计类与信息熵类方差和熵灰度方差函数为什么灰度方差能衡量清晰度因为清晰图像的灰度层次多、动态范围大方差自然大模糊图像各像素灰度趋向于一个平均值方差变小。这个指标计算量极低一遍求和就能算完而且对噪声相对不敏感所以在实时性要求很高的项目里经常被当成首选。不过它有个问题如果画面里有大面积的纯色区域比如背景天空方差的变化会很不明显这时候单靠方差容易区分不出细微的离焦差异。function F Variance(I) I double(I); mu mean(I(:)); F sum((I(:) - mu).^2); end信息熵函数信息熵的直觉很简单信息熵越大说明灰度分布越均匀、信息量越大。清晰图像细节多灰度直方图铺得比较开熵大模糊图像灰度集中在少数几个值上熵小。熵函数对低对比度图像的表现往往比梯度类函数好因为它统计的是整体分布而不是局部差分。需要注意灰度级数会对熵的计算影响很大。如果输入图像的灰度范围很窄比如只有50~80直接计算熵几乎看不出差异。所以我在代码里先把灰度重新映射到0~255再统计直方图这样曲线变化明显得多。function F EntropyMeasure(I) I double(I); I round(I * 255); % 映射到 0~255 p hist(I(:), 0:255) eps; % 直方图加eps防止log(0) p p / sum(p); F -sum(p .* log2(p)); end2.3 频域函数FFT高频能量图像经过傅里叶变换后能量主要集中在低频细节信息集中在高频。离焦在频域上等价于低通滤波高频成分会明显衰减。所以直接用FFT统计高频区域的能量也是一种很经典的清晰度评价方式。这里有个经验值阈值半径R0取图像短边的0.35倍。为什么是0.35这其实是“丢掉中心低频、留下外围高频”的一个常用折中。R0太小会把大量低频能量算进来曲线不够敏感R0太大又只留下极少数高频点抗噪性变差。你自己做实验时可以从0.25调起观察曲线形态再微调。function F FFTEnergy(I) I double(I); [M, N] size(I); Fimg fftshift(fft2(I)); % 把零频移到中心 [X, Y] meshgrid(1:N, 1:M); R sqrt((X - (N/21)).^2 (Y - (M/21)).^2); R0 0.35 * min(M, N); % 高频阈值半径经验值 highMask R R0; F sum(abs(Fimg(highMask)).^2); end使用FFT函数时建议先把所有待比较图像resize到相同尺寸再计算否则尺寸不同会导致频域网格不同评价值之间没有可比性。2.4 自相关类Vollath函数Vollath函数在中文资料里出现得不算多但实际工程效果不错。它的数学逻辑基于自相关清晰图像相邻像素之间的相关程度低因为边缘处灰度突变模糊图像相邻像素高度相关数值接近。Vollath函数通过“相邻像素乘积和”与“均值修正项”的差来度量这种相关性差异。实现上就是水平方向错位一行的像素乘积求和再减去按灰度均值构造的修正项function F Vollath4(I) I double(I); [M, N] size(I); mu mean(I(:)); term1 sum(sum(I(1:M-1, :) .* I(2:M, :))); term2 (M-1) * N * mu^2; F abs(term1 - term2); end代码里取绝对值是因为term1减term2可能出现负值。这个负号不影响极值位置的判断但为了让评价曲线统一成“越大越清晰”的趋势取绝对值更省心。2.5 一张表看完11种函数的适用场景函数名类别数学直觉噪声敏感性计算量典型场景Brenner梯度类隔像素差分平方中低显微镜对焦SMD梯度类相邻差分绝对值高低快速粗对焦SMD2梯度类水平×垂直差分中低纹理增强场景Roberts梯度类交叉梯度平方高低文本扫描Tenengrad梯度类Sobel梯度平方低中工业相机Laplacian梯度类二阶微分能量高中高精度对焦EOG梯度类双向差分平方高低通用粗对焦Variance统计类灰度离差平方和低很低实时系统Entropy信息论灰度熵低中低对比图像FFTEnergy频域高频能量低高科研分析Vollath4相关类自相关差值低低噪声敏感场景网格里看一圈你会发现梯度类函数的计算量普遍偏低适合做实时对焦而频域函数虽稳但计算量大更适合离线分析或者对精度要求极高的场合。3. 用离焦序列实测11种评价函数3.1 为什么用高斯模糊模拟离焦真实离焦过程很复杂涉及镜头光路、衍射、像差。但学术和工程上有一个通行做法用高斯模糊来近似离焦效果。因为高斯模糊是最接近理想低通滤波的简单模型用来评价“清晰度函数在模糊程度递增时的响应”是完全成立的。我用一个经典的cameraman.tif测试图对它施加从0到6逐步增大的高斯模糊sigma得到一个离焦序列sigma0代表最清晰sigma越大代表越模糊。理论上我们期望评价值在sigma0处最大之后单调下降。谁下降得越平滑、峰值越明显谁的对焦定位能力就越强。3.2 一键运行完整MATLAB测试脚本为了避免依赖Image Processing Toolbox我在这里自己写了一个高斯模糊函数gaussBlur用conv2实现整个脚本基本只需要基础MATLAB就能跑。11种评价函数文件按照上面的代码分别保存成同名.m文件然后运行下面的主脚本% test_clearness_functions.m % 用高斯模糊模拟离焦序列测试11种清晰度评价函数 clear; clc; close all; % 读取测试图换成你自己的灰度图也可以 img imread(cameraman.tif); img im2double(img); % 生成离焦序列sigma越大越模糊 sigmas 0:0.5:6; nSeq length(sigmas); imgSeq cell(1, nSeq); imgSeq{1} img; for k 2:nSeq imgSeq{k} gaussBlur(img, sigmas(k)); end % 函数句柄列表 funcs { (I) Brenner(I), Brenner (I) SMD(I), SMD (I) SMD2(I), SMD2 (I) Roberts(I), Roberts (I) Tenengrad(I), Tenengrad (I) Laplacian(I), Laplacian (I) EOG(I), EOG (I) Variance(I), Variance (I) EntropyMeasure(I), Entropy (I) FFTEnergy(I), FFT (I) Vollath4(I), Vollath4 }; % 计算每种函数在所有离焦序列上的评价值 nFuncs size(funcs, 1); scores zeros(nSeq, nFuncs); for fi 1:nFuncs for k 1:nSeq scores(k, fi) funcs{fi, 1}(imgSeq{k}); end end % 归一化并绘图 figure(Color, w); hold on; for fi 1:nFuncs s scores(:, fi); s_norm (s - min(s)) / (max(s) - min(s) eps); plot(sigmas, s_norm, LineWidth, 1.5, DisplayName, funcs{fi, 2}); end xlabel(Gaussian blur sigma (越大越模糊)); ylabel(归一化评价值); legend(Location, best); grid on; title(11种清晰度评价函数对比);高斯模糊函数gaussBlur单独保存成一个m文件function blurred gaussBlur(I, sigma) % 自实现高斯核卷积避免依赖Image Processing Toolbox win max(5, round(3*sigma)*2 1); [x, y] meshgrid(-(win-1)/2:(win-1)/2, -(win-1)/2:(win-1)/2); h exp(-(x.^2 y.^2) / (2*sigma^2)); h h / sum(h(:)); blurred conv2(I, h, same); end跑完这个脚本你会得到一张包含11条曲线的对比图。如果你只想看其中某几个函数把funcs列表里对应的行删掉就可以。3.3 结果怎么读数归一化与曲线形态先说一个隐藏大坑不同评价函数的量纲差异极大。方差可能是几千SMD可能是几十万FFT高频能量可能直接上亿。如果不做归一化就画在同一张图里大部分曲线会直接趴在底部只有量级最大的那一条能被看到。所以我在脚本里统一做了min-max归一化把每条曲线都压到0到1之间。归一化之后你可以从三个角度去读结果第一个看峰值位置。理论上所有曲线的峰值都应该在sigma0处也就是原图位置。如果某个函数的峰值跑到后面去了说明它的评价准则和你预期的“清晰”定义有偏差淘汰。第二个看下降速度。梯度类函数通常下降得很快说明对轻微离焦特别敏感熵和方差下降相对平缓说明它们在“差不多清晰”的图像上区分度没有梯度类高。第三个看曲线光滑度。如果曲线某个位置出现明显拐点或者毛刺先别怪函数去检查图像里是不是有周期性纹理。条纹、栅格这类图案会让梯度类函数产生局部次峰。我在实测中发现Brenner、EOG、Tenengrad这三条曲线的形态最接近“教科书式”的完美单调递减Laplacian在sigma较小时下降特别猛适合精细对焦但加大sigma后曲线容易进入平台区。Vollath4曲线不如梯度类平滑但峰值位置相对稳定。这些表现不是绝对结论换成你的业务图之后可能完全反转所以一定要拿自己的数据跑一遍。4. 工程落地经验与常见问题排查4.1 不同场景下的选型建议如果你的项目是显微镜自动对焦步进电机在Z轴往复扫描每次对焦时间要求很紧我建议优先考虑Brenner或者EOG。显微镜图像背景干净、目标边缘锐度高这两个函数计算快、曲线尖锐能快速锁定峰值。如果你的项目是工业流水线产品检测现场光照复杂、噪声大优先试Tenengrad。Sobel自带平滑权重对噪声的容忍度比SMD、Roberts好得多。如果Tenengrad的曲线仍然有很多毛刺试一下Vollath4或者Entropy它们从相关性和分布角度计算抗噪性更强。如果你的项目是对焦精度要求极高的医疗影像可以试试Laplacian和FFTEnergy。二阶微分和高频分析都能捕捉到很细微的离焦差异但代价是计算量大、对图像质量要求高必须保证输入图像没有明显噪声。否则高灵敏度反而会变成高误判。4.2 常见问题速查表现象可能原因解决办法评价曲线出现多个峰值图像里有周期性纹理或强噪声换Entropy、Vollath4或缩小ROI曲线太平缓峰值不明显图像对比度低、曝光不足固定曝光、增强对比度或改用FFT函数峰值位置偏移到模糊侧ROI包含太多无关背景裁剪后只看目标区域去掉大面积纯色块代码计算结果全为0uint8类型差分被截断计算前先转double评分曲线非单调、跳变严重拍摄过程自动曝光/自动白平衡固定相机参数用raw或关闭自动调节不同批次图像评分不可比图像尺寸、光照不一致统一resize、统一ROI、统一灰度归一化这里要特别提醒一下uint8的问题我用过不少学生代码都是栽在这。MATLAB里uint8是无符号类型两个像素相减如果出现负值比如5减去10结果不是-5而是0这会让大量差值被截断最终评分变成0或者严重失真。所以每个函数开头我都写了I double(I)不是多此一举是血泪教训。4.3 评价函数与对焦搜索算法的配合有了评价函数自动对焦还需要搜索策略。最常用的是爬山法先大步长粗搜索找到单峰区间再小步长精细搜索。也可以用黄金分割或斐波那契搜索这类算法收敛速度更快。评价函数曲线越尖锐搜索算法收敛越快曲线越平坦搜索越容易原地打转。所以如果你的搜索算法总是找不到准不一定是算法代码写错了很可能是评价函数选得太钝。这时候先换一个更尖锐的函数通常比优化搜索算法更见效。我个人在实际操作中的体会是不要试图找一个“万能”的清晰度评价函数然后一劳永逸。每个项目开始前我都会用文中这个测试脚本拿真实采集的离焦序列图像跑一遍把11条曲线画出来肉眼选出最顺眼的2到3个再做A/B测试。这个流程看着笨但比凭经验直接选函数可靠得多。最后再分享一个小技巧如果发现评价曲线有毛刺不要急着换函数先检查图像是否做了边缘padding、ROI里是不是混进了太多无关背景、相机曝光是否稳定。很多时候是预处理问题而不是函数本身的问题。清晰度评价函数是底层标尺标尺没选好上层的对焦算法再花哨也是白搭。这篇里的代码建议你拿自己的图跑一遍看哪几个函数的曲线形态最符合直觉再决定用哪几个上产线。