2. 图像清晰度评价函数:如何量化“清晰”这件事
自动对焦的核心,说白了就是回答一个问题:当前画面到底清不清晰?
你没法让相机自己“看”出画面好不好。你得给它一把尺子,让它去量。这把尺子,就是图像清晰度评价函数。
我刚开始做对焦算法时,觉得这步很简单——不就是算个梯度嘛。后来踩了不少坑才发现,选错评价函数,对焦系统就像近视眼没戴眼镜,怎么调都糊。
这一章,我把常用的评价函数分成三类来讲:基于梯度、基于频域、基于统计。每种我都会讲清楚原理、给出代码、再聊聊实际项目中的坑。
2.1 基于梯度的评价函数
这类方法最直观。你想,一张清晰的图像,边缘肯定锐利,像素值变化剧烈。模糊的图像呢?边缘像被抹了一层黄油,变化平缓。
梯度,就是衡量这种变化快慢的数学工具。
2.1.1 Brenner 函数
Brenner 是所有梯度方法里最简单的。它只计算相隔两个像素的差值,然后平方求和。
公式长这样:
F = Σ |f(x+2, y) - f(x, y)|²
为什么隔两个像素?我个人的理解是:它想跳过相邻像素的噪声干扰,直接抓取真正的边缘跳变。
代码实现也很短:
def brenner(img):
diff = img[:, 2:] - img[:, :-2]
return np.sum(diff ** 2)
2.1.2 Tenengrad 函数
Tenengrad 比 Brenner 讲究一些。它用 Sobel 算子提取水平和垂直方向的梯度,然后求平方和。
说白了,它考虑了两个方向的边缘信息,不像 Brenner 只盯着水平方向。
def tenengrad(img):
gx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
gy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
return np.sum(gx**2 + gy**2)
我记得有一次做显微镜自动对焦,Brenner 死活找不到峰值,换成 Tenengrad 一下就稳了。为什么?因为显微镜图像里有很多斜向纹理,Brenner 只抓水平方向,漏掉了关键信息。
2.1.3 Laplacian 函数
Laplacian 用的是二阶导数。它不直接看灰度变化有多快,而是看变化率的变化率。
你可以这么理解:一阶导数告诉你边缘在哪,二阶导数告诉你边缘有多“陡峭”。
def laplacian(img):
lap = cv2.Laplacian(img, cv2.CV_64F)
return np.var(lap) # 常用方差作为最终评价值
这里有个小技巧:很多人直接用 Laplacian 的值求和,但我习惯用方差。因为方差能更好地反映边缘的分布集中度,对焦峰值更尖锐。
| 方法 | 计算量 | 抗噪性 | 适用场景 |
|---|---|---|---|
| Brenner | 极低 | 中等 | 嵌入式、实时系统 |
| Tenengrad | 中等 | 较好 | 通用场景、显微镜 |
| Laplacian | 中等 | 较差 | 高对比度场景 |
2.2 基于频域的评价函数
梯度方法在空间域干活,频域方法则换个思路——把图像拆成不同频率的成分。
清晰图像的高频信息多(边缘、细节),模糊图像的高频信息少。所以,高频分量越强,图像越清晰。
2.2.1 FFT(快速傅里叶变换)
FFT 把图像从空间域变换到频率域。你会得到一个频谱图,中心是低频,往外是高频。
评价清晰度的方法很简单:计算高频区域的能量占比。
def fft_sharpness(img):
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
magnitude = np.abs(fshift)
# 定义高频区域:中心半径 30% 以外的部分
h, w = img.shape
cy, cx = h // 2, w // 2
mask = np.zeros((h, w), dtype=np.uint8)
cv2.circle(mask, (cx, cy), int(min(h, w) * 0.3), 1, -1)
high_freq = magnitude * (1 - mask)
return np.sum(high_freq) / np.sum(magnitude)
FFT 的优点是全局性好,不受局部纹理影响。缺点是计算量大,不适合实时系统。
2.3 基于统计的评价函数
这类方法不直接看边缘或频率,而是从图像的统计特性入手。
2.3.1 图像熵
熵,原本是信息论里的概念,用来衡量信息量。图像熵越大,说明灰度分布越均匀,细节越丰富。
模糊图像的灰度分布往往集中在几个值上,熵就小。清晰图像的灰度分布更散,熵就大。
def entropy(img):
hist = cv2.calcHist([img], [0], None, [256], [0, 256])
hist = hist / hist.sum()
return -np.sum(hist * np.log2(hist + 1e-7))
2.3.2 灰度方差
方差衡量的是像素值的离散程度。清晰图像对比度高,像素值差异大,方差自然大。
这个方法简单到有点“粗暴”,但有时候意外地好用。
def variance(img):
return np.var(img)
你想想看,一张纯色背景的照片,方差几乎为零,你根本没法用它来对焦。所以方差方法只适合纹理丰富的场景。
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 图像熵 | 信息量度量 | 对纹理敏感 | 受光照影响大 |
| 灰度方差 | 像素离散度 | 计算极快 | 依赖纹理丰富度 |
2.4 知识体系总览
下面这张图,我把三类评价函数的核心逻辑和适用场景画在了一起。你可以把它当作选型时的参考地图。
2.5 如何选择?我的实战建议
讲完理论,说点实在的。你在项目里到底该用哪个?
我一般按这个思路来:
- 先看硬件平台——如果是单片机,别想 FFT,老老实实用 Brenner 或方差。
- 再看场景纹理——纹理少?别用方差,试试熵或 Tenengrad。
- 最后看精度要求——要求高?用组合策略:粗调 + 精调。
嗯,这一章的内容就到这里。记住一句话:评价函数是自动对焦的“眼睛”,选对眼睛,路才能走对。