SiamRPN
Pytorch Re-implementation for [CVPR'18] "(SiamRPN)High Performance Visual Tracking with Siamese Region Proposal Network" on GOT-10k dataset
Install / Use
/learn @haooozi/SiamRPNREADME
1、SiamRPN相比于SiamFC的创新
SiamFC的一个最明显的缺陷在于,当目标发生较大形变时无法跟踪,原因在于它使用的固定多尺度tracking方式,在前一帧的目标位置生成固定的几个尺度框,选择得分最高的框作为当前帧的跟踪结果。这种tracking策略当目标发生大形变时将无法跟踪,然而SiamRPN很好地解决了该问题,它第一次在目标跟踪领域引入了anchor的使用,有了anchor,就不再需要多尺度策略,有人会问:anchor也是多种尺度啊?关键在于数据关联方式,SiamFC是利用全卷积结构把目标模板当作卷积核在搜索图像上卷积,有点类似滑动窗口,而SiamRPN是以一种穷举的方式,在搜索图像上举出所有可能存在目标的anchor,简而言之,就是SiamFC相邻帧的尺度变化是较小的,所以无法跟踪大形变物体,而由于穷举anchor的缘故,SiamRPN相邻帧的尺度变化较大,可以由长条变为横条。anchor可以理解为在搜索图像上生成大大小小的各种框,足以覆盖图像内的每个物体(包含待跟踪目标),示意图如下所示,左侧是经过数据预处理的原始图片,中间图片列出了所有anchor的尺度,最右侧图片是在图片规定好的正方形范围以每个像素为中心生成所有尺度的anchor。 <img src="https://img-blog.csdnimg.cn/20210220130143964.png#pic_center" width = "700" height = "图片高度" > 对比两篇论文的实验结果,发现SiamRPN的fps大于SiamFC,虽然设备不一样,但也不会差别这么大(86fps<160fps),我在相同环境下实验时,发现SiamRPN只比SiamFC快些许。单看网络结构,会发现SiamRPN的更复杂,而且SiamFC网络有的SiamRPN也都有,速度的差异是看测试过程,SiamFC需要多尺度测试,意味着要进行多次全卷积,而SiamRPN只要一次,这是两者跟踪速度差距的主要因素。
2、architecture
下面是从论文截取的SiamRPN框架图 <img src="https://img-blog.csdnimg.cn/20210220163314561.jpg#pic_center" width = "1000" height = "图片高度" >左边是用于特征提取的孪生子网络。区域建议子网络位于中间,有两个分支,上分支用于分类,下分支用于回归。采用互相关的方法得到两个分支的输出。这两个输出特性映射的详细信息在右边。在分类分支中,输出特征图有2k个通道,对应k个anchor的前景和背景。回归分支中,输出特征图有4k通道,对应4个坐标,用于对anchor位置的微调。回归的作用是得到预测的目标框,回归的任务是将得到的目标框微调,使得位置更加精确。
2.1 特征提取网络(Siamese Network)和RPN
代码如下,这部分代码是SiamRPNNet的网络结构定义__init__和网络参数初始化_init_weights。
class SiamRPNNet(nn.Module):
def __init__(self, init_weight=False):
super(SiamRPNNet, self).__init__()
self.featureExtract = nn.Sequential(
nn.Conv2d(3, 96, 11, stride=2), #stride=2 [batch,3,127,127]->[batch,96,59,59]
nn.BatchNorm2d(96),
nn.MaxPool2d(3, stride=2), #stride=2 [batch,96,58,58]->[batch,96,29,29]
nn.ReLU(inplace=True),
nn.Conv2d(96, 256, 5), #[batch,256,29,29]->[batch,256,25,25]
nn.BatchNorm2d(256),
nn.MaxPool2d(3, stride=2), #stride=2 [batch,256,25,25]->[batch,256,12,12]
nn.ReLU(inplace=True),
nn.Conv2d(256, 384, 3), #[batch,256,12,12]->[batch,384,10,10]
nn.BatchNorm2d(384),
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, 3), #[batch,384,10,10]->[batch,384,8,8]
nn.BatchNorm2d(384),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, 3), #[batch,384,8,8]->[batch,256,6,6]
nn.BatchNorm2d(256),
)
self.anchor_num = config.anchor_num #每一个位置有5个anchor
""" 模板的分类和回归"""
self.examplar_cla = nn.Conv2d(256, 256 * 2 * self.anchor_num, kernel_size=3, stride=1, padding=0)
self.examplar_reg = nn.Conv2d(256, 256 * 4 * self.anchor_num, kernel_size=3, stride=1, padding=0)
""" 搜索图像的分类和回归"""
self.instance_cla = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
self.instance_reg = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
#这一步是SiamRPN框架中没有的,1x1的卷积,感觉可有可无,仅仅用于回归分支
#简单理解就是增加了网络的学习能力
self.regress_adjust = nn.Conv2d(4 * self.anchor_num, 4 * self.anchor_num, 1)
if init_weight:
self._init_weights()
def _init_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.xavier_uniform_(m.weight, 1) #xavier是参数初始化,它的初始化思想是保持输入和输出方差一致,这样就避免了所有输出值都趋向于0
if m.bias is not None:
nn.init.constant_(m.bias, 0) #偏置初始化为0
elif isinstance(m, nn.BatchNorm2d): #在激活函数之前,希望输出值由较好的分布,以便于计算梯度和更新参数,这时用到BatchNorm2d函数
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
self.featureExtract里面定义了特征提取网络,使用的是alexnet,这部分具体讲解课参考这篇博客,rpn结构定义了4个不同的nn.Conv2d,分别对应architrcture4个橙黄色的Conv,主要的推导过程在2.2小节详解。
2.2 训练与测试网络推导
2.2.1 训练推导
先给出代码,再看着分析,训练推导代码如下:
"""——————————前向传播用于训练——————————————————"""
def forward(self, template, detection):
N = template.size(0) # batch=32
template_feature = self.featureExtract(template) #[32,256,6,6]
detection_feature = self.featureExtract(detection) #[32,256,24,24]
"""对应模板分支,求分类核以及回归核"""
# [32,256*2*5,4,4]->[32,2*5,256,4,4]
kernel_score = self.examplar_cla(template_feature)
# 类似可得[32,4*5,256,4,4]
kernel_regression = self.examplar_reg(template_feature)
"""对应搜素图像的分支,得到搜索图像的特征图"""
conv_score = self.instance_cla(detection_feature) #[32,256,22,22]
conv_regression = self.instance_reg(detection_feature) #[32,256,22,22]
"""对应模板和搜索图像的分类"""
# [32,256,22,22]->[1,32*256=8192,22,22]
conv_scores = conv_score.reshape(1, -1, 22, 22)
score_filters = kernel_score.reshape(-1, 256, 4, 4) #[32*2*5,256,4,4]
#inout=[1,8192,22,22],filter=[320,256,4,4],得到output=[1,32*2*5,19,19]->[32,10,19,19] 32始终是batch不变,勿忘!
pred_score = F.conv2d(conv_scores, score_filters, groups=N).reshape(N, 10, 19,19)
"""对应模板和搜索图像的回归----------"""
#[32,256,22,22]->[1,32*256=8192,22,22]
conv_reg = conv_regression.reshape(1, -1, 22, 22)
reg_filters = kernel_regression.reshape(-1, 256, 4, 4) #[32*4*5,256,4,4]
#input=[1,8192,22,22],filter=[340,256,4,4],得到output=[1,32*4*5,19,19]->[32,4*5=20,19,19]——>微调
pred_regression = self.regress_adjust(F.conv2d(conv_reg, reg_filters, groups=N).reshape(N, 20, 19, 19))
#score.shape=[32,10,19,19],regression.shape=[32,20,19,19]
return pred_score, pred_regression
代码注释中,假定训练时的batch_size=32,首先127x127x3大小的目标模板图像和271x271x3大小检测图像(搜索图像)共享特征提取网络的网络参数,得到相应大小的feature map,分别为6x6x256、24x24x256,论文中的检测图像大小是255,所以输出的feature map大小为22x22x256;然后,将6x6x256大小的检测图像通过两个卷积层self.instance_cla和self.instance_reg分别用于回归和分类,可以看到,这两个卷积层的结构是完全相同的,最终大小都为22x22x256,但这两个feature map的用途不一样,分别用于分类和回归,再将上分支通过特征提取网络得到的6x6x256大小的feature map分别通过self.examplar_cla和self.examplar_reg,得到的feature map大小为[32,256x2x5,4,4]和[32,4x5x256,4,4],其中2和4分别表示需要学习的实际参数,2代表前景和背景,前景分数越大,代表是目标的概率越高,4表示dx、dy、dw、dh四个微调参数。 接下来就是将得到的4张feature map两两成对互相关,如果有对互相关不理解的小伙伴在博客中有讲解。最终得到用于分类和回归的feature map大小分别是[32,10,19,19]、[32,20,19,19]就可用于训练。
2.2.1 测试推导
先上代码
"""—————————————初始化————————————————————"""
def track_init(self, template):
N = template.size(0) #1
template_feature = self.featureExtract(template)# [1,256, 6, 6]
# kernel_score=[1,2*5*256,4,4] kernel_regression=[1,4*5*256,4,4]
kernel_score = self.examplar_cla(template_feature)
kernel_regression = self.examplar_reg(template_feature)
self.score_filters = kernel_score.reshape(-1, 256, 4, 4) #[2*5,256,4,4]
self.reg_filters = kernel_regression.reshape(-1, 256, 4, 4) #[4*5,256,4,4]
"""—————————————————跟踪—————————————————————"""
def track_update(self, detection):
N = detection.size(0)
# [1,256,24,24]
detection_feature = self.featureExtract(detection)
"""----得到搜索图像的feature map-----"""
conv_score = self.instance_cla(detection_feature) #[1,256,22,22]
conv_regression = self.instance_reg(detection_feature) #[1,256,22,22]
"""---------与模板互相关"""
#input=[1,256,22,22] filter=[2*5,256,4,4] gropu=1 得output=[1,2*5,19,19]
pred_score = F.conv2d(conv_score, self.score_filters, groups=N)
# input=[1,256,22,22] filter=[4*5,256,4,4] gropu=1 得output=[1,4*5,19,19]
pred_regression = self.regress_adjust(F.conv2d(conv_regression, self.reg_filters, groups=N))
#score.shape=[1,10,19,19],regression.shape=[1,20,19,19]
return pred_score, pred_regression
仔细分析,这部分代码与forward类似,只是结构有些变化,训练时是上下分支同时输入输出,而在测试时,明确分为了初始帧和后续待跟踪帧通过的网络结构,后续帧与固定不变的初始帧互相关运算。论文中也提到过,如下,论文说这是SiamRPN快速的原因,但要知道这并不是SiamRPN相比于SiamFC更快的原因。

