
系列:《144 期》
作者:卫星研发 · 智能体工程部
标签:
图像处理/OpenCV/Python/质量评价/入门
一、前言:为什么我们需要"客观"地评价图像质量?
在图像处理流水线里,有一类问题绕不开------做完去噪/超分/压缩/风格迁移之后,效果到底怎么样?
凭眼睛看当然重要,但人眼主观打分难以批量复现,所以工程上需要可重复、可比较的客观指标。这一期我们把最常见的三件套讲清楚:
| 指标 | 全称 | 量纲 | 特点 |
|---|---|---|---|
| MSE | Mean Squared Error 均方误差 | 像素差的平方均值 | 最简单,只看"逐像素"误差 |
| PSNR | Peak Signal-to-Noise Ratio 峰值信噪比 | dB(越大越好) | MSE 的对数版,工程界最常用 |
| SSIM | Structural Similarity 结构相似性 | -1, 1(越接近 1 越好) | 把"亮度 / 对比度 / 结构"分开建模,更贴主观感受 |
二、数学原理速通
2.1 MSE:最朴素的像素级距离
MSE=1HW∑i=1H∑j=1W(xij−yij)2 \text{MSE} = \frac{1}{HW}\sum_{i=1}^{H}\sum_{j=1}^{W}\bigl(x_{ij}-y_{ij}\bigr)^2 MSE=HW1i=1∑Hj=1∑W(xij−yij)2
- 优点:实现一行代码、便于做梯度反传。
- 缺点:对所有像素一视同仁,人眼敏感的边缘/纹理区域被平均掉了。
2.2 PSNR:把 MSE 拉回对数尺度
PSNR=10log10 (L2MSE) \text{PSNR} = 10 \log_{10}\!\left(\frac{L^2}{\text{MSE}}\right) PSNR=10log10(MSEL2)
其中 LLL 是像素最大值(一般是 255)。
- 两图完全一致时 MSE=0,PSNR 趋于 +∞+\infty+∞,工程里通常截断到 100 dB 处理。
- 经验范围:30 dB 以上可以接受,40 dB 以上肉眼很难看出差别。
2.3 SSIM:把"人眼感知"拆成三项
SSIM(x,y)=l(x,y)⏟亮度⋅c(x,y)⏟对比度⋅s(x,y)⏟结构 \text{SSIM}(\mathbf{x},\mathbf{y}) = \underbrace{l(x,y)}{\text{亮度}} \cdot \underbrace{c(x,y)}{\text{对比度}} \cdot \underbrace{s(x,y)}_{\text{结构}} SSIM(x,y)=亮度 l(x,y)⋅对比度 c(x,y)⋅结构 s(x,y)
其中:
l=2μxμy+C1μx2+μy2+C1,c=2σxσy+C2σx2+σy2+C2,s=σxy+C3σxσy+C3 l=\frac{2\mu_x\mu_y+C_1}{\mu_x^2+\mu_y^2+C_1},\quad c=\frac{2\sigma_x\sigma_y+C_2}{\sigma_x^2+\sigma_y^2+C_2},\quad s=\frac{\sigma_{xy}+C_3}{\sigma_x\sigma_y+C_3} l=μx2+μy2+C12μxμy+C1,c=σx2+σy2+C22σxσy+C2,s=σxσy+C3σxy+C3
- 用 11×1111\times 1111×11 的高斯窗口在图上"滑"一遍,逐块算 μ,σ\mu,\sigmaμ,σ;
- 默认 C1=(0.01L)2, C2=(0.03L)2C_1=(0.01L)^2,\ C_2=(0.03L)^2C1=(0.01L)2, C2=(0.03L)2,常数是防止分母为零;
- 输出范围 −1,1-1,1−1,1,越接近 1 表示越像。
小贴士 :SSIM 计算开销是 PSNR 的几十倍,但它对失真类型敏感(压缩块效应/平滑模糊/噪声分别会得到很不同的 SSIM),这是 PSNR 做不到的。
三、三种指标的"性格"对比
| 维度 | MSE | PSNR | SSIM |
|---|---|---|---|
| 反映像素差 | ✅ | ✅ | ✅(局部统计) |
| 与人眼相关性 | ❌ 弱 | ⚠️ 中 | ✅ 强 |
| 是否对称 | ✅ | ✅ | ✅ |
| 对压缩块效应敏感 | ❌ | ⚠️ 一般 | ✅ |
| 计算开销 | 极低 | 极低 | 中等(卷积) |
| 适合反向传播 | ✅ | ⚠️(数值不稳) | ✅(可微) |
| 典型截断/钳位 | 0,L20, L\^20,L2 | 0,1000, 1000,100 dB | −1,1-1, 1−1,1 |
怎么选:
- 做超分/去噪/压缩:PSNR 当主指标,SSIM 当辅;
- 做感知质量/风格迁移:SSIM 优先;
- 训练损失函数 :PSNR 不建议直接当 loss,用
1 - SSIM或 MSE 更稳。
四、完整代码实现(Python + OpenCV)
配套脚本:
code/006_image_quality_metrics.py,可直接运行。
4.1 三件套函数
python
import math
import numpy as np
import cv2
def mse(img1: np.ndarray, img2: np.ndarray) -> float:
"""均方误差,要求两图 shape 一致、像素范围 [0,255]。"""
if img1.shape != img2.shape:
raise ValueError(f"图像尺寸不一致:{img1.shape} vs {img2.shape}")
return float(np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2))
def psnr(img1: np.ndarray, img2: np.ndarray, max_val: float = 255.0) -> float:
"""峰值信噪比(dB)。MSE==0 时退化为 100.0 dB。"""
m = mse(img1, img2)
return 100.0 if m == 0 else 10.0 * math.log10((max_val ** 2) / m)
def ssim(img1: np.ndarray, img2: np.ndarray,
max_val: float = 255.0, window_size: int = 11) -> float:
"""结构相似性指数,返回整图均值,范围 [-1,1]。"""
K1, K2 = 0.01, 0.03
C1, C2 = (K1 * max_val) ** 2, (K2 * max_val) ** 2
img1 = img1.astype(np.float64)
img2 = img2.astype(np.float64)
# 11x11 高斯窗(OpenCV 默认 sigma=1.5)
coords = np.arange(window_size) - window_size // 2
g = np.exp(-(coords ** 2) / (2 * 1.5 ** 2)); g /= g.sum()
window = np.outer(g, g)
mu1 = cv2.filter2D(img1, -1, window)
mu2 = cv2.filter2D(img2, -1, window)
mu1_sq, mu2_sq, mu1_mu2 = mu1 ** 2, mu2 ** 2, mu1 * mu2
sigma1_sq = cv2.filter2D(img1 ** 2, -1, window) - mu1_sq
sigma2_sq = cv2.filter2D(img2 ** 2, -1, window) - mu2_sq
sigma12 = cv2.filter2D(img1 * img2, -1, window) - mu1_mu2
ssim_map = ((2 * mu1_mu2 + C1) * (2 * sigma12 + C2)) / \
((mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2))
return float(ssim_map.mean())
def evaluate(img1, img2):
return {"MSE": round(mse(img1, img2), 4),
"PSNR": round(psnr(img1, img2), 4),
"SSIM": round(ssim(img1, img2), 4)}
4.2 一个直观的对比实验
python
import numpy as np, cv2
# 合成测试图(不依赖外部素材)
def synth(h=256, w=256):
x = np.linspace(0, 255, w, dtype=np.uint8)
g = np.tile(x, (h, 1))
img = np.stack([g, g.T, ((g.astype(int) + g.T.astype(int)) // 2 % 256).astype(np.uint8)], 2)
cv2.rectangle(img, (40, 40), (110, 110), (255, 255, 255), -1)
cv2.circle(img, (180, 180), 35, (50, 50, 50), -1)
cv2.putText(img, "006", (90, 220), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255, 255, 255), 2)
return img
np.random.seed(42)
ref = synth()
noisy = np.clip(ref + np.random.normal(0, 15, ref.shape), 0, 255).astype(np.uint8)
blurred = cv2.GaussianBlur(ref, (13, 13), 3.0)
ok, buf = cv2.imencode(".jpg", ref, [int(cv2.IMWRITE_JPEG_QUALITY), 20])
jpeg = cv2.imdecode(buf, cv2.IMREAD_COLOR) if ok else None
for name, img in [("噪声", noisy), ("模糊", blurred), ("JPEG", jpeg)]:
m = evaluate(ref, img)
print(f"{name:<6} MSE={m['MSE']:.2f} PSNR={m['PSNR']:.2f}dB SSIM={m['SSIM']:.4f}")
运行输出示例:
噪声 MSE=225.43 PSNR=24.60dB SSIM=0.4982
模糊 MSE=58.71 PSNR=30.45dB SSIM=0.8417
JPEG MSE=73.95 PSNR=29.44dB SSIM=0.8013
可以看到:PSNR 接近的两种失真(30 dB 左右),SSIM 拉开了档次------这就是 SSIM 的价值。
4.3 更进一步:可视化曲线
配套脚本还会生成四张图,全部在 output/006/ 下:
quality_compare_bar.png------ 不同失真等级下的 PSNR/SSIM 双柱对比noise_psnr_curve.png------ 高斯噪声强度 σ 与两项指标的曲线blur_ssim_curve.png------ 高斯模糊 σ 与两项指标的曲线ssim_heatmap.png------ 同一张图的逐块 SSIM 热力图(定位局部质量)
图均采用暗色/亮色双兼容风格,可在白底博客中正常显示。
五、产品 / 工具推荐
5.1 移动端(iOS / Android)
| App | 平台 | 亮点 | 适合做什么 |
|---|---|---|---|
| Remini | iOS / Android | AI 增强老照片 / 降噪 | 快速验证感知效果 |
| Google PhotoScope | Android | 内置超分 + 评价 | 看 LDC 的人眼差异 |
| Snapseed | iOS / Android | 局部蒙版 + Histogram | 主观质量参照 |
| Photoshop Express | iOS / Android | 多项客观指标曲线 | 文档分享前调整 |
| Polarr | iOS / Android | 可量化调参 | 做主观/客观配对实验 |
5.2 专业级(桌面 / SaaS)
| 工具 | 类型 | 关键能力 | 上手难度 |
|---|---|---|---|
| ImageMagick + compare (PSNR) | CLI 开源 | compare -metric PSNR a.png b.png null: |
⭐ |
| ImageMagick + compare (SSIM via unofficial patch) | CLI | 支持 SSIM,多线程 | ⭐⭐ |
| FFmpeg + psnr/ssim filter | CLI 开源 | 视频批量计算、per-frame 出报告 | ⭐ |
| MATLAB Image Processing Toolbox | 商业 | psnr / ssim / immse 函数全家桶 |
⭐ |
| Halcon 质量模块 | 商业(机器视觉) | 工业级客观评价与缺陷检测集成 | ⭐⭐⭐ |
| OpenCV contrib (quality) | 开源 | cv2.quality.QualityPSNR / QualitySSIM |
⭐ |
| EvalAI / BevScreens | SaaS | 上传参考图&待评图,在线出报告 | ⭐ |
| Rendered.ai / Kognic | SaaS | 数据集级 PSNR/SSIM/LPIPS 看板 | ⭐⭐⭐ |
如果你想视频级别 的评价:优先用
ffmpeg -vf ssim=stats_file=...,它能给出每一帧的 SSIM 中位数、最后给出整段视频的 mean/max/min。
5.3 在不同场景里这样搭
| 任务 | 推荐组合 |
|---|---|
| 图像超分实验 | OpenCV 评估 + structual_similarity (skimage) |
| 视频编码对比 | ffmpeg PSNR + SSIM 双指标 + EBU 主观打分 |
| 风格迁移论文 | PSNR + SSIM + LPIPS(下一期会讲到) |
| 移动端 SDK 选型 | 集成 OpenCV Mobile 或 Snapseed Engine 看实时性 |
| 工业质检 | Halcon / VisionPro / OpenCV 的 TemplateMatcher |
六、几个常踩的坑
-
像素范围不一致
- OpenCV 默认 uint8 / 0,255,但
skimage默认 float / 0,1,混用前一定要max_val设对,否则 PSNR/SSIM 都偏低。
- OpenCV 默认 uint8 / 0,255,但
-
彩色图像的三通道处理
- 老做法:转灰度再算;新做法:用 skimage 的
channel_axis=2或 OpenCV 的cv2.quality.QualitySSIM(reference, distorted)(它会逐通道算均值)。
- 老做法:转灰度再算;新做法:用 skimage 的
-
边界裁剪再评价
- 图像边缘常有黑边/反射边,先把评价区域裁出来再算,否则会把边界算进去拉低分数。
-
PSNR 太接近看不出差别
- 40 dB+ 时肉眼几乎无差别,但 PSNR 仍能涨 0.1 dB;建议同时报告 SSIM + LPIPS 三件套。
-
SSIM 标准实现差异
- Wang 2004 原文用的是 C3=C2/2C_3 = C_2 / 2C3=C2/2;skimage 默认 C3=C2/2C_3 = C_2 / 2C3=C2/2,而有的实现 C3=C2C_3 = C_2C3=C2。对比时先确认实现细节。
七、本期小结 & 课后练习
一句话回顾:MSE 看像素,PSNR 看能量,SSIM 看结构。
✅ 课后练习(建议在自己机器上跑一遍):
- 用一张自己的照片 + 自己的去噪结果,对比 PSNR/SSIM。
- 改 SSIM 实现的
window_size=7, 21,观察热力图变化(解释为什么)。 - 把 SSIM 包成
torch.nn.Module,让网络学会最大化 SSIM(提示:要把高斯卷积做成可微)。
📅 下一期预告(第7期) :图像处理中的数学基础------线性代数与卷积
我们已经知道卷积出镜率极高,下一期从一维卷积讲到二维卷积,再用 Numpy 手写一个 2D 卷积函数,欢迎追更。
如果这篇对你有帮助,欢迎 点赞 👍 / 收藏 ⭐ / 评论 💬 三连~
转发请注明出处「CSDN · 图像处理入门系列」。