搞CV的应该都有这种经历手里攥着几百张无人机航拍图里面全是朝向乱七八糟的车、房顶、还有带角度排列的太阳能板。第一版用普通YOLO系列跑mAP看起来还挺像回事结果一放大可视化预警框恨不得把半条街都包进来——两辆车斜着挨在一起就变成一个框墙是斜的框却是正的。后来把框改成带角度的旋转框整个检测结果一下就清爽了。这篇就顺着“Yolov11旋转目标检测”这个项目把从原理拆解到训练部署的完整过程捋一遍。内容会覆盖为什么普通框不够用、YOLOv11哪些组件值得留下、角度回归怎么加、数据集怎么处理、训练和推理有哪些坑。既适合刚接触旋转检测、被各种“加一个角度分支”说法绕晕的入门者也适合已经跑通普通YOLO、准备把项目升级成旋转框方案的开发者读完可以直接对着配置去改。1. 为什么非得做旋转目标检测1.1 常规框在遥感与无人场景里的尴尬先说一个最简单的例子。公路上停着两辆车一辆朝东一辆朝西偏南。普通目标检测给它们各画一个矩形框这两个矩形框除了本身的面积之外还会把车头车尾之间一大块空地也算进目标区域里。两辆车只要停得近一点NMS一跑两个框的交并比直接超过阈值其中一个框就被干掉了最后只留下一个框同时罩住两辆车。在航拍、卫星遥感、无人机巡检这类俯视场景里目标朝哪的都有。建筑物有朝向船只停泊有方向车辆更不用说。普通框对角度完全无感结果就是框内背景像素占比高特征被稀释分类精度下降。多个斜向目标之间框重叠严重NMS误杀率高。检测框和地面真实框之间的IoU上限很低就算位置预测完美指标也上不去。所以旋转目标检测解决的核心问题就是让检测框跟着目标的实际走向去倾斜。倾斜框的英文叫OBBOriented Bounding Box也叫旋转框、有向框。本质上它比普通框多了一个角度参数模型除了要回答“目标在哪、是什么”还得回答“目标朝向哪”。1.2 旋转框到底多出了一维什么普通目标检测的回归量是(x, y, w, h)旋转目标检测最常用的表示是(x, y, w, h, θ)多出来的θ就是角度。这个角度通常描述矩形长边与水平x轴的夹角。别小看这一维它带来的连锁改动是系统性的检测头要多输出一个通道。损失函数要处理角度的周期性。后处理NMS不能用普通矩形的IoU要计算旋转矩形的相交面积。可视化代码要支持四角点坐标画图。数据标注格式也要从四角点或水平框变成带角度的五参数。如果你只用过YOLOv5、YOLOv8这样的标准检测框架可能觉得加一个角度就是“多一个输出头”而已。真调过的人会知道角度这一维对训练稳定性的杀伤力远大于前四维。角度差一两度框的形态可能完全变样特别是在宽高比接近1的目标上角度直接就变成“脱缰的野马”。1.3 这个改造适合哪些任务和人群旋转目标检测在以下几类场景里是刚需遥感图像目标检测飞机、船只、油罐、建筑物、储油库。无人机航拍车辆计数、道路设施巡检、光伏板朝向检测。工业质检零件方向不固定的流水线检测。医疗图像细胞、病变区域方向分析。无人驾驶鸟瞰视角车辆朝向预测、停车位识别。人群方面我默认你是用过YOLO系列工具、能跑通训练流程的开发者。如果完全没接触过目标检测建议先把普通YOLO跑一遍再回来看这篇。本文选择的底子是YOLOv11但你理解清楚思路之后换成YOLOv8甚至YOLOv5也能改出来。2. YOLOv11结构里哪些地方要为角度“让步”2.1 先在主干和颈部里拆出原装部件YOLOv11相比YOLOv8的改动不算天翻地覆最主要的几点主干和颈部大量使用C3k2模块把C3模块和跨阶段连接做了融合BottleNeck更轻量。引入C2PSA模块在深层特征里加入注意力机制但只在高层用控制计算量。分类和回归头解耦得更彻底回归头用卷积输出的连续值。正负样本分配沿用TaskAlignedAssigner思想按分类得分和IoU联合打分。这些设计在旋转检测改造里基本都可以保留。因为旋转检测增加的复杂度主要在“怎么输出角度”和“怎么算损失”主干特征提取不关心你最后框是不是倾斜的。我不建议为了旋转检测去魔改主干网络老老实实用原装C3k2和C2PSA就行。有人可能会问直接用带注意力机制的主干是不是对旋转小目标更好我的观点是注意力能帮一点但遥感小目标的主要矛盾是“边框噪声大、样本少”多花算力在注意力上不如多花在数据增强和标签质量上。2.2 检测头的改动回归分支加角度的成本核算既然决定保留主干和颈部改动焦点就落在检测头上。原本的检测头分类分支输出A×C个类别分数A是anchor数量C是类别数回归分支输出A×4个坐标值。旋转检测需要把这4改成5多出的1就是角度。更细一点说YOLOv11的解耦头在最后的卷积层上分了两个方向一个算分类一个算回归。我们只需要把回归分支的输出通道从4改成5并且在损失计算时多算一个角度损失。这个改动的计算量增加几乎可以忽略因为只是多一个输出通道。真正的增加发生在损失计算和后处理损失计算时旋转IoU的计算比水平IoU复杂很多。如果采用旋转框NMS每两个框之间要先求旋转矩形的交点再算多边形面积。角度回归如果使用Smooth L1要处理角度周期性问题如果使用基于高斯分布的KLD或GWD计算复杂度再上一个台阶。所以别看网络结构只改了“4到5”整个训练耗时和显存占用可能比普通检测高出20%到40%大头全在后处理。2.3 角度表示法与边界周期性的坑现在市面上旋转检测的角度表示法五花八门常见的有五参数法(cx, cy, w, h, θ)。用中点、宽高和长边与x轴夹角表示。八参数法用四角点(x1, y1, x2, y2, x3, y3, x4, y4)表示优点是支持任意四边形缺点是回归难度大、参数冗余。长边法与短边法五参数里θ具体指哪条边不同数据集规定不同。我最推荐的是五参数法。参数少、直观、和损失函数配合方便。真正决定训练质量的是θ的取值范围和周期性处理。角度周期性这个问题很典型。假设角度定义域是[-90°, 90°)那么一个朝向89°的框和朝向-89°的框其实只差2°但如果用绝对值做回归误差就是178°Smooth L1会把梯度拉得特别大模型完全没法收敛。解决办法有两类换损失函数。用KLD或GWD这类基于分布距离的损失天然兼容角度周期性。换角度表示。把单角度变成sinθ和cosθ两个连续值让网络回归二维向量而不是一维角度。或者用分类回归结合把角度粗分为若干区间再在区间内回归细角度。我实际用下来KLD损失在遥感场景里表现最稳收敛速度快对宽高比接近1的目标也不容易乱转。GWD精度差不多但KLD在数学推导上更干净函数库实现也现成。2.4 损失函数怎么搭配YOLOv11原始损失由三部分组成分类损失、回归损失和DFL损失。改造之后我的做法是分类损失保留用BCEWithLogitsLoss和原版一致。回归损失把普通的CIoU换成旋转框对应的损失。最稳的方案是直接用KLD损失因为KLD只要求“两个分布之间的散度”不需要显式计算旋转IoU训练稳定。角度损失不要单独给一个大权重。经验上角度损失权重设置为总回归损失的0.5到1倍就够了太大了早期训练阶段框乱转。还有一点如果计算资源允许也可以直接计算旋转IoU作为辅助损失。但旋转IoU需要求解两个凸多边形的交集面积这个操作用GPU实现时经常被卡住要么就写成不可导的近似。我在实际项目里一般不参与训练只用它做后处理评估。一个很脏但有用的技巧训练时把角度回归值规范化到[-π/4, 3π/4)这种“半开半闭”区间等价于把目标的对称性合并掉。因为一个旋转矩形框转180°和原来框是同一个框。端点重合问题在数据增强、可视化、指标计算时都要注意统一。3. 数据标注与训练集准备3.1 数据集选型和标注格式差异旋转目标检测的数据集和普通目标检测有差异。目前常用的大型遥感数据集标注多是四点角标格式也就是每行8个坐标值按顺时针排列。比如DOTA系列数据集的格式是x1 y1 x2 y2 x3 y3 x4 y4 class_name这八个点按顺序首尾相连就是一个任意四边形。官方标注里其实允许不是矩形但实际绝大多数标注都是接近矩形的四边形。最终处理时统一转换成五参数四个点可能被你当成1、3、2、4这样连成交叉四边形的情况要专门处理。如果你不想用公开数据集自己标注有几个好用的工具CVAT支持旋转框标注导出格式可以配置。X-AnyLabeling国产的开源标注工具支持旋转框输出启动快。LabelImg的旋转扩展版除非你只标少量样本否则不推荐因为它导出格式比较乱。标注工作量上旋转框比水平框至少多30%的操作成本因为每标一个框都要调整角度。建议优先标注小目标和高密度区域这些区域是旋转检测优势最大的地方。3.2 从四点角标到五参数cxcywhtheta拿到四点角标后要转成(cx, cy, w, h, θ)才能喂给模型。转换逻辑并不复杂但有很多细节。假设四个点是p1(x1, y1)、p2(x2, y2)、p3(x3, y3)、p4(x4, y4)已经按顺时针排好。那么第一步是计算四条边的长度edge1 p1 p2 长度 edge2 p2 p3 长度 edge3 p3 p4 长度 edge4 p4 p1 长度矩形的长边应该是max(edge1, edge2)短边是min(edge1, edge2)。中心点就是四个点的平均值cx (x1 x2 x3 x4) / 4 cy (y1 y2 y3 y4) / 4角度θ定义通常是长边与x轴的夹角。取p1到p2和p3到p4中较长的那个方向作为长边方向然后计算这个方向向量和水平向右方向的夹角。用atan2算最稳θ atan2(v_y, v_x)维度对应关系w取长边长度h取短边长度。但前提是你在模型里定义θ对应的是长边。如果数据集定义的角度是短边夹角你需要做90°的归一化修正。这个“到底是长边夹角还是短边夹角”的坑最后会导致同一模型在不同数据集上表现几乎一样但在可视化时发现框全部转了90°千万别忽视。另一个细节有些数据集的θ范围是(0°, 180°]有些是(-90°, 90°]有些是(-180°, 180°]转换代码统一用atan2算完以后再加上归一化就行。归一化逻辑我通常写成def normalize_theta(theta, angle_min-np.pi/4): while theta angle_min: theta np.pi / 2 while theta angle_min np.pi: theta - np.pi / 2 return theta这里的核心想法是矩形框转180°等价于原框所以角度其实可以折半周期处理。取值区间只要保证任一角度的旋转框都有唯一的θ值表示即可。3.3 数据增强里最容易忽略的角度一致性普通检测的数据增强很随意Mosaic、随机裁剪、HSV变换随便上。但旋转检测多了角度这个维度后增强操作必须和角度同步随机旋转增强负责角度这个维度的鲁棒性。但注意旋转角度可能让目标转出有效角度范围。水平和垂直翻转翻转之后θ会变。水平翻转后θ应该变成π-θ垂直翻转后θ变成-θ。如果你用的框架里没有这项自动同步模型会学到一堆错误角度样本。裁剪裁剪可能把目标截断。旋转框检测对截断很敏感如果裁剪破坏了角度信息建议降低裁剪强度或者改用缩放增强。我实操下来旋转检测训练里不要直接套用普通检测那套mosaicmixup的默认策略容易出现两个问题小目标被拼图增强切割后角度信息丢失。拼接后的角度边界混在一起让模型误以为目标是跨图拼接的倾斜对象。我推荐的增强组合是大尺度随机旋转±30°到±45°、轻微缩放、HSV扰动。mosaic可以用但强度调低或者只在浅层特征图做。4. 从yaml配置到训练落地的完整路径4.1 修改模型配置的最小改动方案现在假设你手上已经有官方YOLOv11代码那么改造流程大概是这样的。这个流程我自己在类似规模的项目里验证过你可以直接照着做。第一步修改模型配置文件里的输出通道。以YOLOv11的中型模型为例检测头的回归输出通常在模型结构里写成head: - [Conv, 256, 3] - [Conv, 256, 3] - [Detect, 5, 4] # 这里原本是4改成5其实在YOLOv11原版代码里检测头输出通道是根据reg_max和类别数动态计算的。你要做的是在检测头模块里把坐标回归分支的输出从4改成5。具体代码位置看head.py里的Detect类会有一个assert reg_out 4这种检查去掉并改成5同时在forward里把输出张量维度从4改成5。第二步修改loss计算。在模型损失函数里把坐标回归部分从CIoU改成KLD损失或者直接加上角度损失项。最省事的方式是把角度损失写成一个单独的函数def kld_loss(pred, target, eps1e-7): # pred: (batch, 5) cx, cy, w, h, theta # target: (batch, 5) cx, cy, w, h, theta ... return loss.mean()KLD的具体公式网上能搜到很多版本核心思想是把每个旋转框看成一个二维高斯分布然后计算两个分布之间的KL散度。这样做的好处是不需要显式计算旋转IoU的不可导操作整个loss全程可导训练稳定。第三步修改后处理相关函数。YOLOv11原版的nms是针对水平框的。旋转框不能用普通IoU要用以下三种方案直接用shapely库里的intersection计算多边形交集面积。用OpenCV的rotatedRectangleIntersection。自己写基于扫描线的交集算法。我最常选的是opencv方案速度快、稳定性好直接用numpy就能接上不引入额外依赖。第四步修改标签解码逻辑。原始代码在计算目标框坐标时用的是xywh解码旋转框时要额外多出一个角度。同时解码时的角度要经过一次归一化保证预测的张量映射回图像坐标时角度在正确的范围内。这套最小改动方案的好处是主干、颈部、数据加载、大部分训练流程都不动只改检测头和损失计算出问题好排查。4.2 训练超参数的推荐起点我自己在遥感数据集上跑YOLOv11旋转检测时初始参数是这么定的输入尺寸1024×1024。遥感图像原始尺寸往往超过4000像素直接缩放肯定会丢小目标信息。建议强于先tile切图再用1024输入。Batch Size显卡显存不够时优先减batch不要减输入分辨率。目标太小的时候减分辨率等于直接放弃检测。初始学习率0.001搭配余弦退火。旋转检测的loss比较峭初始学习率太高会产生震荡。Momentum0.937与YOLOv8一致。Weight Decay0.0005。训练轮数300轮左右前50轮做warmup。在遥感数据上收敛速度比普通检测慢因为角度维度需要更多epoch去拟合角度分布。混合精度默认开启但损失计算涉及角度时要小心FP16精度溢出。正负样本分配TaskAlignedAssigner在原版就支持旋转框只要把GT和预测的IoU计算改成旋转IoU其他不用动。一个容易忽略的细节角度回归分支的loss权重不要设置过高。我推荐在0.5到1.0之间如果训练曲线里cls和box都下降、只有angle不下降那就是权重设大了压过了空间回归。4.3 训练过程的监控指标怎么读训练时的监控指标和普通检测有一点不同。普通YOLO你主要看box_loss和cls_loss旋转检测要额外盯角度相关损失。建议每次训练输出如下指标total_loss综合损失。cls_loss分类损失下降趋势和普通检测一致。box_loss位置回归损失这里包含了KLD或者旋转框损失。angle_loss如果单独拆出来随时看它是否在震荡。训练20轮之后如果angle_loss反复横跳不下降大概率是周期性处理出问题而不是学习率问题。做法去检查θ归一化是否统一看看真实框标注里角度分布是否出现了跳变点。此外给训练集写一个可视化脚本每5个epoch输出一批预测框和标注框的对比图。这个脚本在旋转检测里比普通检测更关键因为角度错误无法只通过数值看出来必须看图。5. 推理后处理与成本控制5.1 旋转NMS的正确姿势后处理的第一步是解码。模型输出的数量和通道数是(batch, num_anchors, 5C)其中5是cx、cy、w、h、θ。解码时theta pred[..., 4] theta atan2(sin(theta), cos(theta)) # 归一化到稳定区间由于模型回归的是sin和cos的组合直接输出的θ可能不在有效范围必须经过atan2处理。解码完成后就是NMS。旋转NMS的逻辑和普通NMS一样按置信度排序取最高分框删除与它IoU大于阈值的框。唯一差别是IoU计算变成旋转IoU。rotated IoU计算的经典实现是用OpenCV的cv2.rotatedRectangleIntersectionimport cv2 import numpy as np def rotated_iou(box1, box2): rect1 ((box1[0], box1[1]), (box1[2], box1[3]), box1[4]) rect2 ((box2[0], box2[1]), (box2[2], box2[3]), box2[4]) intersection, _ cv2.rotatedRectangleIntersection(rect1, rect2) if intersection is None or intersection.shape[0] 2: return 0.0 inter_area cv2.contourArea(intersection) area1 box1[2] * box1[3] area2 box2[2] * box2[3] union area1 area2 - inter_area if union 0: return 0.0 return inter_area / union注意这里的框格式是(cx, cy, w, h, angle)angle单位是度。在yolov11的内部张量中角度单位是弧度转换时别搞混。旋转NMS在CPU上跑比较慢。实际部署时我建议先用一个小的水平框预筛只对置信度前100~200的框做旋转IoU计算把计算量压住。遥感大图整张输入往往有几万anchors全量旋转IoU寸步难行。5.2 推理时角度归一化与坐标还原推理时经常被忽略的一个环节是坐标还原。训练时输入分辨率是1024×1024输出框的cx、cy、w、h、θ相对于网络输出分辨率。要还原到原始高分辨率图像上需要把特征图尺度乘以strideYOLO的decode尺度再除以原图缩放比。还原时宽高直接乘缩放比角度不变。切图推理时还要把每个tile的坐标加上tile左上角偏移。角度还原看似不起眼但在有大图切片的场景里特别容易出错。我踩过最惨的坑是tile重叠区域的目标框被预测了两次一张图在接缝处同时出现两个角度的同一辆车。解决办法有两个一是推理时去掉低置信度的重复框二是切图时做overlap然后在拼接后做全局NMS。另外注意旋转框的下采样过程。模型输出的框相对于输入图的分辨率是有stride缩放的。你如果直接拿(cx, cy, w, h)乘上stride位置是准的但角度一定不要乘stride。角度是独立维度和空间缩放无关。5.3 模型体积和精度取舍YOLOv11的中型模型在旋转检测任务上体积会比普通检测略大因为输出通道从4变成5。但差异很小主要增大来自训练时的中间变量而不是最终权重。如果你要部署到边缘设备有几个建议导出ONNX时把detect头的解码和NMS放到外部做。用INT8量化时注意角度回归分支的量化误差。角度分支的输出范围比坐标分支小量化时容易丢精度。TensorRT部署时旋转IoU没有内置算子只能回到CPU跑。务必控制候选框数量。精度方面旋转框相比普通框在遥感场景里通常能提升3到8个百分点尤其在车辆、船只、飞机这类长条形小目标上提升最明显。代价是训练速度下降、NMS耗时上升、调试复杂度增加。6. 踩坑实录与实战排查表6.1 训练Loss不降或角度震荡我在实际项目里遇到过最典型的问题就是训练前30轮total loss稳步下降后面开始震荡尤其angle_loss像心电图一样跳。排查思路按顺序来先看θ的归一化是否在数据加载、模型输出、损失计算三处保持一致。再看训练集的角度分布如果大部分目标都朝一个方向极少数目标朝另一个方向模型对少数角度的学习能力会特别差。看角度回归分支的输出范围。如果模型输出的是无约束的连续值加一个tanh约束到[-π/2, π/2]收敛会快很多。如果用了KLD损失检查一下高斯协方差矩阵构建是否正确不要写成对称形式。一个非常实用的技巧把角度分支的权重先用0.1跑30轮让模型先把位置和分类学好之后把权重提高到1.0继续训练。这种两段式训练在旋转检测里非常有效能明显降低前期角度震荡。6.2 框颠倒、角点顺序错乱标注数据转换时如果四角点顺序不是顺时针会出现框“角点交叉”问题。拿8个坐标点直接连线时如果点的顺序错误转成五参数时宽高会出现负值或异常值。解决办法是在数据预处理里加一个凸包检查。转换前先判断四边形的四个顶点能否构成凸四边形且按顺时针排列。只要发现角点交叉重新排序即可。还有一个问题是旋转框的角度范围定义不同。有的数据集用[-90°, 90°)有的用(0°, 180°]。如果你的模型输出和评估代码的角度定义不一致就会出现在训练指标很好、可视化时框全部转90°或者颠倒的现象。统一用long-side定义angle范围设为[-90°, 90°)并保证评估脚本里同样使用该定义症状就会消失。6.3 密集小目标的误检和漏检遥感图像里密密麻麻的小目标旋转检测也怕。我遇到最多的就是大量相近角度、相近位置的框互相压制。调整策略把NMS的IoU阈值从0.5提高到0.7甚至0.75减少对密集目标误杀。如果密集目标角度差异很大可以试试按角度分组做NMS先按30°间隔分桶桶内做NMS再把置信度最高的一组保留。这个方法在车辆密集场景里效果还不错。小目标本身信息量少角度预测容易受到背景纹理干扰。建议在训练的输入预处理里多裁剪一些只有背景的负样本让模型更好地学习“空区域”的置信度输出。过高的回归损失权重在小目标上反而放大贴图噪声。6.4 快速排查清单我把里面的经验压缩成一张速查表适合训练过程中排查现象最可能的原因快速处理方案angle_loss 不降反升角度周期性处理不一致检查归一化区间统一用[-90°, 90°)框总是转90°或颠倒角度定义长边/短边不一致统一长边定义调整转换代码检测框严重重叠NMS阈值过低提高IoU阈值到0.7密集小目标漏检输入分辨率不足用1024×1024输入或做切图训练震荡、不收敛角度损失权重过大两段式训练前期降权重内角交叉、框变形四点标注顺序错误加凸包检查并重排顶点推理结果错位坐标还原时漏乘缩放比统一坐标归一化逻辑大图推理慢全量旋转NMS先水平框预筛再做旋转NMS最后分享一个我自己的经验做旋转目标检测不要一上来就追求改出新网络结构。先把YOLOv11的主干和颈部沿用原版把角度引入检测头和损失函数跑通一条完整链路。等这个链路稳定之后再逐步考虑更复杂的角度损失、轻量化主干、或者使用检测头增强。稳定优先于炫技尤其在旋转检测这种本身就要处理更多不稳定变量的任务里。