☰
YOLOv5 OBB旋转框检测训练全流程:从DOTA标注到部署避坑指南
2026/9/29 18:36:20 网站建设 项目流程

简介:YOLOv5 OBB旋转框训练DEMO是一份面向目标检测学习者的完整示例工程,解决倾斜目标(如树木、车辆)检测中的旋转框训练问题。资源共583个文件,约445.58MB,包含166个xml标注、95个txt标签、93个png和23个jpg图像素材、71个py脚本、38个yaml配置、6个pt预训练权重,以及训练、预测、导出、wts生成等cmd命令,覆盖从数据预处理、损失函数配置、模型训练到评估可视化的全流程。工程中内置md说明文档和ipynb交互示例,并配套dockerfile与makefile便于环境复现;训练环节涉及OBB坐标与角度标注转换、模型结构配置、旋转框损失计算等关键技术点,适合已有YOLO基础、希望扩展旋转框检测能力的中高级开发者。已有1812人学习,通过该demo可直观理解OBB边界框表示,复现训练闭环,并借助poly_nms等扩展模块完成后处理、ONNX导出与部署迁移。

1. 旋转框检测到底解决了什么问题:一个OBB训练demo的价值边界

遥感图里停着十几架朝向各异的飞机,工业传送带上的零件横七竖八,停车场里车辆斜着排成三排——这种场景拿普通YOLOv5的水平框去框,结果要么是框得松松垮垮把邻近目标一起包进去,要么是两个斜目标因为水平框重叠率过高被NMS误杀一个。旋转框(OBB,Oriented Bounding Box)就是用来解决这类“目标本身带角度”的检测问题的。这份YOLOv5 OBB旋转框训练demo,是把YOLOv5从水平框检测改造成可输出带角度预测框的完整训练示例,包含数据格式转换、网络头改动、训练参数和后处理全链路。适合正在做遥感目标检测、工业质检、无人机视角识别的人,也适合想搞懂旋转框标注格式和角度回归原理的开发者。后面所有内容都围绕一份能真正跑通的demo展开,讲清楚每一个参数和每一个坑。

2. 从DOTA标注到YOLO格式:旋转框参数化与转换脚本

2.1 为什么不能直接套用水平框的(x, y, w, h)

YOLO水平框检测的标注是四个值:中心点坐标、宽、高。这个表示在目标不带角度时完全够用,因为水平框无论怎么放都是“上平下平”,唯一变量只有位置和大小。但旋转目标不一样,同样一个长条目标,水平放置和斜45度放置,如果继续用x、y、w、h表示,那么不同角度的同类目标会对应完全不同的宽高比特征,模型要额外去学“这个宽高比和那个宽高比其实是同一个目标转了一下”,浪费模型容量还学不干净。

旋转框的常见参数化方式有三种:四点坐标、中心点加宽高加角度、八个参数(四条边的直线方程)。四点坐标最直观,但回归量多且四点之间顺序容易错乱;八参数主要用在某些遥感检测框架里,数学上更稳但工程实现复杂。YOLOv5这类单阶段检测器改造时几乎都选“中心点、宽、高、角度”的五元组方案,因为原有水平框的回归头是现成的,只需要在输出维度上增加一个角度通道,损失函数多接一个角度分支,改动成本最低。

要注意角度不是随便定个范围就行的。我见过好几个OBB demo翻车,根源都是角度定义没对齐:训练代码里角度范围是0到180度,但标注转换脚本生成的角度在-90到0度;模型输出经过sigmoid后无论如何也不会落到-90度区间,loss永远降不下去。所以拿到一个OBB demo,第一件事就是去源码里找角度归一化函数,看清楚它到底接受什么范围、以哪条边作为参考边。

2.2 DOTA转YOLO-OBB:按最小外接矩形生成五点标注

