专栏首页人工智能前沿讲习【SOT】siameseFC论文和代码解析

【SOT】siameseFC论文和代码解析

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网络代码定义如下:

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网络:

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定义如下:

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__中,找到如下:

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定义如下:

# 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中,定义如下:

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的代码如下:

labels, weights = self.create_labels()  # create corresponding labels and weights
其中 self.create_labels()定义如下:
# 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的数值。如下:

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中所有对应点的损失和,然后取平均即可。

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

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。代码中是这样实现的:

scores_up = cv2.resize(scores, (config.final_sz, config.final_sz), interpolation=cv2.INTER_CUBIC)   # [257,257,3]
同时为了找到尺度合适的候选区域,代码中完成了以下步骤,相关注释都给出了,不过多讲解:
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

本文分享自微信公众号 - 人工智能前沿讲习(AIFrontier),作者:周威

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-09-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ICLR2019 | 表示形式语言:比较有限自动机和循环神经网络

    本文对ICLR2019论文《REPRESENTING FORMAL LANGUAGES:A COMPARISON BETWEEN FINITE AUTOMATA...

    马上科普尚尚
  • CCAI 2020 | 探秘AI的未来:对话Yolanda Gil

    2020年中国人工智能大会(Chinese Congress on Artificial Intelligence 2020,简称“CCAI 2020”)将于8...

    马上科普尚尚
  • 吴恩达发布AI转型指南:只需5步【附英文PDF的下载链接】

    从现在起到2030年之间,AI将创造大约13万亿美元的GDP增长。尽管AI已经为谷歌、百度、微软以及Facebook等科技公司创造了巨大价值,但其创造更多价值的...

    马上科普尚尚
  • [scikit-learn 机器学习] 4. 特征提取

    通常使用 one-hot 编码,产生2进制的编码,会扩展数据,当数据值种类多时,不宜使用

    Michael阿明
  • python读取文件——python读取和保存mat文件

        首先我们谈谈MarkDown编辑器,我感觉些倒是挺方便的,因为用惯了LaTeX,对于MarkDown还是比较容易上手的,但是我发现,MarkDown中有...

    zhaozhiyong
  • 人工智能_1_初识_机器学习介绍_特征工程和文本特征提取

    Dean0731
  • 投稿 | 图卷积网络 GCN: Graph Convolutional Networks

    上面左图是2D卷积神经网络,其输入是4行4列的矩阵,通过卷积核逐步移动实现对整个输入的卷积操作;而右图输入是图网络,其结构和连接是不规则的,无法像卷积神经网络那...

    磐创AI
  • Python|Numpy加粗

    算法与编程之美
  • 【python实现卷积神经网络】开始训练

    代码来源:https://github.com/eriklindernoren/ML-From-Scratch

    绝命生
  • 用Python开始机器学习:文本特征抽取与向量化

    假设我们刚看完诺兰的大片《星际穿越》,设想如何让机器来自动分析各位观众对电影的评价到底是“赞”(positive)还是“踩”(negative)呢? 这类问题就...

    机器学习AI算法工程

扫码关注云+社区

领取腾讯云代金券