最后,验证下搭建网络的正确性,验证代码和结果如下,可以看到网络搭建是正确的。
if __name__ == '__main__':
model = SiamRPNNet()
z_train = torch.randn([32,3,127,127]) #batch=8
x_train = torch.randn([32,3,271,271])
# 返回shape为[32,20,19,19] [32,10,19,19] 20=5*4 10=5*2
pred_score_train, pred_regression_train = model(z_train,x_train)
z_test = torch.randn([1,3,127,127])
x_test = torch.randn([1,3,271,271])
model.track_init(z_test)
# 返回shape为[1,20,19,19] [1,10,19,19]
pred_score_test, pred_regression_test = model.track_update(x_test)

Over,第一部分architecture代码分析就结束了!!
1、数据增强、建立anchor
SiamRPN关于training的详细流程图如下:
graph LR
A[GOT10k数据集] -- 每个视频序列提取两帧--> B[两帧图像]--crop and resize-->C[127x127x3的目标模板图像]
B --crop and resize and center_shift and scale--> D[271x271x3的搜索图像和目标的位置大小]
D--目标的centerxy及wh-->F[分类和回归的target]
E[建立anchors]-->F[分类和回归的target]
C-->G[最终的网络输出]
D--271x271x3的图像-->G
G-->H[分类和回归损失]
F-->H
H-->I{优化}
1.1 数据增强
接下来,分析下数据增强和生成anchor的代码,数据增强就是在得到原始搜索图像的时候对目标进行了随机的小移位以及目标长宽的缩放,当然也可以添加其他数据增强方式,比如模糊处理等,代码中仅对搜索图像的目标进行了这些操作。这里解释下代码中为什么没对目标模板图像数据增强呢?因为在跟踪过程中,anchor是反映在搜索图像上,所以对搜索图像中的目标shift后,训练过程要找的anchor就不仅仅是整个搜索图像的中心位置了,而是在位移所在处,这样训练后的网络可以避免在测试过程中网络对中心的敏感趋向性。而对目标模板图像进行类似的处理显得就没那么重要了,因为可以把目标模板图像的特征图是卷积核,跟踪过程是在搜索图像寻找与卷积核最相似的部分(分类的前景值最大),其实相比于shift,scale和图像模糊等对目标模板图像的意义更大,以上只是我个人的理解,其实加不加都行,多多少少有点益处。在阅读pysot源码时,对目标模板图像和搜索图像采取了同样的数据增强方式。
shift_x = np.random.choice(range(-12,12))
shift_y = np.random.choice(range(-12,12))
scale_h = 1.0 + np.random.uniform(-0.15, 0.15)
scale_w = 1.0 + np.random.uniform(-0.15, 0.15)
如代码所示,对目标的中心12个像素内的shift操作,12个像素的位移是相对于271x271大小的图像而言,并对其高宽进行0.85~1.15范围内的的缩放比例。验证下最终得到的搜索图像效果,
gt = np.array(list(map(round, gt)))
print(gt) #前两个值是s
Related Skills
node-connect
349.7kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.7kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.7kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
349.7kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