DOTA数据集是遥感旋转框检测用得最多的标注格式,每行是四个顶点的x、y坐标、类别和难易度。但YOLOv5 OBB训练需要的是class_id cx cy w h angle这种紧凑格式。转换思路很简单:用OpenCV的cv2.minAreaRect求四个顶点的最小外接矩形,拿到中心、宽高和角度。这里面有个容易踩的细节,minAreaRect返回的角度是width边与x轴的夹角,范围是[-90, 0),而且width不一定是长边。为了统一,必须把长边作为w,并把角度换算到模型约定的范围。

import cv2 import numpy as np def dota_to_yolo_obb(label_path, img_w, img_h, class_map): """ DOTA标注转YOLO-OBB五点格式 DOTA一行: x1 y1 x2 y2 x3 y3 x4 y4 class_name difficult 输出一行: class_id cx cy w h angle(弧度) """ results = [] with open(label_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) < 9: continue # DOTA四点坐标读进来,顺序不影响minAreaRect pts = np.array([ [float(parts[0]), float(parts[1])], [float(parts[2]), float(parts[3])], [float(parts[4]), float(parts[5])], [float(parts[6]), float(parts[7])], ], dtype=np.float32) rect = cv2.minAreaRect(pts) (cx, cy), (w, h), angle = rect # 统一把长边放到w上,angle换算到[0, 90) if w < h: w, h = h, w angle = angle + 90.0 # OpenCV的angle范围[-90, 0) -> [0, 90) if angle >= 90.0: angle -= 90.0 # 归一化到相对坐标 cx_n = cx / img_w cy_n = cy / img_h w_n = w / img_w h_n = h / img_h angle_r = angle * np.pi / 180.0 cls_id = class_map[parts[8]] results.append(f"{cls_id} {cx_n:.6f} {cy_n:.6f} {w_n:.6f} {h_n:.6f} {angle_r:.6f}") return results

这个脚本的核心逻辑是:先由四个顶点求出最小外接矩形,再做两步标准化。第一步是交换宽高,保证w始终是长边,这样模型回归时面对的物理意义是一致的;第二步是把OpenCV的负角度加90度转成0到90度,配合demo源码里的角度归一化。角度用弧度是因为很多OBB实现的损失函数里直接对弧度值做smooth L1,和标注单位保持一致可以省去后处理时反复换算的麻烦。

拿到demo代码包后,建议先看一下它的label文件样例:有的实现是cx cy w h angle class_id,有的实现是class_id cx cy w h angle。我这份脚本输出的是后者,和YOLOv5原版的class cx cy w h风格一致,如果你的demo是前者,把输出行的字段顺序调一下就行。

2.3 目录结构与数据集划分:demo先跑通再谈全量

旋转框数据的目录结构不需要特殊设计,和原版YOLOv5保持一致即可。常见布局如下:

路径内容
datasets/demo/images/train/训练图片
datasets/demo/images/val/验证图片
datasets/demo/labels/train/训练集OBB标注txt
datasets/demo/labels/val/验证集OBB标注txt
datasets/demo/train.txt训练图片路径列表,每行一个绝对路径
datasets/demo/val.txt验证图片路径列表

train.txt和val.txt里写绝对路径还是相对路径,取决于datasets.py里怎么解析。我一般写绝对路径,少一层坑;如果你要换机器复现,写相对路径更灵活,但前提是代码里做了路径拼接。这个细节看起来小,实际遇到过因为路径多了一层导致图片全没加载进去、训练了两百轮还在空跑的情况。

数据集划分上有一个明确建议:第一次跑demo不要拿全量DOTA,也别拿所有类别。挑一个类别、几百张图先跑通训练、验证、推理、画图这一整套流程,确认角度定义、loss曲线、后处理都没有问题,再扩展到全量数据。这样定位问题的时间能缩短一个数量级。另一点是类别序号必须在data.yaml里从0开始连续编号,旋转框的类别映射一旦跳过数字会直接索引越界。

3. 网络头与训练参数:在哪里把角度回归接进去

3.1 Detect head新增angle分支后的输出结构

原版YOLOv5的Detect head在每一个尺度上输出的通道数是(5 + num_classes) * num_anchors,5代表边界框的4个回归量加1个objectness。OBB改造后,每个anchor需要额外回归一个角度值,于是输出通道数变成(6 + num_classes) * num_anchors,新加的那个维度就是角度。

角度回归的处理方式有两种主流方案:一种是直接回归角度标量,网络输出经过sigmoid后乘以角度范围,比如映射到[0, 180);另一种是把角度编码成sin和cos两个值,各占一个通道。直接回归标量实现简单、收敛快,缺点是有角度周期性跳变的问题——179度和1度在数值上差178,但实际只差2度,普通L1损失会把这种“其实很近”的样本当成巨大误差。sin/cos编码能消除周期性问题,但两个值之间天然存在相关性损失,边界上反而可能更抖。demo级别的训练一般用直接回归就够了,因为标签角度已经统一到[0, 90),周期跳变的概率被局限在0度附近的小范围。

修改完head之后,输出的tensor形状会从(batch, anchors, 5 + num_classes)变成(batch, anchors, 6 + num_classes)。这一步改完后,数据加载器、损失函数、后处理三处都要跟着动:数据加载器要解析第6个维度,损失函数要新增一个angle loss分支,后处理要把角度和宽高一起反算成四个顶点。

3.2 训练一条命令跑起来:参数表与超参注意

demo的训练命令和原版YOLOv5几乎完全相同,只不过数据集配置换成了OBB版本,模型权重也建议改用OBB分支对应的预训练权重或者从原版yolov5s.pt迁移。

python train.py \ --img 1024 \ --batch 8 \ --epochs 100 \ --data demo.yaml \ --hyp hyp.scratch-low.yaml \ --weights yolov5s.pt \ --device 0

几个常见参数的含义和注意点,我按实际经验列一下:

参数建议初始值说明
--img1024遥感目标通常小且密集,输入尺寸低于768会丢失大量小目标角度信息
--batch尽量大旋转框demo数据量一般不大,batch 8到16是常见起步值
--epochs100先跑100轮看loss趋势,不要一上来就跑300轮
--hyp lr00.01如果loss曲线震荡剧烈,降到0.001再试
--hyp hyp.scratch-low.yaml低增强配置带角度任务不建议直接上高增强hyp,后面会说为什么
--weights yolov5s.pt迁移权重用COCO预训练权重做迁移,绝大多数情况优于随机初始化

注意一个细节:--hyp指向的配置文件里,默认的mosaic和mixup增强都是开的。OBB训练场景下,mosaic拼接大图时框的角度标注不会自动跟着图像变换走,尤其是水平翻转增强,角度标注如果不同步做负号处理,等于给模型喂错误标签。所以第一次训练我建议手动把hyp.scratch-low.yaml里的mosaic: 0.0、mixup: 0.0、fliplr: 0.0都关掉,等模型稳定收敛之后再逐步打开。

3.3 loss权重怎么调:angle loss不是越大越好

OBB损失函数通常是在原版分类损失和回归损失基础上,叠加一个角度回归损失。问题在于角度loss的权重设置很敏感,默认值未必适合你的数据集。如果angle loss权重设得太大,模型会优先把角度学好,但目标分类和定位反而被忽略,最终表现为框方向很准、类别置信度一塌糊涂;如果设得太小,模型会觉得学角度“不划算”,最后输出一堆接近水平的长条框。

我处理过的一个demo默认angle loss权重是1.0,训练后类别mAP掉了8个点。调到0.1之后,角度精度基本没变,分类和定位都恢复了。这里给一个经验区间,不过还是以你的demo源码里默认值作为基准做微调:

