Label Studio集成YOLOv8 OBB模型:旋转框自动预标注实战
2026/9/8 2:38:59 网站建设 项目流程

简介:这份压缩包提供 Label Studio 机器学习后端所需的 Model.py 文件,面向使用 YOLOv8-OBB 模型进行目标检测与方向框标注的开发者,尤其适合正在搭建自动标注流水线、希望摆脱手动绘制旋转框的算法工程师。包内仅含 1 个 Python 脚本文件,大小约 2KB,结构简洁,核心逻辑集中在 Model.py 中,便于直接移植到已有 Label Studio 项目,并按自己的数据路径与模型配置进行修改。借助该文件,可将 YOLOv8 旋转框检测能力接入 Label Studio 半自动标注流程,由模型自动生成 OBB 预标注结果,再结合人工复核即可完成高质量定向框标注,大幅减少人工框选工作量,尤其适合遥感影像、工业缺陷、航拍目标等存在大量旋转目标的场景。资源还配有对应教程链接,可帮助理解后端搭建思路;但主要内容即为该后端脚本本身,用户可以结合自身模型权重与配置文件快速调试部署。目前已有 829 人学习下载,适合正在搭建智能标注平台或希望提升标注效率的算法工程师与研究学习者参考。 做遥感目标检测或者工业质检的朋友应该都有体会:标注旋转框比普通矩形框痛苦得多,每一辆车、每一艘船都要先拉一个框,再一点点转角度,碰到密集场景更是眼睛都要花掉。我之前在Label Studio上做OBB(Oriented Bounding Box,方向包围框)数据的标注,标到一半实在扛不住了,索性把训练好的YOLOV8 objectdetection-OBB模型接进Label Studio的ML后端,用Model.py实现了一个预标注服务。效果是:标注页一点自动标注,模型推出来的旋转框直接铺满整张图,人只需要微调边缘和角度,效率提升非常明显。

这篇文章就把这份Model.py的核心思路、完整实现和调试经验写出来,给准备在Label Studio里接YOLOv8 OBB模型的同学一些参考。

1. 先理清ML后端到底在标注流程里干了什么活

很多人在Label Studio里用过模型预标注,但一提到“ML后端”就有点懵,以为要改Label Studio源码。其实不是,ML后端本质上是独立运行的一个HTTP推理服务,只是它的输入输出格式要符合Label Studio的协议。

1.1 一个HTTP服务帮你把模型接入标注界面

Label Studio的ML后端是一个独立服务,通过一组固定接口和标注平台通信。核心接口是/predict:标注员在界面上点击“自动标注”或者“Model”按钮时,Label Studio会把当前任务的图片数据作为请求发到这个接口,接口返回标准的标注JSON,前端直接把它渲染成矩形框、旋转框、多边形这些标注结果。

还有/setup接口,Label Studio在初始化ML后端时会调用它,把当前项目的标注配置(label_config)传过来,这样后端就能知道这个项目里有哪些标签、标注控件叫什么名字、对应的是图像还是文本。

这和平时我们自己写推理服务不一样。普通推理服务返回的是检测框坐标、置信度就完事了,ML后端得把推理结果翻译成Label Studio的标注协议,比如坐标要用百分比而不是像素值、框的类型要叫rectanglelabels、旋转框要带上rotation。这也是Model.py里最需要花心思的地方。

1.2 标准ML后端项目里我们只需要改Model.py

Label Studio官方提供了一个label-studio-ml-backend项目模板,目录结构大概是这样的:

my_backend/ ├── model.py ├── _wsgi.py ├── requirements.txt ├── Dockerfile └── label_config.xml

官方模板的逻辑很成熟,_wsgi.py负责把model.py封装成Flask应用,启动服务、接收请求这些事它都处理了。但要注意,OpenAI和HuggingFace等在图像、文本、音频等不同模态下的ML后端均基于该模板设计,模型逻辑则完全集中在model.py。也就是说,你要做的核心工作其实很简单:继承LabelStudioMLBase这个基类,实现初始化方法和predict方法,把模型推理结果转换成Label Studio的标注格式。

