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
默认建软链(不复制数据),产出: