做自动对焦或者机器视觉的朋友,应该都遇到过这个需求:从一组不同离焦程度的图像里,自动判断哪一张最清晰。表面上这是一个“选照片”的问题,但往底层拆,它其实是一个“如何给图像清晰度打分”的问题——这个打分的函数,就是图像清晰度评价函数。它被用在显微镜载物台对焦、工业相机检测、扫描仪调焦,甚至手机摄像头的辅助对焦逻辑里。这篇文章我会把我常用的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/2+1)).^2 + (Y - (M/2+1)).^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,得到一个离焦序列:sigma=0代表最清晰,sigma越大代表越模糊。
理论上我们期望:评价值在sigma=0处最大,之后单调下降。谁下降得越平滑、峰值越明显,谁的对焦定位能力就越强。
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之间。
归一化之后,你可以从三个角度去读结果:
第一个看峰值位置。理论上所有曲线的峰值都应该在sigma=0处,也就是原图位置。如果某个函数的峰值跑到后面去了,说明它的评价准则和你预期的“清晰”定义有偏差,淘汰。
第二个看下降速度。梯度类函数通常下降得很快,说明对轻微离焦特别敏感;熵和方差下降相对平缓,说明它们在“差不多清晰”的图像上区分度没有梯度类高。
第三个看曲线光滑度。如果曲线某个位置出现明显拐点或者毛刺,先别怪函数,去检查图像里是不是有周期性纹理。条纹、栅格这类图案会让梯度类函数产生局部次峰。
我在实测中发现,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包含太多无关背景 | 裁剪后只看目标区域,去掉大面积纯色块 |
| 代码计算结果全为0 | uint8类型差分被截断 | 计算前先转double |
| 评分曲线非单调、跳变严重 | 拍摄过程自动曝光/自动白平衡 | 固定相机参数,用raw或关闭自动调节 |
| 不同批次图像评分不可比 | 图像尺寸、光照不一致 | 统一resize、统一ROI、统一灰度归一化 |
这里要特别提醒一下uint8的问题,我用过不少学生代码都是栽在这。MATLAB里uint8是无符号类型,两个像素相减如果出现负值,比如5减去10,结果不是-5而是0,这会让大量差值被截断,最终评分变成0或者严重失真。所以每个函数开头我都写了I = double(I),不是多此一举,是血泪教训。
4.3 评价函数与对焦搜索算法的配合
有了评价函数,自动对焦还需要搜索策略。最常用的是爬山法:先大步长粗搜索,找到单峰区间,再小步长精细搜索。也可以用黄金分割或斐波那契搜索,这类算法收敛速度更快。
评价函数曲线越尖锐,搜索算法收敛越快;曲线越平坦,搜索越容易原地打转。所以如果你的搜索算法总是找不到准,不一定是算法代码写错了,很可能是评价函数选得太钝。这时候先换一个更尖锐的函数,通常比优化搜索算法更见效。
我个人在实际操作中的体会是:不要试图找一个“万能”的清晰度评价函数,然后一劳永逸。每个项目开始前,我都会用文中这个测试脚本,拿真实采集的离焦序列图像跑一遍,把11条曲线画出来,肉眼选出最顺眼的2到3个,再做A/B测试。这个流程看着笨,但比凭经验直接选函数可靠得多。
最后再分享一个小技巧:如果发现评价曲线有毛刺,不要急着换函数,先检查图像是否做了边缘padding、ROI里是不是混进了太多无关背景、相机曝光是否稳定。很多时候是预处理问题,而不是函数本身的问题。清晰度评价函数是底层标尺,标尺没选好,上层的对焦算法再花哨也是白搭。这篇里的代码建议你拿自己的图跑一遍,看哪几个函数的曲线形态最符合直觉,再决定用哪几个上产线。