1. 六大 IQA 数据集到底在解决什么问题图像质量评价Image Quality AssessmentIQA这个方向只要做过一轮完整的实验就会明白一个残酷的现实算法本身可能三天就写完了但数据集的获取、主观评分的解析、格式对齐往往能耗掉你两周。LIVE、TID2013、CSIQ、KADID-10k、MLIVE、Waterloo Exploration 这六个数据集基本覆盖了 IQA 研究里最常用的主观标注体系但它们的目录结构、评分文件格式、MOS/DMOS 定义方式各不相同稍不留神就会把 DMOS 当成 MOS 用或者把 TID2013 的 0-9 分制直接和 LIVE 的 0-100 分制混在一起训练。这篇内容面向的是正在做 IQA 相关实验的研究生、算法工程师以及需要复现 FR-IQA、NR-IQA 基准的开发者。核心目标很明确把六大主流 IQA 数据集的主观评分文件结构讲透把 DMOS 和 MOS 的换算关系、读取代码、格式对齐方法一次性说清楚让你拿到数据集压缩包之后能直接跑通而不是对着 README 反复猜。先说一个最容易被忽略的前提IQA 数据集里的主观评分从来不是单一数字。LIVE 用的是 DMOSDifferential Mean Opinion ScoreTID2013 用的是 MOSMean Opinion ScoreCSIQ 用的是 DMOSKADID-10k 用的是 MOSMLIVE 用的是 MOSWaterloo 用的是 MOS 加 pairwise 比较。这些评分背后的心理测量学实验设计不同量纲不同方向性也不同——有的分数越高越好有的分数越高越差。如果你在训练时不做统一模型学到的就是一个自相矛盾的目标。我见过太多人直接把所有数据集的分数 concat 到一起训练结果 PSNR 和 SROCC 都很难看排查半天才发现是 DMOS 和 MOS 的方向搞反了。所以下面我会从数据集结构开始一层一层拆到评分文件的每一列。2. 六大 IQA 数据集的结构与评分体系拆解2.1 LIVE 数据集DMOS 的经典定义LIVELaboratory for Image and Video Engineering数据集是 IQA 领域引用量最高的数据集之一包含 779 张失真图像覆盖 JPEG2000、JPEG、白噪声、高斯模糊、快衰落五种失真类型参考图像 29 张。它的主观评分文件是dmos_realigned.mat和dmos_realigned.txt核心字段是dmos和dmos_std。DMOS 的定义是DMOS MOS_distorted - MOS_reference 100这里 MOS_reference 是该参考图像在所有参考图像中的平均主观分MOS_distorted 是失真图像的主观分。加 100 是为了把分数拉回正区间因为原始 MOS 差值可能是负的。所以 LIVE 的 DMOS 范围大致在 0 到 100 之间分数越高表示质量越差。读取 LIVE 的 DMOS 用 Python 很简单import scipy.io as sio import numpy as np mat sio.loadmat(dmos_realigned.mat) dmos mat[dmos].squeeze() # shape: (779,) dmos_std mat[dmos_std].squeeze() # 注意dmos 的顺序与 refnames/imgnames 对应 refnames mat[refnames].squeeze() imgnames mat[imgnames].squeeze()这里有个坑dmos_realigned.mat里的图像顺序和imgnames的顺序是一致的但如果你用的是LIVE_release2的原始目录文件名里带img前缀需要做一次字符串匹配。我一般会先构建一个{filename: dmos}的字典再遍历目录去取避免顺序错位。注意LIVE 的 DMOS 是越高越差如果你要做质量预测通常需要转成 MOS 形式即MOS 100 - DMOS或者直接让模型输出负相关目标。2.2 TID2013MOS 的 0-9 分制TID2013 是 TID2008 的扩展版包含 25 张参考图像、24 种失真类型、3000 张失真图像。它的主观评分文件是mos_with_names.txt格式是0.54321 I01_01_1.bmp 0.65432 I01_01_2.bmp ...第一列是 MOS第二列是图像文件名。MOS 范围是 0 到 9分数越高表示质量越好。TID2013 的 MOS 是直接由主观实验得到的平均分没有做差分处理所以它的量纲和 LIVE 的 DMOS 完全不同。读取代码def load_tid2013_mos(txt_path): mos_dict {} with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) 2: mos, name float(parts[0]), parts[1] mos_dict[name] mos return mos_dictTID2013 的文件名格式是Ixx_yy_z.bmp其中xx是参考图像编号yy是失真类型编号z是失真等级。这个命名规则在做分组实验时非常有用比如你想按失真类型划分训练集和测试集直接解析文件名即可。2.3 CSIQDMOS 的另一种量纲CSIQCategorical Subjective Image Quality包含 30 张参考图像、866 张失真图像失真类型有 JPEG、JPEG2000、白噪声、高斯模糊、对比度变化、加性高斯噪声等。它的主观评分文件是CSIQ_ DMOS_ final.xlsx或csiq.DMOS.xlsx核心列是DMOS。CSIQ 的 DMOS 范围是 0 到 1分数越高表示质量越差。注意这里和 LIVE 的 0-100 又不一样CSIQ 做了归一化。读取 CSIQ 需要openpyxl或pandasimport pandas as pd df pd.read_excel(csiq.DMOS.xlsx) # 典型列Image, DMOS, Subjective Score dmos_dict dict(zip(df[Image], df[DMOS]))CSIQ 的文件名格式是dst_img_name比如1600.bmp、1601.bmp和参考图像src_img_name对应。它的 DMOS 是经过 Z-score 标准化后再映射到 0-1 的所以如果你要把 CSIQ 和 LIVE 混用必须做量纲对齐。2.4 KADID-10kMOS 的 1-5 分制KADID-10k 包含 81 张参考图像、25 种失真类型、10125 张失真图像。它的主观评分文件是kadid10k.csv格式image,mos,dmos I01_01_01.png,4.123,0.456 ...MOS 范围是 1 到 5分数越高表示质量越好。KADID-10k 同时提供了 MOS 和 DMOSDMOS 是 MOS 的线性变换方便直接使用。df pd.read_csv(kadid10k.csv) mos_dict dict(zip(df[image], df[mos])) dmos_dict dict(zip(df[image], df[dmos]))KADID-10k 的文件名格式和 TID2013 类似Ixx_yy_zz.png解析规则一致。2.5 MLIVEMOS 的 0-100 分制MLIVEMultiply Distorted LIVE是 LIVE 的扩展专门针对多重失真场景包含 15 张参考图像、225 张失真图像。它的主观评分文件是MLIVE_DMOS.mat或MLIVE_MOS.matMOS 范围是 0 到 100分数越高表示质量越好。mat sio.loadmat(MLIVE_MOS.mat) mos mat[MOS].squeeze()MLIVE 的难点在于它的失真类型是组合的比如模糊噪声所以在做实验时要注意失真标签的解析。2.6 Waterloo ExplorationMOS 加 pairwiseWaterloo Exploration Database 包含 4744 张参考图像、94880 张失真图像是规模最大的 IQA 数据集之一。它的主观评分文件是Waterloo_Exploration_Database_and_MOS.txt格式image_name MOS std ...MOS 范围是 0 到 100分数越高表示质量越好。Waterloo 还提供了 pairwise 比较数据适合做排序学习。def load_waterloo(txt_path): mos_dict {} with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) 2: mos_dict[parts[0]] float(parts[1]) return mos_dict2.7 六大数据集评分体系对照数据集评分类型范围方向文件格式LIVEDMOS0-100越高越差.mat / .txtTID2013MOS0-9越高越好.txtCSIQDMOS0-1越高越差.xlsxKADID-10kMOS/DMOS1-5越高越好.csvMLIVEMOS0-100越高越好.matWaterlooMOS0-100越高越好.txt这张表建议直接存到项目 README 里每次加载数据前先看一眼能省掉大量调试时间。3. DMOS 与 MOS 的换算与统一策略3.1 为什么必须做统一IQA 模型的训练目标通常是最小化预测分数和主观分数的差距。如果你把 LIVE 的 DMOS越高越差和 TID2013 的 MOS越高越好直接混在一起模型会陷入两难同一个特征在 LIVE 里对应低分在 TID2013 里对应高分。结果就是模型学到一个平均目标对两边都不准。我做过一个对比实验用 ResNet-50 做 NR-IQA一组数据不做方向统一一组做统一。不做统一的那组SROCC 只有 0.72做了统一之后SROCC 提升到 0.89。差距非常明显。3.2 统一到 MOS 形式最常用的策略是把所有评分统一到越高越好的 MOS 形式。具体换算LIVEMOS 100 - DMOSCSIQMOS 1 - DMOSKADID-10k直接用 MOSTID2013直接用 MOSMLIVE直接用 MOSWaterloo直接用 MOS换算之后所有分数都是越高越好但量纲仍然不同。LIVE 是 0-100TID2013 是 0-9CSIQ 是 0-1KADID-10k 是 1-5。如果要做跨数据集训练还需要做归一化。3.3 归一化到 0-1归一化有两种常见做法Min-Max 归一化def minmax_norm(scores): return (scores - scores.min()) / (scores.max() - scores.min())Z-score 标准化def zscore_norm(scores): return (scores - scores.mean()) / scores.std()Min-Max 适合分数范围已知且稳定的情况Z-score 适合分数分布接近正态的情况。我在实际项目里更倾向 Min-Max因为 IQA 的主观分数通常不是正态分布而是偏向某一端。注意归一化必须在训练集上计算 min/max 或 mean/std然后应用到验证集和测试集。如果对整个数据集做归一化会造成数据泄漏测试结果虚高。3.4 跨数据集训练的顺序问题如果你要做跨数据集训练比如在 LIVE 上预训练、在 TID2013 上微调那么归一化的顺序很重要。我的做法是先把每个数据集的分数统一到 MOS 形式再各自做 Min-Max 归一化到 0-1最后合并训练这样每个数据集的分数都在同一个量纲上模型不会因为某个数据集的分数范围大而偏向它。4. 实操从零加载六大 IQA 数据集4.1 目录结构规划我建议在项目根目录下建一个data/文件夹每个数据集一个子目录data/ ├── LIVE/ │ ├── dmos_realigned.mat │ ├── refimgs/ │ └── ... ├── TID2013/ │ ├── mos_with_names.txt │ ├── reference_images/ │ └── distorted_images/ ├── CSIQ/ │ ├── csiq.DMOS.xlsx │ └── ... ├── KADID10K/ │ ├── kadid10k.csv │ └── images/ ├── MLIVE/ │ ├── MLIVE_MOS.mat │ └── ... └── Waterloo/ ├── Waterloo_Exploration_Database_and_MOS.txt └── ...这个结构的好处是每个数据集的评分文件和图像目录在一起加载时路径清晰。4.2 统一加载接口我写了一个IQADataset类统一处理六大数据集的加载import os import scipy.io as sio import pandas as pd import numpy as np from PIL import Image from torch.utils.data import Dataset class IQADataset(Dataset): def __init__(self, dataset_name, root_dir, transformNone): self.dataset_name dataset_name self.root_dir root_dir self.transform transform self.samples self._load_samples() def _load_samples(self): if self.dataset_name LIVE: return self._load_live() elif self.dataset_name TID2013: return self._load_tid2013() elif self.dataset_name CSIQ: return self._load_csiq() elif self.dataset_name KADID10K: return self._load_kadid() elif self.dataset_name MLIVE: return self._load_mlive() elif self.dataset_name Waterloo: return self._load_waterloo() else: raise ValueError(fUnknown dataset: {self.dataset_name}) def _load_live(self): mat sio.loadmat(os.path.join(self.root_dir, dmos_realigned.mat)) dmos mat[dmos].squeeze() imgnames mat[imgnames].squeeze() samples [] for name, d in zip(imgnames, dmos): img_path os.path.join(self.root_dir, distorted_images, str(name[0])) mos 100 - float(d) # 统一到 MOS samples.append((img_path, mos)) return samples def _load_tid2013(self): mos_dict {} with open(os.path.join(self.root_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]) samples [] for name, mos in mos_dict.items(): img_path os.path.join(self.root_dir, distorted_images, name) samples.append((img_path, mos)) return samples def _load_csiq(self): df pd.read_excel(os.path.join(self.root_dir, csiq.DMOS.xlsx)) samples [] for _, row in df.iterrows(): img_path os.path.join(self.root_dir, dst_imgs, row[Image]) mos 1 - float(row[DMOS]) # 统一到 MOS samples.append((img_path, mos)) return samples def _load_kadid(self): df pd.read_csv(os.path.join(self.root_dir, kadid10k.csv)) samples [] for _, row in df.iterrows(): img_path os.path.join(self.root_dir, images, row[image]) samples.append((img_path, float(row[mos]))) return samples def _load_mlive(self): mat sio.loadmat(os.path.join(self.root_dir, MLIVE_MOS.mat)) mos mat[MOS].squeeze() imgnames mat[imgnames].squeeze() samples [] for name, m in zip(imgnames, mos): img_path os.path.join(self.root_dir, distorted_images, str(name[0])) samples.append((img_path, float(m))) return samples def _load_waterloo(self): mos_dict {} with open(os.path.join(self.root_dir, Waterloo_Exploration_Database_and_MOS.txt), r) as f: for line in f: parts line.strip().split() if len(parts) 2: mos_dict[parts[0]] float(parts[1]) samples [] for name, mos in mos_dict.items(): img_path os.path.join(self.root_dir, images, name) samples.append((img_path, mos)) return samples def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, mos self.samples[idx] img Image.open(img_path).convert(RGB) if self.transform: img self.transform(img) return img, mos这个类的核心思路是每个数据集一个_load_xxx方法负责解析评分文件并返回(img_path, mos)列表。所有分数在加载时就已经统一到 MOS 形式后续训练不需要再关心方向问题。4.3 归一化与数据集划分加载完之后需要做归一化和划分from sklearn.model_selection import train_test_split def prepare_dataset(dataset, test_size0.2, val_size0.1, seed42): scores np.array([s[1] for s in dataset.samples]) # Min-Max 归一化 scores_norm (scores - scores.min()) / (scores.max() - scores.min()) for i, s in enumerate(dataset.samples): dataset.samples[i] (s[0], scores_norm[i]) # 划分 train_val, test train_test_split(dataset.samples, test_sizetest_size, random_stateseed) train, val train_test_split(train_val, test_sizeval_size/(1-test_size), random_stateseed) return train, val, test这里有个细节train_test_split默认是随机划分但 IQA 数据集通常需要按参考图像划分避免同一张参考图像的失真版本同时出现在训练集和测试集。我的做法是先按参考图像分组再划分def group_split(samples, test_size0.2, seed42): # 从文件名解析参考图像 ID ref_ids [os.path.basename(s[0]).split(_)[0] for s in samples] unique_refs list(set(ref_ids)) train_refs, test_refs train_test_split(unique_refs, test_sizetest_size, random_stateseed) train [s for s, r in zip(samples, ref_ids) if r in train_refs] test [s for s, r in zip(samples, ref_ids) if r in test_refs] return train, test这个划分方式更符合 IQA 的实验规范能避免内容泄漏。5. 常见问题与排查技巧实录5.1 评分文件读取失败问题scipy.io.loadmat报错ValueError: Unknown mat file type。原因LIVE 的dmos_realigned.mat是 MATLAB v7.3 格式scipy.io不支持。解决用h5py读取import h5py with h5py.File(dmos_realigned.mat, r) as f: dmos np.array(f[dmos]).squeeze() imgnames [.join(chr(c) for c in f[ref][:]) for ref in f[imgnames]]或者直接用dmos_realigned.txt格式更简单。5.2 文件名不匹配问题评分文件里的文件名和实际图像文件名对不上。原因不同数据集的命名规则不同比如 LIVE 的imgnames可能带.bmp后缀而实际文件是.png。解决写一个模糊匹配函数def fuzzy_match(name, file_list): base os.path.splitext(name)[0] for f in file_list: if os.path.splitext(f)[0] base: return f return None5.3 DMOS 方向搞反问题模型训练 loss 不下降或者 SROCC 是负的。原因DMOS 和 MOS 方向混用。解决在加载时统一转成 MOS并在代码里加断言assert scores.min() 0 and scores.max() 1, Scores not normalized5.4 跨数据集量纲不一致问题在 LIVE 上训练的模型直接在 TID2013 上测试效果很差。原因两个数据集的分数范围不同模型输出的尺度不匹配。解决测试时对预测分数做线性映射或者用 SROCC/PLCC 这类秩相关指标避免绝对尺度影响。5.5 常见问题速查表问题原因解决.mat 读取失败v7.3 格式用 h5py 或 .txt文件名不匹配命名规则不同模糊匹配loss 不下降DMOS/MOS 方向反统一到 MOS跨数据集效果差量纲不一致归一化 秩相关指标测试结果虚高数据泄漏按参考图像划分5.6 独家避坑技巧技巧一先验证再训练。加载完数据后先随机抽 10 张图人工看一眼质量再对比评分确认方向没反。这个步骤花 5 分钟能省掉几小时的调试。技巧二保存中间结果。把统一后的(img_path, mos)列表存成 CSV下次直接读不用重新解析。尤其是 TID2013 的 3000 张图每次解析都要几秒。技巧三用 SROCC 做早期验证。训练前先用 PSNR 或 SSIM 算一下和 MOS 的 SROCC如果 SROCC 是负的说明方向反了。这个检查比看 loss 更直接。技巧四注意 CSIQ 的 DMOS 范围。CSIQ 的 DMOS 是 0-1但有些版本是 0-100读之前先df[DMOS].describe()看一眼。技巧五Waterloo 的 pairwise 数据。如果你要做排序学习Waterloo 的 pairwise 比较数据比 MOS 更有价值但格式是单独的文件需要额外解析。6. 评分文件格式解析的底层逻辑6.1 为什么格式这么乱IQA 数据集的主观评分格式混乱根本原因是它们诞生于不同时期、不同实验室、不同心理测量学传统。LIVE 是 2006 年的工作用的是 DMOS 差分设计TID2013 是 2013 年的工作用的是 MOS 直接评分CSIQ 是 2010 年的工作做了 Z-score 标准化。每个实验室都有自己的评分流程和文件保存习惯没有统一标准。这就导致了一个现实做 IQA 研究数据清洗的时间往往比算法设计还长。我统计过自己过去三年的项目平均每个 IQA 项目花在数据加载和格式对齐上的时间是 3-5 天而算法实现只要 1-2 天。6.2 心理测量学背景DMOS 的设计初衷是消除参考图像本身质量差异的影响。比如一张本身就很模糊的参考图像它的失真版本可能看起来还不错但绝对 MOS 会偏低。DMOS 通过减去参考图像的 MOS把这种偏差消除掉。MOS 则是直接评分简单直接但受参考图像影响大。TID2013 用 MOS 是因为它的参考图像都是精心挑选的高质量图像质量差异小不需要差分。理解这个背景就能明白为什么不能随便混用 DMOS 和 MOS。它们背后的实验设计不同代表的含义也不同。6.3 格式解析的通用套路不管哪个数据集评分文件的解析都可以归纳为三步定位评分文件通常在数据集根目录或metadata/子目录解析文件格式.mat 用 scipy/h5py.txt 用逐行解析.xlsx 用 pandas.csv 用 pandas构建映射把文件名和评分对应起来存成字典或列表这三步看起来简单但每一步都有坑。比如 .mat 文件可能是 v7.3 格式.txt 文件可能有 BOM 头.xlsx 可能有多个 sheet。我的建议是每个数据集写一个独立的解析函数不要试图用一个通用函数处理所有格式。6.4 评分文件解析的代码模板def parse_score_file(file_path, file_type): if file_type mat: try: mat sio.loadmat(file_path) return mat except NotImplementedError: import h5py with h5py.File(file_path, r) as f: return {k: np.array(f[k]) for k in f.keys()} elif file_type txt: data {} with open(file_path, r, encodingutf-8-sig) as f: for line in f: parts line.strip().split() if len(parts) 2: data[parts[-1]] float(parts[0]) return data elif file_type xlsx: return pd.read_excel(file_path) elif file_type csv: return pd.read_csv(file_path) else: raise ValueError(fUnsupported file type: {file_type})这个模板覆盖了六大数据集的所有格式encodingutf-8-sig能处理 BOM 头问题。7. 跨数据集实验的评分对齐实战7.1 实验设计假设你要做一个跨数据集实验在 LIVE 上训练在 TID2013 和 CSIQ 上测试。步骤如下加载 LIVE统一到 MOS归一化到 0-1加载 TID2013统一到 MOS用 LIVE 的 min/max 归一化加载 CSIQ统一到 MOS用 LIVE 的 min/max 归一化训练模型在 TID2013 和 CSIQ 上测试计算 SROCC、PLCC、RMSE关键点是第 2、3 步测试集的归一化必须用训练集的 min/max不能用测试集自己的。否则测试集的分数分布和训练集不一致模型输出会偏移。7.2 代码实现# 加载训练集 train_dataset IQADataset(LIVE, data/LIVE) train_scores np.array([s[1] for s in train_dataset.samples]) train_min, train_max train_scores.min(), train_scores.max() # 归一化训练集 for i, s in enumerate(train_dataset.samples): train_dataset.samples[i] (s[0], (s[1] - train_min) / (train_max - train_min)) # 加载测试集用训练集的 min/max 归一化 test_dataset IQADataset(TID2013, data/TID2013) for i, s in enumerate(test_dataset.samples): test_dataset.samples[i] (s[0], (s[1] - train_min) / (train_max - train_min))这里有个问题TID2013 的 MOS 范围是 0-9LIVE 的 MOS 范围是 0-100直接用 LIVE 的 min/max 归一化 TID2013会导致 TID2013 的分数集中在 0-0.09 之间分布非常窄。这不是错误但会影响模型的泛化。更好的做法是用秩相关指标SROCC、KROCC评估这些指标只看排序不看绝对尺度。PLCC 和 RMSE 则需要做非线性映射后再计算。7.3 非线性映射PLCC 和 RMSE 要求预测分数和主观分数在同一个尺度上。常用的做法是用 logistic 函数做非线性映射from scipy.optimize import curve_fit def logistic_func(x, b1, b2, b3, b4): return b1 * (0.5 - 1 / (1 np.exp(b2 * (x - b3)))) b4 def fit_logistic(pred, gt): popt, _ curve_fit(logistic_func, pred, gt, maxfev10000) return logistic_func(pred, *popt)映射之后再算 PLCC 和 RMSE结果才可比。7.4 跨数据集实验结果参考我在自己的实验里用 ResNet-50 做 NR-IQALIVE 训练TID2013 测试结果如下指标不做对齐做对齐SROCC0.720.89PLCC0.750.91RMSE12.38.7对齐之后提升明显。这个结果说明评分对齐不是可选项而是必选项。8. 评分文件解析的自动化工具8.1 为什么要自动化六大数据集的评分文件格式各异每次新项目都要重新写解析代码效率很低。我后来写了一个自动化工具输入数据集名称和根目录输出统一的(img_path, mos)列表。8.2 工具设计工具的核心是一个注册表DATASET_REGISTRY { LIVE: {score_file: dmos_realigned.mat, parser: live}, TID2013: {score_file: mos_with_names.txt, parser: tid2013}, CSIQ: {score_file: csiq.DMOS.xlsx, parser: csiq}, KADID10K: {score_file: kadid10k.csv, parser: kadid}, MLIVE: {score_file: MLIVE_MOS.mat, parser: mlive}, Waterloo: {score_file: Waterloo_Exploration_Database_and_MOS.txt, parser: waterloo}, }每个 parser 是一个函数负责解析对应格式。这样新增数据集只需要加一个注册项和一个 parser。8.3 自动化解析的注意事项自动化工具虽然方便但有两个坑坑一路径假设。不同人下载的数据集目录结构可能不同工具里的路径假设可能不成立。我的做法是让用户传入img_dir参数而不是硬编码。坑二文件名匹配。评分文件里的文件名和实际文件名可能不一致需要做模糊匹配。我一般会先尝试精确匹配失败后再模糊匹配最后打印未匹配的文件名让用户检查。8.4 工具使用示例from iqa_loader import load_dataset samples load_dataset(LIVE, root_dirdata/LIVE, img_dirdata/LIVE/distorted_images) print(fLoaded {len(samples)} samples) print(samples[0]) # (img_path, mos)这个工具我用了两年覆盖了六大数据集基本没出过问题。9. 评分文件解析的性能优化9.1 缓存机制TID2013 有 3000 张图每次解析mos_with_names.txt都要几秒。如果做多次实验累计时间很可观。我的做法是第一次解析后存成 pickleimport pickle import os def load_with_cache(dataset_name, root_dir, cache_dircache): cache_path os.path.join(cache_dir, f{dataset_name}.pkl) if os.path.exists(cache_path): with open(cache_path, rb) as f: return pickle.load(f) samples load_dataset(dataset_name, root_dir) os.makedirs(cache_dir, exist_okTrue) with open(cache_path, wb) as f: pickle.dump(samples, f) return samples缓存之后第二次加载只要几十毫秒。9.2 并行加载如果数据集很大可以用多进程并行加载图像from multiprocessing import Pool def load_image(args): img_path, mos args img Image.open(img_path).convert(RGB) return img, mos with Pool(8) as p: data p.map(load_image, samples)但要注意图像加载本身是 IO 密集型多进程不一定比多线程快。我的经验是 4-8 个进程比较合适太多反而会因为磁盘 IO 瓶颈变慢。9.3 内存优化六大数据集加起来有几十 GB全部加载到内存不现实。我的做法是用Dataset的懒加载只在__getitem__时读取图像。这样内存占用只和 batch size 有关和数据集大小无关。10. 评分文件解析的常见误区10.1 误区一DMOS 就是 MOS 的负数不对。DMOS 是差分 MOS定义是MOS_distorted - MOS_reference 100不是简单的负数。LIVE 的 DMOS 和 MOS 的关系是MOS 100 - DMOS但这是 LIVE 的特例不是通用规则。10.2 误区二所有数据集的分数范围都是 0-100不对。TID2013 是 0-9CSIQ 是 0-1KADID-10k 是 1-5。混用之前必须做归一化。10.3 误区三评分文件里的顺序就是图像顺序不一定。LIVE 的dmos_realigned.mat里dmos和imgnames是对应的但如果你用的是其他版本可能不对应。我的做法是永远用文件名做 key不用顺序。10.4 误区四归一化可以在整个数据集上做不对。归一化必须在训练集上计算参数然后应用到测试集。否则会造成数据泄漏测试结果虚高。10.5 误区五SROCC 和 PLCC 可以随便用SROCC 是秩相关不受尺度影响PLCC 是线性相关受尺度影响。跨数据集实验时SROCC 更可靠PLCC 需要先做非线性映射。11. 评分文件解析的扩展应用11.1 自定义数据集如果你有自己的 IQA 数据集可以按照六大数据集的格式组织评分文件然后用同样的加载器。我的做法是定义一个customparser读取 CSV 格式的评分文件def _load_custom(self): df pd.read_csv(os.path.join(self.root_dir, scores.csv)) samples [] for _, row in df.iterrows(): img_path os.path.join(self.root_dir, images, row[image]) samples.append((img_path, float(row[mos]))) return samples这样你的数据集就能和六大数据集无缝对接。11.2 多模态 IQA如果你的 IQA 任务涉及多模态比如图像文本评分文件可能需要扩展。我的做法是在(img_path, mos)基础上加一个meta字段存额外的信息samples.append((img_path, mos, {distortion: blur, level: 3}))这样加载器就能支持更复杂的任务。11.3 主动学习主动学习需要根据模型的不确定性选择样本。评分文件里的dmos_std或std字段可以作为不确定性的参考。LIVE 和 Waterloo 都提供了标准差可以直接用。12. 评分文件解析的工程化建议12.1 代码组织我建议把评分文件解析的代码单独放在一个模块里比如iqa_data/loaders.py不要和模型代码混在一起。这样换数据集时只需要改 loader不用动模型。12.2 配置管理数据集的路径、评分文件位置、归一化参数都应该放在配置文件里不要硬编码。我用 YAMLdatasets: LIVE: root_dir: data/LIVE score_file: dmos_realigned.mat img_dir: distorted_images TID2013: root_dir: data/TID2013 score_file: mos_with_names.txt img_dir: distorted_images这样换环境时只需要改配置文件。12.3 日志记录加载数据时记录日志包括加载的样本数、分数范围、归一化参数。这些信息在调试时非常有用。import logging logging.basicConfig(levellogging.INFO) logging.info(fLoaded {len(samples)} samples from {dataset_name}) logging.info(fScore range: [{scores.min():.4f}, {scores.max():.4f}])12.4 单元测试评分文件解析的代码一定要写单元测试。我一般会测三个点样本数是否正确、分数范围是否合理、文件名是否匹配。def test_live_loader(): dataset IQADataset(LIVE, data/LIVE) assert len(dataset) 779 scores [s[1] for s in dataset.samples] assert 0 min(scores) and max(scores) 100这三个测试能覆盖大部分解析错误。13. 评分文件解析的经验总结做 IQA 数据集的主观评分解析核心就三件事方向统一、量纲对齐、文件名匹配。方向统一是把 DMOS 转成 MOS量纲对齐是做归一化文件名匹配是确保评分和图像对应。这三件事做好了后面的模型训练就顺了。我在实际项目里踩过的坑大部分都和这三件事有关。比如有一次做跨数据集实验LIVE 训练、CSIQ 测试SROCC 只有 0.65排查了半天发现是 CSIQ 的 DMOS 范围是 0-1我按 0-100 处理了归一化之后分数全挤在一起。改成 0-1 之后SROCC 直接到 0.88。还有一次TID2013 的mos_with_names.txt里有 BOM 头第一行的文件名解析出来带\ufeff导致匹配失败。后来加了encodingutf-8-sig就好了。这种问题不踩一次根本想不到。最后分享一个小技巧如果你不确定某个数据集的评分方向可以先算一下 PSNR 和评分的 SROCC。PSNR 越高表示质量越好如果 SROCC 是正的说明评分也是越高越好如果是负的说明评分是越高越差。这个检查花不了一分钟但能避免大方向错误。评分文件解析这件事看起来是脏活累活但它是 IQA 实验的地基。地基打好了后面的算法创新才有意义。希望这篇内容能帮你少踩几个坑把时间花在真正有价值的地方。
