4. 图像清晰度评测:拉普拉斯方差、Brenner梯度、Tenengrad梯度、SMD(灰度方差)等算法原理与实现

图像清晰度评测,说白了就是判断一张图是「清楚」还是「模糊」。

我做了这么多年图像质量评测,发现很多同学一上来就上深度学习模型,其实没必要。传统方法在工业场景下又快又准,尤其是对焦、缺陷检测这些任务,传统梯度算法依然是首选。

今天我们就来聊聊四种最经典的清晰度评价算法:拉普拉斯方差、Brenner梯度、Tenengrad梯度、SMD(灰度方差)。

核心思想:清晰图像边缘锐利,像素间变化剧烈;模糊图像边缘平滑,像素间变化平缓。所有算法都在量化这个「变化程度」。

图像清晰度评测算法体系 清晰度评价 拉普拉斯方差 二阶微分算子 Brenner梯度 水平差分平方 Tenengrad梯度 Sobel算子 SMD灰度方差 相邻像素差 算法对比 计算量:中 计算量:小 计算量:大 计算量:小 抗噪性:弱 抗噪性:中 抗噪性:强 抗噪性:弱 适用:纹理丰富 适用:快速对焦 适用:通用场景 适用:简单场景

4.1 拉普拉斯方差(Laplacian Variance)

拉普拉斯算子是一个二阶微分算子。它不关心图像的整体亮度,只关注像素值的变化率。

为什么用二阶?因为一阶微分只能检测到边缘的存在,而二阶微分能区分「边缘的强度」。清晰图像的边缘更「陡峭」,二阶响应更大。

我的经验:拉普拉斯方差是我在工业检测项目中最常用的方法。它简单、稳定,对大多数场景都有效。但要注意——它对噪声非常敏感。我曾经在一个低光照场景下用它做自动对焦,结果噪声被放大成了「伪边缘」,导致对焦失败。

算法步骤:

  1. 将图像转为灰度图
  2. 用拉普拉斯核(通常是3x3)做卷积
  3. 计算卷积结果的方差

拉普拉斯核:

010
1-41
010
import cv2
import numpy as np

def laplacian_variance(image):
    """
    拉普拉斯方差清晰度评价
    值越大,图像越清晰
    """
    # 转灰度
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # 拉普拉斯卷积
    laplacian = cv2.Laplacian(gray, cv2.CV_64F)
    # 计算方差
    variance = laplacian.var()
    return variance

# 使用示例
img = cv2.imread('test.jpg')
score = laplacian_variance(img)
print(f'清晰度评分: {score:.2f}')

避坑指南:我曾经在纹理非常少的图像(比如纯色背景)上使用拉普拉斯方差,结果方差接近0,误判为模糊。后来我加了一个预处理步骤——先做直方图均衡化,增强对比度,效果好了很多。

4.2 Brenner梯度

Brenner梯度是最简单的清晰度评价方法之一。它只计算水平方向上相隔两个像素的灰度差平方。

你想想看,为什么是相隔两个像素?因为相邻像素的差异可能太小,相隔两个像素能放大边缘信号,同时抑制一些随机噪声。

def brenner_gradient(image):
    """
    Brenner梯度清晰度评价
    计算水平方向相隔2像素的灰度差平方和
    """
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = gray.astype(np.float32)
    
    # 水平方向相隔2像素的差分
    diff = gray[:, 2:] - gray[:, :-2]
    # 平方和
    score = np.sum(diff ** 2)
    return score

特点:

  • 计算量极小,适合嵌入式设备
  • 只考虑水平方向,对垂直纹理不敏感
  • 抗噪性一般

我的习惯:在做实时对焦系统时,我通常先用Brenner梯度做粗调,再用拉普拉斯方差做精调。这样既保证了速度,又保证了精度。

4.3 Tenengrad梯度

Tenengrad梯度使用Sobel算子计算水平和垂直方向的梯度。它比Brenner更全面,因为考虑了双方向。

说白了,Sobel算子就是带权重的差分。它给中心像素更高的权重,边缘像素更低的权重,这样能更好地抑制噪声。

def tenengrad_gradient(image):
    """
    Tenengrad梯度清晰度评价
    使用Sobel算子计算梯度幅值平方和
    """
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = gray.astype(np.float32)
    
    # Sobel x和y方向
    gx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
    gy = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
    
    # 梯度幅值
    mag = np.sqrt(gx ** 2 + gy ** 2)
    # 平方和作为清晰度评分
    score = np.sum(mag ** 2)
    return score
算法 计算量 抗噪性 适用场景
拉普拉斯方差 纹理丰富场景
Brenner梯度 快速对焦
Tenengrad梯度 通用场景
SMD灰度方差 简单场景

重要提示:Tenengrad梯度虽然效果好,但计算量大。我在一个4K视频的实时处理项目中,不得不把它降采样到720p再计算,否则帧率上不去。

4.4 SMD(灰度方差)

SMD全称是Sum of Modulus of Difference,也就是灰度差绝对值之和。它计算每个像素与相邻像素的灰度差绝对值,然后求和。

为什么用绝对值而不是平方?因为平方会放大大的差异,而绝对值对所有差异一视同仁。这在某些场景下更合理。

def smd_gradient(image):
    """
    SMD灰度方差清晰度评价
    计算相邻像素灰度差绝对值之和
    """
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = gray.astype(np.float32)
    
    # 水平方向差分绝对值
    diff_h = np.abs(gray[:, 1:] - gray[:, :-1])
    # 垂直方向差分绝对值
    diff_v = np.abs(gray[1:, :] - gray[:-1, :])
    
    # 求和
    score = np.sum(diff_h) + np.sum(diff_v)
    return score

避坑指南:我曾经在一个项目中用SMD做清晰度评价,结果发现评分总是偏高。排查了半天,发现是图像有轻微的条纹噪声,SMD把噪声也当成了「清晰度」。后来我加了一个中值滤波预处理,问题就解决了。

4.5 四种算法对比总结

嗯,到这里四种算法都讲完了。我做个简单的总结:

  • 追求速度:选Brenner或SMD,计算量小,适合嵌入式
  • 追求精度:选Tenengrad,抗噪性好,结果稳定
  • 追求平衡:选拉普拉斯方差,简单实用,但要注意噪声
  • 特殊场景:纹理极少的图像,先做预处理再评价

我记得有一次,一个同事问我:「为什么我用拉普拉斯方差评价一张模糊图,得分反而比清晰图高?」我一看,原来那张「模糊图」其实是运动模糊,但图像本身纹理非常丰富,拉普拉斯方差对纹理敏感,反而给出了高分。所以,没有万能的算法,理解算法的原理和局限,才能用好它。

我的建议:在实际项目中,不要只用一种算法。我会同时计算2-3种指标,然后综合判断。比如拉普拉斯方差+Brenner+Tenengrad,三个指标都高,那基本可以确定图像是清晰的。


公众号:蓝海资料掘金营,微信deep3321