☰
旋转目标检测后处理:实宽高、虚宽高与对齐约束详解
2026/10/7 12:49:09 网站建设 项目流程

做检测的同行应该都有过这种体验:模型在公开集上刷分很漂亮,一到自己的业务数据上,文本框稍微倾斜、长宽比再极端一点,后处理的阈值直接失灵,不是误杀就是漏检。前两篇我们把 RGA(Region Geometry Alignment,区域几何对齐)的基础流程和特征组织讲完了,这一篇专门聊一个容易被人忽略、但实际调参时绕不开的痛点——实宽高、虚宽高与对齐约束。

这三个词放在一起,本质上是在回答一个问题:检测框落地之后,你拿什么标准去衡量它“像不像一个真实的目标边界”。实宽高是坐标直接算出来的,虚宽高是语义上真正有意义的尺寸,对齐约束则负责把它们之间的偏差量化成可以优化的指标。很多人调了半天阈值,其实调的根本不是参数,而是这两个宽高概念之间的换算关系。这篇文章就把这套逻辑拆开讲清楚,该给公式的地方给公式,该给代码的地方给代码,末尾附上我实际跑数据时踩过的坑。

1. 实宽高与虚宽高:两个容易混淆的几何量

1.1 实宽高:从坐标直接算出来的“盒子尺寸”

实宽高很好理解,就是检测框四个顶点坐标直接算出来的最小外接轴对齐矩形的宽度和高度。假设一个旋转文本框的四个顶点坐标分别是 (x1,y1)、(x2,y2)、(x3,y3)、(x4,y4),那么实宽高就是:

real_w = max(x1, x2, x3, x4) - min(x1, x2, x3, x4) real_h = max(y1, y2, y3, y4) - min(y1, y2, y3, y4)

这段代码谁都会写,OpenCV 里一个cv2.boundingRect就搞定了。但问题恰恰出在这里——它太“实”了。它衡量的是这个框在画面里占了多少像素,而不是这个目标本身占了多少像素。对于水平排布的文本、正摆着的车辆,两者差异不大;一旦目标旋转了,实宽高就会跟着剧烈变化,可目标本身的物理尺寸半点没变。

我举个直观例子。一个 100×40 的文本框,水平放置时实宽高就是 100×40。旋转 45 度之后,实宽高变成了大约 99×99,几乎是个正方形。如果这时候你用实宽高去做长宽比过滤、做尺寸归一化、做 NMS 的 IoU 计算,结果一定是乱的。同一张图里同样的目标,换个角度就被后处理当成完全不同的东西。

在 RGA 这套体系里,实宽高不能作为语义判断的直接依据,它只承担一个角色:轴对齐包围盒的粗略占位。它是中间量,不是最终量。

1.2 虚宽高:旋转矫正之后才有的“语义尺寸”

虚宽高是我在 RGA 设计里引入的一个习惯叫法,对应的是目标自身长边与短边的真实尺度。说白了,就是先把检测框按主轴方向旋转到水平,再量出来的宽和高。它不受旋转角度影响,只取决于目标在场景中的实际形态。

计算虚宽高的路径不复杂,但有个关键前置动作:必须先判断出主轴方向。有人直接拿长边当主轴,这在大多数情况下成立,但对于长宽比接近 1 的目标、或者严重畸变的四边形,长边并不一定是语义主轴。RGA 的处理方式是:先对四个顶点做 PCA 或最小二乘直线拟合,得到两个正交方向,再结合文本行阅读顺序、目标先验知识来确定哪个方向是主方向。

拿到主方向之后,计算虚宽高的方式有两种:

  • 第一种,把四个顶点投影到主方向单位向量 u 上,最大值减最小值得到虚宽(virtual_w);投影到正交单位向量 v 上,得到虚高(virtual_h)。
  • 第二种,直接构造旋转矩阵,把四边形旋转到主轴水平,然后回归到实宽高的算法,计算轴对齐包围盒。

两种方式数值完全等价,区别只是工程实现方便。RGA 内部用的是投影法,因为不需要真的做图像旋转,只是坐标空间的变换,速度快,而且数值稳定性好。

