
2019年初我啃这篇预印本的时候第一反应是又一个把NMS换成花活儿的框架。读完才发现作者是真的“不装了”——热力图取个局部最大值读一下宽高框就出来了后面不再接任何候选框筛选逻辑。标题里的“no Anchorno NMS”不是营销话术而是把所有同行习惯依赖的两根拐杖都抽掉了。这篇东西适合两类人看一类是刚刷完R-CNN系和YOLO系、正被锚框参数和NMS阈值搞到头大的算法新人另一类是已经在用带Anchor方案做落地、被召回率和高延迟双重折磨的工程向读者。我尽量把原理、公式背后的动机以及实际跑代码会踩的坑一次讲透。1. 从“Anchor NMS套餐”说起这次要把两个一起干掉1.1 为什么检测器总是离不开Anchor过去几年目标检测的主流范式其实可以概括成一句话先撒一大堆框再告诉模型这些框里有没有目标、目标是什么、框偏移了多少。以最典型的锚框机制为例你在特征图的每个位置上预设多个不同尺度、不同长宽比的矩形框训练时拿它们跟真实目标做IoU匹配正样本负责回归出目标的位置和大小负样本负责学会“这里没东西”。这套逻辑本身没毛病问题是它带来了三个持续不断的麻烦。第一个麻烦是超参数太多。Anchor的尺度、长宽比、每个位置放多少个框都是需要人为拍脑袋的东西。换一个数据集最优配置往往就变了。COCO上work得很好的锚点参数换到航拍小目标或者工业质检场景精度可以直接掉几个点得上调参工具去重新搜。第二个麻烦是正负样本严重失衡。一个特征图上动不动洒几万到几十万个锚框真正跟目标匹配上的正样本区区几百个。为了平衡这个比例当年大家折腾出OHEM、Focal Loss一系列补偿手段本质上都是在给失衡的优化过程“打补丁”。第三个麻烦是后处理特别重。因为目标周围会同时产生大量高置信度的重复框不做NMS非极大值抑制就会输出一堆互相重叠但其实是同一个物体的框。NMS本身是个看似简单、实现细节极其影响效果的算子IoU阈值定高了会保留一堆噪声定低了又会把邻近目标误杀。这三个麻烦彼此嵌套锚框生成方式和NMS输出质量直接挂钩两边都要精细调节才能让整个pipeline转起来。1.2 no NMS的前提让重复预测在源头消失如果只把“no NMS”当成省掉一个后处理步骤那就小看CenterNet的设想了。它的真正思路是既然NMS处理的是“同一个目标被多个框重复覆盖”的问题那我能不能让模型从根本上只为每个目标产生一个预测怎么做到把“目标框”拆成三个更本质的东西一个中心点、一个宽高、一个中心点因下采样产生的偏移。模型不学习“从所有锚框里找出哪个框更像目标”而是直接学习“图上哪个像素是目标的中心”。每个目标在训练标注里只对应一个中心点像素推理时每个目标理论上只会产生一个峰值。没有了大量互相重叠的边界框自然也就不需要NMS来筛。这个转向对应了检测范式从“稠密候选框分类”到“稀疏关键点定位”的迁移。Anchor-based的方法问的是“这些预定义框里谁有目标”CenterNet问的是“目标长在哪个坐标上”。后者天然是并行的、局部的、不需要全局互斥操作的设计。2. CenterNet全貌Objects as Points的工作方式2.1 三个输出头热力图、中心偏移、目标尺寸CenterNet的骨干网络可以选用ResNet、DLA或者Hourglass注意它不像普通检测网络那样最后把特征图下采样到很小而是希望输出特征图尽量保留空间分辨率通常保持在输入图像的1/4尺寸。这样做是有意的中心点定位越准最终框的质量越高。网络从骨干输出特征后会分出三个平行的预测头热力图头输出形状是H/4 × W/4 × CC是类别数。每个像素点的值代表该位置存在某类目标中心点的置信度。偏移头输出H/4 × W/4 × 2预测中心点因下采样产生的亚像素偏移(δx, δy)。尺寸头输出H/4 × W/4 × 2直接回归目标框的宽和高(w, h)。如果你在PyTorch里写大致长这样class CenterNetHead(nn.Module): def __init__(self, in_channels, num_classes): super().__init__() self.heatmap nn.Sequential( nn.Conv2d(in_channels, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, num_classes, 1) ) self.offset nn.Sequential( nn.Conv2d(in_channels, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 2, 1) ) self.size nn.Sequential( nn.Conv2d(in_channels, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 2, 1) ) def forward(self, x): return self.heatmap(x).sigmoid(), self.offset(x), self.size(x)整张图总共经过一次前向三个头并行输出后面没有RPN、没有ROI Pooling、没有特征对齐模块。复杂度控制得住而且易于工程化。2.2 训练目标变成往图上画高斯鼓包训练时每个真实目标框的中心点会被投影到下采样后的特征图坐标上然后在这个坐标对应的类别通道上画一个高斯鼓包。高斯鼓包的范围一般和目标大小挂钩目标越大鼓包半径越大。这样做的含义是离中心点越近的像素我们期望模型输出的置信度越高越远置信度越低让模型学到“中心点附近都是潜在的正位置”而不是只逼一个孤立像素。我这里给你一个典型的高斯核构造逻辑import torch def gaussian_radius(height, width, min_overlap0.7): # 根据目标尺寸估算高斯半径 # 具体实现方式在不同开源版本里略有差异核心是保证鼓包覆盖一定范围 a1 1 b1 (height width) c1 width * height * (1 - min_overlap) / (1 min_overlap) ... return radius def draw_heatmap(heatmap, cx, cy, radius): # 在热力图上以(cx, cy)为中心、r为半径画一个二维高斯 for x in range(cx - radius, cx radius 1): for y in range(cy - radius, cy radius 1): if 0 x heatmap.shape[1] and 0 y heatmap.shape[0]: v torch.exp(-((x - cx) ** 2 (y - cy) ** 2) / (2 * radius ** 2)) heatmap[y, x] max(heatmap[y, x], v)要注意的是如果两个高斯鼓包有重叠区域代码里通常取最大值而不是累加。取max的语义是“当两个目标的中心点非常接近时让高处的那一个主导训练信号”这个细节后面在讲中心点碰撞的时候还要提。2.3 下采样量化误差是怎么补回来的骨干网络输出的特征图只有输入尺寸的1/4这意味着真实中心点坐标从原图映射到特征图时会丢掉小数点后面的精度。极端情况下一个真实中心点在特征图上的理想坐标是(12.7, 8.3)取整之后变成(12, 8)错位最高可达将近1个像素放大回原图就是4个像素的偏差。小目标可能整个宽度才十几个像素4个像素的框位置误差已经非常可观。于是CenterNet额外加了一个偏移头让网络学习取整造成的残差量。损失直接用L1范数约束。这个头很轻量但它是保证小目标定位精度不可缺少的一块拼图。很多复现版本在写代码时容易漏掉这个头或者忘记把偏移加回最终坐标结果小目标AP明显下滑回头排查半天才发现是这种“不起眼”的细节。3. Loss设计里的三个关键细节Focal Loss、高斯半径与多目标重叠3.1 为什么一个点也要用Focal Loss热力图预测本质上是一个像素级的二分类问题而且正负样本的失衡程度比锚框时代更夸张。在一张512×512的图上下采样后特征图有128×12816384个位置类别数如果是80那这个三维特征图的负样本位置总数是百万级的而正样本只有图片里那几个目标中心的对应像素。如果直接用交叉熵模型会迅速学会把一切都预测成背景因为那样几乎总能拿到很低的loss。CenterNet沿用了Focal Loss的思想对易分类样本压低loss权重对难分类样本加大惩罚L_heat -1/N * sum( (1 - Y_hat)^alpha * log(Y_hat), if Y 1 (1 - Y)^beta * Y_hat^alpha * log(1 - Y_hat), otherwise )其中alpha一般取2beta取4。这个公式里最值得注意的不是Focal本身而是对不同负样本的差别对待离中心点近的像素但因为距离仍不算正样本因为有高斯鼓包给出了一个介于0到1之间的软标签它们对应的loss权重会被(1-Y)^beta压下去很多。这样模型不会强行把中心点附近打得非常低而是保留一个平滑的过渡区域。换句话说高斯鼓包和Focal Loss是一对配合动作前者提供软标签后者让软标签区域不被过度惩罚。3.2 高斯核半径与中心点碰撞高斯鼓包的半径怎么定比较粗暴的做法是根据目标宽高折算出一个sigma目标越大sigma越大。这在视觉上很直观大目标占据的面积大中心区域模糊一点也能接受小目标如果鼓包画得太大会把本不该属于它的响应用神经网络的“软标签”覆盖掉导致定位模糊。更麻烦的是中心点碰撞。当两个目标在图像上高度重叠比如街拍里前后两个人几乎挨在一起它们的中心点距离非常近各自的高斯鼓包会重叠。处理策略是取max但这带来一个直接后果靠近重叠区域的那个目标的峰值可能被另一个目标的鼓包“污染”其顶部置信度被压低推理时可能排不到top-k里或者峰值位置发生偏移。这一类错误在CenterNet里比在锚框方法里更值得警惕。锚框方法本身允许一个目标被多个视角描述CenterNet只允许“一个目标对应一个位置”。所以一旦中心点严重重合它理论上就失去了分开这两个目标的机会。实际使用时你会发现人群密集场景下CenterNet的漏检率明显比多阶段检测方法高这不是代码bug而是设计取舍。3.3 训练超参和增强配置参考我复现时采用的配置大致是输入分辨率512×512resize时使用长边缩放加padding的策略。骨干先用ResNet-18把pipeline跑通后面再换DLA-34提精度。优化器Adam初始学习率5e-4训练到约120个epoch时降到5e-5。数据增强随机翻转、随机缩放幅度0.6到1.3、随机裁剪、颜色扰动这些是基本操作。损失权重偏移loss权重1.0尺寸loss权重0.1。尺寸loss权重打这么低是有原因的。热力图是“在哪里”的问题它的错误影响面大尺寸回归是“多大”的问题如果给它太高权重训练时模型会把注意力过多放在目标大小回归上反而影响中心点的定位稳定性。实际调试中如果你发现模型框的位置看起来还行但尺寸忽大忽小可以先检查这个权重是不是被改得太高了。4. 推理3×3池化当NMStop-k直接出框4.1 峰值提取的具体操作CenterNet的推理流程可以拆成三步。第一步是拿到热力图之后对每个类别通道单独做一次3×3的最大值池化然后只保留那些“经过池化后仍等于原值”的像素点。这本质上就是在特征图上找局部极大值。从信号处理的角度理解3×3极大值滤波就是一个“隐式NMS”它把热力图上单峰周围那些略低的响应直接抹掉只留下最高峰。因为每个目标在训练时只会在中心点附近形成一个主峰峰值检测天然地做了“每目标一个预测”的互斥操作。如果用5×5甚至更大的池化核峰值会更稀疏但可能把两个靠得很近的独立目标同时抹掉一个降低召回池化核太小又会在物体边缘产生多个响声所以3×3是一个折中。4.2 一次前向算完的端到端pipeline找到所有峰值后通常按置信度从高到低排序截取前100个你也可以根据类别数量调整。然后对每个峰值从偏移头和尺寸头对应位置取出预测值还原成框坐标x (px offset_x - w / 2) * stride y (py offset_y - h / 2) * stride x2 (px offset_x w / 2) * stride y2 (py offset_y h / 2) * stride最后可以直接输出也可以用置信度阈值再做一次过滤比如低置信度的不要。整个过程没有任何按类别执行的非极大值抑制步骤没有动态规划式的框匹配也没有每个位置anchor的decode过程。你甚至可以写出一个不依赖循环、完全以矩阵操作完成的推理函数这对于在移动端或嵌入式设备上的算子裁剪非常友好。4.3 实测中哪些错漏依然会出现跑过几轮实验之后我发现无NMS设计必然伴随的几种失败模式第一是重影现象。如果某个目标中心点附近有两个局部极大值且两个峰值都被保留下来会输出两个几乎重叠的框。这种情况常见于纹理复杂的目标比如密集的斑马条纹或者衣服上图案极多的人像。3×3池化只能抑制紧邻的响应抑制不了次峰出现在稍远位置的情况。第二是漏检。因为top-k是全局选取如果一张图上目标很多置信度排在后面的目标可能被截掉。实际项目中如果目标数量波动很大需要动态调整top-k参数。第三是坐标偏移。偏移头本身也是网络预测出来的如果训练样本里小目标占比太低偏移头容易学到平均值偏大的解导致小目标框中心点系统性外移。遇到这种情况单纯加数据、调loss作用有限更直接的办法是把输入分辨率提上去让下采样损失降低。5. 一套框架包打2D/3D/姿态CenterNet的多头扩展5.1 加个分支预测深度和朝向变成3D检测器CenterNet那种“目标中心点若干属性”的抽象方式有一个天然优势检测框只是属性的一种你想预测什么属性就在中心点所在位置加回归头。做3D目标检测时除了2D框的宽高还需要知道目标的深度、三维尺寸和朝向角。CenterNet的处理方法是在中心点特征上回归一个深度值并用带约束的目标函数让深度预测与真实投影一致同时预测目标的长度、宽度、高度以及一个朝向角表示。推理时利用相机内参把2D中心点和3D尺寸投影回三维空间就能恢复出3D包围盒与姿态。这样做比很多纯3D方法简洁因为它把2D检测的高召回率优势和关键点定位的精确性直接复用到了3D任务上。我曾用这套思路做过一个小型实验在自动驾驶场景数据集上只改输出头和损失函数2D框的质量几乎影响不大3D检测指标也能跑到一个可用的水平这足以看出“点表示”的泛化能力。5.2 中心点到关键点姿态估计的编码方式人体姿态估计的常见做法是预测每个人的所有关节热力图然后做分组CenterNet则提供了一种更直接的编码把每个关节的位置建模成“相对于目标中心点的偏移”。具体来说如果网络已经找到了一个人的中心点那么在这个中心点对应的特征向量上再回归出17个关节的二维偏移坐标对于COCO是17个关键点。训练时把关键点真实坐标和中心点坐标做差作为监督目标。推理时先得到中心点再通过偏移直接算出关节位置不需要逐关节做空间分组。这样做的优点是不需要额外的分组算法中心点自己就把每个人“钉”住了缺点是如果中心点定位失败所有关键点都跟着失效误差会沿着整条链路放大。所以这类任务的版本通常对输入分辨率的要求比纯2D检测更高。6. 同一时期关键点流派CenterNet vs CornerNet/ExtremeNet6.1 中心点和角点谁更适合做“锚”在CenterNet之前用关键点做检测的代表是CornerNet它预测目标的左上角和右下角两个角点再通过embedding向量把属于同一个目标的角点配对起来。这个思路在当时很惊艳但也留下了明显的隐患角点往往落在背景区域那里的视觉信息极其匮乏模型很难从背景纹理中判断“这里是不是目标的角”而且两个角点配对依赖的高维embedding在目标密集时容易发生错配一个左角和一个左角也可能被分到一组。CenterNet把关键点从“角点”换成了“中心点”这一手实际上是回到了更符合人类直觉的表示中心点一般落在目标内部它周围的上下文信息物体颜色、纹理、边缘比角点丰富得多所以模型更容易把这个点学得稳。更重要的是没有了角点配对步骤也就没有了那个需要调阈值的embedding匹配过程整个算法干净了一个量级。其实ExtremeNet已经做过类似探索它同时预测目标的四个极值点和一个中心点然后用几何关系组合候选框。这种方式也避开了角点配对但需要生成大量由极值点组合而成的候选框最后还是要按几何中心是否与预测中心点一致来打分。CenterNet算是把这条线收敛到了极致我只要中心点其他东西都回归出来。6.2 后续FCOS等路线与CenterNet的取舍CenterNet发表后不久FCOS等为代表的另一个Anchor-Free流派迅速发展起来。FCOS在特征图的每个位置直接预测到目标框四条边的距离同时用center-ness分支给每个预测框打一个“中心性”分数。其思路不是“找一个唯一的峰值”而是“允许每个像素都预测再按质量分数筛选”。从实现效果看FCOS的召回率通常比CenterNet高因为它可以输出多个候选框再筛但这也意味着FCOS并没有真正做到no NMS——它在推理时仍然需要一次非极大值抑制来清理密集输出。CenterNet则把多候选框的空间直接砍掉了换来了极简的后处理和推理流程。两种路线没有谁绝对强取决于你更看重极限的速度和简单性还是看重密集场景下的上限精度。7. 跑CenterNet的落地经验复现、调参、避坑7.1 最小可运行的PyTorch推理demo如果你只想快速验证CenterNet的效果不需要从头训练。用官方开源的预训练权重跑一张图流程其实很短读图、做归一化、保证输入尺寸是stride的整数倍、前向得到热力图和回归头、按上面说的峰值提取步骤取top-k、画框。我自己第一次写推理代码时犯过一个低级错误没有把图像的中心偏置补齐。CenterNet在预处理时会在图像四周补边使坐标对齐到像素中心。如果漏掉这一步输出框整体会偏移大约半个到1个像素小目标看起来不明显但大目标框边会有肉眼可见的错位。这类问题属于“不跑测试永远意识不到”的那种所以我建议复现时先拿官方样例图去对比确认坐标还原步骤和官方实现完全一致。7.2 小目标与密集场景的处理心得对于小目标最有效的调节不是换loss而是提高输入分辨率。从512×512提高到768×7684倍下采样后特征图分辨率同步增加小目标的中心点可定位性会显著改善。当然代价是推理速度下降设备算力紧张时要做权衡。对于密集场景我通常会做一个简单小实验来评估CenterNet是否适合当前业务统计数据集里两个目标中心点之间的最小距离。如果大量目标中心点之间的像素距离小于10那么恭喜你用CenterNet大概率要面对一批吵成一团的目标。你可以在目标周围扩大高斯核半径尝试缓解但本质限制很难突破这种情况下不如直接换成FCOS或带NMS的检测器后处理成本换推理稳定性是值得的。7.3 无NMS给工程化带来的实际好处最后说一个工程上容易被忽视的收益把NMS从pipeline里拿掉不仅仅是省了一个算子而是省掉了一整条动态形状处理的链路。NMS的输入是变长的box列表内部包含排序、循环、条件判断在GPU上还好说在移动端CPU上往往是耗时最高的瓶颈之一。CenterNet的推理可以写成固定形状的张量操作峰值提取、top-k、坐标还原每一步都是静态shape这对芯片部署非常友好。你可以把整个过程完全在线程内跑完不需要引入额外的后处理框架。这也是为什么在一些低功耗设备上CenterNet的端到端时延反而比那些论文指标更花哨的方案好看得多。我自己跑完这个模型后最大的感受是“少即是多”不只是设计哲学在工程上确实能换成真金白银的延迟收益。如果你要对它做二次开发也别急着往上堆模块先把输入分辨率、高斯半径、top-k这三个参数在你的业务数据上调平衡很多问题根本不需要改网络结构就能解决。