简介:这份资源面向图像处理方向的学习者与开发者,系统整理了图像质量评价领域的经典算法代码实现,涵盖BRISQUE、RankIQA、NIMA等代表性方法,可用于监控视频传输质量评估、图像压缩与增强效果验证、医疗影像分析等场景,兼顾学术研究与工程落地。压缩包共821个文件,约155.08MB,以122个py、92个m、57个cpp等源码文件为核心,配套jpg、bmp、png等测试图像,以及prototxt、cfg、h5、pb等模型与配置文件,另有txt、md、pdf等说明文档,覆盖MATLAB、C++、Python三种语言,方便不同背景的开发者直接上手。目前已有8686人学习下载,热度较高。资源不仅提供算法源码,还包含预训练模型、数据集索引与多平台编译产物,读者可据此复现无参考质量评估流程,理解局部统计特征、视觉显著性排名与CNN回归评分的实现差异,并快速迁移到自有图像处理系统中。
1. 从一次模型翻车说起:这套图像质量评价代码到底能干什么
去年帮一个做短视频审核的朋友看模型,他们用了个开源的美学打分模型,离线测试集上 SRCC 能到 0.9 以上,上线之后却频繁把一些明显糊成马赛克的封面判成高分。排查了两天,问题不在模型本身,而是他们拿来做对比的“基准分数”来自一个没校准过的数据集,评价指标和业务目标根本不在一个维度上。这件事让我重新翻出了这套图像质量评价经典算法代码——BRISQUE、RankIQA、NIMA 这些名字,做图像的人多少都听过,但真正把它们凑齐、能跑、能对比的工程代码并不多。
这份资源就是干这个的:把几类经典的无参考图像质量评价(NR-IQA)算法整理成可运行的代码集合,覆盖传统手工特征路线(BRISQUE)和深度学习路线(RankIQA、NIMA 等)。适合两类人:一是做图像增强、超分、去噪之后需要客观指标验证效果的算法工程师;二是想入门 IQA 领域、需要一个能直接跑通 baseline 的学生或转行者。你不需要从零复现论文,拿到就能在本地跑出分数,再拿自己的数据去微调。
2. 技术选型与目录结构:为什么是这几套算法凑在一起
2.1 传统派与深度派的组合逻辑
这套代码的选型思路很清晰,不是随便堆几个模型。BRISQUE 代表的是传统手工特征 + 回归的路线,它不依赖深度学习框架,用自然场景统计(NSS)特征加 SVR 就能出分,优点是轻量、可解释、CPU 上跑得飞快,缺点是面对合成失真(比如 GAN 生成的伪影)泛化能力有限。RankIQA 和 NIMA 则是深度路线的两个典型代表:RankIQA 的核心思想是用排序学习解决 IQA 标注数据难获取的问题,通过大量“A 比 B 好”的相对标注来训练;NIMA 则是直接做回归/分类,用预训练骨干网络加一个打分头,输出质量分布而非单一分数。
把这两派放在一起,实际使用时的价值在于交叉验证。我一般会先用 BRISQUE 快速筛一遍,如果它的分数和主观感受偏差大,再上 NIMA 看深度模型怎么说。两者一致时基本可以下结论,不一致时往往意味着图像存在某种非自然失真,需要人工介入。
2.2 目录结构长什么样
拿到源码包后,典型的目录组织大致如下(不同整理版本会有差异,但核心模块跑不掉):
iqa-algorithms/ ├── brisque/ │ ├── brisque.py # 特征提取与打分主逻辑 │ ├── svr_model.pkl # 预训练 SVR 权重 │ └── utils.py # 图像预处理、归一化 ├── rankiqa/ │ ├── model.py # 孪生网络结构定义 │ ├── train.py # 排序损失训练脚本 │ └── inference.py # 单图/批量推理 ├── nima/ │ ├── nima_model.py # 骨干网络 + 打分头 │ ├── predict.py # 加载权重出分 │ └── weights/ # 预训练权重目录 ├── datasets/ │ └── loader.py # 统一数据加载接口 ├── evaluate.py # 批量评测,输出 SRCC/PLCC └── requirements.txt这个结构的合理性在于:每个算法自成一个子包,互不干扰,但共享datasets和evaluate两个公共模块。你新增一个算法时,只要在顶层加一个目录,实现统一的predict(image_path) -> score接口,就能直接接入评测流程。
2.3 依赖环境与版本约束
requirements.txt里通常包含这些核心依赖:
| 依赖包 | 作用 | 版本建议 |
|---|---|---|
| torch | RankIQA/NIMA 的模型推理与训练 | >=1.10 |
| torchvision | 预训练骨干网络加载 | 与 torch 匹配 |
| scikit-learn | SVR 训练、SRCC/PLCC 计算 | >=1.0 |
| opencv-python | 图像读取与 resize | >=4.5 |
| scipy | 统计检验、特征计算 | >=1.7 |
| numpy | 基础数值运算 | >=1.21 |
注意:BRISQUE 部分如果用的是 libsvm 的 Python 绑定,在 Windows 上编译容易出问题,常见做法是换成 scikit-learn 的 SVR,接口几乎一样,省去编译麻烦。
3. 核心模块拆解:BRISQUE 的特征提取与打分链路
3.1 自然场景统计特征是怎么算出来的
BRISQUE 的核心假设是:自然图像在局部归一化后,其 MSCN(Mean Subtracted Contrast Normalized)系数服从特定的统计分布,失真会破坏这种分布。代码里第一步就是算 MSCN 系数:
import cv2 import numpy as np from scipy.special import gamma def compute_mscn_coefficients(image_path): """计算图像的 MSCN 系数,BRISQUE 特征提取的第一步""" img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f"无法读取图像: {image_path}") img = img.astype(np.float64) # 用 7x7 高斯核做局部均值滤波 kernel = cv2.getGaussianKernel(7, 7/6) kernel = kernel @ kernel.T mu = cv2.filter2D(img, -1, kernel) # 局部方差 sigma = np.sqrt(cv2.filter2D(img**2, -1, kernel) - mu**2) sigma[sigma < 1e-10] = 1e-10 # 防止除零 # MSCN 归一化 mscn = (img - mu) / (sigma + 1) return mscn这段代码的逻辑链条是:先读灰度图,用高斯核估计每个像素的局部均值和局部方差,再做归一化。参数上,7x7 的窗口是论文里的经验值,改大改小都会影响特征稳定性;sigma加 1 是为了数值稳定,不是随便写的。实际跑的时候如果图像特别小(比如小于 32x32),这个窗口会覆盖到边界外,需要先做 padding 或者直接拒绝处理。
3.2 特征向量拼接与 SVR 回归
拿到 MSCN 系数后,BRISQUE 会拟合广义高斯分布(GGD)参数,再对相邻系数乘积拟合非对称 GGD,最终拼成一个 36 维特征向量:
def extract_brisque_features(image_path): """提取完整的 BRISQUE 特征向量""" mscn = compute_mscn_coefficients(image_path) # 拟合 GGD 得到两个参数 (alpha, sigma_sq) alpha, sigma_sq = fit_ggd(mscn) # 四个方向的相邻系数乘积 features = [alpha, sigma_sq] shifts = [(0, 1), (1, 0), (1, 1), (1, -1)] for dx, dy in shifts: prod = mscn[1:-1, 1:-1] * mscn[1+dy:mscn.shape[0]-1+dy, 1+dx:mscn.shape[1]-1+dx] # 每个方向拟合非对称 GGD,得到 4 个参数 params = fit_aggd(prod) features.extend(params) return np.array(features) # 2 + 4*4 = 18 维,原论文还有多尺度,共 36 维这里只展示了单尺度的 18 维特征,原论文还会把图像降采样一次再算一遍,拼成 36 维。fit_ggd和fit_aggd内部用的是矩估计法,不是最大似然,因为矩估计快且足够稳定。实际使用时,如果你只想要一个快速分数,单尺度版本已经能反映大部分失真情况,多尺度主要提升的是对模糊和噪声的区分度。
3.3 打分与批量评测
特征提取完之后,加载预训练的 SVR 模型直接预测:
import joblib def brisque_score(image_path, model_path='brisque/svr_model.pkl'): """输出 BRISQUE 质量分数,越低表示质量越好""" features = extract_brisque_features(image_path).reshape(1, -1) svr = joblib.load(model_path) score = svr.predict(features)[0] return float(score)BRISQUE 的分数范围大致在 0 到 100 之间,越低越好,这和 NIMA 的 1 到 10 分、越高越好是反的。批量评测时evaluate.py会统一把方向对齐,再算 SRCC 和 PLCC。我踩过的一个坑是:不同来源的 SVR 权重对应的特征归一化方式可能不同,直接混用会导致分数整体偏移,换权重时一定要连归一化参数一起换。
4. 深度模型实战:RankIQA 与 NIMA 的推理与微调
4.1 RankIQA 的孪生网络推理
RankIQA 训练时用的是成对图像,推理时只需要单张图过其中一个分支。代码里通常把共享权重的骨干网络单独抽出来:
import torch import torch.nn as nn from torchvision import models class RankIQAScorer(nn.Module): """RankIQA 推理分支:骨干网络 + 全连接打分头""" def __init__(self, backbone='resnet50', pretrained=True): super().__init__() self.backbone = models.resnet50(pretrained=pretrained) # 替换最后的分类层为回归头 feat_dim = self.backbone.fc.in_features self.backbone.fc = nn.Sequential( nn.Linear(feat_dim, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, 1) # 输出单一质量分数 ) def forward(self, x): return self.backbone(x).squeeze(-1)推理时把图像 resize 到 224x224,做 ImageNet 标准化,然后直接 forward 就行。RankIQA 的分数是相对的,没有绝对物理意义,所以必须用同一批数据内的排序来解读,不能拿两张不同来源的图直接比分数。我一般会把它当排序器用:给一批图打分后按分数排序,看排在前面的图是不是确实观感更好。
4.2 NIMA 的质量分布输出
NIMA 和 RankIQA 最大的区别是输出不是一个标量,而是一个 10 维的分布(对应 1 到 10 分):
class NIMAScorer(nn.Module): """NIMA 模型:骨干 + 分布预测头""" def __init__(self, backbone='vgg16', num_classes=10): super().__init__() base = models.vgg16(pretrained=True).features self.features = base self.classifier = nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(4096, num_classes), nn.Softmax(dim=1) # 输出概率分布 ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) return self.classifier(x) def nima_score(model, image_tensor): """从分布计算期望分数""" probs = model(image_tensor) scores = torch.arange(1, 11, dtype=torch.float32) mean_score = (probs * scores).sum(dim=1) return mean_score.item()用分布而不是标量的好处是:你可以看到模型对这张图的“信心”。如果分布很尖,说明模型很确定;如果分布很平,说明这张图处于质量边界,可能需要人工复核。这个特性在实际业务里比单一分数有用得多。
4.3 在自己的数据上微调
如果你有自己标注的数据(哪怕只有几百张),微调 NIMA 比从头训练划算得多:
# 冻结骨干前几层,只训练后面的层 for name, param in model.named_parameters(): if 'features' in name and int(name.split('.')[1]) < 20: param.requires_grad = False optimizer = torch.optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4 ) criterion = nn.KLDivLoss(reduction='batchmean') # 分布用 KL 散度微调时学习率要压小,1e-4 是常见起点,太大容易把预训练权重冲垮。损失函数用 KLDivLoss 而不是 MSE,因为 NIMA 输出的是分布,用 MSE 会破坏分布的性质。如果标注数据里分数集中在某几个值,还要注意做类别平衡,否则模型会退化成只预测那几个分数。
5. 避坑与排查:跑这套代码时最容易翻车的几个点
5.1 图像读取通道不一致导致分数异常
现象:同一张图用不同方式读入,BRISQUE 分数差了十几分。原因:cv2.imread默认读成 BGR,而有些预处理代码按 RGB 处理,灰度转换时权重不同,导致 MSCN 系数偏移。解决:统一在读取后立刻转成灰度,或者显式指定cv2.COLOR_BGR2GRAY,不要依赖默认行为。
5.2 预训练权重与模型结构不匹配
现象:加载 NIMA 权重时报size mismatch,或者加载成功但输出全是 0.1。原因:骨干网络版本不对,比如权重是在 VGG16 上训的,代码里却用了 ResNet50;或者分类头维度对不上。解决:先打印模型 state_dict 的 key 和权重文件的 key,逐个对比 shape,确认一致再加载。不要用strict=False掩盖问题。
5.3 批量推理时显存溢出
现象:单张图跑得好好的,批量跑几十张就 OOM。原因:没有用torch.no_grad(),或者 batch size 设得太大,中间激活值累积。解决:推理时强制加with torch.no_grad():,batch size 从 8 开始试,逐步往上加,同时用torch.cuda.empty_cache()清理缓存。
5.4 SRCC 算出来是负数
现象:评测脚本输出的 SRCC 是负的,但模型明明看起来没问题。原因:BRISQUE 是越低越好,NIMA 是越高越好,评测时没有统一方向。解决:在evaluate.py里对 BRISQUE 分数取负号,或者用scipy.stats.spearmanr时手动调整符号。这个坑我见过至少三个人踩过。
5.5 图像尺寸过小导致特征提取失败
现象:处理缩略图或图标时,BRISQUE 报错或输出 NaN。原因:7x7 的高斯窗口在小于 16x16 的图上无法正常计算局部统计量。解决:在特征提取前加一个尺寸检查,小于 32x32 的图先放大,或者直接跳过并记录日志。
6. 二次开发与改造:从跑通到真正用起来
6.1 统一接口封装
这套代码最值得做的第一件事,是把所有算法的推理接口统一成同一个签名:
class IQAPredictor: """统一封装不同 IQA 算法的推理接口""" def __init__(self, method='brisque', device='cpu'): self.method = method self.device = device if method == 'brisque': self.model = joblib.load('brisque/svr_model.pkl') elif method == 'nima': self.model = NIMAScorer().to(device) self.model.load_state_dict(torch.load('nima/weights/nima.pth')) self.model.eval() def predict(self, image_path): if self.method == 'brisque': feat = extract_brisque_features(image_path).reshape(1, -1) return float(self.model.predict(feat)[0]) elif self.method == 'nima': tensor = preprocess(image_path).unsqueeze(0).to(self.device) with torch.no_grad(): return nima_score(self.model, tensor)这样上层业务代码只需要predictor = IQAPredictor(method='nima'),不用关心底层是 SVR 还是神经网络。新增算法时也只需要在__init__里加一个分支。
6.2 多模型融合打分
单一模型总有盲区,我一般会把 BRISQUE 和 NIMA 的分数做加权融合:
| 融合策略 | 适用场景 | 权重建议 |
|---|---|---|
| 简单平均 | 两种方法分数尺度接近 | 各 0.5 |
| 排序融合 | 只关心相对质量 | 按 SRCC 加权 |
| 分段融合 | 低分区间信 BRISQUE,高分区间信 NIMA | 阈值 40 分 |
分段融合的逻辑是:BRISQUE 对严重失真敏感,NIMA 对轻微质量差异更敏感。实际用的时候先算 BRISQUE,如果低于 40 分直接采信,高于 40 分再看 NIMA 的分数。
6.3 验证改造效果的方法
改完之后怎么确认没改坏?我的习惯是准备一个小型验证集,至少包含三类图:清晰原图、轻度压缩图、严重模糊图。改造前后各跑一遍,看三类的分数排序是否保持一致。如果清晰原图的分数反而低于模糊图,说明改造引入了 bug。另外,SRCC 和 PLCC 要在同一批数据上对比,不要拿改造前的 A 数据集和改造后的 B 数据集比,没有意义。
从那以后我每次动 IQA 相关代码,都强制先跑一遍这个三图验证集,确认排序没乱再继续。希望帮到你。
本文还有配套的精品资源,点击获取