前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【SOT】siameseFC论文和代码解析

【SOT】siameseFC论文和代码解析

作者头像
马上科普尚尚
发布2020-09-22 15:20:25
1.1K0
发布2020-09-22 15:20:25
举报

1. 前言

除了深度学习【目标检测】专栏[1],我开通了深度学习【目标追踪】专栏[2],用来记录学习目标追踪算法(单目标追踪SOT/多目标追踪MOT)论文/代码的解析。最近我在阅读目标追踪领域的文献综述时,遇到了很多关于孪生网络(siamese network)在目标追踪领域的应用。这里,我们以单目标追踪SOT中比较经典的Fully-Convolutional Siamese Networks(称之为siameseFC[3])网络,结合论文和代码,展开对siameseFC的讲解。

SOT的思想是,在视频中的某一帧中框出你需要跟踪目标的bounding box,在后续的视频帧中,无需你再检测出物体的bounding box进行匹配,而是通过某种相似度的计算,寻找需要跟踪的对象在后续帧的位置,如下动图所示(图中使用的是本章所讲siameseFC的升级版siameseMask),常见的经典的方法有KCF[4]等。

作者提出的siameseFC网络兼顾了追踪的速度和精度,在当初多个benchmarks中达到了最优(SOTA)。论文中,作者提到:

The key contribution of this paper is to demonstrate that this approach achieves very competitive performance in modern tracking benchmarks at speeds that far exceed the frame-rate requirement.

那么这个孪生网络siamese network到底是什么呢?

常见的孪生网络的结构

答:简单来说,孪生网络siamese network主要用来衡量两个输入的相似程度。如上图,孪生神经网络有两个输入(Input1 and Input2),将两个输入输入到两个神经网络(Network1 and Network2,其实是共享权重的,本质上是同一个网络),这两个神经网络分别将输入映射到新的空间,形成输入在新的空间中的表示。最后通过Loss的计算,评价两个输入的相似度。

那么本文中提出的全卷积的siamese network本质上也是寻找给定的模板图像z(论文中的exemplar image,类似于你在测试时框出的第一个bounding box中的图像)在搜索图像x(search image,其他视频帧)上的位置,从而实现单目标追踪。

这其实是类似于人的视觉感官常识的,人在进行目标追踪的时候,前一秒记住该目标的一些特征(身高,长相,衣着等),后一秒在另一张视频帧中根据这些特征找到该目标。

上面结构图中,siamese网络由以下几部分构成:

  • 输入为模板图像z(大小为127x127x3) + 搜索图像x(大小为255x255x3)
  • 特征提取网络/卷积神经网络 ,论文中采用了较为简单的AlexNet,输出为

,大小为6x6x128以及

,大小为22x22x128

  • 互相关运算

(论文中的cross-correlation),实质上是以

特征图,以

卷积核进行的卷积互运算

  • 结果 score map ,大小为(17x17x1)。这里的17=(22-6)/1+1,符合卷积互运算的定义

得到了score map后,我们知道score map反应了

中每个对应部分相似度关系。score越大,相似度越大,越有可能是同一个物体。

上述互相关运算的步骤,像极了我们手里拿着一张目标的照片(模板图像),然后把这个照片按在需要寻找目标的图片上(搜索图像)进行移动,然后求重叠部分相似度,从而找到这个目标,只不过为了计算机计算的方便,使用AlexNet对图像数据进行了编码/特征提取。这里我引用了某版本pytorch的siamFC代码中的动图[5],如下,生动形象地将siameseFC的互相关运算描述出来了。

我们对siamese的结构大致就讲完了,还有一些内容结合代码来讲,效果更好。

代码我是找了一个基于pytorch版本(星星不是很多,不过看起来比较简单)的,链接如下:

https://github.com/mozhuangb/SiameseFC-pytorch

2. SiameseFC网络结构定义

上面我们讲到SiameseFC的结构,主要由以下几部分构成

  • 特征提取网络AlexNet
  • 互相关运算网络

AlexNet网络代码定义如下:

代码语言:javascript
复制
class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 96, 11, stride=2, padding=0),
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=0))

        self.conv2 = nn.Sequential(
            nn.Conv2d(96, 256, 5, stride=1, padding=0, groups=2),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=0))

        self.conv3 = nn.Sequential(
            nn.Conv2d(256, 384, 3, stride=1, padding=0),
            nn.BatchNorm2d(384),
            nn.ReLU(inplace=True))

        self.conv4 = nn.Sequential(
            nn.Conv2d(384, 384, 3, stride=1, padding=0, groups=2),
            nn.BatchNorm2d(384),
            nn.ReLU(inplace=True))

        self.conv5 = nn.Sequential(
            nn.Conv2d(384, 256, 3, stride=1, padding=0, groups=2))

    def forward(self, x):
        conv1 = self.conv1(x)
        conv2 = self.conv2(conv1)
        conv3 = self.conv3(conv2)
        conv4 = self.conv4(conv3)
        conv5 = self.conv5(conv4)
        return conv5

该部分为图像特征提取网络,不过多进行解析,接着根据AlexNet定义SiameseFC网络:

代码语言:javascript
复制
class Siamfc(nn.Module):
    def __init__(self, branch):
        super(Siamfc, self).__init__()
        self.branch = branch
        self.bn_adjust = nn.Conv2d(1, 1, 1, stride=1, padding=0)
        self._initialize_weights()

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
                if m.bias is not None:
                    m.bias.data.zero_()
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()

    def Xcorr(self, x, z):         # x denote search, z denote template
        out = []
        for i in range(x.size(0)):
            out.append(F.conv2d(x[i, :, :, :].unsqueeze(0), z[i, :, :, :].unsqueeze(0)))
        return torch.cat(out, dim=0)

    def forward(self, x, z):        # x denote search, z denote template
        x = self.branch(x)
        z = self.branch(z)
        xcorr_out = self.Xcorr(x, z)
        out = self.bn_adjust(xcorr_out)
        return out

可见Siamfc类中self.branch即为AlexNet特征提取网络,输入模板图像z搜索图像x经过selg.branch(AlexNet)提取特征后,作为 self.Xcorr 输入,进行互运算。这里的self.Xcorr定义如下:

代码语言:javascript
复制
def Xcorr(self, x, z):         # x denote search, z denote template
        out = []
        for i in range(x.size(0)):
            out.append(F.conv2d(x[i, :, :, :].unsqueeze(0), z[i, :, :, :].unsqueeze(0)))
        return torch.cat(out, dim=0)

很明显,就是一个卷积互运算的定义。最后模型在最后一层,添加了一个1x1卷积层self.bn_adjust(输入输出维度都是1) 。

结合代码一看,说真的,siameseFC结构比较简单(所以人家速度快!)

3. siameseFC的输入、输出和损失函数

想了解一个网络,不光要知道网络的结构,还得需要知道网络的输入输出(预测值和真实值label)是什么,然后根据网络输出的预测值和真实值来计算损失函数。那么我们将按照siameseFC网络的输入、输出和损失函数分别进行讲解。

3.1 siameseFC网络的输入

上面我们知道siameseFC每次需要两张图片作为输入,一张是所谓的模板图像z,另一张是搜索图像x。模板图像需要缩放到127x127大小,搜素图像需要缩放到255x255大小。那么关于模板图像和搜索图像的选取和位置放置,有什么需要注意的呢?下面分别从论文和代码的角度进行分析。

论文中给出一组图,如下

上方三张图为模板图像z,下面三张图为搜索图像x。这里的模板图像z好像和想象中有点不一样,本以为截取bounding box内的图像内容作为模板图像z的,但是这里看来,好像还包含了bounding box周围的一些环境信息和padding

论文中是这么说的:

意思就是在bounding box加上了周围的一些边界信息,上下左右各加上p个像素信息。其中:

假设bounding box的大小为(w,h),加上边界后的大小为(w+2p,h+2p)。对于模板图像而言,还需要对其加上边界后的结果乘上一个缩放系数s,使得区域的面积为127x127,当然了,这里缩放后的长宽不一定非要等于127,那么公式

成立。

然后呢,会发现虽然现在区域面积为127x127,但是他并不是127x127的方形,所以需要进行resize。

如果你还是不太清楚,那我们来看一下代码上是怎么做的。这种基于pytorch的代码,一般获取和处理数据,都定义在数据集定义中的__getitem__类方法中。这里我们到dataset.py中的Pair类的__getitem__中,找到如下:

代码语言:javascript
复制
crop_z = self.crop(
            img_z, bndbox_z, self.exemplarSize
        )  # crop template patch from img_z, then resize [127, 127]
        crop_x = self.crop(
            img_x, bndbox_x, self.instanceSize
        )  # crop search patch from img_x, then resize [255, 255]

这里的img_z 和 bndbox_z和 img_x和 bndbox_x分别为模板图像z搜索图像x以及他们中物体所出bounding box的坐标(左上横坐标x,左上纵坐标y,宽,高)

这里的self.crop定义如下:

代码语言:javascript
复制
# crop the image patch of the specified size - template(127), search(255)
    def crop(self, image, bndbox, out_size):
        center = bndbox[:2] + bndbox[2:] / 2
        size = bndbox[2:]

        context = self.context * size.sum() #(w+h)/2
        patch_sz = out_size / self.exemplarSize * \
                       np.sqrt((size + context).prod())

        return crop_pil(image, center, patch_sz, out_size=out_size)

context就是添加边界的宽度,即上述公式中的2p。这里又跳入函数crop_pil中,定义如下:

代码语言:javascript
复制
def crop_pil(image, center, size, padding='avg', out_size=None):
    # convert bndbox to corners
    size = np.array(size)
    corners = np.concatenate((center - size / 2, center + size / 2)) #(左上和右下)
    corners = np.round(corners).astype(int)

    pads = np.concatenate((-corners[:2], corners[2:] - image.size)) #填充原图,防止后面裁剪的时候出现越界现象
    npad = max(0, int(pads.max()))

    if npad > 0:#大于0则需要进行pad
        image = pad_pil(image, npad, padding=padding)
    corners = tuple((corners + npad).tolist())
    patch = image.crop(corners)

    if out_size is not None:
        if isinstance(out_size, numbers.Number):
            out_size = (out_size, out_size)
        if not out_size == patch.size:
            patch = patch.resize(out_size, Image.BILINEAR)

    return patch

主要完成了两个操作,分别为:

1. 填充(padding),如果需要添加边界的部分超出了原图的大小(一般出现在bounding box靠近图像边界的情况),则使用像素的均值代替。论文中这么说的:

When a sub-window extends beyond the extent of the image, the missing portions are lled with the mean RGB value

2. 缩放,这里用resize做的

同理对搜索图像x也做类似的操作,然后你会发现一个很有趣的事。

此时每对模板图像和搜索图像中心就是物体的bounding box中心!

这有什么好处呢? 答:我们在训练的时候,每次把模板图像滑到搜索图像的中心的时候,这时候会有最大的相似度,这为网络输出的真实值/标签设置,省了很多功夫!

到这里,siamese的输入就讲解结束了。下面介绍siamese的输出(真实值label)。

3.2 siameseFC的真实标签值

上面我们说到每对模板图像和搜索图像中心就是物体的bounding box中心!这为label设置提供了很大的便利。回到__getitem__中,获取label和weight的代码如下:

代码语言:javascript
复制
labels, weights = self.create_labels()  # create corresponding labels and weights
代码语言:javascript
复制
其中 self.create_labels()定义如下:
代码语言:javascript
复制
# create labels and weights. This section is similar to Matlab version of Siamfc
    def create_labels(self):
        labels = self.create_logisticloss_labels()
        weights = np.zeros_like(labels)

        pos_num = np.sum(labels == 1)
        neg_num = np.sum(labels == 0)
        weights[labels == 1] = 0.5 / pos_num
        weights[labels == 0] = 0.5 / neg_num
        #weights *= pos_num + neg_num

        labels = labels[np.newaxis, :]
        weights = weights[np.newaxis, :]

        return labels, weights

    def create_logisticloss_labels(self):
        label_sz = self.scoreSize #17
        r_pos = self.rPos / self.totalStride #16/8=2
        r_neg = self.rNeg / self.totalStride #0
        labels = np.zeros((label_sz, label_sz))

        for r in range(label_sz):
            for c in range(label_sz):
                dist = np.sqrt((r - label_sz // 2)**2 + (c - label_sz // 2)**2)
                if dist <= r_pos:
                    labels[r, c] = 1
                elif dist <= r_neg:
                    labels[r, c] = self.ignoreLabel
                else:
                    labels[r, c] = 0

        return labels

这里我们把一些默认值带入,获得labels的数值。如下:

代码语言:javascript
复制
array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])

这不中心区域都是1,其他都为0嘛!这意味着当模板图像移动搜索图像的中心时,获得最大相似度,即匹配成功

