chore: 数据迁到 data/raw/ 后同步所有路径引用

原始数据从 data/<数据集> 移到了 data/raw/<数据集>。

- 聚类配置的 dataset.root:film / pearlescent 均改为 data/raw/ 下
- 检测配置的 img_dir 与 label_dir 注释同步
- README、data/README、算法文档、流程图(含重新渲染的 PNG)与各脚本
  docstring 里的路径示例一并更新
- data/README 补充目录约定:原始数据只读放 data/raw/,
  export_clusters.py 的派生数据集放在 data/ 下与 raw/ 平级
- 顺手把 unlabeled 数据集加进 datasets/__init__.py:此前它没在包初始化时注册,
  build_dataset({"name": "unlabeled"}) 只有在别处先 import 过该模块时才可用

迁移后重跑全量聚类,结果与迁移前逐项一致(轮廓 0.3255、稳定性 0.9641、
簇大小 [4507, 3795, 4849, 1428])。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-07 11:19:42 +08:00
co-authored by Claude Opus 5
parent 5f31ed0a92
commit 5d395b7c25
15 changed files with 56 additions and 46 deletions
+8 -8
View File
@@ -56,7 +56,7 @@ uv run python train.py --config configs/train/baseline.yaml --device cpu
每行 `class_id cx cy w h`(相对宽高归一化到 [0,1]),空文件或缺失 `.txt` 即无缺陷样本。 每行 `class_id cx cy w h`(相对宽高归一化到 [0,1]),空文件或缺失 `.txt` 即无缺陷样本。
``` ```
data/film/ data/raw/film/
├── images/{train,val}/0001.png ├── images/{train,val}/0001.png
├── labels/{train,val}/0001.txt ├── labels/{train,val}/0001.txt
└── data.yaml # path / train / val / nc / names └── data.yaml # path / train / val / nc / names
@@ -65,15 +65,15 @@ data/film/
```bash ```bash
# 1. labelme / VOC -> YOLO txt(顺带生成 data.yaml) # 1. labelme / VOC -> YOLO txt(顺带生成 data.yaml)
uv run python scripts/convert_annotations.py --format labelme \ uv run python scripts/convert_annotations.py --format labelme \
--input data/film/labelme --images data/film/images \ --input data/raw/film/labelme --images data/raw/film/images \
--output data/film/labels --data-yaml data/film/data.yaml --output data/raw/film/labels --data-yaml data/raw/film/data.yaml
# 2. 切分 train / val(默认建软链,不复制数据) # 2. 切分 train / val(默认建软链,不复制数据)
uv run python scripts/split_dataset.py --images data/film/images \ uv run python scripts/split_dataset.py --images data/raw/film/images \
--labels data/film/labels --val-ratio 0.2 --data-yaml data/film/data.yaml --labels data/raw/film/labels --val-ratio 0.2 --data-yaml data/raw/film/data.yaml
``` ```
配置里的 `classes` 既可以直接写类别列表,也可以写成 `data/film/data.yaml`,由框架读 `names`。 配置里的 `classes` 既可以直接写类别列表,也可以写成 `data/raw/film/data.yaml`,由框架读 `names`。
读取时也接受 YOLO 分割格式的多边形行(取外接矩形)。细节见 [data/README.md](data/README.md) 读取时也接受 YOLO 分割格式的多边形行(取外接矩形)。细节见 [data/README.md](data/README.md)
与 [datasets/yolo.py](datasets/yolo.py)。 与 [datasets/yolo.py](datasets/yolo.py)。
@@ -187,7 +187,7 @@ uv run python scripts/profile_inference.py --config configs/train/baseline.yaml
### 调用 ImageNet 预训练模型 ### 调用 ImageNet 预训练模型
`data/film` 里大量样本还没人工分完。这条流水线把目录下**所有图片拉平**(不看任何 `data/raw/film` 里大量样本还没人工分完。这条流水线把目录下**所有图片拉平**(不看任何
目录名),做无监督聚类,产出可人工核对的分组: 目录名),做无监督聚类,产出可人工核对的分组:
```bash ```bash
@@ -443,7 +443,7 @@ resnet50 每 epoch 的耗时与 batch 基本无关(算力受限),所以显
只是 NT-Xent 的负样本变少。 只是 NT-Xent 的负样本变少。
注意默认的 `method: pretrained` 用的**也是 resnet18**(ImageNet 权重,不训练,约 51 秒出结果); 注意默认的 `method: pretrained` 用的**也是 resnet18**(ImageNet 权重,不训练,约 51 秒出结果);
上面这个配置是在 `data/film` 上自监督训练它。两者实测对比(14,579 张,k=4): 上面这个配置是在 `data/raw/film` 上自监督训练它。两者实测对比(14,579 张,k=4):
| 方案 | 轮廓 | 稳定性 | 成本 | | 方案 | 轮廓 | 稳定性 | 成本 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
+2 -2
View File
@@ -1,6 +1,6 @@
# 未分类样本的无监督聚类。 # 未分类样本的无监督聚类。
# #
# 预处理决策(针对 data/film 的实际情况:14.5k 张已裁好的缺陷小图, # 预处理决策(针对 data/raw/film 的实际情况:14.5k 张已裁好的缺陷小图,
# 中位尺寸 152x113,宽高比 0.24~39.9,尺寸几乎每张都不同): # 中位尺寸 152x113,宽高比 0.24~39.9,尺寸几乎每张都不同):
# #
# 前提:图片的尺寸和宽高比是上游 ROI 裁剪环节的产物,不是缺陷属性。 # 前提:图片的尺寸和宽高比是上游 ROI 裁剪环节的产物,不是缺陷属性。
@@ -63,7 +63,7 @@ fit_max_samples: 0
dataset: dataset:
name: unlabeled name: unlabeled
root: data/film root: data/raw/film
gray: false gray: false
exclude: [images, labels] # 跳过已经整理成 YOLO 结构的部分 exclude: [images, labels] # 跳过已经整理成 YOLO 结构的部分
transforms: transforms:
+1 -1
View File
@@ -1,7 +1,7 @@
# 用 resnet18 做域内自监督(SimCLR)再聚类。 # 用 resnet18 做域内自监督(SimCLR)再聚类。
# #
# 与默认配置的区别只有骨架:configs/cluster/film.yaml 的 method=pretrained 用的也是 # 与默认配置的区别只有骨架:configs/cluster/film.yaml 的 method=pretrained 用的也是
# resnet18,但那是 ImageNet 权重、不训练;这里是在 data/film 上自监督训练它。 # resnet18,但那是 ImageNet 权重、不训练;这里是在 data/raw/film 上自监督训练它。
# #
# 用法: # 用法:
# uv run python scripts/cluster_unlabeled.py --config configs/cluster/film_simclr_resnet18.yaml \ # uv run python scripts/cluster_unlabeled.py --config configs/cluster/film_simclr_resnet18.yaml \
+2 -2
View File
@@ -1,6 +1,6 @@
# 未分类样本的无监督聚类。 # 未分类样本的无监督聚类。
# #
# 预处理决策(针对 data/film 的实际情况:14.5k 张已裁好的缺陷小图, # 预处理决策(针对 data/raw/film 的实际情况:14.5k 张已裁好的缺陷小图,
# 中位尺寸 152x113,宽高比 0.24~39.9,尺寸几乎每张都不同): # 中位尺寸 152x113,宽高比 0.24~39.9,尺寸几乎每张都不同):
# #
# 前提:图片的尺寸和宽高比是上游 ROI 裁剪环节的产物,不是缺陷属性。 # 前提:图片的尺寸和宽高比是上游 ROI 裁剪环节的产物,不是缺陷属性。
@@ -63,7 +63,7 @@ fit_max_samples: 0
dataset: dataset:
name: unlabeled name: unlabeled
root: data/pearlescent root: data/raw/pearlescent
gray: false gray: false
exclude: [images, labels] # 跳过已经整理成 YOLO 结构的部分 exclude: [images, labels] # 跳过已经整理成 YOLO 结构的部分
transforms: transforms:
+4 -4
View File
@@ -1,6 +1,6 @@
# 薄膜缺陷数据集(YOLO 风格标注:labels/*.txt,每行 `class_id cx cy w h`,归一化坐标)。 # 薄膜缺陷数据集(YOLO 风格标注:labels/*.txt,每行 `class_id cx cy w h`,归一化坐标)。
# 类别顺序即标注里的 class_id,改动顺序等于改变标签含义,必须重新训练。 # 类别顺序即标注里的 class_id,改动顺序等于改变标签含义,必须重新训练。
# 也可以写成 data.yaml 的路径,从其中的 names 读类别:classes: data/film/data.yaml # 也可以写成 data.yaml 的路径,从其中的 names 读类别:classes: data/raw/film/data.yaml
classes: [scratch, bubble, particle, crease, stain] classes: [scratch, bubble, particle, crease, stain]
# 图像级标签形式;模型的分类头、分类损失、分类指标都跟随它。 # 图像级标签形式;模型的分类头、分类损失、分类指标都跟随它。
@@ -18,8 +18,8 @@ data:
train: train:
dataset: dataset:
name: film_defect name: film_defect
img_dir: data/film/images/train img_dir: data/raw/film/images/train
label_dir: null # 缺省按 YOLO 约定取 data/film/labels/train label_dir: null # 缺省按 YOLO 约定取 data/raw/film/labels/train
gray: false gray: false
max_objects: 64 max_objects: 64
transforms: transforms:
@@ -38,7 +38,7 @@ data:
val: val:
dataset: dataset:
name: film_defect name: film_defect
img_dir: data/film/images/val img_dir: data/raw/film/images/val
label_dir: null label_dir: null
gray: false gray: false
transforms: transforms:
+23 -16
View File
@@ -2,20 +2,27 @@
存放数据集本体或指向数据集的软链接;除本文件外的内容都不入库(见 .gitignore)。 存放数据集本体或指向数据集的软链接;除本文件外的内容都不入库(见 .gitignore)。
标注采用 **YOLO 风格**:`images/` 下每张图在同层 `labels/` 里有一个同名 `.txt`。 **原始数据一律放在 `data/raw/<数据集名>/` 下**,派生数据(如按聚类结果导出的数据集)
放在 `data/` 下与 `raw/` 平级,两者一眼可分:
``` ```
data/ data/
└── film/ ├── raw/ # 原始数据,只读
├── images/ # 原图(或指向 NAS 的软链:ln -s /mnt/nas/film/images data/film/images) │ └── film/
│ ├── train/0001.png │ ├── images/ # 原图(或指向 NAS 的软链:ln -s /mnt/nas/film/images data/raw/film/images)
│ └── val/0002.png │ │ ├── train/0001.png
├── labels/ # 与 images 目录结构一一对应 │ │ └── val/0002.png
│ ├── train/0001.txt │ ├── labels/ # 与 images 目录结构一一对应
│ └── val/0002.txt │ │ ├── train/0001.txt
└── data.yaml # path / train / val / nc / names │ │ └── val/0002.txt
│ └── data.yaml # path / train / val / nc / names
└── film_r18_cluster_k4/ # 派生:scripts/export_clusters.py 的产物
├── 0/ 1/ 2/ 3/
└── manifest.json
``` ```
标注采用 **YOLO 风格**:`images/` 下每张图在同层 `labels/` 里有一个同名 `.txt`。
标注文件每行一个目标,坐标**相对图像宽高归一化**到 [0, 1],中心点格式: 标注文件每行一个目标,坐标**相对图像宽高归一化**到 [0, 1],中心点格式:
``` ```
@@ -31,18 +38,18 @@ data/
`data.yaml` 与 ultralytics 通用: `data.yaml` 与 ultralytics 通用:
```yaml ```yaml
path: data/film path: data/raw/film
train: images/train train: images/train
val: images/val val: images/val
nc: 5 nc: 5
names: {0: scratch, 1: bubble, 2: particle, 3: crease, 4: stain} names: {0: scratch, 1: bubble, 2: particle, 3: crease, 4: stain}
``` ```
配置里的 `classes` 既可以直接写类别列表,也可以写成 `data/film/data.yaml` 由框架读取 `names`。 配置里的 `classes` 既可以直接写类别列表,也可以写成 `data/raw/film/data.yaml` 由框架读取 `names`。
## 还没分类的样本 ## 还没分类的样本
`data/film` 下按人工整理留下的目录(`grease`、`wrinkle`、`反面纵黑点` ……)只是历史产物, `data/raw/film` 下按人工整理留下的目录(`grease`、`wrinkle`、`反面纵黑点` ……)只是历史产物,
不是最终类别。要重新梳理类别时用无监督聚类,它会忽略这些目录名、把所有图拉平处理: 不是最终类别。要重新梳理类别时用无监督聚类,它会忽略这些目录名、把所有图拉平处理:
```bash ```bash
@@ -57,10 +64,10 @@ uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml
```bash ```bash
# labelme / VOC -> YOLO txt(顺带生成 data.yaml) # labelme / VOC -> YOLO txt(顺带生成 data.yaml)
uv run python scripts/convert_annotations.py --format labelme \ uv run python scripts/convert_annotations.py --format labelme \
--input data/film/labelme --images data/film/images \ --input data/raw/film/labelme --images data/raw/film/images \
--output data/film/labels --data-yaml data/film/data.yaml --output data/raw/film/labels --data-yaml data/raw/film/data.yaml
# 划分 train / val(默认建软链,不复制数据) # 划分 train / val(默认建软链,不复制数据)
uv run python scripts/split_dataset.py --images data/film/images \ uv run python scripts/split_dataset.py --images data/raw/film/images \
--labels data/film/labels --val-ratio 0.2 --data-yaml data/film/data.yaml --labels data/raw/film/labels --val-ratio 0.2 --data-yaml data/raw/film/data.yaml
``` ```
+3
View File
@@ -5,6 +5,7 @@ from datasets.film_defect import FilmDefectDataset, collate_fn
from datasets.registry import DATASETS from datasets.registry import DATASETS
from datasets.target_encoder import CenterTargetEncoder from datasets.target_encoder import CenterTargetEncoder
from datasets.transforms import build_transforms from datasets.transforms import build_transforms
from datasets.unlabeled import UnlabeledImageDataset, unlabeled_collate
from datasets.yolo import format_label_lines, load_classes, parse_label_file, write_data_yaml from datasets.yolo import format_label_lines, load_classes, parse_label_file, write_data_yaml
__all__ = [ __all__ = [
@@ -13,6 +14,8 @@ __all__ = [
"FilmDefectDataset", "FilmDefectDataset",
"collate_fn", "collate_fn",
"DATASETS", "DATASETS",
"UnlabeledImageDataset",
"unlabeled_collate",
"CenterTargetEncoder", "CenterTargetEncoder",
"build_transforms", "build_transforms",
"parse_label_file", "parse_label_file",
+1 -1
View File
@@ -31,7 +31,7 @@ from utils.labels import MULTILABEL, check_label_mode
@DATASETS.register("film_defect") @DATASETS.register("film_defect")
class FilmDefectDataset(Dataset): class FilmDefectDataset(Dataset):
"""Args: """Args:
img_dir: 图片目录(YOLO 约定,如 `data/film/images/train`),递归查找图片。 img_dir: 图片目录(YOLO 约定,如 `data/raw/film/images/train`),递归查找图片。
classes: 类别名列表,或指向 YOLO data.yaml 的路径(读其中的 `names`)。 classes: 类别名列表,或指向 YOLO data.yaml 的路径(读其中的 `names`)。
列表顺序即类别下标顺序,必须与标注 txt 里的 class_id 对应。 列表顺序即类别下标顺序,必须与标注 txt 里的 class_id 对应。
label_dir: 标注目录;缺省按 YOLO 约定把路径里的 `images` 换成 `labels`。 label_dir: 标注目录;缺省按 YOLO 约定把路径里的 `images` 换成 `labels`。
+1 -1
View File
@@ -3,7 +3,7 @@
目录约定(与 ultralytics 一致,`images` 与 `labels` 同层对应): 目录约定(与 ultralytics 一致,`images` 与 `labels` 同层对应):
``` ```
data/film/ data/raw/film/
├── images/train/0001.png ├── images/train/0001.png
├── labels/train/0001.txt # 与图片同名,后缀换成 .txt ├── labels/train/0001.txt # 与图片同名,后缀换成 .txt
├── images/val/... ├── images/val/...
+4 -4
View File
@@ -1,7 +1,7 @@
# 未分类样本的无监督聚类:算法与数学说明 # 未分类样本的无监督聚类:算法与数学说明
本文档说明 `scripts/cluster_unlabeled.py` 背后的完整算法:每一步在做什么数学变换、 本文档说明 `scripts/cluster_unlabeled.py` 背后的完整算法:每一步在做什么数学变换、
为什么这样选、以及在 `data/film` 上的实测依据。 为什么这样选、以及在 `data/raw/film` 上的实测依据。
代码入口:[`scripts/cluster_unlabeled.py`](../scripts/cluster_unlabeled.py)、 代码入口:[`scripts/cluster_unlabeled.py`](../scripts/cluster_unlabeled.py)、
[`engine/cluster.py`](../engine/cluster.py)、[`engine/simclr.py`](../engine/simclr.py)、 [`engine/cluster.py`](../engine/cluster.py)、[`engine/simclr.py`](../engine/simclr.py)、
@@ -28,7 +28,7 @@
## 1. 问题设定与记号 ## 1. 问题设定与记号
给定一组**无标注**图像 $\mathcal{I} = \{I_1, \dots, I_N\}$(`data/film` 下递归扫描到 给定一组**无标注**图像 $\mathcal{I} = \{I_1, \dots, I_N\}$(`data/raw/film` 下递归扫描到
$N = 14{,}579$ 张 PNG),在没有任何类别标签的前提下,把它们划分成 $k$ 个互不相交的簇 $N = 14{,}579$ 张 PNG),在没有任何类别标签的前提下,把它们划分成 $k$ 个互不相交的簇
$$ $$
@@ -76,7 +76,7 @@ flowchart TB
classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12 classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
scan["data/film 递归扫描<br/>N = 14,579 张 PNG<br/>尺寸各异 84×83 ~ 4464×1139"]:::input scan["data/raw/film 递归扫描<br/>N = 14,579 张 PNG<br/>尺寸各异 84×83 ~ 4464×1139"]:::input
subgraph PRE["① 预处理 — 抹掉采集因子"] subgraph PRE["① 预处理 — 抹掉采集因子"]
direction LR direction LR
@@ -487,7 +487,7 @@ $$
**为什么这个指标关键**:数据里真有 $k$ 团结构时,不同初始化会收敛到同一套划分,NMI ≈ 1;若是在硬切一团连续的数据,边界位置随初值乱跳,NMI 会很低。它比轮廓系数更能识破"强行分 $k$ 类"的情形。实测中它一眼揪出了白化的问题(稳定性 0.01)。 **为什么这个指标关键**:数据里真有 $k$ 团结构时,不同初始化会收敛到同一套划分,NMI ≈ 1;若是在硬切一团连续的数据,边界位置随初值乱跳,NMI 会很低。它比轮廓系数更能识破"强行分 $k$ 类"的情形。实测中它一眼揪出了白化的问题(稳定性 0.01)。
> **不使用外部标签。** `data/film` 下的目录名(`grease`、`wrinkle`、`反面纵黑点`……) > **不使用外部标签。** `data/raw/film` 下的目录名(`grease`、`wrinkle`、`反面纵黑点`……)
> 是人工整理的历史产物,既不完整也未必正确,因此既不参与聚类、也不用于评估。 > 是人工整理的历史产物,既不完整也未必正确,因此既不参与聚类、也不用于评估。
--- ---
+2 -2
View File
@@ -1,6 +1,6 @@
# 无监督聚类流水线 # 无监督聚类流水线
从 `data/film` 的原始图片到可人工核对的分组结果:六个阶段的数据流、张量形状与每步的数学操作。 从 `data/raw/film` 的原始图片到可人工核对的分组结果:六个阶段的数据流、张量形状与每步的数学操作。
虚线是可选的 SimCLR 自监督分支(`method: simclr`,CUDA 与 Apple MPS 均可)。 虚线是可选的 SimCLR 自监督分支(`method: simclr`,CUDA 与 Apple MPS 均可)。
```mermaid ```mermaid
@@ -13,7 +13,7 @@ flowchart TB
classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12 classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
scan["data/film 递归扫描<br/>N = 14,579 张 PNG<br/>尺寸各异 84×83 ~ 4464×1139"]:::input scan["data/raw/film 递归扫描<br/>N = 14,579 张 PNG<br/>尺寸各异 84×83 ~ 4464×1139"]:::input
subgraph PRE["① 预处理 — 抹掉采集因子"] subgraph PRE["① 预处理 — 抹掉采集因子"]
direction LR direction LR
+1 -1
View File
@@ -7,7 +7,7 @@ flowchart TB
classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12 classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12
classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
scan["data/film 递归扫描<br/>N = 14,579 张 PNG<br/>尺寸各异 84×83 ~ 4464×1139"]:::input scan["data/raw/film 递归扫描<br/>N = 14,579 张 PNG<br/>尺寸各异 84×83 ~ 4464×1139"]:::input
subgraph PRE["① 预处理 — 抹掉采集因子"] subgraph PRE["① 预处理 — 抹掉采集因子"]
direction LR direction LR
Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 194 KiB

+2 -2
View File
@@ -2,8 +2,8 @@
"""把 labelme / VOC 标注批量转换成 YOLO 风格的 .txt。 """把 labelme / VOC 标注批量转换成 YOLO 风格的 .txt。
python scripts/convert_annotations.py --format labelme \ python scripts/convert_annotations.py --format labelme \
--input data/film/labelme --images data/film/images \ --input data/raw/film/labelme --images data/raw/film/images \
--output data/film/labels --data-yaml data/film/data.yaml --output data/raw/film/labels --data-yaml data/raw/film/data.yaml
每张图输出一个同名 .txt,每行 `class_id cx cy w h`(相对宽高归一化); 每张图输出一个同名 .txt,每行 `class_id cx cy w h`(相对宽高归一化);
类别顺序默认按扫描到的名字排序,也可以用 `--classes scratch bubble ...` 固定。 类别顺序默认按扫描到的名字排序,也可以用 `--classes scratch bubble ...` 固定。
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
"""按比例切分成 YOLO 的 train / val / test 目录结构。 """按比例切分成 YOLO 的 train / val / test 目录结构。
python scripts/split_dataset.py --images data/film/images --labels data/film/labels \ python scripts/split_dataset.py --images data/raw/film/images --labels data/raw/film/labels \
--val-ratio 0.2 --data-yaml data/film/data.yaml --val-ratio 0.2 --data-yaml data/raw/film/data.yaml
默认建软链(不复制数据),产出: 默认建软链(不复制数据),产出: