SiamRPN代码讲解,推理测试讲解

siamRPN论文:High Performance Visual Tracking with Siamese Region Proposal Network

gitHub代码:https://github.com/HonglinChu/SiamTrackers/tree/master/SiamRPN/SiamRPN

论文模型架构:
在这里插入图片描述

在此文章中将以代码+注释的形式详解推理过程,即test.py中的代码。
后续有空将会详解训练过程即train.py的代码。

siamrpn推理的大致过程:

|—1.搭建模型
|—2.跟踪过程:
|——2.1 第一帧:
|——截取exemplar_frame(Img_z)
|——获得初始bbox(需要注意这里x,y值的含义),target_sz ,score_filters_z, reg_filters_z(后面两个用于后续互操作)
|——2.2 非第一帧:
|——截取detection_frame(Img_x)
|——通过Backbone得到score_filters_x, reg_filters_x
|——互相关操纵后得到pred_score, pred_regression(dx,dy,dw,dh)(这里很重要)
|——进行后处理(最重要)
|———分类后处理
|————softmax,得到”前景”概率
|———框偏移后处理
|————将bbox与pred_regression进行逆向计算,得到预测框(非最终的框,需要注意这里x,y值的含义)
|——获得惩罚因子(对应论文公式) 通过惩罚因子修正 ”前景”概率,修正预测框
|——预测框第二次修正,得到最终结果框
|——更新修改下一帧的中心偏差和图片大小偏差
|——返回最终结果框用于画图
|—画出box

代码阅读大致顺序:

|—./bin/test.py
|——SiamRPNTracker类
|———初始超参
|———创建SiamRPNNet()
|————BackBone
|————4个卷积块(conv)
|———初始化创建所有区域建议框
|———初始化创建window
|——ExperimentOTB类
|———OTB类(继承Object)
|——run函数(推理)
|——eport函数(结果)

如果读过前面本人写的Siamfc相关文章:
SiamFC代码讲解,训练过程讲解
SiamFC代码讲解,推理测试讲解

那么阅读本篇文章会比较轻松。(因为希望你了解本人的书写流程)
Siamrpn与Siamfc相比,就是多了一个rpn网络,导致会多一个相关后处理的操作。后处理的相关操作比较复杂,希望本文能讲解清楚。

如果你不了解什么是rpn网络,那么本人推荐先看此篇文章:
(博主:懒人元)RPN 解析

代码流程

./bin/my_test.py
首先看到Line27,进入到

SiamRPNTracker类的init函数

./siamrpn/tracker.py中
代码详解(注释)
过程简述:

  1. 超参初始化
  2. 创建siamfc架构;
  3. 模型加载;
  4. 初始transformer
  5. 初始所有候选区域框
  6. 初始proposed_box_num个 window(由hanning窗生成,用于最后加权求和)
ExperimentOTB类:

回到./bin/my_test.py
看到Line33 通过ExperimentOTB类获取数据集,与siamFC类似:
siamFC中相关代码详解(注释)
这里不做赘述

experiments/otb.py下run函数

回到./bin/my_test.py
看到Line56 (关键代码)
进入到./experiments/otb.py中run函数
代码详解(注释)
过程简述:

通过for循环遍历dataset
并执行如下操作:

  1. 创建输出结果.txt(如果不存在的话)
  2. 进行推理 (最重要)
  3. 结果保存

当前函数中看到tracker.track(experiments/otb.py—Line55)
这是最关键的一步,点进去,进入到./siamrpn/racker.py下的track函数

siamrpn/tracker.py下track函数

ps:需要注意不要进入到siamFC.py下track函数
代码详解(注释)
过程简述:

  1. 获得第一个框(目标框的参数)
  2. 如果是第一帧,进行推理初始化(init函数)
  3. 如果是非第一帧,进行正常推理(update函数)
  4. 结果框的显示
siamrpn/tracker.py下init函数:

代码详解(注释)
过程简述:

  1. 所有初始box 的坐标转换(left,up,w,h)–>(cx,cy,w,h)
  2. 初始化裁剪图片的中心点坐标 (self.pos)
  3. 初始化大小(后续会随着过程改变),原始图片大小(self.target_sz;;self.origin_target_sz)
  4. 初始化exemplar_img
  5. 初始化score_filters_z, reg_filters_z(self.model.track_init函数)并固定,用于后续互相关

如果非第一帧,跳转进入到update函数(siamfc.py --Line301):

siamrpn/tracker.py下update函数::

代码详解(注释)
过程简述:

  1. 获得detection_img
  2. 经过模型,得到pred_score, pred_regression(dx,dy,dw,dh)
  3. 后处理
    3.1 分类后处理
    3.2 框偏移后处理
  4. 获取惩罚因子并更新前景概率
  5. 获得最优前景概率下的id(index)
  6. 再次修正框值,得到最终值
  7. 修改下一帧的中心偏差和图片大小偏差
  8. 返回Box值,用于后续可显示化

