恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
YOLOv5 OBB旋转框训练实战:从DOTA数据准备到调参避坑全流程
首页
资讯中心
/
YOLOv5 OBB旋转框训练实战:从DOTA数据准备到调参避坑全流程
YOLOv5 OBB旋转框训练实战:从DOTA数据准备到调参避坑全流程
发布时间:2026/10/5 0:00:00
简介一套面向旋转目标检测场景的YOLOv5 OBB训练DEMO适合需要识别树木、车辆、遥感地物等倾斜对象的深度学习开发者可帮助快速理解OBB标注格式、角度回归与训练范式。资源包共583个文件、约445.58MB数据侧包含166个XML标注文件、95个TXT标签文件和93张PNG图片等代码侧涵盖Python脚本、YAML配置、预训练权重、ONNX模型、C/CUDA后处理源码并带有训练日志、Jupyter示例与Dockerfile基本覆盖旋转框检测从数据准备到模型推理的完整工具链。已有1814人学习下载包内提供了可直接运行的训练、预测、导出脚本也给出了OBB标签和配置文件示例便于对照理解旋转框回归、角度损失等关键设计。整体上是一份适合入门到进阶实践的旋转框检测参考项目尤其适合遥感解译、无人机巡检、工业质检等场景可用于快速搭建实验环境、二次开发并迁移到自有数据集。1. yolo v5 OBB 旋转框训练 demo解决什么问题适合谁无人机航拍里一架飞机在停机坪上任一角度停着车辆检测里集装箱斜着摆放遥感图里存储罐排成斜线——这些目标如果还用水平矩形框一个框里经常塞进另一辆车或一片背景mAP 被噪声拉低NMS 也会因为重叠区域过大而误删目标。YOLOv5 OBB 旋转框就是在水平框的 4 个参数上多回归一个角度用 5 参数格式或者 4 顶点格式描述一个“带方向的矩形”让网络学会贴着目标本身的走向输出检测框。本文围绕“yolo v5 OBB 旋转框训练 demo”这条完整链路展开先用 DOTA 格式准备数据再写一套转换脚本最后把训练命令、必调参数和那些容易翻车的细节串起来。适合已经会跑普通 YOLOv5、手头有倾斜目标数据、想尽快看到旋转框效果的从业者也适合准备把检测模型从水平框升级到 OBB 的团队做技术预研。2. 旋转框数据从哪来DOTA 与本地标注格式对齐2.1 旋转框标注和水平框的差异8 个坐标 vs 4 个坐标水平框只要记录左上角和右下角两个点即 x1, y1, x2, y2目标旋转后框内会混入大量背景。旋转框则用带角度的四边形包住目标常见的存储方式有两种一种是中心点加尺寸加角度即 cx, cy, w, h, angle适合回归头直接输出另一种是四个顶点的 8 个坐标x1, y1, x2, y2, x3, y3, x4, y4适合精细标注。社区里训练 YOLOv5 OBB 时更常见的是把标签统一成“类别 id 归一化后的 8 个顶点坐标”这样数据加载逻辑可以直接复用水平框那套只要把解码和 NMS 换成旋转框版本即可。DOTA 是目前最常被提及的遥感旋转框基准数据集原始标签里每一行是一个目标的 8 个坐标、类别名和 difficult 标志。它的坐标是像素级绝对值类别名是字符串比如“plane”“ship”“storage-tank”。训练 demo 的第一步就是把这套像素坐标归一化到图像尺寸并把类别映射成从 0 开始的数字 id。如果你手上有自己的业务数据格式转换的原则同样一致先统一顶点顺序再归一化最后确认类别表顺序三件事缺一不可。2.2 用 roLabelImg 或 X-AnyLabeling 标注自己的旋转框数据没有现成 DOTA 数据时需要自己画旋转框。老牌工具 roLabelImg 适合小批量标注界面和传统 LabelImg 类似画框时右键选择 create rotated box按四个点画出四边形缺点是快捷键和扩展性一般。更推荐 X-AnyLabeling它支持旋转框标注、自动保存、类别导入导出格式可以选择 DOTA 或者 VOC 变体上手成本低。标注完自己的数据后建议先导出一份和 DOTA 一致的文本格式再统一走转换脚本避免在不同工具之间反复倒格式。因为后面训练脚本读取的是“类别 id 归一化坐标”的 txt 文件所以无论用哪个工具最终都要落成这套格式。标注时还要格外注意顶点顺序同一个实现里四个顶点的顺序一旦不一致计算多边形面积和 IoU 时会得到完全错误的数值。最常见约定是顺时针或逆时针你只需要保证转换脚本全统一成一种顺序即可。2.3 把 DOTA 转成 yolov5_obb 需要的格式转换脚本与四个边界坑下面这段脚本是过去训练航拍数据时保留下来的最小可运行版本作用是把 DOTA 风格的原始标签转成训练用的归一化标签。这里以类别为 ship、airplane、storage_tank 三类为例你的数据按实际类别替换。import os import glob from PIL import Image src_txt labelTxt/train # 原始 DOTA 标签目录 out_txt labels/train # 转换后标签目录 class_names [ship, airplane, storage_tank] os.makedirs(out_txt, exist_okTrue) for txt_path in glob.glob(os.path.join(src_txt, *.txt)): img_path txt_path.replace(labelTxt, images).replace(.txt, .png) if not os.path.exists(img_path): img_path img_path.replace(.png, .jpg) width, height Image.open(img_path).size out_lines [] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 9: continue x1, y1, x2, y2, x3, y3, x4, y4 map(float, parts[:8]) cls_name parts[8] if cls_name not in class_names: continue coords [ x1 / width, y1 / height, x2 / width, y2 / height, x3 / width, y3 / height, x4 / width, y4 / height, ] cls_id class_names.index(cls_name) out_lines.append( str(cls_id) .join(f{c:.6f} for c in coords) ) out_path os.path.join(out_txt, os.path.basename(txt_path)) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(out_lines))这段脚本的逻辑很直接遍历每个标签文件读取同名图片的宽高把像素坐标除以宽高完成归一化再把类别名替换成 class_names 列表中的索引。写脚本一定要先确认 class_names 的顺序因为后面 data.yaml 里的 names 要和这个列表严格一致。如果原数据里类别名和列表不匹配这一行会被过滤掉日志上看不出来最后模型只会对其中一部分类别有响应。脚本里有一个隐藏很深的坑DOTA 图片可能是 png 或 jpg直接 replace 后缀会失败所以代码里做了二次替换兜底。另一个坑是 DOTA 原始标签最后还有一个 difficult 字段脚本只取了前 8 个坐标和类别紧凑但实用。如果你的数据里存在跨切图边界的旋转框切图后必须重新截取标签不能直接拿大图标签硬训否则会出现大量标签坐标超出小图范围的情况训练时损失直接炸。3. 跑通训练 demo环境、配置与最小命令3.1 环境准备clone 合适分支与依赖安装普通 YOLOv5 官方仓库并不支持角度回归需要的是社区维护的 yolov5_obb 分支。这类分支通常是在 YOLOv5 基础上新增了角度输出头、旋转框解码和旋转 NMS。环境建议 Python 3.8 到 3.10PyTorch 1.13 或 2.xCUDA 对应版本即可。先把代码拉到你信任的一个 fork然后安装依赖常见做法是git clone 你的yolov5_obb仓库地址 cd yolov5_obb pip install -r requirements.txt装完依赖后建议先跑一个不带权重的空 forward 脚本确认所有自定义算子编译通过。由于旋转框分支里普遍包含 C/CUDA 算子比如旋转 NMS 或 KLD 损失里的自定义函数很多分支第一次运行时占用编译几分钟日志会输出一堆 warning。只要最终没有报“undefined symbol”之类的错误就说明环境可以继续。如果你是新手建议直接用已有预训练权重初始化而不是从零训练。常见做法是拿官方 YOLOv5s 的水平框 COCO 权重做 backbone 初始化角度输出头随机初始化。加载权重时会出现部分键名比如 angle 分支的参数不匹配的提示这是正常现象。3.2 data.yaml 怎么写类别、路径与角度参数旋转框训练的 data.yaml 和水平框结构类似但多了角度相关配置。下面是一个经过实测的配置样例train: datasets/dota/train/images val: datasets/dota/val/images nc: 3 names: [ship, airplane, storage_tank] angle_deg: 180 kld: Truetrain 和 val 指向存放图片的目录加载器会自动在相邻的 labels 目录里查找同名 txt。nc 必须和 names 长度一致否则训练会在第一次迭代时报错。angle_deg 是角度表示范围180 表示角度值落在 [-90, 0) 这类半开区间360 表示全角度范围。不同实现定义不同建议以你拉取的分支文档为准。kld 表示是否使用 Kullback-Leibler Divergence 作为角度回归损失的一部分旋转框训练中 KLD 对角度周期性有更好的容忍度建议保持 True。有一点要警惕data.yaml 的 names 顺序必须和转换脚本里的 class_names 顺序完全一致。这里错位最常见的表现是 loss 掉得很正常但检出来的目标类别张冠李戴甚至 mAP 一直在个位数徘徊。我一般会在 data.yaml 旁边放一个 classes.txt转换脚本和配置都从同一个文件读取从源头避免顺序漂移。3.3 最小训练命令与参数含义环境验证通过、配置写好之后训练命令和普通 YOLOv5 差别不大核心参数是 data、weights、batch-size、img、epochspython train.py \ --data data/dota.yaml \ --weights yolov5s.pt \ --batch-size 12 \ --img 640 \ --epochs 100这条命令会以 yolov5s 为初始权重在 640x640 输入下训练 100 轮。batch-size 取决于显卡显存12 适合 16GB 左右显存显存 8GB 就降到 8。img 不建议一开始就放大到 1280先跑通 demo 再追求精度。epochs 对旋转框来说 100 是底线因为角度回归比水平框更难收敛尤其是数据量大、目标长宽比差异大的场景建议先跑到 100 观察曲线走势。训练过程中要重点看两个指标一个是 loss 曲线的总 loss 是否稳定下降另一个是验证集上的 mAP50 在最后一个阶段是否还有上升趋势。如果前 30 轮 mAP 一直为 0不用慌旋转框分支的输出头是新加的前期需要先学好分类和水平位置但超过 60 轮仍是 0大概率是数据格式或类别映射有问题直接停掉去检查上一章提的转换步骤。4. 影响旋转框精度的 4 个必调参数角度范围、损失权重、anchor 与超参4.1 角度范围选择180° 还是 360°角度回归的本质是预测一个连续值但这个值有周期性。比如 89° 和 -91° 其实是同一个朝向如果网络直接回归角度的绝对值稍有扰动就会产生巨大误差梯度也容易来回震荡。常见实现用角度范围 180° 或 360° 来规范化目标值180° 表示以长边为轴角度落在 [-90°, 0°) 之间360° 则覆盖 [0°, 360°)。选择哪种不是随意的要看你标注原始数据的格式。如果数据的旋转框是从 DOTA 转换来的四个顶点推荐统一到 180° 范围这样每个矩形只有唯一表示训练更稳定。如果你用的是 cv2.minAreaRect 得到的方向角默认范围是 [-90°, 0)也对应 180° 范围。千万不要把 180° 和 360° 混着用训练用 180°推理时后处理又写成 360°结果就是同一批目标时而角度正确、时而翻转 90°mAP 剧烈抖动。我一般会在配置文件里写死 angle_deg并在验证可视化脚本里加一个断言确保推理角度范围和训练一致。4.2 损失权重KLD 和角度项如何配比旋转框检测头通常输出 cx, cy, w, h, angle 五项损失由三部分组成分类损失、水平框回归损失、角度损失。角度损失如果直接对角度差做 L1遇到周期性边界会失效因此常见分支里引入 KLD 或者 GWD 这类基于高斯分布的距离度量。KLD 的思想是把旋转框建模成二维高斯分布两个框的分布距离作为回归损失这样即使角度落在边界附近损失依然平滑。训练日志中 loss 字段会分成 cls_loss、box_loss、angle_loss 或 kld_loss。假如发现 loss 在下降但可视化结果里目标的方向总是偏 30° 或 90°优先检查是不是角度权重太小。常见的超参配置里有一个角度权重项比如 hyp_mask2former不是yolov5_obb 的 hyp 文件里可能有一个 angle 权重。调整建议是从默认权重开始如果 angle_loss 比 box_loss 小一个数量级就把它调大到 1.5 到 2 倍再训一轮。注意不要一次性拉到 10 倍那样主干特征会为了迁就角度而牺牲位置精度。4.3 anchor 自适应旋转框和水平框的差异很多 YOLOv5 OBB 分支仍然使用水平 anchor 作为预设。anchor 是从训练集标签里聚类出来的候选框水平框聚类时只需要统计目标的宽高。旋转框虽然多了一个角度但 anchor 依然可以用旋转框的水平外接矩形来统计。问题是如果目标长宽比极大比如细长的飞机或船舶聚类出来的 anchor 数量不足网络无法有效覆盖所有朝向。建议第一轮训练前启用 autoanchorpython train.py --data data/dota.yaml --epochs 100 --autoanchor启用后脚本会计算出适合当前数据的 anchor并打印新旧 anchor 的召回率对比。如果原来的 anchor 在 640 输入下最佳召回率已经超过 0.9那大概率不需要调如果召回率低于 0.85特别是你的目标大多是细长形状就需要提高 anchor 数量或者增加一个偏向大长宽比的 anchor 尺度。很多 demo 跑不出效果不是因为网络结构而是 anchor 和角度没有适配。4.4 超参文件里值得调的 5 个参数旋转框训练在默认 hyp 文件基础上最值得动的是学习率、分类损失权重、box 损失权重、角度权重和 weight decay。下表给了常见的调整方向和参考范围参数名默认范围调整建议说明lr00.001 ~ 0.01小数据集用 0.001大数据用 0.01旋转框收敛较慢不要一开始就开超大学习率lrf0.01 ~ 0.1保持 0.01余弦退火的最终学习率倍数cls0.5 ~ 1.0类别不平衡时调高影响分类损失在整个 loss 中的占比box0.05 ~ 0.1旋转框回归不稳时调高影响位置回归权重angle / kld 权重0.5 ~ 2.0角度偏了调高具体字段名以分支实现为准我习惯先跑 30 轮快速验证看 loss 曲线里几个分项的下降趋势。如果 box_loss 下降很快但 angle_loss 基本不动就优先调角度权重如果整体 loss 都降但验证 mAP 不涨回过去看数据而不是继续堆 epoch。5. 旋转框训练避坑5 个翻车案例与排查手记5.1 现象loss 掉到 0.05 但 mAP 一直是 0这是第一次跑旋转框 demo 最常见的翻车现场。训练日志里分类和回归损失都正常下降看起来学得很对但每次 val 结束 mAP50 都打印 0。检查模型输出时目标框位置大致是对的但类别全部对不上。原因绝大多数是转换脚本里的 class_names 顺序和 data.yaml 里的 names 不一致比如脚本里 data 是按“飞机、船、罐”排序配置里写的是“船、飞机、罐”。解决方法是写一个独立校验脚本随机抽一张训练图的标签用 OpenCV 按 class_id 映射回 class_names 画框和原图人工核对一遍。这个过程能在训练前清掉大部分低级错误。5.2 现象训练正常可视化输出的框是水平框有的分支在推理阶段默认代码里仍然调用水平框解码只取了 cx, cy, w, h把 angle 丢了。也可能是后处理时用了官方 YOLOv5 原版的 non_max_suppression它内部按水平框的坐标格式裁剪预测结果角度自然被截断。解决方法是确认你用的分支里推理入口是否调用了 rotate_nms 或 engine.rotate_decoder并查看输出张量的最后一维尺寸如果是 5 类别数表示带角度如果是 4 类别数说明输出头没接上。也有一种特殊情况是训练时 angle 权重随机初始化后没收敛模型学出来的角度接近 0 或固定值这时去看 angle_loss 是否仍然很高而不是怀疑后处理。5.3 现象DOTA 转格式后类别顺序错乱DOTA 原始 15 个类别名包含飞机、船、罐、棒球场等实际使用时常只挑其中几类。写转换脚本时如果把类别名过滤和类别 id 分配写反比如先用 index 而不是名称映射就会出现所有第一行的目标标成 0 类第二行的标成 1 类每张图里类别分布完全随机。解决方法是先遍历全部标签文件打印出所有出现的类别字符串务必在任何过滤逻辑之前完成去重和排序再生成 id 映射表。这样即使一张图里有 DOTA 官方标注中不常见的类别也会因为列表缺失而在转换时直接排除而不是乱序。5.4 现象eval 时同一个目标被重复输出多个框输入一张旋转框密集的图比如机场停机坪NMS 之后目标周围仍然保留了五六个框。旋转框 NMS 和水平框 NMS 的 IoU 计算方式不同旋转框重叠面积需要多边形求交很容易被实现简化后算错。常见解决方法是降低 NMS 阈值。水平框常用 nms_iou0.45旋转框可以降到 0.3 一试如果还不行检查 NMS 的输入坐标顺序是否和解码输出一致。某些分支把角度定义成弧度但 NMS 里当角度用也会导致相邻框之间 IoU 被低估从而保留大量重复框。5.5 现象train 能跑但 val 时直接报 shape mismatch 崩溃这类问题一般出在验证集图片没有目标对应 txt 为空文件。转换脚本对空文件写入一个空串dataset 在加载时按行分割得到空数组后续解析 len(points) 无法对齐固定长度报维度错误。解决方法是转换脚本里加一个判断如果 out_lines 为空不写文件或写一个特殊的只含负数的“无目标”文件并在 data loader 中跳过该样本。另一个隐藏原因是图片尺寸非正方形验证时按 batch 堆叠要求统一尺寸建议用 letterbox 或直接把 img 设成 640x640 并保证所有图片大于 640 后再切中心区域而不是直接 resize否则旋转框的坐标和角度在缩放后全部失真。6. 验证旋转框 demo 的最终效果mAP、可视化与角度统一6.1 用 val.py 看正确的 rotated mAP训练完成后用验证脚本评估模型命令和普通 YOLOv5 一致python val.py --data data/dota.yaml --weights runs/train/exp/weights/best.pt --img 640输出里会打印 mAP50 和 mAP50-95。需要注意这两个指标是基于旋转框 IoU 计算的即两个四边形的交集面积除以并集面积而不是水平框的矩形 IoU。因此同一模型用普通 YOLOv5 的 eval 代码跑出的 mAP 是没有意义的必须确保代码里调用了旋转框匹配逻辑。对比基线时也应同时对比普通水平框模型和 OBB 模型在相同数据上的 mAP才能说明旋转框带来的增益。6.2 可视化预测结果先看框再看指标指标之外一张直观的可视化图能快速定位问题。常见推理脚本输出 cx, cy, w, h, angle需要转换成四个顶点才能画出旋转矩形。下面是一段把中心格式转成顶点的片段import math import cv2 def convert_to_vertices(box, angle_deg): cx, cy, w, h box cos_a math.cos(math.radians(angle_deg)) sin_a math.sin(math.radians(angle_deg)) dx, dy w / 2, h / 2 pts [ [-dx, -dy], [dx, -dy], [dx, dy], [-dx, dy] ] verts [] for px, py in pts: x cx px * cos_a - py * sin_a y cy px * sin_a py * cos_a verts.append((int(x), int(y))) return verts这段代码把中心点格式按角度旋转得到四个顶点然后就可以用 cv2.polylines 绘制。如果发现顶点顺序在可视化时交叉错乱重新确认角度正负方向和旋转公式而不是强行继续训练。可视化时用原始分辨率图片不要用训练时的 letterbox 图否则拖出来的框会有偏移。6.3 导出部署模型前统一角度和坐标格式写 demo 是一回事真正部署是另一回事。导出 ONNX 或 TensorRT 前需要确认模型输出的角度定义与部署端 NMS 期望的一致。比如训练时角度范围是 [-90°, 0)导出后若部署端默认 [0°, 180°)推理结果会整体误差 90°表现为细长目标全部转成短边对齐。解决方式是在导出脚本里增加一个角度映射层把网络输出统一到部署端格式或者直接修改部署端后处理和训练保持一致。另一个坑是坐标是否归一化、是否在 0~1 之间。推理完要乘回原图尺寸角度不能乘宽高比这些细节在 demo 阶段看不出来真上线就暴露。第一次跑通旋转框 demo 时我犯的最傻的错误是把 DOTA 标签里的类别顺序写反了mAP 连挂三天后来用可视化批量画了一遍才意识到当时只觉得“这模型怎么这么玄学”。之后我给自己定了个规矩任何新数据集先随机抽 5 张可视化标签再抽 5 张可视化预测最后才看指标。顺序反过来你会被 mAP 骗到怀疑人生。旋转框训练并不是比水平框难多少而是多了一个需要全链路对齐的角度变量角度对齐了剩下的调参经验基本可以平移。希望这篇笔记能帮你在旋转框上少走一段弯路。本文还有配套的精品资源点击获取