diff --git a/README.md b/README.md index de5e1e0..30e6ef3 100644 --- a/README.md +++ b/README.md @@ -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` 即无缺陷样本。 ``` -data/film/ +data/raw/film/ ├── images/{train,val}/0001.png ├── labels/{train,val}/0001.txt └── data.yaml # path / train / val / nc / names @@ -65,15 +65,15 @@ data/film/ ```bash # 1. labelme / VOC -> YOLO txt(顺带生成 data.yaml) uv run python scripts/convert_annotations.py --format labelme \ - --input data/film/labelme --images data/film/images \ - --output data/film/labels --data-yaml data/film/data.yaml + --input data/raw/film/labelme --images data/raw/film/images \ + --output data/raw/film/labels --data-yaml data/raw/film/data.yaml # 2. 切分 train / val(默认建软链,不复制数据) -uv run python scripts/split_dataset.py --images data/film/images \ - --labels data/film/labels --val-ratio 0.2 --data-yaml data/film/data.yaml +uv run python scripts/split_dataset.py --images data/raw/film/images \ + --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) 与 [datasets/yolo.py](datasets/yolo.py)。 @@ -187,7 +187,7 @@ uv run python scripts/profile_inference.py --config configs/train/baseline.yaml ### 调用 ImageNet 预训练模型 -`data/film` 里大量样本还没人工分完。这条流水线把目录下**所有图片拉平**(不看任何 +`data/raw/film` 里大量样本还没人工分完。这条流水线把目录下**所有图片拉平**(不看任何 目录名),做无监督聚类,产出可人工核对的分组: ```bash @@ -443,7 +443,7 @@ resnet50 每 epoch 的耗时与 batch 基本无关(算力受限),所以显 只是 NT-Xent 的负样本变少。 注意默认的 `method: pretrained` 用的**也是 resnet18**(ImageNet 权重,不训练,约 51 秒出结果); -上面这个配置是在 `data/film` 上自监督训练它。两者实测对比(14,579 张,k=4): +上面这个配置是在 `data/raw/film` 上自监督训练它。两者实测对比(14,579 张,k=4): | 方案 | 轮廓 | 稳定性 | 成本 | | --- | --- | --- | --- | diff --git a/configs/cluster/film.yaml b/configs/cluster/film.yaml index e4ae9b8..5a1bef4 100644 --- a/configs/cluster/film.yaml +++ b/configs/cluster/film.yaml @@ -1,6 +1,6 @@ # 未分类样本的无监督聚类。 # -# 预处理决策(针对 data/film 的实际情况:14.5k 张已裁好的缺陷小图, +# 预处理决策(针对 data/raw/film 的实际情况:14.5k 张已裁好的缺陷小图, # 中位尺寸 152x113,宽高比 0.24~39.9,尺寸几乎每张都不同): # # 前提:图片的尺寸和宽高比是上游 ROI 裁剪环节的产物,不是缺陷属性。 @@ -63,7 +63,7 @@ fit_max_samples: 0 dataset: name: unlabeled - root: data/film + root: data/raw/film gray: false exclude: [images, labels] # 跳过已经整理成 YOLO 结构的部分 transforms: diff --git a/configs/cluster/film_simclr_resnet18.yaml b/configs/cluster/film_simclr_resnet18.yaml index 3cc318c..63adfae 100644 --- a/configs/cluster/film_simclr_resnet18.yaml +++ b/configs/cluster/film_simclr_resnet18.yaml @@ -1,7 +1,7 @@ # 用 resnet18 做域内自监督(SimCLR)再聚类。 # # 与默认配置的区别只有骨架: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 \ diff --git a/configs/cluster/pearlescent.yaml b/configs/cluster/pearlescent.yaml index fb08c84..086fc6d 100644 --- a/configs/cluster/pearlescent.yaml +++ b/configs/cluster/pearlescent.yaml @@ -1,6 +1,6 @@ # 未分类样本的无监督聚类。 # -# 预处理决策(针对 data/film 的实际情况:14.5k 张已裁好的缺陷小图, +# 预处理决策(针对 data/raw/film 的实际情况:14.5k 张已裁好的缺陷小图, # 中位尺寸 152x113,宽高比 0.24~39.9,尺寸几乎每张都不同): # # 前提:图片的尺寸和宽高比是上游 ROI 裁剪环节的产物,不是缺陷属性。 @@ -63,7 +63,7 @@ fit_max_samples: 0 dataset: name: unlabeled - root: data/pearlescent + root: data/raw/pearlescent gray: false exclude: [images, labels] # 跳过已经整理成 YOLO 结构的部分 transforms: diff --git a/configs/dataset/film_defect.yaml b/configs/dataset/film_defect.yaml index d80453a..c891d5a 100644 --- a/configs/dataset/film_defect.yaml +++ b/configs/dataset/film_defect.yaml @@ -1,6 +1,6 @@ # 薄膜缺陷数据集(YOLO 风格标注:labels/*.txt,每行 `class_id cx cy w h`,归一化坐标)。 # 类别顺序即标注里的 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] # 图像级标签形式;模型的分类头、分类损失、分类指标都跟随它。 @@ -18,8 +18,8 @@ data: train: dataset: name: film_defect - img_dir: data/film/images/train - label_dir: null # 缺省按 YOLO 约定取 data/film/labels/train + img_dir: data/raw/film/images/train + label_dir: null # 缺省按 YOLO 约定取 data/raw/film/labels/train gray: false max_objects: 64 transforms: @@ -38,7 +38,7 @@ data: val: dataset: name: film_defect - img_dir: data/film/images/val + img_dir: data/raw/film/images/val label_dir: null gray: false transforms: diff --git a/data/README.md b/data/README.md index 363f1c2..365efc7 100644 --- a/data/README.md +++ b/data/README.md @@ -2,20 +2,27 @@ 存放数据集本体或指向数据集的软链接;除本文件外的内容都不入库(见 .gitignore)。 -标注采用 **YOLO 风格**:`images/` 下每张图在同层 `labels/` 里有一个同名 `.txt`。 +**原始数据一律放在 `data/raw/<数据集名>/` 下**,派生数据(如按聚类结果导出的数据集) +放在 `data/` 下与 `raw/` 平级,两者一眼可分: ``` data/ -└── film/ - ├── images/ # 原图(或指向 NAS 的软链:ln -s /mnt/nas/film/images data/film/images) - │ ├── train/0001.png - │ └── val/0002.png - ├── labels/ # 与 images 目录结构一一对应 - │ ├── train/0001.txt - │ └── val/0002.txt - └── data.yaml # path / train / val / nc / names +├── raw/ # 原始数据,只读 +│ └── film/ +│ ├── images/ # 原图(或指向 NAS 的软链:ln -s /mnt/nas/film/images data/raw/film/images) +│ │ ├── train/0001.png +│ │ └── val/0002.png +│ ├── labels/ # 与 images 目录结构一一对应 +│ │ ├── train/0001.txt +│ │ └── 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],中心点格式: ``` @@ -31,18 +38,18 @@ data/ `data.yaml` 与 ultralytics 通用: ```yaml -path: data/film +path: data/raw/film train: images/train val: images/val nc: 5 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 @@ -57,10 +64,10 @@ uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml ```bash # labelme / VOC -> YOLO txt(顺带生成 data.yaml) uv run python scripts/convert_annotations.py --format labelme \ - --input data/film/labelme --images data/film/images \ - --output data/film/labels --data-yaml data/film/data.yaml + --input data/raw/film/labelme --images data/raw/film/images \ + --output data/raw/film/labels --data-yaml data/raw/film/data.yaml # 划分 train / val(默认建软链,不复制数据) -uv run python scripts/split_dataset.py --images data/film/images \ - --labels data/film/labels --val-ratio 0.2 --data-yaml data/film/data.yaml +uv run python scripts/split_dataset.py --images data/raw/film/images \ + --labels data/raw/film/labels --val-ratio 0.2 --data-yaml data/raw/film/data.yaml ``` diff --git a/datasets/__init__.py b/datasets/__init__.py index e960139..c7896ec 100644 --- a/datasets/__init__.py +++ b/datasets/__init__.py @@ -5,6 +5,7 @@ from datasets.film_defect import FilmDefectDataset, collate_fn from datasets.registry import DATASETS from datasets.target_encoder import CenterTargetEncoder 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 __all__ = [ @@ -13,6 +14,8 @@ __all__ = [ "FilmDefectDataset", "collate_fn", "DATASETS", + "UnlabeledImageDataset", + "unlabeled_collate", "CenterTargetEncoder", "build_transforms", "parse_label_file", diff --git a/datasets/film_defect.py b/datasets/film_defect.py index 4c8be05..483478c 100644 --- a/datasets/film_defect.py +++ b/datasets/film_defect.py @@ -31,7 +31,7 @@ from utils.labels import MULTILABEL, check_label_mode @DATASETS.register("film_defect") class FilmDefectDataset(Dataset): """Args: - img_dir: 图片目录(YOLO 约定,如 `data/film/images/train`),递归查找图片。 + img_dir: 图片目录(YOLO 约定,如 `data/raw/film/images/train`),递归查找图片。 classes: 类别名列表,或指向 YOLO data.yaml 的路径(读其中的 `names`)。 列表顺序即类别下标顺序,必须与标注 txt 里的 class_id 对应。 label_dir: 标注目录;缺省按 YOLO 约定把路径里的 `images` 换成 `labels`。 diff --git a/datasets/yolo.py b/datasets/yolo.py index fa259fd..4b0adee 100644 --- a/datasets/yolo.py +++ b/datasets/yolo.py @@ -3,7 +3,7 @@ 目录约定(与 ultralytics 一致,`images` 与 `labels` 同层对应): ``` -data/film/ +data/raw/film/ ├── images/train/0001.png ├── labels/train/0001.txt # 与图片同名,后缀换成 .txt ├── images/val/... diff --git a/docs/unsupervised-clustering.md b/docs/unsupervised-clustering.md index a5abf11..08673e2 100644 --- a/docs/unsupervised-clustering.md +++ b/docs/unsupervised-clustering.md @@ -1,7 +1,7 @@ # 未分类样本的无监督聚类:算法与数学说明 本文档说明 `scripts/cluster_unlabeled.py` 背后的完整算法:每一步在做什么数学变换、 -为什么这样选、以及在 `data/film` 上的实测依据。 +为什么这样选、以及在 `data/raw/film` 上的实测依据。 代码入口:[`scripts/cluster_unlabeled.py`](../scripts/cluster_unlabeled.py)、 [`engine/cluster.py`](../engine/cluster.py)、[`engine/simclr.py`](../engine/simclr.py)、 @@ -28,7 +28,7 @@ ## 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$ 个互不相交的簇 $$ @@ -76,7 +76,7 @@ flowchart TB classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12 classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F - scan["data/film 递归扫描
N = 14,579 张 PNG
尺寸各异 84×83 ~ 4464×1139"]:::input + scan["data/raw/film 递归扫描
N = 14,579 张 PNG
尺寸各异 84×83 ~ 4464×1139"]:::input subgraph PRE["① 预处理 — 抹掉采集因子"] direction LR @@ -487,7 +487,7 @@ $$ **为什么这个指标关键**:数据里真有 $k$ 团结构时,不同初始化会收敛到同一套划分,NMI ≈ 1;若是在硬切一团连续的数据,边界位置随初值乱跳,NMI 会很低。它比轮廓系数更能识破"强行分 $k$ 类"的情形。实测中它一眼揪出了白化的问题(稳定性 0.01)。 -> **不使用外部标签。** `data/film` 下的目录名(`grease`、`wrinkle`、`反面纵黑点`……) +> **不使用外部标签。** `data/raw/film` 下的目录名(`grease`、`wrinkle`、`反面纵黑点`……) > 是人工整理的历史产物,既不完整也未必正确,因此既不参与聚类、也不用于评估。 --- diff --git a/figures/clustering-pipeline.md b/figures/clustering-pipeline.md index 596558a..b3cda31 100644 --- a/figures/clustering-pipeline.md +++ b/figures/clustering-pipeline.md @@ -1,6 +1,6 @@ # 无监督聚类流水线 -从 `data/film` 的原始图片到可人工核对的分组结果:六个阶段的数据流、张量形状与每步的数学操作。 +从 `data/raw/film` 的原始图片到可人工核对的分组结果:六个阶段的数据流、张量形状与每步的数学操作。 虚线是可选的 SimCLR 自监督分支(`method: simclr`,CUDA 与 Apple MPS 均可)。 ```mermaid @@ -13,7 +13,7 @@ flowchart TB classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12 classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F - scan["data/film 递归扫描
N = 14,579 张 PNG
尺寸各异 84×83 ~ 4464×1139"]:::input + scan["data/raw/film 递归扫描
N = 14,579 张 PNG
尺寸各异 84×83 ~ 4464×1139"]:::input subgraph PRE["① 预处理 — 抹掉采集因子"] direction LR diff --git a/figures/clustering-pipeline.mmd b/figures/clustering-pipeline.mmd index 3426869..926fd0f 100644 --- a/figures/clustering-pipeline.mmd +++ b/figures/clustering-pipeline.mmd @@ -7,7 +7,7 @@ flowchart TB classDef out fill:#FFEDD5,stroke:#EA580C,stroke-width:2px,color:#7C2D12 classDef metric fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F - scan["data/film 递归扫描
N = 14,579 张 PNG
尺寸各异 84×83 ~ 4464×1139"]:::input + scan["data/raw/film 递归扫描
N = 14,579 张 PNG
尺寸各异 84×83 ~ 4464×1139"]:::input subgraph PRE["① 预处理 — 抹掉采集因子"] direction LR diff --git a/figures/clustering-pipeline.png b/figures/clustering-pipeline.png index b0b84b6..1094933 100644 Binary files a/figures/clustering-pipeline.png and b/figures/clustering-pipeline.png differ diff --git a/scripts/convert_annotations.py b/scripts/convert_annotations.py index a852b49..6d65353 100755 --- a/scripts/convert_annotations.py +++ b/scripts/convert_annotations.py @@ -2,8 +2,8 @@ """把 labelme / VOC 标注批量转换成 YOLO 风格的 .txt。 python scripts/convert_annotations.py --format labelme \ - --input data/film/labelme --images data/film/images \ - --output data/film/labels --data-yaml data/film/data.yaml + --input data/raw/film/labelme --images data/raw/film/images \ + --output data/raw/film/labels --data-yaml data/raw/film/data.yaml 每张图输出一个同名 .txt,每行 `class_id cx cy w h`(相对宽高归一化); 类别顺序默认按扫描到的名字排序,也可以用 `--classes scratch bubble ...` 固定。 diff --git a/scripts/split_dataset.py b/scripts/split_dataset.py index 3d467a9..81c38a1 100755 --- a/scripts/split_dataset.py +++ b/scripts/split_dataset.py @@ -1,8 +1,8 @@ #!/usr/bin/env python """按比例切分成 YOLO 的 train / val / test 目录结构。 - python scripts/split_dataset.py --images data/film/images --labels data/film/labels \ - --val-ratio 0.2 --data-yaml data/film/data.yaml + python scripts/split_dataset.py --images data/raw/film/images --labels data/raw/film/labels \ + --val-ratio 0.2 --data-yaml data/raw/film/data.yaml 默认建软链(不复制数据),产出: