4. 噪声评估指标:PSNR、SSIM、LPIPS、噪声曲线(NPC)、信噪比(SNR)计算
做降噪这么多年,我最大的体会是:没有完美的指标,只有合适的场景。你辛辛苦苦调了一版降噪参数,肉眼看着挺干净,结果一跑PSNR反而降了——这种事我遇到过太多次了。
这一章,咱们就把几个主流评估指标掰开揉碎讲清楚。哪些指标看像素误差,哪些指标看人眼感知,哪些指标专门用来分析噪声本身。搞懂了这些,你才能知道自己的算法到底好在哪里、差在哪里。
4.1 PSNR —— 最经典的像素级指标
PSNR全称是峰值信噪比(Peak Signal-to-Noise Ratio)。说白了,它就是算两幅图像之间像素差了多少。数值越高,说明像素越接近。
公式长这样:
MSE = (1/m*n) * Σ(I - K)²
PSNR = 10 * log10(MAX² / MSE)
其中MAX是像素最大值,8bit图像就是255。
我个人习惯:PSNR低于30dB的降噪结果,基本不能看。30-35dB算及格,35-40dB算不错,40dB以上就非常好了。但注意——这只是经验值,不同场景差异很大。
4.2 SSIM —— 结构相似性,更接近人眼
SSIM(Structural Similarity Index)比PSNR聪明一点。它不光看像素差,还看亮度、对比度和结构三个维度的一致性。
公式拆开看:
SSIM(x,y) = [l(x,y)]^α * [c(x,y)]^β * [s(x,y)]^γ
其中:
l(x,y) = (2μxμy + C1) / (μx² + μy² + C1) // 亮度
c(x,y) = (2σxσy + C2) / (σx² + σy² + C2) // 对比度
s(x,y) = (σxy + C3) / (σxσy + C3) // 结构
SSIM的取值范围是0到1,越接近1越好。0.95以上说明结构保留得很好。
我在项目中遇到过这样的情况:PSNR涨了2dB,但SSIM反而掉了。一查原因,降噪算法把纹理细节磨平了,像素误差小了,但结构信息丢了。这时候SSIM比PSNR更诚实。
| 指标 | 关注点 | 取值范围 | 典型阈值 |
|---|---|---|---|
| PSNR | 像素级误差 | 0~∞ (dB) | >35dB 良好 |
| SSIM | 结构相似性 | 0~1 | >0.9 良好 |
| LPIPS | 感知相似性 | 0~∞ | <0.1 良好 |
4.3 LPIPS —— 用深度学习“看”图像质量
LPIPS(Learned Perceptual Image Patch Similarity)是近几年火起来的指标。它用预训练的CNN(比如AlexNet、VGG)提取特征,然后在特征空间里算距离。
为什么需要它?因为PSNR和SSIM在某些场景下会“骗人”。比如一张图稍微平移了一个像素,PSNR会暴跌,但人眼根本看不出来。LPIPS对这种微小位移更鲁棒。
代码实现很简单:
import lpips
import torch
# 加载预训练模型
loss_fn = lpips.LPIPS(net='alex')
# 假设img0和img1是归一化到[-1,1]的tensor
d = loss_fn(img0, img1)
print(f'LPIPS distance: {d.item():.4f}')
4.4 噪声曲线(NPC)—— 无参考场景下的利器
前面几个指标都需要ground truth。但实际项目中,你哪有那么多干净图?这时候就需要无参考指标了。
噪声曲线(Noise Profile Curve, NPC)是我在工业项目中用得最多的工具。它的思路很简单:把图像分成均匀区域,统计这些区域的方差随亮度的变化。
具体步骤:
- 将图像分割成N×N的小块(我一般用32×32)
- 计算每个块的均值和方差
- 按均值亮度分bin(比如0-255分成16个bin)
- 每个bin内取方差的中位数或均值
- 绘制亮度-方差曲线
噪声曲线能告诉你三件事:
- 噪声水平:曲线越高,噪声越大
- 噪声类型:曲线随亮度线性增长→高斯噪声;曲线在暗部翘起→读出噪声或暗电流噪声
- 降噪效果:降噪后的曲线应该整体压低,且不能出现异常尖峰
4.5 信噪比(SNR)计算
SNR在信号处理领域很基础,但在图像降噪里,计算方式有点讲究。
标准公式:
SNR = 10 * log10(信号功率 / 噪声功率)
问题来了:图像里的“信号”和“噪声”怎么分开?
我常用的两种方法:
- 均匀区域法:找一块均匀区域(比如灰卡),均值当信号,方差当噪声。适合实验室环境。
- 差分法:拍两张完全相同的场景,相减得到噪声图。适合有固定场景的测试。
实际代码:
import numpy as np
def compute_snr(image, noise_region):
"""
image: 整张图像
noise_region: 均匀区域的mask (bool数组)
"""
signal = np.mean(image[~noise_region])
noise_std = np.std(image[noise_region])
snr = 20 * np.log10(signal / noise_std)
return snr
4.6 指标组合使用策略
说了这么多,到底怎么用?我总结了一个组合策略:
| 场景 | 推荐组合 | 原因 |
|---|---|---|
| 有ground truth | PSNR + SSIM + LPIPS | 像素级+结构级+感知级全覆盖 |
| 无ground truth | 噪声曲线 + SNR + BRISQUE | 无参考场景下的黄金组合 |
| 算法调参 | 噪声曲线 + PSNR | 噪声曲线看噪声形态,PSNR看整体误差 |
| 最终验收 | 主观评测 + LPIPS | 人眼说了算,LPIPS辅助量化 |
嗯,指标这东西,用多了就有感觉了。刚开始你可能觉得PSNR就是一切,后来你会发现SSIM更靠谱,再后来你会意识到——真正的好算法,是让所有指标都“不差”,同时让肉眼觉得“舒服”。