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>
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 验证)。
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
启动
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,后端会自行下载。
健康检查:
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 卡住。
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,就是为了能这样测)。