目前,跟踪领域主要分为两条主线,即基于相关滤波的跟踪算法和基于孪生网络的跟踪算法。由于深度特征的提取和更新很难做到实时,基于在线微调网络的深度目标跟踪方法会使跟踪器的效率大大降低。为解决这一问题,SiamFC提出基于离线端到端训练的全卷积李生网络的跟踪方法,在拥有较快的跟踪速度的同时,保持着较高的跟踪精度,因此受到了广泛的关注,近年来也出现基于此的大量研究。下图就是基于SaimFC改进的发展脉络:
Pysot(https://github.com/STVIR/pysot)是由商汤视频智能研究小组上传在Github的一个开源项目,由pytorch深度学习框架提供支持,这个项目还包含一个评估跟踪器的python工具包端口。Pysot 的目标是为视觉跟踪研究提供高质量、高性能的代码库,十分灵活,便于支持新研究的快速实施和评估。Pysot 包括以下视觉跟踪算法的实现:SiamRPN、DaSiamRPN、SiamRPN++、SiamMask,使用以下骨干网络架构:AlexNet、MobileNetV2、ResNet{18, 34, 50},评价工具包可以支持的数据集有OTB2015、VOT16/18/19、VOT18-LT、LaSOT、UAV123。
下面,我将结合论文与代码实现来梳理这五个算法。
《Fully-Convolutional Siamese Networks for Object Tracking》是2016年提出的,最大创新点在于使用全卷积孪生网络进行相似性学习,来解决跟踪任意对象的问题。在孪生网络之前,相关滤波算法使用从视频本身提取的示例以在线方式学习目标外观的模型,但是这些算法的明显缺陷是只能学习相对简单的模型。虽然这个问题可以通过采用从大型监督数据集训练而来的深度卷积网络得到改善,但监督数据的稀缺性和实时操作的限制阻止了深度学习在为每个视频学习一个检测器的范式中应用。
参考链接:
[1] https://zhuanlan.zhihu.com/p/107428605
[2] http://geyao1995.com/SiamFC/
SiamFC的数据处理代码实现:
def create_dataset(image, bbox, exemplar_size=127, isntance_size=255, padding=(0,0,0)):
'''
params:
image = cv2.imread(img_path)
bbox = [int(xmin), int(ymin), int(xmax), int(ymax)]
padding = tuple(map(int, np.mean(image, axis=(0, 1))))
'''
target_pos = [(bbox[2] + bbox[0]) / 2., (bbox[3] + bbox[1]) / 2.]
target_size = [bbox[2] - bbox[0], bbox[3] - bbox[1]]
w_z = target_size[1] + 0.5 * sum(target_size) # z means exemplar
h_z = target_size[0] + 0.5 * sum(target_size)
s_z = np.sqrt(w_z * h_z)
# z = crop_image(image, target_pos, s_z, exemplar_size, padding)
scale_z = exemplar_size / s_z # 缩放比例
s_x = instance_size / scale_z
# 但此时原图尺寸可能远不足s_z/s_x或超出,因此需要cropping or padding
# 一般padding选择全图平均值
# x = crop_image(image, target_pos, s_x, exemplar_size, padding)
由于SiamFC的问题在于跟踪框不够灵活,而SiamRPN便借鉴了目标检测的RPN结构,让跟踪框更加的准确,并且省去多尺度测试耗费的时间。《High Performance Visual Tracking with Siamese Region Proposal Network》是商汤在CVPR2018上提出的,孪生候选区域生成网络(Siamese region proposal network),简称Siamese-RPN,包含用于特征提取的孪生子网络和候选区域生成网络,其中候选区域生成网络包含分类和回归两条支路。
参考链接:
[1] 【SOT】Siamese RPN论文解读和代码解析
SiamRPN制作训练数据的关键代码:
# 1.create anchors
def genetate_anchors(self.total_stride, self.base_size, self.scales, self.ratios, self.score_size):
'''
params:
total_stride: 8
base_size: 8
scales:[8,] # anchor最终在原图的基准size = base_size * scale
ratios = [0.33, 0.5, 1, 2, 3]
score_size = 17 # feature map上的像素点感受野大小恰好为127的exemplar size
'''
# 1.计算每个锚点处anchor_num个锚框的宽高尺寸
self.anchor_num = len(self.scales)×len(self.ratios)
ws = int(np.sqrt(base_size * base_size)/ratio)
hs = int(ws * ratio)
w = ws * scale
h = hs * scale
self.anchors = np.zeros((anchor_num, 4), dtype=np.float32)
# 可以得到anchor以(0,0)为中心的[x1, y1, x2, y2]
self.anchors[i][:] = [-w*0.5, -h*0.5, w*0.5, h*0.5]
# 2.在原图上计算锚框位置,进行划分
ori = - (self.score_size // 2) * self.total_stride
# ori = img_center - self.score_size // 2 * self.total_stride
# img_center = self.instance_size//2
cx, cy = np.meshgrid([ori + self.total_stride * dx for dx in range(self.score_size)],\
[ori + self.total_stride * dy for dy in range(self.score_size)])
# meshgrid函数用两个坐标轴上的点在平面上画网格
# 3.整合结果
# 2.create anchor target
def anchor_target(self.anchors, target):
'''
params:
target = [tcx, tcy, tw, th]: ground truth bbox
'''
# 通过坐标转换得到anchors的cx, cy, w, h
# -1 ignore 0 negative 1 positive
cls = -1 * np.ones((self.anchor_num, self.score_size, self.score_size), dtype=np.int64)
delta = np.zeros((4, self.anchor_num, self.score_size, self.score_size), dtype=np.float32)
delta_weight = np.zeros((self.anchor_num, self.score_size, self.score_size), dtype=np.float32)
delta[0] = (tcx - cx) / w
delta[1] = (tcy - cy) / h
delta[2] = np.log(tw / w)
delta[3] = np.log(th / h)
overlap = IOU(anchors, target)
pos = np.where(overlap > cfg.TRAIN.THR_HIGH) # 0.6
neg = np.where(overlap < cfg.TRAIN.THR_LOW) # 0.3
pos, pos_num = select(pos, cfg.TRAIN.POS_NUM) # 16
neg, neg_num = select(neg, cfg.TRAIN.TOTAL_NUM) # 64
cls[pos] = 1
delta_weight[pos] = 1. / (pos_num + 1e-6)
cls[neg] = 0
return cls, delta, delta_weight, overlap
# 3.create sub dataset
class TrkDataset(Dataset):
# ......
def __getitem__(self, index):
# template, search = dataset.get_positive_pair(index)
# get image
template_image = cv2.imread(template[0])
search_image = cv2.imread(search[0])
# get bounding box(like siamFC)
# augmentation
# get labels: cls, delta, delta_weioht, overlap
return {'template': template,
'search': search,
'label_cls': cls,
'label_loc': delta,
'label_loc_weight': delta_weight,
'bbox': np.array(target)
}
SiamRPN网络模块的关键代码:
class UPChannelRPN(RPN):
def __init__(self, anchor_num=5, feature_in=256):
super(UPChannelRPN, self).__init__()
cls_output = 2 * anchor_num
loc_output = 4 * anchor_num
self.template_cls_conv = nn.Conv2d(feature_in,
feature_in * cls_output, kernel_size=3)
self.template_loc_conv = nn.Conv2d(feature_in,
feature_in * loc_output, kernel_size=3)
self.search_cls_conv = nn.Conv2d(feature_in,
feature_in, kernel_size=3)
self.search_loc_conv = nn.Conv2d(feature_in,
feature_in, kernel_size=3)
self.loc_adjust = nn.Conv2d(loc_output, loc_output, kernel_size=1)
def forward(self, z_f, x_f):
# 对模板帧进行升维
cls_kernel = self.template_cls_conv(z_f)
loc_kernel = self.template_loc_conv(z_f)
cls_feature = self.search_cls_conv(x_f)
loc_feature = self.search_loc_conv(x_f)
cls = xcorr_fast(cls_feature, cls_kernel)
loc = self.loc_adjust(xcorr_fast(loc_feature, loc_kernel))
return cls, loc
def xcorr_fast(x, kernel):
"""group conv2d to calculate cross correlation, fast version
"""
batch = kernel.size()[0]
pk = kernel.view(-1, x.size()[1], kernel.size()[2], kernel.size()[3])
px = x.view(1, -1, x.size()[2], x.size()[3])
po = F.conv2d(px, pk, groups=batch)
po = po.view(batch, -1, po.size()[2], po.size()[3])
return po
《Distractor-aware Siamese Networks for Visual Object Tracking》是在ECCV2018上提出的,DaSiamRPN分析了已有的孪生网络算法提取的特征及其具有的缺点,然后聚焦在训练distractor-aware孪生网络准确且长期的跟踪,主要在数据集扩展、训练方法以及local-to-global搜索策略方面对SiamRPN进行了改进。
孪生网络的缺点:首先,大部分孪生网络只能区分前景和非语义背景,语义背景一直被认为是很大的干扰,尤其是当背景杂乱时跟踪性能无法保证。其次,大多数孪生追踪器不能更新模型,虽然他们的简单性和固定模式的性质提升了跟踪速度,但是在跟踪场景下目标发生剧烈的外观变化时,孪生网络失去了在线更新模型的能力。第三,目前的孪生网络采取局部搜索策略,无法处理完全遮挡和超出视野的问题。如下图所示,在背景中差异很大的目标也能获得高分,比如白衣的球员和目标,甚至在SiamFC中一些无关的物体也能获得高分。作者分析了原因:(1)孪生网络提取的特征是根据训练数据的类别进行判别式训练得到的,在 SiamFC 和 SiamRPN 中来自同一视频不同帧的训练数据组成对,对于每个搜索区域的非语义背景占大多数,语义物体和干扰项占少数。因此,这种训练数据不平衡的分布使得训练模型难以学习实例层次的表示,而是倾向于学习前景和背景的区别。(2)在推理过程中,使用最近邻搜索搜索区域中最相似的目标,而在第一帧中被标记的背景信息被忽略,所以相似度最高的目标很有可能是干扰项,而并非目标。
数据集扩展:高质量的训练数据是视觉跟踪端到端表征学习成功的关键,作者通过引入一系列的策略来消除训练数据的不平衡分布,从而提高特征的泛化能力。 最初SiamFC是在ILSVRC视频检测数据集上训练,SiamRPN探索使用稀疏标签的Youtube-BB视频,这些视频检测数据集只包含很少的类别(VID 20,Youtube-BB 30) ,这不足以提供高质量和通用的跟踪特征。为了提高泛化能力并且对新类别的边界框回归更加准确,在训练集中增加了正样本对(detection pairs)。为了节省视频标记的繁琐和耗时,直接引入大规模ImageNet Detection和COCO Detection数据集,通过增强技术(平移、调整大小、灰度等) ,检测数据集的静态图像可用于生成图像对进行训练,如下图(a)所示。为了提高判别能力增加了语义负对(negative pairs from the same/different categories),来自不同类别的负对可以使跟踪器避免在超出视野和完全遮挡时漂移到任意目标,而来自同一类别的负对使得跟踪器专注于细粒度表示,即同类不同物的区分,如下图(b)和(c)所示。
Distractor-aware Incremental Learning:作者提出一个干扰-感知模块来有效的将一般表示( general representation)转换到特定视频域(video domain)。为了充分利用标签信息,我们将目标上下文中的负样本(干扰项)集成到相似度量中,采用非最大值抑制(NMS)来选择每帧中的可能的distractors d i d_i di,然后得到一个distractor set D : = { ∀ d i ∈ D , f ( z , d i ) > h ∩ d i ≠ z t } D :=\{∀d_i ∈ D, f (z, d_i ) > h ∩ d_i \not=z_t \} D:={∀di∈D,f(z,di)>h∩di=zt}, ∣ D ∣ = n |D|=n ∣D∣=n,其中 f ( z , x ) f (z, x) f(z,x)是模板和搜索图像的相似性度量, z t z_t zt是第 t t t帧通过最高得分选择的目标, h h h是预先设定好的阈值,来挑选剩下目标中的干扰项。在此基础上,引入一种新的干扰项感知目标函数,对与样本最像的top-k个proposals P P P进行重新排序。最后选择的目标被标记为 q q q: q = arg max p k ∈ P f ( z , p k ) − α ^ ∑ i = 1 n α i f ( d i , p k ) ∑ i = 1 n α i = arg max p k ∈ P ( φ ( z ) − α ^ ∑ i = 1 n α i φ ( d i ) ∑ i = 1 n α i ) ⋆ φ ( p k ) q = \argmax_{p_k ∈P} f (z, p_k )- \frac{\hat{α}\sum_{i=1}^nα_if (d_i , p_k ) }{\sum_{i=1}^n α_i}\\ =\argmax_{p_k ∈P} (φ(z)- \frac{\hat{α}\sum_{i=1}^nα_iφ(d_i) }{\sum_{i=1}^n α_i})⋆φ(p_k) q=pk∈Pargmaxf(z,pk)−∑i=1nαiα^∑i=1nαif(di,pk)=pk∈Pargmax(φ(z)−∑i=1nαiα^∑i=1nαiφ(di))⋆φ(pk)
α ^ \hat{α} α^控制整体干扰项学习的影响, α i α_i αi控制每一个干扰项 d i d_i di的影响,但是直接计算会使计算复杂度和内存占用量增加 n n n倍,因此用第二个等式加速计算,即通过减少卷积的次数来减小计算复杂度。更进一步可以进行增量学习,将现有的相似性度量(general)调整为新领域(specific)的相似性度量。(这里不是很理解,在代码中也没有找到实现过程,/(ToT)/~~) q = arg max p k ∈ P ( ∑ t = 1 T β t φ ( z t ) ∑ t = 1 T β t − ∑ t = 1 T β t α ^ ∑ i = 1 n α i φ ( d i , t ) ∑ t = 1 T β t ∑ i = 1 n α i ) ⋆ φ ( p k ) q =\argmax_{p_k ∈P} (\frac{\sum_{t=1}^T β_tφ(z_t)}{\sum_{t=1}^T β_t}- \frac{\sum_{t=1}^T β_t\hat{α}\sum_{i=1}^nα_iφ(d_{i,t}) }{\sum_{t=1}^T β_t\sum_{i=1}^n α_i})⋆φ(p_k) q=pk∈Pargmax(∑t=1Tβt∑t=1Tβtφ(zt)−∑t=1Tβt∑i=1nαi∑t=1Tβtα^∑i=1nαiφ(di,t))⋆φ(pk)
如下图所示,该算法充分利用了目标和背景信息,有效地抑制了跟踪过程中干扰因素的影响。
Long-term Tracking:提出了一种在短期跟踪和跟踪失败情形间简单而有效的切换方法,主要是根据检测分数的变化。如下图所示,SiamRPN的检测分数并不标准,即使在视野外和完全遮挡的情况下仍然很高,所以其倾向于在这些情形下较为武断地找到一个目标,从而导致跟踪漂移。然而在 DaSiamRPN中,检测得分能与跟踪相位的变化更为一致。因此,设计了一种在跟踪失败的情形下,通过local-to-global搜索策略来逐渐增加搜索区域的方法(即将搜索区域的大小以一个恒定的步长迭代增长),从而对目标re-detect。
在SiameseFC算法之后,基本上孪生网络都使用浅层的AlexNet做为基准特征提取器,直接使用预训练好的深层网络反而会导致跟踪算法精度的下降。由于孪生网络无法利用深层网络,所以仍然跟先进算法相比精度存在差距。《SiamRPN++: Evolution of Siamese Visual Tracking with Very Deep Networks》是商汤在CVPR2019上提出的,对现有的孪生追踪器进行了分析,证明了核心原因是缺乏严格的平移不变性(strict translation invariance)。创新点:(1)提出了一种简单而有效的采样策略,以打破空间不变性限制,成功地训练了基于Resnet架构的孪生跟踪器。(2)提出了一种基于层的互相关运算特征聚合结构(a layer-wise feature aggravation structure),该结构有助于跟踪器从多个层次的特征中预判出相似度图。(3)提出了一种深度可分离的相关结构(a depth-wise separable correlation structure),它不仅大大减少了目标模板分支中的参数个数,而且使模型的训练过程更加稳定。
SiamRPN++网路模块的关键代码:
# ModelBuilder中主要包含三个模块,backbone、neck、rpn_head
x = self.backbone(x)
z = self.backbone(z)
x = self.neck(x)
z = self.neck(z)
cls, loc = self.rpn_head(z, x)
其中,neck是为了将Resnet的layer2/3/4 层的输出(也就是论文图中的conv3/4/5 层)维度进行转换,即512 =>256、1024 =>256、2048 =>256。
# neck.py
class AdjustLayer(nn.Module):
def __init__(self, in_channels, out_channels, center_size=7):
super(AdjustLayer, self).__init__()
self.downsample = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channels),
) # 使用1×1的卷积核做了downsample
self.center_size = center_size
def forward(self, x):
x = self.downsample(x)
if x.size(3) < 20: # 针对模板帧经过layer234后变为15*15
l = (x.size(3) - self.center_size) // 2
r = l + self.center_size
x = x[:, :, l:r, l:r] # 为了减轻计算量,从中心裁减7×7的区域作为模板特征
return x
然后,rpn_head是用模板作为滤波器的核,与neck得到的三层输出分别进行相关操作,得到的回归和定位预测结果进行加权求和。
class MultiRPN(RPN):
def __init__(self, anchor_num, in_channels, weighted=False):
'''
params:
anchor_num: 5
in_channels: [256, 256, 256]
weighted: true
'''
super(MultiRPN, self).__init__()
self.weighted = weighted
for i in range(len(in_channels)):
self.add_module('rpn'+str(i+2),
DepthwiseRPN(anchor_num, in_channels[i], in_channels[i]))
if self.weighted:
self.cls_weight = nn.Parameter(torch.ones(len(in_channels))) # 3
self.loc_weight = nn.Parameter(torch.ones(len(in_channels))) # 3
# nn.Parameter函数让变量在参数优化的时候可以进行优化以达到最优取值
def forward(self, z_fs, x_fs):
cls = []
loc = []
for idx, (z_f, x_f) in enumerate(zip(z_fs, x_fs), start=2):
rpn = getattr(self, 'rpn'+str(idx))
c, l = rpn(z_f, x_f) # DepthwiseRPN
cls.append(c)
loc.append(l)
if self.weighted:
cls_weight = F.softmax(self.cls_weight, 0)
loc_weight = F.softmax(self.loc_weight, 0)
def avg(lst):
return sum(lst) / len(lst)
def weighted_avg(lst, weight):
s = 0
for i in range(len(weight)):
s += lst[i] * weight[i]
return s
if self.weighted: # 加权平均
return weighted_avg(cls, cls_weight), weighted_avg(loc, loc_weight)
else: # 求和平均
return avg(cls), avg(loc)
class DepthwiseRPN(RPN):
def __init__(self, anchor_num=5, in_channels=256, out_channels=256):
super(DepthwiseRPN, self).__init__()
self.cls = DepthwiseXCorr(in_channels, out_channels, 2 * anchor_num)
self.loc = DepthwiseXCorr(in_channels, out_channels, 4 * anchor_num)
# 输入256,隐藏层256,输出分别为2×5=10的分类feature和4×5=20的偏移feature
def forward(self, z_f, x_f):
cls = self.cls(z_f, x_f)
loc = self.loc(z_f, x_f)
return cls, loc
class DepthwiseXCorr(nn.Module):
def __init__(self, in_channels, hidden, out_channels, kernel_size=3, hidden_kernel_size=5):
super(DepthwiseXCorr, self).__init__()
self.conv_kernel = nn.Sequential(
nn.Conv2d(in_channels, hidden, kernel_size=kernel_size, bias=False),
nn.BatchNorm2d(hidden),
nn.ReLU(inplace=True),
)
self.conv_search = nn.Sequential(
nn.Conv2d(in_channels, hidden, kernel_size=kernel_size, bias=False),
nn.BatchNorm2d(hidden),
nn.ReLU(inplace=True),
)
self.head = nn.Sequential(
nn.Conv2d(hidden, hidden, kernel_size=1, bias=False),
nn.BatchNorm2d(hidden),
nn.ReLU(inplace=True),
nn.Conv2d(hidden, out_channels, kernel_size=1)
)
def forward(self, kernel, search):
# 模板帧和搜索帧分别先经过一个3×3的卷积
kernel = self.conv_kernel(kernel)
search = self.conv_search(search)
# 深度可分离卷积
feature = xcorr_depthwise(search, kernel)
# 变维到2k或者4k
out = self.head(feature)
return out
def xcorr_depthwise(x, kernel):
"""depthwise cross correlation
"""
batch = kernel.size(0)
channel = kernel.size(1)
x = x.view(1, batch*channel, x.size(2), x.size(3))
kernel = kernel.view(batch*channel, 1, kernel.size(2), kernel.size(3))
out = F.conv2d(x, kernel, groups=batch*channel) # 通过分组数设置可分离卷积
out = out.view(batch, channel, out.size(2), out.size(3))
return out
对比上述代码可以看出,SiamRPN++是先以可分离卷积的方式进行相关操作,在xcorr_depthwise之后经过提升1×1卷积核维度得到2k/4k的输出,而SiamRPN网络是在相关卷积操作之前先提升通道维度,然后进行普通卷积得到2k/4k的输出。
参考链接:PySOT代码之SiamRPN++分析——训练
视频物体分割(VOS)中物体由一个二元分割mask构成,也就是需要判断某个像素点是否属于目标物体,而视频物体跟踪是定位简单的边框。为了利用跟踪提高视频分割的性能,因此有了与SiamRPN++算法一并在CVPR2019上提出的SiamMask,《Fast Online Object Tracking and Segmentation: A Unifying Approach》,可以同时实现视频目标跟踪和视频目标分割这两个任务,并能达到实时的效果。SiamMask是一个多任务学习算法,每一个任务由一个分支来实现,都基于一个共享的CNN,最后由一个最终损失值汇总计算。
SiamMask网路模块的关键代码:
class MaskCorr(DepthwiseXCorr):
def __init__(self, in_channels, hidden, out_channels,
kernel_size=3, hidden_kernel_size=5):
super(MaskCorr, self).__init__(in_channels, hidden,
out_channels, kernel_size,
hidden_kernel_size)
def forward(self, kernel, search):
kernel = self.conv_kernel(kernel)
search = self.conv_search(search)
feature = xcorr_depthwise(search, kernel)
out = self.head(feature)
return out, feature
从上述代码可以看出,mask分支的结构代码与其他两分支完全相同。
class Refine(nn.Module):
def __init__(self):
super(Refine, self).__init__()
# exemplar branch
self.v0 = nn.Sequential(nn.Conv2d(64, 16, 3, padding=1), nn.ReLU(),
nn.Conv2d(16, 4, 3, padding=1),nn.ReLU()) # channel 64=>4
self.v1 = nn.Sequential(nn.Conv2d(256, 64, 3, padding=1), nn.ReLU(),
nn.Conv2d(64, 16, 3, padding=1), nn.ReLU()) # channel 256=>16
self.v2 = nn.Sequential(nn.Conv2d(512, 128, 3, padding=1), nn.ReLU(),
nn.Conv2d(128, 32, 3, padding=1), nn.ReLU()) # channel 512=>32
# mask branch
self.h2 = nn.Sequential(nn.Conv2d(32, 32, 3, padding=1), nn.ReLU(),
nn.Conv2d(32, 32, 3, padding=1), nn.ReLU())
self.h1 = nn.Sequential(nn.Conv2d(16, 16, 3, padding=1), nn.ReLU(),
nn.Conv2d(16, 16, 3, padding=1), nn.ReLU())
self.h0 = nn.Sequential(nn.Conv2d(4, 4, 3, padding=1), nn.ReLU(),
nn.Conv2d(4, 4, 3, padding=1), nn.ReLU())
self.deconv = nn.ConvTranspose2d(256, 32, 15, 15)
self.post0 = nn.Conv2d(32, 16, 3, padding=1)
self.post1 = nn.Conv2d(16, 4, 3, padding=1)
self.post2 = nn.Conv2d(4, 1, 3, padding=1)
def forward(self, f, corr_feature, pos):
# pos: 根据score分支得到的RoW的位置
# 根据下采样步长将pos对应到不同特征图上,切取在搜索帧上目标特征图大小
p0 = F.pad(f[0], [16, 16, 16, 16])[:, :, 4*pos[0]:4*pos[0]+61, 4*pos[1]:4*pos[1]+61]
p1 = F.pad(f[1], [8, 8, 8, 8])[:, :, 2*pos[0]:2*pos[0]+31, 2*pos[1]:2*pos[1]+31]
p2 = F.pad(f[2], [4, 4, 4, 4])[:, :, pos[0]:pos[0]+15, pos[1]:pos[1]+15]
p3 = corr_feature[:, :, pos[0], pos[1]].view(-1, 256, 1, 1)
out = self.deconv(p3)
out = self.post0(F.upsample(self.h2(out) + self.v2(p2), size=(31, 31)))
out = self.post1(F.upsample(self.h1(out) + self.v1(p1), size=(61, 61)))
out = self.post2(F.upsample(self.h0(out) + self.v0(p0), size=(127, 127)))
out = out.view(-1, 127*127)
return out
参考链接:目标跟踪–Siammask从头到尾的详解