论文:Fully-Convolutional Siamese Networks for Object Tracking
项目官方地址(包括论文下载地址、源码地址等):这里
论文阅读:SiameseFC
精读深度学习论文(31) SiameseFC
【SOT】siameseFC论文和代码解析
浅谈SiameseFC的优点与不足
siamfc-pytorch代码讲解(一):backbone&head
SiamFC完整的跟踪过程
其余的文章参考较少,在文中时会给出参考文章。
SOT的思想是,在视频中的某一帧中框出你需要跟踪目标的bounding box,在后续的视频帧中,无需你再检测出物体的bounding box进行匹配,而是通过某种相似度的计算,寻找需要跟踪的对象在后续帧的位置,如下动图所示(图中使用的是本章所讲siameseFC的升级版siameseMask),常见的经典的方法有KCF[2]等。
而目前基于深度学习的方法,要不就是采用 shallow methods(如:correlation filters)利用网络的中间表示作为 feature;要不就是执行 SGD 算法来微调多层网络结构。但是,利用 shallow 的方法并不能充分发挥 end-to-end 训练的优势,采用 SGD 的方法来微调也无法达到实时的要求。
将DL用于tracking中,有两点制约其发展:
1、训练数据的稀缺。由于跟踪目标事先未知,只能通过最初的框选定,无法预先准备大量训练数据。
2、实时的约束。对于跟踪问题来说,基于DL的做法虽然能有效提升模型的丰富度,能够很好的提升跟踪的效果,但是在时效性这一方面却做的很差,因为DL复杂的模型往往需要很大的计算量,尤其是当使用的DL模型在跟踪的时候需要对模型进行更新的话,需要在线SGD调整网络参数,限制了速度,可能使用GPU都没法达到实时。
SiameseFC分别针对这两点,利用ILSVRC15 数据库中用于目标检测的视频来训练模型(离线训练),在跟踪时,不更新模型(也就没有fine-tuning),保证速度够快。成为了使用了CNN进行跟踪,同时又具有很高的效率的跟踪算法。并因其速度很快,效果很好,成为之后很多算法(例如CFNet、DCFNet)的baseline。
使用孪生网络(Siamese Net)结构来进行相似度比较,对比模版图片(在训练前应该指定好)和需比较的目标图片之间的相似度。
速度是SiameseFC的最大优势。
可以用于追踪任意物体(不需要预先训练)。
在当时某几个benchmark上达到了最优。
图中z代表的是模板图像,算法中使用的是第一帧的groundtruth;x代表的是search region,代表在后面的待跟踪帧中的候选框搜索区域;ϕ代表的是一种特征映射操作,将原始图像映射到特定的特征空间,文中采用的是CNN中的卷积层和pooling层;66128代表z经过ϕ后得到的特征,是一个128通道66大小feature,同理,2222128是x经过ϕ后的特征;后面的代表卷积操作,让2222128的feature被66128的卷积核卷积,得到一个17*17的score map,代表着search region中各个位置与模板相似度值。score越大,相似度越大,越有可能是同一个物体。
总体来说,卷积网络将search image作为整体输入,直接计算两个输入图像的feature map的相似度匹配,节省了计算。计算得到相似度最高的位置,并反向计算出目标在原图中的位置。
算法本身是比较搜索区域与目标模板的相似度,最后得到搜索区域的score map。其实从原理上来说,这种方法和相关性滤波的方法很相似。其在搜索区域中逐个的对目标模板进行匹配,将这种逐个平移匹配计算相似度的方法看成是一种卷积,然后在卷积结果中找到相似度值最大的点,作为新的目标的中心。
上述互相关运算的步骤,像极了我们手里拿着一张目标的照片(模板图像),然后把这个照片按在需要寻找目标的图片上(搜索图像)进行移动,然后求重叠部分的相似度,从而找到这个目标,只不过为了计算机计算的方便,使用AlexNet对图像数据进行了编码/特征提取。
上图所画的ϕ其实是CNN中的一部分,并且两个ϕ的网络结构是一样的,这是一种典型的孪生神经网络,并且在整个模型中只有conv层和pooling层,因此这也是一种典型的全卷积(fully-convolutional)神经网络。
如果上面没看懂,下面给出另一种我觉得也很棒的解释:
孪生结构网络是卷积神经网络中的一种特殊结构。其结构如上图所示,它由两个结构相同的子网络构成,网络的输入是两张像,其中一张称为模板图像,通常选取的是序列第一帧,另外一张称为搜索图像,选取的是后续帧,每一个子网络负责处理一张图像,通过子网络的前向计算,可以提取图像的特征,最后将两者特征通过相似性度量函数,最终计算得到一个17×17×1的热力图,代表着搜索图像中各个位置与模板图像的相似度值。并根据以下函数计算相似度(卷积函数):
其中z是模板图像,x是搜索图像,
φ代表的是一种特征映射操作,将原始图像映射成特定的空间特征,这里采用的是卷积神经网络里的卷积层和池化层,f是相似性度量函数,这里代表的是卷积函数。模板图像虽然使用的是视频序列的第一帧,但是它是经过裁剪而来的,以待跟踪目标为中心,把原图像裁剪成127×127的尺寸。
也是经过裁剪而来的,它是以网络上一次输出的目标位置的中心点作为裁剪的中心,裁剪成固定的255×255的尺寸。
在这里就相当于充当一个待跟踪目标的外观模型,与后续图像帧里面的对象进行配对,热力图最里面分值最高的那个点则认为是与待跟踪对象外观模型最相似,就认为它是后续帧里的待跟踪对象。
用模板的CNN特征与搜索图像的特征进行卷积,得到整个图像的相似图。网络结构如下:(有的博客中说整个网络结构类似与AlexNet,但是没有最后的全连接层,只有前面的卷积层和pooling层。 )
整个网络结构入上表,其中pooling层采用的是max-pooling,每个卷积层后面都有一个ReLU非线性激活层,但是第五层没有。另外,在训练的时候,每个ReLU层前都使用了batch normalization,用于降低过拟合的风险。
一,获取输入数据:
SiameseFC需要的输入数据有模版图片z和候选图片x。
二,通过分数矩阵获取追踪结果
总体过程:
三,通过分数矩阵进行训练
使用交叉熵作为损失函数。对于score map中每个点,损失函数如下:
总体损失函数如下:
6×6和22×22的feature map“卷积”得到17×17的score map。对于每个score map,计算其loss为每个卷积得到的6*6小图的loss的均值。即:
这里的u∈D代表score map中的位置。 卷积网络的参数由SGD方法最小化上图损失函数得到。这里采用的是卷积神经网络里的卷积层和池化层,f是相似性度量函数,这里代表的是卷积函数。
计算相似度(卷积函数)
根据以下函数计算相似度(卷积函数):
其中z是模板图像,x是搜索图像, φ代表的是一种特征映射操作,将原始图像映射成特定的空间特征
四, 多尺寸处理
思路:
五,训练的一些细节
参考:SiamFC完整的跟踪过程
准备两路输入图像:模板图像和检测图像。
将模板图像和检测图像输入CNN网络中,分别得到6x6x128和22x22x128的特征图。
最后使用交叉相关,将模板图像的特征图当做卷积核,对检测图像的特征进行滑窗检测,最后得到3x1x17x17的得分图(三个尺度)。交叉函数如下所示:
f(x,z)=φ(z)∗φ(x)+bi
使用双三次线性插值生成277x277的图像: 3x277x277.
获得三个得分图中最大值的位置(x,y)。
获得最大值位置与上一帧目标中心的相对位移。
因为之前是crop,再resize得到检测图像,之后CNN(包含交叉卷积)得到得分图,最后上采样得到[3,277,277]。所以将第(6)步得到的相对位移进行逆运算,最终获得视频帧之间的相对位移。
根据相对位移更新目标的中心点。
获得目标尺寸变换的比例(最大值所在的尺度(三个尺度中的一个)):
1、实时性(>24帧/s):
SiamFC-3s FPS : 86帧/s
SiamFC-5s FPS : 58帧/s
该网络把跟踪任务转换成一个模板匹配的问题而不是一个常见的二分类问题,整个跟踪过程中不需要更新模板,使得算法的速度大幅度提高。这也是深度学习领域神经网络在目标跟踪一直以来难以到达的一个关键点,直到孪生网络应用于目标跟踪使得在跟踪精度较高的条件下还达到了实时性,在深度学习领域不愧为重大的突破,继此网络后,基于此网络为基本框架的目标跟踪算法层出不尽并且精度、速度能达到一个兼顾,占据深度学习应用在目标跟踪中的主流方向。
2、小范围晃动
对于小范围晃动,背景信息变化不大使得模板匹配的结果较好
3、运动模糊
4、短时局部遮挡
模板匹配对短暂性局部遮挡处理较好
SiameseFC是一个模板匹配的任务,在跟踪过程中并不更新目标模板和网络权值,这造成如下问题:
(1)当目标发生较大的形变时,会造成目标候选框与目标模板出现较大差异,从而导致跟踪失败。网络权值不更新导致要使用同一套网络结构和网络参数适应所有的跟踪场景,这是很难做到的。
(2)对于没有处于复杂背景下的跟踪来说,该算法能基本平衡实时性与准确性要求,但是跟踪目标一旦发生遮挡、快速运动、相似外观,搜索图像的大小可能就覆盖不了目标,通过最后的相似性度量函数得出来的结果就是错误的,随着跟踪过程中发生的错误累加,导致跟踪不可恢复,所以孪生结构网络的跟踪性能在背景复杂的情况下会下降。
失败原因:
目标特征不够具体、突出、全面 (AlexNet提取特征不够细致)
没能利用好空间信息、运动信息 (运动模型不够合理)
搜索域方法的局限性 (多尺度增加计算量,无法适应尺度变化)
匹配与分类的本质差别 (分类对背景前景区分较好)
解决思路:
加入在线更新的策略(增加目标信息,牺牲速度或者增强特征的提取)
需要对首帧标注图像做处理(抑制背景信息,增加前景和背景的区分度)
SiamFC选用第一帧作为模板并不予更新,因此首帧目标信息为关键信息,而SiamFC模型中,最后采用相似度学习,如果不能降模板图像中背景信息的干扰,则必然会对结果造成影响。 因此应当对标注图像进行进一步的目标提取,并对背景信息进行抑制。
利用空间信息,估计运动模型
在存在较多相似目标的场景中,可能特征匹配难以准确地判断哪个才是真正的目标。而人在这种场景下追踪目标的策略往往是根据
(1)目标的空间信息,例如一队人中的第几个,或者目标周围有哪些参照物。这一点可以通过对目标旁边的背景进行建模实现。
(2)根据目标的运动轨迹进行预测,因为目标的变化(无论是位置还是外观)在序列中往往是连续可微的。
这里主要参考:siamfc-pytorch代码讲解(三):demo&track
更简洁的请参考:【SOT】siameseFC论文和代码解析讲的也很棒~
功能分析:
这个module主要实现了3个AlexNet版本作为backbone,开头的__all__ = [‘AlexNetV1’, ‘AlexNetV2’, ‘AlexNetV3’]主要是为了让别的module导入这个backbones.py的东西时,只能导入__all__后面的部分。
后面就是三个类AlexNetV1、AlexNetV2、AlexNetV3,他们都集成了类_AlexNet,所以他们都是使用同样的forward函数,依次通过五个卷积层,每个卷积层使用nn.Sequential()堆叠,只是他们各自的total_stride和具体每层卷积层实现稍有不同(当然跟原本的AlexNet还是有些差别的,比如通道数上):
AlexNetV1和AlexNetV2:
共同点:conv2、conv4、conv5这几层都用了groups=2的分组卷积,这跟原来的AlexNet会更接近一点
不同点:conv2中的MaxPool2d的stride不一样大,conv5层的输出通道数不一样
AlexNetV1和AlexNetV3:前两层的MaxPool2d是一样的,但是中间层的卷积层输入输出通道都不一样,最后的输出通道也不一样,AlexNetV3最后输出经过了BN
AlexNetV2和AlexNetV3:conv2中的MaxPool2d的stride不一样,AlexNetV2最后输出通道数小很多。
其实感觉即使有这些区别,但是这并不是很重要,这一部分也是整体当中容易理解的,所以不必太去纠结为什么不一样,最后作者用的是AlexNetV1,论文中是这样的结构,其实也就是AlexNetV1:
代码分析
1.def __init__(self, num_features, *args, **kwargs):
中*args和**kwargs到底是什么
参考Python中*args、**args到底是什么、有啥区别、怎么用
*args的用法:当传入的参数个数未知,且不需要知道参数名称时。
**args的用法:当传入的参数个数未知,但需要知道参数的名称时(立马想到了字典,即键值对)
也就是说**args在输入的时候一般是键值对,而*args任意。
注意:这里的args不是必须的,也就是说可以换成kwargs等等,但是星号是必须的。
2.BatchNorm2d函数分析:
参考:BatchNorm2d原理、作用及其pytorch中BatchNorm2d函数的参数讲解
例如BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
nn.ReLU(inplace=True)
代码分析4.output stride = 4
解析:
output stride为该矩阵经过多次卷积pooling操作后,尺寸缩小的值(这个还保留疑问)
5.__all__ = ['AlexNetV1', 'AlexNetV2', 'AlexNetV3']
中__all__解析
参考:python中【all】的用法
__all__是一个字符串list,用来定义模块中对于from XXX import 时要对外导出的符号,即要暴露的接口,但它只对import *起作用,对from XXX import XXX不起作用。
控制 from xxx import * 的行为
6.nn.Conv2d(384, 32, 3, 1, groups=2))
这个groups=2
from __future__ import absolute_import
import torch.nn as nn
import torch.nn.functional as F
__all__ = ['SiamFC']
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):
# fast cross correlation
# x size 8,256,20,20
# z size 8,256,6,6
nz = z.size(0) #size(0)即取第一个shape值
#nz = 8
nx, c, h, w = x.size()
#nx = 8,c = 256,h = 20,w = 20
x = x.view(-1, nz * c, h, w)
#x.shape = [1,2048,20,20]
out = F.conv2d(x, z, groups=nz)
# out.shape = [1,8,15,15]
#输入是4维,输出也是4维,高层补1
# print(out.size())
out = out.view(nx, -1, out.size(-2), out.size(-1))
# out.shape = [8,1,15,15]
return out
功能分析:
为什么这里会有个out_scale,根据作者说是因为, z zz和x xx互相关之后的值太大,经过sigmoid函数之后会使值处于梯度饱和的那块,梯度太小,乘以out_scale就是为了避免这个。
重点:nn.conv2d和nn.function2d解析
_fast_xcorr函数中最关键的部分就是F.conv2d函数了,可以通过官网查询到用法:
torch.nn.functional.conv2d(input, weight, bias=None, stride=1, padding=0, dilation=1, groups=1) → Tensor
input – input tensor of shape (\text{minibatch} , \text{in_channels} , iH , iW)(minibatch,in_channels,iH,iW)
weight – filters of shape (\text{out_channels} , \frac{\text{in_channels}}{\text{groups}} , kH , kW)(out_channels, groups in_channels ,kH,kW)
bias – optional bias tensor of shape (\text{out_channels})(out_channels). Default: None
stride – the stride of the convolving kernel. Can be a single number or a tuple (sH, sW). Default: 1
padding –implicit paddings on both sides of the input. Can be a string {‘valid’, ‘same’}, single number or a tuple (padH, padW). Default: 0 padding=‘valid’ is the same as no padding. padding=‘same’ pads the input so the output has the shape as the input. However, this mode doesn’t support any stride values other than 1.
这里nn.Con2d要与nn.function.conv2d区分开:
参考:pytorch中nn.Conv2d和nn.function.conv2d的区别
这里针对nn.function.conv2d讨论
例如:torch.nn.functional.conv2d(self, input, weight, bias=None, stride=1, padding=0, dilation=1, groups=2)
input:
minibatch:batch中的样例个数
in_channels:每个样例数据的通道数
iH:每个样例的高(行数)
iW:每个样例的宽(列数)
weight(就是filter):
out_channels:卷积核的个数
in_channels/groups:每个卷积核的通道数
kH:每个卷积核的高(行数)
kW:每个卷积核的宽(列数)
groups作用:对input中的每个样例数据,将通道分为groups等份,即每个样例数据被分成了groups个大小为(in_channel/groups, iH, iW)的子数据。对于这每个子数据来说,卷积核的大小为(in_channel/groups, kH, kW)。这一整个样例数据的计算结果为各个子数据的卷积结果拼接所得。
举例:
import torch.nn.functional as F
inputs = torch.arange(1, 21).reshape(1, 2, 2, 5)
filters = torch.arange(1, 7).reshape(2, 1, 1, 3)
print(inputs)
print(filters)
res = F.conv2d(input=inputs, weight=filters, stride=(1, 1), groups=2)
print(res)
输出如下:
tensor([[[[ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10]],
[[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20]]]])
tensor([[[[1, 2, 3]]],
[[[4, 5, 6]]]])
tensor([[[[ 14, 20, 26],
[ 44, 50, 56]],
[[182, 197, 212],
[257, 272, 287]]]])
样例数据:
[ [ [ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10] ],
[ [11, 12, 13, 14, 15],
[16, 17, 18, 19, 20] ] ]
被分成:
[ [ [ 1, 2, 3, 4, 5], [ [ [11, 12, 13, 14, 15],
[ 6, 7, 8, 9, 10] ] ] 和 [16, 17, 18, 19, 20] ] ]
卷积核:
[ [ [ [ 1, 2, 3 ] ] ],
[ [ [ 4, 5, 6 ] ] ] ]
被分成:
[ [ [1, 2, 3] ] ] 和 [ [4, 5, 6] ] ]
结果中:
[ [ 14, 20, 26],
[ 44, 50, 56] ]
是
[ [ [ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10] ] ] 和 [ [ [ 1, 2, 3 ] ] ] 卷积所得。
作者使用了GOT-10k这个工具箱,train.py代码非常少
具体可以参考官方文档:Downloads - GOT-10k
顺着代码流看到调用了siamfc.py中类TrackerSiamFC的train_over方法,在这个类里面就是进行数据增强,构造和加载,然后进行训练,这里先讨论transforms:
SiamFCTransforms是transforms.py里面的一个类,主要是对输入的groung truth的z, x, bbox_z, bbox_x进行一系列变换,构成孪生网络的输入,这其中就包括了:
RandomStretch:主要是随机的resize图片的大小,其中要注意cv2.resize()的一点用法,可以参考这篇博客:cv2.resize()的一点小坑
CenterCrop:从img中间抠一块(size, size)大小的patch,如果不够大,以图片均值进行pad之后再crop
RandomCrop:用法类似CenterCrop,只不过从随机的位置抠,没有pad的考虑
Compose:就是把一系列的transforms串起来
ToTensor: 就是字面意思,把np.ndarray转化成torch tensor类型
代码分析:
torch.from_numpy(img).float().permute((2, 0, 1))
中permute(2,0,1)分析>>> x = torch.randn(2, 3, 5)
>>> x.size()
torch.Size([2, 3, 5])
>>> x.permute(2, 0, 1).size()
torch.Size([5, 2, 3])
if isinstance(size, numbers.Number):
中isinstance分析np.random.choice
img = cv2.copyMakeBorder(img, npad, npad, npad,
npad,border_type,value=border_value)
参考:OpenCV-Python: cv2.copyMakeBorder()函数详解
现在继续回到train_over方法,里面构造dataset的时候用了Pair类,所以从代码角度具体来看一下,因为继承了Dataset类,所以要overwrite __getitem__和__len__方法:
getitem:分析代码,这个方法就是通过index索引返回item = (z, x, box_z, box_x),然后经过transforms返回一对pair(z, x),就需要像论文里面说的:The images are extracted from two frames of a video that both contain the object and are at most T frames apart 。
_filter:通过该函数筛选符合条件的有效索引val_indices,这里不详细分析,因为我也不知道为什么会有这样的filter condition。
_sample_pair:如果有效索引大于2个的话,就从中随机挑选两个索引,这里取的间隔不超过T=100
len:这里定义的长度就是被索引到的视频序列帧数×每个序列提供的对数
train_step
现在来到siamfc.py里面最后一个关键的地方,数据准备好了,经过变换和加载进来就可以训练了,下面代码是常规操作,具体在train_step里面实现了训练和反向传播:
而train_step里面难度又是在于理解_create_labels,具体的一些tensor的shape可以看我的注释,我好奇就把他打印出来了,看来本来__getitem__返回一对pair(z, x),经过dataloader的加载,还是z堆叠一起,x堆叠一起,并不是(z, x)绑定堆叠一起
而且criterion使用的BalancedLoss,是调用F.binary_cross_entropy_with_logits,进行一个element-wise的交叉熵计算,所以创建出来的labels的shape其实就是和responses的shape是一样的:
创建标签,论文里是这么说的:
因为我们的exemplar image z zz 和search image x xx都是以目标为中心的,所以labels的中心为1,中心以外为0。
对于np.tile、np.meshgrid、np.where函数的使用:
参考:np.tile、np.meshgrid、np.where学习总结
最后出来的一个batch下某一个通道下的label就是下面这样的:
还有train_over部分,就是保存模型,没什么说的。
现在就来看一下类TrackerSiamFC下的track方法。这个函数的作用就是传入video sequence和first frame中的ground truth bbox,然后通过模型,得到后续帧的目标位置,可以看到主要有两个函数实现:init和update,这也是继承Tracker需要重写的两个方法:
init(self, img, box):
init:就是传入第一帧的标签和图片,初始化一些参数,计算一些之后搜索区域的中心等等
update:
update:就是传入后续帧,然后根据SiamFC网络返回目标的box坐标,之后就是根据这些坐标来show,起到一个demo的效果。
补充一些函数:
1.getatter()
:
参考:Python getatter() 通过方法名字符串调用方法
getattr()这个方法最主要的作用是实现反射机制。也就是说可以通过字符串获取方法实例。
获取函数/属性/从模块获取类
2.enumerate()
参考:python中enumerate()函数的用法
enumerate(sequence, start=0),返回一个枚举对象。sequence必须是序列或迭代器iterator,或者支持迭代的对象。enumerate()返回对象的每个元素都是一个元组,每个元组包括两个值,一个是计数,一个是sequence的值,计数是从start开始的,start默认为0。
The position of the maximum score relative to the centre of the score map, multiplied by the stride of the network, gives the displacemen of the target from frame to frame.
我们假设有三个帧,第二帧的中心截取是按照第一帧的目标位置来截取的,如果要进行第三帧的中心截取,那我们需要按照第二帧对于第一帧的相对位移,来获取第三帧的截取位置–也就是帧与帧之间的位移.网络步长是指缩小的倍数.再经过一系列的卷积层之后,图像缩小了一定的倍数,这个倍数就是网络步长.当我们得到第二帧的map中目标点位置时,要与网络步长相乘,就能知道第二帧目标相对第一帧的目标位移,第三帧就通过这个位移来进行中心截取.
这里来讲解一下这个公式
实际上,s是缩小的倍数,(h,w)是指长宽,而p是指边框厚度,其结果A就是面积
双三插值的理解请看这个:插值(五)Bicubic interpolation(双三次插值)