首页 / 资讯中心 / 文章详情

YOLOv8-OBB半自动标注实战:Label Studio ML后端model.py改造指南

YOLOv8-OBB半自动标注实战:Label Studio ML后端model.py改造指南 ★ FEATURED ARTICLE
简介这份资源是 Label-studio 机器学习后端的 YOLOv8 旋转目标检测OBB模型接入脚本面向需要搭建半自动标注流水线的算法工程师与标注团队。它解决的是 Label Studio 原生后端缺少 OBB 任务支持的问题让用户可以直接调用 YOLOv8-OBB 模型对遥感、航拍、文字检测等倾斜目标场景进行预标注从而大幅减少人工框选工作量。压缩包内共 1 个文件为单个 model.py 脚本整体约 2KB体量轻巧便于直接放入 Label Studio ML Backend 项目中替换或扩展原有模型文件。目前已有 831 人学习关注说明该方案在半自动标注社区中具备一定实用参考价值。读者可借助该脚本快速打通 Label Studio 与 YOLOv8-OBB 的推理链路理解后端模型类的接口约定、预测结果到标注任务的转换逻辑并在此基础上替换为自己的权重与类别配置是入门半自动标注与旋转框检测落地的实用起点。1. 半自动标注落地把 YOLOv8-OBB 塞进 Label Studio ML 后端到底要改什么如果你已经用 Label Studio 标过一批旋转框数据大概会经历这样一个阶段纯手工拉框一天下来眼睛发花标注员开始怀疑人生。这时候自然会想到半自动标注——让模型先跑一遍人只做修正。Label Studio 本身提供了 ML 后端机制官方也给了label-studio-ml-backend这个仓库但当你真正去翻里面 YOLOv8-OBB 的model.py时会发现它和普通水平框检测的写法差别不小。旋转框多了一个角度维度预测结果要转成 Label Studio 能认的rotation字段预处理和后处理都得跟着改。这篇就围绕这个model.py文件把半自动标注链路拆开讲清楚它怎么接 Label Studio、YOLOv8-OBB 的推理结果怎么转标注、以及你想换成自己的模型时该动哪几行。2. 架构拆解ML 后端与 Label Studio 的通信链路2.1 整体数据流从任务下发到标注回写Label Studio 的 ML 后端本质上是一个独立的 HTTP 服务它不直接操作数据库而是通过一组约定好的接口和 Label Studio 主服务通信。整个链路可以拆成四步Label Studio 把待标注任务 POST 到 ML 后端的/predict接口后端从任务里取出图片 URL 或本地路径做预处理后送进模型模型输出原始张量后处理转成 Label Studio 的标注格式最后把结果按predictions数组返回Label Studio 前端渲染成可编辑的框。这个设计的好处是解耦。你的模型跑在什么环境、用什么框架Label Studio 不关心它只认 JSON。坏处是格式转换这一层必须自己写死一旦模型输出结构变了model.py里的解析逻辑就得同步改。YOLOv8-OBB 的输出和普通 YOLOv8 检测最大的区别在于每个框除了xywh和置信度、类别还多了一个角度θ而且这个角度在不同版本里表示方式可能是弧度也可能是角度坐标系原点也不一样。这是后面避坑章节要重点说的。2.2 model.py 在仓库里的位置与职责在label-studio-ml-backend的目录结构里模型相关代码通常放在models/下每个模型一个子目录里面至少有一个model.py和一个_wsgi.py。_wsgi.py是启动入口负责把model.py里的类实例化并挂到 WSGI 服务上。model.py才是核心它一般继承自LabelStudioMLBase需要实现两个方法predict和fit。predict是推理入口接收 tasks 列表返回 predictions 列表。fit用于在线学习或微调半自动标注场景下通常留空或者只做日志。对于 YOLOv8-OBBpredict里要做的事包括解析 task 里的图片路径、调用 ultralytics 的YOLO对象推理、把OBB结果转成 Label Studio 的rectanglelabels加rotation。下面这段是骨架实际项目里我会把预处理和后处理拆成单独函数方便替换。from label_studio_ml.model import LabelStudioMLBase from ultralytics import YOLO import numpy as np class YOLOv8OBBModel(LabelStudioMLBase): def __init__(self, **kwargs): super().__init__(**kwargs) # 模型路径从环境变量读方便容器化部署时挂载 self.model YOLO(os.environ.get(MODEL_PATH, yolov8n-obb.pt)) # 从 Label Studio 项目配置里拿标签映射 self.from_name, self.to_name, self.value self._get_label_config() def predict(self, tasks, **kwargs): predictions [] for task in tasks: image_path self._get_image_path(task) results self.model(image_path, conf0.25) # 后处理把 OBB 结果转成 LS 标注 pred self._convert_to_ls(results[0]) predictions.append({result: pred, score: ...}) return predictions这里有几个参数值得注意。conf是置信度阈值半自动标注场景下我一般会调低到 0.2 左右宁可多出一些框让人删也别漏标。_get_label_config是从 Label Studio 项目 XML 配置里解析出from_name、to_name和标签值这些字段必须和前端配置完全一致否则返回的标注渲染不出来。_get_image_path要兼容本地路径和 URL 两种情况Label Studio 传过来的可能是/data/upload/xxx.jpg这种相对路径也可能是完整的 HTTP URL。2.3 旋转框标注格式Label Studio 要的 JSON 长什么样Label Studio 里旋转框对应的标注类型是rectanglelabels但多了一个rotation字段。一个完整的标注对象大概是这样{ from_name: label, to_name: image, type: rectanglelabels, value: { x: 12.5, y: 20.0, width: 30.0, height: 15.0, rotation: 45.0, rectanglelabels: [ship] } }关键点在于x、y、width、height都是百分比不是像素。rotation是角度制逆时针为正范围通常取 -180 到 180。YOLOv8-OBB 输出的xywhr里r是弧度而且它的xy是中心点坐标。所以转换时要先除以图片宽高转成百分比再把弧度转角度。这一步如果搞错框会整体偏移或者旋转方向反了看起来就像模型“抽风”。3. 环境搭建与模型接入从零跑通一次推理3.1 依赖安装与目录准备先把仓库拉下来进到对应模型目录。假设你已经有一个能跑 Python 的环境建议用虚拟环境隔离因为 ultralytics 对 torch 版本有要求。git clone https://github.com/HumanSignal/label-studio-ml-backend.git cd label-studio-ml-backend pip install -e . pip install ultralyticspip install -e .会把label-studio-ml-backend以可编辑模式装进当前环境这样你改model.py不用重装。ultralytics装完会自动带上 torch 和 torchvision如果机器有 GPU确认一下 CUDA 版本和 torch 是否匹配不匹配的话推理会回退到 CPU速度差很多。目录结构上我一般会在models/下新建一个yolov8_obb文件夹把model.py、_wsgi.py和requirements.txt放进去。requirements.txt里写死ultralytics的版本避免不同机器上装出不同行为。3.2 启动 ML 后端并连上 Label Studio启动命令有两种方式一种是直接用label-studio-ml命令行一种是手动跑_wsgi.py。推荐前者它会自动处理端口和 worker 数量。label-studio-ml start ./models/yolov8_obb -p 9090-p 9090指定后端监听端口。启动后你会看到类似Listening at: http://0.0.0.0:9090的日志。这时候去 Label Studio 项目里打开 Settings - Machine Learning添加一个模型URL 填http://localhost:9090保存后点 Validate。如果显示 connected说明链路通了。注意一点如果你的 Label Studio 跑在容器里ML 后端跑在宿主机localhost在容器里指向的是容器自己得换成宿主机的局域网 IP 或者用host.docker.internal。这个坑我踩过不止一次日志里一直报连接超时排查半天才发现是网络命名空间的问题。3.3 用一张图验证推理结果链路通了之后别急着批量跑。先拿一张有代表性的图在 Label Studio 里点开一个任务手动触发一次预测。看返回的框位置对不对、角度对不对、标签类别对不对。如果框的位置整体偏移检查_get_image_path读到的图片尺寸和模型实际推理的尺寸是否一致。YOLO 默认会把图片 resize 到 640但后处理时用的宽高应该是原图宽高不是 640。如果角度反了检查rotation的正负号Label Studio 的旋转方向和 OpenCV 的minAreaRect不一定一致可能需要取反。# 弧度转角度并处理方向 angle_deg np.degrees(obb.xywhr[0][4].item()) # Label Studio 逆时针为正YOLO OBB 输出可能需要取反 ls_rotation -angle_deg if angle_deg 90 else -angle_deg这段逻辑看起来有点绕实际调的时候拿一张已知角度的图比如一个倾斜 30 度的矩形看返回的rotation是不是 30 或 -30多试两次就能确定符号规则。4. 核心代码逐段拆解predict 里的预处理与后处理4.1 图片路径解析与预处理predict拿到的 task 结构里图片信息藏在task[data]里键名取决于你在 Label Studio 里怎么配的。常见的是image或img。下面这个函数兼容本地路径和 URLimport os import requests from PIL import Image from io import BytesIO def _get_image(self, task): 从 task 里取出图片返回 PIL Image 和原始宽高 image_url task[data].get(image) or task[data].get(img) if image_url.startswith(http): resp requests.get(image_url, timeout10) img Image.open(BytesIO(resp.content)).convert(RGB) else: # Label Studio 本地路径通常以 /data 开头 local_path image_url.replace(/data/, os.environ[LS_DATA_DIR]) img Image.open(local_path).convert(RGB) return img, img.width, img.heightLS_DATA_DIR是环境变量指向 Label Studio 的媒体文件根目录。如果你用的是 Label Studio 的本地文件存储上传的图片实际落在media/upload/下路径映射要对上。URL 方式多一个网络请求批量推理时建议先把图片下载到本地缓存避免每次预测都走网络。4.2 OBB 结果转 Label Studio 标注这是整个model.py里最核心的一段。YOLOv8-OBB 推理返回的results[0].obb里有xywhr、conf、cls三个张量。xywhr是中心点加宽高加弧度conf是置信度cls是类别索引。def _convert_to_ls(self, result, img_w, img_h): 把 YOLO OBB 结果转成 Label Studio 的 result 列表 obb result.obb if obb is None: return [] xywhr obb.xywhr.cpu().numpy() confs obb.conf.cpu().numpy() clss obb.cls.cpu().numpy() results [] for i in range(len(xywhr)): cx, cy, w, h, r xywhr[i] # 中心点转左上角再转百分比 x (cx - w / 2) / img_w * 100 y (cy - h / 2) / img_h * 100 width w / img_w * 100 height h / img_h * 100 rotation -np.degrees(r) # 弧度转角度并取反 label self.labels[int(clss[i])] results.append({ from_name: self.from_name, to_name: self.to_name, type: rectanglelabels, value: { x: float(x), y: float(y), width: float(width), height: float(height), rotation: float(rotation), rectanglelabels: [label] }, score: float(confs[i]) }) return results几个细节x、y是左上角坐标的百分比不是中心点。Label Studio 的旋转是绕左上角还是绕中心不同版本行为有差异实测下来rectanglelabels的rotation是绕框的中心旋转但x、y仍然按未旋转时的左上角算。这意味着你不需要在坐标转换里考虑旋转带来的偏移Label Studio 前端会自己处理。self.labels是从项目配置里解析出的标签列表顺序要和模型训练时的类别顺序一致否则标签会串。4.3 置信度过滤与 NMS 参数YOLO 推理时自带 NMS但半自动标注场景下我建议把iou阈值调高一点比如 0.7让重叠框多保留一些人工修正时删比补容易。conf阈值前面说了0.2 到 0.3 之间比较合适。results self.model( image_path, conf0.25, iou0.7, imgsz1024, # 大图用大尺寸小目标更准 verboseFalse )imgsz默认是 640如果你的图片分辨率很高、目标很小调到 1024 或 1280 会明显提升召回代价是推理变慢。这个参数要根据实际数据分布来定没有万能值。5. 避坑与排查旋转框半自动标注的五个血泪经验5.1 框整体偏移或缩放不对现象预测出来的框位置整体偏左上或偏右下尺寸也偏大偏小。原因通常是坐标转换时用的宽高不是原图宽高而是模型输入尺寸。YOLO 推理时内部会做 letterbox返回的xywhr是基于原图坐标的但如果你自己又做了一次 resize 再传进去坐标就对不上了。解决确保传给模型的图片和_get_image返回的是同一张不要在外面再 resize。5.2 角度方向反了现象框的位置对但旋转方向镜像了比如实际倾斜 30 度预测出来是 -30 度。原因是 YOLO OBB 的弧度定义和 Label Studio 的角度定义坐标系不同。解决在rotation -np.degrees(r)这里试一下取反和不取反拿一张已知角度的图验证。如果取反后还是不对可能是 90 度偏移加一个90或-90再取模。5.3 标签类别串了现象预测的框位置都对但标签全错比如船标成了飞机。原因是self.labels的顺序和模型训练时的names不一致。解决打印模型的model.names和 Label Studio 项目配置里的标签列表逐项对比。最稳妥的做法是直接用model.names[int(cls)]取标签名不要自己维护列表。5.4 批量预测时内存爆了现象单张图预测正常一次传几十个 task 就 OOM。原因是predict里循环处理 tasks每张图都保留在内存里GPU 显存也没释放。解决在循环里用torch.cuda.empty_cache()或者把 batch size 控制小一点。更彻底的做法是改成流式处理每处理完一张就把中间变量置空。5.5 本地路径映射失败现象日志报FileNotFoundError路径是/data/upload/xxx.jpg。原因是 Label Studio 容器里的/data和 ML 后端容器里的/data不是同一个挂载点。解决统一用环境变量LS_DATA_DIR做前缀替换或者干脆让 ML 后端通过 URL 拉图不走本地文件系统。URL 方式多一层网络开销但省去了挂载配置的麻烦。6. 二次开发改造换模型、加类别、接主动学习6.1 换成自己的 OBB 模型把MODEL_PATH环境变量指向你的.pt文件就行。但要注意如果你训练时用的 ultralytics 版本和推理环境不一致加载可能报错。我一般会在requirements.txt里锁死版本比如ultralytics8.1.0然后在 Dockerfile 里固定基础镜像。换模型后第一件事是拿验证集跑一遍确认model.names和 Label Studio 标签配置对得上。6.2 动态调整置信度阈值不同图片的难度不一样固定conf要么漏标要么多标。一个实用的改造是从 task 的meta里读阈值Label Studio 支持在任务里带自定义字段。代码里改成conftask.get(meta, {}).get(conf, 0.25)这样标注员可以在前端针对难图调低阈值简单图调高阈值。6.3 接入主动学习循环半自动标注的进阶玩法是主动学习模型预测后把置信度低的样本挑出来优先让人标标完再微调模型。fit方法就是为此准备的。虽然官方示例里fit大多是空的但你可以在这里实现一个简单的微调逻辑收集人工修正后的标注转成 YOLO 格式跑几个 epoch然后热更新模型权重。def fit(self, event, data, **kwargs): 主动学习用人工修正数据微调模型 if event ! ANNOTATION_CREATED: return # 从 data 里取出标注转 YOLO OBB 格式 # 调用 self.model.train(...) 微调 # 训练完重新加载权重 self.model YOLO(runs/obb/train/weights/best.pt)这个循环跑起来后模型会越用越准人工修正量逐渐下降。但要注意微调频率太频繁会过拟合到最近几张图我一般攒够 50 到 100 张再触发一次。6.4 一个容易忽略的验证习惯改完model.py后别只在 Label Studio 前端点一下看结果。我习惯写一个小的本地测试脚本直接构造一个 task 字典调用predict把返回的 JSON 打印出来和原图叠加可视化。这样能快速定位是坐标转换错了还是标签映射错了不用每次都启动整个 Label Studio。从那以后我每次改完model.py都强制走一遍这个本地验证省下来的排查时间够多标好几批数据。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站