资讯动态

Label-Studio ML后端接入YOLOv8 OBB:Model.py半自动标注实践

发布时间:2026/10/17 10:52:04 来源:尧图企业网站定制
简介这是针对 Label Studio 半自动标注场景提供的 YOLOv8 检测模型OBB 旋转框后端 Model.py 文件面向希望提升标注效率的算法工程师、数据标注人员及 Label Studio 二次开发使用者。该 Python 文件定义了 ML 后端与 Label Studio 的接口逻辑可加载训练好的 YOLOv8-OBB 权重将模型推理结果自动回传为标注建议辅助完成旋转目标的高效预标注。配套博文详细说明了接入步骤、环境配置与常见问题便于快速落地。资源包为 zip 压缩包共 1 个文件类型为 py 脚本大小约 2KB结构精简适合直接放入自定义 ML 后端目录中按教程改造使用。目前已有 831 人浏览/学习适用于已有一定 YOLO 使用基础、需要把检测能力接入 Label Studio 流程的开发者。获取这份 Model.py 后可以省去从零编写后端回调与标签解析逻辑的时间并结合实际标注项目微调返回格式从而快速搭建起半自动旋转框标注链路。1. Label-Studio ML 后端接入 YOLOv8 OBBModel.py 是半自动标注的枢纽半自动标注听起来很美标注几张、模型自动预标下一批、人来修正。但在 Label-Studio 里真正跑通这一套瓶颈往往不是模型权重而是那层把 Label-Studio 请求翻译成 YOLOv8 OBB 推理结果的胶水代码也就是 ML 后端目录下的 Model.py。它要负责接收前端传来的图片任务、调用旋转目标检测模型、把坐标系里的角度和边长换算回界面能渲染的多边形结果。对做遥感、文档检测或倾斜工件质检的标注项目来说这是最值得复制和改造的一个文件。适合已具备基础 Python 技能的开发者下面先从 Label-Studio ML 后端的请求机制讲起。2. ML 后端到底怎么工作请求链与 Model.py 必须撑起的四个方法2.1 一个常见误解Model.py 不是普通推理脚本很多第一次接触 Label-Studio ML 后端的人以为是把一个 YOLO 推理脚本丢进后端目录就行。实际上 Label-Studio 通过独立进程管理 ML 服务Model.py 里的类需要继承 label-studio-ml SDK 提供的 BaseModel。BaseModel 内部已经实现了 HTTP 路由、任务队列和健康检查逻辑你需要做的只是重写几个方法让框架知道怎么把你的模型包进去。最常涉及四个方法setup()在服务启动时被调用一次适合做权重加载、类名注册这类重操作predict()每次前端发来标注或自动标注请求时都会触发是真正的推理入口healthcheck()返回服务状态前端用它判断 ML 服务是否在线get_headers()则可以告知前端支持的结果类型通常不必动。若把权重加载写在 predict 里每次标注都会重新读一次模型排查时第一时间应该确认启动阶段有没有报错。OBB 任务对setup()尤其敏感。旋转目标检测的权重文件通常比普通检测模型更大加载一次动辄几秒加上 PyTorch 的 CUDA 上下文初始化如果每个工作进程都重复 setup 一次显存会快速被打满。我的习惯是给 setup 加适量日志打印权重路径、设备信息和模型类别数量方便后面排查多进程导致的问题。2.2 请求体里藏着什么tasks、data 与 image 字段想要写好 predict必须先知道 Label-Studio 给你发了什么。请求到达 ML 后端时主体是一个 JSON结构大致是{tasks: [...], model: {...}}。tasks 是列表每个元素对应一个待标注任务。常用的字段是task[id]和task[data][image]前者用来在日志里追踪某个任务后者是图片地址。这个地址可能是公网 URL也可能是 Label-Studio 在你启动 ML 后端时配置的本地缓存路径。比较稳妥的方式是先打印一遍原始请求再动手写逻辑。第一版 Model.py 里建议加一行logger.info(task %s, json.dumps(task, ensure_asciiFalse))。我最初做这个项目时就是靠这一步才发现图片来源不是标准 URL而是带签名的本地文件路径直接导致用 urllib 下载时一直报 404。处理 image 字段时要兼容 URL 和本地绝对路径两种情况URL 用 urllib 下载到临时目录本地路径用cv2.imread直接读取。import os import json import uuid import urllib.request import cv2 def load_image_from_task(task, cache_dir./cache): image_ref task[data][image] if image_ref.startswith(http://) or image_ref.startswith(https://): os.makedirs(cache_dir, exist_okTrue) ext os.path.splitext(image_ref.split(?)[0])[1] or .jpg local_path os.path.join(cache_dir, f{uuid.uuid4().hex}{ext}) urllib.request.urlretrieve(image_ref, local_path) image cv2.imread(local_path) return image, local_path return cv2.imread(image_ref), image_ref这段代码解决两个问题统一图片来源、避免反复下载同一张图。参数说明里 cache_dir 建议用单独目录因为 OBB 数据集原始影像往往很大下载到内存再转换容易占用过多资源uuid 文件名防止多任务并发时互相覆盖。如果本地路径返回 None多半是中文路径或权限问题后面章节会细说。另外context参数在复杂工作流里可能携带项目配置或用户信息第一版不用管但要保证入参存在否则某些版本 SDK 调用时会因缺少参数直接报错。2.3 最小 Model.py先让链路通起来再谈坐标我强烈建议第一版 Model.py 不要写任何坐标换算逻辑先让服务启动、请求能进来、返回一个合法的空结果。这样可以把整个链路拆成两段排查前端到后端通不通模型输出到界面通不通。下面这个骨架完全可以跑但只返回空 result。# Model.py 最小骨架 import logging from label_studio_ml.model import BaseModel from label_studio_ml.response import ModelResponse from ultralytics import YOLO logger logging.getLogger(__name__) class OBBModel(BaseModel): def setup(self): # setup 在服务进程启动时执行一次加载大权重最合适 self.engine YOLO(./best_obb.pt) logger.info(OBB model loaded, classes: %s, self.engine.names) def predict(self, tasks, contextNone, **kwargs): results [] for task in tasks: logger.info(get task: %s, task.get(id)) results.append({ result: [], # 空的合法结果前端不会报错 model: yolov8-obb, # 前端展示模型来源 }) return ModelResponse(resultsresults) def healthcheck(self): return {status: UP}运行方式有两种开发时直接label-studio-ml start ./backend -p 9090生产部署时可交给 gunicorn 托管。不同版本 CLI 参数可能有差异以--help为准。启动后访问http://localhost:9090/health返回{status: UP}就说明服务正常。注意 setup 里的权重路径建议用环境变量或配置项不要硬编码否则换模型要改代码。拿到空结果后你可以在 Label-Studio 任意一张图上点自动标注如果能正常返回虽然没框说明请求链已经通了后面再填坐标换算就不会把问题混在一起。3. 把 YOLOv8 输出翻译成 Label-Studio 结果坐标换算与标签映射3.1 推理入口和输出对象别用检测思路处理 OBB在 Model.py 里写推理常见做法是把图片转成 RGB numpy 数组后传给self.engine.predict(image, conf0.25, verboseFalse)。对 OBB 模型返回对象不是ret[0].boxes而是ret[0].obb。obb对象有xywhr和xyxyxyxy两种常用属性前者是(cx, cy, w, h, angle)的旋转框中心表达角度单位是弧度后者是四个角点坐标顺序按旋转方向排列。两者都可以用但我的建议是优先用xyxyxyxy。理由很直接Label-Studio 的渲染层并不认识中心宽高角度这种表达它需要一个可以画出来的多边形点集。虽然你完全可以自己把 xywhr 转换成角点这也会在下面给出但由于 ultralytics 在 predict 内部已经默认把坐标还原到原图尺度直接取角点能少踩一次坐标系换算的坑。另外conf参数要放在 predict 调用里而不是在导出模型时固化半自动标注需要频繁调节阈值来找召回和精度的平衡点。# 假设 image 是已经转成 RGB 的 ndarray pred self.engine.predict(image, conf0.1, verboseFalse)[0] if pred.obb is None: return [] # 四点角点坐标shape 为 (N, 4, 2)基于原图像素 corners pred.obb.xyxyxyxy.cpu().numpy() scores pred.obb.conf.cpu().numpy() cls_ids pred.obb.cls.cpu().numpy().astype(int)注意输入图像self.engine.predict对 ndarray 输入会用模型内部预处理但如果你从 cv2 读图得到的是 BGR务必先用cv2.cvtColor(image, cv2.COLOR_BGR2RGB)转换否则预测结果会明显偏差。pred.obb为 None 通常意味着目标数低于阈值不代表模型没有输出可以先调低 conf 验证。3.2 当必须自己算四点时xywhr 到多边形顶点的换算有些场景下你会想绕过 ultralytics 的自带还原比如做多尺度推理、TTA 或者在导出的 TorchScript 里推理。这时就得拿 xywhr 自己算。公式不复杂先把局部坐标系的四个角点用旋转矩阵转一下再加上中心点坐标。角度正方向按数学约定逆时针但不同训练框架输出的角度定义可能不太一样所以换算后建议拿一张已知方向的测试图对比而不是盲信数学推导。import math def xywhr_to_polygon(xywhr): 把 (cx, cy, w, h, angle_rad) 转为 4 个角点坐标 cx, cy, w, h, angle xywhr cos_a, sin_a math.cos(angle), math.sin(angle) half_w, half_h w / 2.0, h / 2.0 local [ (-half_w, -half_h), (half_w, -half_h), (half_w, half_h), (-half_w, half_h), ] pts [] for dx, dy in local: x cx dx * cos_a - dy * sin_a y cy dx * sin_a dy * cos_a pts.append([round(float(x), 1), round(float(y), 1)]) return pts用这份代码前先核对两件事xywhr 里的坐标是否已经是原图尺度角度是否为弧度。曾经我把某个库导出的度数值直接丢进来结果所有框正好转了 90 度回看源码才发现那批权重输出的角度单位是度。换算完成后还需要把标签信息拼成 Label-Studio 的 result 条目下面来说明字段结构。3.3 一条 result 里缺一不可的字段from_name、type 与 valueLabel-Studio 后端返回的 result 是一个列表每项代表一个标注实体的渲染信息。以旋转框为例最稳妥的 type 是polygon因为rectangle没法定向表示角度。一个合法 result 的结构如下{ id: abc123, from_name: label, to_name: image, type: polygon, value: { points: [[x1, y1], [x2, y2], [x3, y3], [x4, y4]], closed: true }, score: 0.82 }from_name和to_name必须匹配你在 Label-Studio 项目里配置的标注控件 id。我一般建议在项目设置里把多边形标签控件命名为label图像控件命名为imageModel.py 里写死这两个值。否则会出现后端返回成功、前端却不显示框的怪问题。score字段是可选但推荐加的半自动标注工作流的排序和过滤全靠它。def build_results(corners, scores, cls_ids, names, score_threshold0.25): res [] for idx in range(len(scores)): if scores[idx] score_threshold: continue label_name names[int(cls_ids[idx])] res.append({ from_name: label, to_name: image, type: polygon, value: { points: [[float(x), float(y)] for x, y in corners[idx]], closed: True }, score: float(scores[idx]) }) return res这里的 names 来自self.engine.names它是模型 class 索引到字符串类名的映射。若界面标签名和模型类名不完全一致就要在函数外面显式维护一张映射表把names[cls_id]映射为界面 value。不要直接把模型类名塞给前端因为 Label-Studio 的标签配置里可能叫 plane模型里叫 airplane对不上就一条框都渲染不出来。4. 实战排查四个把我卡住数小时的 OBB 标注坑4.1 预测总是返回空列表但模型单独跑有输出现象Model.py 接入后自动标注没有任何框日志里也没报错。原因排查先看pred.obb是否为 None。若是说明当前阈值下目标全部被过滤或者输入图像颜色通道反了。YOLOv8 权重在训练时普遍使用 RGB 输入若你从 OpenCV 读图直接传BGR 输入会让特征错位导致置信度整体偏低甚至无输出。解决image cv2.imread(local_path) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 关键 pred self.engine.predict(image, conf0.05, verboseFalse)[0]另外先调低 conf 再排查其他逻辑。把conf0.05临时写进推理如果出现大量低分框说明模型正常问题在阈值或过滤逻辑如果依然没有框就要检查权重是否真的是 OBB 模型因为用普通检测权重调用 obb 属性多半拿不到有效结果。4.2 旋转框在界面上歪了方向整体偏转现象结果有框位置也对但旋转方向明显错了比如正方形框变成菱形长边指向错误方向。原因最常见的是角度单位混用。OBB 权重导出的角度可能是弧度、度甚至是象限角三种表达而我们在 3.2 节换算公式里默认弧度。如果模型输出的 angle 是度直接套公式就会整体偏转。其次是对图像做 resize 后没有还原坐标比如预测前手动缩放了图片但 xywhr 里的坐标仍按缩放后尺寸算。解决先打印一条xywhr的原始值和换算后的四点坐标手工在图上描点验证再确认输入推理的图是否是原图尺寸。调试阶段写一个独立脚本用 matplotlib 画框而不是依赖 Label-Studio 渲染。# 调试脚本片段直接打印原始角度与换算后的点 sample objs[0] print(原始 xywhr:, sample) poly xywhr_to_polygon(sample) print(四点坐标:, poly)这样做的好处是把前端问题排除在外。如果调试脚本画出来是对的就去查 result 报文如果脚本画出来已是歪的问题一定出在坐标换算或预处理。我在这个坑里耗过一整个下午最后发现是角度单位问题从那以后我拿到新权重第一件事就是打印一条 xywhr 判断单位。4.3 服务启动后卡死或显存暴涨多进程模型加载的经典死局现象ML 后端启动后GPU 显存迅速被占满Label-Studio 界面的自动标注按钮转圈很久才返回甚至直接超时。原因ML 后端常通过 gunicorn 启动多 worker。每个 worker 进程都会独立执行一次setup()也就是每个进程加载一份权重并初始化一次 CUDA 上下文N 个 worker 就是 N 份显存占用部分 worker 初始化顺序不对还会互相等待。解决启动时限制 worker 数量为 1并确认 worker 类型为gthread。更稳的做法是在setup()里加全局标记让每个进程只加载一次但 PyTorch 推理时仍会各自建上下文所以限制 worker 最直接。命令行参数一般写成-w 1或者通过 gunicorn 配置传入。显存只要还没溢出可以先从 worker 数上找原因。若是多卡机器还要注意CUDA_VISIBLE_DEVICES是否把所有 worker 都指到同一张卡上。4.4 后端返回 JSON 正常但前端怎么都不显示框现象验证脚本请求 /predict 能看到完整 result进入 Label-Studio 界面点自动标注画布上没有任何变化控制台报错提示类型不对或字段缺失。原因Label-Studio 前端按照标注配置里的控件 id 去找 result 里的from_name和to_name。如果你项目里的多边形标签不叫 label而是叫 polygon_labels那后端返回的 label 就匹配不上前端直接丢弃这段结果。有些版本还会严格要求type必须是配置中控件对应的类型你返回rectangle但配置是polygon也会被忽略。解决打开项目设置里的标注配置把所有控件 id 抄出来逐一填到 Model.py 的 result 构造中。配置形如View Image nameimage value$image/ PolygonLabels namelabel toNameimage Label valueplane/ Label valueship/ /PolygonLabels /View那么from_name必须是labelto_name必须是image。多对一场景下尤其容易踩图像控件 name 是 image标签控件 name 是 label两者不能颠倒。最后用一条固定的假 result 数据在界面上验证能显示后再接真实推理能省掉一大半排查时间。5. 把半自动标注真正用起来先模拟请求再让模型当标注助手5.1 不打开界面就能验证的本地脚本每改一次 Model.py 都去 Label-Studio 界面点自动标注来回切换非常低效。我通常会写一段独立的验证脚本直接向 ML 后端发一个伪造的 predict 请求输出结构像 2.3 的骨架那样打印到终端。这样能看到完整 JSON、状态码、推理耗时。# verify_predict.py import json import time import requests payload { tasks: [ { id: 1, data: {image: /absolute/path/to/test_obb.jpg} } ], model: {} } t0 time.time() resp requests.post(http://localhost:9090/predict, jsonpayload, timeout60) print(状态码:, resp.status_code) print(耗时:, round(time.time() - t0, 2), s) print(json.dumps(resp.json(), ensure_asciiFalse, indent2)[:2000])这段脚本的关键参数是 timeout 和后面的截断打印OBB 推理加权重加载可能超过默认的 10 秒超时容易被误判为服务异常输出截断则是避免大任务打爆终端。把脚本放在项目根目录配合一条python verify_predict.py即可。如果返回里能看到至少一个 polygon 且坐标在图片宽高范围内再进界面做人工确认。5.2 先让模型当助手而不是让模型当答案半自动标注要跑得顺最重要的不是把置信度阈值调得很低、追求框多而是把工作流设计成“模型给初稿、人工改差异”。我现在的做法是先用少量已标注数据微调一个基础 OBB 模型把 conf 设在 0.3 左右让前端预标注人工只修正明显偏转或漏检的目标。对遥感场景先按置信度排序优先处理高分的批次低分区域留给第二轮。实践中高分辨率影像往往要切块推理。如果整张大图直接送给 predict显存和耗时都不可控而且会因内部 resize 导致小目标丢失。常见做法是把原图切成 1024 或 640 的瓦片每块独立推理再把 polygon 坐标加上瓦片偏移量拼回原图坐标系。这块逻辑可以在 Model.py 的 predict 里实现也可以在数据预处理层做。我的经验是先在小图上跑通端到端再引入切块避免把坐标偏移和图像缩放两个变量混在一起排查。那以后每次改 Model.py我都会先跑一遍 verify_predict.py确认输出坐标范围、标签映射和耗时三个指标都正常再进界面做交互验证。这个习惯帮我躲过了不少低级错误。需要说明的是这份资源里的 Model.py 走的是最通用的接口结构权重文件、标签配置和切块参数都要按你的数据集重调链路通了后面换模型就只剩换权重和调阈值的事。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