分支权重参考范围观察指标
box loss0.05训练集回归误差是否收敛
cls loss0.5 ~ 1.0类别mAP是否正常
angle loss0.05 ~ 0.2验证集角度偏差和loss曲线

判断角度是否学到了,不能只看训练loss,要在训练中途就用验证脚本画出预测框,直观对比预测和真实标注的长边方向。如果角度方向经常和真实框相差90度,那多半是长边定义和数据格式没对齐,不是权重问题。

4. 后处理要不翻车:旋转框解码、NMS与评测口径

4.1 解码输出:boxPoints反算四个顶点

模型输出的仍然是(cx, cy, w, h, angle)这种紧凑形式,但可视化、计算IoU、提交评测都需要四个顶点坐标。最省事的做法是直接用cv2.boxPoints,它接收中心点、宽高和角度,返回旋转矩形的四个顶点。

import cv2 import numpy as np def decode_obb(pred, angle_in_degree=True): """ pred: (cx, cy, w, h, angle) 返回: 四边形四个顶点,坐标是像素值 """ cx, cy, w, h, angle = pred[:5] if not angle_in_degree: angle = angle * 180.0 / np.pi rect = ((cx, cy), (w, h), angle) box = cv2.boxPoints(rect) # 返回4x2 float32 return box.astype(np.int32)

这里最需要注意的就是角度单位。训练时如果标签是弧度,模型输出的角度分支经过非线性激活后自然也是弧度;但cv2.boxPoints只认度数,而且旋转方向是顺时针。角度单位搞错的结果就是画出来的框要么全部偏转一个固定角度,要么在目标位置附近疯狂打转。我通常会在解码函数里强制加一个单位参数,训练和推理都走同一入口,避免两个模块各写各的换算逻辑。

4.2 OBB-NMS为什么是刚需

普通YOLOv5的NMS用水平框IoU作为重叠判断依据。旋转框场景下这个逻辑直接失效:两个斜45度放置的长框,中心接近、水平范围几乎完全重叠,但实际旋转框IoU可能只有0.3。如果按水平IoU做NMS,置信度低的那一个会被错误地抑制掉,长条目标密集排列时甚至会整排消失。

OBB-NMS的核心就是把IoU计算从水平框换成旋转框。实现方式有两种:一种是用shapely直接对多边形求交并面积,另一种是用旋转框专用IoU算子加速。shapely版本够直观,适合验证和调试,但速度慢,几百个框跑一次NMS要几十毫秒,不适合实时推理。

from shapely.geometry import Polygon import numpy as np def obb_iou(box_a, box_b): """ box_a, box_b: 四个顶点坐标, shape (4, 2) """ poly_a = Polygon(box_a) poly_b = Polygon(box_b) if not poly_a.is_valid or not poly_b.is_valid: return 0.0 inter = poly_a.intersection(poly_b).area union = poly_a.area + poly_b.area - inter if union <= 0: return 0.0 return inter / union def obb_nms(dets, iou_threshold=0.5): """ dets: list of dict, 每个包含 vertices(4,2), score, cls_id """ dets = sorted(dets, key=lambda x: x["score"], reverse=True) keep = [] while dets: best = dets.pop(0) keep.append(best) dets = [ det for det in dets if not (det["cls_id"] == best["cls_id"] and obb_iou(best["vertices"], det["vertices"]) > iou_threshold) ] return keep

这个示例只体现核心逻辑,真实工程里会用C++或CUDA算子把旋转IoU的计算提速,但算法骨架是一样的:按置信度排序、逐类计算、IoU超阈值就抑制。如果你的demo代码里实现的还是水平框NMS,训练结果再好也白搭,务必先确认后处理这一段有没有换成旋转框版本。

4.3 评测与可视化:本地验证角度偏差

评测也不能照搬水平框那套。遥感旋转框的标准评测指标是DOTA mAP,判定一个预测框是否算正样本时,要求预测框与真实框的旋转IoU超过阈值(比如0.5),且角度差不能超过设定阈值。后者是关键区别——有的预测框旋转IoU达标但长边方向反了180度,按DOTA规则也是错的。

