简介:面向图像质量分析研究者与Python开发者的无参考图像质量评价工程,包含完整源码与训练数据,聚焦无参考场景下的图像质量评估。工程共274个文件、约12.37MB,以Python脚本(163个py)为核心,辅以prototxt网络配置、checkpoint模型权重、shell脚本和MATLAB数据生成代码,覆盖数据准备、网络训练与质量预测流程;目前已有1279人浏览学习。源码在原版224输入RankIQA网络基础上,提供适应人脸大小的128输入重训练版本,并通过小网络回归RankIQA输出;还附带同时预测人脸角度与图像质量的多任务训练脚本,针对低质量图像label做了0-10非线性拉伸以提升性能。对复现无参考图像质量评价算法、开展人脸图像质量分析或研究多任务学习的读者而言,是一份可直接运行的完整工程。
1. 无参考图像质量评价是什么:没有原图也能评分的判断逻辑
视频上传链路报“画质下降”,监控录像夜间噪点变多,AI 出图库要按质量筛图,都有一个共同点:只有一张已经失去原始参考的成品图。无参考图像质量评价(No-Reference Image Quality Assessment,NR-IQA)要解决的正是这类问题——没有原图,也要给当前图像一个能排序的质量分数。它和 PSNR、SSIM 等全参考方法的最大差别,不是公式更复杂,而是评价目标从“和原图比”变成“与自然图像的统计规律比”。在 Python 里搭这条链路成本很低:python 3.11 的虚拟环境装 numpy、opencv-python、scipy、torch 就能跑通最小版本。下面按原理、特征实现、回归模型、指标排错、上生产前校准展开,适合图像算法评测、视频质量运营与内容审核系统的工程师看。
2. 用 Python 落地无参考质量评价:MSCN 特征与广义高斯参数
2.1 为什么失真会在统计特征里留痕
自然图片的局部像素并不是白噪声,它存在很强的空间结构。人眼看到“画质好”,本质是图像在统计上符合自然图像的规律:边缘的锐利程度、纹理的分布尺度、局部亮度的一致性。各种失真,不管是压缩、模糊、噪声还是锐化过度,都会以不同方式破坏这种统计规律。
MSCN(Mean Subtracted Contrast Normalized)是自然场景统计方法里最常用的一步预处理。它逐像素把局部灰度减去均值、除以标准差,得到“归一化亮度”。这样做之后,整体光照和相机曝光的影响被压掉,保留下来的是局部结构变化的数值序列。对一张清晰的自然图,MSCN 系数大致服从零均值的广义高斯分布;对模糊图,分布形状变窄;对噪声图,分布尾部明显变厚。因此拟合出分布的形状参数,就能对图像质量做无参考判断。
实际搭建环境时,用 conda 建一个干净环境即可:conda create -n iqa python=3.11 -y,然后安装numpy opencv-python scipy torch。后续所有脚本都在这个环境里跑,避免系统 python 被不断升级打断。
2.2 提取 MSCN 系数的最小实现
import cv2 import numpy as np def extract_mscn(img_gray, kernel_size=7, sigma=7 / 6): # 输入是单通道灰度图,先把数值范围压到 0~1 img = img_gray.astype(np.float32) / 255.0 # 用高斯窗估计局部均值 mu = cv2.GaussianBlur(img, (kernel_size, kernel_size), sigma) # 中心化后计算局部方差 centered = img - mu sigma_local = cv2.GaussianBlur(centered ** 2, (kernel_size, kernel_size), sigma) sigma_local = np.sqrt(sigma_local) # 加 eps 是为了避免纯色区域除零 mscn = centered / (sigma_local + 1e-8) return mscn这段代码是 BRISQUE 类方法的公共前置步骤。kernel_size=7和sigma=7/6是原论文里的默认搭配,两者共同决定高斯窗覆盖半径。窗口变小,MSCN 对纹理的响应更碎,后续拟合出的分布参数整体偏移;窗口变大,局部结构被过度平滑。没有特殊理由时,不要把这两个值单独改动。
需要说明的是+1e-8的语义:它只是防零除,不是真正的噪声强度。如果把它调到 0.1 这类量级,低对比度区域的信号会被直接盖掉,模型对暗部细节的判别力会明显下降。
| 参数 | 默认值 | 调整边界 |
|---|---|---|
| kernel_size | 7 | 一般只在 5~9 之间试 |
| sigma | 7/6 | 与 kernel_size 配套,不建议单独改 |
| epsilon | 1e-8 | 只取极小数,不能当正则强度用 |
2.3 拟合广义高斯参数并组成特征向量
MSCN 系数最常见的建模方式是广义高斯分布(GGD),它的密度函数由形状参数 beta 和尺度参数 sigma 控制。清图像拟合出的 beta 值集中在某个区间,模糊图 beta 偏大,噪声图 beta 偏小,所以这两个参数本身就是质量特征。
from scipy.stats import gennorm from scipy.optimize import minimize import numpy as np def fit_ggd(coeff): def nll(params): beta, loc, scale = params if beta < 0.1 or scale <= 0: return 1e9 # 最大化对数似然,这里用负对数似然给 minimize return -np.sum(gennorm.logpdf(coeff, beta, loc, scale)) scale0 = float(np.std(coeff)) result = minimize(nll, x0=[2.0, 0.0, scale0], method="Nelder-Mead", options={"maxiter": 300}) return result.xfit_ggd返回三个值:形状 beta、位置 loc、尺度 scale。MSCN 理论上零均值,如果 loc 拟合后明显偏离 0,说明输入图存在全局亮度偏移,或者灰度化预处理做得不干净。beta 低于 0.2 通常表示高噪声图像,beta 高于 4 则对应过度平滑的画面。
整图百万像素全部参与拟合会比较慢,常见做法是随机抽样 3 万到 5 万个系数点。两次独立抽样拟合出的 beta 差异通常小于 2%,对最终排序影响很小。
单看 MSCN 的分布看不出方向性,还需要补充相邻系数乘积的统计量:水平方向x(i,j)*x(i,j+1)、垂直方向x(i,j)*x(i+1,j),以及对角两个方向,分别用非对称广义高斯分布(AGGD)拟合。BRISQUE 原始特征就是把这些参数拼起来,对两个尺度各算一遍,得到 36 维向量。两个尺度指原图尺寸和 1/2 下采样后的尺寸,下采样是为了捕捉不同距离下观察图像时的结构差异。
2.4 把特征向量变成业务需要的分数
拿到特征向量后,常见做法分两条路:有 MOS 标注时,训练一个 SVR 回归器把特征映射到分数;没有标注时,用一批无失真图像的特征建立多元高斯模型,再计算待测图特征与这个模型的马氏距离,距离越远质量越差。后者更接近 NIQE 的思路,不需要人工标图,适合快速上线。
无论用哪种方式,输出的原始分数都不能直接当业务分数用。模型输出范围受特征定义和训练数据分布影响,不同实现之间差异很大。更稳的做法是准备一组锚点图:比如原图、JPEG q90/q70/q50、高斯模糊两档、椒盐噪声一档,先跑出模型的原始分数,再人工给这组图定业务分,然后用线性或分段线性映射把模型原始分搬到业务刻度上。这组锚点一旦定下来,后续每次模型更新、特征改动,都要重跑一遍,保证新旧分数可对比。
3. 深度学习的无参考图像质量评价路线:PyTorch 回归模型与 MOS
3.1 回归头为什么比分类头更贴合 MOS 标注
MOS 本质是连续的主观评分。3.2 分和 3.3 分的两张图在内容上可能比 1 分和 2 分之间更接近,把任务切成 N 个类别会丢失这种序关系,还会引入类别不平衡问题。所以深度学习做 IQA 时,主流是在分类骨干网络后面接一个单神经元回归头,直接输出 MOS 的预测值。损失函数用回归损失而不是交叉熵。
深度方法和传统特征路线的区别在于:传统方法先定义“哪些统计量能反映质量”,然后拟合参数;深度方法把这一步交给网络,让模型自己从原始像素里找规律。理论上更通用,但对数据分布、输入尺寸和训练策略更敏感。
3.2 数据集选择与 MOS 归一化
| 数据集 | 规模与失真类型 | 标注 | 适用场景 |
|---|---|---|---|
| LIVE | 29 张参考图,779 张合成失真图 | DMOS | 消融实验、失真机理验证 |
| TID2013 | 25 张参考图,3000 张合成失真图 | MOS | 覆盖更多失真类型的基准测试 |
| KonIQ-10k | 10073 张真实拍摄图像 | MOS | 移动端拍摄、真实内容生产线 |
| PIPAL | 大量生成与超分相关失真 | MOS 和排序标签 | 生成模型、AIGC 出图质量评估 |
训练前要对 MOS 做归一化。建议把训练集的 MOS 线性缩放到 [0,1],同时把 min 和 max 保存成文件。测试和推理时必须复用训练集的这两个数,不能在测试集上重新算一遍,否则属于标签统计量泄漏,得到的 SROCC 和 PLCC 都会虚高。
3.3 最小可跑的 PyTorch 无参考质量模型
import torch import torch.nn as nn from torchvision import models def build_iqa_model(pretrained=True): # ImageNet 预训练权重对 MOS 回归仍然有明显的加速作用 weights = models.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None model = models.resnet18(weights=weights) # 把 1000 类分类头换成单输出回归头 model.fc = nn.Linear(model.fc.in_features, 1) return model def iqa_loss(pred, target, alpha=0.3): # MSE 擅长拉近离群点,L1 负责稳定大多数样本的梯度 mse = ((pred - target) ** 2).mean() l1 = (pred - target).abs().mean() return mse + alpha * l1这里的关键点有三个。第一,输出层不要接 Sigmoid 或 ReLU,MOS 预测是开区间回归,截断会让分数堆到边界。第二,损失函数用 MSE 加 L1 混合,纯 MSE 对标注噪声大的 MOS 太敏感,单条异常标注会把模型带偏。第三,输入图像不要按分类习惯缩到 224×224,压缩块痕迹和噪声纹理在这个尺寸下已经损失大半。
| 超参数 | 建议值 | 调整原因 |
|---|---|---|
| optimizer | AdamW | 权重衰减处理更稳 |
| 初始学习率 | 1e-4,两个 epoch 后降到 5e-5 | 微调预训练权重时过大容易破坏底层特征 |
| batch_size | 16~32 | 高分辨率输入时降到 8 并用梯度累积 |
| 输入尺寸 | 384×384 | 保留压缩块和噪声纹 |
| epoch | 15~25 | 回归任务收敛快,过拟合 MOS 标签很快 |
3.4 通道选择、数据增强与跨数据集差异
质量评价模型对输入通道不算挑剔,但工程上很多人会把 RGB 转成 YCbCr 后只用 Y 通道,因为人眼对亮度敏感度远高于色度。这个做法的代价是损失色偏、色彩断层这类失真信息。如果业务检测目标包含色偏,保留三通道或额外加一阶色差特征是更稳的选择。
数据增强方面,一般不要用RandomResizedCrop。对 IQA 任务,随机缩放等于主动改变质量:小图被放大后会产生模糊感,清晰图被缩小后反而掩盖了噪声。更合理的增强是固定尺寸随机裁剪、水平翻转、旋转 90 度的整数倍、轻微颜色抖动。随机缩放的幅度如果非要用,也应该限制在 0.9~1.1 之间。
跨数据集测评时,要注意 LIV 和 TID2013 的标注往往是 DMOS,KonIQ 是 MOS,分数范围完全不同。直接比较两个模型在不同数据集上的 PLCC 没有意义,跨数据集比较统一用 SROCC,并且把训练集的归一化参数固定下来。
4. 无参考图像质量评价的调参排错:SROCC、PLCC 与数据泄漏
4.1 排序看 SROCC,数值看映射后的 PLCC
SROCC 只看预测和人工评分的秩相关性,所以它对单调变换不敏感,是论文里最常用的指标。但业务上常需要知道“这个分数能不能直接当底线的依据”,这时要看 PLCC。直接对预测值和 MOS 算皮尔逊系数通常偏低,因为模型输出和主观评分不一定是线性关系,需要先做一个单调映射。
from scipy.stats import spearmanr, pearsonr from scipy.optimize import curve_fit import numpy as np def logistic4(x, a, b, c, d): # IQA 里最常用的 4 参数 logistic 映射 return (a - b) / (1 + np.exp(-(x - c) / d)) + b def evaluate(pred, mos): srcc, _ = spearmanr(pred, mos) p0 = [mos.min(), mos.max(), np.median(pred), np.std(pred)] popt, _ = curve_fit(logistic4, pred, mos, p0=p0, maxfev=10000) plcc, _ = pearsonr(logistic4(pred, *popt), mos) return srcc, plcc, poptlogistic4的四个参数分别控制上下渐近线、拐点位置和曲线斜率。a、b接近 MOS 的范围两端,c是预测值中位数对应的映射点,d表示曲线绕拐点变化的陡峭程度。SROCC 高、PLCC 低,说明排序正确但数值尺度没有对齐,业务上不能直接把分数阈值写死在代码里。
| 指标 | 回答的问题 | 局限 |
|---|---|---|
| SROCC | 模型排序和人工排序是否一致 | 对分数绝对值没有约束 |
| PLCC | 经过单调映射后与 MOS 的线性相关 | 跨数据集不能直接比较 |
| RMSE/MAE | 预测值与 MOS 的绝对偏差 | 必须保证训练测试同分布 |
4.2 数据泄漏的两种隐蔽形态
内容级泄漏是最容易犯的错误。LIVE 和 TID2013 中同一张参考图派生大量失真图,如果只是按文件名随机划分训练集和测试集,同一参考图的多个失真版本会同时出现在两边。模型学的就不是失真程度,而是图像内容指纹。正确做法是按参考图分组,一个参考图的所有版本要么全在训练集,要么全在测试集。
统计量泄漏更隐蔽。有人把整个数据集的 MOS 统一做 min-max 归一化,然后把训练集和测试集一起缩放。这等于测试标签的分布信息已经进入了训练过程。正确做法是先只从训练集算出 min 和 max,存成一个 JSON 文件,测试集和推理阶段都用同一个 JSON 里的数值。
4.3 常见失败模式与处理方式
| 症状 | 可能根因 | 常用解法 |
|---|---|---|
| 过曝或纯白图像分数异常稳定 | MSCN 在均匀区域几乎无信息量 | 在输入端追加全局均值、标准差作为辅助特征 |
| 同一张图反复推理分数抖动 | 随机裁剪、Dropout、BatchNorm 仍处于训练模式 | model.eval(),测试时多块取中位数 |
| 合成失真好,真实照片上崩 | 训练集只有合成失真 | 加入 KonIQ、SPAQ 等真实采集标注数据 |
| 模糊图像和噪声图像分数接近 | 高频能量这一维区分度不够 | 增加频域统计特征或改用多尺度输入 |
表里最后一行值得展开。模糊和噪声都会改变高频分量,但方向相反:模糊削弱高频,噪声增强高频。如果模型只学了“高频少就是质量差”,那噪声图就会被误判为高质量。常见补救是输入多个尺度,或者在网络前部加一个可学习的频域分解模块。
4.4 超参观测表
模型训不动的时候,先看 SROCC 的曲线形态再调参。
| 观测 | 调整方式 |
|---|---|
| 训练 loss 降,验证 SROCC 波动大 | 降低学习率,加两个 epoch 的 warm-up |
| 高分辨率测试反而更差 | 输入尺寸从 224 提到 384 或 512 |
| 分数全部堆在均值附近 | 检查 MOS 分布,必要时用加权 MSE 或按分数区间重采样 |
| 增强越加越差 | 删除 RandomResizedCrop,只保留固定尺寸随机裁剪和翻转 |
一个容易被忽略的细节是model.eval()的位置。PyTorch 里 BatchNorm 在训练模式下使用当前 batch 的统计量,同一张图放进不同 batch,预测结果会轻微波动。如果预测时忘了切到 eval 模式,线上图像的分数稳定性会差很多。更稳妥的是在推理入口统一写死model.eval(),而不是依赖调用方记住。
5. 无参考图像质量评价的上产前应用:锚点校准、多块投票与漂移监控
5.1 用锚点图集把模型输出搬到业务刻度
模型输出的 MOS 预测值范围与训练集标注强相关,直接把它当业务分很难解释。我一般会准备十到二十张锚点图,覆盖清晰、轻微压缩、明显模糊、高噪声、过曝几种状态,让模型跑一遍原始分,再由业务方给这组图定出可用分数。然后用最小二乘拟合线性映射。
def anchor_calibrate(raw_scores, business_scores): # raw_scores 是模型在锚点集上的输出 # business_scores 是同一批锚点图的人工业务分 coeff = np.polyfit(raw_scores, business_scores, 1) return lambda raw: float(np.polyval(coeff, raw))如果发现高分段和低分段明显不再线性,可以改成三段分段线性映射,但锚点区间内每个分段至少要三张图,否则会被个别标定误差带偏。锚点集一旦固定就不要再频繁改图,模型每次更新后都要重跑这组图,观察校准曲线是否出现整体平移。
5.2 推理时用多块投票降低抖动
无参考评价模型对输入分辨率很敏感,直接把整张图缩放到 224×224 会损失压缩痕迹。常见做法是在原分辨率下切块,对每块预测后取中位数。
def infer_stable(model, tile, flip=True): model.eval() with torch.inference_mode(): # tile 是预处理后的 (3, H, W) 张量,不缩放到固定尺寸 pred = model(tile.unsqueeze(0)).item() if flip: flipped = torch.flip(tile.unsqueeze(0), dims=[3]) pred = 0.5 * (pred + model(flipped).item()) return pred多块预测时不要用均值,中位数对个别纯色块或过曝块的异常输出更有抵抗力。更精细的做法是按块内方差加权:方差小的块往往是天空、白墙这类信息量低的区域,预测置信度也低,权重自然要降下来。
5.3 固定锚点集的漂移监控
模型上线后,最怕的不是单次分数不准,而是模型或推理环境变化导致分数整体漂移。每次更新代码、升级依赖、切换预处理库,都拿同一套锚点集回归一次,记录每个锚点预测分数的平均绝对变化。如果平均变化超过 0.1,先检查图像读取的通道顺序、色彩空间转换矩阵、归一化参数是否被改动,再去怀疑数据分布变化。
给模型配一个固定锚点集做回归测试,比在测试集上刷一次 SROCC 更能说明它在线上没有退化。
本文还有配套的精品资源,点击获取