Model.py的执行流程可以拆成三步:

  1. __init__里加载模型,解析Label Studio传来的标注配置。
  2. predict接收任务列表,逐个取出图片路径或URL。
  3. 推理得到检测框,转换成标注协议要求的JSON,返回。

理解了这个架构,后面写代码就很顺了。

2. OBB旋转框的协议细节:矩形框多了一个rotation字段

OBB之所以比普通检测麻烦,是因为每个框除了位置和大小,还要带一个角度。对接的时候要特别注意Label Studio和YOLOv8对“旋转框”的描述方式不一样,这个转换也是Model.py最容易出错的地方。

2.1 Label Studio端如何描述一个旋转框

在Label Studio里,旋转框依然使用RectangleLabels类型的标注控件,界面上按住Shift拖动可以旋转框。它返回的每个result大致长这样:

{ "id": "pred_0", "type": "rectanglelabels", "value": { "x": 12.5, "y": 20.0, "width": 35.0, "height": 18.0, "rotation": 45.0, "rectanglelabels": ["vehicle"] }, "original_width": 1920, "original_height": 1080, "score": 0.92 }

几个关键点:

  • xywidthheight都是以原图宽高为基准的百分比数值,范围0~100,不是像素。
  • xy是旋转框外接轴对齐矩形的左上角坐标,通俗说就是不考虑旋转时那个水平矩形的左上角。
  • rotation是旋转角度,单位是度,表示框绕中心点旋转的角度。
  • original_widthoriginal_height是原图尺寸,Label Studio前端用它把百分比换算回像素。

这里有一个容易理解错的地方:Label Studio的xy不是旋转后矩形的真实角点坐标,而是旋转前水平外接矩形的左上角。所以我们在Model.py里做转换时,不能直接把YOLOv8 OBB输出的四个角点拿过来,需要先把中心点坐标转换成“外接矩形左上角+宽高”的形式。

2.2 YOLOv8 OBB输出坐标系与标注坐标系的换算

YOLOv8 OBB模型的预测结果,通过results[0].obb可以拿到一个OBB对象,里面的xywhr是核心数据。

boxes = results[0].obb xywhr = boxes.xywhr.cpu().numpy()

xywhr的每一行是[cx, cy, w, h, angle],含义如下:

  • cxcy:旋转框中心点的像素坐标(默认是相对于原图尺寸)。
  • wh:旋转框的宽和高,单位是像素。
  • angle:旋转角度,单位是弧度,范围一般是[-pi/2, 0),负号表示顺时针旋转。

而Label Studio需要的是百分比坐标和逆时针角度,所以Model.py里做了这几步换算:

import math # 中心点坐标换算成外接矩形的左上角,并转为百分比 x_percent = (cx - w / 2) / img_width * 100 y_percent = (cy - h / 2) / img_height * 100 w_percent = w / img_width * 100 h_percent = h / img_height * 100 # 弧度转角度,取负号让方向与Label Studio的逆时针正方向一致 rotation_deg = -math.degrees(angle)

关于角度方向,这是我踩过坑的地方。YOLOv8 OBB的angle为负数,在图像坐标系里通常表示顺时针旋转,而Label Studio的rotation默认按逆时针为正,所以转换时要取负号。但这个细节在不同版本的Label Studio里表现可能有差异,第一次对接完成后一定要用一张有明显方向的测试图验证,框转反了就把负号去掉,差90度就加减90。

坐标换算还有一个容易忽略的点:如果直接给model.predict传原始尺寸的numpy数组或PIL Image,YOLOv8内部做letterbox之后,会通过后处理把检测框坐标还原到原图坐标系,所以xywhr里的坐标直接就是原图像素。如果你手动对图片做了resize或者预处理,那么返回的坐标就可能是resize之后的,这时候必须自己做逆变换,否则框会错位。建议尽量让YOLO自己处理输入,不要手动干预。