本地快速验证建议分两步走。第一步是可视化,随机抽20张验证集图片,真实框和预测框一起画出,目测角度方向是否正确,这步能发现角度定义类问题;第二步是量化,统计预测框与真实框之间的角度绝对差,计算偏差小于15度的比例。这个比例如果低于80%,说明角度回归还没收敛,先去检查增强配置和angle loss权重,而不是急着调NMS阈值。

5. 避坑实录:角度定义、增强与评测的五个坑

5.1 角度定义不一致:loss降了但输出框旋转方向全乱

现象:训练loss正常下降,验证集mAP看着也不低,但可视化时预测框要么整体偏转一个固定角度,要么长边方向和真实框完全是垂直的。

原因:训练代码的角度归一化函数和数据转换脚本的角度定义不一致。举例来说,训练代码认为角度0到90度是长边绕x轴顺时针旋转的角度,但你的转换脚本生成的是逆时针角度,等于标签整体被镜像了一个角度偏移。模型确实学到了一个规律的映射,但这个映射和真实标注对不上。

解决:打开demo源码,找到角度归一化函数,确认它接受的范围、参考轴方向,然后把转换脚本里的角度换算逻辑对齐过去。有一个简单验证方法:拿一张只有一个目标的图,跑一次前向推理,把预测角度和真实角度打印出来对比。如果预测角和真实角呈现一个固定的差值,那基本就是这个问题。从那以后,我拿到任何一个OBB demo都会先跑这个单目标验证。

5.2 翻转和Mosaic增强偷改角度:训练集指标与验证集mAP严重分层

现象:训练集loss非常低,准确率接近100%,到验证集上暴跌,且越训越严重。

原因:YOLOv5默认开启的水平翻转增强对水平框完全无感,水平框左右翻转后宽高不变、类别不变,只是中心点的x坐标变了,标签不用做任何额外处理。但OBB不一样,水平翻转图像后,旋转框的顶点顺序会反转,角度必须同步取负或者做周期映射。mosaic拼接虽然没有翻转图像,但如果拼接过程中有随机旋转或者仿射变换,角度标注同样失效。很多demo的datasets.py里没有实现角度同步变换,等于一部分训练样本的标注角度是错的,模型被迫去拟合脏标签。

解决:最稳妥的方式是在第一个版本训练时把fliplr、mosaic、mixup全部关掉,只保留随机缩放和色彩抖动这类不影响角度的增强。等模型角度回归稳定后,再逐项打开并验证。如果非要在增强条件下训练,必须确认代码里翻转时对角度做了angle = -angle这样的处理。

5.3 极端长宽比与极小目标:角度回归震荡甚至NaN

现象:训练过程中angle loss突然跳到极大值,或者loss曲线持续震荡然后出现NaN,之后整个模型输出全变成垃圾值。

原因:细长目标的旋转框对角度极其敏感,长宽比10比1的目标旋转1度,dIoU就能变化几个百分点,梯度方向剧烈变化;再加上极小目标占的像素少,角度信息原本就弱,SGD在这种高曲率区域很容易震荡。

解决:三个手段配合使用。第一,把angle loss换成smooth L1,降低离群点带来的梯度冲击。第二,对极端长宽比样本降权,常见做法是按w/h比值设定一个权重系数,比值越大权重越小。第三,EMA(指数移动平均)能帮助稳定训练波动,如果demo默认没开,建议手动开启,YOLOv5里对应--ema参数或配置文件里的开关。还有一个保底手段是关闭AMP混合精度,FP16下角度梯度的精度损失可能直接导致NaN,这个在下一个坑里展开。

5.4 角度类别不平衡:模型选择“抄近路”把所有框学成水平