————————————————————————————————————

代码解析:

TrackerSiamRPN类init函数
./siamrpn/tracker.py----Line18
class SiamRPNTracker:
    def __init__(self, model_path,cfg=None,is_deterministic=False):

        self.name='SiamRPN'

        if cfg:
            config.update(cfg)

        self.model = SiamRPNNet()
        self.is_deterministic = is_deterministic
       
        checkpoint = torch.load(model_path)

        if 'model' in checkpoint.keys():
            self.model.load_state_dict(torch.load(model_path)['model'])#
        else:
            self.model.load_state_dict(torch.load(model_path))
        
        self.model = self.model.cuda()
        self.model.eval()
        self.transforms = transforms.Compose([ToTensor()])


        
        self.anchors = generate_anchors(config.total_stride,        ##8
                                        config.anchor_base_size,    ##8
                                        config.anchor_scales,       ##np.array([8,])
                                        config.anchor_ratios,       ##np.array([0.33, 0.5, 1, 2, 3])
                                        config.valid_scope) #       ##19

        self.window = np.tile(np.outer(np.hanning(config.score_size), np.hanning(config.score_size))[None, :],
                              [config.anchor_num, 1, 1]).flatten()
        ##生成5个window, 由汉宁窗生成,汉宁窗是使用加权余弦形成的。
        ##self.window的值: 越靠近中心,值越大
        ##用于最后加权求和

        """
        np.outer()  的doc说明
        Compute the outer product of two vectors.
        Given two vectors, a = [a0, a1, ..., aM] and b = [b0, b1, ..., bN], the outer product [1] is:
        [[a0*b0  a0*b1 ... a0*bN ]

     [a1*b0    .
         [ ...          .
         [aM*b0            aM*bN ]]
        """

对应代码补充
config.update(参数初始化)
SiamRPNNet类(详细模型搭建代码)
generate_anchor函数

run函数
otb.py下run函数 (otb.py---Line38)
    def run(self, tracker, visualize=False):
        print('Running tracker %s on %s...' % (
            tracker.name, type(self.dataset).__name__))

        # loop over the complete dataset
        for s, (img_files, anno) in enumerate(self.dataset): 
        ## img_files是list ,保存的是dataset中某一子文件夹中的所有图片路径
         ##anno 是list ,保存的是dataset中某一子文件夹中的groundtruth路径
            seq_name = self.dataset.seq_names[s] ##取第s批数据
            print('--Sequence %d/%d: %s' % (s + 1, len(self.dataset), seq_name))

            # skip if results exist
            record_file = os.path.join(
                self.result_dir, tracker.name, '%s.txt' % seq_name)
            if os.path.exists(record_file):
                print('  Found results, skipping', seq_name)
                continue

            # tracking loop
            boxes, times = tracker.track(       ##最重要部分
                img_files, anno[0, :], visualize=visualize)
            assert len(boxes) == len(anno)
            """
            img_files: 保存的是一个文件夹下所有图片的路径
            anno[0, :]: 第一张图片的annotation值;目标框的annnotation值(因为siamfc始终实以第一帧图片作为目标框)
            visualize: 结果的可视化
            """

            # record results
            self._record(record_file, boxes, times)
track函数
siamrpn/tarcker.py下track函数 (.siamrpn/tracker.py---Line245)
    def track(self, img_files, box, visualize=False):
        ##box: 第一帧,在siamfc中是要跟踪的物体,且后续不会发生变化
        frame_num = len(img_files)       ##总帧数
        boxes = np.zeros((frame_num, 4)) ##准备预测所有框的参数
        boxes[0] = box                   ##获得第一个框(目标框的参数)
        times = np.zeros(frame_num)      ## 时间,用于后续计算fps

        for f, img_file in enumerate(img_files):
            img = ops.read_image(img_file) 
            ##img_file 这里传入的是一个img路径

            begin = time.time()
            if f == 0:          ##第一帧
                self.init(img, box)
                ##初始化了很多参数,并固定feature_z 并作为后续的卷积核
            else:##不过不是第一帧
                boxes[f, :] = self.update(img)  ##重要函数,实际推理过程
            times[f] = time.time() - begin##耗时

            if visualize:
                ops.show_image(img, boxes[f, :])

        return boxes, times
init函数(第一帧)
siamrpn/tarcker.py下init函数 (.siamrpn/tracker.py---Line72)

    def init(self, frame, bbox):
        """ initialize siamrpn tracker
        Args:
            frame: an RGB image
            bbox: one-based bounding box [left_x, up_y, width, height]
        """
        self.bbox = np.array([bbox[0]-1 + (bbox[2]-1) / 2 , bbox[1]-1 + (bbox[3]-1) / 2 , bbox[2], bbox[3]]) #cx,cy,w,h
        self.pos = np.array([bbox[0]-1 + (bbox[2]-1) / 2 , bbox[1]-1 + (bbox[3]-1) / 2])  # center x, center y, zero based
        ## center_pos
        ##self.pos=self.bbox[:2,...]

        self.target_sz = np.array([bbox[2], bbox[3]])  # width, height   ## ps,后续会更新
        self.origin_target_sz = np.array([bbox[2], bbox[3]])#原始w,h 后续不会更新
        # get exemplar img
        self.img_mean = np.mean(frame, axis=(0, 1)) ###边界填充像素值

        ##
        exemplar_img, scale_z, _ = get_exemplar_image(frame, self.bbox,config.exemplar_size, config.context_amount, self.img_mean)
        ## scale_z ==127 / crop_size 未用到

        # get exemplar feature
        exemplar_img = self.transforms(exemplar_img)[None, :, :, :]#在测试阶段,转换成tensor类型就可以了
        self.model.track_init(exemplar_img.cuda()) ##初始score_filters_z, reg_filters_z

对应代码补充:
self.model.track_init()

update函数(非第一帧)
siamrpn/tarcker.py下update函数 (.siamrpn/tracker.py---Line97)
def update(self, frame):
        """track object based on the previous frame
        Args:
            frame: an RGB image

        Returns:
            bbox: tuple of 1-based bounding box(xmin, ymin, xmax, ymax)
        """
        instance_img_np, _, _, scale_x = get_instance_image(frame, self.bbox, config.exemplar_size,
                                                         config.instance_size,
                                                         config.context_amount, self.img_mean)
        ##scale_x == 271/detection_frame_crop_size  值靠近1
        instance_img = self.transforms(instance_img_np)[None, :, :, :]
        ##model_inference_
        pred_score, pred_regression = self.model.track(instance_img.cuda())##互操作 ;;后续会有补充代码说明
        # pred_score=1,2x5,19,19 ; pre_regression=1,4x5,19,19
        ## 前后背景概率;;;;;;;预测框的值:偏移中心点坐标和预测wh


        ######后处理

        ### 1. 分类后处理
        pred_conf = pred_score.reshape(-1, 2, config.anchor_num * config.score_size * config.score_size).permute(0,2,1)
        ##1,2*5,19,19---->1,2,5*19*19--->1,5*19*19,2
        # 前后背景所有框个数: 5*19*19;;;分类问题
        score_pred = F.softmax(pred_conf, dim=2)[0, :, 1].cpu().detach().numpy()  # 计算预测分类得分
        ##score_pred.shape==(1, 2*5*19*19,1)           ↑ 只取"物体"的概率  ,这里,默认第一个值是”背景"的概率,第二个值是”物体“的概率


        ### 2.框偏移后处理
        pred_offset = pred_regression.reshape(-1, 4,config.anchor_num * config.score_size * config.score_size).permute(0,2,1)
        ##1,4*5,19,19---->1,4,5*19*19--->1,5*19*19,4
        # 回归问题,,true_box==proposed_box+offset  proposed_box是初始固定的,这里回归问题是offset,也就是模型要学习的东西
        delta = pred_offset[0].cpu().detach().numpy()
        #使用detach()函数来切断一些分支的反向传播;返回一个新的Variable,从当前计算图中分离下来的,但是仍指向原变量的存放位置,不同之处只是requires_grad为false,得到的这个Variable永远不需要计算其梯度,不具有grad。
        #即使之后重新将它的requires_grad置为true,它也不会具有梯度grad #这样我们就会继续使用这个新的Variable进行计算,后面当我们进行反向传播时,到该调用detach()的Variable就会停止,不能再继续向前进行传播
        
        box_pred = box_transform_inv(self.anchors, delta) #通过 anchors 和 offset 来获得预测的box ;;;后续会有补充代码说明
        ###self.anchors: 初始的anchors == x,y,w,h  其中x,y 是相对于图中心的坐标偏移(经过dx,dy计算后);;w,h为候选预测框的大小(经过dw,dh计算后)
        ###e.g. self.anchors[0]==-72,-72 ,104,32
        ##因此,这里计算的box_pred  x,y也是相对于图中心的坐标偏移
        """
        下面如不好理解可以跳过
       所以true_box_x = F( center_x + box_pred[0] )  ##其中F 代表某种变换,可能是惩罚因子 ;;其中center_x ==上一时刻的box[0]
           true_box_y = F( center_y + box_pred[1] )
           true_box_w = G( w + box_pred[2] )         ##其中G 代表某种变换,可能与F相同也可能不同;;功能可能是加权求和
           true_box_h = G( y + box_pred[3] )
        """


        def change(r): 
            return np.maximum(r, 1. / r) #np.maximum(x,y): x 和 y 逐位进行比较选择最大值


        def sz(w, h):
            pad = (w + h) * 0.5 ##(很像模板图像扩充))
            sz2 = (w + pad) * (h + pad)
            return np.sqrt(sz2)

        def sz_wh(wh):
            pad = (wh[0] + wh[1]) * 0.5
            sz2 = (wh[0] + pad) * (wh[1] + pad)## (模板图像扩充)
            return np.sqrt(sz2)


        ####获得惩罚因子,对应论文中  4.3 公式(13)
        s_c = change(sz(box_pred[:, 2], box_pred[:, 3]) / (sz_wh(self.target_sz * scale_x)))  # scale penalty
        """
        sz_result=sz(box_pred[:, 2], box_pred[:, 3])
        sz_wh_result=sz_wh(self.target_sz * scale_x)  ##target_sz.shape==(1,2)  scale_x近似 127/257
        change(sz_result/sz_wh_result)
        
        只有当box_pred[:,2:]===self.target_sz的时候为1 ,其他情况下肯定大于1
        
        """
        r_c = change((self.target_sz[0] / self.target_sz[1]) / (box_pred[:, 2] / box_pred[:, 3]))  # ratio penalty
        ##获取 宽高比率的最大值


        penalty = np.exp(-(r_c * s_c - 1.) * config.penalty_k)#尺度惩罚和比例惩罚   获得最终惩罚因子
        ###penalty_k =0.22
        ####获得惩罚因子,对应论文中  4.3 公式(13)

        pscore = penalty * score_pred#对每一个anchors的分类预测×惩罚因子
        pscore = pscore * (1 - config.window_influence) + self.window * config.window_influence #再乘以余弦窗,加重图片中间是物体的概率,削减图片边缘是物体的概率
        """
        pscore: 两个权重求和
        self.window汉宁窗 大小19*19,越靠近中心点值越大
        window_influence = 0.40
        增加中心点附近的的像素值为物体的概率
        """
        ##选择最优框
        best_pscore_id = np.argmax(pscore) #得到  最大的得分对应的下标
        
        target = box_pred[best_pscore_id, :] / scale_x #最优物体概率对应id的box进行scale_x(==271/detection_frame_crop_size)缩放

        lr = penalty[best_pscore_id] * score_pred[best_pscore_id] * config.lr_box#预测框的学习率
        ##CONFIG.LR_BOX==0.30  ???推理预测的时候为什么还要涉及到lr???(只影响后续更新 box_w,box_h)
        ###原因是------用于后续加权修正W,H



        ####如下步骤是获得真正的box (true_box)
        ##首先要知道,这里的target的值===x,y,w,h  其中x,y 是相对于图中心的坐标偏移(经过dx,dy计算后);;w,h为候选预测框的大小(经过dw,dh计算后)
        res_x = np.clip(target[0] + self.pos[0], 0, frame.shape[1])#w=frame.shape[1]
          ##等价于np.clip(x+offset_x , 0 , W)  防止cx 越界
        ##np.clip(a,a_min,a_max)函数 该函数的作用是将数组a中的所有数限定到范围a_min和a_max中。
      
        """
        res: result 最终结果
        target== cx,cy,w,h
        self.pos== center_x,center_y
        """
        res_y = np.clip(target[1] + self.pos[1], 0, frame.shape[0])#h=frame.shape[0]

		###宽高加权修正, lr_box的作用
        res_w = np.clip(self.target_sz[0] * (1 - lr) + target[2] * lr, config.min_scale * self.origin_target_sz[0],
                        config.max_scale * self.origin_target_sz[0])
        """
            config.min_scale ==0.1   config.max_scale==10
            target_sz初始值为 origin_target_sz,后续会进行更新
            一般情况下: res_w= self.target_sz[0] * (1 - lr) + target[2] * lr ;;权重求和
        """

        res_h = np.clip(self.target_sz[1] * (1 - lr) + target[3] * lr, config.min_scale * self.origin_target_sz[1],
                        config.max_scale * self.origin_target_sz[1])
                        

        ###更新信息
        self.pos = np.array([res_x, res_y]) #更新之后的坐标

        self.target_sz = np.array([res_w, res_h]) #更新框大小

        bbox = np.array([res_x, res_y, res_w, res_h])##得到最终预测的box值

        self.bbox = ( #cx, cy, w, h
            np.clip(bbox[0], 0, frame.shape[1]).astype(np.float64), ##np.clip 在这里是防止越界
            np.clip(bbox[1], 0, frame.shape[0]).astype(np.float64),
            np.clip(bbox[2], 10, frame.shape[1]).astype(np.float64),
            np.clip(bbox[3], 10, frame.shape[0]).astype(np.float64)) 

        #这个用来画图使用 
        bbox=np.array([# left,up, W, H
            self.pos[0] + 1 - (self.target_sz[0]-1) / 2,
            self.pos[1] + 1 - (self.target_sz[1]-1) / 2,
            self.target_sz[0], self.target_sz[1]])

        #return self.bbox, score_pred[best_pscore_id]
        """
        可以看到没有修改 feature_z 和 score_filters_z, reg_filters_z
        和siamfc一致,只选取第一帧作为模板图(exemplar_img)
        """
        return bbox

论文中,惩罚因子的计算公式:
SiamRPN代码讲解,推理测试讲解_第1张图片

对应代码补充:
self.model.track
box_transform_inv函数

————————————————————————————————————

代码补充说明

Config类
功能:超参数的定义
class Config:
    # dataset related
    exemplar_size = 127                    # exemplar frame_size
    instance_size = 271 #默认271             # detection frame_size;;与论文不同,论文大小是255
    context_amount = 0.5                   # context amount 图像模板扩充大小
    sample_type = 'uniform'

    #--------
    train_epoch_size = 1000 # 
    val_epoch_size = 100    #
    out_feature = 19 #对应论文,互相关后的尺寸大小;;与论文不同,这里是19,因为detection_frame_size的不同
    #max_inter   = 80#  
    eps = 0.01    #
    #--------

    # training related
    #...
    # training related
    
    total_stride = 8                       ## training related
    valid_scope = int((instance_size - exemplar_size) / total_stride + 1)
    ## (271-127)/8+1  ==18+1==19  等于最后互相关后的尺寸大小

    anchor_scales = np.array([8,])                 ##Backbone会进行8倍下采样
    anchor_ratios = np.array([0.33, 0.5, 1, 2, 3]) ##5个anchor的 尺度大小 
    anchor_num = len(anchor_scales) * len(anchor_ratios)##==5 论文中对应K的数字
    anchor_base_size = 8                           ##Backbone会进行8倍下采样
    pos_threshold = 0.6
    neg_threshold = 0.3
    num_pos = 16
    num_neg = 48
    lamb = 5            # 原始 cls:res = 1:5
    save_interval = 1
    show_interval = 100#100
    show_topK = 3
    pretrained_model = './models/alexnet.pth'

    # tracking related
    gray_ratio = 0.25
    blur_ratio = 0.15
    score_size = int((instance_size - exemplar_size) / total_stride + 1) ##### (271-127)/8+1  ==18+1==19 等于最后互相关后的尺寸大小
    penalty_k =0.22 #0.22 #0.22          #0.16   
    window_influence = 0.40#0.40 #0.40  #0.40 会创建汉宁窗相关窗口,进行后续的加权求和
    lr_box =0.30 #0.30                  #0.30
    min_scale = 0.1                     ##预测框的最小情况比率
    max_scale = 10                      ##预测框的最大情况比率

    def update(self, cfg):              ##更新超参数
        for k, v in cfg.items():
            setattr(self, k, v)
        self.score_size = (self.instance_size - self.exemplar_size) //self.total_stride + 1 #
        #self.valid_scope = int((self.instance_size - self.exemplar_size) / self.total_stride / 2)#anchor的范围
        self.valid_scope= self.score_size
        ## (271-127)/8+1  ==18+1==19

config = Config()

补充说明:
Config类的超参数不用刻意去记住,等看到代码中存在config.XXX的时候,再去查看。这能进一步去理解代码的含义。

SiamRPNNet类(详细模型搭建代码)
init函数+update函数

