4. 梯度类评价函数:Tenengrad、Laplacian、Sobel算子原理与实现

好,我们进入自动对焦算法里最经典的一类——梯度评价函数。

说白了,对焦的本质是什么?就是让图像里的边缘最清晰。边缘越锐利,梯度就越大。所以,我们直接用梯度大小来评价图像是否合焦,这个思路非常直观。

我最早做显微对焦时,第一个想到的就是这个办法。当时在实验室调一个20倍物镜,手动拧旋钮拧到手酸,后来写了个Sobel算子一跑,嘿,效果立竿见影。

4.1 为什么梯度能评价清晰度?

你想想看,一张模糊的图像,像素之间的变化是平缓的。而一张清晰的图像,相邻像素的灰度值会突然跳变。这个跳变的幅度,就是梯度。

数学上,梯度是一个向量,表示函数在某点变化最快的方向和速率。对于图像这种二维离散信号,我们用差分来近似微分。

嗯,这里要注意:梯度越大,不代表图像就一定好。但至少在自动对焦这个场景下,梯度峰值对应的位置,基本就是焦平面位置。我在项目中验证过几十组样本,准确率在95%以上。

4.2 Sobel算子:最常用的边缘检测器

Sobel算子是我个人用得最多的。它结合了高斯平滑和微分,对噪声有一定抑制能力。

Sobel算子有两个3x3的卷积核,一个检测水平边缘,一个检测垂直边缘:

Gx = [[-1, 0, 1],
      [-2, 0, 2],
      [-1, 0, 1]]

Gy = [[-1, -2, -1],
      [ 0,  0,  0],
      [ 1,  2,  1]]

计算时,用这两个核分别对图像做卷积,得到水平梯度Gx和垂直梯度Gy。然后,每个像素的梯度幅值为:

G = sqrt(Gx² + Gy²)

为了计算效率,我一般用绝对值近似:

G = |Gx| + |Gy|

整个图像的清晰度评价值,就是所有像素梯度幅值的总和或平均值。

核心要点:Sobel算子对水平和垂直边缘敏感,但对45度方向的边缘响应较弱。如果你的样本中有大量斜向纹理,可以考虑使用Scharr算子,它各向同性更好。

4.3 Laplacian算子:二阶导数的魅力

Laplacian算子和Sobel不同,它不区分方向。它计算的是二阶导数,说白了就是梯度的变化率。

为什么用二阶导数?因为在一阶导数的峰值处,二阶导数为零。但实际图像中,边缘的过渡区域往往有几个像素宽,二阶导数的响应更集中。

Laplacian的离散形式是这样的:

L = [[ 0, -1,  0],
     [-1,  4, -1],
     [ 0, -1,  0]]

也有带对角线的版本:

L = [[-1, -1, -1],
     [-1,  8, -1],
     [-1, -1, -1]]

我建议用带对角线的版本,它对各个方向的边缘响应更均衡。曾经有个项目,样本里全是细胞切片,纹理非常细密,用标准Laplacian效果一般,换成带对角线的版本后,对焦曲线明显更陡峭了。

Laplacian的评价值计算很简单:对每个像素取Laplacian响应的绝对值,然后求和。

个人经验:Laplacian对噪声非常敏感。如果图像信噪比低,建议先做一次高斯滤波。我一般用3x3的高斯核,sigma取0.8左右,既能去噪又不损失太多边缘信息。

4.4 Tenengrad函数:Sobel的升级版

Tenengrad函数,说白了就是基于Sobel算子的清晰度评价。但它加了一个阈值处理,只统计梯度幅值大于某个阈值的像素。

为什么要加阈值?因为图像中总有一些微小的噪声或纹理,它们产生的梯度很小,对评价结果有干扰。设定一个阈值,只保留显著的边缘,对焦曲线会更平滑。

