ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

无参考图像质量评价原理与Python实践:BRISQUE特征提取到批量打分

无参考图像质量评价原理与Python实践:BRISQUE特征提取到批量打分 简介面向图像质量分析场景的Python无参考图像质量评价NR-IQA资源包适合图像处理、人脸质量评估方向的研究者与开发者。内容围绕Rank网络实验链展开既有生成不同失真等级数据的MATLAB代码也有将网络输入从224降至128以适配人脸尺寸的重训版本还提供原版网络源码及用小网络拟合RankIQA效果的回归模块同时支持在单一网络中联合预测人脸角度与图像质量并给出任务loss加权、对低质量样本的标签做非线性拉伸映射到0-10等关键处理思路。资源包共274个文件以py源码、prototxt模型配置、checkpoint权重和md说明为主另含sh脚本、xml/m辅助文件整体约12.37MB目录按数据、源码、训练脚本划分便于定位。目前已有1280人学习下载适合需要复现无参考质量评价实验、研究多任务训练策略或开展相关课题的读者直接参考。1. 无参考图像质量评价给没有“原图”的画质打分做内容审核或视频处理的同学应该都遇到过这种需求平台收到一批用户上传的压缩图领导让你评估这批图整体画质怎么样而你手里只有图本身没有原始版本可对比。基于Python的无参考图像质量评价NR-IQA解决的就是这个问题——不需要原图只靠单张图的统计特征就能给出一个和主观感受高度相关的质量分数。更反直觉的是经典方案BRISQUE这类甚至不需要训练数据pip装好依赖就能跑。适合图像/视频算法、内容审核后端和QA工程师新手按本文能跑通熟手可以绕过我踩过的那些坑。2. 跑通最小Demo解压zip、装Python依赖、给第一张图打分2.1 先解压再看目录别急着执行main.py拿到“基于Python的无参考图像质量评价.zip”这种老工具包第一件事不是双击解压然后马上python main.py而是先确认包完整、目录结构和你预期一致。zip解压这件事在Windows上简单到了Linux上反而容易翻车很多这类工具包是在Windows上打的包文件名是GBK编码Linux下用默认的unzip解压出来全是乱码。# Linux上遇到乱码时用这个参数指定GBK编码 unzip -O GBK 基于Python的无参考图像质量评价.zip -d nr-iqa/ cd nr-iqa find . -maxdepth 2 -type f | sortmacOS自带的unzip不支持-O参数如果手边只有macOS我一般用7z替代7z x 基于Python的无参考图像质量评价.zip -mcp936效果一样。另外要留意“zip伪加密”这个老坑有些包在zip目录头里把加密标志位置了1但文件体根本没加密unzip会向你要密码实际只要把标志位改回去就能正常解压。遇到这种情况先怀疑是伪加密别急着去找“zip密码移除”工具——先检查压缩包注释和文件列表多数是打包工具的兼容性bug不是真的加密。解压后先核对目录。一个典型实现通常长这样nr-iqa/ ├── main.py # 命令行入口 ├── iqa_features.py # 特征提取核心 ├── models/ │ ├── brisque_svm.pickle # 训练好的SVR模型 │ └── svm_range.npy # 特征归一化参数 ├── requirements.txt ├── demo/ │ ├── sharp.jpg │ └── blurry.jpg └── README.md如果models/目录是空的后面100%会报模型文件找不到先记下这个隐患后面避坑章节会专门讲。2.2 用Python 3.8搭环境锁住numpy和scikit-image的版本这类质量评价代码对科学计算库版本很敏感。老工具包多半是2020年前后写的用新numpy跑经常直接崩——典型报错是module numpy has no attribute float。所以别用最新版按依赖清单锁版本。python -m venv .venv source .venv/bin/activate python -m pip install -U pip wheel pip install -r requirements.txt如果服务器在内网pip下载会卡住。常见做法是找一台同架构、能访问PyPI的机器先pip download -r requirements.txt -d whl/把wheel包拉下来再拷进内网执行pip install --no-index --find-links./whl -r requirements.txt。依赖列表里最需要盯住的是numpy、scipy、scikit-image这三个。老代码建议numpy锁在1.x分支scikit-image建议用0.19左右的版本因为新版scikit-image改了不少内部API_shared.utils这类老接口可能被移走。至于Python本身3.8到3.10之间最稳妥3.11以上跑老scipy也有概率遇到二进制兼容问题。VSCode和PyCharm里配置解释器时直接指向.venv/bin/python即可别用全局环境。2.3 命令行入口单张图片打分环境就绪后先用demo目录里的样例图跑通最小命令python main.py --input demo/blurry.jpg --output result.json --model models/brisque_svm.pickle cat result.json预期输出一个浮点分数大致长这样{input: demo/blurry.jpg, score: 42.37, mos_pred: 42.37}各参数含义如下表参数默认值说明--input必填支持jpg/png/bmp内部统一转RGB处理--outputstdout建议给json文件路径批量时方便收集--modelmodels/brisque_svm.pickle换模型文件可适配不同失真类型--scale2多尺度特征数默认2个尺度想加速可以设1--verbose0设为1时打印每个特征矩阵的形状方便排错先跑默认参数不要一上来就调--scale。默认参数通常是作者调好的改少了影响精度改多了拖慢速度。跑通了再去动它。有个Windows老坑必须提醒cv2.imread遇到中文路径会直接返回None而解压出来的样例图文件名经常是中文。代码里尽量用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)这种读法绕开OpenCV对系统编码的依赖。如果打分结果突然全是-1或者None先怀疑这一步。2.4 结果怎么看分数不是一个绝对的“好/坏”第一次跑通后很多人会问“42.37这个分数到底是好是坏”。答案是看方向。BRISQUE拟合的是LIVE数据集的DMOS差异平均主观分分数越高代表质量越差。这个方向经常被博客抄反务必先确认你手里的实现是“高坏低好”还是“高好低坏”最简单的办法是把demo/sharp.jpg和demo/blurry.jpg各跑一遍看哪张分高。另外这个分数是SVR的回归输出不是概率偶尔会跳出0到100的区间别当成异常值。不同失真类型之间也别直接比——同一张图的模糊分数是40加噪后变成55不代表“噪声比模糊严重50%”只是说明在模型眼里后者更偏离自然图像统计。3. 分数是怎么算出来的MSCN系数、广义高斯分布与SVR回归3.1 第一步是局部归一化把内容“剥掉”留下失真痕迹NR-IQA的核心假设是自然图像经过局部归一化后系数服从标准高斯分布而任何失真都会让这个分布变形。所以第一步永远是把图像转成MSCN系数Mean Subtracted Contrast Normalized这一步直接决定后续特征有没有意义。import numpy as np from scipy.ndimage import uniform_filter _PATCH 7 # 局部统计窗口大小 _C 1.0 # 防止除零的常数图像范围0-255时加1 def compute_mscn(img_gray): img img_gray.astype(np.float64) mu uniform_filter(img, size_PATCH, modereflect) sigma np.sqrt(uniform_filter((img - mu) ** 2, size_PATCH, modereflect)) return (img - mu) / (sigma _C)逻辑上做了三件事先算局部均值再算局部标准差最后用“像素值减均值除以标准差”完成归一化。这样计算的结果是平坦区域的MSCN系数接近0边缘区域的系数被拉到和纹理区域一个量纲——内容信息被压掉失真的统计痕迹就暴露出来了。modereflect处理图像边界避免边缘出现异常的大系数。_PATCH7表示7x7的局部窗口。这是BRISQUE论文里验证过的值不要随便改。_C1.0是防止纯色块区域标准差为0导致除零。完整实现里这里应该用高斯加权窗口而不是均匀窗口上面代码只用来理解原理真正复现时看iqa_features.py里的实现。值得多说一句如果输入图像是纯色块sigma全是0MSCN系数也全是0后面的GGD拟合会直接失效。这类图像不该走自然图像质量评价后面避坑章节会再回来讲。3.2 四个方向的相邻系数炸出36维特征MSCN系数本身携带的统计信息还不够BRISQUE的关键步骤是把相邻像素的MSCN系数相乘再对乘积拟合广义高斯分布GGD。为什么要乘相邻系数因为模糊会让相邻像素变得更“像”噪声会让相邻像素变得更“不像”——这种相关性的变化在单点系数分布里看不出来需要成对统计。from scipy.optimize import minimize_scalar from scipy.special import gamma as gamma_func def fit_ggd(vec): 拟合广义高斯分布返回形状参数alpha和尺度参数sigma abs_mean np.mean(np.abs(vec)) sigma np.sqrt(np.mean(vec ** 2)) if sigma 1e-6: return 2.0, sigma rho abs_mean / (sigma 1e-8) def err(alpha): rhs gamma_func(2 / alpha) / np.sqrt(gamma_func(1 / alpha) * gamma_func(3 / alpha)) return (rho - rhs) ** 2 alpha minimize_scalar(err, bounds(0.1, 10), methodbounded).x return alpha, sigma这段代码的数学逻辑是矩匹配广义高斯分布的一阶绝对矩与二阶矩的比值只和形状参数alpha有关和尺度无关。所以先算rho再用一维搜索反解alpha最后二阶矩开方就是sigma。bounds(0.1, 10)限制了alpha的搜索范围——自然图像的alpha基本都在0.2到4之间搜索范围不需要更大。sigma 1e-6的判保是为了防止纯色块导致除零。水平、垂直、主对角、副对角四个方向都要做一次相邻乘积各拟合出一对(alpha, sigma)MSCN系数自己也拟合一对。一个尺度下就有18维特征再用原始图像的一半尺寸做第二尺度总共36维。两个尺度是精度和速度的平衡点只用一个尺度对模糊失真的敏感度明显下降用到三个尺度速度慢了但精度提升有限。--scale 2默认值就是这里来的。3.3 为什么不直接上深度学习2025年做图像质量评价很多人第一反应是上PyTorch。但这类zip工具包几乎全是经典统计方法不是作者保守而是因为两者的适用场景差别很大维度BRISQUE经典统计深度学习NR-IQA训练数据不需要用预训练SVR模型需要数千张带主观评分的图算力CPU单张毫秒级需要GPU模型加载也有开销跨域能力对压缩、噪声这类失真稳定训练分布外的场景不一定更好可解释性每个特征有统计含义黑匣子代码复杂度几十行核心逻辑整套训练管线经典方案真正的优势是“不要训练数据”。LIVE数据集上训好的SVR模型拿到真实业务里压缩图上排序能力依然可用。深度学习模型在公开数据集上指标更好但你不知道它在你这个业务的数据分布上会不会突然失效。我的建议是没有足够的主观标注数据时先用经典方案跑出分数分布后如果发现对某些失真严重错判再考虑用小样本微调一个学习型模型。3.4 输出SVR回归模型怎么把36维特征变成分数36维特征不能直接当分数用还需要一个回归模型把它映射到主观评分量纲。BRISQUE用的是支持向量回归SVR核函数是RBF模型文件就是zip包里的brisque_svm.pickle。加载方式一般是用pickle或joblib反序列化然后把特征向量喂进svr.predict()。SVR的选择有它的道理训练样本量只有几百张LIVE图特征维度36样本量远小于维度时SVR加RBF核比随机森林或全连接网络都稳。而且SVR的输出是连续值天然适合“打分”这个任务。注意预测前要做特征归一化——SVR对特征尺度敏感svm_range.npy里存的就是归一化用的均值和方差这个文件别删。4. 拿公开数据集算SROCC验证这套代码不是玄学4.1 LIVE数据集与5种失真类型在把分数接到业务之前必须先验证手里的实现是靠谱的。最常见的验证集是LIVE它包含5种失真JPEG压缩、JP2K压缩、白噪声、高斯模糊、快速衰落。解压后目录结构大致是LIVE_release1/ ├── jpeg/ # 文件名类似 img1.bmp ├── jp2k/ ├── wn/ ├── gblur/ ├── fastfading/ └── mos.txt # 每行文件名 DMOS分数读取代码很直观from pathlib import Path live_root Path(datasets/LIVE_release1) images sorted(list(live_root.glob(*/*.bmp))) mos {} for line in (live_root / mos.txt).read_text().splitlines(): name, score line.split() mos[name] float(score)注意mos.txt的分隔符可能是空格也可能是Tab用split()不传参数可以通吃。LIVE的DMOS是“越大越差”和BRISQUE的输出方向一致。4.2 SROCC为什么不能拿预测分数和MOS直接比“误差”很多新手会直接算预测分数和MOS的均方误差这是错的。主观评分是序数概念3.5分和4分之间的差距不能等同于4.5分和5分之间的差距。我们要验证的是排序能力——模型认为更差的图人眼也觉得更差这就够了。对应的指标是SROCCSpearman秩相关系数。from scipy.stats import spearmanr preds [run_model(img) for img in images] # run_model返回每张图的预测分数 gt [mos[img.name] for img in images] res spearmanr(preds, gt) print(SROCC:, res.correlation)如果scipy版本较新res.correlation依然可用老版本里可能要取res[0]。SROCC只看秩不看具体值所以哪怕模型的预测分数整体偏移只要排序对SROCC照样高。LIVE上BRISQUE的公开参考值大约在0.94到0.96之间。你手里的实现如果低于0.9先别怀疑模型回去查特征提取——最常出错的是四个方向里漏了方向或者两尺度只算了一个。真实业务场景里SROCC掉到0.7到0.8是常态因为业务图是压缩、缩放、噪声叠加的混合失真LIVE那种单一合成失真太好分了。4.3 MOS校准把分数掰到和主观评分同一量纲SROCC只验证排序能力但生产环境需要的是“这个分数能不能直接当阈值用”。BRISQUE的输出是DMOS拟合值和实际业务里你自己打的MOS比如1到5分制量纲不同需要做一个线性校准。from sklearn.linear_model import LinearRegression # preds_train: 在训练图片上得到的预测分数 # mos_train: 这批图片的人工评分 reg LinearRegression().fit(preds_train.reshape(-1, 1), mos_train) mos_pred reg.predict(preds_test.reshape(-1, 1))校准逻辑很直白预测分数和人工MOS之间是近似线性关系用最小二乘拟合一个斜率和截距。这里有个纪律问题校准的线性参数只能在测试集上做一次不能拿测试集调完又回去调SVR模型否则SROCC和校准后的误差都会虚高。校准只是为了给业务定阈值不是模型的一部分。注意SROCC评估时不需要任何校准校准只服务于生产环境的阈值设定。4.4 指标好看不等于业务可用LIVE上SROCC达到0.95不代表这套方案直接能用。LIVE的失真类型是“纯”的要么纯模糊要么纯噪声。真实业务图是“脏”的原图先被压缩再被缩放又加了水印还可能整体偏暗。我见过不少模型在LIVE上表现优异到了生产数据上排序乱跳。所以正规流程是先用LIVE验证代码方向正确然后自采200到300张业务图让两三个人按“差、一般、好”三档标注再算一次排序准确率。这个步骤别省它决定了你后面所有阈值决策是否可信。5. 避坑指南真实场景里最容易翻车的5个问题5.1 模型文件找不到运行直接FileNotFoundError现象是FileNotFoundError: [Errno 2] No such file or directory: models/brisque_svm.pickle但明明解压目录里看着有models文件夹。原因有三类一是zip包解压不完整尤其用某些国产解压软件时空目录或大文件被跳过二是文件名大小写不一致代码里写的是models/brisque_svm.pickle实际文件名是brisque_svm.Pickle三是这个模型文件是Git LFS指针源码托管平台只存了文本指针没传实际文件下载下来只有几百字节。解决方法是先file models/brisque_svm.pickle看文件类型pickle文件应该显示data而且以\x80开头只有几百字节的文本文件就是LFS空指针。确认真实文件缺失后从原发布页面重新下载完整包或者直接看README里有没有模型文件的备用下载地址。顺手把--model参数改成绝对路径能排除“当前工作目录不对”这种低级问题。5.2 彩色图与灰度图打分结果对不上现象是同一张图转成灰度后分数从45跳到70肉眼明明没啥差别。原因在于特征提取内部对输入通道的处理不统一有的实现只取Y通道亮度计算灰度图直接当单通道用有的实现要求三通道输入遇到灰度图会复制成三通道再算。两种预处理下的MSCN系数统计量不一样分数自然不可比。解决的办法是统一入口。在调用打分函数之前强制把灰度图复制成三通道import numpy as np def ensure_rgb(img): if img.ndim 2: return np.stack([img] * 3, axis-1) return img这样做的目的是让灰度图和彩色图走同一条计算路径。定这个规则之前先用20张图测一下灰度版和彩色版分数的均值偏移选一个方向固定下来不要今天用Y通道明天用三通道。5.3 超小图、超大图特征算出nan或无穷大现象是输入一张8x8的小图score直接是nan输入一张8000万像素的航拍图跑了十几分钟不出结果。原因很简单局部统计窗口是7x78x8的图算完MSCN后边界区域占了大半统计特征基本是噪声超大图则是数组太大特征计算里的临时矩阵叠加起来把内存吃满。解决要分两头。小图在预处理阶段直接拦截短边小于32像素的图不参与评价单独打一个“过小无法评价”的标记。大图则统一缩放到短边为512再打分。但这里有个隐蔽的坑缩放会让模糊程度变轻——一张1080p的模糊图缩到512后模糊看起来就没那么严重了分数会变好。所以缩放策略必须对全量图片一致分数只用于排序不要拿缩放后的分数绝对值当真实质量。批量处理前先拿100张图分别测512和720两个分辨率下的分数确认排序基本一致再固化参数。5.4 批量跑10000张图内存先崩了现象是用concurrent.futures.ThreadPoolExecutor批量打分跑了2000张内存涨到16G然后进程被系统杀掉。原因是特征提取是CPU密集计算Python的多线程受GIL限制线程再多CPU也跑不满反而每张图生成的numpy临时数组把内存堆爆。解决是用进程池替代线程池ProcessPoolExecutor的max_workers设为CPU物理核数减1给系统留一个核跑其他任务。另外注意进程池提交的是文件路径不是图像数组——让子进程自己去读文件读进内存的数组处理完就随进程退出释放不会在主进程里累积。Windows环境还要把创建进程池的代码包在if __name__ __main__:里否则子进程会递归执行主模块直接报RuntimeError。代码写法在第6章会给出这里先记住一个结论CPU密集的图像处理进程池配路径输入是批量打分的底线配置。5.5 纯色块、文字截图、UI界面打分完全失效现象是最离谱的一种一张纯白图打出90分一张带几行文字的截图打出15分肉眼觉得两张图的“质量差异”远没有分差这么大。原因要回到NR-IQA的基本假设这套统计特征描述的是自然图像的失真程度。纯色块的MSCN系数几乎全为0GGD拟合得到的是无意义的参数——gamma函数在0和无穷的边界上数值不稳定文字截图的强边缘和规则纹理被当成“高对比度伪影”模型会认为图“失真严重”。解决的方向是前置分流先用简单规则把“非自然图像”拦下来。像素标准差小于1判为纯色图用Canny边缘检测统计边缘密度超过阈值的判为文字截图或UI图。然后原始分数只用于自然照片非自然图像走另一个策略比如截图表直接按清晰度阈值判断纯色图直接标记“无需评价”。这个前置规则本身不复杂但能挡住90%的离谱分数。我见过有人拿截图测试集硬调SVR模型参数越调越糟——因为问题根本不在模型在输入域。6. 把单张打分变成批量质检服务并发、阈值与HTTP接口6.1 文件夹批量打分进程池并发数怎么设不翻车跑通单张之后批量打分是第一步。核心是用进程池摊满CPU同时避免内存累积from concurrent.futures import ProcessPoolExecutor from pathlib import Path def score_one(path): # 子进程里独立打分返回文件名和分数 s run_model(str(path)) return str(path), s if __name__ __main__: paths list(Path(imgs).glob(*.jpg)) with ProcessPoolExecutor(max_workers8) as pool: results pool.map(score_one, paths, chunksize64) for name, score in results: print(name, score)chunksize64的意思是每次往子进程分发64个路径减少主进程和子进程之间的通信开销路径比图像数组轻得多所以传路径不传数组。max_workers8在物理8核机器上比较合适16核对32核机器可以加到os.cpu_count() - 1留一个核给系统。6.2 阈值不是拍脑袋定的用业务标注搜一遍很多团队直接把分数80定为“劣质”这是拍脑袋。正确做法是在你的业务标注数据上做个简单搜索把200张已标注图按分数排序然后遍历不同阈值算这个阈值下的“拦截准确率”和“漏放比例”选一个业务能接受的平衡点。业务诉求阈值策略质量拦截宁可错杀不可漏放取排序后最差10%的分数为阈值质量分级A/B/C档用MOS校准后的分位点切三段告警只关心突发劣质对比昨天同时间段分数分布超过3个标准差才告警别指望不同批次的图用同一个绝对阈值内容分布一变分数分布就整体偏移。固定用排序百分位比固定分数值稳得多。6.3 用Flask包成HTTP打分接口批量脚本只能离线用业务系统需要的是在线接口。Flask封装一个POST接口是最常见的做法from flask import Flask, request, jsonify import cv2, numpy as np app Flask(__name__) app.post(/score) def score_image(): data request.files[image].read() img cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR) if img is None: return jsonify({error: invalid image}), 400 return jsonify({score: float(run_model(img))}) if __name__ __main__: app.run(host0.0.0.0, port8000, workers2)生产部署时用gunicorn起多worker每个worker进程独立加载一次模型避免进程间共享大对象导致的内存开销。注意cv2.imdecode不接受bytes直接解码必须先np.frombuffer再decode这是Python和OpenCV之间最常见的编码坑。6.4 什么时候该换更重的模型如果业务是纯自然照片的质量排序经典统计模型够用且稳定。但当图里混着美颜滤镜、HDR合成、夜景降噪这类“非物理失真”统计特征明显不够用——这类失真在MSCN系数上不是单调变化的经典模型会上下乱跳。这时候才值得切到学习型NR-IQA拿几千张业务自采图做主观标注微调一个轻量分类网络。但要控制预期标注成本很高而且标注一致性本身就会引入新的噪声。我现在拿到任何新的质量评价工具第一反应永远是拿20张自己业务里的图跑一遍看分数分布、看排序合不合理然后再决定要不要信它——这个习惯救过我很多次。希望帮到你。本文还有配套的精品资源点击获取
返回列表