
简介这份资源面向计算机视觉开发者与目标检测学习者提供基于Python与YOLOv5的旋转目标检测完整实现用于解决倾斜、旋转物体难以用常规水平矩形框精确定位的问题适用于遥感影像、航拍、文字检测等场景。压缩包共150个文件约6.26MB以66个py脚本和33个yaml配置为主涵盖模型训练、推理与参数配置同时包含cpp、cu、h、hpp等C/CUDA源码用于旋转框NMS与多边形IoU的加速计算另有md说明、sh脚本、Dockerfile及少量图片与字体文件工程结构较为完整。资源已有852人学习下载。内容围绕Oriented Bounding Box展开涉及角度回归、旋转数据增强、GIOU/DIoU损失调整、优化器与学习率调度、旋转NMS后处理等关键环节读者可据此搭建训练与评估流程理解旋转检测从数据标注到推理部署的完整链路并借助CUDA算子实现高效计算。1. 旋转框检测到底难在哪从 YOLOv5 的轴对齐框说起普通 YOLOv5 输出的是轴对齐矩形框也就是xywh四个值框的边永远平行于图像坐标轴。这个假设在多数通用检测场景里够用但一旦目标本身带方向——遥感影像里的舰船、航拍里的飞机、工业质检里的 PCB 板、密集排列的货架商品——轴对齐框就会把大量背景像素裹进来两个相邻目标的外接矩形还会大面积重叠NMS 一压就丢目标。旋转目标检测要解决的就是这个问题让模型直接回归带角度的框用xywha或四点坐标描述目标把框紧紧贴在目标真实朝向上。这篇讲的是基于 Python 的 YOLOv5 实现旋转目标检测从环境配置、数据标注格式、角度回归头的改法到训练参数、后处理旋转 NMS、部署推理走一遍能复现的路径。适合已经跑通过原版 YOLOv5、想把手里的检测任务升级到带角度框的从业者也适合刚配好 conda 和 vscode python 环境、想找一个有明确落地价值的方向练手的人。旋转框不是换个 loss 就完事标注、角度定义、损失函数、后处理每一环都有坑下面按顺序拆。2. 旋转框的表示与 YOLOv5 改造点先想清楚角度怎么定义2.1 三种旋转框表示法选错一个后面全乱旋转框主流有三种表示OpenCV 的(cx, cy, w, h, angle)、四点(x1,y1,x2,y2,x3,y3,x4,y4)、以及长边定义的(cx, cy, longside, shortside, theta)。YOLOv5 的旋转版本社区常见做法是在 Detect 头基础上加一个角度分支大多采用(cx, cy, w, h, angle)其中 angle 的取值范围直接决定训练稳不稳。表示法角度范围优点坑OpenCV 定义[-90, 0)与 cv2.minAreaRect 一致边界跳变w/h 会互换长边定义[-90, 90)角度连续回归稳定需要额外判断长边四点回归无角度表达能力强损失难设计易自交我一般用长边定义把 w 固定为长边、h 为短边angle 落在[-90, 90)。这样角度在边界处不会因为 w/h 互换产生 180 度跳变回归头学起来平滑得多。如果你直接用 cv2.minAreaRect 的输出喂进去它给的是[-90, 0)且 w/h 不保证长短边训练时 loss 会周期性炸一下这就是很多人说的玄学不收敛。2.2 角度回归头的加法与损失选择原版 YOLOv5 的 Detect 头每个 anchor 输出(tx, ty, tw, th, obj, cls...)。旋转版常见做法是在回归分支多加一个ta输出角度。加完之后 head 的输出通道从na * (5 nc)变成na * (6 nc)。改的地方集中在models/yolo.py的 Detect 类和models/common.py以及 loss 里的 box loss。角度回归的损失有两种主流选择一是把角度当普通回归量用 SmoothL1二是用 CSLCircular Smooth Label把角度分类化。CSL 把 180 度离散成 180 个 bin用交叉熵学能缓解边界问题但推理时要解码回连续角度且 bin 数是个超参。我实测在数据量中等几千张时 SmoothL1 配合长边定义已经够用CSL 更适合角度精度要求极高的遥感场景。# 角度归一化把任意角度映射到 [-90, 90) import math def normalize_angle(theta): # theta 单位度 while theta 90: theta - 180 while theta -90: theta 180 return theta def longside_form(rect): # rect: cv2.minAreaRect 返回的 ((cx,cy),(w,h),angle) (cx, cy), (w, h), angle rect if w h: w, h h, w angle 90 angle normalize_angle(angle) return cx, cy, w, h, angle这段代码做两件事normalize_angle把角度压到[-90, 90)longside_form保证 w 是长边。参数上注意angle 90是在交换 w/h 后补偿的漏掉这步角度就整体偏 90 度训练 loss 会卡在一个高位下不去。标注转换、数据增强、后处理三处都要用同一个函数任何一处用了不同定义结果就是框看着对但 mAP 死活上不去。2.3 数据标注DOTA 格式与 YOLO 旋转格式互转旋转框标注常用 DOTA 格式每行是x1 y1 x2 y2 x3 y3 x4 y4 category difficult。YOLOv5 旋转版训练一般要转成class cx cy w h angle的归一化格式。转换脚本要处理归一化和角度定义两件事。import os import cv2 import numpy as np def dota_to_yolo_rot(dota_line, img_w, img_h): parts dota_line.strip().split() coords list(map(float, parts[:8])) cls_name parts[8] pts np.array(coords, dtypenp.float32).reshape(4, 2) rect cv2.minAreaRect(pts) # 得到 ((cx,cy),(w,h),angle) cx, cy, w, h, angle longside_form(rect) # 归一化到 0-1 cx / img_w; cy / img_h w / img_w; h / img_h angle angle / 90.0 # 归一到 [-1,1) return f{cls_id(cls_name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f} {angle:.6f}逻辑说明先用cv2.minAreaRect求最小外接旋转矩形再过longside_form统一成长边定义最后把 cx、cy、w、h 按图像宽高归一化角度除以 90 映射到[-1,1)。参数上angle / 90.0这个缩放要和训练时角度分支的解码保持一致训练代码里如果乘回 90 而这里没除角度就差了 90 倍。转换完建议抽 20 张用 cv2 画出来肉眼核对别信脚本一次就对。3. 环境配置与训练跑通conda、数据集、超参一次说清3.1 conda 建环境与依赖安装先建独立环境别和系统 python 混。YOLOv5 对 torch 版本敏感旋转版还依赖 opencv 和 pycocotools。conda create -n yolov5_rot python3.8 -y conda activate yolov5_rot # 按显卡 CUDA 版本装 torch这里以 cu118 为例 pip install torch1.13.1cu118 torchvision0.14.1cu118 -f https://download.pytorch.org/whl/torch_stable.html pip install opencv-python pycocotools matplotlib tqdm scipy参数说明python 3.8 是兼容性最稳的版本3.10 以上部分旧版 torch 轮子找不到。torch 版本要和 CUDA 驱动匹配nvidia-smi右上角显示的 CUDA Version 是驱动支持上限装不超过它的版本。装完用python -c import torch; print(torch.cuda.is_available())验证返回 False 就先查驱动和 torch 版本别急着往下走。3.2 数据集目录与 yaml 配置YOLOv5 要求的数据集结构是 images/labels 分开train/val 分目录。旋转版 label 每行 6 个值比原版多一个角度。# data/rot_dataset.yaml path: /data/rot_dataset train: images/train val: images/val nc: 4 names: [ship, plane, storage-tank, harbor]注意nc和names长度必须一致names 顺序要和标注转换时cls_id的映射完全对应。我见过把 names 顺序写反导致模型学出来的类别全错位的情况loss 正常下降但验证时框的类别乱跳排查半天才发现是 yaml 里顺序错了。3.3 训练命令与关键超参python train.py \ --data data/rot_dataset.yaml \ --cfg models/yolov5s_rot.yaml \ --weights yolov5s.pt \ --epochs 300 \ --batch-size 16 \ --imgsz 1024 \ --hyp data/hyp.rot.yaml \ --device 0参数说明--imgsz 1024是旋转检测的常见选择遥感和小目标场景分辨率低了角度信息损失严重--batch-size按显存调1024 输入下 16 大概要 12G 显存不够就降到 8 并开--accumulate。--weights用 COCO 预训练权重初始化主干旋转头是随机初始化的前几个 epoch loss 偏高正常。--hyp里重点调box、cls、obj三个 loss 权重旋转任务里 box loss 权重可以适当调高因为角度回归误差对最终 IoU 影响大。训练过程中盯三个指标box_loss是否稳定下降、val/mAP_0.5是否上升、以及角度分支的单独 loss如果代码里拆出来了。如果 box_loss 降到某个值就不动大概率是角度定义和标注不一致回去查 2.1 节。4. 旋转 NMS 与后处理轴对齐那套直接搬会丢框4.1 为什么普通 NMS 在旋转框上会翻车普通 NMS 用轴对齐 IoU 算重叠度。两个斜着的相邻目标它们的外接轴对齐矩形重叠可能超过 0.5但真实旋转框几乎不重叠普通 NMS 会把其中一个当冗余压掉。密集场景下这个问题尤其明显召回率直接掉一截。旋转检测必须用旋转 IoU 做 NMS。4.2 旋转 IoU 的两种实现旋转 IoU 没有闭式解主流两种做法一是用 shapely 做多边形交集二是用 OpenCV 的cv2.rotatedRectangleIntersection。前者精度高但慢后者快但边界情况要处理。import cv2 import numpy as np def rotated_iou(box1, box2): # box: (cx, cy, w, h, angle) angle 单位度 r1 ((box1[0], box1[1]), (box1[2], box1[3]), box1[4]) r2 ((box2[0], box2[1]), (box2[2], box2[3]), box2[4]) inter_type, inter_pts cv2.rotatedRectangleIntersection(r1, r2) if inter_type cv2.INTERSECT_NONE: return 0.0 inter_area cv2.contourArea(inter_pts) area1 box1[2] * box1[3] area2 box2[2] * box2[3] return inter_area / (area1 area2 - inter_area 1e-7)逻辑说明rotatedRectangleIntersection返回交集类型和交点contourArea算交集面积最后按标准 IoU 公式算。参数上1e-7防除零。注意rotatedRectangleIntersection在 OpenCV 不同版本对相切、包含等边界情况返回不一致生产环境建议加一层 shapely 兜底或者直接用mmcv.ops.nms_rotated这类成熟实现别自己造轮子。4.3 后处理完整流程推理输出是(batch, num_anchors, 6nc)后处理顺序是解码 xywh 和 angle、按 obj 阈值筛、按类别做旋转 NMS、映射回原图坐标。def decode_and_nms(pred, conf_thres0.25, iou_thres0.45): # pred: (num_anchors, 6nc) obj pred[:, 4] mask obj conf_thres pred pred[mask] if pred.shape[0] 0: return [] # 解码cx,cy,w,h 是相对 grid 的偏移angle 是 [-1,1) boxes decode_boxes(pred) # 返回 (cx,cy,w,h,angle) scores pred[:, 5:].max(1) classes pred[:, 5:].argmax(1) keep [] for c in classes.unique(): idx (classes c).nonzero().squeeze(1) boxes_c boxes[idx] scores_c scores[idx] order scores_c.argsort(descendingTrue) while order.numel() 0: i order[0] keep.append(idx[i].item()) if order.numel() 1: break ious torch.tensor([rotated_iou(boxes_c[i], boxes_c[j]) for j in order[1:]]) order order[1:][ious iou_thres] return keep参数说明conf_thres和iou_thres是后处理两个核心阈值旋转场景下iou_thres通常比轴对齐调低一点0.4 左右因为旋转 IoU 本身数值偏小。decode_boxes里角度要乘回 90 还原成度这一步和 2.3 节标注时的/90对应。这段逐类 NMS 用 python 循环写便于理解实际部署要换成向量化或 C 实现否则帧率上不去。5. 避坑与排查旋转检测最容易栽的五个地方5.1 训练 loss 不降或周期性震荡现象box_loss 在某个值附近来回跳mAP 不涨。原因角度定义在标注、增强、损失三处不一致或者用了 OpenCV 的[-90,0)定义导致边界跳变。解决统一用长边定义[-90,90)写一个normalize_angle函数在三处都调用转换后抽图核对。5.2 验证时框位置对但角度整体偏 90 度现象可视化出来框能框住目标但长宽方向反了。原因longside_form里交换 w/h 后忘了angle 90补偿或者标注转换和推理解码用了不同的长短边约定。解决检查 2.2 节那段代码确保交换和补偿成对出现推理解码时也走同一个函数。5.3 密集场景召回率明显偏低现象单目标检测正常密集排列时漏检多。原因还在用轴对齐 NMS相邻目标外接矩形重叠被误压。解决换成旋转 IoU NMSiou_thres从 0.5 降到 0.4 左右试同时检查 anchor 尺寸是否匹配目标长宽比。5.4 显存爆掉或 batch 上不去现象--imgsz 1024时 OOM。原因旋转头多了一个输出通道激活值比原版大加上高分辨率输入。解决降 batch 并开梯度累积或者用--imgsz 768先跑通再往上加。混合精度--amp能省不少显存但角度回归在 fp16 下可能精度损失建议先 fp32 跑通再试 amp。5.5 部署时后处理耗时占比过高现象模型推理 20ms后处理 80ms。原因python 循环逐对算旋转 IoUanchor 多的时候是 O(n²)。解决先按 obj 阈值大幅筛掉低分框再做 NMS或者换mmcv.ops.nms_rotated、TensorRT 的旋转 NMS 插件。树莓派 5 这类边缘设备上部署自己训练的 yolov5 旋转模型时后处理建议直接上 C 或 ONNX Runtime 的自定义算子python 那套只能用来验证。6. 进阶把角度分支换成 CSL 并验证角度精度前面用的是 SmoothL1 直接回归角度简单但角度精度有上限。如果场景对角度敏感——比如遥感舰船朝向、工业零件装配角度——可以换成 CSLCircular Smooth Label。做法是把 180 度离散成 180 个 bin角度分支输出 180 维用交叉熵学推理时取期望或最大值解码回连续角度。改造点在 head 和 loss 两处。head 里角度分支输出维度从 1 变成 180loss 里角度部分从 SmoothL1 换成 CrossEntropy且要对标签做 one-hot 加高斯窗平滑窗口半径是个超参一般取 6 左右。import torch import torch.nn.functional as F def csl_loss(angle_pred, angle_gt, radius6): # angle_pred: (N, 180) logits # angle_gt: (N,) 单位度范围 [-90, 90) bins torch.arange(180, deviceangle_gt.device).float() - 90 # [-90, 90) diff (bins.unsqueeze(0) - angle_gt.unsqueeze(1)).abs() diff torch.where(diff 90, 180 - diff, diff) # 环形距离 target torch.exp(-(diff ** 2) / (2 * radius ** 2)) target target / target.sum(1, keepdimTrue) return -(target * F.log_softmax(angle_pred, dim1)).sum(1).mean()逻辑说明bins是 180 个角度中心diff算预测 bin 和真值的环形距离超过 90 度取补角高斯窗生成软标签最后算交叉熵。参数radius控制软标签宽度太小退化成硬标签、边界不连续太大角度分辨率下降6 是常见起点。换 CSL 后训练前期 loss 会比 SmoothL1 高因为交叉熵量级不同别慌看 mAP 就行。验证角度精度不能只看 mAP要单独统计角度误差。做法是把验证集预测框和真值框按中心距离匹配上算角度差的绝对值画直方图。如果误差集中在 0 附近说明角度学得好如果双峰分布说明有 180 度歧义回去查长边定义。我自己的习惯是每换一次角度表示或损失都先跑 20 个 epoch 看角度误差直方图比等 300 epoch 再看 mAP 省时间。旋转检测这行角度定义统一比模型结构重要得多血泪经验。希望帮到你。本文还有配套的精品资源点击获取