
去年做无人机巡检项目甲方要从高分辨率航拍影像里把停车场、物流园里的车辆和集装箱全部框出来。第一版我直接拿标准YOLOv5训练结果在密集区域翻车很彻底横七竖八的车在水平框里互相重叠NMS一压一排车只剩下两三辆。后来切换到YOLO旋转目标检测OBBOriented Bounding Box路线同样一批数据mAP直接涨了一截。这篇就把从航拍影像到精确轮廓的完整流程整理出来——旋转框标注怎么做、DOTA格式怎么转成YOLO-OBB、模型选哪个、训练时loss为什么会莫名其妙炸掉、推理后处理有哪些坑。适合正在做航拍、遥感、无人机巡检以及文档版面、医疗切片这类目标方向任意场景的检测需求的朋友参考。1. 为什么航拍场景必须用旋转框先搞懂水平框的局限1.1 当IoU计算失真时一切指标都跟着失真先算一笔简单的账。一辆车长4.5米、宽1.8米水平停放的面积是8.1平方米。如果这辆车斜着45度停它的水平外接矩形面积大概是多大通过简单几何计算大约是19.8平方米——框里面超过一半是背景。也就是说一个斜着的细长目标在水平框下会引入大量的无效区域。这个现象带来的连锁反应比想象中严重。第一训练阶段的正负样本分配会劣化。大多数YOLO系列依赖Anchor或者基于中心点的匹配策略。水平框表示斜目标时框中心和真实目标中心即使重合框的四边和目标实际轮廓偏差也很大导致候选框和目标之间的IoU普遍偏低很多目标匹配不到高质量的正样本模型学习效率自然下降。第二推理阶段NMS会把密集目标误删。航拍场景里车辆密集停放、建筑物排列紧凑水平框之间的重叠面积远大于旋转框之间的真实重叠。置信度稍低的目标很容易被高置信度目标的水平框压掉造成同一辆车被重复检测的反而少、邻近目标被误删的情况多。我在自建车辆数据集上做过对比同一个训练集、同样的模型骨架和优化器水平框方案mAP50只能到68左右旋转框方案能到79上下。这个数字当然跟数据分布有关但趋势是稳定的目标方向越随意水平框吃亏越大。第三方向信息直接丢失。检测结构里没有角度这一维后处理想修正都没有抓手。所以在航拍、遥感这类小目标多、方向复杂、目标拥挤的场景里旋转目标检测不是炫技是刚需。1.2 旋转框的五种表示方式格式统一是第一道坎旋转框的表示方式没有统一标准这是实际开发中最容易被绊倒的地方。不同框架、不同数据集、不同标注工具各用各的定义互相转换时稍不注意就出现框的方向颠倒或者角度范围错乱。目前常见的表示方式有五种表示方式核心参数典型场景OpenCV角度cx, cy, w, h, angleangle范围[-90, 0)cv2.minAreaRect、传统视觉处理长边角定义cx, cy, w, h, angleangle范围[0, 180)大量论文与工业代码DOTA四点坐标x1, y1, x2, y2, x3, y3, x4, y4DOTA数据集标注与官方评测YOLO-OBB四点坐标归一化的x1, y1, x2, y2, x3, y3, x4, y4YOLOv5-OBB、YOLOv8-OBB的标签格式任意四边形带凹凸的四个点文字检测、不规则目标标注最容易踩坑的是OpenCV角度。cv2.minAreaRect返回的角度范围是[-90, 0)它表示的是矩形宽边与横轴之间的夹角而且这个宽边不一定是你概念里的长边。如果你直接把这个angle塞进深度模型里当回归标签训练到一半就会发现loss莫名其妙波动因为同一个旋转矩形可以有两种完全等价的表示两种表示的数值却相差很大。DOTA数据集的四点坐标相对直观但四个点的顺序在不同数据子集里并不完全一致。工程上必须统一成逆时针或者顺时针顺序一般推荐统一为逆时针。再强调一个容易被忽视的点四点坐标描述的不一定是严格矩形某些标注工具画出来的是普通四边形。虽然OBB检测把四边形近似为最小外接矩形能兜住但如果四边形严重不规则这个近似会引入误差。YOLOv5-OBB和YOLOv8-OBB的标签统一使用归一化后的四点坐标。这个设计的好处是绕开了角度周期性问题后处理阶段再通过cv2.minAreaRect恢复成旋转矩形就行。但它的前提是四个点要严格按照相邻顺序排列否则最小外接矩形会算出一个带大旋转角度的错误结果。2. 数据是旋转检测的命门标注工具与格式转换全流程2.1 标注工具选型从单机到团队协作做旋转检测标注工具的选择直接决定数据质量和生产效率。我试过几款主流工具各自的适用场景差异很大。roLabelImg是老牌工具基于labelImg改的支持画旋转矩形输出Pascal VOC格式的XML文件每个目标带一个robndbox节点。界面简陋快捷键逻辑和labelImg一致适合个人小规模标注。但它已经很久没更新在较新的Ubuntu和macOS上装依赖经常要折腾而且它对旋转框的编辑体验一般框画错了想微调角度很别扭。X-AnyLabeling是我现在单机标注的主力。它支持旋转框、多边形和多种自动标注模型尤其是SAM相关的辅助分割模型能大幅提升效率。航拍图里密集的车辆如果手动框一小时标不了几张先用分割模型生成掩膜再转成旋转框人工微调速度能翻倍。它可以直接导出YOLO-OBB格式省去二次转换。CVAT适合团队协作。它的在线标注流程、任务分配和审核机制很成熟数据规模大了以后效率优势明显。新版本对旋转框和YOLO-OBB导出支持得不错但部署和维护成本比前两者高单人项目没必要上。labelme画多边形也可以但如果你最终需要的是旋转矩形而不是任意四边形导出后还得自己转。选型建议很简单一个人做几百张图的实验用X-AnyLabeling团队做批量生产上CVAT没有特殊原因不要回头用roLabelImg。2.2 DOTA格式转YOLO-OBB四点标签DOTA数据集是遥感旋转目标检测的事实基准很多开源数据都采用它的标注格式。每行一条目标记录x1 y1 x2 y2 x3 y3 x4 y4 class_name difficulty四个点按一定顺序给出difficulty为0表示容易目标1表示困难目标。训练YOLO-OBB之前需要转换成YOLO标签class_id x1 y1 x2 y2 x3 y3 x4 y4所有坐标归一化到[0, 1]四个点保持相邻顺序。核心转换代码可以参考下面的逻辑import numpy as np def dota_to_yolo_obb(line, img_w, img_h, class_map): parts line.strip().split() if len(parts) 9: return None coords np.array(list(map(float, parts[:8])), dtypenp.float32).reshape(4, 2) cls_name parts[8] if cls_name not in class_map: return None # 统一为逆时针顺序 coords order_points_ccw(coords) # 归一化坐标 coords[:, 0] / img_w coords[:, 1] / img_h # 越界截断到[0,1]如果截断后面积过小则丢弃 coords np.clip(coords, 0.0, 1.0) class_id class_map[cls_name] values [str(class_id)] [f{v:.6f} for v in coords.flatten()] return .join(values) def order_points_ccw(pts): # 计算质心再按极角排序 centroid pts.mean(axis0) angles np.arctan2(pts[:, 1] - centroid[1], pts[:, 0] - centroid[0]) return pts[np.argsort(angles)]这里的关键是order_points_ccw目的不是算术性极角排序而是保证同一个目标的所有框在整份数据集中都保持同一绕序。否则训练时模型学到的四个点语义是混乱的同一个角点在有些样本里是左上在另一些样本里变成了右上。转换时还要做两个过滤。第一短边小于图像尺寸2%的目标直接去掉这类目标在下采样后基本只剩几个像素标注本身也容易有偏差模型很难学会反而干扰训练。第二四个点围成的面积过小或者出现自相交的异常标注直接剔除这些大概率是标注错框。2.3 切图策略别让你的目标缩成像素点高分辨率航拍图动辄4000x4000甚至更大直接resize到1024训练会把目标缩到没法看。标准做法是切图也就是把大图切成一堆有重叠的小块。切图尺寸推荐1024x1024或者1280x1280重叠率一般在0.2到0.5之间。重叠区域的目的只有一个让跨切图边界的目标在至少一个图块里保持完整。切图时最关键的一个决策是被切断的目标怎么处理。我的做法是目标被切图框截断后如果剩余面积占原目标面积的70%以上就保留这个残缺目标如果低于这个阈值直接丢弃。原因是模型没必要学习识别一个只剩车灯的目标这会引入大量质量很差的训练样本。另一个经常被忽略的问题是数据集划分的泄漏。如果原图是一张4000x4000的大图切出的16张1024子图必须划分到同一个集合要么全部进训练集要么全部进验证集绝不能混分。否则验证集里会出现和训练集高度相似的同场景图块评估出来的mAP虚高上线后被打回原形。3. 模型选型与训练调参的实战细节3.1 用哪个YOLO做旋转检测YOLOv5-OBB还是YOLOv8-OBB目前主流的YOLO旋转检测路线就两条YOLOv5-OBBu版维护和YOLOv8-OBBultralytics官方支持。对比项YOLOv5-OBBYOLOv8-OBB角度处理方式回归一个弧度值预定义角度范围角度分类输出180类用DFL求期望损失函数KLD高斯KL散度近似旋转IoUProbIoU DFL调用方式clone仓库、自行配置环境yolo命令行一条龙文档与社区活跃度一般老项目沉淀多高更新频繁部署生态ONNX、TensorRT均有方案ONNX、TensorRT均有方案示例丰富YOLOv5-OBB的回归方式更接近传统做法模型直接预测一个角度值训练时用KLD作为近似旋转IoU的损失。这种方案有个天然痛点就是角度周期性问题难以彻底解决需要自己在数据加载和标签编码时做大量约束。YOLOv8-OBB把角度建模成一个180类的分类任务通过DFL得到最终角度。这在工程上是一个很聪明的设计角度分类天然规避了回归的周期性问题网络每学一点就是朝着正确的方向区间逼近而不是被±180度的数值突变干扰。因此对新项目我会直接推荐YOLOv8-OBB。3.2 训练参数调整的完整思路训练配置不要直接照抄系统默认值要根据航拍数据特点逐项调。预训练权重的选择上有两种做法。一是用ultralytics提供的yolov8m-obb预训练权重它已经在DOTA类数据上训过一轮迁移到自己的航拍数据上收敛快。二是用通用yolov8m.pt权重backbone和neck都已经学到了通用特征但最后的OBB头需要随机初始化。实测下来数据量少于5000张时用第一种mAP能高1到2个点。输入尺寸建议和切图尺寸一致。切图1024就用imgsz1024切图1280就用imgsz1280。强行把更小尺寸的图resize到大分辨率没有意义反而增加显存开销更小的imgsz会让目标分辨率不足。批次大小尽量拉满。OBB的角度分类头要学的东西比水平框多batch太小的情况下梯度噪声大训练过程容易抖动。我习惯8卡A100上batch拉到64单卡的话至少16起步再配合梯度累积。学习率用默认的warmupcosine策略即可但要注意如果batch从默认16改成更大值适当把初始学习率按比例调大否则收敛速度会变慢。一个可用的训练配置如下# obb_dataset.yaml path: /data/obb train: images/train val: images/val names: 0: car 1: ship 2: planeyolo train modelyolov8m-obb.pt dataobb_dataset.yaml \ epochs80 imgsz1024 batch16 device0数据增强方面要留意Mosaic。切图后的数据本身就包含大量小目标Mosaic拼接会让目标更小更碎片模型反而可能学到一堆残缺的局部模式。如果训练前期loss下降正常、val mAP却迟迟上不去优先检查Mosaic的开启比例可以尝试把Mosaic降到0.5甚至关闭。4. 训练中踩过的坑loss暴涨、小目标与收敛判断4.1 角度回环问题为什么loss会突然跳到几十旋转检测训练最吓人的现象就是loss曲线在前几个epoch突然飙升几十甚至上百都有可能。问题根子在角度表示上。旋转矩形有一个天然属性角度是周期性的。一个角度94度的旋转框和角度-86度的旋转框表示的是同一个矩形数值上却差了180度。如果用L1或Smooth L1做角度回归这两个等价表示之间会产生巨大损失模型会被梯度推着来回震荡。YOLOv8-OBB通过角度分类 DFL绕开了这个问题因为分类目标是一个区间索引94度和-86度映射到同一个类不存在周期跳变。但如果你还在用YOLOv5-OBB或者自己魔改的OBB头就必须在数据增强环节保持角度标签始终落在同一个区间同时把损失函数换成KLD或者GWD这类对角度周期性不敏感的度量。另一个loss突然暴涨的原因是梯度爆炸常见于batch里混入异常样本。标注严重错误的框会得到一个离谱的ProbIoU值梯度瞬间放大。遇到这种情况先检查用model.val()跑出的预测可视化找出具体是哪些样本触发的问题再回到数据清洗环节不要急着调学习率。4.2 小目标漏检切图不是万能的航拍目标检测躲不开小目标问题。一辆车在3000米高度拍摄的影像里往往只有几十个像素经过5次下采样到原图1/32后在最后的特征图上可能连两个像素都不到模型想学也学不到信息。提高imgsz是一个方向但代价很大。更有效的是走训练切图 推理切片的组合方案。训练阶段把原图切成1024的图块提高目标在输入图像中的相对占比推理阶段同样把大图切成多个有重叠的图块分别检测再把检测结果合并。合并结果时不能直接拼接必须对重叠区域产生的重复检测做一次全局旋转NMS否则同一个目标会在重叠区域被检测出两三次。推理切片的重叠率建议在0.25左右既保证目标完整性又控制计算量。如果你的目标尺寸特别小可以考虑在模型结构上增加一个P2检测头stride更小的特征图。YOLOv8的OBB架构扩展性不错但有代价P2头会显著增加显存占用训练速度也慢不少。数据量大、硬件又不富裕的情况下先老老实实把切图策略优化好比加头更划算。4.3 判断模型是否真正收敛别只看loss数值OBB的训练loss包含多个子项边界框回归损失、ProbIoU损失、DFL角度分布损失、分类损失。训练脚本里打印的总loss在epoch间波动完全正常尤其开启Mosaic和多尺度增强后每个batch的难度不同loss曲线会有锯齿状起伏。我判断收敛看的三个指标验证集mAP50和mAP50-95曲线是否趋于平稳而不是看训练集loss是否降到最低。训练集loss可以无限制降低但那通常是过拟合信号。PR曲线在低召回率段的表现。航拍场景很多目标非常小正样本数量多如果低召回率时precision就开始崩说明模型学到的是宁可错杀也不放过的激进策略需要调高置信度阈值或者回查数据标注质量问题。目视检查预测框是否贴住目标长边。模型可能mAP不错但很多框的长边方向反了90度这是因为标注数据里长边定义混乱。把预测结果可视化出来多挑几张密集场景看图比数值指标更能暴露问题。5. 推理后处理、格式转换与部署建议5.1 旋转NMS的实现要点推理阶段旋转框的NMS不能复用水平框逻辑。原因很简单两个旋转框之间的IoU计算复杂度远高于水平框而且角度周期性的存在让判断变得更加复杂。实现旋转NMS有两个层级的选择。简单方式借助shapely库的Polygon求交面积。代码简洁、正确性高但速度慢帧率要求不高的离线推理可以用。高效方式使用cv2.rotatedRectangleIntersection。它直接对RotatedRect结构做快速判定底层是OpenCV的优化实现适合部署到实时推理管线。实际工程里我推荐组合方案先用置信度排序对置信度高于阈值的框做快速过滤只对同样类别且中心距离接近的候选框计算完整旋转IoU。中心距离可以用L2距离阈值设为对角线长度的0.5倍左右可以在不损失精度的情况下大幅减少计算量。旋转NMS的IoU阈值一般设在0.5到0.7之间。密集航拍目标之间本身就有较高的物理重叠度阈值设得太高会把本应分开的目标吞掉。5.2 从结果到DOTA评测输出格式转换如果你的目标是提交到DOTA官方评测平台或者使用DOTA的评测协议来评估自建数据需要把模型输出转成DOTA格式。每行一条检测结果格式如下class_name confidence x1 y1 x2 y2 x3 y3 x4 y4注意两个容易踩的坑。第一DOTA官方评测是逐类别进行的先按类别把所有图片的检测结果聚在一起再统一按置信度排序并计算mAP。所以输出文件里图片间的顺序无所谓但每个类别的文件要合并正确。第二difficulty标签的问题。DOTA数据集中有difficult目标官方评测protocol默认不把它们算作真值。如果你用自己的验证集跑评测需要明确知道自己的评估脚本是否做了这个处理否则mAP会有偏差。如果只是自建数据验证强烈推荐直接使用ultralytics内置的val接口它会自动计算OBB格式的mAP50和mAP50-95并生成混淆矩阵和PR曲线省去自己写评估脚本的时间。5.3 导出与部署把模型塞进生产环境模型训练完成后部署的第一步是导出为ONNXyolo export modelbest.pt formatonnx opset12导出后的ONNX包括完整的OBB预测头但注意后处理需要自己实现。ONNX输出的原始张量是角度分类分布和回归量需要解码成旋转框坐标再做旋转NMS。如果部署到TensorRTFP16精度下基本不掉点可以放心使用。INT8量化要谨慎因为角度分类分支对量化误差比较敏感。我实测过一个模型FP16比FP32的mAP只掉了0.2个点INT8直接掉了2.5个点。如果一定要INT8校准集里必须覆盖不同方向角度的目标尤其是45度、135度这种斜向目标否则量化后角度预测会系统性偏移。在C端实现时推荐用OpenCV的RotatedRect结构和cv2.rotatedRectangleIntersection接口避免引入额外的几何库依赖。要注意把模型输出的归一化四点坐标先还原成图像像素坐标再做旋转框合并。最后再说一个实用细节航拍影像的地面采样距离是变化的同样的目标在不同高度的影像里像素尺寸完全不同。如果你的应用场景飞行高度不固定部署时千万不要固定一个imgsz最好做多尺度输入推理时根据影像分辨率动态选择切图尺寸这样比单尺度模型稳得多。