数字图像处理
2.6 图像质量评价
2.6.1 图像质量评价概述
图像质量评价
图像质量评价(image quality assessment, IQA)方法有:👇
- 主观评价:以人作为观测者 ,对图像进行主观评价,力求能够真实地反映人的视觉感知。
- 客观评价: 无人参与 ,是借助于某种数学模型,给出基于数字计算的结果来反映图像质量。
2.6.1 主观评价
主观评价是建立在统计学基础上的,其基本思想是:通过对图像的感知,从感知角度出发,对图像进行评价。分为2种:绝对评价和相对评价。
绝对评价
将 待评价图像 和 原始参考图像 按一定规则交替播放持续一定时间给观察者,然后在播放后留出一定的时间间隔供观察者打分,最后将所有给出的分数取平均作为该序列的评价值。
国际上的评分制:“全优度尺度”

相对评价
相对评价中 没有原始图像 作为参考。将一批待评价图像按照一定的序列播放,此时观察者在观看图像的同时给出待评图像相应的评价分值。
评分制度称为 群优度尺度

2.6.2 客观评价

全参考图像质量评价指标:
- 均方误差MSE(mean square error),
- 信噪比SNR(Signal to Noise Ratio),
- 峰值信噪比PSNR(Peak Signal to Noise Ratio)
- 平均绝对误差MAE(Mean Absolute Error)
- 结构相似性SSIM(structural similarity)
2.6.2 图像质量评价指标
前言
1.MSE均方误差
均方误差MSE(mean square error) 通过计算评价图像与参考图像之间的像素点的灰度值之间的差异平方和,从统计的角度衡量待评价图像的质量优劣。


f为待评价图片,f'为参考图片 MxN为图片的像素点数,
理论上: MSE越小,图像质量越接近参考图像
存在的问题: 👇
问题1:与感知不符。一个图像稍微平移几个像素,MSE值可能会非常高,但人眼可能几乎看不出区别。反之,在某些区域微小的、集中的失真可能MSE不高,但人眼会非常敏感(例如,一个明亮背景上的暗点)。
问题2:只考虑像素差异,忽略结构关系。MSE无法捕捉到边缘、纹理等结构信息的保留程度。
2.PSNR峰值信噪比
PSNR(Peak Signal-to-Noise Ratio,峰值信噪比) 是一个基于像素级误差的图像或视频质量客观评价指标。它的单位是分贝(dB)。
PSNR通过计算原始图像(参考图像) 和 失真图像(待评估图像) 之间的均方误差(MSE),并将其与图像信号可能的最大功率(即峰值)进行比较,最终得到一个以分贝为单位的比值。
核心思想很简单: 将代表“噪声”或“失真”的误差功率 与 代表“纯净信号”的最大可能功率进行比较。这个比值越高,说明噪声/失真相对于信号来说越微不足道,因此图像质量就越好。

4.SSIM结构相似性:亮度、对比度、结构
SSIM(Structural Similarity Index)是一种用于衡量两幅图像之间相似度的指标。与传统的基于误差的方法(如均方误差MSE、峰值信噪比PSNR)不同,SSIM认为人眼视觉系统(HVS)对图像结构信息的感知非常敏感,而不是对绝对误差敏感。
简单来说,SSIM试图模拟人眼感知图像质量的方式。它认为,像素之间亮度和结构的相互关系比独立的像素误差更重要。
SSIM并不是直接计算一个值,而是通过比较图像的三個关键属性来计算相似度:
- 亮度(Luminance)
- 对比度(Contrast)
- 结构(Structure)
公式如下:👇

最终,SSIM是这三个方面比较结果的组合。
SSIM值的含义👇
- SSIM的取值范围是 [-1, 1]。
- SSIM = 1:表示两幅图像完全一样。
- SSIM ≈ 0 或 SSIM < 0:表示两幅图像完全不相似,结构信息差异巨大。
在实际应用中,我们通常只关心 0 到 1 的范围。
利用skimage.metrics.structural_similarity()和mean_squared_error()函数,以及peak_signal_noise_ratio()函数,可以计算出MSE、PSNR和SSIM值。
案例:使用Python代码求下图的MSE、PSNR和SSIM值。