class SiamRPNNet(nn.Module):
    def __init__(self, ):
        super(SiamRPNNet, self).__init__()                              ##exemplar尺寸变化情况            detection尺寸变化情况
        ## conv_O=(I-K+2P)/S+1   maxpool_O=(I-k)/s+1                     exemplar_size = 127    instance_size = 271
        self.featureExtract = nn.Sequential(                                     ##      127                   271
            nn.Conv2d(3, 96, 11, stride=2),  # stride=2                      (127-11)/2+1==59        (271-11)/2+1==131
            nn.BatchNorm2d(96),              #
            nn.MaxPool2d(3, stride=2),       # stride=2  默认valid模式,         ##(59-3)/2+1==29     (131-3)/2 +1==65
            nn.ReLU(inplace=True), 
            nn.Conv2d(96, 256, 5),                                                      ##25                    61
            nn.BatchNorm2d(256),
            nn.MaxPool2d(3, stride=2),       # stride=2                                 ##12                   ##30
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 384, 3),                                                     ##10                   ##28
            nn.BatchNorm2d(384),             #
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 384, 3),                                                     ##8                    ##26
            nn.BatchNorm2d(384),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, 3),                                                     ##6                   ## 24
            nn.BatchNorm2d(256),             #
        )

        self.anchor_num = config.anchor_num   #每一个位置有5个anchor
        self.input_size = config.instance_size ##config.instance_size==271
        self.score_displacement = int((self.input_size - config.exemplar_size) / config.total_stride)   ##(271-127)/8 ==18
        self.conv_cls1 = nn.Conv2d(256, 256 * 2 * self.anchor_num, kernel_size=3, stride=1, padding=0)
        self.conv_r1 = nn.Conv2d(256, 256 * 4 * self.anchor_num, kernel_size=3, stride=1, padding=0)

        self.conv_cls2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
        self.conv_r2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
        self.regress_adjust = nn.Conv2d(4 * self.anchor_num, 4 * self.anchor_num, 1)# ## 1*卷积 用于线性回归后的一个1*1conv操作

    def forward(self, template, detection):
        
        N = template.size(0) # N=32  batch==32
       
        template_feature = self.featureExtract(template)#[N,256,6,6]     ###exemplar_frame--->exemplar_feature
        detection_feature = self.featureExtract(detection)#[N,256,24,24] ##detection_frame--->detection_feature
        
		##self.anchor_num==5
		kernel_score = self.conv_cls1(template_feature).view(N, 2 * self.anchor_num, 256, 4, 4)    #N,2*5,256,4,4
        kernel_regression = self.conv_r1(template_feature).view(N, 4 * self.anchor_num, 256, 4, 4) #N,4*5,256,4,4
       
        conv_score = self.conv_cls2(detection_feature) #N,256,22,22#对齐操作
        conv_regression = self.conv_r2(detection_feature)#N,256,22,22

		##组卷积 类别分支 互相关操作
        conv_scores = conv_score.reshape(1, -1, self.score_displacement + 4, self.score_displacement + 4)#1,Nx256,22,22
        score_filters = kernel_score.reshape(-1, 256, 4, 4) # Nx10,256,4,4
        pred_score = F.conv2d(conv_scores, score_filters, groups=N).reshape(N, 10, self.score_displacement + 1,self.score_displacement + 1)##groups=N   result.shape==1,32*10,19,19
       
		##组卷积 线性回归分支 互相关操作
        conv_reg = conv_regression.reshape(1, -1, self.score_displacement + 4, self.score_displacement + 4)##N,256,22,22-->1,N*256,22,22
        reg_filters = kernel_regression.reshape(-1, 256, 4, 4)##N,4*5,256,4,4--->N*20,256,4,4
        pred_regression = self.regress_adjust(F.conv2d(conv_reg, reg_filters, groups=N).reshape(N, 20, self.score_displacement + 1,self.score_displacement + 1)) ###           result.shape==1,32*20,19,19
        return pred_score, pred_regression

##补充说明:

  1. 卷积输出conv_O=(I-K+2P)/S+1
  2. 池化输出maxpool_O==I-K+2P)/S+1 (PS:池化一般P=0)
    self.conv_cls1,self.conv_r1;;self.conv_cls2,self.conv_r2; 对应论文中如下:
        self.conv_cls1 = nn.Conv2d(256, 256 * 2 * self.anchor_num, kernel_size=3, stride=1, padding=0)
        self.conv_r1 = nn.Conv2d(256, 256 * 4 * self.anchor_num, kernel_size=3, stride=1, padding=0)

        self.conv_cls2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
        self.conv_r2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)

SiamRPN代码讲解,推理测试讲解_第2张图片

  1. 需要重点关注 exemplar_frame和detection_frame经过backbone的尺度变化。 需要注意,论文中detection_frame_size初始值为255,而代码中为271。这就导致了论文中模型架构图的一些尺寸发生变化,紫框对应的是255尺寸大小后的结果,与271尺寸大小后的结果相差[(271-255)/8==]2。固互相关后的尺寸大小为19而不是17。
  2. 组卷积。口诀:被卷积的(w,h比较大的)的通道为B×C,卷积核(小的wh)的通道数为C。group=N, 组卷积后结果为:1, N×10,19,19。列如本文中的分类分支:
