# 数据加载器分层设计说明 ## 目标 当前阶段只做“真实数据的统一索引层设计”,暂时不接训练器。 目标是: 1. 让不同目录结构的数据集都能转成统一样本记录格式。 2. 先解决“怎么描述样本”,再解决“怎么训练”。 3. 避免在训练器里直接写一堆 `if dataset_name == ...` 的路径解析逻辑。 ## 当前已落地的分层 当前新增了 `lib/data`,结构如下: 1. `records.py` - 定义统一样本结构 `SegSampleRecord` 2. `utils.py` - 提供图像文件识别和文件名处理工具 3. `indexers.py` - 提供三类基础索引器 4. `builder.py` - 负责按数据集名称分发到具体解析逻辑 5. `splits.py` - 负责读取官方划分文件 6. `datasets.py` - 负责真正读取图像与掩膜 7. `loaders.py` - 负责构建 `Dataset` 和 `DataLoader` ## 当前统一样本格式 统一样本记录包含: 1. `dataset_name` 2. `image_path` 3. `mask_path` 4. `split` 5. `sample_id` 6. `class_name` 7. `meta` 这意味着后面不管数据来自乳腺、甲状腺还是其他超声任务,训练器看到的都将是统一格式。 ## 当前支持的三种目录结构 ### 1. 标准图像-掩膜成对目录 适用: 1. `BUS-UCLM` 2. `TG3K` 3. `OTU_2d` 规则: 1. 图像目录和掩膜目录分开 2. 文件名完全一致 ### 2. 已有训练测试划分目录 适用: 1. `TN3K` 规则: 1. `trainval-image` 2. `trainval-mask` 3. `test-image` 4. `test-mask` ### 3. 文件名匹配目录 适用: 1. `BUSI` 规则: 1. 原图文件与掩膜文件放在同一类目录下 2. 通过 `_mask`、`_mask_1` 这类后缀匹配 3. 当前第一版默认取第一个掩膜,并在 `meta.mask_count` 中保留掩膜数量 ## 当前官方划分支持 当前优先支持已有官方划分的数据集,并且已经实际验证过 `Dataset + DataLoader` 可正常读取: ### 1. OTU_2d 支持: 1. `train.txt` 2. `val.txt` 说明: 1. 文件中保存的是样本编号 2. 当前会按编号过滤统一索引结果 3. 已验证: - `train.txt -> 1000` 条 - `val.txt -> 469` 条 4. 图像与掩膜尺寸不统一时,当前批加载会返回列表,不会强行堆叠报错 ### 2. TN3K 支持: 1. `tn3k-trainval.json` 2. `tn3k-trainval-fold3.json` 3. `tn3k-trainval-fold4.json` 说明: 1. `trainval` 图像先进入统一索引 2. 再通过官方 `json` 中的 `train` / `val` 索引过滤 3. `test-image` / `test-mask` 直接作为 `test` 4. 已验证: - `train -> 2303` 条 - `val -> 576` 条 5. 由于原始图像尺寸不一致,当前批加载默认返回 `image list` 与 `mask list` ### 3. TG3K 支持: 1. `tg3k-trainval.json` 说明: 1. 当前优先读取官方 `train` / `val` 2. `test` 为空时,不强行虚构测试集 3. 已验证: - `train -> 3226` 条 - `val -> 359` 条 4. 当前前几个样本尺寸一致时可以直接堆叠;若后续批次尺寸不同,也会自动退回列表模式 ## 当前已支持的数据集 1. `BUS-UCLM` 2. `TG3K` 3. `TN3K` 4. `OTU_2d` 5. `BUSI` 6. `BUS-BRA` 7. `BUS_UC` 8. `CCAUI` 9. `DDTI` ## 当前三类新增支持的说明 ### 1. BUS_UC 采用“分层成对目录”解析: 1. `All/images` 对 `All/masks` 2. `Benign/images` 对 `Benign/masks` 3. `Malignant/images` 对 `Malignant/masks` 其中: 1. `class_name` 会记录 `all`、`benign`、`malignant` 2. `All` 当前保留为单独一组记录,不在索引层里自动去重 ### 2. CCAUI 采用标准成对目录解析: 1. `US images` 2. `Expert mask images` 目录名有空格,但对当前索引层没有影响。 ### 3. DDTI 当前采用“图像 + XML 标注路径”索引方式: 1. 图像来自根目录下的 `*.jpg` 2. 标注来自根目录下的 `*.xml` 3. 通过图像名中的前缀编号与 `xml` 文件名匹配 当前这一层只完成: 1. 图像路径索引 2. `xml` 标注路径关联 更新说明: 1. 已新增 `lib/data/ddti.py` 2. 已支持从 `xml` 中解析自由手绘轮廓 3. 已支持按 `样本编号_图像序号.jpg -> xml 中 image=图像序号` 的规则生成二值掩膜 也就是说,`DDTI` 现在已经具备“可从标注动态生成掩膜”的能力,并且已经并入通用读取型 `Dataset`。 补充说明: 1. `DDTI` 当前没有发现现成的官方 `train.txt` / `val.txt` 2. 现阶段已完成: - `xml -> polygon` 解析 - `polygon -> 二值 mask` 动态生成 - `DataLoader` 批读取验证 3. 已验证总样本数为 `480` ## 当前项目级划分支持 对于当前 `data` 目录中没有现成官方 `train.txt / val.txt` 的数据集,已经补充了统一的项目级划分机制。 ### 支持的数据集 1. `BUSI` 2. `BUS-BRA` 3. `BUS_UC` 4. `CCAUI` 5. `DDTI` ### 生成规则 默认会在对应数据集根目录下生成: 1. `splits/project/train.txt` 2. `splits/project/val.txt` 默认策略: 1. `val_ratio = 0.2` 2. `seed = 42` 3. 如果数据集带有 `class_name`,默认按类别分层抽样 ### 已验证结果 1. `BUSI` - `train = 624` - `val = 156` 2. `BUS-BRA` - `train = 1500` - `val = 375` 3. `BUS_UC` - `train = 649` - `val = 162` - 注意:当前正式划分只基于 `All` 子集生成与读取,避免与 `Benign / Malignant` 重复 4. `CCAUI` - `train = 880` - `val = 220` 5. `DDTI` - `train = 384` - `val = 96` ### 使用方式 先生成: ```bash python tmp/generate_project_split.py --dataset BUSI --root data/BUSI python tmp/generate_project_split.py --dataset BUS-BRA --root data/BUS-BRA python tmp/generate_project_split.py --dataset BUS_UC --root data/BUS_UC python tmp/generate_project_split.py --dataset CCAUI --root data/CCAUI python tmp/generate_project_split.py --dataset DDTI --root data/DDTI ``` 再读取: ```bash python tmp/inspect_dataloader.py --dataset BUSI --root data/BUSI --split train python tmp/inspect_dataloader.py --dataset BUS-BRA --root data/BUS-BRA --split val python tmp/inspect_dataloader.py --dataset BUS_UC --root data/BUS_UC --split train python tmp/inspect_dataloader.py --dataset CCAUI --root data/CCAUI --split train python tmp/inspect_dataloader.py --dataset DDTI --root data/DDTI --split val ``` ## 当前半监督划分支持 在已有 `train` 基底上,当前已经支持继续生成: 1. `labeled.txt` 2. `unlabeled.txt` 并支持多种标注比例,例如: 1. `0.05` 2. `0.1` 3. `0.2` ### 存储结构 生成结果默认写入: ```text 数据集根目录/ splits/ project/ train.txt val.txt semi_supervised/ train/ seed_42/ ratio_0p05/ labeled.txt unlabeled.txt ratio_0p1/ labeled.txt unlabeled.txt ratio_0p2/ labeled.txt unlabeled.txt ``` ### 已验证结果 #### 1. BUSI 基于 `train = 624`: 1. `0.05 -> labeled = 31, unlabeled = 593` 2. `0.1 -> labeled = 63, unlabeled = 561` 3. `0.2 -> labeled = 125, unlabeled = 499` #### 2. BUS_UC 基于 `train = 649`: 1. `0.05 -> labeled = 32, unlabeled = 617` 2. `0.1 -> labeled = 65, unlabeled = 584` 3. `0.2 -> labeled = 130, unlabeled = 519` #### 3. DDTI 基于 `train = 384`: 1. `0.05 -> labeled = 19, unlabeled = 365` 2. `0.1 -> labeled = 38, unlabeled = 346` 3. `0.2 -> labeled = 77, unlabeled = 307` ### 生成方式 ```bash python tmp/generate_project_split.py --dataset BUSI --root data/BUSI --labeled-ratios 0.05,0.1,0.2 python tmp/generate_project_split.py --dataset BUS_UC --root data/BUS_UC --labeled-ratios 0.05,0.1,0.2 python tmp/generate_project_split.py --dataset DDTI --root data/DDTI --labeled-ratios 0.05,0.1,0.2 ``` ### 读取方式 ```bash python tmp/inspect_dataloader.py --dataset BUSI --root data/BUSI --split labeled --labeled-ratio 0.1 python tmp/inspect_dataloader.py --dataset BUS_UC --root data/BUS_UC --split unlabeled --labeled-ratio 0.2 python tmp/inspect_dataloader.py --dataset DDTI --root data/DDTI --split labeled --labeled-ratio 0.05 ``` ### 设计说明 1. `labeled / unlabeled` 当前默认都从 `train` 基底继续切分 2. 若数据集带 `class_name`,默认继续按类别分层抽样 3. `BUS_UC` 仍然只基于 `All` 子集进行半监督划分,避免重复样本 ## 为什么现在先不接训练器 因为当前最重要的是把“数据集结构差异”收敛掉。 如果现在直接在训练器里接,会导致: 1. 路径解析逻辑和训练逻辑混在一起 2. 后续新增数据集时必须改训练器 3. 不利于调试单个数据集的索引是否正确 ## 当前调试方式 增加了: 1. `tmp/inspect_dataset_index.py` 2. `tmp/generate_project_split.py` 可用来快速检查索引结果,例如: ```bash python tmp/inspect_dataset_index.py --dataset BUS-UCLM --root data/BUS-UCLM python tmp/inspect_dataset_index.py --dataset TN3K --root data/TN3K python tmp/inspect_dataset_index.py --dataset TG3K --root data/TG3K python tmp/inspect_dataset_index.py --dataset BUSI --root data/BUSI python tmp/inspect_dataset_index.py --dataset BUS_UC --root data/BUS_UC python tmp/inspect_dataset_index.py --dataset CCAUI --root data/CCAUI python tmp/inspect_dataset_index.py --dataset DDTI --root data/DDTI python tmp/inspect_ddti_mask.py --image data/DDTI/106_3.jpg --xml data/DDTI/106.xml --save tmp/ddti_106_3_mask.png python tmp/inspect_dataloader.py --dataset OTU_2d --root data/OTU_2d --split train python tmp/inspect_dataloader.py --dataset OTU_2d --root data/OTU_2d --split val python tmp/inspect_dataloader.py --dataset TN3K --root data/TN3K --split train python tmp/inspect_dataloader.py --dataset TN3K --root data/TN3K --split val python tmp/inspect_dataloader.py --dataset TG3K --root data/TG3K --split train python tmp/inspect_dataloader.py --dataset TG3K --root data/TG3K --split val python tmp/inspect_dataloader.py --dataset DDTI --root data/DDTI python tmp/generate_project_split.py --dataset BUSI --root data/BUSI python tmp/generate_project_split.py --dataset BUS-BRA --root data/BUS-BRA python tmp/generate_project_split.py --dataset BUS_UC --root data/BUS_UC python tmp/generate_project_split.py --dataset CCAUI --root data/CCAUI python tmp/generate_project_split.py --dataset DDTI --root data/DDTI python tmp/generate_project_split.py --dataset BUSI --root data/BUSI --labeled-ratios 0.05,0.1,0.2 python tmp/inspect_dataloader.py --dataset BUSI --root data/BUSI --split labeled --labeled-ratio 0.1 ``` ## 下一步建议 在数据加载器分层已经确定后,下一步再做: 1. 在配置层显式声明 `dataset_name / split / split_file / image_size / batch_collate_strategy` 2. 再把 `SegSampleRecord` 和读取型 `Dataset` 接入训练器 3. 在训练配置层加入 `labeled_ratio / semi_seed / semi_base_split` 4. 之后再考虑把 `DDTI` 生成后的掩膜缓存到磁盘还是运行时动态生成