
这期聊聊我最近在做一个航拍目标检测项目时踩出来的一些经验。无人机在几百米高度俯拍停车场镜头里几十辆车挨挨挤挤停在一起用常规的水平框去做检测框完一看一个框里经常塞了两三辆车置信度还都不低。当时我第一反应是这个问题用普通YOLO做不了吗真做了才知道密集目标在水平框的约束下IoU重叠率高得吓人NMS一压漏检一大片。后来把方案改成YOLO旋转目标检测OBB从标注、训练到部署整条链路重走一遍问题才算真正解决。这篇东西就是把从航拍影像到精确轮廓这一路的关键步骤、踩过的坑尽量说清楚适合刚接触旋转目标检测、手里正好有航拍类任务的读者也适合想完整理解OBB项目流程的朋友。1. 为什么航拍场景离不开旋转框1.1 水平框的“大而无当”先拿停车场场景说事。水平框天生是轴对齐的目标一旦斜着摆它就必须用一个更大的矩形把目标包住。航拍视角下车辆往往是斜的、密集的几辆车并排停着水平框画出来基本是“一个大框套住两三个小框”框里除了车还有大量路面、阴影、相邻车辆的车身。这个现象我给它起了个名字叫“扫描框效应”。扫描框效应带来的麻烦是连锁的。第一目标之间互相遮挡严重水平框之间的IoU经常超过0.5NMS阶段直接把这些框当成重复检测压掉了表现在结果上就是漏检。第二框里塞了大量背景分类网络要从一团乱糟糟的特征里学“这是车”学到的东西不纯置信度低且容易误检。第三一个水平框也许同时覆盖了前面车的车尾和后面车的车头检测框的位置和真实目标中心错位严重下游要接跟踪或者计数的话数据根本没法用。旋转框解决的就是这个问题。它相当于给每个目标拉了一个最紧凑的斜向矩形容器紧贴物体的真实轮廓。框里几乎只有目标本身分类特征纯净相邻目标之间的重叠也大幅减少NMS误删率明显下降。我后来在同样的数据集上对比过单纯把标注从水平框换成旋转框、模型不换mAP50直接涨了四五个点这个提升完全来自标签质量和框表达方式的改善。1.2 旋转框到底多了一个什么自由度普通水平框用四个参数表示中心点x、中心点y、宽w、高h。旋转框在此基础上多了一个角度θ变成五参数x、y、w、h、θ。别小看这个角度它让检测头从“找框”变成了“找框并转框”学习难度上一个台阶。角度这个参数有个很坑的特性周期性。一个旋转框转180度看起来和原来一样如果标注时有的框标了0度有的框标了180度模型会直接懵掉——同样的形状标签却相差180度回归目标自相矛盾训练时loss震荡不收敛。所以做旋转目标检测的第一步不是调模型而是把角度定义统一掉。目前常见的做法是“长边定义法”以矩形长边与x轴正方向的夹角作为θ范围控制在[-90°, 90°)之间。这样无论标注时怎样旋转目标最终落到标签里的角度都是唯一且连续的。千万不要混用OpenCV那种[-90°, 0)的定义否则转换脚本写得再漂亮训练出来的模型也会在角度上“抽风”。另外和普通水平框相比旋转框对回归精度的要求更苛刻。水平框中心偏一点、宽高偏一点IoU不会有太大变化但旋转框角度偏1度在长边很长的目标上IoU就掉得很快。这解释了为什么同样的数据集OBB任务的mAP50-95往往比水平框任务低——不是模型变差了是评价标准变严了。1.3 为什么选择YOLO做旋转目标检测现在做旋转目标检测的路线其实不少。传统思路是先找候选区域、再对每个候选区域做分类和角度回归代表是Faster R-CNN加旋转候选框精度还可以但速度慢、工程复杂度高不适合航拍这种需要快速响应的场景。YOLO是单阶段检测器直接回归目标框、类别和角度一次前向推理全部搞定部署链路也非常成熟。更重要的是YOLO系对OBB的支持这两年已经相当完善。社区版的YOLOv5-OBB最早把旋转框落地把OBB问题转换到二维高斯分布上求解回归损失在密集场景下表现不错ultralytics官方从YOLOv8开始原生支持OBB训练、验证、导出一条龙出问题去GitHub搜一下基本都有答案YOLO11、YOLO12的OBB能力也在持续更新。相比之下MMRotate这类工具箱虽然功能更强但配置复杂对新手不是很友好。做项目最怕的不是模型不够强而是生态不够好。YOLO系有现成的预训练权重、标注工具适配、ONNX/TensorRT导出示例、大量社区教程这些隐性成本在排期紧的时候比模型本身的精度更关键。2. 数据准备从航拍影像到旋转标注2.1 数据从哪来公开数据集与自采切图航拍旋转目标检测绕不开的数据集是DOTA。DOTA-v1.0有15个类别包括飞机、舰船、储油罐、棒球场、网球场、篮球场、田径场、港口、桥梁、大型车辆、小型车辆、直升机、环岛、足球场、游泳池图像来自不同传感器和分辨率目标尺度差异很大。这里面车辆、飞机、舰船这类“小而密”的目标是练旋转框的好素材因为它会把水平框的弱点暴露得很彻底。不过DOTA毕竟是竞赛数据做项目时一般还需要自采一部分数据来贴合自己的场景。自采航拍影像要注意一个关键操作切图。无人机拍的原始影像动辄上万像素不可能直接扔进网络训练需要切成小图块tile。切图时相邻tile之间建议保留15%到25%的重叠率。重叠率太低目标正好卡在切缝上会被截成两半模型非常难学重叠率太高同一目标在多个tile里反复出现训练样本冗余度高、浪费算力。我自己一开始用10%重叠结果在停车场场景里连续漏检好几个边缘目标排查半天发现是切图时把车从中间切断了改成20%后明显好转。切完图之后建议做一轮人工筛选。不是每个tile都值得训练纯地面、纯绿地、过度模糊的图块留着只会增加训练时间甚至还可能让模型学到错误关联。筛完再标注效率翻倍。2.2 旋转标注工具从roLabelImg到X-AnyLabeling标注工具这块老牌选手是roLabelImg。它是LabelImg的旋转框版本能用矩形框画完再旋转角度生成的XML里有旋转角度信息。优点是稳、轻量、资料多缺点也很明显——界面停留在很多年前不支持好用的自动保存之外的现代化功能而且导出的XML需要自己写脚本转成YOLO的txt格式格式转换这一步就劝退了不少人。我现在主力用的是X-AnyLabeling。它支持旋转框标注界面是现代Web技术做的操作流畅还自带一些AI辅助标注能力可以先用模型预打标签再人工修正。最实用的是它能直接导出YOLO-OBB格式省掉中间转换环节。使用流程很简单新建标签列表选旋转框工具在目标上画出大致矩形再拖拽旋转角把框贴合到目标轮廓保存即可。不管你用哪个工具标注规范一定要先定好。我踩过几次坑之后整理了几条铁律一是统一角点顺序四个点要么都从左上角开始顺时针要么都按工具默认顺序不要想到哪标到哪二是长边优先画框时确保长边对应目标的主轴方向三是框要紧贴目标边界但也不建议贴到零缝隙留1到2个像素的余量反而有利于训练稳定四是密集目标宁可多画几个框也不要图省事用一个框包一簇。标注完成之后务必做一次可视化质检把标注框画回原图人工过一遍这一步能发现大量“标注时看起来很完美、实际歪得离谱”的问题。2.3 从四点坐标到YOLO-OBB格式不管是DOTA格式还是roLabelImg导出的格式本质都是多边形的四个角点。而YOLO-OBB训练需要的是cx、cy、w、h、angle这种紧凑表示。这里就需要一个转换脚本。核心思路是对四个角点求中心点作为cx和cy找到四个边向量里最长的那个作为长边方向长边长度作为w相邻边长度作为h长边方向与x轴的夹角作为angle。下面这个Python函数就是做这件事的import numpy as np import math def four_points_to_obb(points): # points: shape(4,2)四个角点坐标 x, y points[:, 0], points[:, 1] cx, cy x.mean(), y.mean() # 四条边的向量 edges [points[(i 1) % 4] - points[i] for i in range(4)] lengths [np.linalg.norm(e) for e in edges] # 取最长边作为长边方向 idx int(np.argmax(lengths)) vec edges[idx] w float(lengths[idx]) # 短边取相邻边 h_vec edges[(idx 1) % 4] h float(np.linalg.norm(h_vec)) # 长边与x轴夹角归一化到[-90, 90) angle math.degrees(math.atan2(vec[1], vec[0])) if angle 90: angle - 180 elif angle -90: angle 180 return cx, cy, w, h, angle拿到cx、cy、w、h之后切记要除以图片宽高做归一化否则训练时不同尺寸tile的标签量纲不一致模型很难收敛。angle的表示则要严格对齐你使用的训练框架有些框架用角度制有些用弧度制有些甚至要求[-90°, 0)的范围转换脚本里写错了这个细节训练出来的模型会在推理时疯狂旋转看起来完全“疯掉”。这也是我实际项目中排过的一个大坑后面第6节细说。另外如果你的数据有DOTA格式的真值还需要一个反向验证脚本。把OBB格式画回原图人工看一眼确认输出和原始四点标注对应的是同一个目标。这一步看似多余但它能抓出角点顺序不一致、长边选择错误这类转换脚本里的隐性bug。3. 环境配置与模型选择3.1 环境安装先把坑挡在门外YOLO-OBB的环境搭建其实不复杂但版本匹配问题能让新手卡上好几天。我通常用conda建独立环境避免不同项目之间互相污染依赖。conda create -n yolo-obb python3.10 -y conda activate yolo-obb # 根据你的CUDA版本安装对应PyTorch这里以CUDA 12.1为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics装完之后先做一次自检python -c import torch; print(torch.cuda.is_available())输出True才说明GPU可用。如果输出False大概率是PyTorch版本和CUDA驱动不匹配或者PYTHON环境里同时装了CPU版和GPU版的torch把环境清掉重装一次反而更快。显存这块训练OBB模型一般建议至少8GB显存起步否则batch size太小收敛慢、容易震荡。没有NVIDIA显卡的话AMD显卡可以走ROCm或DirectML路线YOLO也能跑只是安装和优化路径会比CUDA生态曲折不少新入门还是建议用N卡或者云GPU把环境先跑通。3.2 模型选型官方OBB还是社区OBB现在想用YOLO做旋转目标检测市面上主流有三个选择我列个表说清楚它们的定位方案优势适用场景YOLOv8-OBBultralytics官方原生支持、文档全、训练推理导出一条龙大多数实际项目首选YOLO11-OBB / YOLO12-OBB官方后续版本精度和速度进一步提升持续更新新项目、追求最新特性YOLOv5-OBB社区开源源码结构清晰方便深度改造和学术研究需要魔改loss或网络结构时RTMDet-RMMRotate精度强训练技巧丰富对精度要求极高的竞赛或研究场景我的建议是如果你要快速落地一个能跑的项目直接上ultralytics的YOLOv8-OBB或者更新版本模型参数文件和后处理代码都现成遇到问题社区里几乎都有答案。如果你拿这个项目做毕业设计或者想深入理解旋转检测的原理社区版的YOLOv5-OBB源码更值得啃它把OBB转高斯分布、KLD损失这些细节都摆在你面前。先别一上来就追求最强模型工程上能稳定跑通的东西才是好模型。3.3 数据配置与预训练权重我用ultralytics框架举例。假设你的数据放在datasets/vehicle_obb目录下布局类似datasets/vehicle_obb/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml内容大概长这样path: datasets/vehicle_obb train: images/train val: images/val nc: 4 names: [car, truck, bus, motorcycle]这里特别要提醒一下类别顺序。YOLO训练读取的是标签txt里每一行的第一个数字它是类别的索引对应data.yaml里names列表的下标。如果你的标注工具导出的类别顺序和data.yaml不一致模型会乖乖训练出一套“张冠李戴”的结果框的位置全对类别全错。训练前务必先写个小脚本把标签和data.yaml的顺序对一遍。预训练权重方面ultralytics官方提供了yolov8n-obb.pt、yolov8s-obb.pt等OBB专用权重直接用就行。如果实在找不到OBB预训练权重也可以加载普通的YOLOv8权重网络骨干部分能够迁移但OBB的检测头参数没法直接用需要重新训练。4. 训练与效果评估让模型真正学会“转角度”4.1 训练命令与关键超参环境配好、数据整理好之后训练命令本身很简单yolo obb train \ datadatasets/vehicle_obb/data.yaml \ modelyolov8n-obb.pt \ epochs100 \ imgsz1024 \ batch16 \ device0命令简单难在参数理解。imgsz这块我要多啰嗦几句。航拍影像里的目标普遍偏小直接把整张图缩到640去训练小目标可能只有三五个像素模型根本没机会学。航拍OBB任务我建议imgsz直接上1024甚至1280前提是你的显存扛得住。如果显存不够优先减batch而不是减imgsz因为尺寸对航拍小目标的影响比batch大得多。batch大小按显存来8GB显存跑yolov8n-obb可以从batch8或16起步如果爆显存就减半。训练轮数100起步但不要死等100轮跑完看到验证集指标连续20轮不涨就可以提前停。ultralytics里加一个patience20参数就能做早停。另外在ultralytics中有一个close_mosaic参数意思是训练最后N轮关闭mosaic增强。mosaic增强虽然能提升泛化能力但它会把多张图拼在一起旋转框的目标在拼接边界上容易被截断所以训练后期关掉它让模型平稳收敛是个实用技巧。我一般会设置close_mosaic10。4.2 训练日志与指标怎么看训练过程中最忌讳的就是只盯loss数字。loss在降不代表模型好用尤其是OBB任务里角度回归loss可能看着很低实际推理时角度还是偏的。我一般会同时看这几个东西loss曲线、precision曲线、recall曲线、mAP50、mAP50-95以及验证集的可视化结果。这里要纠正一个认知OBB任务的mAP50-95普遍比水平框任务低两到三个点这是正常的。因为旋转IoU的计算对角度极其敏感角度差1度IoU就可能掉好几个点。所以不要一看到mAP50-95不高就慌着调模型先看可视化的检测框是不是紧紧贴合目标轮廓如果轮廓贴合度好、只是角度差那么一两度那这个模型已经可以上线用了。如果发现loss持续不降或者降得很慢优先检查三件事一是标签格式到底转换对没有用可视化脚本把标注画回去看一眼二是类别数有没有和data.yaml对上有三是角度定义有没有混用。这三个问题排查完再去看模型结构。4.3 训练中的常见问题类别不均衡在航拍数据里很常见比如车辆多、直升机少。表现是模型对类别多的目标检测得好对少数类别几乎不响应。简单粗暴的办法是给少数类别多复制几份样本做上采样或者对loss做类别权重。新手别一上来就调复杂的采样策略先把数据补齐再看效果。小目标漏检是个反复出现的主题。除了提高imgsz和滑窗推理之外还可以检查目标在tile里的尺寸分布。如果大多数目标边长在10像素以下建议把切图tile改小一点或者提高切图分辨率。目标在训练图像里占的像素太少再强的特征提取器也无能为力。过拟合的表现是训练loss一直降、验证loss先降后升。这时候优先加数据增强旋转框任务里可以尝试90度旋转、镜像、亮度对比度调整等基础增强。OBB任务对影像的旋转比较敏感但如果你的目标本身没有明显的上下方向性比如空中俯拍的车辆、舰船那旋转增强基本是稳赚不赔。显存不足的应急方案有三个减batch、开梯度累积、换更小的模型。梯度累积可以模拟大batch代价是训练时间变长换模型则直接从yolov8s换到yolov8n速度最快。AMP自动混合精度在ultralytics里默认是开的不建议手动关掉它对显存省下的非常可观。5. 推理与部署把模型放到真实航拍大图里5.1 大图滑窗推理与跨窗口去重训练完成的模型面对的输入是几百MB甚至几个GB级别的航拍大图直接整图推理肯定不行。这里还是回到切图老路但推理切图和训练切图有一个关键区别推理时不能丢重叠因为重叠是去重的依据。我的通用做法是设定tile大小为1024x1024重叠率25%按滑动窗口把大图切成tile对每个tile做预测然后用跨窗口NMS把所有检测结果合在一起。这里的NMS阈值需要比单图推理设置得松一点通常conf阈值设0.25、IoU阈值设0.45起步优先保证不漏检后续通过置信度和目标尺寸筛选掉重复框。实测下来重叠窗口导致同一目标在多张tile里被重复检测跨窗口NMS能够把重复框合并得非常干净。关于重叠率的选择建议是15%到25%。重叠率太低目标在切缝处的检测质量会直线下降重叠率太高同一目标被检测五六次后处理耗时增加合并时也容易把相近的不同目标错误合并。如果显存允许直接用小tile、高重叠率推理得到的精度更高显存紧张就适当增大tile、减小重叠牺牲一点精度换速度。5.2 导出ONNX与TensorRT加速部署验证集上效果稳定之后下一步就是把模型导出成部署格式。ultralytics的导出命令很简单yolo export modelbest.pt formatonnx dynamicTrue # 或者直接导出TensorRT engine脚本里onnx也已提供 yolo export modelbest.pt formatengine halfTrue device0TensorRT导出后实测在N卡上FP16推理速度通常能比PyTorch快两三倍航拍大图场景下这个加速至关重要。导出后有几个坑要提前预防一是输入尺寸固定的TensorRT engine不支持动态尺寸部署时要用固定tile大小推理二是half模式在部分老显卡上精度会有轻微下降先跑一遍验证集对比再决定用不用三是模型输出的OBB结果需要自己处理后处理这一步千万别直接用水平框的逻辑。OBB的后处理和水平框最大的不同在于角度处理。模型输出的角度是预测头直接回归的值可能超出我们标注时定义的范围比如超过90度或者小于-90度。这类“角度越界”的输出需要做周期性修正否则画框的时候会得到方向完全错的矩形。简单说就是加180取模、保持角度在[-90,90)区间内。检查后处理代码时第一件事就是画几个框比对训练时的可视化看角度方向是否一致。5.3 结果可视化与GIS矢量导出算法跑通之后很多项目还差最后一步把检测结果做成可用的业务数据。画旋转框在CV里是常规操作用cv2.boxPoints把cx、cy、w、h、angle转成四个顶点画多边形即可。如果航拍影像有地理配准信息把像素坐标转换到经纬度或投影坐标是一个加分项。转换的核心是拿到影像的仿射变换参数通常由无人机后处理软件或GIS工具生成用一个六参数仿射变换就能把像素坐标映射到地理坐标再将旋转框导出为GeoJSON或Shapefile就可以叠加到GIS平台里做后续分析。这个能力在应急救灾、电力巡检、交通统计场景下特别实用省掉了人工在GIS里手动描框的巨大工作量。6. 常见问题与排查技巧实录6.1 角度不统一导致的“旋转失控”这是OBB项目里最容易踩、也最难排查的坑。具体症状是训练loss表现正常验证mAP也不算低但一可视化输出框的角度要么乱转、要么和目标方向差了90度。我在一个项目里遇到过所有框的角度集体偏移90度的情况排查了一整天最后发现是标注工具导出角度的定义和训练框架不一致一个按长边与x轴夹角算一个按OpenCV的负角度定义两者差一个角度的镜像翻转。排查角度问题的方法很直接抽几个标注样本把标签里的角度值打印出来再在图上量一下目标长边的实际倾斜角对比是否一致。一旦发现偏差不要怕写一个角度转换函数统一格式即可。标准做法是把所有角度统一到长边定义法范围[-90°, 90°)并和你的训练框架确认它们预期的是角度制还是弧度制。角度的符号、范围、单位这三个维度任何一个对不上模型都会给你“神色诡异”的乱转效果。6.2 训练直接报错标签文件格式不正确训练启动时报错常见信息是类似“box size smaller than zero”或者“invalid number of coordinates”。这类问题的根源几乎都在标签文件。可能是转换脚本输出时w或h没有归一化数值大于1也可能是某些目标只有两个点四点标注脚本处理出错还有可能是标签文件里的类别索引超出了data.yaml里nc的范围。排查方法很简单写一个脚本遍历所有标签检查每行是否有五个数值、坐标是否在0到1之间、角度是否在合法范围、类别索引是否小于nc。多花十分钟过滤一遍能省下后面好几个小时的debug时间。6.3 小目标漏检严重小目标漏检的表现是recall很低大目标都能检出来小目标几乎全漏。前面说过提高imgsz和切图分辨率是最直接的解法另外还要检查训练集里小目标样本的比例。如果小目标在训练数据里本身就很少模型自然学不到。实际操作中我倾向于对包含小目标的tile做额外复制或者重叠切图让小目标样本数量提上来再配合多尺度训练效果提升非常明显。6.4 部署端结果比训练端差很多训练时结果好好的导出ONNX后部署端一跑框的位置和角度全乱套。这种问题先别怀疑模型导出九成是预处理和后处理不一致。预处理方面要看部署端的缩放、归一化、通道顺序是否和训练时完全一致尤其注意letterbox的填充方式。后处理方面要检查OBB角度的再归一化逻辑是否在导出后的代码里实现了。我之前犯过的低级错误是部署代码里直接把模型输出的角度当成角度值来画框忘了它在训练框架里是以弧度存储的画出来自然乱七八糟。遇到部署端效果不对最快的排查方法是用相同的输入图跑一次PyTorch模型和部署模型把两个模型的输出张量逐元素对比。如果数值基本一致问题一定在后处理如果不一致问题在预处理或者导出配置。6.5 关于旋转框的“角度越界”最后补一个细节模型在推理时预测的角度可能落到标注范围之外。比如长边定义法要求[-90°, 90°)但模型输出可能是120度。此时必须对角度做周期性修正否则生成的矩形方向会突然翻转看起来像目标被“甩”了一下。修正逻辑很简单while angle 90: angle - 180; while angle -90: angle 180。这步通常封装在NMS后处理里但如果你自己写部署代码很容易漏掉。收尾的个人体会最后说点实际项目里的体会。当初在停车场场景第一次跑通旋转框模型时看到整排车辆的边框严丝合缝、不再互相叠套那个瞬间我才真正意识到数据标注的一致性远比模型选型更决定项目上限。YOLO再强标签角度定义混乱一样白搭数据集再大切图重叠率不对照样漏检。如果你正准备上手航拍旋转检测项目我建议在写任何训练代码之前先花半天时间把标注规范、角度定义、工具导出格式彻底定下来后面真的能省出好几个晚上的debug时间。下一步我在尝试把OBB检测结果接一个实例分割模块让轮廓从“最紧凑矩形”再进一步到“精确像素掩膜”同时叠加一个多目标跟踪器来应对视频流的时序稳定性等有新结论了再来同步。