
086、YOLOv8改进实战旋转框检测头设计与实现适配遥感图像与文本检测从一次翻车说起去年接了个遥感项目的活客户发来一堆卫星图像要求检测里面的飞机和舰船。我心想YOLOv8跑水平框不是手到擒来结果一跑好家伙停机坪上并排停着的飞机水平框直接糊成一团——两架飞机的框重叠了80%NMS一过直接干掉一架。客户看了结果问我是不是把飞机识别成了UFO。这就是水平框在密集场景下的死穴。遥感图像里飞机、舰船、车辆还有文本检测里的文字行都是长条形、任意朝向的。水平框的IoU计算在这种场景下就是个笑话——两个完全不重叠的物体因为水平框的矩形区域交叉IoU能给你算出0.5以上。后来我花了三周时间把YOLOv8的检测头改成了旋转框版本才算把项目救回来。旋转框检测的核心矛盾旋转框检测和水平框检测本质上差在哪儿就一个角度参数θ。水平框用(x, y, w, h)四个参数就能描述一个矩形旋转框得用(x, y, w, h, θ)五个参数。但就是这个θ带来了三个大坑角度周期性0°和180°在数学上差180度但在物理意义上代表同一个朝向。网络如果直接回归角度值梯度会在边界处剧烈震荡训练根本收敛不了。损失函数不连续水平框的IoU计算简单粗暴旋转框的IoU计算涉及多边形交并比可导性极差。你没法直接用旋转IoU做损失函数反向传播得想别的办法。角度定义歧义同一个旋转框可以用(θ)表示也可以用(θπ)表示。如果训练数据里标注不一致网络直接学废。检测头改造从四参数到五参数YOLOv8的检测头输出通道数是reg_max * 4其中reg_max是DFLDistribution Focal Loss的bin数量默认16。每个anchor点预测4个分布对应(x, y, w, h)的偏移量。改成旋转框输出通道要变成reg_max * 5多出来的一个通道给角度。但这里有个坑——角度不能直接用DFL做分布回归因为角度是周期性的DFL的离散化会破坏周期性。我试过把角度也拆成16个bin结果训练出来角度预测全在边界附近震荡。正确的做法是角度单独用一个分支直接回归连续值或者用分类回归的混合方式。我最终选的是sin/cos编码输出两个值(sinθ, cosθ)这样天然解决了周期性问题。网络输出层改成# 旋转框检测头输出通道配置self.angle_branchnn.Conv2d(reg_max*5,2,1)# 输出sinθ和cosθ# 注意这里踩过坑不能把角度和位置放在同一个卷积里输出# 位置用DFL角度用sin/cos两者损失函数不同分开处理更稳定推理时通过atan2恢复角度值。但有个细节——atan2的输出范围是[-π, π]而旋转框通常定义在[-π/2, π/2]或[0, π]之间。这里需要做一个范围映射我习惯映射到[0, π)因为遥感图像里物体没有方向性0°和180°的框是一样的。损失函数绕不开的旋转IoU旋转框的损失函数是最大的坑。直接套用水平框的CIoU损失角度误差会被淹没在位置误差里。我试过几种方案方案一纯角度损失L L_xywh λ * L_angleL_angle用SmoothL1计算角度差。但问题来了——角度差怎么算直接算|θ_pred - θ_gt|遇到0°和179°这种边界情况差值179度实际只差1度损失直接爆炸。方案二sin/cos损失L_angle (sinθ_pred - sinθ_gt)^2 (cosθ_pred - cosθ_gt)^2这个方案解决了周期性但sin/cos的梯度在角度接近0或π时很小收敛慢。方案三旋转IoU GWD这是目前工业界的主流方案。GWDGaussian Wasserstein Distance把旋转框建模成二维高斯分布用Wasserstein距离近似旋转IoU。好处是可导、连续、无边界问题。我最终用的是GWD 角度辅助损失的组合defgwd_loss(pred_boxes,target_boxes):# pred_boxes: [x, y, w, h, sinθ, cosθ]# 别这样写直接用角度算IoU梯度会消失# 正确做法转成高斯分布参数mu_predxy_to_mu(pred_boxes[:,:2])sigma_predwh_angle_to_sigma(pred_boxes[:,2:4],pred_boxes[:,4:6])mu_gtxy_to_mu(target_boxes[:,:2])sigma_gtwh_angle_to_sigma(target_boxes[:,2:4],target_boxes[:,4:6])# Wasserstein距离w_disttorch.norm(mu_pred-mu_gt,dim1)\ torch.trace(sigma_predsigma_gt-2*torch.sqrt(sigma_pred sigma_gt))# 归一化到[0,1]gwd1/(1w_dist)return1-gwd权重设置上GWD损失占0.7角度辅助损失占0.3。角度辅助损失用sin/cos的MSE但只对角度误差大于15°的样本计算避免小角度扰动干扰主损失。数据加载标注格式的坑旋转框的标注格式五花八门。DOTA数据集用四个角点(x1,y1,x2,y2,x3,y3,x4,y4)HRSC2016用中心点长宽角度ICDAR文本检测用四个顶点。统一格式是第一步。我写了个标注转换器把所有格式统一成(x_center, y_center, width, height, angle)defpolygon_to_obb(polygon):# polygon: 8个值 [x1,y1,x2,y2,x3,y3,x4,y4]# 这里踩过坑直接用minAreaRect会丢失方向信息# 正确做法计算最小外接矩形但保留原始朝向rectcv2.minAreaRect(np.array(polygon).reshape(4,2).astype(np.float32))centerrect[0]sizerect[1]anglerect[2]# 注意OpenCV的角度范围是[-90, 0]需要映射到[0, 180)ifangle-90:angle180return[center[0],center[1],size[0],size[1],angle]数据增强也要小心。随机旋转增强时标注框的角度要跟着转不能只转图像不转标注。Mosaic增强时四个子图的旋转框要分别变换到拼接后的坐标系角度保持不变。训练技巧从崩到稳第一次训练旋转框检测头loss直接飞到NaN。排查了半天发现是角度分支的初始化问题。角度分支的权重如果初始化太大sin/cos输出接近1atan2恢复的角度全是45°梯度直接爆炸。解决方案角度分支的卷积层用零均值、小方差的初始化偏置设为0。同时给角度损失加一个warmup策略——前1000个iterations角度损失权重从0线性增加到0.3让网络先学好位置再学角度。另一个坑是学习率。旋转框的角度预测对学习率极其敏感lr稍微大一点角度就在0°和180°之间反复横跳。我最终把角度分支的学习率设为主干网络的0.1倍用不同的参数组optimizertorch.optim.AdamW([{params:backbone.parameters(),lr:1e-4},{params:neck.parameters(),lr:1e-4},{params:head.parameters(),lr:1e-4},{params:angle_branch.parameters(),lr:1e-5},# 角度分支用小学习率])NMS后处理旋转框的专属逻辑水平框的NMS直接算IoU旋转框的NMS得算旋转IoU。直接算多边形交并比太慢一张图几千个框算一次NMS要好几秒。优化方案先用水平框的IoU做粗筛把IoU小于0.3的框直接放行只对IoU大于0.3的框算旋转IoU。这样90%的框不需要算旋转IoU速度提升10倍。旋转IoU的计算用shapely库别shapely在GPU上跑不了推理时CPU计算太慢。我手写了一个基于三角剖分的旋转IoU计算函数用C写了个CUDA kernel速度比shapely快两个数量级。如果不想写CUDA可以用OpenCV的rotatedRectangleIntersection函数虽然慢点但精度够用。实际效果与调参经验在DOTA数据集上改进后的YOLOv8旋转框版本mAP比水平框版本高了12个点尤其是在密集停放的飞机场景mAP从0.63提升到0.81。文本检测场景ICDAR2015上的F1-score从0.72提升到0.85。但有个问题——推理速度慢了30%。主要是因为旋转IoU计算和角度分支的额外计算量。如果对速度有要求可以考虑把角度分支量化到INT8或者用知识蒸馏把角度预测能力蒸馏到轻量级网络里。调参经验总结几条角度范围定义要统一。我见过最坑的标注同一个数据集里有的标注用[-90, 90]有的用[0, 180]还有用[-180, 180]的。训练前必须统一否则网络直接学废。长宽比大的物体更容易学偏。像舰船这种长宽比超过5:1的物体角度稍微偏一点IoU就掉很多。对这些物体可以加大角度损失的权重或者在数据增强时多做一些小角度旋转。不要迷信旋转IoU。GWD虽然好用但在某些极端情况下比如长宽比接近1:1的物体GWD和真实旋转IoU的差距很大。这时候可以混合使用GWD和旋转IoU用旋转IoU做验证GWD做训练。一点个人建议旋转框检测在工业界的需求越来越大不只是遥感和文本工业质检、自动驾驶的停车位检测、医学影像里的细胞朝向分析都会用到。但说实话YOLOv8改旋转框这件事坑比想象的多。如果项目时间紧建议直接用现成的旋转框检测框架比如Oriented R-CNN或者S2ANet。如果非要自己改做好心理准备——至少预留两周的调参时间。最后说一句别在角度回归上用L1 Loss血的教训。