把Label Studio变成OBB半自动标注器:基于YOLOv8的旋转框标注插件实现 简介Label Studio ML 后端中用于 YOLOv8 OBB 目标检测的 Model.py 文件专为需要半自动旋转框标注的算法工程师与数据标注团队设计。该文件实现了 Label Studio 与 YOLOv8 OBB 模型的深度对接能够通过 ML 后端接口自动返回旋转框预测结果在人工复核的基础上大幅减少框选与角度调整操作尤其适合遥感影像、工业质检等大量旋转目标标注场景。压缩包采用 zip 格式仅含 1 个 Python 文件约 2KB核心逻辑集中在 Model.py 中完整涵盖模型加载、图像预处理、推理以及结果格式转换等关键步骤结构清晰、依赖简单便于快速替换或二次开发。目前已有 831 人学习下载适合熟悉 Label Studio 与 YOLO 系列模型的中级开发者参考。这份代码文件既可直接作为 OBB 任务的 ML 后端模板省去从零搭建接口的繁琐工作也可借鉴其请求处理与结果封装思路迁移到 YOLOv8 默认检测或其他旋转框检测模型中是提升标注流水线自动化水平的实用工具。1. 把Label Studio变成OBB半自动标注器的关键Model.py做旋转目标检测的都知道给数据打标签有多耗人。遥感图里的飞机、船舶工业质检里的划痕和凹坑往往细长又带角度用普通矩形框一框就是大块背景负样本。Label Studio本身没带旋转框检测模型但它的ML后端机制能挂任意推理服务。这个项目就是把YOLOv8 OBB塞进Label Studio的半自动标注插件核心是那个Model.py文件。它解决的是格式翻译问题YOLOv8 OBB输出的旋转框是四个顶点坐标Label Studio的polygon标签希望拿到的是0到100的百分比点序列两边坐标基准、单位、点顺序全不一样。Model.py两头打通还负责模型加载、预测回调、在线训练。文件不大但把Label Studio ML后端三条主链路都走了一遍适合想直接上旋转框半自动标注的开发者。2. 项目全貌ML后端的数据流与目录结构2.1 架构设计预测、修正、训练三阶段闭环Label Studio的ML后端可以理解为一个跑在独立服务里的推理引擎通过HTTP与主应用通信。它不需要和Label Studio部署在同一台机器只要网络能通就行。当标注员打开一张图片时Label Studio会向后端注册的URL发送一个POST请求请求体里带着任务数据包括图片地址、当前项目标注配置等。后端收到请求后调用predict方法把模型推理结果填进predictions字段返回。前端拿到预测结果后把旋转框绘制在图片上并默认处于可编辑状态。接下来是人工修正。标注员拖动锚点把框调整到贴合目标点击保存Label Studio会把标注结果作为event为annotation_create或annotation_update的数据再发一个请求给后端的fit端点。fit方法从请求里解析出多边形标注转换成YOLOv8 OBB训练用的txt格式写入训练数据目录然后启动一个短训练流程。训练完后更新self.model下次预测就使用新权重。这个闭环的价值在于模型的错误随着每次人工修正不断被纠正初期可能只需要标注几十张图就能让模型有可用精度。Model.py在这个链路里负责两个方向的坐标转换推理时从模型输出转Label Studio训练时从Label Studio标注转模型输入。方向一旦搞反轻则前端显示乱重则训练崩溃。2.2 目录结构与依赖清单一个可运行的ML后端项目核心是Model.py但周边文件也不能少。下面的目录结构是这个场景下的常见组织方式yolo_obb_backend/ ├── model.py ├── _wsgi.py ├── requirements.txt ├── config.yaml └── scripts/ └── prepare_obb_dataset.pymodel.py定义OBBModel类继承LabelStudioMLBase这是整个项目的核心。_wsgi.py是启动入口里面创建OBBModel实例并启动Flask服务。config.yaml存放模型路径、设备、置信度、训练配置。scripts目录里的prepare_obb_dataset.py是可选的用来把YOLO OBB数据集转换成Label Studio可导入的格式方便做预标注。依赖方面requirements.txt至少需要包含label-studio-ml1.0.8 ultralytics8.0.0 torch2.0 opencv-python4.8.0 PyYAML6.0这里要说明的是label-studio-ml本身并不绑定特定模型框架它只是约定了一套接口。所以这个项目的model.py完全可以拿来改成其他检测模型只需要替换内部实现接口签名保持不变。版本组合上要留意label-studio-ml和ultralytics之间的兼容性目前这套组合在测试环境里是稳定的你按这个锁版本比较省心。2.3 环境变量与参数配置模型路径、设备、置信度这类运行参数通常通过环境变量或命令行传入而不是硬编码。label-studio-ml框架在创建ML后端时会解析这些参数并注入Model.__init__的kwargs。常见做法是import os class OBBModel(LabelStudioMLBase): def __init__(self, model_pathNone, deviceNone, conf_thresholdNone, **kwargs): super().__init__(**kwargs) self.model_path model_path or os.getenv(MODEL_PATH, yolov8n-obb.pt) self.device device or os.getenv(DEVICE, cpu) self.conf_threshold float( conf_threshold or os.getenv(CONF_THRESHOLD, 0.25) )环境变量名要统一。MODEL_PATH指定权重文件路径可以是绝对路径也可以是项目相对路径。DEVICE指定cpu、cuda:0等如果机器只有CPU强制用cuda会初始化失败。CONF_THRESHOLD控制预测置信度值越小框越多半自动标注阶段建议设置低一点比如0.15让模型多给候选人工剔除比漏检容易。super().__init__(**kwargs)这行不能漏它会把from_name、to_name、label_interface等标注配置挂到实例上后边的predict和fit都要用。我曾因为漏了这一行接口报错说找不到属性排查了半天。3. 核心代码拆解Model.py的关键实现3.1 setup方法模型懒加载与资源释放setup是LabelStudioMLBase提供的生命周期钩子在服务启动时被调用。但模型加载不应该放在__init__里因为__init__是在进程启动时执行的如果这时候没有可用的GPU或者权重文件还没准备好服务可能直接挂掉。更好的做法是放在setup中并且加上一个判断避免重复加载。def setup(self): if not hasattr(self, model) or self.model is None: from ultralytics import YOLO self.model YOLO(self.model_path) self.model.to(self.device)为什么不在__init__里加载因为label-studio-ml框架在启动时可能会先实例化模型类来做校验此时并不一定真正需要推理资源。懒加载还能让你在修改配置文件后直接重启服务而不必等模型重新加载。另一个细节是setup方法可能会被重复调用比如某些版本的框架在每次请求前都会检查所以用hasattr或is None来保证只加载一次。3.2 坐标转换从OBB四点坐标到Label Studio多边形这是整个Model.py里最容易翻车的地方。YOLOv8 OBB的推理结果存放在result.obb对象里其中xyxyxyxy这个张量的每一行是四个顶点的像素坐标顺序分别是左上、右上、右下、左下。而Label Studio的多边形类型polygon坐标值是相对图片宽高的百分比范围是0到100且要求点序列是闭合多边形的顶点。所以转换逻辑非常直接def _obb_to_polygon(self, obb_xyxyxyxy, width, height): points [] for i, (x, y) in enumerate(obb_xyxyxyxy): px x / width * 100.0 py y / height * 100.0 points.append([round(px, 2), round(py, 2)]) return points这里的width和height必须是原图的像素尺寸而不是模型推理时缩放后的尺寸。如果图片被letterbox处理过直接拿推理输入尺寸来算坐标就会偏移。解决办法是先用cv2.imread读原始图再用image.shape[1]和image.shape[0]取宽高。另外obb_xyxyxyxy是PyTorch张量需要先调.cpu().numpy()切换到NumPy否则下面做除法时会因为设备问题报错属于典型玄学错误。3.3 predict方法组装Label Studio标准返回结构predict是ML后端最核心的方法。它接收一个任务列表任务里可能是一张图也可能是多张图。我们要遍历每个任务解析出图片路径调用模型推理然后按照Label Studio的预测结果格式组装JSON。一个完整的predict实现大概是这样def predict(self, tasks, **kwargs): results [] for task in tasks: image_url task[data][image] image_path self._resolve_image_path(image_url) image cv2.imread(image_path) height, width image.shape[:2] pred self.model.predict( sourceimage_path, taskobb, confself.conf_threshold, deviceself.device, verboseFalse )[0] if pred.obb is None: results.append({ result: [], model_version: yolov8n-obb }) continue predictions [] for i in range(len(pred.obb.conf)): polygon self._obb_to_polygon( pred.obb.xyxyxyxy[i].cpu().numpy(), width, height ) category_id int(pred.obb.cls[i]) predictions.append({ from_name: self.from_name, to_name: self.to_name, type: polygon, value: { points: polygon, polygonlabels: [self.labels[category_id]], closed: True }, score: float(pred.obb.conf[i]), id: fobb_{i} }) results.append({ result: predictions, model_version: yolov8n-obb }) return results这里有几个字段的取值要特别留意。from_name和to_name必须来自self.from_name和self.to_name这是注册ML后端时框架根据标注配置初始化好的千万不要写死。polygonlabels是Label Studio多边形标签的专用字段值来自self.labels列表这个列表我们在__init__里从label_interface解析。score字段是模型的置信度Label Studio会根据它调整显示透明度score太低的预测框会半透明甚至被忽略。id是为每个框生成的唯一标识后面fit会用到它来做增量修正。3.4 fit方法把人工修正后的标注转成YOLOv8 OBB训练数据fit方法的触发时机是标注员保存标注。Label Studio会把整条标注数据以及对应的任务数据发过来我们需要从中提取出多边形转换成YOLOv8 OBB训练用的txt格式。YOLOv8 OBB的标签格式是每行一个目标类别ID加四个归一化顶点坐标x1 y1 x2 y2 x3 y3 x4 y4注意是归一化的0到1浮点数。下面是一个精简实现的骨架def fit(self, event, data, **kwargs): if event not in (annotation_create, annotation_update): return {} annotation data[annotation] task data[task] image_path self._resolve_image_path(task[data][image]) image cv2.imread(image_path) h, w image.shape[:2] lines [] for result in annotation[result]: if result[type] ! polygon: continue points result[value][points] # 0-100百分比坐标 xs [p[0] / 100.0 for p in points] ys [p[1] / 100.0 for p in points] cls 0 # 单类别固定0多类别要映射 lines.append( f{cls} {xs[0]:.6f} {ys[0]:.6f} f{xs[1]:.6f} {ys[1]:.6f} {xs[2]:.6f} {ys[2]:.6f} f{xs[3]:.6f} {ys[3]:.6f} ) label_file self._save_label_file(image_path, lines) self.model.train( dataself.train_config, epochs5, imgsz640, deviceself.device, batch4 ) self.model YOLO(self.model.train_results.best) return {status: ok}注意几个容易出错的地方。第一事件类型要过滤annotation_create和annotation_update都要处理如果只响应一种保存修改后的标注可能不会触发训练。第二points是0到100的百分比转归一化必须除以100不是除以图片宽高。第三多类别时cls不能永远写0需要通过result[value][polygonlabels][0]找到标签名再查self.label_mapping得到类别ID。第四self.model.train会占用显存如果图片尺寸大、epochs多有可能爆显存所以训练参数要调小。4. 运行与部署本地跑通一个半自动标注流水线4.1 准备Python环境和依赖先把项目目录拉到本地然后创建一个干净的虚拟环境。我习惯用Python 3.10因为Ultralytics对3.10的支持最稳定3.11和3.12也能跑但有些第三方C扩展可能需要重编。环境准备步骤如下python -m venv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txt安装完成后检查torch是否能正常调用python -c import torch; print(torch.__version__)如果没有GPUCPU版也能跑只是速度慢一些不影响流程验证。接下来准备一个YOLOv8 OBB的预训练权重比如yolov8n-obb.pt放到models目录。如果你已经有自己训练过的旋转框模型也可以直接用它替换Model.py本身不关心权重文件来自预训练还是微调只要结构是YOLOv8 OBB即可。4.2 启动ML后端服务启动前先确认config.yaml里的参数。一个示例配置如下model_path: models/yolov8n-obb.pt device: cuda:0 conf_threshold: 0.25 train_yaml: dataset/train.yaml然后运行启动脚本python _wsgi.py默认服务监听在http://localhost:9090。可以在另一个终端验证健康检查curl http://localhost:9090/health如果返回ok说明服务正常。接着再验证预测接口curl -X POST http://localhost:9090/predict \ -H Content-Type: application/json \ -d {tasks: [{data: {image: /path/to/test.jpg}}]}看到返回的预测结果里带result数组就说明模型推理和坐标转换都正常工作。这步一定要做可以提前发现图片路径解析、坐标单位等大部分问题。如果返回的是空result先确认图片路径是否能被后端访问到以及模型是否真的输出了OBB结果。4.3 在Label Studio里注册ML后端并测试半自动标注打开Label Studio进入项目Settings - Machine Learning - Add Model填写ML后端地址。这里有一个注意事项如果Label Studio跑在Docker容器里不能填http://localhost:9090要填宿主机的可达IP比如http://192.168.1.100:9090。填写时密钥可以为空from_name和to_name可以选择自动绑定也可以手动指定。如果手动指定必须保证和你的标注配置一致。注册完成后回到标注界面打开一张图片。你会看到模型预测的旋转框以半透明多边形显示出来。此时可以直接拖拽锚点调整框的位置和角度调整满意后点击保存。保存动作会触发fit请求后台开始训练。第一次训练可能需要几十秒到几分钟取决于你的数据集大小和GPU性能。训练完成后模型权重自动更新再打开下一张图片时就能看到预测精度明显提升。如果前端没有出现预测框优先检查三样东西后端日志里predict是否真的被调用返回的result里from_name是否正确预测框的坐标值是否在0到100之间。这三个环节任何一个出错前端都会静默失败。另外半自动标注阶段建议把conf_threshold调低到0.1或0.15让模型多产生候选框人工修正比重新画框快得多。5. 避坑与常见问题坐标、回调与数据结构5.1 旋转框整体偏移位置对不上现象预测框出现位置偏移但角度正确。原因一般是坐标归一化用了推理尺寸而不是原始尺寸。YOLOv8在推理时会对输入图做letterbox缩放即等比缩放并填充灰色边框模型输出的是填充后图像的坐标。如果你用pred.orig_shape的宽高去转百分比而xyxyxyxy是相对于填充后图像的就会整体偏移。解决必须用原始图片尺寸即task[data][image]指向的文件实际尺寸。在我的实现里是cv2.imread后取shape[1]和shape[0]。另一个可能原因是xyxyxyxy没有转NumPy导致后续除法和round出现张量设备问题这个是玄学常客。5.2 ML后端显示连接失败现象服务已经启动/health也返回ok但Label Studio的模型列表里显示Unavailable。原因通常不是后端问题而是网络和Host解析。如果Label Studio运行在Docker容器容器内的localhost指向容器本身并不是宿主机。解决给后端服务增加一个绑定IP确保监听0.0.0.0并在Label Studio里填http://宿主机IP:9090。另外检查Label Studio所在机器能否访问该IP和端口可以用telnet验证。如果还是不行看后端日志里是否有CORS跨域报错label-studio-ml框架默认开了CORS但如果你自定义了Flask中间件可能会覆盖需要在_wsgi.py里显式加上CORS(app)。5.3 预测接口返回正常但前端不渲染现象curl调用/predict返回了完整JSON但标注界面空白预测框完全看不到。排查时先看浏览器开发者工具的网络请求看ML后端的响应是否真的被标注界面接受到。如果响应正常那问题多半出在结果结构的字段名。from_name必须等于标注配置里多边形标签的名称to_name必须等于图片标签的名称。如果你在代码里写死了label而配置里实际叫abc前端控件匹配不上自然不会绘制。正确做法是像我们前面的代码那样用self.from_name和self.to_name它们是在注册后端时由框架从配置注入的。另外还要检查value.polygonlabels字段是否存在Label Studio要求该字段必须是有非空字符串的数组。5.4 保存标注后fit没反应现象保存标注后后端日志没有任何输出训练也没跑。原因可能是事件名称不对。Label Studio的事件名在不同版本略有差异常见的有annotation_create、annotation_update、annotation_delete如果fit只处理了annotation_create那么修改标注时就不会触发。解决在fit方法开头打印event参数确认实际值。另一种可能是annotation里没有result数组或者全被滤波掉了。我在fit里加入了if not lines: return的守卫避免空列表进入训练器。还有fit接口需要返回一个字典哪怕是空字典如果返回非JSON结构Label Studio前端会报错并停止后续请求。6. 二次开发把Model.py换皮成你自己的检测器如果你不想用YOLOv8 OBB想换成PaddleDetection的旋转框模型或者干脆换成普通矩形框核心改动就是predict和fit这两个方法里的坐标转换。以矩形框为例predict里不再需要_obb_to_polygon改成从results[0].boxes.xyxy取x1,y1,x2,y2转换成rectanglelabels类型value里放[x1_percent, y1_percent, x2_percent, y2_percent]。fit里训练标签也要改成class_id x_center y_center width height并且要把imgsz、task参数从obb改成detect。多类别场景下需要动态生成self.labels和类别ID映射。常见做法是在__init__里遍历self.label_interface把所有多边形标签的别名收集起来构建一个name - index的字典。这样训练数据里的cls才能正确对应。有个小习惯可以帮你验证改动写一个坐标转换的往返测试输入一组四点坐标转成Label Studio格式后再转回YOLO格式断言误差小于0.01。从那以后我每次改坐标转换逻辑都强制走一遍这个测试再也不担心标注界面显示半个框。希望帮到你。本文还有配套的精品资源点击获取