1. 从Anchor到Keypoint:CenterNet到底在解决什么痛点
如果你做过一段时间的目标检测,大概率经历过这样的场景:模型训练完了,推理结果里同一个物体被框了三四次,IoU阈值调来调去,NMS后处理写了一堆,最后部署时还得为不同分辨率准备多套Anchor配置。这套流程从Faster R-CNN时代延续到YOLO系列,几乎成了检测任务的标准范式。但标准不代表合理——Anchor机制本质上是一种"暴力枚举+筛选"的思路,它假设物体能被一组预设的框覆盖,然后用回归和分类去逼近真实位置。
CenterNet的出现,直接把这个假设掀翻了。它的核心思路非常朴素:把目标检测变成关键点估计。一张图里有多少个物体,就有多少个中心点,模型只需要预测每个物体的中心点位置以及对应的宽高,剩下的交给后处理去还原边界框。没有Anchor,没有NMS(或者说NMS变得可选),整个流程干净得像一条直线。
我第一次接触CenterNet是在一个需要检测密集小目标的场景里。当时用YOLOv5跑了一版,Anchor聚类做了三轮,小目标的召回率始终卡在70%上下。换成CenterNet之后,同样的数据集,没调Anchor,没做任何特殊预处理,召回率直接拉到85%以上。这个差距让我意识到,Anchor机制在密集场景下的局限性是结构性的,不是调参能解决的。
CenterNet适合谁?如果你正在做以下事情,它值得你花时间研究:
- 密集物体检测(人群、车辆、细胞、零件)
- 需要端到端部署、不想维护复杂后处理的场景
- 对推理速度有要求,但GPU资源有限的边缘设备
- 想理解"无Anchor检测"这一派系的核心思想
但也要说清楚,CenterNet不是万能的。它在极端长宽比物体、严重遮挡场景下的表现,未必比精心调优的Anchor-based方法好。选型这件事,永远要看具体任务。
2. 把检测问题翻译成热力图问题:CenterNet的核心机制拆解
2.1 从边界框到中心点:一次彻底的范式转换
传统检测器的输出是一个四维向量(x, y, w, h),加上类别置信度。CenterNet的输出是一张热力图,尺寸是输入图像的1/4(通常),每个通道对应一个类别。热力图上每个峰值代表一个物体中心,峰值处的值就是"这里有个物体的置信度"。
具体来说,假设输入图像是512×512,经过骨干网络下采样4倍后得到128×128的特征图。对于每个类别,模型输出一张128×128的热力图。如果图中有3个人,那么"人"这个类别的热力图上就有3个峰值。每个峰值的位置就是人的中心点坐标(乘以4还原到原图尺度)。
但光有中心点不够,还需要知道物体的尺寸。所以模型同时输出两个额外的特征图:宽高预测图(128×128×2)和中心点偏移图(128×128×2)。宽高预测图给出每个中心点对应的物体宽高,偏移图修正下采样带来的量化误差。
这里有个关键设计:宽高预测是在中心点位置进行的,而不是在每个像素位置。这意味着模型只需要在中心点处回归尺寸,其他位置的宽高预测不参与损失计算。这个设计大幅降低了回归任务的难度,因为中心点附近的特征通常最具代表性。
2.2 热力图峰值怎么变成最终检测框
训练时,CenterNet把每个物体的中心点映射到热力图上,用一个高斯核在中心点周围生成一个"软"目标。高斯核的半径根据物体尺寸自适应调整——物体越大,高斯核越宽。这样做的好处是,即使预测的中心点稍微偏了一点,只要还在高斯核范围内,损失就不会太大,模型对中心点定位的容错性更强。
推理时,流程是这样的:
- 对每个类别的热力图做3×3最大池化,保留局部峰值
- 取所有峰值中置信度大于阈值(通常0.3)的点
- 对每个峰值点,读取对应的宽高预测和偏移量
- 还原边界框:
x1 = (x + offset_x) - w/2,y1 = (y + offset_y) - h/2,以此类推
整个过程没有IoU计算,没有NMS循环。如果不同类别的物体中心点重合,它们会被分别保留;如果同一类别的两个物体中心点太近,3×3最大池化可能会抑制掉一个,这是CenterNet在极端密集场景下需要注意的地方。
2.3 损失函数的三驾马车:热力图损失、尺寸损失、偏移损失
CenterNet的损失函数由三部分组成,每一项都有明确的分工:
热力图损失用的是改进版的Focal Loss。标准Focal Loss是为分类任务设计的,CenterNet对它做了调整,让正样本(中心点)的损失权重更高,负样本(背景)的损失权重更低。具体公式里有个(1-p)^4的调制因子,p是预测置信度,预测越准的样本损失越小,模型会把注意力集中在难样本上。
尺寸损失用的是L1 Loss,只在中心点位置计算。这里有个细节:CenterNet直接回归宽高的原始值,而不是回归相对于Anchor的比例。因为根本没有Anchor。L1 Loss的权重通常设得很小(0.1),因为尺寸回归比中心点定位容易得多,不需要太强的监督信号。
偏移损失也是L1 Loss,权重更小(通常0.01)。偏移量的值域很小(0到1之间),因为下采样4倍带来的量化误差最多也就3个像素。这个损失的作用是"锦上添花",让中心点定位更精确。
三项损失的权重分配是经验值,我在实际项目中试过调整,发现热力图损失权重稍微调高一点(比如从1.0调到1.5),对小目标的召回有轻微提升,但幅度不大。如果你的任务里小目标特别多,可以试试这个方向。
3. 骨干网络选型与上采样结构:CenterNet的工程实现细节
3.1 为什么ResNet-18加反卷积成了默认配置
CenterNet的官方实现提供了多种骨干网络:ResNet-18、ResNet-101、DLA-34、Hourglass-104。其中ResNet-18+反卷积是最常用的配置,原因很实际:速度和质量平衡得最好。
ResNet-18本身很轻,参数量只有11M左右,在1080Ti上跑512×512输入能到140+FPS。但ResNet-18原本是为分类设计的,最后的下采样倍数是32倍,对于检测任务来说太粗糙了。CenterNet的做法是:取ResNet-18的中间层输出(下采样16倍),然后接三层反卷积,每层上采样2倍,最终得到下采样4倍的特征图。
这个"骨干网络+反卷积"的结构,本质上是在用计算量换空间分辨率。反卷积的参数量不大,但能有效恢复特征图的空间细节。我试过直接用ResNet-18的下采样8倍输出,不做反卷积,检测精度掉了将近5个点。反卷积这三层,不能省。
DLA-34是另一种常见选择,它的全称是Deep Layer Aggregation,核心思想是在网络的不同深度之间做密集连接。DLA-34的精度比ResNet-18高2-3个点,但速度慢一些。如果你的场景对精度要求高、对速度不那么敏感,DLA-34是更好的选择。Hourglass-104精度最高,但参数量大、推理慢,一般只在离线任务里用。
3.2 反卷积层的参数配置与踩坑记录
反卷积层(也叫转置卷积)的配置有几个关键参数:kernel_size、stride、padding、output_padding。CenterNet官方实现用的是kernel_size=4、stride=2、padding=1、output_padding=0。这个配置能让特征图尺寸精确翻倍。
我踩过一个坑:一开始自己搭反卷积层时,用了kernel_size=3、stride=2、padding=1,结果输出尺寸总是差一个像素。后来查了公式才知道,转置卷积的输出尺寸计算是(H-1)*stride - 2*padding + kernel_size + output_padding。用kernel_size=3、stride=2、padding=1时,输出是(H-1)*2 - 2 + 3 = 2H - 1,少了一个像素。改成kernel_size=4、stride=2、padding=1后,输出是(H-1)*2 - 2 + 4 = 2H,正好翻倍。
这个细节在论文里不会写,但实际写代码时如果搞错了,特征图尺寸对不上,后面的损失计算全乱套。建议直接用官方配置,别自己发挥。
3.3 可变形卷积的加入时机与效果
CenterNet在ResNet-18和DLA-34的某些层里加入了可变形卷积(Deformable Convolution)。可变形卷积的核心思想是让卷积核的采样位置可以学习偏移,从而更好地适应物体的几何形变。
在CenterNet里,可变形卷积主要加在反卷积层之前的那几个卷积块里。官方代码里用的是v2版本的可变形卷积,偏移量通过一个额外的卷积层学习得到。我实测下来,加入可变形卷积后,对非刚性物体(比如人、动物)的检测精度有1-2个点的提升,对刚性物体(比如车、箱子)提升不明显。
如果你的任务里非刚性物体占主导,建议开启可变形卷积;如果主要是刚性物体,可以关掉,能省一点计算量。这个开关在配置文件里就能改,不用动代码。
4. 训练策略与数据准备:从零跑通CenterNet的完整路径
4.1 数据标注格式的转换与验证
CenterNet官方代码支持COCO格式的标注,但实际项目中我们的数据往往来自不同来源,标注格式五花八门。最常见的是VOC格式的XML,需要转成COCO的JSON。转换脚本网上很多,但有几个坑要注意:
第一,类别ID必须从1开始,0保留给背景。我见过有人把类别ID从0开始编,结果训练时所有标签都被当成背景,loss一直不降。查了半天才发现是ID映射的问题。
第二,边界框坐标要检查是否越界。有些标注工具导出的坐标可能超出图像尺寸,比如xmax大于图像宽度。这种框在训练时会导致热力图生成出错,因为中心点可能落在特征图外面。建议在转换脚本里加一个clip操作,把所有坐标限制在[0, width]和[0, height]范围内。
第三,空标注图像的处理。有些图像可能没有任何标注,转换后JSON里对应的annotations为空。CenterNet的训练代码默认会跳过这些图像,但如果你用的是自定义数据加载器,要确保空标注不会导致除零错误。
转换完成后,建议写个小脚本随机抽样几张图,把边界框画出来可视化检查。这一步花不了几分钟,但能避免后面训练时才发现标注有问题。
4.2 学习率与优化器的选择逻辑
CenterNet官方用的是Adam优化器,初始学习率5e-4,在第90和120个epoch分别降10倍,总共训练140个epoch。这个配置在COCO数据集上验证过,直接拿来用问题不大。
但如果你在自己的数据集上训练,有几个地方需要调整:
batch size。官方用的是32,但很多人显卡没那么大。如果batch size降到8或16,学习率要相应降低,一般按线性缩放,比如batch size=16时学习率用2.5e-4。不过线性缩放不是铁律,我试过batch size=16时用5e-4也能收敛,只是前期loss波动大一些。
warmup。CenterNet官方代码里没有显式的warmup,但前几个epoch学习率是从0慢慢升上去的(通过一个线性warmup调度器)。如果你自己写训练循环,建议加上500到1000步的warmup,能有效避免训练初期loss爆炸。
权重衰减。官方用的是5e-4,这个值对CenterNet来说比较合适。我试过调到1e-3,模型出现了轻微的过拟合;调到1e-4,收敛速度变慢。5e-4是个甜点值。
4.3 数据增强的取舍:哪些增强对CenterNet有效
CenterNet官方用的数据增强比较克制:随机翻转、随机缩放、随机裁剪。没有用Mosaic、MixUp这些花哨的增强。原因在于,CenterNet的热力图生成对物体的完整性比较敏感,如果物体被裁剪得太厉害,中心点可能落在图像外面,这个样本就废了。
我试过加Mosaic增强,效果不稳定。有些batch里Mosaic把四张图拼在一起,小物体变得更小,热力图上的峰值几乎消失,模型学不到东西。后来改成只对大于一定尺寸的物体做Mosaic,效果才好一些。
随机缩放的范围官方设的是0.6到1.4,这个范围比较温和。如果你的数据集里物体尺寸变化很大,可以适当放宽到0.5到1.6,但要注意小物体缩得太小会丢失信息。
颜色抖动(亮度、对比度、饱和度)可以加,对精度有轻微提升,但不要加得太猛。我一般把亮度变化控制在0.2以内,对比度控制在0.3以内。
5. 推理部署与后处理优化:让CenterNet真正跑起来
5.1 从PyTorch到ONNX的导出陷阱
CenterNet的PyTorch模型导出ONNX时,最大的坑在最大池化层。官方推理代码里用了nn.MaxPool2d(kernel_size=3, stride=1, padding=1)来实现局部峰值提取,这个操作在PyTorch里没问题,但导出ONNX后,某些推理引擎(比如TensorRT)对带padding的最大池化支持不好,会导致输出结果和PyTorch不一致。
解决方案有两个:一是把最大池化换成自己实现的峰值提取(用卷积或手动比较),二是导出时把最大池化层单独拿出来,在CPU上做后处理。我一般选第二种,因为最大池化只在推理时用,不影响训练,放在CPU上做对速度影响很小。
另一个坑是动态尺寸输入。CenterNet的骨干网络是全卷积的,理论上支持任意尺寸输入。但导出ONNX时如果指定了固定尺寸,推理时就只能跑那个尺寸。建议导出时把输入尺寸设为动态,用dynamic_axes参数指定。不过要注意,某些推理引擎对动态尺寸的支持有限,可能需要针对不同分辨率导出多个模型。
5.2 后处理中的阈值调优:置信度、NMS与最大检测数
CenterNet的后处理比Anchor-based方法简单,但仍有几个阈值需要调:
置信度阈值。官方默认0.3,这个值偏低,适合召回率优先的场景。如果你的应用对误检敏感,可以调到0.5甚至0.6。我做过一个工业质检的项目,最终把阈值定在0.55,误检率从8%降到了2%,召回率只掉了1.5个点。
最大检测数。默认是100,对于大多数场景够用。但如果你的图里物体特别多(比如人群计数),需要调大。我试过设成500,推理速度几乎没变化,因为后处理本身很快。
NMS。CenterNet理论上不需要NMS,因为每个物体只有一个中心点。但在实际中,如果两个物体的中心点非常近,3×3最大池化可能无法完全抑制,导致重复检测。官方代码里其实保留了一个可选的NMS,默认关闭。我建议在密集场景下开启NMS,IoU阈值设0.5左右,能去掉大部分重复框。
5.3 边缘设备上的量化与加速实践
在边缘设备上部署CenterNet,量化是绕不开的一步。我试过TensorRT的FP16量化,速度提升约1.8倍,精度几乎无损。INT8量化速度提升更明显(约2.5倍),但精度会掉1-2个点,需要做校准。
校准集的选取很关键。不要随便拿几张图就做校准,最好从验证集里随机抽500到1000张,覆盖各种场景。校准算法用熵校准(Entropy Calibration)比最小最大校准(MinMax Calibration)效果好,精度损失更小。
另一个加速手段是降低输入分辨率。CenterNet的输入从512×512降到384×384,速度提升约1.7倍,精度掉2-3个点。如果你的场景里物体比较大,这个 trade-off 是划算的。我做过一个车辆检测的项目,输入降到320×320,精度只掉了1.8个点,但帧率从45FPS升到了80FPS。
6. 那些官方文档不会告诉你的实战经验
6.1 小目标检测的针对性优化
CenterNet在小目标上的表现,取决于骨干网络的下采样倍数。下采样4倍意味着特征图上1个像素对应原图4个像素,如果物体在原图上只有8×8,特征图上只有2×2,热力图峰值会非常弱。
我试过几种优化方案:
提高输入分辨率。这是最直接的方法,但计算量按平方增长。从512提到768,小目标召回率能提升5-8个点,但速度掉一半。
修改下采样倍数。把反卷积层数增加一层,下采样倍数从4降到2,特征图分辨率翻倍。这个改动对小目标效果明显,但显存占用增加约30%。如果显存够,值得一试。
多尺度训练。在训练时随机选择输入尺寸(比如从320到608之间随机),让模型适应不同尺度的物体。这个技巧对小目标和大目标都有帮助,但训练时间会变长。
6.2 类别不平衡的处理策略
CenterNet的热力图损失本身对类别不平衡有一定的鲁棒性,因为Focal Loss会降低易分类样本的权重。但如果你的数据集里某些类别样本极少(比如只有几十个),模型仍然学不好。
我常用的做法是过采样。在数据加载器里,对稀有类别的图像提高采样概率,让每个batch里稀有类别的样本比例不低于10%。这个比例不能太高,否则模型会过拟合到稀有类别上。
另一个方法是调整损失权重。CenterNet的损失函数里,每个类别的热力图损失是独立计算的,可以给稀有类别更高的权重。我一般把稀有类别的权重设为2.0,常见类别保持1.0。这个改动在配置文件里就能做,不用改代码。
6.3 模型不收敛时的排查清单
训练CenterNet时如果loss不降,按这个顺序排查:
- 检查数据标注。随机抽几张图可视化,确认边界框位置正确、类别ID正确。
- 检查学习率。如果loss一开始就爆炸,学习率可能太大;如果loss几乎不动,学习率可能太小。
- 检查热力图生成。把生成的热力图可视化出来,确认中心点位置正确、高斯核半径合理。
- 检查损失权重。三项损失的权重如果设置不当,某一项可能会主导梯度,导致其他项学不好。
- 检查骨干网络预训练权重。如果从零训练,收敛会慢很多。建议加载ImageNet预训练权重。
我遇到过一次loss不降的情况,排查了半天发现是数据加载器里的归一化参数写错了,图像像素值没有除以255,导致输入值域在0到255之间,网络第一层就饱和了。这种低级错误,往往最耗时。
6.4 从训练到部署的完整检查清单
最后分享一个我每次部署CenterNet前都会过一遍的检查清单:
| 检查项 | 具体内容 | 常见问题 |
|---|---|---|
| 输入尺寸 | 训练和推理是否一致 | 推理尺寸与训练不符导致精度下降 |
| 归一化 | 均值方差是否匹配 | 忘记归一化或参数写错 |
| 类别映射 | 类别ID到名称的映射 | 推理结果类别错乱 |
| 后处理阈值 | 置信度、NMS、最大检测数 | 阈值过高漏检,过低误检 |
| 输出格式 | 边界框坐标格式 | xywh与xyxy混淆 |
| 量化校准 | 校准集是否覆盖所有场景 | 量化后精度骤降 |
这个清单看起来简单,但每次部署新模型时,总有一两项会出问题。养成过清单的习惯,能省下大量调试时间。
CenterNet这套框架,从论文到落地,中间有大量的工程细节需要打磨。但一旦跑通,它的简洁性和可扩展性会让你觉得前期的投入是值得的。尤其是在需要快速迭代、频繁更换骨干网络或调整输入尺寸的场景里,CenterNet的"无Anchor"设计让很多传统检测器里的麻烦事都消失了。如果你还没试过,找个周末跑一遍官方代码,感受一下热力图峰值变成检测框的那个瞬间,挺有意思的。