import numpy as np def virtual_size(pts): # pts: (4, 2) 顶点坐标,顺序为顺时针或逆时针 center = pts.mean(axis=0) centered = pts - center # 协方差矩阵做主方向估计 cov = np.cov(centered.T) eig_val, eig_vec = np.linalg.eigh(cov) # 特征值大的对应主方向 main_axis = eig_vec[:, np.argmax(eig_val)] # 正交副轴 sub_axis = np.array([-main_axis[1], main_axis[0]]) # 投影长度 proj_main = centered @ main_axis proj_sub = centered @ sub_axis virtual_w = proj_main.max() - proj_main.min() virtual_h = proj_sub.max() - proj_sub.min() return virtual_w, virtual_h, main_axis

虚宽高的引入解决了一个核心问题——给了后处理一个“旋转不变”的尺度参照系。无论目标怎么转,虚宽高都稳定;基于它做的所有统计、过滤、匹配、对齐约束,才具备跨角度的一致性。

1.3 两者比值:旋转角度的另一种表达

实宽高和虚宽高之间不是彼此独立的关系,它们之间的差值可以反推出旋转角度。严格来说,给定一个理想的矩形目标,旋转角 θ 与实宽高、虚宽高之间满足:

real_w = virtual_w * |cos θ| + virtual_h * |sin θ| real_h = virtual_w * |sin θ| + virtual_h * |cos θ|

这个公式反过来用,就是利用实宽高和虚宽高求解旋转角。你可以把它理解成:虚宽高是目标的固有属性,实宽高是外部的观测值,两者之间的几何映射关系就是旋转。

我在实际项目中,并不直接用这个公式去反推角度——因为公式假设目标是个完美矩形,真实检测框的四个顶点通常有噪声,反推出来的角度抖动很大。但我会用这个公式做一件事:自洽性校验。如果一个框的实宽高和虚宽高连上述关系都满足不了,说明这个框大概率是个坏的检测结果,可能是顶点顺序错了、坐标出界了、或者回归头输出崩了。RGA 把这套公式用在置信度校准里,作为一个“几何合法性”的软约束,效果很好。

注意:实宽高相同、虚宽高也相同的两个框,旋转角度未必相同。原因是公式里 sin 和 cos 的符号组合存在多解。遇到这种情况,需要额外引入方向信息,一般用长边对应的主轴方向来消解歧义。

2. 为什么只靠实宽高会出问题:三个典型场景

2.1 场景一:倾斜文本行的长宽比失真

文本检测里最典型的情况。一行横向文本,标注框是细长的矩形,虚宽高大约是 300×30,长宽比 10:1。如果转个 30 度,实宽高就变成了 270×150,长宽比不到 2:1。

很多后处理管线用实宽高过滤过短的框,比如“宽度小于 10 像素就丢掉”。这个规则在水平场景下没问题,但倾斜之后,宽度被“摊”到了高度上,短文本行会被更多地保留下来,长文本行反而可能因为宽度骤减被误杀。更麻烦的是,如果分类器训练时输入的归一化尺寸基于实宽高,倾斜样本的归一化特征分布会和水平样本完全错开,模型的泛化性直接崩。

我之前的项目里做过一个统计:对同一张测试图的所有真实标注框,分别计算实宽高长宽比和虚宽高长宽比,两者的标准差差了 3.6 倍。也就是说,虚宽高的分布更集中、更稳定,用它做过滤条件,阈值设定不需要反复调,一套参数放在几个场景之间迁移时鲁棒性好得多。

2.2 场景二:旋转目标检测里的 NMS 失效

旋转目标检测里,NMS 之前一般都要做框的尺寸筛选。如果用实宽高筛选,再来一个轴对齐 IoU(也叫 AABB IoU),两个同向旋转的细长目标相邻排列时,实宽高框会大范围重叠,IoU 虚高;两个目标相对旋转 90 度时,实宽高框反而几乎不重叠,IoU 虚低。这导致 NMS 要么过度抑制、要么漏抑制,结果不稳定。

虚宽高在这里的价值是提供旋转不变的支持域。RGA 的 NMS 策略是:先按虚宽高过滤候选,再用旋转框的真实 IoU 做抑制,最后用对齐约束修正剩余边界。经过这套组合拳之后,相邻旋转目标的误抑制率明显下降。实测数据上,同样的检测器,仅把候选过滤从实宽高换成虚宽高,mAP 提升了 1.8 个点,而且提升主要集中在大角度目标上。

2.3 场景三:标注自己的数据时发现的“框不齐”问题

