"""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, }