简介面向使用 Label Studio 搭建半自动标注流水线的算法工程师与标注团队这份资源提供 YOLOv8 旋转框OBB目标检测模型接入 Label Studio ML 后端所需的 Model.py 实现文件用于打通模型推理与标注平台之间的交互减少人工逐框绘制的工作量。压缩包内共 1 个文件为单个 py 脚本体积约 2KB属于轻量级后端适配代码可直接放入 Label Studio ML backend 目录中按需修改配置。目前已有 832 人学习下载说明该方向在遥感、航拍等旋转目标标注场景中具有实际需求。读者可从中获得 OBB 模型与 Label Studio 对接的核心逻辑参考理解预测结果如何转换为平台可识别的标注格式并以此为起点调整类别、置信度阈值与模型路径快速搭建自己的半自动标注流程。1. 半自动标注的最后一公里从 Label Studio ML 后端到 YOLOv8 OBB 推理标注几百张遥感图像或工业零件图时最磨人的不是画框而是旋转框那四个角点——手一抖角度偏两度训练出来的 OBB 模型就跟着歪。很多人已经用上了 Label Studio 的机器学习后端做预标注可一到 YOLOv8 的 OBB 任务就卡住后端跑起来了任务却一直 pending或者返回的框全是轴对齐的旋转信息丢了。问题往往不在 Label Studio 本身而在后端那个model.py文件里——它决定了模型怎么加载、预测结果怎么转成 Label Studio 认识的标注格式。这篇就围绕这个文件把 YOLOv8 OBB 半自动标注链路拆开讲清楚从环境怎么装、model.py每个方法干什么到怎么改成支持自己的权重和类别让新手能跑通、熟手能直接上手改。2. 环境与项目结构ML 后端到底需要哪些文件2.1 后端最小目录与依赖清单Label Studio ML 后端不是把整个 Label Studio 塞进模型里而是一个独立的 HTTP 服务Label Studio 通过 webhook 把任务推给它它返回预测结果。所以一个能跑的最小后端核心就是三样东西一个继承LabelStudioMLBase的模型类、一个启动入口、一份依赖声明。围绕 YOLOv8 OBB我一般会组织成下面这个结构yolov8-obb-backend/ ├── model.py # 核心模型类实现 predict / fit ├── _wsgi.py # WSGI 入口供 label-studio-ml start 调用 ├── requirements.txt # 依赖 ├── Dockerfile # 可选容器化部署 └── weights/ └── obb_best.pt # 训练好的 YOLOv8 OBB 权重requirements.txt里至少要钉住这几个label-studio-ml1.0.9 ultralytics8.1.0 torch2.0.0 opencv-python-headless numpy这里有个血泪经验ultralytics和label-studio-ml对numpy的版本要求偶尔会打架尤其是老环境里预装了numpy1.24。我一般会在requirements.txt里显式写numpy1.24避免装完之后ultralytics导入时报_ARRAY_API not found。另外opencv-python-headless比带 GUI 的版本更适合服务器少一堆libGL依赖问题。2.2 启动方式与端口约定后端启动有两种常见姿势。开发阶段直接用命令行label-studio-ml start ./yolov8-obb-backend -p 9090-p 9090是后端监听端口Label Studio 那边填的 ML 后端地址就是http://后端IP:9090。生产环境我倾向用 Docker因为ultralytics会拉 torch镜像大但环境干净FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [label-studio-ml, start, ., -p, 9090]注意label-studio-ml start的路径参数是「包含model.py的目录」不是model.py文件本身。这一点翻车过好几次路径写成文件服务能起来但加载不到模型类日志里只报一个很含糊的No model found。2.3 Label Studio 侧的项目配置后端跑起来只是半边Label Studio 项目里的标注模板必须和 OBB 对得上。OBB 在 Label Studio 里对应的是RectangleLabels加旋转属性模板大致长这样View Image nameimage value$image/ RectangleLabels namelabel toNameimage rotatetrue Label valueship background#FFA39E/ Label valuevehicle background#D4380D/ /RectangleLabels /Viewrotatetrue是关键没有它即便后端返回了带角度的结果前端也不会按旋转框渲染。模板里的Label value必须和后端model.py里维护的类别名一一对应顺序错了不会报错但预标注的类别会整体错位——这种问题排查起来最费时间因为框的位置是对的只是标签全串了。3. model.py 核心拆解一个 OBB 预测请求的完整链路3.1 类初始化权重、类别与设备怎么定model.py的骨架是继承LabelStudioMLBase在__init__里把模型和类别表准备好。下面是我常用的写法逐段说明import os import numpy as np from ultralytics import YOLO from label_studio_ml.model import LabelStudioMLBase class YOLOv8OBBModel(LabelStudioMLBase): def __init__(self, **kwargs): super().__init__(**kwargs) # 权重路径优先取环境变量方便容器里挂载 weight_path os.getenv(OBB_WEIGHTS, weights/obb_best.pt) self.model YOLO(weight_path) # 置信度阈值低于它的预测直接丢弃 self.conf_threshold float(os.getenv(CONF_THRESHOLD, 0.25)) # 从标注模板里解析出类别名保证和前端一致 self.labels self._parse_labels() # 设备有 GPU 用 GPU没有就 CPU self.device cuda if os.getenv(USE_CUDA, 0) 1 else cpu def _parse_labels(self): # labeling_config 是 LabelStudioMLBase 解析好的模板 labels [] for tag in self.labeling_config.get(label_config, ).split(Label ): if value in tag: labels.append(tag.split(value)[1].split()[0]) return labelssuper().__init__(**kwargs)这一步不能省它负责把 Label Studio 传过来的项目配置、模板解析结果挂到self上self.labeling_config就是从这里来的。权重路径用环境变量兜底是为了同一份代码在本地和容器里都能跑不用改代码。_parse_labels是个简化版解析真实项目里如果模板复杂更稳的做法是直接用self.labeling_config里的结构化字段或者干脆把类别表写死在配置里——两种都行关键是后端类别顺序和前端模板顺序必须一致。3.2 predict从图像到 Label Studio 结果的转换predict是整个文件里最核心的方法Label Studio 每来一个任务就调一次。它的输入是任务列表输出是预测结果列表两者一一对应。OBB 的难点在于YOLOv8 返回的是xywhr中心点、宽高、旋转角而 Label Studio 的旋转框要的是左上角加宽高再加旋转角坐标系还不一样。def predict(self, tasks, **kwargs): predictions [] for task in tasks: # 任务里存的是 Label Studio 的图片 URL image_url task[data][image] image_path self.get_local_path(image_url) # 推理conf 控制阈值imgsz 影响小目标召回 results self.model.predict( image_path, confself.conf_threshold, imgsz1024, deviceself.device, verboseFalse, ) result_items [] for r in results: if r.obb is None: continue # xywhr: 中心x, 中心y, 宽, 高, 弧度 xywhr r.obb.xywhr.cpu().numpy() cls_ids r.obb.cls.cpu().numpy().astype(int) confs r.obb.conf.cpu().numpy() for (cx, cy, w, h, angle), cid, conf in zip(xywhr, cls_ids, confs): # 转成左上角坐标Label Studio 以左上角为锚点 x cx - w / 2 y cy - h / 2 # 弧度转角度 rotation float(np.degrees(angle)) result_items.append({ from_name: label, to_name: image, type: rectanglelabels, value: { x: float(x / r.orig_shape[1] * 100), y: float(y / r.orig_shape[0] * 100), width: float(w / r.orig_shape[1] * 100), height: float(h / r.orig_shape[0] * 100), rotation: rotation, rectanglelabels: [self.labels[cid]], }, score: float(conf), }) predictions.append({ result: result_items, model_version: yolov8-obb-v1, }) return predictions几个参数必须说清楚。imgsz1024不是随便写的OBB 任务里小目标多默认 640 容易漏但调太大显存吃不消1024 是个常见折中。conf阈值直接决定预标注的召回和噪声0.25 偏宽松适合「先粗标再人工修」的流程如果想让预标注更干净可以提到 0.4 以上。坐标转换里除以orig_shape再乘 100是因为 Label Studio 的rectanglelabels用的是百分比坐标不是像素——这一步漏了框会全部挤在左上角是很典型的翻车点。rotation用np.degrees把弧度转成角度Label Studio 认的是角度制。3.3 fit 与模型版本什么时候需要微调fit方法在 Label Studio 里对应「用人工修正后的标注重新训练」。对 OBB 后端来说完整实现fit意味着把 Label Studio 导出的标注转成 YOLO 格式再训练链路比较长。多数半自动标注场景其实用不到在线训练我一般留一个占位实现把新权重路径记下来即可def fit(self, event, data, **kwargs): # 半自动标注场景通常不在这里训练 # 只记录事件便于排查 Label Studio 的调用时机 print(f[fit] event{event}, tasks{len(data.get(annotations, []))}) return {model_version: yolov8-obb-v1}如果确实要做闭环微调常见做法是在fit里把data[annotations]落盘成 YOLO OBB 的 txt 格式再起一个后台训练进程训练完替换weights/obb_best.pt并重启后端。这里要注意fit是同步调用的训练耗时长了会拖住 Label Studio 的请求所以训练一定要异步化别在fit里直接model.train()跑几小时。3.4 结果字段对照表后端返回的每个字段都得和 Label Studio 的模板对上下面这张表是我调试时常用的对照后端字段含义常见错误from_name控件名和模板name不一致结果不显示to_name目标对象名写成image之外的值会挂不上type结果类型OBB 必须用rectanglelabelsvalue.x/y左上角百分比坐标用像素值会全挤在左上角value.rotation旋转角度度传弧度会转错方向rectanglelabels类别名列表顺序错位导致标签全串这张表看着简单但每一条我都至少踩过一次。尤其是from_name模板里控件叫label后端写成labels前端就是一片空白日志里也不报错只能靠对照模板一点点查。4. 避坑与排查OBB 后端最常见的五类问题4.1 任务一直 pending后端日志无请求现象是 Label Studio 里点了「预标注」任务状态一直转圈后端日志干干净净。原因通常是 Label Studio 容器访问不到后端地址——如果两者都在 Docker 里localhost:9090指的是 Label Studio 自己不是后端容器。解决办法是把后端地址改成宿主机 IP 或 Docker 网络里的服务名并确认后端端口在docker run时做了-p 9090:9090映射。排查时先在 Label Studio 容器里curl一下后端健康检查接口通不通一目了然。4.2 框位置整体偏移或缩放现象是预标注的框位置对但整体偏了或大小不对。原因基本是坐标换算漏了百分比转换或者用了推理后的 resize 尺寸而不是原图尺寸。r.orig_shape是原图高宽r.orig_shape[1]是宽、[0]是高写反了框会横竖颠倒。解决方式是固定用orig_shape做归一化并在返回前打印一两个框的原始像素值核对。4.3 旋转角度方向反了现象是框的位置对但旋转方向和真实目标镜像。原因是 YOLOv8 OBB 的角度定义和 Label Studio 的rotation方向约定不一致一个是顺时针一个是逆时针。解决办法是在转换时对角度取负或者加 90 度再取模具体看你的数据增强里有没有做翻转。我一般会拿一张有明显斜向目标的图人工标一个框对比后端输出的角度差多少就补多少。4.4 类别名对不上导致标签错位现象是框全对但类别整体串了一位。原因是self.labels的解析顺序和模板里Label的顺序不一致或者模型训练时的类别顺序和模板不同。解决方式是不要依赖字符串解析直接把类别表写成配置项并在启动时打印出来和模板人工核对一遍。这个坑最隐蔽因为结果「看起来是对的」。4.5 显存溢出或推理超时现象是批量预标注跑到一半后端崩了日志里CUDA out of memory。原因是imgsz设太大或批量任务并发太高。解决办法是把imgsz降到 640 或 800 先跑通再按显存往上调同时在 Label Studio 侧限制并发预标注的任务数。CPU 环境下则要注意单张推理可能好几秒超时时间要相应放宽。5. 二次开发换权重、加类别与闭环微调的落地技巧拿到这份model.py之后最常见的改造就三件事换自己的权重、加自己的类别、把人工修正接回训练。换权重最简单把weights/obb_best.pt替换掉或者设OBB_WEIGHTS环境变量指向新路径重启后端即可不用改一行代码。加类别要同时动两处Label Studio 模板里加Label以及确认新模型的类别索引和模板顺序一致——我一般会在__init__里加一段断言启动时就把类别数和模板里的Label数量对一遍对不上直接抛异常比跑到一半发现标签错位强得多。闭环微调是进阶玩法。思路是在fit里把人工修正后的标注导出成 YOLO OBB 格式丢给一个后台训练脚本训练完成后写一个标记文件后端下次启动时检测到标记就热加载新权重。这里有个细节Label Studio 的旋转框导出的是左上角加宽高加角度转 YOLO 的xywhr需要把左上角换回中心点角度还要转回弧度方向和前面的predict正好相反。我习惯把这两个转换函数抽成一对ls_to_yolo和yolo_to_ls放在单独的工具模块里正反两个方向都写单测避免改了一边忘了另一边。验证改造是否成功别只看前端显示。我的习惯是拿一张图先用后端predict直接跑一遍把返回的 JSON 打出来和 Label Studio 里渲染的结果逐字段对再人工修一个框导出看格式对不对。这套流程走下来基本能覆盖坐标、角度、类别三个维度的错误。从那以后我每次改model.py的坐标转换逻辑都强制先跑一遍「单图 JSON 对照 人工修正导出」这两步再进批量预标注省下的返工时间远比这两步多。希望帮到你。本文还有配套的精品资源点击获取