3. Model.py从拿到图片到吐出标注结果的完整链路

前面把协议细节理清了,现在看具体的代码实现。这里给出一个可以在实际项目中改改就能用的Model.py,基于Ultralytics YOLOv8 OBB模型。

3.1 初始化:模型加载与标签配置解析

这一节写__init__方法。目标是:模型只加载一次,不要让每次请求都重复加载;同时要从Label Studio传来的标注配置里,解析出标注控件名、对应的图片字段名、以及这个项目里的标签列表。

import math import os from label_studio_ml.model import LabelStudioMLBase from label_studio_ml.response import ModelResponse from label_studio_ml.utils import get_single_tag_keys, get_local_path from ultralytics import YOLO class OBBYoloModel(LabelStudioMLBase): def __init__(self, **kwargs): super().__init__(**kwargs) # 模型路径通过环境变量配置,方便部署时切换模型 model_path = os.getenv("OBB_MODEL_PATH", "models/best_obb.pt") self.model = YOLO(model_path) # 解析Label Studio项目里的标注配置 # "RectangleLabels"是标注控件类型,"Image"是对象类型 self.from_name, self.to_name, self.value, self.labels = get_single_tag_keys( self.parsed_label_config, "RectangleLabels", "Image" )

get_single_tag_keys这个工具函数很省事,它会从parsed_label_config里找出第一个匹配类型RectangleLabels的标注控件名,并返回图片字段名(比如image)和该控件下配置的标签列表。如果你的项目里有多个标注控件,要确保OBB对应的RectangleLabels放在配置里正确的位置,或者手动指定from_name,避免解析到别的控件。

模型路径推荐用环境变量控制,而不是写死。因为训练环境、部署环境、标注服务器的文件布局通常不一样,用OBB_MODEL_PATH方便随时切换,不用改代码。

3.2 预测流程:读图、推理、拼接result

predict方法接收tasks列表,每个task对应标注界面里的一张图片。我们要做的是:取出图片,调用模型推理,然后把检测结果逐条转换成Label Studio的标准result格式。

def predict(self, tasks, **kwargs) -> ModelResponse: predictions = [] for task in tasks: # 1. 获取图片路径,支持本地路径和HTTP URL image_url = task["data"].get(self.value) if not image_url: predictions.append({"result": [], "score": 0.0}) continue image_path = get_local_path(image_url, task_id=task.get("id")) image = Image.open(image_path).convert("RGB") img_width, img_height = image.size # 2. 模型推理 results = self.model.predict(source=image, conf=0.25, verbose=False) obb = results[0].obb items = [] if obb is not None: xywhr = obb.xywhr.cpu().numpy() class_ids = obb.cls.cpu().numpy().astype(int) scores = obb.conf.cpu().numpy() for i in range(len(xywhr)): cx, cy, w, h, angle = xywhr[i] label_name = self.model.names[class_ids[i]] # 3. 坐标转换:像素 -> 百分比,弧度 -> 角度 x = (cx - w / 2) / img_width * 100 y = (cy - h / 2) / img_height * 100 width = w / img_width * 100 height = h / img_height * 100 rotation = -math.degrees(angle) items.append({ "id": f"obb_pred_{i}", "type": "rectanglelabels", "value": { "x": round(x, 2), "y": round(y, 2), "width": round(width, 2), "height": round(height, 2), "rotation": round(rotation, 2), "rectanglelabels": [label_name], }, "original_width": img_width, "original_height": img_height, "score": round(float(scores[i]), 4), }) predictions.append({ "result": items, "score": 0.0, "model_version": "yolov8-obb-v1", }) return ModelResponse(predictions=predictions)