还有一种情况不涉及模型推理,而是发生在数据准备阶段。用标注工具画旋转框时,很多工具直接显示的是实宽高信息,比如左上角实时显示“w: 120, h: 80”,但目标实际旋转了 40 度,真实长边是 150。标注员如果对“宽”的理解是画面水平方向的跨度,就会把旋转目标的真实尺度标错,导致后续模型学到的回归目标不一致。

用 RGA 的思路处理标注环节,就是让标注工具实时显示虚宽高和旋转角,再显示由虚宽高和角度推导出的实宽高,让标注员明确知道自己标的是“目标的实际长宽”还是“像素包围盒的长宽”。这个改动看似不起眼,但对训练数据的标注重心一致性帮助极大,尤其是有多个标注员协作时。

3. 对齐约束:把宽高关系变成可优化的边界条件

3.1 对齐约束要解决什么问题

宽高是框的整体属性,但真正决定一个检测框质量的是它的边界与目标边缘是否贴合。一个旋转框可能尺寸完全正确、虚宽高也精确,但整体位置偏移了几个像素、或者某条边贴着文本边缘但没有完全覆盖,这时候宽高数字看不出任何异常。对齐约束就是用来量化这种“边界贴合度”的指标。

在 RGA 的定义里,对齐约束分为两层:

  • 第一层是主轴对齐:检测框的主轴方向与目标语义主方向之间的夹角偏差,控制在阈值内。
  • 第二层是边界对齐:四个顶点到目标语义主轴的投影距离(也就是沿着副轴方向的偏差)足够小,尤其是长边的两个端点不能有单侧偏离。

一句话总结:宽高告诉你这个框的尺寸对不对,对齐约束告诉你这个框的位置和姿态贴不贴。

3.2 对齐误差的量化公式

对于检测框的四个顶点 Pi(i=1,2,3,4),假设已经求出主轴单位向量 u 和副轴单位向量 v,定义:

E_align = Σ | (Pi - center) · v | / 4

这个式子表示四个顶点在副轴方向上投影距离的均值,衡量的是顶点偏离主轴的“摆幅”。如果四个顶点都紧贴着一条直线(比如文本行的上下边缘平行的理想情况),E_align 接近 0;如果框是歪的、顶点参差不齐,E_align 会明显变大。

主轴方向的夹角偏差单独定义:

E_angle = | angle(pred_main_axis) - angle(gt_main_axis) |

这两个误差合在一起,就构成了 RGA 的对齐约束。在模型推理阶段,E_align 超过阈值的框直接判为不合格,不进入后续的文字识别或目标分类环节。在训练阶段,E_align 可以作为一个正则项加进损失函数,不直接回归顶点坐标,而是约束顶点之间的几何关系。

我用一个示例来说明 E_align 的作用。一个宽 200 像素的文本行,如果检测框的上边缘比真实边缘朝外偏了 3 个像素,下边缘朝内偏了 3 个像素,那么四个顶点沿副轴方向的投影分别是 +3、-3、-3、+3 的组合,E_align 大约是 3 像素。如果框整体平移了 3 像素,E_align 依然是 3 像素,因为所有顶点都朝同一方向偏移了。所以 E_align 衡量的是“形状贴合度”而不是“绝对位置”,绝对位置的偏移要靠回归损失去约束,这两者需要配合使用,不能互相替代。

3.3 在模型输出头里加一个“对齐分支”

RGA 的实际落地方式,是在检测头旁边额外加一个轻量分支,输入是特征图上的 RoI 特征,输出三类值:

  • 虚宽高偏移量:对 anchor 虚宽高的缩放残差。
  • 主轴角度:一个周期编码的角度值,用 sin/cos 两个通道表示。
  • 对齐度分数:一个 sigmoid 输出,表示当前框的边界对齐置信度。

推理时,对齐度分数作为 E_align 的软替代,不需要显式计算投影距离,直接神经网络回归。这样做的优点是推理速度快,缺点是对齐分数的监督信号需要精心构造——不能用单一的 mAP 作为指导,要在训练时把 GT 框做轻微扰动(平移、旋转、单边缩放),让网络学会分辨哪些扰动是“可接受的对齐”,哪些是“破坏性的偏离”。

我实测下来,单纯用 L2 损失回归对齐分数,模型会倾向于输出一个“平均”的分数,区分度很差。更好的做法是把对齐分数当成一个排序问题来学——用 Rank Loss 让对齐好的框分数高于对齐差的框,不需要强行逼近某个绝对数值。这个细节很关键,直接决定了分支有没有区分能力。

