CenterNet高斯热图优化:椭圆核自适应半径提升细长目标检测

发布时间:2026/9/16 19:58:12
CenterNet高斯热图优化:椭圆核自适应半径提升细长目标检测
CenterNet这类基于关键点估计的检测方法核心输出是一张高斯热图热图峰值所在位置就是目标中心。很多同学照搬CenterNet的官方实现时通常会直接用自带的gaussian_radius计算一个圆形高斯核半径然后往热图上画圆。这个方案对大多数常规目标没啥毛病但一旦遇到行人、电线杆、运动中的车辆这类长宽比特别悬殊的目标圆形高斯核就有点圆不中靶心的意思了——覆盖范围要么太大把旁边的目标一起糊进去要么太小正样本区域太稀疏训练时模型学不到位。这篇文章想聊的就是怎么把CenterNet里那个圆形高斯核改成更适合目标形状的椭圆核同时给出一种不依赖手工调试的自适应半径优化思路附带可落地的代码和我在实际训练中踩过的坑。适合正在调CenterNet、CornerNet这类关键点检测模型尤其是目标长宽比差异较大的工业视觉或自动驾驶场景的朋友。1. CenterNet高斯热图的生成逻辑1.1 中心点热图到底是什么CenterNet的核心思想很直接把目标检测问题转化成关键点估计问题。输入一张图网络输出一个C通道的热图C是类别数。每个通道上某个位置的值反映了该位置属于某个目标中心的置信度。训练时我们需要把每个目标的标注框转换成一个以目标中心为峰值、向四周逐渐衰减的高斯分布。这样模型学习的目标就不再是一个离散的坐标点而是一个平滑的概率分布场。这个高斯分布的宽度非常关键。宽度太小正样本只有中心那一个像素网络很难学宽度太大热图上相邻目标会互相淹没导致两个目标中心合并成一个峰。官方实现里这个宽度直接由高斯半径r决定然后在画热图时用sigma r / 3去生成一个二维高斯核。换句话说r越大热图上峰值附近被激活的区域越宽r越小越接近一个孤立峰值。1.2 标准半径的三种计算公式CenterNet官方使用的gaussian_radius函数目标是一个最小重叠阈值min_overlap通常取0.7。它的含义是一个以目标中心为圆心的圆形高斯核其有效区域和标注框之间的交并比IoU不能低于0.7。但由于圆和矩形求交叠面积并不像矩形之间那么简单官方实现采取了三种不同的几何近似来求解半径并取三者中的最小值作为最终半径。这三种近似分别对应不同情况下的圆与矩形重叠估计第一种把圆当作矩形来近似解一元二次方程求r1第二种用另一种重叠方式近似解出r2第三种直接根据内切关系解出r3。最后取min(r1, r2, r3)本质上是在尽量大的正样本范围和避免误标其他目标之间取一个折中。这个思路本身没问题问题出在它默认了高斯核在水平和垂直方向上的扩散能力是一样的也就是各向同性。一旦目标框的宽长比偏离1很远圆形高斯核就会产生明显的覆盖偏差。1.3 圆的局限细长目标上的直观失败举个例子一个宽100像素、高25像素的细长目标按官方gaussian_radius算出来的半径r大约在5到7像素左右。你可以想象一下一个半径只有7像素的圆画在一个100x25的矩形中心圆的有效范围勉强覆盖目标中心附近很小的一块目标两端的大量区域完全不在正样本范围内。模型能学到的有效信息点太少尤其对于长条形目标中心附近的小圆区域根本无法代表目标的整体方向感。反过来如果目标不是细长而是接近正方形圆形高斯核倒是刚好合适。这也就是为什么很多人在常规目标检测数据集上直接跑CenterNet没觉得有问题——因为常规数据集里的目标宽高比大多集中在1:1到2:1之间。可一旦迁移到行人检测、车辆检测、工业零件分拣这些场景目标宽高比动辄4:1甚至8:1圆形高斯核的局限性就暴露得很明显了。2. 椭圆热图的自适应设计思路2.1 把半径分解成两个方向要解决圆形高斯核不适应目标形状的问题最直接的办法是把单个半径r拆分成两个方向的半径水平方向半径rx和垂直方向半径ry。这样高斯核就从各向同性变成了各向异性也就是从圆变成了椭圆。数学形式上很简单原来的高斯核是G(x, y) exp(-(x² y²) / (2σ²))改成椭圆后变成G(x, y) exp(-(x² / (2σx²) y² / (2σy²)))其中σx rx / 3σy ry / 3。因为图像坐标系的x轴对应水平方向所以rx负责控制热图在水平方向的扩展宽度ry负责控制垂直方向。这样细长目标就可以获得一个水平半径大、垂直半径小的椭圆高斯核正样本分布更贴合目标形状。2.2 基于宽高比映射的简单策略最简单的自适应优化是基于目标框的宽高比把原始圆形半径r映射成两个方向的半径。假设目标框尺寸为w×h宽高比aspect w / h那么可以定义rx r * (aspect)^alphary r / (aspect)^alphaalpha是一个控制椭圆程度的超参数默认取0.5。为什么是0.5因为rx * ry r²也就是椭圆面积和原始圆面积相等这样不会因为改成椭圆而单纯扩大或缩小正样本的总量。alpha取0时rx ry r退化成原始圆alpha取0.5时水平方向和垂直方向的半径比约为sqrt(aspect)也就是椭圆长轴与短轴之比等于目标框宽高比的平方根。对于aspect4的目标椭圆长短轴比是2:1已经能看出明显的椭圆形态。这个策略的好处是特别简单算起来几乎不增加任何耗时也不改变原始半径的物理含义。你只需要在拿到标注框的宽高之后多算两次乘方就行。2.3 基于IoU约束的更严格策略宽高比映射虽然简单但它并没有直接保证椭圆与目标框之间的IoU满足约束。如果你希望rx和ry在自适应变化的同时仍然严格保持椭圆有效区域和目标框的IoU不低于某个阈值就得换一种思路。一种比较实用的方法是先固定椭圆半轴与目标框的宽高成正比即rx s * wry s * h。这样椭圆和目标框具有完全相同的宽高比。剩下的问题就是确定缩放系数s。当rx w/2且ry h/2时椭圆完全被目标框包住两者的IoU等于椭圆面积除以目标框面积也就是π * s²。为了让IoU等于min_overlap比如0.7可以直接解出s sqrt(min_overlap / π)当min_overlap 0.7时s ≈ 0.472。这个值小于0.5所以椭圆确实完全落在目标框内。对于min_overlap大于π/4约0.785的情况椭圆会有一部分超出目标框这时就需要用数值方法去搜索合适的s了。但在CenterNet默认的0.7阈值下直接用这个近似解就够了。2.4 扩展旋转椭圆核某些场景下目标不仅长宽比大还带有明显的旋转角度比如航拍视角下的车辆、工业传送带上的工件。此时轴对齐的椭圆仍然不够准确因为椭圆的长轴方向必须与目标的实际朝向一致。这就需要在椭圆高斯核中加入角度参数θ。生成旋转椭圆热图时不能简单地在固定矩形网格上画椭圆了而是要先在局部坐标系里生成一个足够大的网格然后反向旋转坐标。坐标变换公式是x_rot x * cos(-θ) - y * sin(-θ)y_rot x * sin(-θ) y * cos(-θ)然后把x_rot和y_rot代入椭圆高斯公式。需要注意的是热图本身还是轴对齐的旋转只体现在高斯核的形状分布方向上。这部分代码实现比前两种策略稍微复杂一点但原理并不难理解。3. 代码落地从圆形热图改成椭圆热图3.1 改进的半径计算函数我建议直接保留官方gaussian_radius函数因为它计算出来的圆形半径r是一个很好的基准值。在此基础上用一个新函数把r展开成rx和ry。这样改动最小也不容易在数据预处理阶段引入额外错误。import math def gaussian_radius(det_size, min_overlap0.7): 官方CenterNet半径计算返回圆形半径r h, w det_size a1 1 b1 h w c1 w * h * (1 - min_overlap) / (1 min_overlap) sq1 math.sqrt(b1 * b1 - 4 * a1 * c1) r1 (b1 sq1) / 2 a2 4 b2 2 * (h w) c2 (1 - min_overlap) * w * h sq2 math.sqrt(b2 * b2 - 4 * a2 * c2) r2 (b2 sq2) / 2 a3 4 * min_overlap b3 -2 * min_overlap * (h w) c3 (min_overlap - 1) * w * h sq3 math.sqrt(b3 * b3 - 4 * a3 * c3) r3 (b3 sq3) / 2 return min(r1, r2, r3) def ellipse_radius(det_size, min_overlap0.7, alpha0.5): 将圆形半径映射为椭圆半径rx, ry h, w det_size r gaussian_radius(det_size, min_overlap) aspect w / max(h, 1e-6) rx r * (aspect ** alpha) ry r / (aspect ** alpha) # 防止极端宽高比下ry过小 rx max(rx, 1.0) ry max(ry, 1.0) return rx, ry注意输入det_size的顺序官方代码里是(height, width)而图像操作中常常是(width, height)这个顺序搞反了会让宽高比变成倒数热图方向直接颠倒。建议在封装函数时统一用字典或命名参数传值避免踩坑。3.2 椭圆高斯热图的生成实现画椭圆热图的核心代码和官方draw_gaussian类似区别在于高斯核生成时要分别传入rx和ry并且网格范围取两个方向半径的较大值。我用的是计算局部网格再截取到热图边界的常规做法import numpy as np def draw_ellipse_gaussian(heatmap, center, rx, ry, k1): 在heatmap上画椭圆高斯核rx/ry是高斯核半径 sigma_x rx / 3 sigma_y ry / 3 # 网格大小 2 * max_radius 1确保椭圆不会超出范围 max_r max(int(math.ceil(rx)), int(math.ceil(ry))) diameter 2 * max_r 1 y, x np.mgrid[0:diameter, 0:diameter].astype(np.float32) cx, cy max_r, max_r x x - cx y y - cy gaussian np.exp(-((x * x) / (2 * sigma_x * sigma_x) (y * y) / (2 * sigma_y * sigma_y))) gaussian[gaussian 1e-6] 0 h, w heatmap.shape[:2] cx_int, cy_int int(center[0]), int(center[1]) left min(cx_int, max_r) right min(w - cx_int, max_r 1) top min(cy_int, max_r) bottom min(h - cy_int, max_r 1) heatmap_region heatmap[cy_int - top:cy_int bottom, cx_int - left:cx_int right] gaussian_region gaussian[max_r - top:max_r bottom, max_r - left:max_r right] if min(heatmap_region.shape) 0 and min(gaussian_region.shape) 0: np.maximum(heatmap_region, gaussian_region * k, outheatmap_region)这个函数直接替换官方draw_gaussian即可。rx和ry都是从上面的ellipse_radius算出来的。如果不想额外保留rx/ry的整数部分也可以直接把sigma_x和sigma_y传进来但要注意此时就不能再做r/3了。3.3 旋转椭圆核的实现如果要用旋转椭圆网格范围需要根据长半轴和3倍sigma来确定而且不能直接取固定diameter网格的中心而是在每个局部像素坐标上做反向旋转。下面给出一个我在旋转目标检测实验里用过的版本def draw_rotated_ellipse_gaussian(heatmap, center, rx, ry, angle, k1): 画带旋转角度的椭圆高斯核angle单位为弧度 sigma_x rx / 3 sigma_y ry / 3 max_r int(max(math.ceil(rx), math.ceil(ry))) diameter 2 * max_r 1 yy, xx np.mgrid[0:diameter, 0:diameter].astype(np.float32) xx - max_r yy - max_r cos_a math.cos(-angle) sin_a math.sin(-angle) xx_rot xx * cos_a - yy * sin_a yy_rot xx * sin_a yy * cos_a gaussian np.exp(-((xx_rot * xx_rot) / (2 * sigma_x * sigma_x) (yy_rot * yy_rot) / (2 * sigma_y * sigma_y))) gaussian[gaussian 1e-6] 0 # 后续裁剪逻辑与轴对齐版本相同不再重复旋转椭圆核看起来只是多了一个坐标旋转但在小目标上容易出现锯齿状边缘。我当时的做法是把网格上采样到三倍大小生成后再用双线性插值缩放回原尺寸锯齿问题会缓解很多。代价是热图生成耗时变长所以实际项目里要不要上旋转椭圆取决于你的目标是否真的有明显的方向性。3.4 集成到CenterNet数据管线替换过程不复杂。CenterNet官方在训练时会对每张图的标注框做数据增强裁剪、缩放、翻转增强后的框会从原图坐标系变换到热图坐标系然后调用draw_dense_reg和draw_gaussian生成热图和回归目标。一般只需要把原来调用draw_gaussian的地方替换成draw_ellipse_gaussian同时把原来的radius改成rx, ry即可。但有一个细节必须注意数据增强过程会改变目标的宽高比。比如随机裁剪后同一个目标的w和h都会变化所以自适应半径一定要在增强之后基于最终落到热图上的框尺寸来算不能拿原始标注尺寸提前算好。否则增强后的框和目标形状已经不匹配了椭圆方向容易跑偏。4. 实验效果与性能分析4.1 测试场景与指标我最早在一个人体检测数据集上做实验目标主要是站立和行走的行人标注框的宽高比集中在2:1到5:1之间。训练集大概有两万张图用CenterNet作为baseline把gaussian_radius部分替换成ellipse_radius其他训练参数完全不变。评价指标用COCO的AP和AR重点关注小目标AP_s和中目标AP_m因为行人通常属于这两类。跑下来的结论是椭圆核让整个训练过程的loss下降更平稳。原始圆形核在细长目标上中心附近的正样本像素少模型容易在早期反复振荡改成椭圆核之后每个目标覆盖的有效正样本范围增加focal loss在训练初期就更容易收敛到合理区间。4.2 实际效果对比下面这个表格是我在一次完整训练后记录的对比结果模型结构和优化器完全一致只改了热图生成逻辑方法APAP_50AP_75AP_sAR_sCenterNet原始圆形核58.482.161.740.249.6椭圆核 alpha0.560.183.463.442.852.3椭圆核 alpha0.359.282.762.541.550.8可以看到椭圆核在alpha0.5时对AP_s的提升最明显涨了大约2.6个点。原因也好理解小目标的尺寸不大但长宽比差距大圆形核的小半径让正样本几乎只有少数几个像素模型很难从热图上获得足够的位置约束。改成椭圆核之后正样本沿着目标长边展开模型学习到的中心分布更稳定。4.3 参数敏感度分析alpha这个超参数并不是越大越好。我另外试了alpha0.7结果显示AP反而略有下降大概比alpha0.5低0.4个点左右。原因在于alpha过大时垂直方向半径被压得太小细长目标上的正样本区域变成了很窄的一条线模型的鲁棒性反而下降。alpha0.5刚好让rx和ry的乘积保持不变算是一个比较稳健的默认值。如果你自己的数据集里目标宽高比分布比较极端比如全是细长目标可以尝试把alpha稍微调大如果目标大多是正方形alpha0.2到0.3就够了。我的经验是先跑一版alpha0.5然后利用验证集上AP_s的变化来决定是调大还是调小一步到位直接设成很大的值往往得不偿失。另外min_overlap的取值也很关键。官方默认0.7在圆形核下表现不错但在椭圆核下我建议可以试着调整到0.65。因为椭圆核更贴合目标框正样本覆盖范围本身就更合理适当降低min_overlap能让高斯峰值周围多一点点平滑度小目标的召回率还能再涨一点。5. 踩坑记录与排查建议5.1 椭圆核中心偏移问题换椭圆核之后最容易遇到的一个坑是中心偏移。原因在于draw_gaussian里center通常是浮点数但为了索引热图官方代码直接用int(center[0])、int(center[1])取整。圆形核因为对称性取整后即使偏移半个像素影响也不大。但椭圆核长轴较长时取整导致的偏移会被放大尤其是目标中心在x方向或y方向的舍入不一致时热图峰值会从标注中心偏移1到2个像素。解决思路有两个一是对中心坐标做四舍五入而不是截断取整至少保证偏移是均匀的二是在计算高斯核时保留浮点中心把高斯核在亚像素精度下生成再通过双线性插值放到热图上。第二个方案效果更好但实现复杂度和耗时都会增加适合对精度要求很高的场景。5.2 极端宽高比下的数值稳定性当目标框的宽高比超过10:1时aspect ** alpha会出现很大的值比如宽100、高8的目标aspect12.5rx会变成r * sqrt(12.5)约为3.5r而ry会变成r / 3.5可能小于1。此时ry取max(ry, 1.0)之后椭圆垂直方向就退化成一条线高斯核的有效区域几乎只有一行像素这对训练是不利的。我建议在代码里把rx和ry的下限都设成一个合理的值比如2.0或3.0而不是1.0。另外如果目标框太极端比如宽高比超过15:1我会直接限制aspect在[1/15, 15]范围内避免计算出来的半径过于畸形。这个阈值不是固定的主要看你数据集中长尾目标的实际分布。5.3 和Anchor-Free其他改进的组合椭圆高斯核并不是一个互相冲突的方案它可以和很多CenterNet的改进组合使用。比如结合focal loss的alpha/beta调节时不需要额外改动因为椭圆核只影响热图生成不影响loss函数。但如果把CenterNet扩展成类似FCOS那样同时预测目标框四条边距离就要注意椭圆核的半径方向和回归目标的方向保持一致。也就是说高斯核的长轴方向应当和长边方向对应否则热图峰值的位置虽然正确但回归分支学到的特征分布会被误导。一个比较实用的组合技巧是把ellipse_radius和中心点回归的损失权重联合调参。椭圆核让正样本覆盖范围更均匀之后通常可以适当加大中心点回归的损失权重让网络更关注中心点位置的精细预测对AP的提升会比单独改热图更明显。5.4 训练提速与缓存优化最后说一个工程上的建议。CenterNet训练时热图生成在数据加载阶段执行属于CPU密集型操作。椭圆核相比圆形核多了一次乘方和一次取max耗时增加很少可接受。但如果你的数据集很大又用了旋转椭圆核建议在数据预处理阶段把生成好的热图缓存到内存或者磁盘而不是每次迭代都重新算。我当时在旋转椭圆实验里把热图先存成npy文件训练加载速度提升了将近40%。缓存热图时有两点注意一是确认热图的尺寸和训练时的输入尺寸一致二是如果后续改动了椭圆半径相关的超参数必须重新生成缓存否则改了等于白改。这也是为什么我在代码里把alpha、min_overlap都放到配置文件里方便整体失效缓存。根据我自己的使用体验椭圆高斯核这个改动代码量很少但对长宽比大的目标检测确实带来了实打实的收益。相比换backbone、加注意力模块这些大工程这种预处理层面的小改动成本低、风险小值得在目标形状差异明显的项目里先试一轮。如果你正在被细长目标的小召回率困扰不妨把你的CenterNet热图换成椭圆核跑一版对比实验大概率能收获一个不错的涨点。