一、基本原理SiamFC 是 2016 年由 Luca Bertinetto 等人提出的开创性深度目标跟踪方法其核心思想为将目标跟踪转化为寻找相似相似度学习的问题也就是利用模板图像在搜索区域中找最相似的地方。核心构架孪生网络互相关孪生网络由两个分支构成。第一个是模块分支对于输入视频的第一帧裁剪出来的目标模块图像提取特征第二个是搜索分支对于每一个下一帧的搜索区域图像提取特征。两个分支使用完全相同的卷积神经网络提取的特征通过互相关操作来计算相似度生成响应图响应图中值最大的位置就是目标所在的位置。二、关键设计本项目对于SiamFC有以下关键设计1.全卷积整个网络本项目采用AlexNet只有卷积层没有全连接层所以可以接受任意尺寸的输入。卷积层相当于扫描仪对图像每一个地方进行扫描记录扫描区域特征每一次扫描产生一个数值最后将大图像浓缩为特征图。全连接层将所有信息排成一串进行处理。所以对于像素过大的图片全连接层的第一个节点就有可能为十几万个权重一层的参数更多容易过拟合。如果存在全连接层输入图像的尺寸必须是固定的所以全卷积层输入图像的尺寸会更灵活。2.共享权重模块和搜索分支共享参数学习到的特征对二者均适用。3.端到端训练在GOT-10k数据集上用逻辑损失进行端到端的训练。逻辑损失简单来说就是打分标准预测对了高分预测错了给低分高分就是小损失低分就是大损失。通常设置标签 target 1 为目标中心target 0 为背景越靠近0说明损失越大。端到端训练首先输入然后AlexNet进行特征提取再互相关计算相似度最后得到损失函数。整个输入到输出的过程是一个整体后续可以通过反向传播的梯度来根据结果优化参数特征提取器自动学会提取分类器所需要的特征。4.多尺度测试推理是采用3种尺寸的搜索图像对应目标尺度变化。选择多尺度三种尺度分别设置为原始大小×0.964原始大小×1原始大小×1.0375这是由于如果目标在画面范围内变小了就将搜索框设置大一点反之就将搜索框变小一点哪个尺度下产生的响应图峰值最高就说明在该尺寸下目标最清晰。5.汉宁窗惩罚对响应图施加汉宁窗一直远离中心的响应鼓励平滑运动。汉宁窗惩罚相当于给响应图加滤镜中间亮四周按也就是将响应图目标区域与非目标区域划分的更加清晰。使用该惩罚是防止目标位置突然跳到很远的地方还可以保证跟踪的稳定性。平滑运动物体在两帧之间的移动距离连续的、逐渐变化的不会突然跳变。优点速度极快满足实时性要求结构简单易于理解端到端训练无需微调。缺点使用简单的AlexNet特征表达能力有限无模块更新一旦漂移无法恢复。三、关键代码实现1.骨干网络class AlexNetV1(_AlexNet): output_stride 8 # 总步长 8输入到输出的缩放比 def __init__(self): super(AlexNetV1, self).__init__() self.conv1 nn.Sequential( nn.Conv2d(3, 96, 11, 2), # 输入3通道RGB输出96通道11×11卷积核步长2 _BatchNorm2d(96), # BatchNormeps1e-6, momentum0.05 nn.ReLU(inplaceTrue), nn.MaxPool2d(3, 2)) # 3×3最大池化步长2 self.conv2 nn.Sequential( nn.Conv2d(96, 256, 5, 1, groups2), # groups2 是 AlexNet 的特性 _BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(3, 2)) self.conv3 nn.Sequential( nn.Conv2d(256, 384, 3, 1), _BatchNorm2d(384), nn.ReLU(inplaceTrue)) self.conv4 nn.Sequential( nn.Conv2d(384, 384, 3, 1, groups2), _BatchNorm2d(384), nn.ReLU(inplaceTrue)) self.conv5 nn.Sequential( nn.Conv2d(384, 256, 3, 1, groups2)) # 最终输出256通道没有ReLU和池化使用AlexNet是因为它在速度和性能之间取得了良好平衡output_stride 8 意味着输入 255×255 的图像经过 5 层卷积和池化后特征图尺寸为 255/8 ≈ 22×22。与标准AlexNet的区别在于去掉了 LRN 采用了 BatchNorm 移除了全连接层做全卷积设计第五层卷积不做ReLU和池化保留特征的正负响应。2.检测头class SiamFC(nn.Module): def __init__(self, out_scale0.001): super(SiamFC, self).__init__() self.out_scale out_scale # 输出缩放因子防止响应值过大 def forward(self, z, x): return self._fast_xcorr(z, x) * self.out_scale def _fast_xcorr(self, z, x): # 快速互相关将模板特征作为卷积核在搜索特征上滑动 nz z.size(0) # 模板数量batch size nx, c, h, w x.size() # 搜索特征的形状 x x.view(-1, nz * c, h, w) # 重塑以便分组卷积 out F.conv2d(x, z, groupsnz) # 分组卷积实现互相关 out out.view(nx, -1, out.size(-2), out.size(-1)) return out互相关的物理意义conv2d(x, z, groupsnz) 本质上是将模板 z 作为卷积核去卷积搜索区域 x。模板特征尺寸为 6×6搜索特征为 22×22卷积结果尺寸为 22-6117即最终响应图是 17×17。将特征图进行相似度计算。3.网络组装class Net(nn.Module): def __init__(self, backbone, head): super(Net, self).__init__() self.backbone backbone # AlexNetV1 self.head head # SiamFC互相关层 def forward(self, z, x): z self.backbone(z) # 模板特征 x self.backbone(x) # 搜索特征 return self.head(z, x) # 互相关 → 响应图将骨干网络和检测头组合成完整网络。前向传播流程模块图像经过骨干网络的卷积得到模块特征搜索图像经过骨干网络的卷积得到搜索特征模块特征和搜索特征经过互相关函数的计算得到响应图响应图中值最大的就是目标的中间位置。4.损失函数class BalancedLoss(nn.Module): def __init__(self, neg_weight1.0): super(BalancedLoss, self).__init__() self.neg_weight neg_weight def forward(self, input, target): pos_mask (target 1) # 正样本目标附近 neg_mask (target 0) # 负样本背景区域 pos_num pos_mask.sum().float() neg_num neg_mask.sum().float() weight target.new_zeros(target.size()) weight[pos_mask] 1 / pos_num # 正样本权重 weight[neg_mask] 1 / neg_num * self.neg_weight # 负样本权重 weight / weight.sum() return F.binary_cross_entropy_with_logits( input, target, weight, reductionsum)加权二元交叉熵损失响应图上目标周围 R16 像素内的位置视为正样本其余为负样本。由于正样本远少于负样本约 100:1用权重平衡使正负样本贡献均等。5.工具函数def crop_and_resize(img, center, size, out_size, border_typecv2.BORDER_CONSTANT, border_value(0, 0, 0)): # 以 center 为中心裁剪 size×size 的区域缩放到 out_size size round(size) corners np.concatenate(( np.round(center - (size - 1) / 2), np.round(center - (size - 1) / 2) size)) corners np.round(corners).astype(int) # 如果超出图像边界用 border_value 填充 pads np.concatenate(( -corners[:2], corners[2:] - img.shape[:2])) npad max(0, int(pads.max())) if npad 0: img cv2.copyMakeBorder(img, npad, npad, npad, npad, border_type, valueborder_value) # 裁剪并缩放到目标尺寸 corners (corners npad).astype(int) patch img[corners[0]:corners[2], corners[1]:corners[3]] patch cv2.resize(patch, (out_size, out_size)) return patch将图像中以任意点为中心、任意尺寸的区域裁剪并缩放到固定尺寸。6.主跟踪器def init(self, img, box): self.net.eval() # 将标注框从 (x,y,w,h) 转为以中心点表示 (cy,cx,h,w) box np.array([ box[1] - 1 (box[3] - 1) / 2, # 中心 y box[0] - 1 (box[2] - 1) / 2, # 中心 x box[3], box[2]], dtypenp.float32) # 高, 宽 self.center, self.target_sz box[:2], box[2:] # 创建汉宁窗用于抑制边缘响应 self.upscale_sz self.cfg.response_up * self.cfg.response_sz # 16×17272 self.hann_window np.outer( np.hanning(self.upscale_sz), np.hanning(self.upscale_sz)) self.hann_window / self.hann_window.sum() # 3种尺度缩放因子1.0375^{-1}, 1.0375^0, 1.0375^{1} self.scale_factors self.cfg.scale_step ** np.linspace( -(self.cfg.scale_num // 2), self.cfg.scale_num // 2, self.cfg.scale_num) # 计算模板裁剪尺寸目标区域加上上下文边距 context self.cfg.context * np.sum(self.target_sz) # 0.5×(hw) self.z_sz np.sqrt(np.prod(self.target_sz context)) # 模板边长 self.x_sz self.z_sz * self.cfg.instance_sz / self.cfg.exemplar_sz # 搜索边长 # 裁剪模板图像并提取特征 self.avg_color np.mean(img, axis(0, 1)) z ops.crop_and_resize(img, self.center, self.z_sz, out_sizeself.cfg.exemplar_sz, # 127 border_valueself.avg_color) z torch.from_numpy(z).to(self.device).permute(2, 0, 1).unsqueeze(0).float() self.kernel self.net.backbone(z) # 保存模板特征后续帧复用初始化时将标注框从 (x,y,w,h) 角点格式转为中心点 (cy,cx,h,w) 格式并且根据目标大小和上下文边距计算出模板和搜索区域的物理尺寸最后裁剪模板图像提取特征后保存后续所有帧都复用该特征。7.跟踪阶段def update(self, img): self.net.eval() # 1. 裁剪3种尺度的搜索区域多尺度测试 x [ops.crop_and_resize(img, self.center, self.x_sz * f, out_sizeself.cfg.instance_sz, # 255 border_valueself.avg_color) for f in self.scale_factors] x np.stack(x, axis0) x torch.from_numpy(x).to(self.device).permute(0, 3, 1, 2).float() # 2. 提取搜索特征计算响应图 x self.net.backbone(x) responses self.net.head(self.kernel, x) # 互相关 responses responses.squeeze(1).cpu().numpy() # 3. 响应图双三次插值上采样到 272×272 responses np.stack([cv2.resize(u, (self.upscale_sz, self.upscale_sz), interpolationcv2.INTER_CUBIC) for u in responses]) # 4. 惩罚非中间尺度的结果鼓励保持原始尺度 responses[:self.cfg.scale_num // 2] * self.cfg.scale_penalty # 0.9745 responses[self.cfg.scale_num // 2 1:] * self.cfg.scale_penalty # 5. 选响应值最大的尺度 scale_id np.argmax(np.amax(responses, axis(1, 2))) # 6. 对响应图施加汉宁窗抑制远离中心的响应 response responses[scale_id] response - response.min() response / response.sum() 1e-16 response (1 - self.cfg.window_influence) * response \ self.cfg.window_influence * self.hann_window loc np.unravel_index(response.argmax(), response.shape) # 峰值位置 # 7. 将响应图上的位移转换回图像坐标 disp_in_response np.array(loc) - (self.upscale_sz - 1) / 2 disp_in_instance disp_in_response * self.cfg.total_stride / self.cfg.response_up disp_in_image disp_in_instance * self.x_sz * \ self.scale_factors[scale_id] / self.cfg.instance_sz self.center disp_in_image # 8. 更新目标大小平滑更新 scale (1 - self.cfg.scale_lr) * 1.0 \ self.cfg.scale_lr * self.scale_factors[scale_id] self.target_sz * scale self.z_sz * scale self.x_sz * scale # 9. 返回 (x,y,w,h) 格式的结果框 box np.array([ self.center[1] 1 - (self.target_sz[1] - 1) / 2, # x self.center[0] 1 - (self.target_sz[0] - 1) / 2, # y self.target_sz[1], self.target_sz[0]]) # w, h return box首先进行多尺度搜索特征提取互相关互相关是与初始化的首帧模板特征进行计算得到响应图再进行上采样尺度惩罚上采样进行双三次插值将响应图扩大尺度惩罚对中间尺度施加惩罚抑制尺度突变其次是尺度选择汉宁窗惩罚选出响应值最高的尺度归一化后与汉宁窗按比例加权融合接着为坐标转换尺度更新最后返回结果。纯靠响应图有时候会在远离中心的位置产生噪声峰值汉宁窗的中心值大边缘值小会惩罚远离当前中心的响应保证目标运动的平滑。训练关键参数参数值说明epoch_num50总训练轮数batch_size8每批8对模板-搜索图像initial_lr1e-2初始学习率ultimate_lr1e-5最终学习率weight_decay5e-4L2正则化系数momentum0.9SGD动量优化器SGD带动量的随机梯度下降学习率调度ExponentialLR指数衰减四、局限性1.模板固定不变初始化后 self.kernel 不再更新。当目标外观发生显著变化光照、姿态、形变时用初始模板无法匹配变化后的目标。2.单层特征表达能力有限AlexNet 只在最后一层输出 256 维特征相比后来 SiamRPN 使用的 3 层 ResNet-50 特征每层256通道缺乏语义层次。3.尺度变化只能选3种离散尺度虽然可以通过 scale_lr 平滑更新但本质上受限于 3 级尺度金字塔对剧烈尺度变化适应能力有限。4.没有 Anchor 机制定位完全依赖响应图的最大值位置无法直接回归边界框。5.损失函数只考虑分类BalancedLoss 只关心像素属于前景还是背景不直接优化边界框质量如 IoU这限制了定位精度。