refactor(detect): 数据集改为扁平结构、不划分 train/val,Label Studio 文件放到根目录

prepare_yolo_dataset.py 的产出先导入 Label Studio 复核,划分留到复核之后,于是:

- 去掉按类别分层的 train/val 切分以及 --val-ratio / --seed,images/ 与 labels/
  下直接是文件;name_map.csv 去掉 split 列,summary.json 的逐类数量改为单列。
- label_studio/ 子目录的内容上移一层:扁平的数据集本身就是 label-studio-converter
  要求的 <input>/{images,labels,classes.txt},tasks.json 与 labeling config 直接
  生成在根目录,不再需要额外的硬链接视图,DOCUMENT_ROOT 即数据集根目录。
- data.yaml 的 train 与 val 只能指向同一个 images/。ultralytics 能读,但验证指标
  算的是训练集本身,mAP 虚高、早停与 best.pt 的选择失真。脚本结束时告警,
  detect.py 检测到 train == val 时也告警。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-15 17:19:53 +08:00
co-authored by Claude Opus 5
parent 1a43267b55
commit af0897f8c7
4 changed files with 150 additions and 195 deletions
+25 -33
View File
@@ -589,15 +589,15 @@ runs/<name>/
## 检测模型训练(YOLO)
上一步的产出按类别分目录,方便人工核对;检测训练要的是 train/val 划分——类别写在标签
第一列,不靠目录名。所以先整理再训练:
上一步的产出按类别分目录,方便人工核对;YOLO 的类别写在标签第一列,不靠目录名。
所以先拍平整理,导入 Label Studio 复核,再训练:
```bash
# 1) 拍平类别目录、分层切分、硬链接到 data/(不占额外磁盘)
uv run python scripts/prepare_yolo_dataset.py --source runs/boxes_gradcam
# 1) 拍平类别目录、统一 PNG、按类别重命名,硬链接到 data/;同时生成 Label Studio 任务
uv run python scripts/prepare_yolo_dataset.py --source runs/boxes_shap --label-studio
# 先看计划加 --dry-run
# 2) 训练
# 2) 训练(训练前需要先划分,见下文"不划分 train/val")
uv run python detect.py --config configs/detect/film.yaml
# 只评估 / 续训
@@ -609,15 +609,22 @@ uv run python detect.py --config configs/detect/film.yaml --resume
产出:
```
data/boxes_gradcam/
├── images/{train,val}/<类别>_<序号>.png # 一律 PNG:PNG 原图硬链接,BMP 等无损转换
├── labels/{train,val}/<类别>_<序号>.txt
├── classes.txt # 每行一个类别名,行号即 class_id
├── name_map.csv # 新文件名 ↔ 源文件路径
├── data.yaml # path 写绝对路径,ultralytics 从哪跑都能读到
└── summary.json # 划分明细、逐类数量、落盘方式
data/boxes_shap/
├── images/<类别>_<序号>.png # 一律 PNG:PNG 原图硬链接,BMP 等无损转换
├── labels/<类别>_<序号>.txt
├── classes.txt # 每行一个类别名,行号即 class_id
├── name_map.csv # 新文件名 ↔ 源文件路径
├── data.yaml # train 与 val 都指向 images/
├── summary.json # 逐类数量、空标签处置、落盘方式
├── tasks.json # 仅 --label-studio:每张图一个任务,框作为 predictions
└── tasks.label_config.xml # 仅 --label-studio:各类别的 RectangleLabels
```
**不划分 train/val。** 这份数据集先导入 Label Studio 复核,划分留到复核之后。`data.yaml`
的 `train` 与 `val` 只能指向同一个 `images/`,ultralytics 能读,但验证指标算的是训练集本身,
mAP 会虚高,早停与 `best.pt` 的选择也随之失真。脚本结束时会告警,`detect.py` 检测到
train 与 val 相同时也会告警。正式训练前要先划分。
**图片统一成 PNG。** pearlescent / blue / uv 的原始数据是 BMP(共 10,814 张),
`generate_boxes.py` 按原后缀链接,于是 YOLO 数据集里会混着两种格式。这一步按**文件头**
(不是后缀)判断:真 PNG 照旧硬链接不占磁盘,其余无损转 PNG——不缩放像素,归一化的框坐标
@@ -626,8 +633,8 @@ data/boxes_gradcam/
**按类别重新编号。** 源文件名五花八门(`反面纵黑点_1_550mmX1561.71m_20260416160441_0.png`、
`pearlescent_404.bmp`……),输出统一改成 `<类别>_<序号>.png`,如 `blackdot_0042.png`。
类别以标签第一列为准,文件名永远不会与标签矛盾;序号在类内按源路径排序、与 train/val 划分
无关,换 `--seed` 或 `--val-ratio` 重跑,同一张图拿到的名字不变。原文件名里的正反面、位置、
类别以标签第一列为准,文件名永远不会与标签矛盾;序号在类内按源路径排序,同一份来源重跑,
同一张图拿到的名字不变。原文件名里的正反面、位置、
时间戳等信息都在 `name_map.csv` 里(带 BOM,Excel 直接打开中文不乱码)。想保留原名加 `--keep-names`。
**classes.txt** 是给 LabelImg / X-AnyLabeling 这类标注工具用的,人工复核、修改框时
@@ -635,18 +642,8 @@ data/boxes_gradcam/
### 导入 Label Studio 人工复核
弱监督框需要人工把关。加 `--label-studio` 会额外生成一份可以直接导入的视图:
```bash
uv run python scripts/prepare_yolo_dataset.py --source runs/boxes_shap --label-studio
```
```
data/boxes_shap/label_studio/
├── images/*.png labels/*.txt classes.txt # 两个 split 拍平,硬链接
├── tasks.json # 每张图一个任务,框作为 predictions
└── tasks.label_config.xml # 9 个类别的 RectangleLabels
```
弱监督框需要人工把关。加 `--label-studio` 会在数据集根目录额外生成 `tasks.json` 与
`tasks.label_config.xml`,数据集根目录本身就是 Label Studio 的 `DOCUMENT_ROOT`。
脚本结束时会打印带实际路径的导入步骤:设置 `LABEL_STUDIO_LOCAL_FILES_*` 环境变量启动、
粘贴 labeling config、添加 Local files 存储(**只保存不要 Sync**,否则每张图多出一个空任务)、
@@ -654,8 +651,8 @@ data/boxes_shap/label_studio/
几个取舍:
- **为什么要拍平**:`label-studio-converter import yolo` 只认 `<input>/{images,labels,classes.txt}`,
且 `os.listdir` 不递归,看不见 `train/`、`val/`。按类别重命名后文件名全局唯一,可以直接合并。
- **为什么是扁平结构**:`label-studio-converter import yolo` 只认 `<input>/{images,labels,classes.txt}`,
且 `os.listdir` 不递归,看不见子目录。数据集不划分、直接扁平,正好就是它要的结构。
- **硬链接而不是软链接**:Label Studio 读本地文件时会检查路径是否在 `DOCUMENT_ROOT` 之下,
软链接解析后可能落到外面。
- **框作为 predictions 而不是 annotations**:它们是模型生成的。审核员修改提交后才成为 annotation,
@@ -667,11 +664,6 @@ data/boxes_shap/label_studio/
> **复核重点是 `normal`。** SHAP 在每张图上都框了东西,`normal` 有 4,295 个框、占 38%。
> 若 `normal` 表示无缺陷,这些框要删掉,否则检测器会学着"检测正常"。
### 划分
按类别分层(`--val-ratio` 默认 0.2,`--seed` 默认 42,两次运行结果一致)。分层是必需的:
`black` 只有 53 张、`others` 69 张,随机切很容易让某个小类在验证集里挂零,指标就成了空的。
### 没有框的图怎么处理
Grad-CAM 没框出东西的有 339 张(占 3%)。它们**默认保留为负样本**,对压低误检有用。