
简介这份资源是面向使用 Label Studio 进行半自动标注的算法工程师与深度学习开发者提供 YOLOv8 目标检测 OBB旋转框模型接入 Label Studio ML 后端所需的 Model.py 文件。它解决的是在 Label Studio 中集成 YOLOv8 OBB 模型、实现预标注与半自动标注流程时的后端适配问题适合已具备一定 Python 与目标检测基础、希望提升标注效率的中高级用户。压缩包为 zip 格式共 1 个文件即一个 py 脚本整体约 2KB体积轻量便于直接放入 ML 后端目录使用。该脚本承担模型加载、推理与 Label Studio 预测结果转换等核心逻辑可作为搭建半自动标注流水线的关键组件。目前已有 831 人学习配套博文提供了完整接入教程读者可据此快速完成 YOLOv8 OBB 模型与 Label Studio 的对接减少自行调试后端接口的时间成本。1. Label-studio ml 后端接入 YOLOV8 OBBModel.py 到底在管什么如果你正在做旋转框标注大概率绕不开 Label-studio 的 ml 后端。普通水平框检测接起来很顺换成 YOLOV8 objectdetection-OBB 就卡在Model.py上——预测结果要么画不出来要么角度全乱要么后端直接报维度不匹配。这个文件就是 ml 后端和模型之间的翻译官它负责把 Label-studio 发来的图片 URL 读成张量调用 YOLOV8 OBB 推理再把旋转框的四个角点转成 Label-studio 认识的百分比坐标。搞懂它你就能把任意 OBB 权重挂进标注流水线做预标注、主动学习、模型迭代闭环。适合已经跑通 YOLOV8 训练、但卡在后端对接这一步的工程师也适合想用旋转框做遥感、航拍、文档版面分析的团队。2. 从 Label-studio ml 后端到 OBB 推理Model.py 的职责边界2.1 ml 后端不是模型服务是协议适配层很多人第一次看 Label-studio ml 后端会以为它是个通用推理服务器。其实它只做三件事接收 Label-studio 发来的 JSON 任务、按约定格式返回预测、把预测转成标注。真正的模型加载和推理全部由你写的Model.py承担。Label-studio 官方给的label-studio-ml-backend仓库里model.py是一个抽象基类你继承它并实现predict方法即可。对于 YOLOV8 objectdetection-OBB关键差异在于输出不是[x, y, w, h, conf, cls]而是[cx, cy, w, h, angle, conf, cls]且角度定义在不同版本里有差异。ml 后端不关心这些它只要求你返回的result列表里每个元素带from_name、to_name、type、value。所以Model.py的核心任务就是把 OBB 的旋转框转成 Label-studio 的rectanglelabels或polygonlabels能消费的结构。常见做法是继承LabelStudioMLBase在__init__里加载 YOLO 模型在predict里逐张推理。这里有个容易忽略的点ml 后端默认会并发调用predict而 Ultralytics 的 YOLO 对象不是线程安全的。我一般会在__init__里加一个threading.Lock()推理时加锁或者直接用model.predict的stream模式配合队列。另一个边界是图片读取Label-studio 传过来的是本地文件路径或 URLModel.py里要用requests或PIL读成 numpy 数组不能直接丢给 YOLO 的predict传路径否则在容器里路径映射会对不上。2.2 YOLOV8 OBB 的输出结构与坐标转换YOLOV8 OBB 的results[0].obb里有xywhr、conf、cls其中xywhr是中心点、宽、高、弧度角。Label-studio 的旋转框标注格式是rectanglelabels带rotation字段角度是角度制且旋转中心是矩形中心。转换时要注意Ultralytics 输出的角度范围通常是[-pi/2, pi/2)而 Label-studio 的rotation接受-180到180。直接乘180/pi后如果角度为负需要根据宽高是否交换来修正。更稳妥的做法是转成polygonlabels用四个角点的百分比坐标这样不依赖角度定义Label-studio 也能正常渲染。四个角点由cv2.boxPoints或手动计算以中心为原点宽高为边旋转角度后加回中心再除以图片宽高归一化。下面是一个最小可用的Model.py片段展示从 OBB 输出到 Label-studio 结果的转换逻辑import threading import numpy as np from PIL import Image from ultralytics import YOLO from label_studio_ml.model import LabelStudioMLBase class YOLOv8OBBModel(LabelStudioMLBase): def __init__(self, **kwargs): super().__init__(**kwargs) # 加载 OBB 权重任务类型必须是 obb self.model YOLO(yolov8n-obb.pt) self.lock threading.Lock() # 从 label config 里解析标签名保证顺序一致 self.labels self.get_labels() def _obb_to_polygon(self, obb_xywhr, img_w, img_h): cx, cy, w, h, angle obb_xywhr # 计算四个角点再归一化成百分比 cos_a, sin_a np.cos(angle), np.sin(angle) dx, dy w / 2, h / 2 corners np.array([ [-dx, -dy], [dx, -dy], [dx, dy], [-dx, dy] ]) rot np.array([[cos_a, -sin_a], [sin_a, cos_a]]) corners corners rot.T np.array([cx, cy]) corners[:, 0] np.clip(corners[:, 0] / img_w * 100, 0, 100) corners[:, 1] np.clip(corners[:, 1] / img_h * 100, 0, 100) return corners.flatten().tolist() def predict(self, tasks, **kwargs): results [] for task in tasks: img_path task[data][image] image Image.open(img_path).convert(RGB) img_w, img_h image.size with self.lock: preds self.model.predict(image, conf0.25, iou0.5, verboseFalse) obb preds[0].obb items [] if obb is not None: xywhr obb.xywhr.cpu().numpy() confs obb.conf.cpu().numpy() clss obb.cls.cpu().numpy().astype(int) for i in range(len(xywhr)): points self._obb_to_polygon(xywhr[i], img_w, img_h) items.append({ from_name: label, to_name: image, type: polygonlabels, value: { points: [points[j:j2] for j in range(0, 8, 2)], polygonlabels: [self.labels[clss[i]]] }, score: float(confs[i]) }) results.append({result: items}) return results这段代码里self.labels来自 Label-studio 的标注配置必须和训练时的类别顺序一致否则标签会错位。conf和iou是 OBB 推理的两个关键阈值旋转框重叠多时iou要调低到 0.3 左右。polygonlabels的points是[[x1,y1],[x2,y2],...]的列表每个点是百分比。如果你坚持用rectanglelabels需要把value换成x, y, width, height, rotation其中x, y是左上角百分比width, height是百分比宽高rotation是角度制。但旋转框的左上角在旋转后不好定义所以 polygon 更稳。2.3 把 Model.py 挂进 ml 后端并跑通本地推理写完Model.py只是第一步还要让 ml 后端能加载它。标准做法是在项目目录下建model.py和_wsgi.py用label-studio-ml start启动。但 YOLOV8 依赖较重我一般直接写一个Dockerfile基于python:3.10-slim装ultralytics、label-studio-ml、opencv-python-headless。启动命令用label-studio-ml start . --port 9090。本地验证时不要直接开 Label-studio 界面先用curl发一个假任务看返回结构对不对curl -X POST http://localhost:9090/predict \ -H Content-Type: application/json \ -d {tasks:[{data:{image:/data/test.jpg}}]}如果返回的result里points数量不对或者polygonlabels为空先检查self.labels是否解析成功。get_labels()会从 Label-studio 的label_config里读Label标签的value如果配置里用的是PolygonLabels解析出来的就是多边形标签名。另一个常见问题是图片路径ml 后端容器里的/data要和 Label-studio 的本地存储挂载一致否则Image.open会报FileNotFoundError。我通常会在predict里加一层判断如果是http开头就用requests读字节流否则按本地路径读。3. 避坑OBB 接 ml 后端最容易翻车的 5 个点3.1 角度定义不一致导致框旋转 90 度现象预测出来的旋转框和实际目标垂直或者宽高互换。原因Ultralytics 的 OBB 角度定义在不同版本里有变化早期版本xywhr的r是弧度且范围是[-pi/2, pi/2)但有些导出模型比如 ONNX会输出[0, pi/2)。Label-studio 的rotation是角度制且以顺时针为正。解决统一转成 polygon 输出彻底绕开角度定义。如果必须用rectanglelabels在转换后加一个判断当w h时交换w, h并把角度加 90 度再归一化到[-90, 90]。3.2 并发推理导致 CUDA OOM 或结果错乱现象Label-studio 一次发多张图后端报CUDA out of memory或者返回的框和图片对不上。原因ml 后端默认多线程调用predict而 YOLO 模型实例共享显存被多线程同时占用。解决在__init__里加self.lock threading.Lock()推理时with self.lock:。如果吞吐要求高改成单线程队列或者每个线程独立加载模型显存够的话。更稳的做法是设置model.predict的batch参数一次推理多张但 ml 后端的predict接口是按任务列表来的需要自己攒批。3.3 标签顺序错位导致类别全错现象预测框位置对但标签名全是错的比如把“飞机”标成“船”。原因self.labels的顺序和训练时data.yaml里的names顺序不一致。Label-studio 的label_config里标签顺序是你手动写的而 YOLO 训练时的类别顺序由数据集决定。解决不要依赖get_labels()的返回顺序直接在Model.py里硬编码一个class_names列表和训练时完全一致。如果标签多从data.yaml里读但确保 ml 后端容器里能访问到该文件。3.4 图片 EXIF 方向导致坐标偏移现象手机拍的图标注框整体偏移或旋转。原因PIL 读图时默认不应用 EXIF 旋转而 Label-studio 前端显示时应用了。解决用PIL.ImageOps.exif_transpose(image)先纠正方向再推理。或者统一在predict里读图后做一次exif_transpose保证和前端显示一致。3.5 置信度阈值和 NMS 参数不匹配业务现象漏检严重或者同一个目标出多个框。原因OBB 的conf默认 0.25iou默认 0.7但旋转框的 NMS 和水平框不同密集场景下 0.7 会保留大量重叠框。解决遥感或文档场景conf调到 0.4~0.5iou调到 0.3~0.4。如果还是漏检查agnostic_nms是否开启多类别重叠时开启会合并不同类。这些参数在model.predict里传不要写死在模型导出时。4. 进阶用 polygon 输出做主动学习与热力图验证4.1 把 OBB 预测转成 Label-studio 预标注的完整配置要让预标注在 Label-studio 里直接可编辑label_config必须用PolygonLabels并且Model.py返回的from_name要和配置里的name一致。一个可用的配置片段View Image nameimage value$image/ PolygonLabels namelabel toNameimage Label valueplane background#FFA39E/ Label valueship background#D4380D/ /PolygonLabels /View对应Model.py里from_namelabelto_nameimage。如果配置里用了RectangleLabels返回的type要改成rectanglelabelsvalue里用x, y, width, height, rotation。我建议统一用 polygon因为旋转框的四个角点信息更完整后续导出 COCO 或 YOLO OBB 格式时不用再反算。4.2 用预测结果反推模型短板热力图与损失曲线预标注跑起来后别急着全量标注。先抽 50 张图把Model.py返回的score存下来按分数排序。低分样本就是模型的盲区。更直观的做法是在predict里额外返回一个热力图叠加图但 ml 后端不支持直接返回图片。我一般会在本地写一个脚本用同样的Model.py逻辑跑一遍验证集把 OBB 的xywhr画到原图上同时用model.predict的saveTrue输出可视化结果。如果发现某类目标的角度预测总是偏检查训练时的degrees增强参数OBB 任务里degrees设 0 会导致角度过拟合设 180 又太激进通常 90 左右比较稳。损失曲线方面YOLOV8 OBB 的loss包含box_loss、cls_loss、dfl_loss和angle_loss如果angle_loss震荡不降多半是学习率太大或 batch 太小先把lr0降到 0.001 试试。4.3 一个我踩过的坑ml 后端超时与重试Label-studio 默认等待 ml 后端 30 秒超时后会重试。OBB 模型如果输入分辨率大比如 1024单张推理可能超过 30 秒。现象是前端一直转圈后端日志里同一个任务被请求多次。解决在Model.py里把输入尺寸降到 640或者用model.export(formatonnx)导出后走 ONNX Runtime推理速度能快 2~3 倍。另外在 ml 后端的启动参数里加--timeout 120给足时间。如果还是慢检查是不是每次predict都重新加载了模型——模型必须在__init__里加载一次不能放在predict里。我现在的习惯是任何 OBB 项目接 ml 后端先跑通一张图的 polygon 输出再用curl压 10 张图看显存和耗时最后才开 Label-studio 界面。这样能把大部分坐标和并发问题挡在标注之前。希望帮到你。本文还有配套的精品资源点击获取