代码里几个细节值得说明:

  • get_local_path是官方工具函数,处理了URL下载、本地路径、缓存等多种情况,不要自己写读图的逻辑,容易漏掉边界情况。
  • results[0].obb在检测不到目标时可能为None,必须加判断,否则直接取属性就报错。
  • self.model.names是从模型文件里带过来的类别名字典,通过class_ids[i]拿到具体的类名,比如vehicleship
  • 如果Label Studio项目里的标签列表和模型训练的类别名不完全一致,需要在拼接rectanglelabels之前做一次映射。最简单的方式是对照两个列表建立字典,比如LABEL_MAP = {"ship": "boat"},再取映射后的名字。

这里有一个非常常见的问题:模型训练时的类别顺序和Label Studio项目里配置的标签顺序往往不同,如果直接用索引当类别名,就会出现“模型明明检测到船,界面上却标成了车”的情况。我建议在初始化时把self.model.names打印出来,和Label Studio项目里的标签列表核对一遍,再写映射关系。

3.3 一张图对应的返回JSON长什么样

调试或者写文档的时候,有一个具体的返回示例会方便很多。假设一张1920x1080的遥感图,模型检出了一个中心点在(600, 800)、宽300、高150、角度-0.6弧度的“ship”,返回的JSON大致是:

{ "result": [ { "id": "obb_pred_0", "type": "rectanglelabels", "value": { "x": 23.44, "y": 67.59, "width": 15.62, "height": 6.94, "rotation": 34.38, "rectanglelabels": ["ship"] }, "original_width": 1920, "original_height": 1080, "score": 0.91 } ], "score": 0.0, "model_version": "yolov8-obb-v1" }

拿到这个JSON后,前端会自动在图片上渲染出带角度的旋转框。如果rotation方向不对,标注员在界面上看到的框和目标就有明显夹角,这也是检查角度逻辑最直观的办法。

4. 本地调试技巧与常见翻车点

写完Model.py不要直接部署到服务器,本地先跑一遍,能省下大量来回调试的时间。我调试时吃过的几个亏,这里一并列出来。

4.1 不启动Web服务,直接用脚本测predict

ML后端本质上是个HTTP服务,但调试时不用每次都起服务、发HTTP请求。LabelStudioMLBase可以直接实例化,手动构造一个task传进去,调用predict就能看到返回结果。

model.py里的类保存好,写一个简单的调试脚本:

import json # 注意:这里的label_config要与Label Studio项目里的配置保持一致 label_config = """ <View> <Image name="image" value="$image"/> <RectangleLabels name="label" toName="image"> <Label value="ship" background="#FF0000"/> <Label value="vehicle" background="#00FF00"/> </RectangleLabels> </View> """ task = { "id": 1, "data": {"image": "/data/remote_sensing/test_001.jpg"} } model = OBBYoloModel(label_config=label_config) resp = model.predict([task]) print(json.dumps(resp.dict(), indent=2, ensure_ascii=False))

这里的核心是:label_config字符串必须和Label Studio项目里的XML配置一致,否则解析出来的标签列表对不上,预测结果可能被过滤掉或者显示成错误的类名。

还有一个更直观的验证方法:把模型预测得到的xywhr画到原图上,直接肉眼对比旋转框是否贴合目标。这样能快速发现角度符号和坐标系转换的问题。让YOLO直接预测得到OBB对象,再用results[0].plot()就能得到可视化结果图。

4.2 常见问题排查与实测经验

我在对接过程中遇到过的几类问题,整理成了一张排查表,基本覆盖了大多数翻车场景:

现象可能原因解决方案
预测框位置偏到角落或整体偏移使用了resize后的图像坐标,没做letterbox逆变换直接传原图给model.predict,不要在外部预处理
框大小对但旋转方向反了rotation符号错误去掉代码里的负号,或加90/减90验证
类别名显示错乱训练类别顺序和Label Studio标签顺序不一致打印model.names,建立类别映射字典
完全没有预测结果OBB检测框为None,或置信度阈值太高检查obb is not None判断,调低conf
图片URL无法访问ML后端和Label Studio不在同一网络环境确认图片URL端口开放,或使用共享存储路径
界面渲染卡顿单张图预测任务太大、推理时间过长调低图像推理尺寸,限制批量并发数

