Label Studio与YOLOv8 OBB集成:Model.py核心实现与旋转框坐标转换指南 简介面向Label Studio用户与YOLOv8目标检测开发者Model.py文件用于将YOLOv8 OBB旋转框检测模型接入Label Studio ML后端实现图片旋转目标的半自动标注适合遥感影像、工业质检、航拍物体识别等需要旋转框标注的场景。资源仅包含1个Python脚本压缩包大小2KB体积精简但逻辑完整可直接放入Label Studio ml backend项目中使用也可作为二次开发的参考模板。对应CSDN博客教程提供了详细的配置流程能帮助读者快速跑通模型预测、标注结果回传与标签映射等关键环节。已有829人学习下载适合具备一定YOLOv8与Label Studio基础、希望搭建半自动标注管线的开发者。通过该脚本可学习OBB模型在Label Studio推理接口中的写法理解预测坐标与Label Studio标注格式之间的转换方法减少从零实现后端的时间成本并为后续接入其他自定义模型提供清晰的代码结构。1. 这个 Model.py 到底要解决什么问题1.1 Label Studio ML 后端不是“跑模型的插件”很多人第一次接触 Label Studio 的 ML 后端时容易把它想象成一个“插件”安装一个包、配置一个地址模型就能自动在标注界面里识别目标。实际用过一轮就会发现这玩意儿是一个独立的 HTTP 服务Label Studio 在需要预标注、交互标注或者训练触发的时刻会按约定好的 API 去调用你的服务。也就是说ML 后端本质上是“给你的标注流程装了一个 AI 大脑”而这个大脑怎么思考、怎么说话全看 Model.py 怎么实现。我在做旋转目标检测OBB项目时第一个任务就是写这个 Model.py。当时项目里有一大批遥感图像的旋转框标注需求目标物全部是任意朝向的飞机、油罐、车辆用普通矩形框去包会把大量背景包进来模型根本训不出来。所以标注阶段就必须用旋转框。Label Studio 在 1.11 之后的版本里支持了带 rotation 参数的 RectangleLabels正好和 YOLOv8 的 OBB 输出对得上。而要把两者串起来核心文件就是 Model.py。1.2 为什么 OBB 比普通矩形检测难一截普通目标检测模型的输出是 x, y, width, height最多加一个类别和置信度坐标系的坑相对少。OBB 模型多输出一个角度而且 YOLOv8 官方 OBB 模型的输出格式是 xywhr其中 r 是弧度不是角度。这个“弧度”问题是我当时踩的第一个坑。Label Studio 的旋转框渲染方式也很有迷惑性。它不是给你一个任意四点坐标的多边形而是把矩形框拆成一个中心点坐标 (x, y)、宽width、高height、旋转角度rotation角度单位是度范围是 -180 到 180。这和模型输出的弧度体系需要对一套换算逻辑。更麻烦的是Label Studio 里的 x、y、width、height 全部用了相对图像的百分比坐标甚至旋转角度也要理解成相对整体坐标系的角度而不是相对图像 X 轴的局部夹角。很多文档只写了“支持旋转框”但没人告诉你 rotation 计算时还要注意图像宽高比带来的视觉差异这些细节只能在写代码时逐步试出来。1.3 整体服务链路和 Model.py 的职责ML 后端的标准链路是这样的Label Studio 前端发起预标注请求 → 后端服务收到 POST /predict → 在 Model.py 的 predict 方法里拿到标注任务的 JSON → 解析出图像路径和标签配置 → 调用 YOLOv8 模型推理 → 把结果转换成 Label Studio 的标注格式返回。整个链路中Model.py 既是一个协议翻译器又是一个推理封装层。此外如果要在标注平台上直接点击 “Start Training” 按钮Model.py 里还得实现 fit 方法用来接收已完成的标注任务并触发重训练。这一块很多人干脆不做但实际项目里用起来非常顺手。下面我会按照“骨架搭建 → predict 核心实现 → fit 实现 → 部署配置 → 排坑记录”的顺序把这份 Model.py 从头到尾掰开讲清楚。2. 先搭骨架Model.py 的类结构与初始化2.1 导入与工具函数先把基础打好label-studio-ml-backend 官方模板里会自动生成一个 Model.py里面有一个继承自 LabelStudioMLBase 的类。我建议保留这个继承关系不要自己另起炉灶去写普通类因为 predict 和 fit 的调用约定、参数格式都由这个基类管理。一个最基础的导入清单长这样import os import json import logging from pathlib import Path import numpy as np from PIL import Image from ultralytics import YOLO from label_studio_ml.model import LabelStudioMLBase from label_studio_ml.response import ModelResponse from label_studio_ml.utils import get_image_local_path, get_single_tag_keys这些工具函数都很实用。get_image_local_path 可以把 Label Studio 传过来的图片 URL 或者本地路径转换成可以直接读取的本地文件路径get_single_tag_keys 可以一次性拿到标注配置里的 from_name、to_name 和标签列表。这里有一个比较容易被忽视的点get_single_tag_keys 默认只处理单一标签配置如果你的项目里一个图像同时使用了多个 control tag这个方法就不够用了需要自己写一段遍历标签配置的逻辑。2.2init里该缓存什么、不该缓存什么我在 init 里做三件事加载模型、读取标签配置、保存一个预热推断。模型加载放在 init 里很重要因为 ML 后端服务一启动就会实例化这个类之后每次 predict 请求都复用同一个模型对象不用反复加载权重。class OBBYOLOModel(LabelStudioMLBase): def __init__(self, model_path: str yolov8n-obb.pt, device: str cpu, conf_threshold: float 0.35, **kwargs): super().__init__(**kwargs) self.model_path model_path self.device device self.conf_threshold conf_threshold # 这里是关键YOLO OBB 模型需要明确指定 task 为 obb self.model YOLO(model_path, taskobb) self.model.to(device) # 从标注配置里解析标签名 self.from_name, self.to_name, self.value, self.labels get_single_tag_keys( self.parsed_label_config, RectangleLabels, Image ) # 预热一次避免首次推理时 CUDA 初始化延时导致前端请求超时 dummy np.zeros((640, 640, 3), dtypenp.uint8) self.model.predict(dummy, verboseFalse) logging.info(fOBB model loaded from {model_path}, labels: {self.labels})有人会把 conf_threshold、iou_threshold 这些参数也一股脑塞进 init其实没必要这些参数更适合放进 predict 方法里按请求动态调整因为不同的标注任务可能需要不同的置信度阈值。另外设备选择我建议用字符串参数从环境变量读取而不是写死这样部署到 GPU 服务器或者本地 CPU 环境时不用改代码。2.3 使用 GPU 还是 CPU热搜里有个问题叫“需要用到 gpu 吗”我的观点很明确YOLOv8n-obb 这种小体量模型CPU 推理单张 640 分辨率的图大概 200~500 毫秒标注场景下完全能接受但如果你用的是 yolov8m-obb 或更大的模型CPU 推理会到 3 秒以上标注界面的体验就很差了。我的建议是本地调试用 CPU正式部署用 GPU。export LABEL_STUDIO_ML_DEVICEcuda:0然后在 init 里从环境变量读取 device 即可。这样换机器部署时只改环境变量不用改代码。3. predict 核心实现从标注请求到旋转框结果3.1 解析请求中的图像和标签predict 方法的输入是一个大 JSON里面包含任务信息、图像路径、标注配置等。第一步是把这个 JSON 处理成可用的数据。def predict(self, tasks, contextNone, **kwargs): tasks 是一个列表每个元素是一个标注任务 results [] for task in tasks: # 获取图像本地路径 image_path get_image_local_path(task[data][image]) # 读取为 RGB 数组注意 YOLO 用的是 RGB不要用 cv2 默认的 BGR image Image.open(image_path).convert(RGB) orig_w, orig_h image.size # 统一放入列表中方便最后返回 results.extend(self._predict_single(task, image, orig_w, orig_h)) return ModelResponse(predictionsresults)这里有个非常容易出问题的点很多人习惯用 cv2.imread 读图但 cv2 读出来是 BGR 通道顺序直接用 YOLO 推理会让模型效果明显变差。我一开始用 cv2 读图测 OBB 模型准确率直接掉了十几个点排查了半天才发现是通道顺序的问题。用 PIL 读图转 RGB 就省心得多。3.2 YOLOv8 OBB 推理与结果筛选YOLO 推理本身很简单一行代码。但要特别注意OBB 模型推理返回的对象里包含一个 obb 属性里面才有旋转框的 xywhr 数据。def _predict_single(self, task, image, orig_w, orig_h): # 推理 result self.model.predict( sourceimage, confself.conf_threshold, verboseFalse, )[0] if result.obb is None: return [{ result: [], score: 0.0, }] boxes result.obb.xywhr.cpu().numpy() # (N, 5) - x_center, y_center, w, h, angle(rad) classes result.obb.cls.cpu().numpy().astype(int) confs result.obb.conf.cpu().numpy().astype(float) return self._convert_to_ls_format(boxes, classes, confs, orig_w, orig_h)result.obb 是 None 表示模型没有检测到任何目标这种情况要直接返回空结果。我在实际项目里见过一些代码在这里处理不当没有对 None 做判断导致前端标注页面直接打不开或者报错。另外如果项目中有背景类或者需要过滤某些类别也可以在这里加一个类别白名单。3.3 OBB 输出转成 Label Studio 的 rotation 格式这一节是整个 Model.py 的关键也是新手最容易翻车的地方。model 输出的 boxes 是 xywhr其中 x、y、w、h 都是像素坐标r 是弧度Label Studio 需要的是相对坐标百分比x、y、width、height 以及角度 rotation度。转换逻辑如下def _convert_to_ls_format(self, boxes, classes, confs, orig_w, orig_h): results [] for box, cls, conf in zip(boxes, classes, confs): x_center, y_center, w, h, theta_rad box # 1. 像素坐标转相对坐标百分比Label Studio 用 0~100 x_rel (x_center - w / 2) / orig_w * 100.0 y_rel (y_center - h / 2) / orig_h * 100.0 w_rel w / orig_w * 100.0 h_rel h / orig_h * 100.0 # 2. 弧度转角度并归一化到 [-180, 180] theta_deg np.degrees(theta_rad) if theta_deg 180: theta_deg - 360 elif theta_deg -180: theta_deg 360 # 3. 组织成 Label Studio 的 result 结构 results.append({ from_name: self.from_name, to_name: self.to_name, type: rectanglelabels, value: { rectanglelabels: [self.labels[cls]], x: round(x_rel, 2), y: round(y_rel, 2), width: round(w_rel, 2), height: round(h_rel, 2), rotation: round(theta_deg, 2), }, score: float(conf), }) return [{result: results, score: float(max(confs)) if len(confs) else 0.0}]这里我踩过一个大坑YOLOv8 OBB 的 xywhr 输出中w 是旋转框的长边h 是短边角度是相对 x 轴逆时针旋转的角度。而 Label Studio 里 rotation 为 0 时框的宽边沿着水平方向height 边沿着竖直方向。模型的角度体系如果直接搬过来视觉上会出现“框的方向对了但长短边互换”的效果尤其是对于长宽比很大的旋转目标比如飞机机身这种看起来特别违和。解决方式有两种一个是在后处理里对 w/h 和角度做统一修正比如当框的宽度小于高度时交换 w/h 并加上 90 度另一个是接受模型的输出约定在标签里约定与模型一致的朝向。我在生产项目里用的是第一种具体做法是# 如果旋转框的“高”大于“宽”说明模型把长边放在了高上 # 视觉上 Label Studio 显示时就会混淆需要交换宽高并调整角度 if h_rel w_rel: w_rel, h_rel h_rel, w_rel theta_deg 90.0 if theta_deg 180: theta_deg - 360这个方法实测下来效果最好但注意它只适用于“模型输出的 w 代表长边”的约定不同训练方式可能会有差异建议在接入自己的模型时先拿几张图验证一下。3.4 关键换算模型缩放、图像原始尺寸、标注坐标系另一个让人脑袋发胀的问题是缩放。YOLO 推理时内部会把输入图统一缩放到模型输入尺寸默认 640返回的 xywhr 坐标依然是相对原始输入图的像素位置这个由 ultralytics 库自己处理好了理论上我们不需要手动做任何坐标反算。但如果有人用其他框架导出的 OBB 模型或者手动预处理了图像输出坐标就不一定是相对原图的这时就必须记录 resize 比例做逆映射。还有一点如果你在 predict 里对图像做了中心裁剪、letterbox 之类的预处理输出坐标的逆变换逻辑会更麻烦我建议能不做就不做。一张常规的遥感图像直接整图喂给模型做 OBB 推理速度完全够用不值得为了省一点显存引入坐标映射的复杂度。4. fit 方法把标注平台的旋转框数据回传训练4.1 fit 的入口和数据整理Label Studio 的 ML 后端在界面点击 “Start Training” 时会调用 POST /train 接口对应 Model.py 里的 fit 方法。这个方法接收一批已经完成标注的任务列表我们可以在这里把旋转框标注导出成模型训练需要的数据集。fit 的完整流程有三个步骤收集标注结果、转换成 YOLO OBB 训练格式、启动训练。我实现过一个简化版本核心代码如下def fit(self, tasks, workdirNone, **kwargs): if workdir is None: workdir os.path.dirname(os.path.abspath(__file__)) # 创建 YOLO OBB 格式的数据集目录 train_dir Path(workdir) / dataset / train train_dir.mkdir(parentsTrue, exist_okTrue) sample_count 0 for task in tasks: image_path get_image_local_path(task[data][image]) # 将图片复制到训练集目录 img_dst train_dir / Path(image_path).name shutil.copy(image_path, img_dst) # 提取旋转框标注 annotations task.get(annotations, []) if not annotations: continue annotation annotations[0] label_txt self._convert_annotation_to_yolo_obb( annotation, task[data][image] ) txt_dst train_dir / (Path(image_path).stem .txt) txt_dst.write_text(label_txt) sample_count 1 if sample_count 10: logging.warning(样本数量太少建议至少 10 张图再开始训练) return {status: skipped, reason: not enough samples} # 启动训练 yaml_path self._create_dataset_yaml(train_dir) self.model.train( datastr(yaml_path), epochsint(os.getenv(YOLO_EPOCHS, 50)), imgsz640, deviceself.device, ) return {status: ok, samples: sample_count}4.2 转换标注到 YOLO OBB 训练格式YOLO OBB 的标签格式是class_id 后面跟着 4 个点的 8 个坐标值坐标是相对图像宽高的归一化浮点数范围 0~1。这和 Label Studio 的 x、y、width、height、rotation 格式不一样必须做一次几何转换。转换方法是先把 Label Studio 的中心点 宽高 旋转角还原成四个顶点坐标再归一化def _convert_annotation_to_yolo_obb(self, annotation, image_path): image Image.open(get_image_local_path(image_path)) orig_w, orig_h image.size lines [] for item in annotation.get(result, []): if item.get(type) ! rectanglelabels: continue value item[value] label_name value[rectanglelabels][0] class_id self.labels.index(label_name) # 还原 OBB 顶点相对像素坐标 cx value[x] / 100.0 * orig_w cy value[y] / 100.0 * orig_h w value[width] / 100.0 * orig_w h value[height] / 100.0 * orig_h theta_deg value[rotation] theta_rad np.radians(theta_deg) # 计算旋转矩形的四个顶点 cos_a, sin_a np.cos(theta_rad), np.sin(theta_rad) dx, dy w / 2, h / 2 corners [ (cx dx * cos_a - dy * sin_a, cy dx * sin_a dy * cos_a), (cx - dx * cos_a - dy * sin_a, cy - dx * sin_a dy * cos_a), (cx - dx * cos_a dy * sin_a, cy - dx * sin_a - dy * cos_a), (cx dx * cos_a dy * sin_a, cy dx * sin_a - dy * cos_a), ] # 归一化到 0~1 norm_corners [(x / orig_w, y / orig_h) for x, y in corners] # 推荐的 OBB 格式顺时针从左上角开始这里按逆时针写也能训练但要保持一致 flat [coord for point in norm_corners for coord in point] lines.append(f{class_id} .join(f{c:.6f} for c in flat)) return \n.join(lines)这个转换看着简单但几何细节很容易出错。我建议做好之后先拿一张图对着数一下四个顶点的坐标再和 Label Studio 界面里显示的实际框位置对照确认无误后再批量训练不然会把错误数据喂给学生模型。5. 部署、配置与启动5.1 用模板工程快速拉起来Label Studio 官方提供了 label-studio-ml-backend 模板里面已经包含了 FastAPI 服务和 Model.py 的脚手架。我推荐用这个模板起步别自己从零写 HTTP 服务那会浪费大量时间。pip install label-studio-ml label-studio label-studio-ml start ./my_obb_model --port 9090如果你用的是官方模板把 Model.py 替换成上面的内容然后在同目录下新建一个模型权重文件比如 yolov8n-obb.pt就可以直接启动。5.2 标签配置示例要让预标注正常生效Label Studio 项目里的标签配置必须和 Model.py 里解析的配置一致。我的 OBB 项目配置长这样View Image nameimage value$image/ RectangleLabels namelabel toNameimage rotatedtrue Label valueairplane background#FF0000/ Label valueship background#00FF00/ Label valuevehicle background#0000FF/ /RectangleLabels /View这里必须要写 rotatedtrue否则 Label Studio 不会接受带 rotation 的预标注结果界面上的框会被硬生生地当成普通水平矩形显示。远程图片记得在项目设置里共享文件或者开启本地文件访问否则 ML 后端请求图片时会遇到权限问题。5.3 在 Label Studio 页面里绑定 ML 后端部署完成后打开 Label Studio进入项目的 Settings → Machine Learning → Add Model填上http://localhost:9090即可。测试的时候可以点“Validate”按钮确认服务连通。启动服务后建议先手动在标注界面画一个旋转框观察它的坐标和角度再点击“智能标注”或者“预标注”按钮对比模型生成的框。这个过程能帮你快速发现坐标转换和角度转换的问题。6. 常见问题与排查技巧实录6.1 旋转框显示问题速查表我在接入过程中碰到的问题以及对应的排查方向整理成表格现象可能原因解决方案预标注的框是水平矩形不旋转标签配置缺少 rotatedtrue在 RectangleLabels 标签上加入该属性旋转框方向反了角度符号没有统一Label Studio 中逆时针为正检查角度符号约定框的位置偏移和物体不重合图像读取时通道顺序错乱或坐标未按原图尺寸归一化用 PIL 按 RGB 读图确保先取 orig_w/orig_h 再转换框的长短边颠倒模型输出的 xywhr 中 w/h 含义和 Label Studio 不一致在后处理中交换 w/h 并调整角度完全不输出任何框模型推理失败或 result.obb 为空检查模型权重是否是 OBB 类型增加日志输出排查推理结果前端请求超时首次推理加载模型耗时过长在init里做一次预热推理6.2 性能和显存问题的处理Model.py 里一个容易被忽略的性能瓶颈是图像读取方式。get_image_local_path 传入的如果是远程 URL每次预测都要下载图片那速度会非常慢尤其是标注大量图像时前端会明显卡顿。我的做法是提前在 Label Studio 里把图片同步到本地并把 ML 服务部署在标注服务器同一台机器上这样 get_image_local_path 返回的是本地路径读取基本就是瞬时的事实测下来预标注速度能提升好几倍。如果显存不够可以稍微限制模型输入尺寸。ultralytics 的 predict 支持 imgsz 参数比如self.model.predict(sourceimage, confself.conf_threshold, imgsz960, verboseFalse)遥感旋转目标通常比较小盲目把 imgsz 从 640 改成 320 会掉点明显建议至少保持 640有条件的话用 960 或者 1280。6.3 与 Label Studio 交互失败的处理ML 后端服务跑起来后最常见的联调问题是接口通了但返回格式不匹配。我建议用一个简单的 curl 请求直接测试 predict 接口curl -X POST http://localhost:9090/predict \ -H Content-Type: application/json \ -d {tasks: [{data: {image: /path/to/test.jpg}}]}返回的 JSON 里应该能看到 result 数组里面每个元素带 from_name、to_name、type、value 等字段。如果返回出错服务日志里也会打印具体的异常堆栈。有一点需要注意如果 Label Studio 版本比较新predict 接口的参数可能多了一个 context 字段官方 SDK 的基类已经兼容了这种变化但是如果你重写了 predict 的签名记得不要漏掉 context 参数的占位。6.4 关于训练时数据增强的小建议写 fit 方法时很多人会直接调用 model.train 缺省参数The trouble是默认的 mosaic 增强在旋转框任务上表现并不理想尤其是当数据集中有大量小目标时mosaic 后的目标会被裁剪得七零八落导致模型学到错误的旋转角度。我建议在训练参数里把 mosaic0fliplr0因为水平翻转会改变目标的旋转方向语义比如飞机头朝左和朝右在 OBB 任务中是两种完全不同的标注翻转相当于额外引入了噪声。self.model.train( datastr(yaml_path), epochs50, imgsz640, deviceself.device, mosaic0.0, # 关闭 mosaic避免小目标被切碎 fliplr0.0, # 关闭水平翻转保留旋转方向语义 plotsTrue, # 开启训练曲线绘制方便查看损失 )每次训练完顺手把 runs/detect 目录下的 loss 曲线图存下来标注团队开会讨论模型效果时直接甩这个图比说一百句话都有说服力。7. 最后再分享一点动手经验如果你现在正准备接入 YOLOv8 OBB 到 Label Studio我的建议是不要把时间全花在配置环境上先把 Model.py 里 predict 方法和前面那个坐标转换函数跑通其他功能模块可以慢慢补。我最初花了大半天在调角度转换上后来发现真正影响标注效率的反而是图像读取和坐标归一化这些看起来再基础不过的细节。Label Studio 结合 YOLOv8 OBB核心价值在于把“预标注 → 人工修正 → 模型迭代”的闭环跑起来。预标注模型哪怕只有 60% 的准确率也能把标注员的鼠标点击量减少一半以上。而所谓智能标注其实就是这个 Model.py 文件里几十行代码的事。多花点时间把后处理逻辑做严谨后面整个团队的效率都会受益。本文还有配套的精品资源点击获取