权重的定义也很简单,这里就不展示了。

当输入,输出标签获得了,通过网络前向过程的输出(预测值)和真实值(标签)就可以设置损失函数了。当然,这里的损失函数也很简单。

3.3 损失函数

论文中的损失函数部分如下展示:

其实就是利用logisitic loss 来计算预测的scores map真实的scores map中所有对应点的损失和,然后取平均即可。

代码实现更为简单了,如下:

代码语言:javascript
复制
criterion = BCEWeightLoss()                 # define criterion
output = model(search, template)
loss = criterion(output, labels, weights)/template.size(0)

至此损失函数部分就讲完了,siameseFC主体部分就聊完了。接下来我们将解析最后一个部分:实际使用中siameseFC的tracking部分。

4. siameseFC的tracking部分

如下为论文中对siameseFC的tracking部分就行的讲解。

大致的意思就是将网络输出的scores map通过插值的上采样法,将scores map从17x17放大至272x272。代码中是这样实现的:

代码语言:javascript
复制
scores_up = cv2.resize(scores, (config.final_sz, config.final_sz), interpolation=cv2.INTER_CUBIC)   # [257,257,3]
代码语言:javascript
复制
同时为了找到尺度合适的候选区域,代码中完成了以下步骤,相关注释都给出了,不过多讲解:
代码语言:javascript
复制
scores_ = np.squeeze(scores_up)
# penalize change of scale
scores_[0, :, :] = config.scalePenalty * scores_[0, :, :]
scores_[2, :, :] = config.scalePenalty * scores_[2, :, :]
# find scale with highest peak (after penalty)
new_scale_id = np.argmax(np.amax(scores_, axis=(1, 2)))
# update scaled sizes
x_sz = (1 - config.scaleLR) * x_sz + config.scaleLR * scaled_search_area[new_scale_id]
target_w = (1 - config.scaleLR) * target_w + config.scaleLR * scaled_target_w[new_scale_id]
target_h = (1 - config.scaleLR) * target_h + config.scaleLR * scaled_target_h[new_scale_id]

# select response with new_scale_id
score_ = scores_[new_scale_id, :, :]
score_ = score_ - np.min(score_)
score_ = score_ / np.sum(score_)
# apply displacement penalty
score_ = (1 - config.wInfluence) * score_ + config.wInfluence * penalty
p = np.asarray(np.unravel_index(np.argmax(score_), np.shape(score_)))                   # position of max response in score_
center = float(config.final_sz - 1) / 2                                                 # center of score_
disp_in_area = p - center
disp_in_xcrop = disp_in_area * float(config.totalStride) / config.responseUp
disp_in_frame = disp_in_xcrop * x_sz / config.instanceSize
pos_y, pos_x = pos_y + disp_in_frame[0], pos_x + disp_in_frame[1]
bboxes[f, :] = pos_x - target_w / 2, pos_y - target_h / 2, target_w, target_h

至此,siameseFC的tracking部分就讲完了!siameseFC的解析就告一段落了!

5. 总结

后面,我将接着siamese家族用在目标追踪领域上的其他成果,例如siameseRPN,siameseMask等进行讲解,继续分析这个类型网络的魅力所在。另外,我将解析更多的和目标追踪领域有关的论文和代码,希望大家支持!

参考:

[1] https://zhuanlan.zhihu.com/c_1166445784311758848

[2] https://zhuanlan.zhihu.com/c_1177216807848529920

[3] http://xxx.itp.ac.cn/abs/1606.09549

[4] Henriques, J.F., Caseiro, R., Martins, P., Batista, J.: High-speed tracking with kernelized correlation lters. PAMI 37(3) (2015) 583{596

[5] https://github.com/rafellerc/Pytorch-SiamFC

作者:知乎—周威

作者地址:https://www.zhihu.com/people/zhou-wei-37-26

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 人工智能前沿讲习 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 除了深度学习【目标检测】专栏[1],我开通了深度学习【目标追踪】专栏[2],用来记录学习目标追踪算法(单目标追踪SOT/多目标追踪MOT)论文/代码的解析。最近我在阅读目标追踪领域的文献综述时,遇到了很多关于孪生网络(siamese network)在目标追踪领域的应用。这里,我们以单目标追踪SOT中比较经典的Fully-Convolutional Siamese Networks(称之为siameseFC[3])网络,结合论文和代码,展开对siameseFC的讲解。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档