SingleTrack_Project(八)深度学习方法——SiamFC

SingleTrack_Project(八)深度学习方法——SiamFC

一、基本原理

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), # BatchNorm,eps=1e-6, momentum=0.05 nn.ReLU(inplace=True), nn.MaxPool2d(3, 2)) # 3×3最大池化,步长2 self.conv2 = nn.Sequential( nn.Conv2d(96, 256, 5, 1, groups=2), # groups=2 是 AlexNet 的特性 _BatchNorm2d(256), nn.ReLU(inplace=True), nn.MaxPool2d(3, 2)) self.conv3 = nn.Sequential( nn.Conv2d(256, 384, 3, 1), _BatchNorm2d(384), nn.ReLU(inplace=True)) self.conv4 = nn.Sequential( nn.Conv2d(384, 384, 3, 1, groups=2), _BatchNorm2d(384), nn.ReLU(inplace=True)) self.conv5 = nn.Sequential( nn.Conv2d(384, 256, 3, 1, groups=2)) # 最终输出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_scale=0.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, groups=nz) # 分组卷积实现互相关 out = out.view(nx, -1, out.size(-2), out.size(-1)) return out

互相关的物理意义:conv2d(x, z, groups=nz) 本质上是将模板 z 作为卷积核去卷积搜索区域 x。模板特征尺寸为 6×6,搜索特征为 22×22,卷积结果尺寸为 22-6+1=17,即最终响应图是 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_weight=1.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, reduction='sum')

加权二元交叉熵损失:响应图上目标周围 R=16 像素内的位置视为正样本,其余为负样本。由于正样本远少于负样本(约 100:1),用权重平衡使正负样本贡献均等。

5.工具函数

def crop_and_resize(img, center, size, out_size, border_type=cv2.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, value=border_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]], dtype=np.float32) # 高, 宽 self.center, self.target_sz = box[:2], box[2:] # 创建汉宁窗(用于抑制边缘响应) self.upscale_sz = self.cfg.response_up * self.cfg.response_sz # 16×17=272 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×(h+w) 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_size=self.cfg.exemplar_sz, # 127 border_value=self.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_size=self.cfg.instance_sz, # 255 border_value=self.avg_color) for f in self.scale_factors] x = np.stack(x, axis=0) 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), interpolation=cv2.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_num

50

总训练轮数
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),这限制了定位精度。