图像质量评价这块绕不开的就是那几个经典数据集。LIVE、TID2013、CSIQ、KADID-10k、MLIVE、Waterloo做IQAImage Quality Assessment研究的人基本都跟它们打过交道。但真正上手的时候很多人卡在第一步——数据怎么下、主观评分文件怎么读、DMOS和MOS到底有什么区别、格式为什么每个数据集都不一样。我自己在这个环节踩过不少坑有的数据集下载页面失效了要换镜像源有的评分文件编码格式诡异导致读出来全是乱码还有的参考图和失真图命名规则对不上号。这篇文章就把这6个常用IQA数据集的主观评分获取和格式解析完整梳理一遍从文件结构到代码实现从字段含义到常见坑点尽量做到你照着操作就能跑通。不管你是刚入门IQA的新手还是想快速把多个数据集统一成标准格式的老手下面的内容应该都能帮你省不少时间。1. 六大IQA数据集整体认知与选型思路1.1 为什么是这六个数据集做图像质量评价数据集的选择直接决定了你实验结论的可信度和论文的接受率。LIVE、TID2013、CSIQ这三个是传统IQA领域的“老三样”几乎每篇对比实验都会用到。KADID-10k是后来居上的大规模数据集失真类型更丰富。MLIVE和Waterloo则是各有侧重的补充。先看一张整体对比表心里有个数数据集参考图数量失真图数量失真类型数主观评分类型评分范围LIVE297795DMOS[0, 100]TID201325300024MOS[0, 9]CSIQ308666DMOS[0, 1]KADID-10k811012525MOS[1, 5]MLIVE154504MOS[0, 100]Waterloo47494804MOS[0, 100]这张表里的信息量其实很大。LIVE虽然失真图不多但它是DMOS评分的鼻祖很多经典算法PSNR、SSIM、MS-SSIM的基准性能都是在LIVE上报告的。TID2013失真类型最多24种适合做失真泛化性分析。CSIQ的DMOS范围是[0,1]跟LIVE的[0,100]不一样混用的时候要特别注意归一化。KADID-10k的MOS是[1,5]的5级量表跟其他数据集的评分尺度都不同。1.2 DMOS和MOS的本质区别这个问题我被问过无数次这里一次性说清楚。MOSMean Opinion Score是主观评分的直接平均值。比如让20个人对一张图打分每人打1到5分最后取平均得到的就是MOS。分数越高表示质量越好。DMOSDifferential Mean Opinion Score是差分平均主观评分。它是在MOS的基础上减去了参考图的MOS再经过一定的线性变换。分数越高表示失真越严重、质量越差。用公式表示就是DMOS (MOS_ref - MOS_dis) / (MOS_ref - MOS_min) * 100其中MOS_ref是参考图的MOSMOS_dis是失真图的MOSMOS_min是评分量表的下限。注意不同数据集对DMOS的计算细节有差异LIVE和CSIQ的具体公式不完全一样。LIVE是先做Z-score标准化再做线性映射CSIQ是直接做线性缩放。不要以为DMOS的定义是统一的。这个区别在实际使用中的影响是如果你的算法输出的是质量分数越高越好在LIVE上评估时就需要把预测值取反或者做映射才能跟DMOS对齐。很多论文里没写清楚这一点导致复现时结果对不上。1.3 选型建议什么场景用哪个数据集做传统算法对比LIVE TID2013 CSIQ这是标配三件套做深度学习IQAKADID-10k TID2013数据量够大不容易过拟合做跨数据集泛化用LIVE训练在TID2013和CSIQ上测试这是最常见的protocol做特定失真类型研究看TID2013的24种失真类型按需选取做大规模预训练Waterloo的9480张失真图可以用来自监督预训练2. 各数据集主观评分文件格式深度解析2.1 LIVE数据集DMOS文件结构与读取LIVE数据集的评分文件叫dmos_realigned.mat是一个MATLAB的.mat文件。里面包含两个变量dmos和dmos_std分别是DMOS均值和标准差。读取方式Pythonimport scipy.io as sio import numpy as np # 读取DMOS文件 mat sio.loadmat(dmos_realigned.mat) dmos mat[dmos][0] # 形状为(779,)的数组 dmos_std mat[dmos_std][0] # 对应的图像文件名在refnames_all和orgs中 refnames mat[refnames_all][0] # 失真图文件名 orgs mat[orgs][0] # 参考图编号这里有个坑dmos数组的顺序跟refnames_all是一一对应的但refnames_all里的文件名格式是I01_01_1.bmp这种你需要根据命名规则解析出参考图编号和失真类型。LIVE的命名规则是I{参考图编号}_{失真类型编号}_{失真等级}.bmp。失真类型编号对应关系编号失真类型01JP2K压缩02JPEG压缩03白噪声04高斯模糊05快速衰落实操心得LIVE的DMOS值范围理论上是[0,100]但实际数据中最小值可能是负数。这是因为Z-score标准化后的线性映射导致的。如果你要做归一化建议先clip到[0,100]再除以100。2.2 TID2013数据集MOS文件与24种失真类型TID2013的评分文件是mos_with_names.txt纯文本格式每行是“MOS值 文件名”的形式。看起来简单但有几个细节要注意。文件内容示例6.3524 I01_01_1.bmp 5.9473 I01_01_2.bmp ...读取代码def read_tid2013_mos(filepath): mos_dict {} with open(filepath, r) as f: for line in f: parts line.strip().split() if len(parts) 2: mos_val float(parts[0]) img_name parts[1] mos_dict[img_name] mos_val return mos_dictTID2013的MOS范围是[0, 9]分数越高质量越好。它的24种失真类型编号如下编号失真类型编号失真类型01加性高斯噪声13高斯模糊02加性噪声颜色分量14压缩伪影03空间相关噪声15对比度变化04掩蔽噪声16饱和度变化05高频噪声17JPEG2000压缩06脉冲噪声18JPEG压缩07量化噪声19噪声压缩08高斯噪声20振铃伪影09图像去噪21局部块失真10JPEG传输错误22亮度变化11JPEG2000传输错误23色差12对比度拉伸24稀疏采样注意TID2013的参考图是25张但编号从I01到I25。失真图命名格式是I{参考图编号}_{失真类型编号}_{失真等级}.bmp失真等级从1到5。2.3 CSIQ数据集DMOS格式与特殊之处CSIQ的评分文件是csiq.DMOS.xlsx对是Excel文件。这在IQA数据集里算是比较少见的。你需要用openpyxl或pandas来读取。import pandas as pd df pd.read_excel(csiq.DMOS.xlsx) # 列名通常是image, dmos, dst_type, ref_num print(df.head())CSIQ的DMOS范围是[0, 1]跟LIVE的[0, 100]完全不同。而且CSIQ的DMOS是越小表示质量越好还是越大表示质量越好答案是越大表示失真越严重跟LIVE的DMOS方向一致但尺度不同。CSIQ的6种失真类型编号失真类型1JPEG压缩2JPEG2000压缩3全局对比度退化4加性高斯白噪声5加性高斯粉噪声6高斯模糊CSIQ的参考图是30张失真图866张。命名格式是{参考图名}.{失真类型}.{等级}.png比如1600.1.1.png。实操心得CSIQ的Excel文件里DMOS列有时候会有空值需要先dropna。另外CSIQ的参考图本身也有DMOS值通常是0读取时要注意区分。2.4 KADID-10k数据集MOS格式与大规模处理KADID-10k的评分文件是kadid10k.csvCSV格式包含三列image_name,dmos,mos。注意这里同时提供了DMOS和MOS但实际常用的是MOS。import pandas as pd df pd.read_csv(kadid10k.csv) # image_name: 如 I01_01_01.png # dmos: 差分评分 # mos: 原始MOS范围[1, 5]KADID-10k的MOS范围是[1, 5]分数越高质量越好。它的25种失真类型覆盖了各种压缩、噪声、模糊、颜色变换等。注意KADID-10k的文件名格式是I{参考图编号}_{失真类型编号}_{失真等级}.png参考图编号从I01到I81失真等级从01到05。2.5 MLIVE与Waterloo补充数据集的评分格式MLIVE的评分文件是MLIVE_DMOS.matMATLAB格式。包含dmos和dmos_std两个变量。MLIVE有15张参考图450张失真图4种失真类型JPEG、JP2K、模糊、噪声。Waterloo的评分文件是waterloo_dmos.txt或类似名称纯文本格式。Waterloo的特点是参考图数量多474张失真图9480张但每种失真的等级较少。这两个数据集在实际论文中使用频率不如前四个但在做大规模实验或特定分析时很有价值。3. 统一格式转换与实操流程3.1 为什么要做格式统一你如果只用一个数据集那无所谓按它原来的格式读就行。但做IQA研究几乎不可避免要在多个数据集上做交叉验证。这时候如果每个数据集都写一套读取代码不仅冗余还容易出错。统一格式的目标是把所有数据集转换成一个标准的DataFrame包含以下字段字段名含义类型img_path失真图路径strref_path参考图路径strscore主观评分floatscore_type评分类型DMOS/MOSstrdataset数据集名称strdistortion失真类型str3.2 统一转换代码实现下面是一个完整的转换脚本框架import os import scipy.io as sio import pandas as pd import numpy as np class IQADatasetConverter: def __init__(self, root_dir): self.root_dir root_dir self.records [] def convert_live(self): live_dir os.path.join(self.root_dir, LIVE) mat sio.loadmat(os.path.join(live_dir, dmos_realigned.mat)) dmos mat[dmos][0] refnames mat[refnames_all][0] for i, name in enumerate(refnames): name str(name[0]) if isinstance(name, np.ndarray) else str(name) img_path os.path.join(live_dir, distorted, name) # 解析参考图编号 ref_num name.split(_)[0] ref_path os.path.join(live_dir, refimgs, f{ref_num}.bmp) self.records.append({ img_path: img_path, ref_path: ref_path, score: float(dmos[i]), score_type: DMOS, dataset: LIVE, distortion: name.split(_)[1] }) def convert_tid2013(self): tid_dir os.path.join(self.root_dir, TID2013) mos_dict {} with open(os.path.join(tid_dir, mos_with_names.txt), r) as f: for line in f: parts line.strip().split() if len(parts) 2: mos_dict[parts[1]] float(parts[0]) for img_name, mos in mos_dict.items(): img_path os.path.join(tid_dir, distorted, img_name) ref_num img_name.split(_)[0] ref_path os.path.join(tid_dir, reference, f{ref_num}.bmp) self.records.append({ img_path: img_path, ref_path: ref_path, score: mos, score_type: MOS, dataset: TID2013, distortion: img_name.split(_)[1] }) def convert_csiq(self): csiq_dir os.path.join(self.root_dir, CSIQ) df pd.read_excel(os.path.join(csiq_dir, csiq.DMOS.xlsx)) df df.dropna(subset[dmos]) for _, row in df.iterrows(): img_name row[image] img_path os.path.join(csiq_dir, dst_imgs, img_name) ref_name img_name.split(.)[0] .png ref_path os.path.join(csiq_dir, src_imgs, ref_name) self.records.append({ img_path: img_path, ref_path: ref_path, score: float(row[dmos]), score_type: DMOS, dataset: CSIQ, distortion: str(row.get(dst_type, )) }) def get_dataframe(self): return pd.DataFrame(self.records)实操心得转换过程中最容易出错的地方是参考图路径的拼接。不同数据集的参考图命名规则不同LIVE是I01.bmpTID2013是I01.bmpCSIQ是1600.png。建议先手动确认几个样本的路径是否正确再批量跑。3.3 评分归一化处理统一格式之后还有一个关键步骤评分归一化。因为DMOS和MOS的尺度不同跨数据集评估时需要统一到相同的范围。常用的归一化方法有两种方法一Min-Max归一化到[0,1]def minmax_normalize(scores): min_val scores.min() max_val scores.max() return (scores - min_val) / (max_val - min_val)方法二Z-score标准化def zscore_normalize(scores): mean_val scores.mean() std_val scores.std() return (scores - mean_val) / std_val注意归一化之后DMOS的方向问题依然存在。如果你的模型输出是“质量分数越高越好”那DMOS需要取反。建议在DataFrame里加一列normalized_score统一成“越高越好”的方向。4. 常见问题与排查技巧实录4.1 文件读取失败与编码问题问题现象读取TID2013的mos_with_names.txt时出现UnicodeDecodeError。原因文件可能包含BOM头或者使用了非UTF-8编码。解决方法with open(filepath, r, encodingutf-8-sig) as f: # 或者尝试 latin-1 content f.read()问题现象读取CSIQ的Excel文件时提示openpyxl未安装。解决方法pip install openpyxl4.2 文件名匹配不上的排查思路这是最常见的问题。表现是读取评分文件成功但根据文件名去拼接图像路径时发现文件不存在。排查步骤打印评分文件中的前5个文件名用os.listdir列出图像目录的前10个文件名对比命名规则是否一致大小写、扩展名、分隔符常见的不一致情况数据集评分文件中的名称实际文件名问题LIVEI01_01_1.bmpi01_01_1.bmp大小写TID2013I01_01_1.bmpI01_01_1.BMP扩展名大小写CSIQ1600.1.1.png1600.1.1.PNG扩展名大小写实操心得写一个find_file_case_insensitive函数在文件不存在时自动尝试大小写变体能省很多事。4.3 DMOS方向搞反导致模型不收敛这个问题非常隐蔽。你的模型训练loss正常下降但验证集上的PLCC和SROCC一直是负数或者接近0。原因就是DMOS的方向跟模型输出相反。排查方法计算预测值和DMOS的Spearman相关系数如果是显著负相关说明方向反了。解决方法在标签处理阶段统一方向。对于DMOS取反或者用max_val - dmos转换。4.4 跨数据集评估时的尺度对齐在LIVE上训练的模型直接拿到TID2013上测试性能通常会掉很多。除了域偏移之外评分尺度不同也是重要原因。解决方法在测试集上做非线性拟合如逻辑函数拟合把预测值映射到测试集的评分尺度上。这是IQA领域的标准做法VQEG提出的拟合函数是Q β1 * (0.5 - 1/(1exp(β2*(x-β3)))) β4*x β5其中x是预测值Q是拟合后的值β1到β5是通过最小二乘法拟合的参数。注意做拟合时要用测试集的全部数据但不要用测试集的标签来训练模型。拟合只是评估时的后处理步骤。4.5 常见问题速查表问题可能原因解决方法读取.mat文件报错scipy版本不兼容升级scipy或使用h5pyMOS值超出预期范围数据集版本不同检查数据集版本和对应论文图像路径拼接错误命名规则不一致打印文件名对比相关系数为负DMOS方向反了取反或做尺度变换跨数据集性能骤降尺度不匹配做非线性拟合Excel读取失败缺少openpyxlpip install openpyxl内存不足一次性加载所有图像使用DataLoader分批加载5. 批量处理与自动化脚本实战5.1 一键转换所有数据集把前面的转换逻辑封装成一个脚本支持命令行参数import argparse def main(): parser argparse.ArgumentParser() parser.add_argument(--root, typestr, requiredTrue) parser.add_argument(--output, typestr, defaultiqa_metadata.csv) parser.add_argument(--datasets, nargs, default[LIVE, TID2013, CSIQ]) args parser.parse_args() converter IQADatasetConverter(args.root) if LIVE in args.datasets: converter.convert_live() if TID2013 in args.datasets: converter.convert_tid2013() if CSIQ in args.datasets: converter.convert_csiq() df converter.get_dataframe() df.to_csv(args.output, indexFalse) print(fTotal records: {len(df)}) print(df.groupby(dataset).size()) if __name__ __main__: main()运行方式python convert_iqa.py --root /data/IQA --output metadata.csv --datasets LIVE TID2013 CSIQ5.2 数据完整性校验转换完成后建议做一次完整性校验确保每条记录的图像文件都存在def validate_records(df): missing [] for idx, row in df.iterrows(): if not os.path.exists(row[img_path]): missing.append((img, row[img_path])) if not os.path.exists(row[ref_path]): missing.append((ref, row[ref_path])) if missing: print(fFound {len(missing)} missing files:) for t, p in missing[:10]: print(f [{t}] {p}) else: print(All files validated.) return missing实操心得校验这一步千万别省。我有一次跑了一整天的实验最后发现CSIQ的参考图路径全错了因为CSIQ的参考图扩展名是.png而不是.bmp。如果提前做了校验五分钟就能发现。5.3 生成PyTorch Dataset最后一步把统一的DataFrame封装成PyTorch Dataset方便直接用于训练from torch.utils.data import Dataset from PIL import Image import torchvision.transforms as T class IQADataset(Dataset): def __init__(self, df, transformNone): self.df df.reset_index(dropTrue) self.transform transform or T.Compose([ T.Resize((256, 256)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img Image.open(row[img_path]).convert(RGB) ref Image.open(row[ref_path]).convert(RGB) img_tensor self.transform(img) ref_tensor self.transform(ref) score torch.tensor(row[score], dtypetorch.float32) return img_tensor, ref_tensor, score这个Dataset同时返回失真图和参考图适用于全参考IQA模型。如果是无参考模型把ref_tensor去掉即可。5.4 处理大文件时的内存优化Waterloo和KADID-10k的数据量比较大一次性加载所有图像到内存不现实。除了用DataLoader的num_workers参数之外还有几个技巧使用Image.open的懒加载模式不要提前convert(RGB)在__getitem__里做resize而不是在__init__里预加载如果磁盘IO是瓶颈考虑把图像转成LMDB格式import lmdb import pickle def create_lmdb(df, lmdb_path): env lmdb.open(lmdb_path, map_size1099511627776) # 1TB with env.begin(writeTrue) as txn: for idx, row in df.iterrows(): with open(row[img_path], rb) as f: img_bytes f.read() txn.put(fimg_{idx}.encode(), img_bytes) env.close()注意LMDB的map_size要根据数据集大小设置太小会报错太大浪费磁盘空间。KADID-10k大概需要20GB左右。6. 评分分析与可视化实操6.1 评分分布对比拿到统一格式的数据后第一件事应该是看评分分布。不同数据集的评分分布差异很大这直接影响模型的训练策略。import matplotlib.pyplot as plt import seaborn as sns fig, axes plt.subplots(1, 3, figsize(15, 4)) for ax, dataset in zip(axes, [LIVE, TID2013, CSIQ]): subset df[df[dataset] dataset] ax.hist(subset[score], bins30, edgecolorblack) ax.set_title(f{dataset} Score Distribution) ax.set_xlabel(Score) ax.set_ylabel(Count) plt.tight_layout() plt.savefig(score_distributions.png, dpi150)从分布图上你能看出很多东西。LIVE的DMOS分布比较均匀TID2013的MOS集中在中间区域CSIQ的DMOS在低分段比较密集。这些信息可以帮助你决定是否需要对评分做重采样或者加权。6.2 失真类型与评分的相关性分析以TID2013为例24种失真类型对评分的影响程度不同。做一个箱线图tid_df df[df[dataset] TID2013].copy() tid_df[distortion] tid_df[img_path].apply( lambda x: os.path.basename(x).split(_)[1] ) plt.figure(figsize(16, 6)) sns.boxplot(xdistortion, yscore, datatid_df) plt.xticks(rotation45) plt.title(TID2013: MOS by Distortion Type) plt.tight_layout() plt.savefig(tid2013_distortion_boxplot.png, dpi150)这个图能帮你快速识别哪些失真类型的评分方差大、哪些方差小。方差大的失真类型通常更难预测也是模型提升的重点方向。6.3 参考图与失真图的配对检查全参考IQA模型需要参考图和失真图配对。在训练之前建议做一次配对检查确保每张失真图都能找到对应的参考图。def check_pairing(df): paired df.apply( lambda row: os.path.exists(row[ref_path]), axis1 ) print(fPaired: {paired.sum()}/{len(df)}) if not paired.all(): print(Unpaired samples:) print(df[~paired][[img_path, ref_path]].head()) return paired实操心得CSIQ的参考图命名跟失真图命名不是简单的前缀关系。失真图是1600.1.1.png参考图是1600.png。你需要取失真图文件名的第一个点之前的部分再加上.png。这个规则跟LIVE和TID2013都不同写代码时要单独处理。6.4 跨数据集评分尺度对齐的可视化最后做一个跨数据集评分尺度的对比图直观展示不同数据集的评分范围差异fig, ax plt.subplots(figsize(10, 5)) datasets [LIVE, TID2013, CSIQ, KADID-10k] colors [#1f77b4, #ff7f0e, #2ca02c, #d62728] for ds, color in zip(datasets, colors): subset df[df[dataset] ds] if len(subset) 0: ax.scatter( [ds] * len(subset), subset[score], alpha0.3, s5, colorcolor ) ax.set_ylabel(Raw Score) ax.set_title(Score Scale Comparison Across Datasets) plt.tight_layout() plt.savefig(score_scale_comparison.png, dpi150)这张图在写论文的时候很有用可以直接放在实验设置部分说明为什么需要做尺度对齐。7. 实操中踩过的坑与经验总结7.1 数据集下载的坑LIVE数据集的官方下载页面是live.ece.utexas.edu但这个网站有时候会抽风下载速度极慢。我一般会找学术镜像站或者从其他实验室的公开FTP下载。TID2013的下载相对稳定但文件比较大约2GB建议用下载工具。CSIQ的下载页面是vision.okstate.edu文件不大但要注意下载的是完整版还是精简版。KADID-10k在GitHub上有官方仓库直接clone就行。注意下载完成后一定要校验文件完整性。我遇到过一次TID2013下载不完整解压后少了300多张图跑实验的时候才发现。7.2 评分文件版本差异同一个数据集可能有多个版本的评分文件。比如LIVE有dmos_realigned.mat和dmos.mat两个版本前者是重新对齐过的后者是原始的。建议用dmos_realigned.mat因为它的评分一致性更好。TID2013的mos_with_names.txt也有不同版本有的包含所有3000张图的评分有的只包含部分。下载时看清楚说明。7.3 图像格式转换的注意事项有些数据集的图像是BMP格式文件很大。转成PNG或JPEG可以节省大量磁盘空间但要注意JPEG是有损压缩转格式会引入额外失真影响评分一致性PNG是无损的但文件大小可能比BMP还大如果要做格式转换建议只转参考图失真图保持原格式我个人的做法是不转格式直接用一个大的SSD存所有数据。现在2TB的NVMe SSD也不贵比折腾格式转换省心多了。7.4 多进程读取的性能优化当数据集比较大时单进程读取图像会成为训练瓶颈。PyTorch的DataLoader支持多进程但有几个参数需要调from torch.utils.data import DataLoader loader DataLoader( dataset, batch_size32, shuffleTrue, num_workers8, # 根据CPU核心数调整 pin_memoryTrue, # 加速GPU传输 prefetch_factor4, # 每个worker预取的数据量 persistent_workersTrue # 避免每个epoch重新创建worker )实操心得num_workers不是越大越好。我试过设成16结果因为磁盘IO瓶颈反而比8还慢。建议从4开始试逐步增加找到最优值。另外如果数据集在机械硬盘上num_workers设太大也没用因为磁盘寻道时间才是瓶颈。7.5 评分异常值的处理主观评分数据里偶尔会有异常值比如某张图的MOS明显偏离同失真类型同等级的其他图。这些异常值可能来自标注者的失误或者数据录入错误。处理方法def remove_outliers(df, z_thresh3): df df.copy() for ds in df[dataset].unique(): mask df[dataset] ds scores df.loc[mask, score] z_scores np.abs((scores - scores.mean()) / scores.std()) df.loc[mask (z_scores z_thresh), score] np.nan return df.dropna(subset[score])注意删异常值要谨慎。有些“异常值”其实是真实的极端失真情况删掉会导致模型在极端情况下的性能下降。建议先可视化确认再决定是否删除。7.6 跨数据集实验的protocol设计最后说一个方法论层面的经验。做跨数据集实验时最常见的protocol是在源数据集上训练在目标数据集上直接测试zero-shot在目标数据集上做非线性拟合后再测试第3步的拟合是在测试集上做的这算不算数据泄露在IQA领域这被认为是标准做法因为拟合只用了4-5个参数而且不涉及模型参数更新。但写论文时要明确说明这一点避免审稿人质疑。另一个protocol是在目标数据集上fine-tune。这时候要注意fine-tune用的数据不能跟测试数据重叠。通常的做法是目标数据集的一半用于fine-tune另一半用于测试。我个人在实际操作中的体会是跨数据集实验的结果对随机种子非常敏感。同一个模型换个种子SROCC可能差0.05以上。所以建议至少跑3次取平均值和标准差这样结论才可靠。另外数据集的加载顺序也会影响结果建议在训练前先shuffle一遍并且固定随机种子保证实验可复现。