import numpy as np
from skimage.metrics import mean_squared_error, peak_signal_noise_ratio, structural_similarity
import matplotlib.pyplot as plt
# 设置中文字体
plt.rcParams['font.family'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 创建一个简单的5x5灰度矩阵作为原始图像
original = np.array([
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 2],
[1, 1, 180, 150, 120],
[110, 130, 150, 130, 110],
[100, 120, 130, 120, 100]
], dtype=np.uint8)
# 创建一个有噪声的版本作为压缩/失真图像
compressed = np.array([
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 2],
[1, 1, 181, 150, 121],
[112, 128, 152, 132, 108],
[102, 118, 128, 118, 102]
], dtype=np.uint8)
# 计算图像质量指标
mse = mean_squared_error(original, compressed)
psnr = peak_signal_noise_ratio(original, compressed, data_range=255)
# 对于小图像,显式指定窗口大小
# 5x5的图像可以使用3x3的窗口
ssim = structural_similarity(original, compressed,
win_size=3, # 指定窗口大小为3
data_range=255) # 指定数据范围为255
# 打印结果
print("原始矩阵:")
print(original)
print("\n压缩/失真矩阵:")
print(compressed)
print("\n图像质量评估结果:")
print(f"MSE (均方误差): {mse:.4f}")
print(f"PSNR (峰值信噪比): {psnr:.2f} dB")
print(f"SSIM (结构相似性): {ssim:.4f}")
# 可视化图像对比
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12, 4))
# 原始图像
im1 = ax1.imshow(original, cmap='gray', vmin=0, vmax=255)
ax1.set_title('原始图像')
ax1.axis('off')
plt.colorbar(im1, ax=ax1)
# 压缩图像
im2 = ax2.imshow(compressed, cmap='gray', vmin=0, vmax=255)
ax2.set_title('压缩/失真图像')
ax2.axis('off')
plt.colorbar(im2, ax=ax2)
# 差异图像
diff = original.astype(float) - compressed.astype(float)
im3 = ax3.imshow(diff, cmap='coolwarm', vmin=-20, vmax=20)
ax3.set_title('差异图像 (原始 - 压缩)')
ax3.axis('off')
plt.colorbar(im3, ax=ax3)
plt.tight_layout()
plt.show()
MSE (均方误差): 1.68
这是一个相对较小的值,说明两幅图像差异不大
主要差异来自几个像素点的微小变化
PSNR (峰值信噪比): 45.88 dB
PSNR > 40 dB 通常表示图像质量很好
这个值说明压缩图像与原始图像非常接近
在图像压缩中,PSNR > 30 dB 通常就认为质量可以接受
SSIM (结构相似性): 0.9996
SSIM 接近 1.0,说明两幅图像在结构上几乎完全相同
这个极高的值表明人眼几乎无法区分两幅图像的差异
总结
课堂作业
MSE, PSNR, SSIM 核心特性对比表 👇
| 特性 | MSE (均方误差) | PSNR (峰值信噪比) | SSIM (结构相似性指数) |
|---|---|---|---|
| 全称 | Mean Squared Error | Peak Signal-to-Noise Ratio | Structural Similarity Index |
| 核心思想 | 计算像素值之间的数学平方误差 | 基于MSE,计算信号与噪声的功率比 | 模拟人眼,比较亮度、对比度、结构的相似性 |
| 计算公式 | MSE = 1/n Σ(I - K)² | PSNR = 10·log₁₀(MAX²/MSE) | SSIM = [l(x,y)]·[c(x,y)]·[s(x,y)] |
| 指标类型 | 误差指标 (越小越好) | 质量指标 (越大越好) | 相似性指标 (越大越好) |
| 值域范围 | [0, +∞) | [0, +∞) dB | [-1, 1] (通常[0, 1]) |
| 感知一致性 | 非常差 | 差 | 良好 |
| 计算复杂度 | 极低 | 很低 | 中 |
| 主要优点 | 1. 计算简单快速 2. 意义明确 3. 可微,适合作为损失函数 | 1. 引入峰值信号,结果更直观 2. 应用广泛,是事实基准 3. 有单位(dB),便于比较 | 1. 更符合人眼主观感受 2. 能捕捉结构信息保留程度 3. 已成为新标准 |
| 主要缺点 | 1. 与主观感受严重脱节 2. 对所有误差一视同仁 3. 无法反映结构信息损失 | 1. 本质仍是MSE,继承其感知缺陷 2. 依赖MAX值设定 3. 对结构性失真不敏感 | 1. 计算相对复杂 2. 需要设置窗口和参数 3. 对某些失真类型可能不最优 |
| 一句话概括 | "数学上有多错?" | "信号比噪声强多少?" | "看起来有多像?" |
2.7 Python的图像处理编程基础
Python的图像处理编程基础
准备工作
Python图像处理工具包:👇
NumPy 是一个强大的Python库,主要用于科学计算,提供了大量的数学函数库,支持多维数组和矩阵运算,图像的本质是一个多维矩阵,因此NumPy是Python图像处理的基础库。
OpenCV 是一个开源的计算机视觉库,提供了大量的图像处理算法,支持图像的读取、显示、保存等操作,是Python图像处理的核心库。
PIL(Python Image Library) 是一个图像处理库,支持图像的读取、显示、保存等操作,是Python图像处理的核心库。
Scipy系列 是一个科学计算库,提供了大量的科学计算函数库,支持图像的傅里叶变换、图像的滤波等操作,是Python图像处理的核心库。
Matplotlib 是一个绘图库,便可以生成图表、直方图、功率谱、条形图、错误图、散点图等,是Python图像处理的核心库。
NumPy和Matplotlib是anaconda的常用工具包,如果安装了anaconda,则自动安装了这两个工具包
OpenCV的安装:
以管理员身份打开Anaconda Prompt,然后运行:
pip install --user opencv-python
案例实操
1. 用OpenCV函数读入一个灰度图,并显示图片,按下’s’键保存图像后再退出,按下ESC键则不保存图像退出。
img文件夹下有iris.jpg,将这个文件夹放到当前目录下,然后运行代码,按下s键保存图像,按下ESC键不保存图像退出。
# 用OPenCV打开、显示和保存图像
import cv2
img = cv2.imread(r'./img/iris.jpg' ,0) #参数0说明读入灰度图像,不填就是彩色图像
cv2.namedWindow("image",cv2.WINDOW_AUTOSIZE) #创建窗口 #创建窗口
cv2.imshow('image',img) #显示图像
k=cv2.waitKey(0) #等待按键
if k == 27: # wait for ESC key to exit
cv2.destroyAllWindows() #销毁窗口
elif k == ord('s'): # wait for 's' key to save and exit
cv2.imwrite('iris.png',img) #保存图像
cv2.destroyAllWindows()
3. 【例2-2】有时人们需要对一幅图像的特定区域进行操作。这时可以使用NumPy切片索引功能来提取图像中人们感兴趣ROI(Region Of Interest)区域。
# 图像ROI区域提取
import numpy as np
import cv2 #opencv
img_color = cv2.imread(r'./img/alphabet.jpg') #读入彩色图像
img_gray=cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)#将彩色图像转换为灰度图像
print(type(img_gray)) #打印输出所读入图像的类型
cv2.rectangle(img_color,(160,140),(190,170),(0,0,255),3) #openCV数组下标是[width,height,depth] #画出矩形
img_ROI=img_gray[140:170,160:190] #numpy数组的下标是[height,width,depth] #提取感兴趣区域
cv2.imshow("color image",img_color) #显示彩色图像
cv2.waitKey(0)#等待按键
cv2.imshow("ROI image",img_ROI)#显示感兴趣区域
cv2.waitKey(0)#等待按键
cv2.destroyAllWindows()#销毁窗口
2.【例2‑3】由OpenCV打开图片,然后由matplotlib显示并保存图片。
OpenCV读取的彩色图像是BGR模式,直接用Matplotib显示时,实际上是将蓝色通道当作红色通道、红色通道当作蓝色通道来显示
# 使用opencv打开图像,用matplotlib显示、保存图像
#opencv是BGR模式,而matplot是RGB模式
import cv2
import matplotlib.pyplot as plt # 导入matplotlib库
%matplotlib inline # 在jupyter中显示图像
%config InlinBackend.figure_format="retina" # 显示高清图像
plt.rcParams['font.family']=['SimHei'] #用来正常显示中文
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
img_BGR = cv2.imread(r'./img./iris.jpg')
img_RGB=cv2.cvtColor(img_BGR,cv2.COLOR_BGR2RGB)#将BGR模式转换为RGB模式
plt.figure()#创建一个空白图像
plt.subplot(121)#创建子图,1行2列,第一个子图
plt.axis("off")
plt.title('BGR模式的彩色图像')
plt.imshow(img_BGR)
plt.subplot(122)#创建子图,1行2列,第二个子图
plt.axis("off")
plt.title('RGB模式的彩色图像')
plt.imshow(img_RGB)
plt.savefig("BGR2RGB.jpg")
plt.show()
4. 使用OpenCV创建一张128×128的黑色图像,并在图像中央绘制一个56×77的白色矩形框,然后分析该图像的灰度直方图特征。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 创建128x128的黑底图像
image = np.zeros((128, 128), dtype=np.uint8)#创建一个128x128的全0矩阵,数据类型为uint8,即无符号8位整数
# 计算白色框的位置(居中)
white_width = 77
white_height = 56
start_x = (128 - white_width) // 2 # (128-77)/2 = 25.5 → 25
start_y = (128 - white_height) // 2 # (128-56)/2 = 36
end_x = start_x + white_width # 25+77=102
end_y = start_y + white_height # 36+56=92
cv2.rectangle(image, (start_x, start_y), (end_x, end_y), 255, -1)# rectagngle参数说明:(图像,左上角坐标,右下角坐标,颜色,线宽) 255是白色,-1是填充
# 显示
plt.figure(figsize=(10, 4))#创建一个空白图像,大小为10x4
plt.subplot(1, 2, 1)#创建子图,1行2列,第一个子图
plt.imshow(image, cmap='gray')#显示灰度图像,cmap='gray'表示灰度图像
plt.title(f'OpenCV创建\n128×128黑底, {white_width}×{white_height}白框') # 显示标题
plt.xlabel('x轴')
plt.ylabel('y轴')
plt.axis('off') # 关闭坐标轴
plt.subplot(1, 2, 2)#创建子图,1行2列,第二个子图
hist = cv2.calcHist([image], [0], None, [256], [0, 256])#计算灰度直方图,参数说明:(图像,通道,掩码,直方图大小,范围)
plt.bar(range(256), hist.flatten(), width=1, color='black')
plt.title('直方图')
plt.xlabel('灰度值')
plt.ylabel('数量')
plt.xlim(0, 255)
plt.tight_layout()
plt.show()
# 打印详细信息
black_pixels = np.sum(image == 0)
white_pixels = np.sum(image == 255)
total_pixels = 128 * 128
print("=" * 50)
print("图像详细信息:")
print("=" * 50)
print(f"图像尺寸: 128 × 128")
print(f"白色框尺寸: {white_width} × {white_height}")
print(f"白色框位置: [{start_x}:{end_x}, {start_y}:{end_y}]")
print(f"黑色像素数量: {black_pixels} ({black_pixels/total_pixels*100:.2f}%)")
print(f"白色像素数量: {white_pixels} ({white_pixels/total_pixels*100:.2f}%)")
# OpenCV显示
cv2.imshow('Result', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