有一个经验值得单独提出来分享:旋转框角度方向的问题,一定要用单张带方向特征的测试图验证。我第一次部署时没有做这个验证,直接标注了一批图像,结果所有框虽然大小位置都对,但角度全都反了,等于重新标了一遍,非常惨。后来我把验证步骤固定成:先找一张有明显方向的测试图,跑完Model.py后把结果JSON的可视化效果截图留档,每次换模型或改代码都要检查一次。

5. 部署上线与并发场景的性能取舍

模型在本地跑通只是第一步,真正部署到标注团队能用的环境,还有几个事情要处理好,尤其是并发标注场景下的稳定性和推理速度。

5.1 依赖清单与启动命令

Model.py的依赖不多,核心是这几个:

label-studio-ml>=1.0.0 ultralytics>=8.1.0 opencv-python Pillow torch torchvision

安装完依赖后,启动服务很简单。官方模板通常会生成一个_wsgi.py,可以直接用uvicorn或gunicorn启动:

uvicorn _wsgi:app --host 0.0.0.0 --port 9090

然后在Label Studio的管理后台,Settings页面的Machine Learning选项卡里添加ML后端,填这个服务的地址,比如http://192.168.1.20:9090。添加完成后,Label Studio会请求/setup接口,如果一切正常,状态会变成绿色“Connected”。

这里有一个容易踩的坑:Label Studio项目和ML后端如果不在同一台机器上,图片URL的地址可能是http://localhost:8080/...,ML后端拿到这个URL去下载图片时,localhost指的就是ML后端自己,自然访问不到。解决办法是让两者在同一台机器上,或者把Label Studio的访问地址配置成局域网可访问的IP。

5.2 并发标注时别让Model.py成为瓶颈

团队里多人同时标注时,ML后端会收到并发的/predict请求。YOLO模型在推理时内部会有并发安全的考量,但实测下来,多线程同时调用同一个模型实例仍然可能出现奇怪的问题,表现是偶尔报错、显存溢出,或者几个请求挤在一起导致响应变慢。

我的做法是加一个全局锁,保证同时只有一个请求在推理:

import threading _infer_lock = threading.Lock() def predict(self, tasks, **kwargs) -> ModelResponse: with _infer_lock: # 这里的推理逻辑与上面一致 ...

加锁之后并发请求会排队,吞吐量不会无限上涨,但稳定性明显变好。如果你的标注团队规模大、并发要求高,更好的方案是把ML后端部署成多个副本,再在Label Studio侧做简单的负载均衡。不过大多数标注场景下,一个GPU卡跑几个人的预标注请求完全够用。

GPU环境检查也别忽略。我遇到过装了ultralyticstorch的设备上,推理用的还是CPU,速度慢到难以接受。部署完以后先跑一行命令确认:

import torch print(torch.cuda.is_available())

如果输出False,大概率是PyTorch版本和CUDA版本不匹配,需要重装对应gpu版的torch。模型默认会在首次推理时自动选择设备,但显式指定设备更稳妥:

device = "cuda:0" if torch.cuda.is_available() else "cpu" self.model = YOLO(model_path).to(device)

模型版本号model_version这个字段,很多人会忽略,但它在真实标注流程里非常有用。每次模型更新后,改动这个版本号,标注平台会自动记录哪些标注是由哪个版本的模型预标注的,后期做数据质量分析和模型迭代对比时,这个信息价值很大。

最后再分享一个小技巧:预标注返回的score字段不只是展示给标注员看的信心值,它还可以用来做数据筛选和后处理。比如在Model.py里把置信度低于0.3的框直接过滤掉,减少标注员要微调的低质量框;或者把高置信度框的边框颜色和不置信的区分开,标注员一眼就知道哪些需要重点检查。实际用下来,这一条对标注效率的提升作用并不比接入模型本身小。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询