detecition_cls_feature.shape=N,256,22,22 ,则需要reshape为  -1, N*C,22,22 (C=256)  [被卷积的(w,h比较大的)的通道为B*C] 
exemplar_cls_feature.shape=N,2*5,256,4,4, 则需要reshape为  -1, C,4,4 (C=256)    [卷积核(小的wh)的通道数为C]
generate_anchor函数
./utils.py---Line33 功能:初始创建19*19尺寸下的所有anchor
def generate_anchors(total_stride, base_size, scales, ratios, score_size):
    """
    ##total_stride=8
    ##base_size=8
    ##scales=numpy.array([8.])
    ##rations=numpy.array([0.33,0.5,1,2,3])
    ##score_size=19
    """

    anchor_num = len(ratios) * len(scales) ##默认为5
    anchor = np.zeros((anchor_num, 4), dtype=np.float32) ## 
    size = base_size * base_size  ##64, 19*19下一个像素点相对于原图的大小;也即标准锚框大小
    count = 0
    for ratio in ratios:
        # ws = int(np.sqrt(size * 1.0 / ratio))
        ws = int(np.sqrt(size / ratio))	##根据ration 生成不同的毛框大小
        hs = int(ws * ratio)
        for scale in scales:
            wws = ws * scale
            hhs = hs * scale
            anchor[count, 0] = 0
            anchor[count, 1] = 0
            anchor[count, 2] = wws ##初始w
            anchor[count, 3] = hhs ##初始h
            count += 1
    anchor= np.tile(anchor, score_size * score_size)# [5,4]----->[5,4 *score_size*score_size]==[5,4*19*19]
    #tile铺平复制
    anchor = anchor.reshape((-1, 4))#[5,4*19*19]--->[5*19*19,4]  #
    ##总共有 5*19*19个框

    ori = - (score_size // 2) * total_stride  ##-19//2  *8 ===-9*8=-72
    # the left displacement
    xx, yy = np.meshgrid([ori + total_stride * dx for dx in range(score_size)],
                         [ori + total_stride * dy for dy in range(score_size)])
    ##获得原始尺寸大小下  相对于中心点的 x,y坐标
    
    xx, yy = np.tile(xx.flatten(), (anchor_num, 1)).flatten(), np.tile(yy.flatten(), (anchor_num, 1)).flatten()
    ## #tile铺平复制

    """
    xx[:20]
    >>>
    [-72 -64 -56 -48 -40 -32 -24 -16  -8   0   8  16  24  32  40  48  56  64 72 -72]
    yy[:20]
    >>>
    [-72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -64]
    """
    anchor[:, 0], anchor[:, 1] = xx.astype(np.float32), yy.astype(np.float32)

    return anchor

##补充说明:

  1. 生成的anchor,数据形如:
    anchor[0]== -72,-72,104,104。
    一定要注意!
    一定要注意!
    一定要注意!
    这里的前面两个值代表的是:原始尺寸大小下 相对于中心点的 x,y坐标
    例如这里的anchor[0],应该是第一个像素点的第一个anchor框,即图片左上角的anchor框。
    -72,-72则是相对于图像中间的偏移。这里一定要好好理解,后面后处理的时候会用到。

  2. 涉及到np.tile函数和np.meshgrid函数,读者可以上网自行查阅,本文只做小demo演示
    np.tile函数:
    功能,铺平复制

demo:
import numpy as np

a=np.arange(0,20)
a=a.reshape(5,4)
print(a)
print(a.shape)
a=np.tile(a,17*17)
print(a[0,:20])
print(a[1,:20])
print(a.shape)  
----------------------
>>>
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]
 [16 17 18 19]]
>>>
 (5, 4)
>>>
[0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3]
>>>
[4 5 6 7 4 5 6 7 4 5 6 7 4 5 6 7 4 5 6 7]
>>>
(5, 1156)

np.meshgrid函数:
功能:生成网格数据

demo:
import numpy as np
x = np.linspace(1, 4, 4)
y = np.linspace(6, 7, 2)
X, Y = np.meshgrid(x, y)
print(x)
print(y)
print(X)
print(Y)
----------------------
>>>
[1. 2. 3. 4.]
>>>
[6. 7.]
>>>
[[1. 2. 3. 4.]
 [1. 2. 3. 4.]]
>>>
[[6. 6. 6. 6.]
 [7. 7. 7. 7.]]

track_init函数
./siamrpn/network.py --Line79 (跟踪推理第一帧的时候会调用到) 功能: 初始化 score_filters_z和reg_filters_z
    def track_init(self, template):
        ##初始 score_filters_z, reg_filters_z
        N = template.size(0) ##N==1
        template_feature = self.featureExtract(template)# 输出 [1, 256, 6, 6]
        # kernel_score=1,2x5,256,4,4   kernel_regression=1,4x5, 256,4,4
        kernel_score = self.conv_cls1(template_feature).view(N, 2 * self.anchor_num, 256, 4, 4) ##后续 分类回归的2k个box
        kernel_regression = self.conv_r1(template_feature).view(N, 4 * self.anchor_num, 256, 4, 4)###regression
        self.score_filters = kernel_score.reshape(-1, 256, 4, 4)   # 2x5, 256, 4, 4  
        self.reg_filters = kernel_regression.reshape(-1, 256, 4, 4)# 4x5, 256, 4, 4

./network.py下track函数
功能:前面步骤通过track_init得到了self.score_filters和self.reg_filters。这里通过传入detection_frame,经过Backbone,可以得到conv_scores,conv_reg,进而得到互相关后的两个结果。 对应论文公式:

