2. 对焦评价函数(上):Tenengrad梯度函数、Laplacian算子、Sobel算子原理与代码实现
对焦评价函数,说白了就是给图像算一个“清晰度分数”。
你想想看,自动对焦的核心问题是什么?是判断镜头往哪个方向移动,画面才会更清晰。但“清晰”这个词太主观了,机器没法直接理解。所以我们需要一个数学工具,把“清晰度”量化成一个数值。
这个数值,就叫对焦评价函数值。
我个人习惯把这类函数分成三大类:梯度类、频域类、统计类。今天我们先啃最硬核、也最常用的——梯度类评价函数。
2.1 为什么梯度能衡量清晰度?
一张清晰的图像,边缘锐利,像素之间的灰度变化剧烈。一张模糊的图像,边缘被平滑了,相邻像素的灰度差异很小。
梯度,就是用来描述这种变化剧烈程度的。梯度越大,说明边缘越陡峭,图像越清晰。
我在项目中遇到过一个问题:用某款手机镜头拍纹理丰富的物体时,梯度值会异常高,但实际画面并不锐利。后来发现是传感器噪声被梯度算子放大了。嗯,这里要注意——梯度算子对噪声敏感,后面我们会聊怎么处理。
2.2 三种经典梯度算子
梯度算子的本质是一个卷积核。用这个核在图像上滑动,就能算出每个像素的梯度。
| 算子名称 | 核心思想 | 卷积核 | 特点 |
|---|---|---|---|
| Sobel | 一阶导数,带平滑 | 3×3,分水平和垂直 | 抗噪性较好,最常用 |
| Laplacian | 二阶导数,无方向 | 3×3,各向同性 | 对噪声极敏感,边缘响应强 |
| Tenengrad | Sobel 的平方和 | 基于 Sobel 计算 | 评价曲线更陡峭,区分度高 |
2.3 Sobel 算子:最实用的梯度工具
Sobel 算子是我在项目中用得最多的。它做了两件事:先平滑,再差分。平滑能抑制一部分噪声,差分能提取边缘。
Sobel 有两个核:
- 水平方向 Gx:检测垂直边缘
- 垂直方向 Gy:检测水平边缘
卷积核长这样:
Gx = [[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]
Gy = [[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]]
计算每个像素的梯度幅值:
G = sqrt(Gx² + Gy²)
整幅图像的 Sobel 评价值就是所有像素 G 的累加和。
2.4 Laplacian 算子:二阶导数的锐利
Laplacian 算子是二阶导数,它不区分方向,只关心灰度变化的“加速度”。
标准 3×3 核:
L = [[ 0, -1, 0],
[-1, 4, -1],
[ 0, -1, 0]]
还有一种带对角线的变体:
L2 = [[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]]
Laplacian 的响应比 Sobel 更强烈,但噪声也会被放大。我曾经在一个低光照场景下用 Laplacian 做对焦,结果评价曲线全是毛刺,根本找不到峰值。后来换成 Sobel 就好了很多。
2.5 Tenengrad 函数:Sobel 的升级版
Tenengrad 本质上还是基于 Sobel,但它不是简单累加梯度幅值,而是累加梯度幅值的平方。
Tenengrad = Σ (Gx² + Gy²)
为什么要平方?
- 平方运算放大了大梯度的贡献,让清晰和模糊的差距更明显
- 评价曲线的“峰”更尖锐,对焦精度更高
代价是计算量稍大,而且对噪声更敏感。不过现在嵌入式平台的算力普遍够用,Tenengrad 在高端相机和工业检测中很常见。
2.6 代码实现:从原理到实战
下面我用 Python + OpenCV 实现这三种算子。代码风格偏工程化,加了边界处理和归一化。
import cv2
import numpy as np
def sobel_focus_score(image):
"""Sobel 梯度评价函数"""
# 确保灰度图
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image
# 计算水平和垂直梯度
grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
# 梯度幅值
magnitude = np.sqrt(grad_x**2 + grad_y**2)
# 返回均值,便于不同尺寸图像比较
return np.mean(magnitude)
def laplacian_focus_score(image):
"""Laplacian 梯度评价函数"""
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image
# 使用标准 Laplacian 核
laplacian = cv2.Laplacian(gray, cv2.CV_64F, ksize=3)
return np.mean(np.abs(laplacian))
def tenengrad_focus_score(image):
"""Tenengrad 梯度评价函数"""
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image
grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
# 平方和,不取根号
tenengrad = grad_x**2 + grad_y**2
return np.mean(tenengrad)
# 使用示例
img = cv2.imread('test.jpg')
print(f"Sobel score: {sobel_focus_score(img):.4f}")
print(f"Laplacian score: {laplacian_focus_score(img):.4f}")
print(f"Tenengrad score: {tenengrad_focus_score(img):.4f}")
np.mean 而不是 np.sum。这样评价分数不受图像分辨率影响,方便不同尺寸的图像之间做对比。如果你做的是固定分辨率的对焦搜索,用 np.sum 也可以。
2.7 三种算子的对比与选型
我把三种算子的关键特性整理成了一张表,方便你根据场景选择:
| 特性 | Sobel | Laplacian | Tenengrad |
|---|---|---|---|
| 计算速度 | 快 | 最快 | 中等 |
| 抗噪能力 | 较好 | 差 | 中等 |
| 曲线尖锐度 | 中等 | 较尖锐 | 最尖锐 |
| 适用场景 | 通用,首选 | 高信噪比、快速粗对焦 | 高精度、工业检测 |
| 对焦精度 | 良好 | 一般(易受噪声干扰) | 优秀 |
我个人建议:新手先从 Sobel 入手。它足够稳定,能应对 80% 的场景。等你对焦系统跑通了,再换成 Tenengrad 提升精度。Laplacian 嘛……除非你确定图像噪声很低,否则别轻易用它做主评价函数。
2.8 本章知识体系
下面这张图梳理了三种算子的关系和核心逻辑:
三种算子的核心逻辑是一致的:用卷积核提取图像的高频信息,累加后作为清晰度分数。区别在于卷积核的设计和累加方式。理解了这一点,你就能根据实际场景灵活选型,甚至自己设计新的评价函数。