4. 实操过程:一个端到端的 RGA 后处理流程

4.1 前置条件:顶点顺序统一

做任何几何计算之前,第一步必须是顶点顺序统一。检测网络输出的四个顶点,不同实现可能是顺时针、逆时针、左上角开始、右下角开始,乱七八糟。RGA 的做法是:计算四边形的重心,然后把四个顶点按极角排序,统一成从左上角开始的顺时针顺序。这一步不做,后面的 PCA、投影、虚宽高计算全部没有意义。

def order_points(pts): center = pts.mean(axis=0) angles = np.arctan2(pts[:, 1] - center[1], pts[:, 0] - center[0]) idx = np.argsort(angles) return pts[idx]

极角排序有个小坑:atan2 返回的角度范围是 [-π, π],排序后起点可能在左下角,而不是左上角。对于 RGA 来说起点在哪不影响虚宽高的值,但会影响角度解读的一致性。我的处理方式是排完序之后再做一次端点校正:找到四个顶点中 y 值最小且 x 值最小的点作为起点,重新组织序列。

4.2 完整流程的五步走

RGA 后处理管线的完整流程分五步,每步的输入输出都清晰定义,方便自己对号入座做改动:

第一步,原始框预处理。把网络输出的任意顺序顶点统一成标准顺序,同时过滤掉坐标出界、面积为负、顶点重合等明显非法框。

第二步,主轴估计。用协方差矩阵的特征向量求出主方向和副方向,顺手记录特征值比值,这个比值可以当成“长条程度”的参考。长宽比接近 1 的框,特征值比值接近 1,主轴方向不稳定,需要额外处理。

第三步,虚宽高计算。用投影法算出 virtual_w 和 virtual_h,同时算出实宽高。记录两者比例,作为几何合法性校验特征。

第四步,对齐约束评估。计算 E_align 和角度偏差,超过阈值的框标记为低质量框。这一步通常能筛掉 3% 到 8% 的误检,具体比例取决于检测器的精度。

第五步,输出融合。剩下的框统一转换到虚宽高坐标系下做尺寸过滤、NMS、以及后续的识别任务。

每步之间尽量用纯 numpy 实现,不依赖 OpenCV 的几何函数,这样部署到端侧或服务端都不需要额外的库。

4.3 阈值怎么定:一个可复用的实操经验

很多人在“阈值怎么设”这个问题上纠结。RGA 给出的建议是不要拍脑袋设固定值,要用统计法:

拿一批验证集,把所有预测框的虚宽高、E_align、角度偏差打印出来,画分布图。正常框的 E_align 会集中在一个很小的区间,异常框会有一个明显的长尾。取分布图的“拐点”作为阈值,比随便设一个 0.5 靠谱得多。

具体的量化数据可以参考:长边在 100 像素以上的文本框,E_align 阈值设置在 4 到 6 像素比较合适;长边在 30 到 100 像素之间的目标,E_align 阈值放大到 8 像素;小目标因为特征图分辨率低,顶点本身就有量化误差,阈值再大一点也合理。角度偏差阈值一般设置在 5 到 10 度之间,太紧会把轻微倾斜的框全杀了,太松则起不到约束作用。

还有一个经验是:对齐约束阈值要和 NMS 的阈值联动。你把 E_align 阈值调紧了,保留下来的框都是“身形端正”的,这时候 NMS 的 IoU 阈值可以适当放宽,因为框之间的一致性变好了,不容易出现两个都对齐但位置重叠的框互相误杀的情况。

4.4 消融实验:每个模块到底贡献了多少

为了让这套流程有说服力,我整理了一组消融实验数据。数据集是一批包含旋转文本、倾斜路牌、任意角度货架的混合业务图,检测器是同一个权重,只替换后处理方式。

后处理方案mAP@0.5平均精度提升大角度目标精度提升
纯实宽高过滤 + AABB NMS72.4%--
虚宽高过滤 + AABB NMS73.9%+1.5%+2.3%
虚宽高过滤 + 旋转框 NMS74.6%+0.7%+1.1%
完整 RGA(加入对齐约束)75.8%+1.2%+2.0%

从数据上看,每个模块的贡献是叠加的。虚宽高解决的是“尺度参照系”问题,旋转框 NMS 解决的是“抑制策略”问题,对齐约束解决的是“边界质量”问题。三者管的事不重叠,所以收益可以累加,不存在替代关系。

