ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLOv5旋转目标检测实战:从OBB标注到角度回归与旋转NMS部署

YOLOv5旋转目标检测实战:从OBB标注到角度回归与旋转NMS部署 简介这是一份基于YOLOv5的旋转目标检测完整工程包面向目标检测算法学习者和需要处理带角度目标如车牌、文字、细胞等的开发者。压缩包内含可训练、验证与部署的源码和预训练模型覆盖网络结构、OBB损失函数、数据预处理、训练与推理脚本、评估工具及配置文档方便直接上手或二次开发。包内共177个文件以Python脚本和yaml配置为主同时包含cu/cpp加速模块、pt权重、onnx模型及Markdown说明整体大小约103.66MB目录按模型、工具、配置等模块划分查找方便。已有382人学习下载适合既想掌握旋转框检测原理、又需要快速落地实践的工程师与研究者。整套工程提供了一条从数据准备到模型部署的完整路径预训练模型可直接用于推理或微调源码结构清晰是深入理解YOLOv5旋转检测机制的有力参考。1. 为什么YOLOv5要做旋转目标检测水平框的精度瓶颈无人机俯拍一张停车场画面里的车横七竖八。普通目标检测输出的是水平矩形一辆斜停的车水平框至少多包进 20% 到 30% 的背景两车并排斜停时水平框互相重叠NMS 甚至会直接删掉其中一个。旋转目标检测就是把检测框参数从 (x, y, w, h) 换成 (x, y, w, h, θ)让每个框都贴着目标朝向。基于 YOLOv5 的旋转目标检测系统指的不只是让网络输出多加一个角度而是要连带解决标注编码、角度损失函数、旋转 NMS、训练调参和部署后处理这一整条链路最后交付的是能跑的训练代码和可用权重模型。适合遥感航拍、工业质检、文档版面这类目标朝向任意的场景也适合想把水平检测能力平滑升级到 OBB 输出的算法工程师。2. 旋转标注格式转换从DOTA四顶点到OBB角度定义先立住理论为什么水平框会在旋转场景失真以及最实际的问题数据从哪来、怎么转。2.1 水平框标注在旋转场景中的两个问题第一个是背景混入。一条斜跨画面的细长目标比如河道里的挖沙船水平框的上半部分是天空下半部分是水面检测器必须额外学把框里的背景过滤掉的能力。样本里背景比例不一致模型学出来就不稳定同样的目标换个背景角度置信度就往下掉。第二个问题更隐蔽叫 mAP 虚高。评测时水平框和斜目标的 IoU 计算方式是水平 IoU哪怕框没有完全贴合目标朝向只要水平方向上重叠多IoU 依然很高AP 数值很好看但画出来一看每个框都是歪的。换成旋转 IoU 重新评测同一份模型 mAP 直接掉十几个点。所以我拿到别人的旋转目标检测源码模型包第一件事不是跑 demo而是先看它的标注是怎么定义角度的这决定了后面所有代码能不能复现。2.2 三种OBB角度定义方式与适用场景旋转框的表示方式不是只有一种最常碰到的是下面三种定义方式参数形式角度范围典型场景长边定义(cx, cy, w, h, θ)w 取目标长边[-90°, 0°)OpenCV minAreaRect、多数 YOLOv5-OBB 分支四边形顶点(x1,y1,x2,y2,x3,y3,x4,y4)无角度天然任意四点DOTA 原生标注、评测用 polygon IoU中心点边长角度(cx, cy, w, h, θ)[0°, 180°)MMrotate 部分配置、CSL 分类输出三种定义之间的关系是四边形顶点标注可以转成长边定义[0°, 180°) 和 [-90°, 0°) 只是同一个矩形取了不同的等价角度。转换时最怕的是 w、h 与角度三者不匹配例如标注文件里写着 θ-45°但 w 对应的是短边这样同一个框在训练和评测时会被当成两个不同的目标。长边定义之所以在 YOLOv5-OBB 这类工程实现里成为事实标准就是因为它消除了一半的歧义。2.3 四顶点标注转OBB坐标的Python代码import cv2 import numpy as np def quad_to_obb(points): 把4个顶点坐标转成OBB (cx, cy, w, h, theta)。 theta 取值范围(-90, 0]w 表示长边方向长度。 pts np.array(points, dtypenp.float32) hull cv2.convexHull(pts).reshape(-1, 2) # 统一顶点排列方向 # 找最长边作为主方向确保 w 永远是长边避免宽高互换 max_len, best_i 0, 0 for i in range(4): p1, p2 hull[i], hull[(i 1) % 4] length float(np.linalg.norm(p2 - p1)) if length max_len: max_len, best_i length, i p1, p2 hull[best_i], hull[(best_i 1) % 4] p3, p4 hull[(best_i 2) % 4], hull[(best_i 3) % 4] m1 (p1 p2) / 2 m2 (p3 p4) / 2 cx, cy (m1 m2) / 2 w max_len h float(np.linalg.norm(m2 - m1)) dx, dy p2 - p1 theta float(np.degrees(np.arctan2(dy, dx))) if theta 0: # 折叠到(-90, 0]与OpenCV约定一致 theta - 180 return cx, cy, w, h, theta逻辑说明先把任意顺序的四个点用凸包整理成统一方向排列然后以最长边作为矩形主方向宽 w 取主方向长度高 h 取两条对边中点的距离。这样在目标接近方形时也能稳定保证 w≥h不会出现同一个类别一会儿把横边当宽、一会儿把竖边当宽的情况。角度用 arctan2 算出主方向与 x 轴夹角再折叠到 (-90, 0] 匹配 OpenCV 的 RotatedRect 约定后续计算旋转 IoU 时不用再做角度换算。参数说明points 输入是 N×2 的浮点数组DOTA 标注文件里每个目标存的是 x1,y1,x2,y2,x3,y3,x4,y4 八个数值读取时按行 reshape 成四组坐标即可送入本函数。2.4 角度回绕和图像增强同步的两个坑角度回绕是第一个坑。θ 和 θ180° 表示同一个矩形θ 和 θ90° 在宽高互换后也等价。训练时如果标注文件里既有 -89° 又有 1°损失函数直接算 |Δθ| 会得到 90° 的巨大误差模型被迫学一个不存在的矛盾。解决方式有两种一种是在数据加载里把所有角度统一到同一区间并让 θ 差值的计算对 180° 取模另一种是改用循环平滑损失CSL把角度当成分类任务天然避开回绕问题。第二个坑在数据增强。给图像做随机旋转时OBB 的角度必须跟着图像旋转量同步累加做水平翻转时角度要取反。如果用了 albumentations 这类库增强输出的是多边形四点坐标而不是 OBB必须在增强之后再执行上面的 quad_to_obb否则增强前后角度定义不一致训练曲线会像锯齿一样抖动。3. 检测头与角度回归yolov5网络结构的旋转改造3.1 PANet检测头多输出一个角度通道YOLOv5 的网络结构由三块组成CSPDarknet 骨干、PANet 特征融合、三个尺度的检测头。旋转改造最轻量的做法是只动检测头每个 scale 的输出通道由 4坐标1objnc类别变成 5坐标角度1objnc类别即每个 anchor 多预测一个角度值通道数加 1其余结构不变。这个改动虽小解码逻辑要同步改三处。第一处是输出层维度换算head 输出的张量要按角度通道单独拆出来第二处是解码时对角度做激活和缩放通常做法是过 Sigmoid 后再乘以角度区间例如映射到 [-90, 0)第三处是损失计算坐标分支用 IoU 类损失角度分支单独用平滑 L1 或分类损失权重不能和坐标共用。这三处改完加载 YOLOv5s.pt 预训练权重时只有新增的角度分支是随机初始化其余权重全部迁移这是模型能快速收敛的关键。3.2 角度损失选型SmoothL1、CSL与KLD的取舍角度损失的选型直接决定模型上限。下表是三种常见角度建模方式方式建模思路对回绕的敏感度实现要点SmoothL1回归角度作为连续值直接回归高需手工处理±180/±90损失权重单独调w 取长边可降低歧义CSL分类把角度离散成 180 类天然免疫标签做高斯平滑输出层改为 180 类KLD/GWD把 OBB 映射成二维高斯分布求分布距离中定义更连续替换 IoU 分支训练速度较慢如果输入图在 800×800 到 2048×2048 之间且目标长宽比普遍大于 2:1SmoothL1 回归配合长边定义就够用这也是多数YOLOv5旋转目标检测源码的默认配置如果目标接近方形或者密集堆叠、角度歧义大直接换 CSL 更稳。KLD 的精度上限最高但实现复杂度和训练开销也最大我一般只在追求 mAP 榜单数据时才会上。3.3 影响收敛的3个必调参数无论用哪种损失实际训练里必调的只有三个参数调对了曲线才降得下去关键参数建议值调参说明angle_loss_weight0.02~0.10权重太小角度不收敛太大分类分支受压制角度区间与定义与标注转换完全一致常见组合w取长边(-90°,0°] 或 w取短边[0°,180°)必须统一旋转IoU阈值0.30~0.40影响正负样本分配密集场景调到0.35以下angle_loss_weight 是最常被忽视的一个。YOLOv5 原版的 box 分支用 CIoU梯度来自中心点距离和长宽比旋转角度本质上是另一个自由度。权重给到 0.01 以下时前 50 个 epoch 的 angle loss 几乎不动看曲线常常以为模型没学到给到 0.05 以上时w、h 的回归会乱因为角度还在震荡长边和短边没有稳定下来两者互相拉扯。建议从 0.05 起步前 10 个 epoch 跑一遍小验证集比较 angle loss 是否下降再微调。角度区间和标注转换的一致性是拿到源码模型包时首先要核对的。比如训练脚本里写的角度范围是 [0, 180)而数据转换脚本按 [-90, 0) 生成标签角度分支永远有一半区间是空的模型检出的框会偏向某一个方向。核对方法很简单随机抽 20 个训练标签把角度值打印出来看它落在哪个区间、w 是不是都比 h 大。3.4 解耦头与Transformer方案的对照在 3.1 的共享检测头上w/h 与 θ 共用同一个回归分支。长条目标在 θ 振荡时 w、h 也在变反向传播互相干扰。我一般会把回归分支拆成两个并行的卷积分支一个预测 (x, y, w, h)一个预测 θ 或者 180 类的角度概率这就是常说的解耦头decoupled head。改动成本不大推理速度几乎不变角度分支的收敛明显变快。对比方向上有另一条路OBB-DETR、RTMDet-R 这类直接用 Transformer 解码器做旋转框角度作为 DETR 的 query 的一部分参与全局注意力。Transformer 的位置编码对周期角度更友好回绕问题在结构层面就被消化但训练收敛慢部署依赖的 NMS 也要配套改造。推理延迟预算不紧张时可以换精度但在边缘端设备上YOLOv5 的 CNN 选型在生态成熟度和迁移学习上是更稳的选择。4. 用旋转YOLOv5训练自己的数据集4.1 大图切块与半自动标注流程航拍大图和遥感图动辄几千像素直接训练显存放不下。常见做法是切块把原图切成 1024×1024 的块相邻块重叠 200 像素。重叠的作用是避免目标恰好被切在边界上切块后标注坐标要做换算中心点坐标减去当前块的偏移量再除以块尺寸得到归一化标签。如果目标尺度跨度大切块尺寸降到 800 对小目标更友好。至于标注很多人问能不能不手动标。可以先用打包好的模型权重跑一遍推理得到初步的 OBB 结果再打开带角度编辑的标注工具只调整角度和误检框这个流程本质就是半自动标注。手动标注里最容易犯的错误是方向定义不一致同一个目标这张图把左上角当起点下张图把右下角当起点转换脚本交换了 w、h。靠 2.3 的 quad_to_obb 归一化能消除一部分但标注工具本身最好锁定长边定义问题越早发现越好。4.2 训练命令、数据集组织与超参数调整目录结构按 YOLOv5 的约定组织即可dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ # 每行: class cx cy w h theta │ └── val/ └── obb.yaml # path、train、val、nc、names训练命令# 环境配置完成后从 zip 解压的目录直接跑 python train.py --data dataset/obb.yaml \ --weights yolov5s.pt \ --img 1024 --batch-size 16 --epochs 300 \ --hyp hyp_obb.yaml --device 0命令行参数说明--weights yolov5s.pt用于迁移骨干网权重新增的角度分支会自动随机初始化--img 1024要与切块尺寸保持一致否则标签要等比缩放--batch-size按显存调整旋转分支比水平版多出的计算量很小显存占用大约只多 5%--hyp hyp_obb.yaml是自定义超参文件angle_loss_weight 这类参数写在这里。想快速验证收敛建议先用小数据集跑 60 个 epoch只关心损失是否下降、角度输出是否肉眼正确确认管线没问题再上全套数据跑 300 epoch。4.3 看loss曲线判断角度回归是否真的在学训练时最常遇到的异常是box loss 和 obj loss 都在降angle loss 居高不下甚至走平。排查顺序是先查角度区间和标签转换是否一致用 3.3 的核对法再确认角度损失权重是否太低最后看可视化输出里检测框是否反向——框很贴目标但 w、h 与目标长短边互换这种情况要在训练脚本和标注转换里同时保证 w 取长边。loss 曲线正常的情况下可以额外看混淆矩阵里的角度错误分布。如果把 ±5° 和 ±90° 的错分分开统计前 200 epoch 里 ±5° 错误占比高是正常的如果一直出现大量 ±90° 错误说明宽高定义在某个环节被翻转了不是训练不够的问题。5. 推理与部署旋转NMS与ONNX落地5.1 普通NMS在旋转框上的误抑制模型推理出来的 OBB 不能用 YOLOv5 默认的普通 NMS。斜着的两个目标水平包围框大面积重叠但旋转 IoU 其实很小。普通 NMS 按水平 IoU 计算会把挨着的第二个目标当作重复检测删掉漏检率直线上升。所以推理环节必须替换成旋转 NMS。5.2 用cv2.rotatedRectangleIntersection实现R-NMSimport cv2 import numpy as np def rbox_iou(r1, r2): r: (cx, cy, w, h, theta)theta 单位度范围(-90, 0] rect1 ((r1[0], r1[1]), (r1[2], r1[3]), r1[4]) rect2 ((r2[0], r2[1]), (r2[2], r2[3]), r2[4]) inter, _ cv2.rotatedRectangleIntersection(rect1, rect2) if inter is None: # 不相交 return 0.0 inter_area cv2.contourArea(inter) # 相交多边形面积 area1, area2 r1[2] * r1[3], r2[2] * r2[3] return inter_area / (area1 area2 - inter_area 1e-6)实现思路是把旋转框组织成 OpenCV 的 RotatedRect 结构直接求相交多边形再用 contourArea 算面积除以并集面积得到旋转 IoU。R-NMS 主流程和传统 NMS 一样按分数排序取最高分框删除与它 rbox_iou 大于阈值的其余同类框。注意旋转框数量大时要先按中心点距离做一次粗筛只对距离接近的框算旋转 IoU否则几百个预测框互相算一遍CPU 也会卡。5.3 ONNX导出后的后处理重写与边缘端部署要点python export.py --weights best_rotated.pt --include onnx --opset 12 # 导出后先对着单张测试图跑一遍 python detect.py --source test.jpg --weights best_rotated.onnxONNX 导出的坑在角度分支含义变化导出后的输出张量里角度通道仍是 Sigmoid 后的 0~1 归一化值后处理必须用训练时相同的区间还原成角度再把角度转成多边形点去绘制。可以用下面这张清单核对模型包输出与部署端的角度定义是否一致核对项常见取值部署端不一致时看到的现象角度范围(-90, 0] 或 [0, 180)框整体镜像、角度反向w 是否长边是细长目标框横竖颠倒角度通道顺序回归输出第 5 列坐标与角度错位框全乱旋转IoU阈值0.30~0.40密集目标漏检或重复框边缘端部署时角度分支的浮点精度比坐标分支更敏感INT8 量化前最好单独统计角度通道的激活分布必要时让角度分支保留 FP16。常见的做法是在模型转换时只量化坐标分支角度分支单独走浮点计算在 Jetson 和带 NPU 的边缘盒子上FP16 推理基本能保住角度精度再往上压就得换 CSL 角度分类结构配合 NPU。这张核对表可以直接拿去对照你拿到的模型包逐项勾一遍旋转目标检测这条链路就算完整打通了。本文还有配套的精品资源点击获取
返回列表