说句实话,目标跟踪方向这几年迭代太快,新模型一个接一个,但我始终觉得有一份基础工作值得反复读、反复复现,那就是 SiamFC,全称 Siamese Fully Convolutional Network,也就是全卷积孪生网络。它的项目标题“siamfC”虽然写起来有点随意,却是多少跟踪项目改来改去都绕不开的起点。如果你正准备复现一个经典跟踪器,或者刚进入单目标跟踪这个方向,我强烈建议先把这套思路吃透。
SiamFC 能做的事情非常朴素:给定第一帧里的目标框,在后续每一帧里持续把这个目标找出来。它不依赖于在线微调,不搞复杂的候选框回归,核心就一句话——把跟踪变成模板匹配。它解决了当时深度学习跟踪“精度上去了但速度惨不忍睹”的痛点,在 GPU 上跑出上百帧每秒的速度,同时又比传统相关滤波类方法在鲁棒性上高出一截。这篇文章不会从头讲目标跟踪是什么,而是直接落在实现层面,把论文里没写清楚、代码里又绕不过去的细节全部摊开。
1. 为什么孪生网络把目标跟踪变成了“一次卷积”
1.1 传统跟踪器始终绕不开的“在线更新”包袱
早些年大家做跟踪,脑子里几乎都有一个定式:每一帧都要在线学一个分类器。经典的相关滤波方法就是这种思路的极致——用循环移位构造大量训练样本,在频域里算一个岭回归分类器,然后逐帧更新。这个路线的优点是快,但问题也很明显:特征基本靠手工设计,梯度直方图这类特征应对光照、形变和旋转的能力有限;另外每次更新分类器本质上是在用当前帧附近的上下文“临时抱佛脚”,一旦目标被遮挡或者背景里出现长得像的干扰物,分类器很容易被污染。
后来深度学习进场,很多早期工作还是带着“在线微调”的惯性走。思路大体是:用一个预训练好的卷积网络提取特征,然后在跟踪过程中不断拿新样本微调网络参数,甚至重新训练一个分类层。效果确实比传统方法好不少,但那是用速度换来的。我印象很深,当时有些模型跑一帧要一两百毫秒,肉眼可见地卡顿,放到监控场景或者嵌入式设备上根本没法落地。这里面的根本矛盾在于:跟踪是一个时序任务,你不可能像做分类那样离线训一次就万事大吉,但你又不敢完全不更新模型,否则目标一变化就跟丢。
1.2 SiamFC 的解题思路:离线学相似度,在线只做匹配
SiamFC 的出现把这个问题换了一个角度。它不试图在跟踪过程中更新任何网络参数,而是提前离线训练好一个相似度函数。这个函数接收两个输入:一个是模板,也就是第一帧的目标外观;另一个是当前帧的搜索区域。网络输出一个得分图,得分最高的位置就是目标所在。整个过程在推理时只有一次前向计算,不需要求解任何优化问题,也不需要反向传播。
这种“离线学相似、在线做匹配”的设计,最大的价值是把跟踪问题的复杂度拆开了。离线阶段你可以拿海量视频数据去训练网络,让它学会“什么是同一个物体的不同外观”“什么情况下两个区域不应该是同一个目标”;而在线阶段,网络权重完全冻结,你只需要保证模板和搜索区域的裁剪、缩放方式与训练时一致即可。这样一来,跟踪器在运行时几乎没有额外的学习开销,速度自然能提上去。
我当时第一次看到这个思路时,心里其实是有点怀疑的:模板锁死了,目标后面变化很大怎么办?这个问题确实存在,SiamFC 的原始设计里也有对应的补救手段,比如多尺度搜索、模板更新等。这些细节放在后面细聊,但从方法论上讲,这个框架让我意识到一件事:跟踪器不一定非得“在线学习”,它完全可以靠离线学到的通用相似度来应对未知视频,这才是它真正领先于同时代工作的原因。
2. 全卷积加互相关:模板和搜索区是怎么结合起来的
2.1 全卷积到底省掉了什么
SiamFC 名字里的“全卷积”不是随口说的,它是一个关键设计。常规的分类网络在最后会接上全连接层,把二维特征图压成一维向量,这样做的代价是彻底丢掉空间位置关系。而跟踪任务恰恰需要知道目标“在哪个位置”,所以 SiamFC 把全连接层全部砍掉,从输入到输出都保留了二维空间结构。
保留空间结构带来的直接好处是:网络输出本身就是一个响应图,而不是一个抽象的类别分数。你可以把响应图想象成一张热力图,图中每一个点的值表示“模板在这个位置上的相似程度”。这样目标的位置信息被编码在空间坐标里,而不是被拍扁成一堆数字。更重要的是,这种结构天然具有平移等变性——目标在输入图像里往右挪了几个像素,响应图的峰值也会对应地往右移动,网络不需要重新学习这种位移关系。
我在复现的时候特别喜欢这个特性,因为它让代码变得极其干净。不需要写一堆锚框、候选区域、后处理逻辑,只需要定义一个卷积网络,然后把模板特征当卷积核,在搜索区域特征上滑一遍,就完成了匹配。如果你理解二维卷积,那你已经理解了 SiamFC 的一半。
2.2 从 127×127 和 255×255 到 17×17 响应图的运算过程
具体到输入输出,SiamFC 的常用配置是:模板分支输入 127×127 的彩色图,搜索分支输入 255×255 的彩色图。为什么是这个尺寸?因为这两个尺寸经过同一个特征提取网络后,模板特征图是 6×6×128,搜索区域特征图是 22×22×128,而 6×6 的核在 22×22 的特征图上做互相关,刚好得到一个 17×17 的输出。这个 17 不是随意凑出来的,它等于 (22 - 6 + 1),对应目标中心在整个搜索区域内允许出现的所有可能位置。
让我把这个过程说得更直白一点。把模板特征当做一个卷积核,对搜索区域特征做卷积。卷积核尺寸是 6×6,通道数是 128,输入特征图也是 128 个通道。在每个空间位置上,计算模板特征与局部区域特征的逐通道点积之和,得到一个相似度分数。滑动完成后,17×17 的响应图里每一个值就代表“目标在当前这个位置上的相似程度”。
如果目标恰好位于搜索区域的中心,那么响应图的最高峰应该出现在中心位置。如果目标向右下方移动了 16 个像素,由于网络的总步长是 8,响应图的峰值就会相对中心偏移 2 个格子。所以在代码实现里,把峰值位置与中心位置的偏移量乘以步长 8,就能换算出目标在当前帧中的实际位移。这个换算关系虽然简单,但一旦步长设置不一致,或者裁剪尺寸不对,跟踪位置就会整体偏移,这是人人都容易踩的坑。
2.3 为什么不直接加一个边界框回归
看到这里你可能想问:既然都算出了目标位置,为什么不顺便把目标尺寸也回归出来?SiamFC 原始版本确实没有做尺寸回归,它只用三种尺度缩放搜索区域,然后用响应图峰值来选尺度。这种设计在现在看来有点“糙”,但在当时是刻意的——回归目标框的宽高需要额外引入监督信号和回归头,模型复杂度上去了,训练难度也上去了。SiamFC 选择先把“定位”做到极致,把“尺度变化”交给多尺度搜索去兜底。这种“先解决主问题、用工程手段补次问题”的思路,我觉得很值得借鉴。
3. 训练阶段的细节:采样、标签和损失
3.1 训练样本配对与裁剪的正确姿势
SiamFC 的训练数据是成对出现的,每一对都来自同一段视频里的两帧。模板帧提供目标的外观,搜索帧提供目标可能移动后的位置。关键不在于直接从视频里截两张图像,而在于如何裁剪。
我的做法是:先读一组标注好的目标框,然后对每一帧做一次随机平移和随机尺度缩放,再把目标框映射到裁剪图上。模板帧以目标中心为裁剪中心,裁出以目标为中心的方块区域,边长按目标框的短边或长边乘以一个扩边系数处理,比如常见的是把目标框扩大 2.6 倍左右再截取,最后 resize 到 127×127。搜索帧同样以目标中心为裁剪中心,但裁剪边长更大,通常取模板裁剪边长的两倍左右,最后 resize 到 255×255。这样保证同一个目标在模板和搜索区域里的尺度一致,只是因为随机平移而位置不同。
这里有一个容易被忽略的点:裁剪之后要做一个“以目标为中心”的归一化。目标不在裁剪区域中心的时候,模板和搜索区域的对齐关系就会乱,网络会学到错误的匹配模式。我早期复现时偷懒直接用原始坐标截取,结果训练 loss 能降到很低,但跟踪时峰值总偏一点,后来才发现是训练数据里目标中心没有统一对齐。
3.2 损失函数与正负样本半径怎么设
SiamFC 的损失函数定义在响应图的每一个空间位置上。预测值就是互相关输出的分数,经过 sigmoid 变成概率;标签只有两类:这个位置是不是目标中心。问题是“是不是目标中心”的边界怎么定。
论文里用了一个简单的规则:以目标中心对应的空间位置为圆心,半径 R 以内的点都算正样本,其余算负样本。这个 R 直接决定训练的宽松程度。R 太小,正样本区域就只有几个像素,网络学不到“目标周围一点也是目标”的容错能力;R 太大,负样本太少,分类器容易把背景也当成目标。常见实现里 R 会设成 4 到 8 个像素不等,需要配合响应图的总步长来换算。我个人的经验是,这个参数宁可设小一点,也不要设大,因为跟踪器最忌讳的是对背景的误判。
损失函数的具体形式是逐点的逻辑损失,等价于把每个位置看成一个二分类问题。把所有位置的损失求和后取平均,再加一点权重平衡正负样本。实际写代码时你会发现,正负样本比例可能悬殊,尤其是目标较小的时候,负样本可能占百分之九十九。因此要么对正样本加权,要么用类似焦点损失的思想,把难分样本的权重调高。原始论文没有做复杂处理,但我建议你在实践中至少做一个样本均衡,否则训练初期的梯度基本被背景淹没。
3.3 训练时的增强与优化器配置
数据增强方面,我复现时用了三件套:灰度扰动、尺度扰动、翻转。灰度扰动模拟光照变化;尺度扰动模拟目标远近变化;翻转比较微妙,因为跟踪任务其实是具有左右不对称性的——物体从头到尾的运动方向可以改变外观朝向,但翻转不能做得太过分。我的做法是只做水平翻转,而且只在部分样本上做,避免网络学到错误的方向一致性。
优化器方面,原始训练用随机梯度下降,学习率采用从大到小的退火策略。我实验下来,使用带动量的随机梯度下降比自适应学习率方法更稳定。自适应优化器在训练初期确实爽,但后期容易在响应图上产生一些很尖锐的伪峰值,导致验证集分数波动。同样的数据量下,随机梯度下降收敛更慢,但学到的特征泛化性明显更好。
还有一点是关于批归一化。SiamFC 的特征网络当时是基于简化版的卷积网络,没有像后来的网络那样大量堆叠批归一化。如果你在自己的实现里加入了批归一化,一定要小心:跟踪推理时模板和搜索区域通常是一起做前向的,不会单独处理;如果批归一化在训练和推理之间的行为不一致,响应图的分布会发生偏移。很多复现版本直接取消批归一化,或者用实例归一化代替,都是有道理的。
4. 推理侧细节:多尺度、惩罚窗口和模板更新
4.1 多尺度搜索背后的精度与速度取舍
推理阶段,SiamFC 每次只在当前帧裁剪一个搜索区域,输入网络得到响应图,然后取最大值位置。但目标大小会变,如果搜索区域里目标缩小了,直接套用原尺寸仍然会得到一个峰值,但峰值强度可能不够高,位置也可能偏。
所以推理时通常要跑三个尺度。做法是把搜索区域先按尺度系数放大或缩小,再裁剪到同样的网络输入尺寸。尺度系数一般取 1.025 的负一次方、零次方和一次方,也就是三个层次很接近的缩放。为什么要取得这么密?因为尺度变化如果太剧烈,响应图的峰值会有跳动;太稀疏又跟不上目标的连续变化。跑三个尺度后,取三者中响应值最高的那个作为当前帧结果,同时用这个尺度更新目标框大小。这个办法很土,但在当时已经能覆盖相当一部分尺度变化场景,而且计算量只多出两倍,依然可以实时。
4.2 惩罚窗口:为什么裸响应图不能直接用
这是复现 SiamFC 时最容易忽略、也最容易出问题的部分。直接取响应图最大值是完全不够的,因为跟踪器和相关滤波器一样,存在边界效应。目标如果接近搜索区域边缘,就会只露出一半甚至更少,此时模板和外露部分匹配出的相似度可能异常高,导致跟踪器把峰值误判到边缘位置,也就是所谓的“边界反应”。
SiamFC 的处理方法是给响应图乘一个余弦汉宁窗。汉宁窗从中心向四周衰减,中心保持为 1,边缘趋近于 0。这样即使边缘位置本身响应值很高,也被窗口惩罚系数压下去了。最终的选择是对加窗后的得分图取最大值。这个窗的惩罚强度是一个超参数,一般取 0.1 到 0.35 之间。我试过不乘窗和乘窗的差别:不乘窗时,目标一旦跑到图像边缘附近就很容易“跳走”;乘窗之后,同样场景下跟踪稳定很多。这个细节值得反复调,找到适合你场景的系数。
4.3 模板更新的保守策略
SiamFC 原始论文里的模板并不是完全固定的。理想情况下,用第一帧的目标作为标准模板可以避免误差积累,但目标外观变化后,固定模板的匹配分数会逐渐降低。于是很多实现采用了一种简单而有效的折中:把当前帧的结果作为新模板的候选,与初始模板做带权平均。具体权重有人用 0.5,有人用 0.6,也有人只在响应峰值超过一定阈值时才更新模板。
我在实际项目里更倾向于做一个“更新门控”:只有当当前帧的峰值分数高于历史平均分数的一定比例时才更新模板,否则保持原模板不动。这样做的好处是,目标一旦被遮挡或者出现剧烈形变,跟踪器不会立刻把错误目标的外观学进来;等到目标重新出现、分数恢复,再开始更新。这种做法脱离了原始论文的设限,但更接近工程上的真实需求。
另外提醒一点,模板更新的对象不只是模板图像本身,还有目标框的尺度信息。尺度更新也需要做平滑,比如新的目标框尺寸取历史尺寸和当前检测尺寸的加权平均,而不是一步到位。一步到位的隐患是,只要某一帧尺度估计出错,目标框就会越调越歪,后续帧再用这个歪框去裁剪,结果只会更歪。
5. 复现时最容易出现的坑与完整排查链路
5.1 损失一直不降:先怀疑数据配对和标签
如果你复现的同时发现 loss 卡在一个高位不动了,不要急着调网络结构,先怀疑两件事:数据配对和标签。
我遇到过最典型的错误是,训练样本里“模板帧”和“搜索帧”不是来自同一段视频,或者虽然来自同一段视频但目标框对不上。这样网络学到的东西没有任何意义,损失自然降不下去。排查方法很简单:把训练数据里随机抽出一对样本,把模板和搜索区域的裁剪图直接可视化,看一眼目标是不是同一个人、位置和尺度是不是合理。
标签方面,最容易搞错的是正负样本半径和响应图尺寸不对应。如果你的特征网络改动过、步长变了,响应图尺寸就不再是 17×17,标签坐标也要跟着换算。很多复现版本直接用 17×17 去算标签,一旦网络输出不同,标签就对不上了,loss 看起来在降,但实际上是在把“不匹配的错”学成正确。这种隐蔽错误在跟踪测试阶段才会爆发,损失曲线反而骗过了你。
5.2 目标跟踪中途漂移:根因往往在尺度而不在位置
跟踪器跑着跑着突然跳到背景上的相似物体上,这是 SiamFC 复现里最让人头疼的问题。很多人第一反应是模板更新权重太大,于是把更新系数调小,但往往没啥效果。
我复盘过几次漂移案例,发现相当一部分根因在尺度更新上。具体表现是:目标框越变越大或越变越小,一旦框里包含了太多背景,后续帧的模板就被背景“绑架”了。背景区域和某个干扰物长得相似时,响应峰值就会跳到干扰物上。所以排查漂移时,除了看模板更新策略,还要看尺度更新是否足够平滑。一个很有效的改进是把尺度变化限制在一个小的范围里,当前帧尺度估计如果超出历史尺度一定比例,就直接截断到边界值。这个方法成本为零,效果却很明显。
另外提醒你注意惩罚窗口的权重是否和你的目标尺寸匹配。目标越小,它在响应图中的峰值越弱,对边缘惩罚的容忍度越低;目标越大,峰值越宽,汉宁窗的抑制效果反而会压掉真实峰值。所以我一般在窗口惩罚系数上做一个和目标框大小相关的自适应调整,大目标用小惩罚,小目标用大惩罚。
5.3 边界和遮挡:搜索区域超界与恢复机制
目标跑到画面边缘附近时,搜索区域的裁剪框会超出原始图像边界。多数实现会选择用均值像素做填充,但这样会在响应图边缘产生一圈“假峰值”,因为填充区域和模板匹配时,往往会出现虚假的高相似度。即便有汉宁窗惩罚,也不能完全消除这种影响。
我自己的处理是:在裁剪之前先算出搜索区域与原始图像的交集,计算实际裁剪范围;超出边界的部分用均值像素填充,同时在响应图里把对应位置的权重降为零或者特别小的值。这样处理之后,边界处的假峰值基本消失,目标靠近边缘时的跟踪稳定度提升明显。
遮挡是 SiamFC 的另一个天然软肋。模型没有记忆,也没有重检测机制,一旦目标被完全遮挡几帧,跟踪器只能靠模板在搜索区域里找一个最像的位置,这个位置很可能是错的。应对办法也很直接——在推理时加入低峰判断:如果当前帧的响应最大值明显低于历史上的正常水平,就认为目标处于遮挡状态,此时不要更新模板,也不要更新目标框,只保持上一帧的位置和尺度,甚至可以适当扩大搜索范围。这个逻辑看似朴素,却是让 SiamFC 在真实视频里能扛住短时遮挡的关键。
6. SiamFC 的能力边界与后续扩展
6.1 它到底怕什么
SiamFC 的结构决定了它的几个上限。第一,它假设模板和搜索区域是同一个尺度的相似物体,对剧烈尺度变化只能靠多尺度搜索硬扛,扛不住就会出现框漂;第二,它没有在线更新的判别能力,一旦画面里出现和模板长得很像的干扰物,响应图会出现双峰甚至多峰,这时候单靠取最大值很容易选错;第三,它缺少记忆和重检测机制,目标短暂出画面后再进来,基本上是重新找一遍,找不到就是跟丢。这些不是 bug,而是这套框架本身的天花板。知道它的边界在哪里,你才能在选型时不至于拿它硬扛长时跟踪任务。
6.2 从 SiamFC 往后续扩展的常见路线
如果你打算在 SiamFC 基础上继续前进,业界已经验证过几个扩展方向。一个是把互相关升级为更复杂的匹配模块,通过在通道维度上做逐元素相乘或注意力加权来实现,对应后续一系列孪生跟踪器的思路;另一个是加入分类和回归双分支,在预测目标位置的同时回归目标框宽高,这解决了 SiamFC 原始版本无法输出精确尺度的问题;还有一个是加入在线更新分支,让模板外面套一层可以快速调整的小网络,兼顾速度和判别能力。
在工程落地上,如果算力有限,SiamFC 反而还是首选基线。它的模型体量小、前向计算简单,很容易量化到低精度模型上;模板和搜索区是独立的两个分支,偶尔还可以换一种工程设计思路——把模板特征预先算好在缓存里,后续每一帧只跑搜索分支,进一步节省计算量。我在某个边缘平台上就这么干过,模板特征只在首帧或模板更新时计算一次,后续帧的耗时直接减半。
我在实际复现和调试 SiamFC 的过程中,最大的体会是:好的算法不需要堆砌太多花活,关键是每个设计点都打在痛点上。它的全卷积结构、互相关运算、离线训练方式,每一个选择都指向一个朴素而明确的问题。如果你能亲手把训练、推理、后处理全部跑通,再去读后续的一切跟踪论文,思路会顺畅很多。复现经典算法的意义也正在于此——你补上的不只是代码,而是对“为什么这样设计”的真正理解。