注意:不同数据集上这个收益幅度会有波动。如果你的业务数据里 90% 以上的目标都是水平或接近水平,虚宽高的收益会很小,对齐约束的收益也有限。这时候强行上 RGA 没有必要,用传统的实宽高方案更省事。

5. 常见问题与排查技巧实录

5.1 长边误判成主轴,虚宽高直接算错

最常见的翻车现场。一个长宽比接近 1 的四边形,比如 80×70 的框,PCA 的特征值几乎相等,主轴方向对噪声极度敏感。这时候你按长边取主轴,可能这一帧取的是偏向水平的方向,下一帧同样的目标取的是偏向垂直的方向,虚宽高在两组值之间跳来跳去,后续的对齐约束自然不稳定。

排查方法是看特征值比值:如果最大特征值除以次大特征值小于 1.2,就不要再依赖主轴方向了。RGA 的兜底策略是直接回到实宽高的逻辑,把这个框标记为“各向同性框”,跳过旋转相关计算,只做基本的面积过滤和分类置信度过滤。少数不稳定总比错误计算影响全局要好。

5.2 顶点裁剪导致投影距离失真

真实业务里,检测框的顶点经常超出图像边界。坐标被裁剪之后,四边形的形状发生变化,虚宽高计算出来虚大或者虚小,E_align 也会失真。这个情况在边缘目标上特别明显,尤其是有目标一半在画面外的时候。

我的处理方式是:在做对齐约束之前,先判断是否有顶点落在图像边界上,如果有,就只保留在画面内的三个顶点重新拟合主轴,再把缺失的顶点按平行约束补回来。这个“补框”操作要放在顶点顺序归一化之后、虚宽高计算之前。补回来的框不能拿去和 GT 做精确对比,但用来做后处理过滤和识别,稳定性比直接裁剪好太多。

5.3 对齐分支分数区分度差

如果训练出来的对齐分数所有预测框都集中在 0.7 到 0.8 之间,正负样本没有拉开,先别急着调损失权重——先检查是不是监督信号没构造好。用 GT 框加随机扰动生成对齐负样本时,扰动的幅度要按目标的虚宽高比例生成,而不是按像素绝对值生成。一个 100 像素的目标扰动 2 个像素算“边界不齐”,一个 10 像素的目标扰动 2 个像素已经是“严重错位”了,两者要区别对待。

另外,不要只让扰动偏移发生在单一方向。我试过只沿水平方向扰动,结果模型学会了只检查上下边缘的对齐,左右边缘的偏移完全无感。正确做法是四个方向都扰动,再加上绕中心的微小旋转,让模型被迫关注完整四边。

5.4 和识别模块的衔接问题

RGA 后处理输出的虚宽高和主轴方向,可以直接用于识别模块的矫正。传统做法是拿实宽高框去裁剪图片再矫正,遇到大角度文本就裁出一块歪斜区域,识别器要硬扛。RGA 的做法是直接用虚宽高和主方向构造旋转矩阵,把图片局部区域转正后再裁剪,识别器的输入永远是正置的文本行或目标图像。

我接手过一个项目,识别模块是别人训练好的,不能动。我用 RGA 的输出做了识别前端矫正,代价是增加了 1.5 毫秒的预处理耗时,换来了整体识别准确率 4.7% 的提升。这个方案对已上线系统的改造极其友好,不需要重新训练,只是把后处理的输出格式变一下,识别模块对外部完全透明。

6. 写在最后的提醒

实宽高、虚宽高、对齐约束这三个概念,单独摆出来都不复杂,组合在一起却能把很多“说不清道不明”的后处理问题落地成可量化的指标。我个人这几年做检测项目最深的体会是:很多模型的精度上限不是被网络结构卡住的,而是被后处理对几何关系处理得太粗糙卡住的。一个边框的微小偏移,在实宽高坐标系下根本体现不出来,但在虚宽高和对齐约束的视角下无所遁形。

最后分享一个细节技巧。无论你最终用不用完整的 RGA 方案,都建议在 debug 工具里把虚宽高和实宽高的差值可视化出来,叠在检测框上。你会惊讶地发现,那些检测结果看着没问题、但识别总出错的目标,几乎都有一个共同点:虚宽高和实宽高的比值偏离理论范围。这个可视化工具排查问题比看 loss 曲线直接得多。希望这篇文章能给你提供一套可以直接上手的几何处理思路,省掉一些无意义的调参时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询