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>
95 lines
4.2 KiB
Markdown
95 lines
4.2 KiB
Markdown
# 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`,就是为了能这样测)。
|