Files
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

74 lines
2.3 KiB
Python

"""YOLO 结果 <-> Label Studio 结果格式的互转。
单独成一个模块、不 import label_studio_ml,这样主环境里就能直接测:
ML backend 跑在自己的虚拟环境里(见 ml_backend/README.md),主环境装不了它。
Label Studio 的框用**百分比**表示左上角与宽高(0~100),YOLO 用归一化的中心点与宽高。
`original_width` / `original_height` 必须是图片的真实像素尺寸,Label Studio 据此换算。
"""
from __future__ import annotations
import uuid
from collections.abc import Sequence
def box_to_result(
class_name: str,
xyxyn: Sequence[float],
width: int,
height: int,
from_name: str,
to_name: str,
score: float | None = None,
) -> dict:
"""一个归一化 xyxy 框 -> 一条 Label Studio 的 rectanglelabels 结果。"""
x1, y1, x2, y2 = (min(max(float(v), 0.0), 1.0) for v in xyxyn)
item = {
"id": uuid.uuid4().hex[:10],
"type": "rectanglelabels",
"from_name": from_name,
"to_name": to_name,
"original_width": int(width),
"original_height": int(height),
"image_rotation": 0,
"value": {
"x": x1 * 100,
"y": y1 * 100,
"width": (x2 - x1) * 100,
"height": (y2 - y1) * 100,
"rotation": 0,
"rectanglelabels": [class_name],
},
}
if score is not None:
item["score"] = float(score)
return item
def to_prediction(
boxes: Sequence[tuple[str, Sequence[float], float]],
width: int,
height: int,
from_name: str,
to_name: str,
model_version: str,
) -> dict:
"""一张图的若干框 -> 一条 Label Studio 预测。
Args:
boxes: (类别名, 归一化 xyxy, 置信度) 的序列。
预测整体的 `score` 取所有框里最高的置信度:Label Studio 用它给任务排序,
取最大值才能让"模型最有把握的图"排在前面;没有框时为 0,表示这张图没找到缺陷。
"""
results = [
box_to_result(name, xyxyn, width, height, from_name, to_name, score)
for name, xyxyn, score in boxes
]
return {
"model_version": model_version,
"score": max((score for _, _, score in boxes), default=0.0),
"result": results,
}