Files
ModelTrainEval/ml_backend/README.md
T
Shaun ZhouandClaude Opus 5 a4d099e41d feat(ml-backend): 把训练好的 YOLO 接进 Label Studio 做预标注
ml_backend/ 是一个 label-studio-ml 后端:Label Studio 打开任务时调 /predict,
后端跑 YOLO 并把框转成 Label Studio 的百分比坐标返回,人工只需修正。

- model.py:FilmDefectDetector。权重懒加载且只加载一次(Flask 多线程,加锁);
  控件名从项目的 labeling config 读取,不写死;model_version 带权重的修改时间,
  换权重后 Label Studio 能区分新旧预测。
- convert.py:坐标转换,刻意不 import label_studio_ml,好在主环境里单测。
- fit():每条标注累加计数,达到 RETRAIN_AFTER 就起子进程执行 RETRAIN_COMMAND
  并立刻返回——训练几十分钟,同步做会卡住 Label Studio。
- _wsgi.py 由官方模板生成,ruff 跳过,便于跟随上游。

必须用独立虚拟环境:label-studio-ml 会把 jinja2 降到 3.0.3,而 label-studio 1.23
需要 3.1,装进主环境会直接弄坏 Label Studio。PyPI 上的 1.0.9 是 2023 年的旧版,
要装 GitHub 上的现行版本。

本地用 COCO 权重端到端验证过:/health、/predict(bus.jpg 出 10 个框、坐标换算正确)、
/webhook(第 3 条标注触发重训命令并清零计数)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 17:36:12 +08:00

95 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Label Studio ML Backend:用训练好的 YOLO 做预标注
把 `runs/detect/<name>/weights/best.pt` 接进 Label Studio,打开任务时自动给出框,
人工只需修正而不是从零画。标注攒够数量还能自动触发重训,形成"边标边训"的闭环。
## 为什么要单独的虚拟环境
`label-studio-ml` 会把 jinja2 降到 3.0.3、带进 Flask 1.1.2,而 label-studio 1.23 需要
jinja2 3.1。装进主环境会直接弄坏 Label Studio。它本来就是个独立服务,单独建环境最省事。
另外 **PyPI 上的 `label-studio-ml` 是 2023 年的 1.0.9,不要用**,要装 GitHub 上的现行版本
(本文档基于 2.0.1.dev0 验证)。
```bash
uv venv .venv-ml --python 3.12
VIRTUAL_ENV=.venv-ml uv pip install \
"label-studio-ml @ git+https://github.com/HumanSignal/label-studio-ml-backend.git" \
ultralytics
```
## 启动
```bash
cd ml_backend
YOLO_WEIGHTS=$PWD/../runs/detect/film/weights/best.pt \
YOLO_DEVICE=cuda \
LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT=/home/deep/labeling \
../.venv-ml/bin/python _wsgi.py -p 9090
```
**用 `python _wsgi.py` 而不是 `label-studio-ml start .`**:后者用 PATH 上的 python 起子进程,
在独立环境里会 `ModuleNotFoundError`。
`LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT` 必须和启动 Label Studio 时的值一致——任务里的图片是
`/data/local-files/?d=...`,后端要靠它把 URL 还原成磁盘路径。图片若来自上传或云存储,
则改为设置 `LABEL_STUDIO_URL` 与 `LABEL_STUDIO_API_KEY`,后端会自行下载。
健康检查:
```bash
curl http://localhost:9090/health
# {"model_class":"FilmDefectDetector","status":"UP"}
```
## 接入项目
Label Studio 里:Settings → **Model** → Connect Model,URL 填 `http://localhost:9090`。
- 勾选 **Interactive preannotations** 后,打开任务就会实时调用模型;
- 也可以在 Data Manager 里选中若干任务 → Actions → **Retrieve Predictions** 批量拉取。
**类别名必须和 labeling config 里的 `<Label value="...">` 完全一致。** 后端用的是权重里的
`model.names`(也就是训练时 `data.yaml` 的 names),对不上的话 Label Studio 会拒绝这条预测。
我们的模型是 9 类:black、blackdot、grease、mosquito、normal、obstacle、ripple、uneven、wrinkle。
## 环境变量
| 变量 | 默认 | 说明 |
| --- | --- | --- |
| `YOLO_WEIGHTS` | `yolo26n.pt` | 权重路径。版本号里带它的修改时间,换权重后 Label Studio 能区分新旧预测 |
| `YOLO_CONF` | `0.25` | 置信度阈值。预标注可以调低些,宁可多给几个框让人删 |
| `YOLO_IMGSZ` | `640` | 推理尺寸,与训练保持一致 |
| `YOLO_DEVICE` | `cuda` | `cuda` / `cpu` / `mps` |
| `RETRAIN_AFTER` | `0` | 累计多少条新标注触发重训,0 = 不触发 |
| `RETRAIN_COMMAND` | 空 | 触发时执行的命令 |
## 边标边训
Label Studio 每产生一条标注就会 POST `/webhook`,后端在 `fit()` 里累加计数,
达到 `RETRAIN_AFTER` 就起一个**子进程**执行 `RETRAIN_COMMAND` 并立刻返回——
训练要几十分钟,绝不能在这个请求里同步做,否则会把 Label Studio 卡住。
```bash
RETRAIN_AFTER=200 \
RETRAIN_COMMAND="/home/deep/ModelTrainEval/scripts/retrain.sh" \
../.venv-ml/bin/python _wsgi.py -p 9090
```
`retrain.sh` 需要自己写,大致是:导出项目 → `build_yolo_dataset.py` 装配 → `detect.py` 训练 →
把新权重放到 `YOLO_WEIGHTS` 指向的位置 → 重启本服务(或让它下次启动时加载新权重)。
**目前权重只在进程启动时加载一次**,重训完要重启服务才会生效。
## 已验证的行为
本地用 COCO 预训练权重实测过:
- `/health` 返回 UP;
- `/predict` 用 Label Studio 的真实请求体(tasks + label_config + params)调用,
`bus.jpg` 返回 10 个框,坐标换算正确(百分比、`original_width/height` 与原图一致);
- 图片无检出时返回一条空 `result` 的预测,表示"这张图没问题";
- `/webhook` 连发 3 条 `ANNOTATION_CREATED`,在第 3 条触发 `RETRAIN_COMMAND` 并把计数清零。
转换逻辑的单元测试在 `tests/test_ml_backend_convert.py`,跑在主环境里
(`convert.py` 刻意不 import `label_studio_ml`,就是为了能这样测)。