简介:这份资源面向具备一定深度学习基础的计算机视觉开发者与算法工程师,提供基于Python与YOLOv5的旋转目标检测完整实现,用于解决传统水平边界框难以精确框定倾斜、旋转物体的痛点,适用于遥感影像、航拍、工业质检等场景。压缩包共150个文件,约6.26MB,以66个Python脚本和33个YAML配置为主,涵盖模型训练、推理与配置管理;同时包含CUDA与C++扩展源码(.cu、.cpp、.h、.hpp)用于旋转NMS与多边形IoU的加速计算,并配有说明文档、Shell脚本、Dockerfile及少量图片与字体资源,目录结构清晰。已有852人学习下载。资源围绕Oriented Bounding Box展开,涉及角度回归、旋转数据增强、GIOU/DIoU损失调整、角度感知NMS后处理及训练策略配置等关键环节,读者可据此搭建环境、组织OBB标注数据、完成训练评估与实时推理,快速掌握旋转目标检测的工程落地思路。
1. 旋转框检测落地:从 YOLOv5 到 OBB 的工程化路径
做遥感影像、航拍巡检或者工业质检的朋友大概率遇到过这种场景:明明是同一类目标,水平框标注出来却互相重叠、背景混入严重,mAP 卡在 0.6 上不去。问题往往不在模型本身,而在于水平框(Axis-Aligned Bounding Box)天生无法贴合倾斜目标。这份基于 Python 的 YOLOv5 旋转目标检测资源,核心就是解决这个痛点——它把 YOLOv5 的检测头扩展出角度回归分支,配合poly_nms、poly_overlaps、polyiou这一整套多边形 IoU 计算模块,让模型直接输出带角度的 Oriented Bounding Box(OBB)。资源里能看到setup.cfg、poly_nms.cpp、poly_overlaps.cpp、polyiou.cpp、nms_rotated_cpu.cpp、nms_rotated_ext.cpp、poly_nms_cpu.cpp、poly_overlaps_kernel.cu、poly_nms_kernel.cu、poly_nms_cuda.cu这些文件,说明它走的是 C++/CUDA 扩展 + Python 调用的路线,而不是纯 Python 的近似实现。适合谁?已经跑通过原版 YOLOv5、想把手里的倾斜目标检测精度再往上推一截的从业者;也适合刚接触旋转框、想找一个能直接编译跑通的工程模板的新手。下面按「资源是什么 → 怎么编译用起来 → 坑在哪 → 怎么验证」的顺序拆开讲。
2. 旋转框 IoU 与 NMS 的底层实现:为什么必须编译 C++/CUDA 扩展
2.1 水平框 NMS 在旋转场景下为什么会失效
原版 YOLOv5 的后处理用的是水平框 IoU,两个框只要在 x、y 方向上有重叠就算相交。但旋转框多了一个角度维度,两个角度差很大的框,水平投影可能重叠面积很大,实际多边形交集却很小。举个典型例子:一个细长的舰船目标,标注角度 30°,另一个预测框角度 120°,两者中心几乎重合,水平 IoU 能到 0.7 以上,但真实多边形 IoU 可能不到 0.1。如果继续用水平 NMS,低置信度的错误角度框会被保留,高置信度的正确框反而被抑制掉,最终输出一堆方向错误的检测结果。这就是为什么旋转目标检测必须换一套 IoU 计算和 NMS 逻辑,不能只改损失函数就完事。
2.2 polyiou 与 poly_overlaps 的分工
资源里的polyiou.cpp负责最底层的多边形 IoU 计算,它把两个旋转矩形拆成四个顶点,用多边形裁剪算法(常见做法是 Sutherland-Hodgman 裁剪)求交集面积,再除以并集面积。poly_overlaps.cpp则是在这个基础上做批量计算,接收两组框的顶点坐标数组,输出一个 IoU 矩阵,供后续 NMS 或损失函数使用。poly_nms.cpp是 NMS 的主入口,它调用poly_overlaps得到 IoU 矩阵,再按置信度排序做抑制。CPU 版本对应poly_nms_cpu.cpp、nms_rotated_cpu.cpp,CUDA 版本对应poly_nms_kernel.cu、poly_overlaps_kernel.cu、poly_nms_cuda.cu。nms_rotated_ext.cpp是 PyTorch 的扩展绑定文件,把 C++/CUDA 函数暴露成 Python 可调用的接口。setup.cfg则是编译配置,告诉 setuptools 怎么编译这些扩展。
2.3 编译扩展的完整步骤
在动手之前,先确认环境里装了 CUDA Toolkit 和对应版本的 PyTorch。我一般会先跑一遍nvcc --version和python -c "import torch; print(torch.version.cuda)",确保两者版本对得上。然后进入项目根目录,执行编译:
# 清理旧编译产物,避免缓存导致链接错误 rm -rf build/ dist/ *.egg-info # 编译 C++/CUDA 扩展,-j 后面跟 CPU 核心数 python setup.py build_ext --inplace -j 8编译成功后,目录下会出现poly_nms_cpu.*.so、poly_nms_cuda.*.so这类动态库文件。如果只编译 CPU 版本,可以在setup.cfg里把 CUDA 相关源文件注释掉,或者设置环境变量CUDA_HOME为空。编译完成后,用下面这段代码验证扩展是否可用:
import torch from poly_nms import poly_nms # 具体导入名以项目实际暴露的接口为准 # 构造两个旋转框,格式为 [x1, y1, x2, y2, x3, y3, x4, y4, score] boxes = torch.tensor([ [10, 10, 50, 10, 50, 30, 10, 30, 0.9], [12, 12, 52, 12, 52, 32, 12, 32, 0.8], ], dtype=torch.float32) keep = poly_nms(boxes, 0.5) print("保留的框索引:", keep)这段代码的逻辑是:构造两个高度重叠的旋转框,调用poly_nms做抑制,阈值 0.5 表示 IoU 大于 0.5 就抑制低分框。如果输出只保留了索引 0,说明扩展编译和调用都正常。参数说明:第一个参数是 N×9 的 tensor,前 8 位是四个顶点坐标,第 9 位是置信度;第二个参数是 NMS 阈值,遥感场景常用 0.5,密集目标可以降到 0.3。
提示:如果编译时报
undefined symbol或nvcc fatal,先检查 PyTorch 和 CUDA 版本是否匹配,再检查setup.cfg里的源文件路径是否正确。
3. 数据标注与训练配置:把 DOTA 格式喂给 YOLOv5-OBB
3.1 标注格式转换的四个关键字段
旋转目标检测的数据标注和水平框最大的区别在于,每个目标需要八个坐标值加一个类别,而不是四个坐标值。常见做法是用 DOTA 格式:x1 y1 x2 y2 x3 y3 x4 y4 category difficult。但 YOLOv5-OBB 训练时通常需要转成归一化的[cx, cy, w, h, angle]或者保留八点格式。资源里没有附带转换脚本,但按这个项目的结构,我一般会写一个转换脚本,把 DOTA 的八点坐标转成 YOLO 训练所需的格式。转换时要注意四个顶点必须按顺时针或逆时针顺序排列,否则多边形 IoU 计算会出错。
import numpy as np def dota_to_yolo_obb(line, img_w, img_h): """将 DOTA 格式一行转为 YOLO-OBB 格式 [cx, cy, w, h, angle]""" parts = line.strip().split() coords = list(map(float, parts[:8])) cls = int(parts[8]) # 四个顶点 pts = np.array(coords).reshape(4, 2) # 计算中心点 cx, cy = pts[:, 0].mean(), pts[:, 1].mean() # 计算外接旋转矩形的宽高和角度 rect = cv2.minAreaRect(pts.astype(np.float32)) (rcx, rcy), (w, h), angle = rect # 归一化 cx_n, cy_n = cx / img_w, cy / img_h w_n, h_n = w / img_w, h / img_h # 角度归一化到 [-90, 0) 或 [0, 90),按项目约定 angle = angle % 180 return f"{cls} {cx_n:.6f} {cy_n:.6f} {w_n:.6f} {h_n:.6f} {angle:.6f}"这段代码的核心逻辑是:先用cv2.minAreaRect求四个顶点的最小外接旋转矩形,拿到中心、宽高和角度,再做归一化。参数说明:img_w、img_h是原图尺寸,归一化后所有值都在 0 到 1 之间;角度范围按项目约定处理,有的实现用弧度,有的用角度,训练前一定要和配置文件对齐。常见坑是顶点顺序不对导致minAreaRect返回的角度偏差 90°,建议转换后随机抽几张可视化检查。
3.2 配置文件里必须改的五个参数
YOLOv5-OBB 的配置文件通常包括数据集 yaml、模型 yaml 和超参 yaml。数据集 yaml 里要指定train、val路径和nc(类别数),以及names列表。模型 yaml 里关键改动在检测头部分,需要把输出通道从(nc + 5)改成(nc + 5 + 1),多出来的 1 就是角度预测。超参 yaml 里我一般会调整这几个:lr0初始学习率设 0.01 左右,lrf最终学习率系数设 0.01,momentum用 0.937,weight_decay用 0.0005,box损失权重可以适当调高到 0.05 到 0.08 之间,因为角度回归需要更强的位置约束。另外anchors也要重新聚类,用旋转框的宽高而不是水平框的宽高。
3.3 启动训练与日志观察
训练命令和原版 YOLOv5 基本一致:
python train.py \ --data data/dota.yaml \ --cfg models/yolov5s_obb.yaml \ --weights yolov5s.pt \ --batch-size 8 \ --epochs 100 \ --img-size 1024 \ --device 0参数说明:--data指定数据集配置,--cfg指定带 OBB 头的模型结构,--weights加载预训练权重做迁移学习,--batch-size根据显存调整,--img-size遥感场景常用 1024,--device 0表示用第一块 GPU。训练启动后重点看三个指标:box_loss是否稳定下降、cls_loss是否收敛、mAP@0.5是否在验证集上提升。如果box_loss震荡严重,先把学习率降到 0.005 试试;如果mAP一直不涨,检查标注角度是否和模型输出角度范围一致。
注意:旋转框训练对标注质量非常敏感,角度标注偏差 5° 以上就可能让 mAP 掉好几个点,标注阶段建议用可视化工具逐张检查。
4. 避坑与排查:旋转框训练里最容易翻车的五个地方
4.1 编译扩展时报 CUDA 版本不匹配
现象:python setup.py build_ext执行到 CUDA 源文件时报nvcc fatal: Unsupported gpu architecture或者undefined reference to cudaLaunchKernel。原因通常是 PyTorch 编译时用的 CUDA 版本和本机nvcc版本不一致,或者setup.cfg里指定的算力架构和当前 GPU 不匹配。解决办法:先用python -c "import torch; print(torch.version.cuda)"确认 PyTorch 的 CUDA 版本,再用nvcc --version确认本机 CUDA 版本,两者主版本号必须一致。如果本机有多套 CUDA,通过export CUDA_HOME=/usr/local/cuda-11.x指定正确的路径。算力架构可以在setup.cfg里把-gencode参数改成当前 GPU 对应的值,比如 RTX 30 系用sm_86。
4.2 多边形 IoU 计算结果为 NaN 或负数
现象:训练过程中box_loss突然变成 NaN,或者 NMS 后所有框都被抑制掉。原因多半是顶点顺序错乱导致多边形自交,或者两个框完全重合时除零。解决办法:在polyiou.cpp里加一个保护,当并集面积小于 1e-6 时直接返回 0;同时在数据加载阶段检查每个标注的四个顶点是否构成凸多边形,可以用叉积判断顶点顺序是否一致。我一般会在 dataset 的__getitem__里加一行断言,发现异常标注直接跳过并打印文件名。
4.3 角度回归范围与损失函数不匹配
现象:模型训练 loss 正常下降,但推理时角度预测总是偏向某个固定值,或者角度误差很大。原因是角度回归的范围没有和损失函数对齐。常见做法是把角度归一化到[-90, 0)或[0, 90),然后用smooth_l1_loss或者KLD损失。如果配置文件里写的是弧度,标注转换时用的是角度,就会差一个pi/180的系数。解决办法:统一用角度制,在模型输出层加一个sigmoid或tanh把角度限制在约定范围内,训练前用一张图过一遍前向传播,打印角度输出值确认范围。
4.4 数据增强把旋转框角度改乱了
现象:训练集 mAP 正常,验证集 mAP 很低,或者模型对某些角度的目标完全检测不到。原因是用了水平翻转、随机旋转等增强,但标注框没有同步变换。水平翻转时角度要变成180 - angle,随机旋转时角度要加上旋转角。解决办法:在数据增强的代码里,对图像做变换的同时,对标注的八个顶点做同样的变换,再重新计算[cx, cy, w, h, angle]。不要直接对角度值做加减,一定要基于顶点变换重新计算,否则累积误差会越来越大。
4.5 推理时 NMS 阈值设得太高或太低
现象:推理结果里同一个目标出现多个重叠框,或者密集小目标被大量漏检。原因是poly_nms的阈值没有根据场景调整。遥感场景目标密集时,阈值设 0.5 会导致相邻目标互相抑制,设 0.3 又可能保留太多重复框。解决办法:先在一个小验证集上跑不同阈值,画 precision-recall 曲线,选 F1 最高的那个值。我一般会从 0.3 到 0.7 每隔 0.1 试一遍,记录 mAP 变化。另外可以开启agnostic_nms,让不同类别的框也参与抑制,减少跨类重叠。
5. 验证与进阶:用 mAP 和可视化确认旋转框真的生效了
5.1 评估指标怎么看
训练完成后,用val.py跑评估:
python val.py \ --data data/dota.yaml \ --weights runs/train/exp/weights/best.pt \ --img-size 1024 \ --task val \ --save-json重点看输出里的mAP@0.5和mAP@0.5:0.95。旋转框检测的 mAP 通常比水平框低 5 到 10 个点,这是正常的,因为 IoU 计算更严格。如果mAP@0.5低于 0.5,先检查标注质量,再检查角度范围是否对齐。另外可以单独看每个类别的 AP,如果某个类别特别低,大概率是那个类别的目标角度分布太集中,训练集覆盖不够。
5.2 可视化验证的两种方式
第一种是用detect.py跑推理并保存图片:
python detect.py \ --weights runs/train/exp/weights/best.pt \ --source test_images/ \ --img-size 1024 \ --conf-thres 0.25 \ --iou-thres 0.5 \ --save-txt保存的图片里会画出旋转框,直接看框是否贴合目标。第二种是把预测的八点坐标导出成 DOTA 格式,用 DOTA 官方的可视化工具或者自己写脚本画出来对比。我一般会随机抽 20 张图,把预测框和标注框画在同一张图上,用不同颜色区分,一眼就能看出角度偏差和漏检情况。
5.3 一个容易被忽略的技巧:角度平滑
如果推理视频流时发现框的角度在相邻帧之间跳变,可以在后处理加一个简单的角度平滑。具体做法是维护一个长度为 5 的队列,对同一个目标的预测角度做滑动平均,再输出。注意角度是周期性的,平均前要先解缠,比如把-90°和90°映射到连续区间。这个技巧在航拍视频检测里很实用,能让输出框看起来稳定很多。
5.4 从训练到部署的检查清单
| 检查项 | 合格标准 | 常见问题 |
|---|---|---|
| 扩展编译 | .so文件生成且可导入 | CUDA 版本不匹配 |
| 标注格式 | 八点坐标顺序一致 | 顶点自交导致 IoU 异常 |
| 角度范围 | 训练和推理一致 | 弧度/角度混用 |
| NMS 阈值 | 验证集 F1 最高 | 密集目标漏检 |
| 数据增强 | 标注同步变换 | 角度累积误差 |
| 推理可视化 | 框贴合目标 | 角度跳变 |
这张表是我每次跑完旋转框训练都会过一遍的,尤其是扩展编译和角度范围这两项,翻车概率最高。从那以后我每次改完setup.cfg或者标注转换脚本,都会先拿一张图跑一遍前向传播,确认角度输出在预期范围内,再启动完整训练。希望帮到你。
本文还有配套的精品资源,点击获取