feat(detect): 按来源类别处置无框图片(--empty-policy)

空标签在 YOLO 里的语义是"这张图什么都没有",是一个明确的负样本断言,而不是
"有缺陷但没能定位出来"。原先一律保留,于是 runs/boxes_gradcam 的 339 张空标签
里有 270 张是人工标成缺陷、Grad-CAM 却失手的图(blackdot 占该类 11%、black 占
24%),它们正以"此图无缺陷"的身份进入训练,等于在教模型抑制真实缺陷。

--empty-policy 接一个全局默认加若干 `类别=策略`:
  keep 当负样本保留(默认)/ drop 丢弃 / full 退化成整图框

整图框需要重写标签而不是链接源文件(内容不同),且有了确定类别之后要跟着本类
一起参与分层划分。类别名或策略拼错直接报错——静默忽略会让策略看着生效其实没有。
没显式指定、又落到 keep 的类别会打一条告警。

替换掉原来的 --drop-empty:它是一刀切,会把 normal 那 69 张有价值的负样本一起扔掉。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-11 16:34:16 +08:00
co-authored by Claude Opus 5
parent d6c6aca325
commit 012b3efc73
3 changed files with 247 additions and 34 deletions
+32 -2
View File
@@ -621,8 +621,38 @@ data/boxes_gradcam/
按类别分层(`--val-ratio` 默认 0.2,`--seed` 默认 42,两次运行结果一致)。分层是必需的:
`black` 只有 53 张、`others` 69 张,随机切很容易让某个小类在验证集里挂零,指标就成了空的。
无框的图(Grad-CAM 没框出东西的那 339 张,占 3%)**默认保留**——在检测训练里它们是负样本,
对压低误检有用。不要时加 `--drop-empty`。
### 没有框的图怎么处理
Grad-CAM 没框出东西的有 339 张(占 3%)。它们**默认保留为负样本**,对压低误检有用。
但要当心:**空标签在 YOLO 里的语义是"这张图什么都没有"**——一个明确的负样本断言,
而不是"有缺陷但没能定位出来"。而这 339 张里只有 69 张真的来自 `normal`,其余 270 张是人工
标成缺陷、Grad-CAM 却失手的图:
| 来源类别 | 空标签 | 占该类 |
|---|---|---|
| grease | 95 | 7.8% |
| blackdot | 82 | 11.3% |
| normal | 69 | 2.6% |
| strip_h | 28 | 2.9% |
| zigzag | 23 | 2.4% |
| black | 17 | 24.3% |
| 其余 9 类 | 25 | <3% |
照搬的话,`blackdot` 有 11% 的正样本、`black` 有 24% 会以"此图无缺陷"的身份进入训练,
等于在教模型抑制真实缺陷。用 `--empty-policy` 按来源类别分别处置:
```bash
uv run python scripts/prepare_yolo_dataset.py --source runs/boxes_gradcam \
--empty-policy drop normal=keep black=full edge_clean=full
```
- `keep`(默认)——当负样本保留。只有真正无缺陷的类别该用
- `drop`——丢弃。缺陷类没定位出来的属于**信息缺失**,不该冒充负样本
- `full`——退化成整图框。适合 `black`/`edge_clean` 这种描述整图属性、缺陷本就铺满全图的类别
第一个裸词是全局默认,后面 `类别=策略` 逐类覆盖。类别名或策略写错会直接报错而不是静默忽略。
没有显式指定策略、又落到 `keep` 的类别会打一条告警,提醒你确认它是否真的无缺陷。
拍平之后不同类别目录下的同名文件会互相覆盖,脚本会直接报错而不是静默丢数据;
真遇上就用 `scripts/export_clusters.py --prefix-parent` 重新导出。