1. 遥感旋转目标检测到底在解决什么问题遥感图像里的目标检测和自然图像里那套东西乍一看是一回事实际落地的时候完全是两个世界。自然图像里你检测一个人、一辆车目标基本是正向站着的标注框水平一拉就完事。但遥感图像是从天上往下拍的飞机、舰船、油罐、桥梁这些东西的朝向是任意的你用水平框去框一艘斜着停的船框里会塞进大量背景框的面积和目标实际面积严重不匹配。这个问题在密集场景下会要命——港口里一排排斜向停靠的船只水平框之间重叠得一塌糊涂后处理阶段做非极大值抑制的时候要么把真正相邻的目标误删要么留下一堆冗余框。旋转目标检测的核心思路就是让检测框带一个角度参数通常表示为(x, y, w, h, θ)其中 θ 是框相对于水平方向的旋转角。这样一来框可以紧贴目标的外接矩形背景占比大幅下降密集场景下的区分度也上来了。CSL 这个方法全称是 Circular Smooth Label中文一般叫圆形平滑标签它要解决的是旋转检测里一个非常底层、非常容易被忽视的问题角度回归的边界不连续问题。我先把这个问题的来龙去脉讲清楚不然后面看 CSL 的设计会觉得莫名其妙。假设你用回归的方式直接预测角度 θθ 的取值范围通常是 [-90°, 0°) 或者 [-45°, 135°) 这种区间。问题在于角度是有周期性的-90° 和 0° 在几何上其实是同一个方向或者说相差 90° 的边界处存在突变。当一个目标的真实角度在边界附近比如 -89°而网络预测成了 0.5°从数值上看只差了不到 1.5°但从几何上看这两个角度对应的框方向差了将近 90°损失函数会给出一个巨大的惩罚网络就被这个突变搞得很难收敛。这就是所谓的角度边界不连续也叫角度周期性带来的回归困境。CSL 的做法很巧妙它不直接回归角度而是把角度预测变成一个分类问题并且用圆形平滑的方式处理标签。具体来说把 180° 的角度范围均匀分成若干个 bin比如 180 个 bin每个 bin 1°然后不采用 one-hot 硬标签而是用一个窗口函数在真实角度附近的 bin 上生成一个平滑的分布。这样做的直接好处是边界处的突变被平滑掉了网络在边界附近预测时不会因为一个 bin 的偏差就受到巨大惩罚。同时分类的形式天然规避了回归的连续性问题。这个方法适合谁看如果你正在做遥感图像的目标检测尤其是舰船、飞机、车辆这类方向敏感的目标或者你在做文本检测、工业缺陷检测这类同样存在旋转框需求的场景CSL 的思路都值得仔细琢磨。哪怕你不直接用 CSL理解它对角度问题的处理方式对你设计自己的角度表示方案也有很大帮助。下面我会从整体设计、核心细节、实操实现、问题排查几个层面把这个方法拆开讲透。2. CSL 方法的整体设计与思路拆解2.1 为什么放弃直接回归角度要理解 CSL 的设计动机得先看清楚直接回归角度到底卡在哪里。旋转框的表示通常有两种主流形式一种是 OpenCV 风格的(x, y, w, h, θ)θ 表示框的旋转角另一种是长边表示法用(x, y, w, h, θ)但约定 w 是长边θ 范围压缩到 [-90°, 0°)。不管哪种表示角度都存在周期性和边界突变。我用一个具体的例子说明。假设角度范围是 [-90°, 0°)一个目标的真实角度是 -89°另一个目标的真实角度是 -1°。从几何上看这两个框的方向其实很接近都接近水平但数值上差了 88°。如果网络对第一个目标预测成 -1°数值误差只有 88°但几何上框几乎转了个方向。反过来如果真实角度是 -90°预测成 0°数值上差了 90°但几何上这两个角度是等价的因为 -90° 和 0° 在周期意义上重合。这种数值和几何的不一致就是回归方法难以处理的根源。有人会说那我用正弦余弦来编码角度不就行了确实用(sin θ, cos θ)或者(sin 2θ, cos 2θ)可以消除边界突变因为正弦余弦是连续周期函数。但这样做又引入了新的问题网络预测的是两个连续值需要满足 sin²cos²1 的约束实际训练中这个约束很难严格满足而且从预测值反解角度时会遇到符号歧义。更关键的是正弦余弦编码在角度接近某些值时梯度会变得很小收敛速度受影响。CSL 的思路是绕开这些麻烦既然角度回归这么难那我干脆不回归了改成分类。分类的天然优势是每个类别是离散的、独立的不存在边界突变的问题——第 0 类和第 179 类在分类框架下就是两个不同的类网络学的是这个目标属于哪个角度区间而不是这个目标的角度数值是多少。但直接做 one-hot 分类也有问题角度是连续的相邻角度之间应该有关系one-hot 把这种关系切断了而且当真实角度落在两个 bin 中间时one-hot 标签会显得很武断。2.2 圆形平滑标签的核心思想CSL 的关键创新就在圆形平滑这四个字上。它不把标签做成 one-hot而是做成一个平滑的分布。具体做法是对于真实角度 θ在它对应的 bin 附近用一个窗口函数生成一个概率分布离真实角度越近的 bin 概率越高越远的越低并且这个分布在角度边界处是循环连续的。这个窗口函数通常用高斯函数或者三角形函数。以高斯为例假设角度被分成 n 个 bin真实角度落在第 k 个 bin那么第 i 个 bin 的标签值大致是 exp(-(i-k)²/(2σ²))然后归一化。σ 控制平滑的程度σ 越大分布越平缓网络学到的角度越模糊σ 越小越接近 one-hot边界问题又回来了。所以 σ 的选择是个需要权衡的超参数。圆形体现在哪里体现在处理边界的时候。如果真实角度在第 0 个 bin 附近那么第 n-1 个 bin 也应该获得一定的标签值因为角度是循环的。实现上就是把 bin 索引做模运算或者把窗口函数在角度轴上周期延拓。这个细节看起来小但正是它保证了角度边界处的连续性是 CSL 区别于普通平滑标签的关键。2.3 分类与回归的取舍分析CSL 把角度预测变成分类后整个检测头的设计也跟着变了。原本角度是一个回归分支输出一个标量现在角度是一个分类分支输出 n 维向量n 是 bin 的数量用交叉熵或者 KL 散度来算损失。这带来几个连锁影响我在实际项目里都踩过第一输出维度变大了。如果 n180角度分支的输出从 1 维变成 180 维参数量和计算量都上去了。对于轻量化模型这个开销不能忽视。我试过把 n 降到 90 甚至 60精度损失在可接受范围内但速度提升明显。第二推理阶段需要把分类结果转回角度。通常取概率最大的 bin 对应的角度作为预测角度或者用期望值的方式加权平均。取最大值简单但精度受 bin 粒度限制加权平均更平滑但计算稍复杂。实测下来在 bin 数量足够比如 180时取最大值和加权平均差别不大bin 少的时候加权平均更稳。第三损失函数的权重需要重新调。分类损失和回归损失中心点、宽高的量级不一样直接相加容易让某一项主导训练。我一般会给角度分类损失一个稍大的权重因为它对最终 IoU 的影响更直接。2.4 与其他角度处理方案的对比在 CSL 之前和之后还有几种处理角度问题的思路我简单对比一下方便你选型。方案核心思路优点缺点直接回归直接预测角度标量简单输出维度低边界突变难收敛正弦余弦编码预测 sin/cos 再反解消除边界突变约束难满足反解有歧义CSL角度分类平滑标签彻底规避边界问题输出维度高bin 粒度受限高斯热图角度用热图表示平滑可视化好后处理复杂峰值提取有误差角度二分把角度分成多个区间分别回归缓解边界问题区间划分需要先验从实际落地角度看CSL 的性价比是比较高的。它的实现不算复杂对现有检测框架的改动集中在检测头和损失函数上不需要大改骨干网络。而且它的思路清晰调试起来有章可循。如果你的场景对角度精度要求极高比如需要精确到 0.1°CSL 的 bin 粒度可能不够这时候可以考虑 CSL 加回归的混合方案或者用更细的 bin 配合插值。3. 核心细节解析与实操要点3.1 角度范围的确定与 bin 划分CSL 的第一步是确定角度的表示范围。这里有个容易混淆的点旋转框的角度定义和长边约定是绑定的。如果你用 OpenCV 的 minAreaRect它返回的角度范围是 [-90°, 0°)并且宽高不保证长边在前。如果你用长边表示法通常约定 w 是长边角度范围是 [-90°, 0°)但有些实现用 [-45°, 135°)。我建议在项目里统一用长边表示法角度范围取 [-90°, 0°)这样角度和宽高的对应关系明确不会出现同一个框有两种表示的情况。确定范围后bin 的数量 n 决定了角度分辨率。n180 对应 0.5° 的分辨率n90 对应 1°n360 对应 0.25°。bin 数量怎么选我的经验是看你的目标角度分布和精度需求。如果目标主要是舰船、飞机这类角度分布比较集中n90 通常够用如果目标角度分布很广或者你需要高精度角度n180 是稳妥选择。n 太大不仅增加计算量还会让每个 bin 的训练样本变少小样本 bin 容易学不好。我做过一个对比实验n 从 60 增加到 180mAP 提升大约 1.5 个点但再往上增加到 360提升不到 0.3 个点性价比很低。3.2 平滑窗口函数的选择与参数调优窗口函数是 CSL 的灵魂。常用的有高斯窗和三角窗。高斯窗的公式是label(i) exp(-(i - k)² / (2σ²))其中 k 是真实角度对应的 binσ 是平滑参数。三角窗则是线性衰减label(i) max(0, 1 - |i - k| / r)r 是窗口半径。两者都能用但高斯窗更平滑边界处的过渡更自然。我一般首选高斯窗。σ 的选择很关键。σ 太小标签接近 one-hot边界问题回归σ 太大标签过于平缓网络学到的角度模糊定位精度下降。经验值是 σ 取 bin 数量的 1/10 到 1/6 左右。比如 n180σ 取 18 到 30 之间。我通常从 σ20 开始调观察验证集上的角度误差和 mAP再微调。还有一个细节窗口函数需要做周期延拓。也就是说当 k 靠近 0 或 n-1 时窗口的另一端要绕回来。实现上计算 label(i) 时用min(|i-k|, n-|i-k|)作为距离这样边界处就自然连续了。这个细节不做的话边界附近的角度会学得很差我一开始就栽在这里验证集上边界角度的误差明显比中间角度大。3.3 损失函数的设计与权重分配CSL 的损失由几部分组成角度分类损失、中心点回归损失、宽高回归损失可能还有置信度损失。角度分类损失用交叉熵或者 KL 散度。因为标签是平滑分布而不是 one-hot用 KL 散度更合适它衡量的是两个分布的差异对平滑标签的处理更自然。KL 散度的公式是L_angle Σ label(i) * log(label(i) / pred(i))实际实现时为了避免 log(0) 的问题通常给 pred 加一个极小值。也可以用交叉熵的变体把平滑标签当作软标签来处理。权重分配上我的经验是角度分类损失的权重给 1.0 到 2.0中心点和宽高回归损失的权重给 1.0 左右。如果发现训练初期角度学得很慢可以适当加大角度损失的权重如果角度学好了但框的位置偏就加大回归损失的权重。这个需要根据训练曲线来调没有固定公式。另外正负样本的平衡也要注意。旋转检测里正样本通常很少目标占比小如果角度分类只对正样本算损失负样本不参与那角度分支的训练信号会很稀疏。我的做法是只对正样本算角度损失但通过调整采样策略保证正样本数量足够。如果正样本实在太少可以考虑对正样本做增强或者用 focal loss 的思路给难样本更高权重。3.4 检测头的结构设计CSL 的检测头需要在原有回归分支的基础上增加一个角度分类分支。以常见的 anchor-based 检测器为例检测头通常有分类分支前景/背景和回归分支中心点偏移、宽高、角度。加了 CSL 后回归分支里的角度部分被替换成分类分支。具体结构上我一般这样设计共享的卷积特征经过几层卷积后分成三个子分支——前景分类、框回归中心点和宽高、角度分类。角度分类分支最后接一个全连接层或者 1x1 卷积输出 n 维向量再过 softmax 得到概率分布。这里有个实操细节角度分类分支的特征最好和框回归分支有一定隔离因为两者的任务差异较大。我试过完全共享特征和部分隔离后者效果更好。具体做法是共享前几层后面分开或者用不同的卷积核。这个改动不大但对精度有稳定提升。4. 实操过程与核心环节实现4.1 数据准备与标注格式转换旋转检测的数据标注通常有几种格式DOTA 数据集用的是四边形四个点的坐标有些数据集用(x, y, w, h, θ)。CSL 训练需要的是(x, y, w, h, θ)格式所以第一步是格式转换。从四边形转(x, y, w, h, θ)的步骤先求四边形的中心点然后用最小外接矩形拟合得到宽高和角度。这里要注意长边约定如果 w h就交换 w 和 h同时角度加 90°或者减 90°取决于你的范围定义。转换后要检查角度是否落在 [-90°, 0°) 内不在的话做周期归一化。我写过一个转换脚本核心逻辑是这样的import numpy as np import cv2 def poly_to_rbox(poly): # poly: 8个值四个点的x,y pts np.array(poly).reshape(4, 2).astype(np.float32) rect cv2.minAreaRect(pts) (cx, cy), (w, h), angle rect # 长边表示法 if w h: w, h h, w angle 90 # 归一化到 [-90, 0) angle angle % 180 if angle 90: angle - 180 return cx, cy, w, h, angle这个脚本看着简单但有几个坑cv2.minAreaRect 返回的角度范围是 [-90, 0)但不同版本的 OpenCV 行为略有差异有的返回 [0, 90)。所以转换后一定要做归一化并且用可视化工具检查一批数据确保框和角度对应正确。我见过有人因为角度范围没统一训练出来的模型角度全反了。数据增强方面旋转检测对旋转增强很敏感。水平翻转、垂直翻转、90° 旋转都会改变角度需要同步更新角度标签。比如水平翻转后角度 θ 变成 -θ在 [-90, 0) 范围内。这些变换要小心处理搞错了标签就全乱了。我建议增强后随机抽几张可视化确认框和角度都对。4.2 标签生成的具体实现标签生成是 CSL 的核心环节。对于每个正样本 anchor需要生成角度分类的平滑标签。实现步骤如下第一步计算真实角度对应的 bin 索引。假设角度范围 [-90, 0)bin 数量 n那么k int((angle 90) / 180 * n) % n第二步生成平滑标签。用高斯窗σ 取经验值def generate_csl_label(k, n, sigma): labels np.zeros(n) for i in range(n): d min(abs(i - k), n - abs(i - k)) # 周期距离 labels[i] np.exp(-(d ** 2) / (2 * sigma ** 2)) labels labels / labels.sum() # 归一化 return labels第三步把标签分配给对应的 anchor。只有正样本才生成角度标签负样本的角度标签可以置零或者忽略。这里有个性能问题如果对每个正样本都循环 n 次生成标签当 n180 且正样本很多时速度会很慢。优化方法是预计算一个查找表因为 k 只有 n 种可能可以预先算好 n 个标签向量训练时直接查表。这个优化能把标签生成的时间降到可忽略。4.3 训练配置与调参记录我用一个中等规模的遥感数据集做过完整训练记录一下配置和调参过程供参考。骨干网络用的是 ResNet-50 加 FPN输入尺寸 1024x1024batch size 4显存限制优化器 SGD初始学习率 0.005动量 0.9权重衰减 0.0001。训练 24 个 epoch学习率在第 16 和 22 个 epoch 衰减 10 倍。角度 bin 数量 n180σ20角度分类损失用 KL 散度权重 1.5。中心点和宽高回归用 smooth L1权重 1.0。训练过程中的观察前 4 个 epoch 角度分类损失下降很快从 4.5 降到 1.2 左右第 5 到 12 个 epoch 下降变慢稳定在 0.8 附近后面基本在 0.6 到 0.8 之间波动。验证集上的角度误差平均绝对误差从初始的 15° 降到 3° 左右最终稳定在 2.5° 上下。调参时发现几个敏感点学习率太大0.01时角度分类损失震荡严重降不下来σ 太小比如 5时边界角度误差明显偏大σ 太大比如 50时整体角度误差都偏大定位精度下降。最终 σ20 是权衡后的选择。4.4 推理与后处理推理阶段角度分类分支输出 n 维概率分布取 argmax 得到 bin 索引再转回角度angle (k / n) * 180 - 90如果 bin 数量不够多可以加一步插值取概率最大的 bin 及其左右邻居用加权平均得到更精细的角度。这个技巧在 n90 时能提升约 0.5° 的精度。后处理主要是旋转 NMS。旋转框的 IoU 计算比水平框复杂需要用多边形相交面积。我一般用 shapely 库或者自己写多边形裁剪。旋转 NMS 的阈值通常比水平 NMS 稍高因为旋转框重叠本来就少。我一般设 0.5 到 0.6。还有一个细节CSL 预测的角度和框的宽高是解耦的可能出现角度对但宽高反的情况。虽然长边表示法在标签生成时已经统一了但网络预测时仍可能出错。我的做法是在后处理时检查如果预测的 w h就交换并调整角度保证输出的一致性。5. 常见问题与排查技巧实录5.1 角度预测全偏或全反这是最常见的问题通常有几个原因。第一角度范围定义和标签生成不一致。比如标签用 [-90, 0)但推理时按 [0, 180) 转角度结果全偏 90°。排查方法是拿几个训练样本手动算一遍标签再和代码生成的标签对比。第二数据增强时角度没同步更新。水平翻转、旋转增强后角度标签没改网络学到的就是错的。排查方法是关掉所有增强只用原图训练如果角度正常了就是增强的问题。第三长边约定没统一。标注时有的框长边在前有的短边在前标签生成时又做了交换导致角度混乱。排查方法是统计训练集里 w 和 h 的分布看看是否有大量 w h 的样本。5.2 边界角度误差大如果验证集上靠近 -90° 和 0° 的样本角度误差明显大于中间角度说明圆形平滑没做好。检查标签生成时的周期距离计算确保用了min(|i-k|, n-|i-k|)。另外检查 σ 是否太小太小的话边界处平滑不够。可以适当增大 σ或者在边界处用更大的窗口。还有一种可能是 bin 数量不够。n90 时每个 bin 2°边界处的量化误差就有 1°加上平滑的影响误差会更大。这种情况可以增加到 n180 或 360。5.3 训练不收敛或损失震荡角度分类损失震荡通常和学习率、batch size 有关。学习率太大时分类分支的梯度不稳定损失上下跳。可以降低学习率或者用 warmup 策略前几个 epoch 用很小的学习率让网络先适应。另一个原因是正负样本极度不平衡。如果正样本太少角度分类的梯度信号很稀疏损失下降慢且不稳。可以调整采样策略增加正样本比例或者用 focal loss 给难样本加权。还有可能是损失权重不合理。角度分类损失和回归损失量级差太多导致某一项主导。可以打印各项损失的值看看是否在同一个量级。如果差一个数量级以上就需要调整权重。5.4 推理速度慢CSL 的角度分类分支输出 n 维n180 时计算量不小。如果推理速度是瓶颈可以从几个方面优化降低 n 到 90 或 60用轻量化的检测头把角度分类分支的卷积核减少用 TensorRT 等推理加速工具。我做过一个测试n 从 180 降到 90mAP 只掉了 0.8 个点但推理速度提升了约 15%。如果对速度要求高这个取舍是值得的。5.5 常见问题速查表问题现象可能原因排查方法解决措施角度全偏 90°范围定义不一致手动算标签对比统一角度范围角度全反长边约定混乱统计 w/h 分布统一长边表示边界误差大周期距离没算检查标签生成代码用周期距离增大 σ损失震荡学习率太大打印损失曲线降学习率加 warmup收敛慢正样本太少统计正样本比例调整采样加 focal loss推理慢n 太大测各分支耗时降 n轻量化检测头5.6 几个容易被忽视的实操心得第一个心得训练初期先把角度分支单独训几个 epoch冻结其他分支让角度分类先学起来然后再联合训练。这样角度分支的初始状态比较好联合训练时不会拖后腿。我试过这个策略收敛速度比直接联合训练快 2 到 3 个 epoch。第二个心得验证时不要只看 mAP要单独统计角度误差的分布。mAP 对角度误差不敏感有时候 mAP 很高但角度误差很大实际使用时会出问题。我一般会画角度误差的直方图看看是否有系统性偏差。第三个心得如果数据集里某些角度的样本特别少可以在标签生成时对这些角度做额外的平滑或者用角度加权的采样策略。角度分布不均衡是旋转检测的常见问题不处理的话少数角度的精度会很差。第四个心得CSL 的 bin 索引和角度转换要写单元测试。这个转换逻辑看着简单但很容易在边界处出错。我写过一个测试遍历所有 bin检查转换后的角度是否在预期范围内以及往返转换是否一致。这个测试帮我抓到了好几个边界 bug。6. 方法扩展与场景迁移CSL 的思路不局限于遥感检测。任何存在周期性或边界突变问题的回归任务都可以借鉴这种分类平滑标签的做法。比如方向估计、姿态估计里的关节角度预测、文本检测里的倾斜框都有类似的角度边界问题。我在一个工业缺陷检测的项目里迁移过这个思路。缺陷的形状有方向性用水平框会框进大量背景改成旋转框后角度回归同样遇到边界问题。套用 CSL 后角度误差从 8° 降到 3° 左右效果很明显。迁移时主要改的是 bin 数量和 σ因为工业场景的角度分布和遥感不同需要重新调。还有一个扩展方向是把 CSL 和其他角度表示结合。比如用 CSL 做粗角度分类再用一个回归分支做精细角度修正。这样既有分类的稳定性又有回归的高精度。我试过这个混合方案在 bin 数量较少n60时精度能接近 n180 的纯 CSL但计算量小很多。最后分享一个我在实际项目里总结的小技巧如果你的场景角度范围不是完整的 180°而是集中在某个区间可以缩小角度范围把 bin 集中在这个区间内这样等效于提高了角度分辨率。比如舰船目标的角度主要集中在 [-30°, 30°]那就可以把范围设成 [-45°, 45°]bin 数量不变的情况下每个 bin 的角度跨度更小精度更高。这个技巧在特定场景下很管用但要注意范围外的目标会被截断需要根据实际数据分布来定。