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 的累加和。

我的经验: 实际工程中,为了减少计算量,常用值近似:G = |Gx| + |Gy|。虽然精度略降,但对焦曲线形状几乎不变,速度能快 30% 左右。

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 就好了很多。

避坑指南: 如果图像信噪比低(比如暗光、高 ISO),慎用 Laplacian。它的二阶差分会加倍放大高频噪声,导致对焦曲线多峰甚至失效。

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 本章知识体系

下面这张图梳理了三种算子的关系和核心逻辑:

梯度类对焦评价函数知识体系 梯度类对焦评价函数 梯度 = 清晰度 Sobel 算子 Laplacian 算子 Tenengrad 函数 Sobel 特性 • 一阶导数 + 平滑 • 抗噪性较好 • 通用首选 Laplacian 特性 • 二阶导数,无方向 • 对噪声极敏感 • 边缘响应强 Tenengrad 特性 • Sobel 梯度平方和 • 曲线更尖锐 • 高精度场景 选型建议:Sobel → Tenengrad → Laplacian

三种算子的核心逻辑是一致的:用卷积核提取图像的高频信息,累加后作为清晰度分数。区别在于卷积核的设计和累加方式。理解了这一点,你就能根据实际场景灵活选型,甚至自己设计新的评价函数。