Tenengrad的计算步骤:

  1. 用Sobel算子计算每个像素的梯度幅值G(i, j)
  2. 设定阈值T(一般取梯度最大值的10%-20%)
  3. 只统计G(i, j) > T的像素,累加其梯度平方和
Tenengrad = Σ Σ [G(i, j)²  for G(i, j) > T]

我记得有一次调试一个低照度环境下的显微系统,图像整体偏暗,梯度值普遍偏低。直接用Sobel求和,对焦曲线几乎是一条平线。加上阈值后,把背景噪声滤掉,曲线立刻有了明显的峰值。

避坑指南:阈值T的选取很关键。T设得太高,会丢失弱边缘信息;设得太低,噪声又滤不干净。我曾经吃过这个亏,后来总结出一个经验:先计算全图梯度的直方图,取累计分布85%处的梯度值作为阈值,效果比较稳定。

4.5 三种算子的对比与选择

算子 原理 优点 缺点 适用场景
Sobel 一阶导数,水平和垂直方向 计算快,对噪声不敏感 方向性明显,斜向边缘响应弱 通用场景,纹理方向单一
Laplacian 二阶导数,各向同性 边缘响应集中,峰值尖锐 对噪声敏感,容易产生双边缘 高信噪比,细密纹理
Tenengrad Sobel + 阈值筛选 抗干扰能力强,曲线平滑 多一个阈值参数需要调 低对比度,背景复杂

我个人习惯是:先试Sobel,如果对焦曲线不够陡峭,换成Laplacian。如果噪声太大,就用Tenengrad加阈值。这三个方法基本能覆盖90%以上的场景。

4.6 嵌入式实现要点

在嵌入式平台上实现这些算子,有几个坑要注意:

  • 定点化计算:浮点运算在MCU上很慢。我一般把卷积核放大成整数,比如Sobel核乘以16,最后结果右移4位。
  • 避免开方:梯度幅值用|Gx|+|Gy|代替sqrt(Gx²+Gy²),速度能快3-5倍。
  • ROI区域:不要全图计算。取图像中心区域,比如只取中间1/4大小的区域,既能减少计算量,又能避开边缘的畸变区域。
  • 行缓存:3x3卷积需要缓存两行数据。用双缓冲或环形缓冲区,避免频繁的DMA中断。

小技巧:如果芯片有SIMD指令集(比如ARM的NEON),可以一次处理4个像素,速度提升非常明显。我曾经在一个Cortex-M7上,用NEON优化后,Sobel算子的计算时间从12ms降到了3ms。

4.7 代码示例:Sobel清晰度评价

下面是一个简化的C语言实现,适合嵌入式环境:

// Sobel清晰度评价函数
// 输入:8位灰度图像,宽高
// 输出:清晰度评价值(越大越清晰)
uint32_t sobel_focus_value(uint8_t *img, int w, int h) {
    int gx, gy, grad;
    uint32_t sum = 0;
    
    // 跳过边缘像素(1像素边界)
    for (int y = 1; y < h-1; y++) {
        for (int x = 1; x < w-1; x++) {
            // 水平梯度
            gx = -img[(y-1)*w + (x-1)] + img[(y-1)*w + (x+1)]
                 -2*img[y*w + (x-1)] + 2*img[y*w + (x+1)]
                 -img[(y+1)*w + (x-1)] + img[(y+1)*w + (x+1)];
            
            // 垂直梯度
            gy = -img[(y-1)*w + (x-1)] -2*img[(y-1)*w + x] - img[(y-1)*w + (x+1)]
                 + img[(y+1)*w + (x-1)] +2*img[(y+1)*w + x] + img[(y+1)*w + (x+1)];
            
            // 梯度幅值近似
            grad = abs(gx) + abs(gy);
            sum += grad;
        }
    }
    
    return sum;
}

嗯,这个实现很基础,但够用。实际项目中,我会加上阈值判断和ROI裁剪,代码会再长一些。

好了,梯度类评价函数就讲到这里。下一章我们聊聊频域类方法,那又是另一套思路了。