现象:训练结束后,预测角度集中在0度附近,旋转目标全被框成接近水平的矩形。

原因:这是我在一个停车场车辆数据集上遇到的典型问题——绝大多数样本是水平停放的车辆,斜着停的只有一小部分。模型发现把全部角度预测成0度就能降低大部分loss,角度分支反正贡献不了多少收益,干脆偷懒。这种情况不是模型坏了,是数据分布和loss权重设计共同导致的。

解决:方向有两个。数据侧,对角度做直方图统计,如果某个角度区间样本过少,对这类样本做在线旋转增强,每个训练轮次随机旋转一个角度再喂给模型。loss侧,把angle loss权重适当增大,让模型意识到不学角度会有明显惩罚。我曾经把angle loss权重从0.1调到0.3,斜停车辆的检出率从31%涨到58%,水平框方向的指标只掉了1个点。

5.5 AMP与显存坑:FP16下角度梯度不稳定

现象:同样的数据和超参,开AMP不到几十轮loss变成NaN,关掉AMP一路正常。

原因:原生YOLOv5默认开启AMP混合精度。水平框回归的四维目标量级都在0到1之间,FP16能扛住;但角度值可能是弧度制,范围0到π,而部分loss实现里对角度做差后平方,数值范围可以放大好几倍,半精度浮点数的表示范围很容易溢出。

解决:训练OBB时优先关掉AMP,用--amp false或者代码里配置关闭。显存不够不要靠AMP省,应该降batch或者降输入分辨率。如果你必须用AMP,那就把angle loss的权重调低,并且监控训练日志里angle分支的梯度统计,发现异常立刻回退。

6. 把demo验证扎实:可视化与两阶段训练

6.1 先画图后评估:GT与预测叠加检查

训练到一半,我会停下来跑一次推理,画一批训练集和验证集的对比图。画图的代码不用复杂:

import cv2 def draw_obb(img, box_points, color=(0, 255, 0), label=None): cv2.drawContours(img, [box_points.astype(np.int32)], -1, color, 2) if label: x, y = box_points[0].astype(int) cv2.putText(img, label, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)

把真实框画成绿色,预测框画成红色,重点检查三件事:长边方向是否一致、角度偏差是否稳定、密集场景下NMS有没有误杀。这组图比任何指标都先暴露问题。只要看到预测框整体长边方向和真实框差90度,就可以直接判定是角度定义问题,不用再去看mAP曲线。

6.2 两阶段训练:先冻结backbone再解锁精调

OBB任务一个不太好直接迁移的点是,COCO预训练权重对水平特征很熟练,但角度相关的特征完全没有。一种我在实际中反复用且有效的训练节奏是:第一阶段冻结backbone,只训练head的检测和角度分支;第二阶段解锁全部权重用低学习率精调。

# 第一阶段:冻结前10层,只训检测头和角度分支 python train.py \ --data demo.yaml \ --epochs 50 \ --batch 8 \ --img 1024 \ --weights yolov5s.pt \ --freeze 10 # 第二阶段:解锁所有层,小学习率精调 python train.py \ --data demo.yaml \ --epochs 100 \ --batch 8 \ --img 1024 \ --weights runs/train/exp/weights/best.pt \ --hyp hyp.finetune.yaml

第一阶段让角度分支快速找到正确方向,第二阶段再让backbone适应旋转特征。这样调出来的模型比一次全量训练更稳,尤其是小数据集上能明显减少抖动量级。我最后一次完整跑通一个OBB demo时,就是先冻结10层跑了50轮,再解锁全量跑了100轮,最终验证集角度偏差均值从11.2度降到5.8度,密集停放车辆的漏检率也降了一截。从那以后我每次拿到旋转框项目,第一轮训练都强制走一遍冻结再解锁的流程,先让框学会转,再让模型学会看。希望帮到你,少走这几个弯路,这个demo跑通就只是时间问题了。

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

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

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

立即咨询