SiamRPN代码讲解,推理测试讲解_第3张图片

 def track(self, detection):
        N = detection.size(0)  ##batch推理的时候,默认N==1
        detection_feature = self.featureExtract(detection)##推理:feature_x.shape  1,256,24,24
        
        conv_score = self.conv_cls2(detection_feature) ##不清楚可以看network.py下的__init__函数  1,256,22,22
        conv_regression = self.conv_r2(detection_feature)##不清楚可以看network.py下的__init__函数  1,256,22,22
        
        conv_scores = conv_score.reshape(1, -1, self.score_displacement + 4, self.score_displacement + 4)
        #self.score_displacement==18, conv_score的shape变化: 1,256,22,22---> 1,256,22,22
        conv_reg = conv_regression.reshape(1, -1, self.score_displacement + 4, self.score_displacement + 4)
        pred_score = F.conv2d(conv_scores, self.score_filters, groups=N).reshape(N, 10, self.score_displacement + 1,self.score_displacement + 1)##组卷积
        pred_regression = self.regress_adjust(F.conv2d(conv_reg, self.reg_filters, groups=N).reshape(N, 20, self.score_displacement + 1,self.score_displacement + 1))
        return pred_score, pred_regression  ## shape : 1,10,19,19 ||| 1,20,19,19


box_transform_inv函数

功能:通过给定anchor框和offset(dx,dy,dw,dh)计算预测框的值
对应论文公式:

SiamRPN代码讲解,推理测试讲解_第4张图片


def box_transform_inv(anchors, offset):
    anchor_xctr = anchors[:, :1] #cx
    anchor_yctr = anchors[:, 1:2]#cy
    anchor_w = anchors[:, 2:3] #w
    anchor_h = anchors[:, 3:]  #h
    offset_x, offset_y, offset_w, offset_h = offset[:, :1], offset[:, 1:2], offset[:, 2:3], offset[:, 3:],

    box_cx = anchor_xctr+ anchor_w * offset_x 
    box_cy = anchor_yctr+ anchor_h * offset_y 
    box_w = anchor_w * np.exp(offset_w)
    box_h = anchor_h * np.exp(offset_h)
    box = np.hstack([box_cx, box_cy, box_w, box_h]) #水平方向堆叠  np.vstack竖直方向堆叠
    return box

这里只讲解box_cx,box_cy的计算公式,box_w和box_h类似。
首先看论文中的相关公式:
SiamRPN代码讲解,推理测试讲解_第5张图片

如下图,我们知道了cx1,cy1,w1,h1(蓝色框),这是anchor_box初始创建的。后来通过模型可以得到reg_delta。如何计算得到cx2,cy2呢?

SiamRPN代码讲解,推理测试讲解_第6张图片

首先,可以知道 cx1+dx=cx2 ;;cy1+dy=cy2;
而 dx=reg_delta[0]*w1;;dy=reg_delta[2]*h1
所以 cx2=cx1+reg_delta[0]*w1;;cy2=cy1+reg_delta[1]*h1

注意!!
这里得到的box_cx,box_cy,box_w,box_h,其中box_cx,box_cy还是相对于中心图片的偏移值,和anchors的前两个值的意义相同。

回顾

siamrpn推理的大致过程:

|—1.搭建模型
|—2.跟踪过程:
|——2.1 第一帧:
|——截取exemplar_frame(Img_z)
|——获得初始bbox(需要注意这里x,y值的含义),target_sz ,score_filters_z, reg_filters_z(后面两个用于后续互操作)
|——2.2 非第一帧:
|——截取detection_frame(Img_x)
|——通过Backbone得到score_filters_x, reg_filters_x
|——互相关操纵后得到pred_score, pred_regression(dx,dy,dw,dh)(这里很重要)
|——进行后处理(最重要)
|———分类后处理
|————softmax,得到”前景”概率
|———框偏移后处理
|————将bbox与pred_regression进行逆向计算,得到预测框(非最终的框,需要注意这里x,y值的含义)
|——获得惩罚因子(对应论文公式) 通过惩罚因子修正 ”前景”概率,修正预测框
|——预测框第二次修正,得到最终结果框
|——更新修改下一帧的中心偏差和图片大小偏差
|——返回最终结果框用于画图
|—画出box

欢迎指正

因为本文主要是本人用来做的笔记,顺便进行知识巩固。如果本文对你有所帮助,那么本博客的目的就已经超额完成了。
本人英语水平、阅读论文能力、读写代码能力较为有限。有错误,恳请大佬指正,感谢。

欢迎交流
邮箱:[email protected]

你可能感兴趣的:(目标跟踪,论文阅读,深度学习,计算机视觉,目标跟踪,pytorch,论文阅读)