关于DIMP:Learning Discriminative Model Prediction for Tracking的Pipeline的理解

文献DIMP点击查看
如果只想看重点,直接看最后一页红色能容

1.数据流

这里的采样方法和ATOM中的几乎完全一致,可以参考ATOM

数据采样方法在“/ltr/train_settings/bbreg/atom.py”中,通过dataset_train = sampler.ATOMSampler(*args)封装。

dataset_train = sampler.ATOMSampler([lasot_train, got10k_train, trackingnet_train, coco_train],
                                   [1, 1, 1, 1],
                                   # 这里的samples_per_epoch=batch_size x n,如果batch是1,在训练中显示的就是
                                   # [train: num_epoch, x / batch_size * n] FPS: 0.3 (4.5)  ,  Loss/total: 43.69654  ,  Loss/segm: 43.69654  ,  Stats/acc: 0.56702
                                   # 由於batch_size * n構成了一個Epoch中所有的TensorDict形式的數據數量,通過LTRLoader包裝成batch結構後,就剩 "n" 個TensorDict,這裏就是1000個
                                   samples_per_epoch=1000*settings.batch_size,
                                   max_gap=50,
                                   processing=data_processing_train)

通过继承了torch.utils.data.dataloader.DataLoader类的LTRLoader()方法,在"/ltr/trainers/ltr_trainer.py"中用for i, data in enumerate(loader, 1)来遍历数据,具体作用就是把采样的数据按照一定规则打包,输出一个[batch, n_frames, channels, H, W]格式的数据

loader_train = LTRLoader('train',
                         dataset_train,
                         #
                         training=True,
                         batch_size=settings.batch_size,
                         # 数据读取线程数
                         num_workers=settings.num_workers,
                         # 在DDP模式下,没有这个参数
                         shuffle=True,
                         # 例如,99个数据,batch=30,最后会剩下9个数据,这时候就把这9个数据扔掉不用
                         drop_last=True,
                         # 在loader中,意思是:按batch_size抽取的数据,在第“1”维度上拼接起来,大概就是num_sequences
                         stack_dim=1)

封装进训练器,训练器的作用就是把数据分发给网络模型部分,然后让网络进行前向过程,计算损失,通过损失函数,来给网络赋能,并且更新数据。

trainer = LTRTrainer(actor, [loader_train, loader_val], optimizer, settings, lr_scheduler)

之所以把数据加载模块封装成[loader_train, loader_val],是因为在"/ltr/trainers/ltr_trainer.py"中,需要实现每隔n个Epoch进行一次validation,具体实现方法为:

# selfl.loaders就是[loader_train, loader_val]
for loader in self.loaders:
	if self.epoch % loader.epoch_interval == 0:
		# 这里就是利用 for i, data in enumerate(loader, 1)来把数据放入网络
		self.cycle_dataset(loader)

2.数据采样方法

于DiMP的num_train_frames=3, num_test_frames=3, 而ATOM的num_train_frames=1, num_test_frames=1, Template和Search分别都只有一张。
数据采样函数是"/ltr/data/sampler.py"中的class ATOMSamlper,其继承于class TrackingSampler,可以这么理解,采样函数实际上就是class TrackingSampler,而class ATOMSamlper的作用就是给其父类初始化一些参数。

①数据集随机抽取

在函数中有一组列表,self.p_datasets = [4, 3, 2, 1],就意味着4个数据集,按照 4 4 + 3 + 2 + 1 \frac{4}{4+3+2+1} 4+3+2+14 3 4 + 3 + 2 + 1 \frac{3}{4+3+2+1} 4+3+2+13 2 4 + 3 + 2 + 1 \frac{2}{4+3+2+1} 4+3+2+12 1 4 + 3 + 2 + 1 \frac{1}{4+3+2+1} 4+3+2+11 的概率进行抽取数某一据集。

p_total = sum(p_datasets)
self.p_datasets = [x / p_total for x in p_datasets]
# 这里的self.datasets是在ltr/train_settings/bbreg/atom.py中封装的[lasot_train, got10k_train, trackingnet_train, coco_train]
# 这里的dataset返回的是ltr/dataset下面的各类函数,例如lasot.py中的class Lasot(BaseVideoDataset):
dataset = random.choices(self.datasets, self.p_datasets)[0]

②某一数据集中的视频序列随机抽取

首先通过dataset.get_num_sequences()获取数据集中一共有多少个视频序列
然后在众多视频序列中抽取一个视频序列(一个视频序列又包含了很多帧图片)

seq_id = random.randint(0, dataset.get_num_sequences() - 1)

③在某一视频序列种采样,如Got-10k数据集

❶ interval采样:

interval

在一个视频序列中,随机抽取一张图片,作为base_frame;
在base_frame前后 max_gap=50的范围内(即 ± \pm ± 50 )分别抽取一张train_frame和一张test_frame;
如果没有抽到,则max_gap + gap_increase,扩大范围去抽取;

为何在 ± \pm ± 50这么大的范围内都抽不到呢?

因为需要抽取含有目标的视频帧,有时候视频中不含有可见目标,此时需要增大搜索范围

❷ casual采样:

casual

以视频序列的中间点作为参考帧,即base_frame
在base_frame前后 max_gap=50的范围内(即 ± \pm ± 50 )分别抽取一张train_frame和一张test_frame;
如果没有抽到,则max_gap + gap_increase,扩大范围去抽取;

❸ default采样:

没有参考帧base_frame,直接先随机在视频序列中抽取train_frame
在train_frame前后 max_gap=50的范围内(即 ± \pm ± 50 )随机抽取一张test_frame;
train_frame和test_frame可能会重复
如果没有抽到,则max_gap + gap_increase,扩大范围去抽取;

④在某一非视频序列中采样,例如COCO数据集

train_frame_ids = [1] * self.num_train_frames
test_frame_ids = [1] * self.num_test_frames
直接抽取

3.采样后的数据处理方法

处理函数继承的基类

class BaseProcessing:
    """
    处理类用于在传入网络之前,处理数据, 返回一个数据集
    例如,可以用于裁剪目标物体附近的搜索区域、用于不同的数据增强
    """
    def __init__(self, transform=transforms.ToTensor(), train_transform=None, test_transform=None, joint_transform=None):
        """
        参数:
            transform       : 用于图片的一系列变换操作
                              仅当train_transform或者test_transform是None的时候才用
            train_transform : 用于训练图片的一系列变换操作
                              如果为None, 取而代之的是'transform'值
            test_transform  : 用于测试图片的一系列变换操作
                              如果为None, 取而代之的是'transform'值
                              注意看,虽然在train_settings中设置的是transform_val,但是赋值的是transform_test=transform_val
                              所以,test_transform和transform_val是一回事
            joint_transform : 将'jointly'用于训练图片和测试图片的一系列变换操作
                              例如,可以转换测试和训练图片为灰度
        """
        self.transform = {
    
    'train': transform if train_transform is None else train_transform,
                          'test': transform if test_transform is None else test_transform,
                          'joint': joint_transform}

    def __call__(self, data: TensorDict):
        raise NotImplementedError

self.transform['joint']处理

就是先把所有图片都ToTensor,还有0.05的概率把图片变成灰度图。

transform_joint = tfm.Transform(tfm.ToGrayscale(probability=0.05))
# 这里的self.transform['joint']指向基类中的self.transform
data['train_images'], data['train_anno'] = self.transform['joint'](image=data['train_images'],
																   bbox=data['train_anno'])

self._get_jittered_box对bbox进行扰动

利用_get_jittered_box生成带扰动的bbox,该扰动只对test_anno有效,train_anno不会产生扰动。扰动控制通过self.scale_jitter_factorself.center_jitter_factor实现,其中mode是控制标志位。

self.scale_jitter_factor = {
    
    'train': 0, 'test': 0.5}
self.center_jitter_factor = {
    
    'train': 0, 'test': 4.5}

最终组合成:

    def _get_jittered_box(self, box, mode):
        """
        抖动一下输入box,box是相对坐标的(cx/sw, cy/sh, log(w), log(h))
        参数:
            box : 输入的bbox
            mode: 字符串'train'或者'test' 指的是训练或者测试数据
        返回值:
            torch.Tensor: jittered box
        """
        # randn(2) 生成两个服从(0,1)的数,范围是【-1,+1】前一个对应w,后一个对应h
        # 对于train,scale_jitter_factor=0,所以 jittered_size=box[2:4]
        jittered_size = box[2:4] * torch.exp(torch.randn(2) * self.scale_jitter_factor[mode])
        # 计算jitter_size后的x * y * w * h然后开方,乘以center_jitter_factor['train' or 'test'],作为最大偏移量
        # 对于train,center_jitter_factor=0,所以 max_offset=0
        max_offset = (jittered_size.prod().sqrt() * torch.tensor(self.center_jitter_factor[mode]).float())
        # 计算中心抖动 [x + w/2 + max_offset * (torch.randn(2)[0] - 0.5),  y + h/2 + max_offset * (torch.randn(2)[1] - 0.5)]
        jittered_center = box[0:2] + 0.5 * box[2:4] + max_offset * (torch.rand(2) - 0.5)
        return torch.cat((jittered_center - 0.5 * jittered_size, jittered_size), dim=0)

具体效果描述:

test_anno=[x, y, w, h]长宽 [ w , h ] [w, h] [w,h]随机放大或者缩小 [ 1 e , e ] [\frac{1}{\sqrt{e}}, \sqrt{e}] [e 1,e ]倍(服从正态分布,倍数是1的概率最大, 放大 e \sqrt{e} e 或缩小 1 e \frac{1}{\sqrt{e}} e 1的概率最低),得到新的长宽 [ w j i t t e r e d , h j i t t e r e d ] [w_{jittered}, h_{jittered}] [wjittered,hjittered]
 
test_anno=[x, y, w, h]中心点坐标 [ x + w 2 , y + h 2 ] [x+\frac{w}{2}, y+\frac{h}{2}] [x+2w,y+2h]随机偏移 [ − 1 2 w j i t t e r e d × h j i t t e r e d × 4.5 , + 1 2 w j i t t e r e d × h j i t t e r e d × 4.5 ] [-\frac{1}{2}\sqrt{w_{jittered}\times h_{jittered}}\times 4.5, +\frac{1}{2}\sqrt{w_{jittered}\times h_{jittered}}\times 4.5] [21wjittered×hjittered ×4.5,+21wjittered×hjittered ×4.5] (服从正态分布, 偏移量为0的概率最大, 偏移量为 1 2 w j i t t e r e d × h j i t t e r e d × 4.5 \frac{1}{2}\sqrt{w_{jittered}\times h_{jittered}}\times 4.5 21wjittered×hjittered ×4.5的概率最低)
 
最终得到 [ x j i t t e r e d , y j i t t e r e d , w j i t t e r e d , h j i t t e r e d ] [x_{jittered}, y_{jittered}, w_{jittered}, h_{jittered}] [xjittered,yjittered,wjittered,hjittered]

prutils.jittered_center_crop根据以上处理的结果裁剪

将输入图片按照 [ x j i t t e r e d , y j i t t e r e d , w j i t t e r e d , h j i t t e r e d ] [x_{jittered}, y_{jittered}, w_{jittered}, h_{jittered}] [xjittered,yjittered,wjittered,hjittered]、真实标注、search_area_factor和output_size裁剪出需要的尺寸,并取得裁剪后的图片中Boundingbox的对应坐标。

④再次transform

将上述过程的结果进行transform,参数封装在“/ltr/train_settings/bbreg/atom.py”中。

transform_train = tfm.Transform(tfm.ToTensorAndJitter(0.2),
                                tfm.Normalize(mean=settings.normalize_mean,
                                                  std=settings.normalize_std))

关于tfm.ToTensorAndJitter(0.2),就是服从正态分布的概率,让图片在 [ 0.8 , 1.2 ] [0.8, 1.2] [0.8,1.2]之间进行亮度调整, 即不变的概率最大, × 0.8 \times 0.8 ×0.8 × 1.2 \times 1.2 ×1.2的概率最低。

class ToTensorAndJitter(TransformBase):
    """
       继承了TransformBase,所有下面的transform_image和transform_mask会在TransformBase
       通过transform_func = getattr(self, 'transform_' + var_name),来调用具体用了哪个函数
       """
    def __init__(self, brightness_jitter=0.0, normalize=True):
        super().__init__()
        self.brightness_jitter = brightness_jitter
        self.normalize = normalize
    def roll(self):
        return np.random.uniform(max(0, 1 - self.brightness_jitter), 1 + self.brightness_jitter)
    def transform(self, img, brightness_factor):
        img = torch.from_numpy(img.transpose((2, 0, 1)))
        # 这里的brightness_factor是随机参数,其实就是roll的返回值
        return img.float().mul(brightness_factor / 255.0).clamp(0.0, 1.0)

关于tfm.Normalize(mean=settings.normalize_mean, std=settings.normalize_std),就是使用平均值settings.normalize_mean = [0.485, 0.456, 0.406]和标准差settings.normalize_std = [0.229, 0.224, 0.225]对图片进行归一化

class Normalize(TransformBase):
    def __init__(self, mean, std, inplace=False):
        super().__init__()
        # settings.normalize_mean = [0.485, 0.456, 0.406]
        self.mean = mean
        # settings.normalize_std = [0.229, 0.224, 0.225]
        self.std = std
        # 计算得到的值不会覆盖之前的值
        self.inplace = inplace

    def transform_image(self, image):
        return tvisf.normalize(image, self.mean, self.std, self.inplace)

⑤利用self._generate_proposals()data['test_anno']添加噪声

data['test_anno']就是根据① ② ③ ④ ⑤过程生成的bbox

	self.proposal_params = {
    
    'min_iou': 0.1, 'boxes_per_frame': 16, 'sigma_factor': [0.01, 0.05, 0.1, 0.2, 0.3]}
	
    def _generate_proposals(self, box):
        """
        通过给输入的box添加噪音,生成proposal
        """
        # 生成proposal
        num_proposals = self.proposal_params['boxes_per_frame']
        # .get(key,'default')查找键值‘key’,如果不存在,则返回‘default’
        proposal_method = self.proposal_params.get('proposal_method', 'default')
        if proposal_method == 'default':
            proposals = torch.zeros((num_proposals, 4))
            gt_iou = torch.zeros(num_proposals)
            for i in range(num_proposals):
                proposals[i, :], gt_iou[i] = prutils.perturb_box(box,
                                                                 min_iou=self.proposal_params['min_iou'],
                                                                 sigma_factor=self.proposal_params['sigma_factor'])

        elif proposal_method == 'gmm':
            proposals, _, _ = prutils.sample_box_gmm(box,
                                                     self.proposal_params['proposal_sigma'],
                                                     num_samples=num_proposals)
            gt_iou = prutils.iou(box.view(1, 4), proposals.view(-1, 4))

        # map to [-1, 1]
        gt_iou = gt_iou * 2 - 1
        return proposals, gt_iou

❶ 第一种扰动方法:

计算data['test_anno] [ x c e n t e r , y c e n t e r , w , h ] [x_{center}, y_{center}, w, h] [xcenter,ycenter,w,h]
 
'sigma_factor': [0.01, 0.05, 0.1, 0.2, 0.3]随机抽取一个值,例如0.1,然后变成perturb_factor=[0.1, 0.1, 0.1, 0.1]的Tensor
 
利用random.gauss(bbox[0], perturb_factor[0]),计算平均值为 [ x c e n t e r , y c e n t e r , w , h ] [x_{center}, y_{center}, w, h] [xcenter,ycenter,w,h]、标准差为[0.1, 0.1, 0.1, 0.1]的扰动,白话就是 [ x c e n t e r , y c e n t e r , w , h ] [x_{center}, y_{center}, w, h] [xcenter,ycenter,w,h]概率最高,得到扰动后的 [ x p e r t u r b e d , y p e r t u r b e d , w p e r t u r b e d , h p e r t u r b e d ] [x_{perturbed}, y_{perturbed}, w_{perturbed}, h_{perturbed}] [xperturbed,yperturbed,wperturbed,hperturbed]
 
计算 [ x p e r t u r b e d , y p e r t u r b e d , w p e r t u r b e d , h p e r t u r b e d ] [x_{perturbed}, y_{perturbed}, w_{perturbed}, h_{perturbed}] [xperturbed,yperturbed,wperturbed,hperturbed] [ x c e n t e r , y c e n t e r , w , h ] [x_{center}, y_{center}, w, h] [xcenter,ycenter,w,h]的IOU
 
将扰动系数perturb_factor *= 0.9
 

将上述过程循环100次,得到结果,如果在100次以内就得到了box_iou > min_iou的结果,直接输出一组box_per, box_iou

将上述过程进行16次,得到16组box_per, box_iou, 就是num_proposals

❷ 第二种扰动方法(高斯混合模型):
高斯混合模型,即使用多个高斯函数去近似概率分布:
p G M M = Σ k = 1 K p ( k ) p ( x ∣ k ) = Σ k = 1 K α k p ( x ∣ μ k , Σ k ) p_{GMM} = \Sigma^{K}_{k=1}p(k)p(x|k) = \Sigma^{K}_{k=1}\alpha_k p(x|\mu_k, \Sigma_k) pGMM=Σk=1Kp(k)p(xk)=Σk=1Kαkp(xμk,Σk)
其中, K K K为模型个数(相当于num_proposals=16),就是用了多少个单高斯分布; α k \alpha_k αk是第 k k k个单高斯分布的概率, Σ k = 1 K α k = 1 \Sigma^{K}_{k=1}\alpha_k = 1 Σk=1Kαk=1 p ( x ∣ μ k , Σ k ) p(x|\mu_k, \Sigma_k) p(xμk,Σk)是的第 k k k个均值为 μ k \mu_k μk,方差为 Σ k \Sigma_k Σk高斯分布的概率密度
代码实现:
方差 Σ k \Sigma_k Σk

# proposal_sigma = [[a, b], [c, d]]
center_std = torch.Tensor([s[0] for s in proposal_sigma])
sz_std = torch.Tensor([s[1] for s in proposal_sigma])
# stack后维度[4,1,2]
std = torch.stack([center_std, center_std, sz_std, sz_std])
# 2
num_components = std.shape[-1]
# 4
num_dims = std.numel() // num_components
# (1,4,2)
std = std.view(1, num_dims, num_components)

模型个数 K K K

k = torch.randint(num_components, (num_samples,), dtype=torch.int64)
# 输出[16, 4], std=[1, 4, 2],由于这里k只有0和1,作用就是把最后一个维度复制成为16,索引方式就是index=0或1
std_samp = std[0, :, k].t()

Bbox经过GMM采样后的中心点坐标(这里是中心点坐标的偏差,相当于 x i − x x_i - x xix),然后根据平均值,计算 x i x_i xi

x_centered = std_samp * torch.randn(num_samples, num_dims)
# rel左上角和长宽的对数表示bbox
proposals_rel = x_centered + mean_box_rel

⑥ 高斯标签分部(ATOM中没有这个操作)

data["train_anno"]是用于网络训练部分,而data["test_anno"]是用于和网络输出pred计算损失的,即高斯标签作为Ground_Truth。

def gauss_1d(sz, sigma, center, end_pad=0, density=False):
    # K 决定了输出的尺寸,理解为meshgrid更合适
    k = torch.arange(-(sz - 1) / 2, (sz + 1) / 2 + end_pad).reshape(1, -1).cuda() - center.reshape(1, -1)
    gauss = torch.exp(-1.0 / (2 * sigma ** 2) * (k) ** 2)
    if density:
        gauss /= math.sqrt(2 * math.pi) * sigma
    return gauss


def gauss_2d(sz, sigma, center, end_pad=(0, 0), density=False):
    return gauss_1d(sz[0].item(), sigma[0], center[:, 0], end_pad[0], density).reshape(center.shape[0], 1, -1) * gauss_1d(sz[1].item(), sigma[1], center[:, 1], end_pad[1], density).reshape(center.shape[0], -1, 1)


def gaussian_label_function(target_bb, end_pad_if_even=False, density=False, uni_bias=0):
    # 为了直观,这里设置一下参数,作为展示
    uni_bias = torch.Tensor([uni_bias]).cuda()
    sigma_factor = torch.Tensor([0.25, 0.25]).cuda()
    feat_sz = torch.Tensor([18, 18]).cuda()
    image_sz = torch.Tensor([511, 511]).cuda()
    #target_bb = target_bb.view(-1, *target_bb.shape)

    # target_bb 包含了3个bbox, 也就是num_train_frames或者num_test_frames
    # target_bb=[3, 4]    target_center=[3, 2]
    # 注意,这里的target_bb是image_crop下的坐标,需要通过norm转换到feat_sz=[18, 18下面
    target_center = target_bb[:, 0:2] + 0.5 * target_bb[:, 2:4]
    target_center_norm = (target_center - image_sz / 2) / image_sz
    center = feat_sz * target_center_norm+ 0.5 * torch.Tensor([(kernel_sz[0] + 1) % 2, (kernel_sz[1] + 1) % 2])
    sigma = sigma_factor * feat_sz.prod().sqrt().item()

    if end_pad_if_even:
        end_pad = (1, 1)
    else:
        end_pad = (0, 0)
    gauss_label = gauss_2d(feat_sz, sigma, center, end_pad, density=density).float()
    if density:
        sz = (feat_sz + torch.Tensor(end_pad)).prod()
        label = (1.0 - uni_bias) * gauss_label + uni_bias / sz
    else:
        label = gauss_label + uni_bias

    return label

效果如图gaussian_label

⑦组合输出

data['test_images']data['train_images']图片(单张),在LTRLoader中才会打包出Batch
data['test_anno']采样后经过self._generate_proposals,变换成:
    data['test_proposals']包含8个bbox
    data['proposal_iou']包含8个扰动bbox的IoU
data['train_anno']经过⑥self._generate_label_function()后的bbox, 送入网络
data['test_anno']经过⑥self._generate_label_function()后的bbox,用于计算损失

4. 网络模型

Dimp_model论文中的模型结构是包含了Train和Inference两个部分,且不完整的,没有对ATOM的IoUnet部分进行展示。Backbone依然是ATOM结构输出layer2和layer3的两部分(图中红线框中表示的)。图中绿线框中才是完整模型,模型有两个输出,一个是Iou_pred, 一个是filter的score_pred。
那么这两个输出有什么用呢?

首先我们回忆一下ATOM中的classifier:
f ( x ; w ) = ϕ 2 w 2 × ϕ 1 ( w 1 × x ) f(x;w) = \phi_{2}{w_{2} \times \phi_{1}{(w_1 \times x)}} f(x;w)=ϕ2w2×ϕ1(w1×x)
实际上是训练了两组filters,只不过训练的位置在Inference中,利用filters去卷feat得到score_raw,利用这个得分,去求位置信息。
而在DIMP中,把这个过程 f ( x ; w ) f(x; w) f(x;w)放到了训练的步骤中,说句大逆不道的话,就是相同classifier模型,换一个模块位置而已…
DiMP中的filter在理论上还是 f ( x ; w ) f(x;w) f(x;w),每个Epoch中,迭代5次,求一组Filters.
在推理中,这组filters卷feat得到类似ATOM中的Score_raw,classifier的输出用途都是一样的,一比一替换就行。

    def track(self, image, info: dict=None) -> dict:
        self.debug_info = {
    
    }
        self.frame_num += 1
        self.debug_info['frame_num'] = self.frame_num

        # 图片从numpy格式转换成tensor
        im = numpy_to_torch(image)

        # 定位

        # 提取backbone特征,调用dimpnet中的extract_backbone_features返回layer2和layer3的featuremap
        backbone_feat, sample_coords, im_patches = self.extract_backbone_features(im,
                                                                                  self.get_centered_sample_pos(),
                                                                                  self.target_scale * self.params.scale_factors,
                                                                                  self.img_sample_sz)
        # 提取分类特征,调用dimpnet中的extract_classification_feat,返回的是经过Bottlenet的layer2和layer3的featuremap
        test_x = self.get_classification_features(backbone_feat)
        
        # 样本的定位, 就是featuremap中bbox的坐标
        sample_pos, sample_scales = self.get_sample_location(sample_coords)

        # 计算分类得分,不同于ATOM的Score_raw是在Inference中训出来的w1和w2计算出来的
        # 这里是直接训练出来了一组filters,然后拿来直接用了
        scores_raw = self.classify_target(test_x)
        # 这里是ATOM的做法:
        # scores_raw = self.apply_filter(test_x)
        # self.apply_filter = operation.conv2d(sample_x, self.filter, mode='same')

        # 定位目标
        translation_vec, scale_ind, s, flag = self.localize_target(scores_raw, sample_pos, sample_scales)
        new_pos = sample_pos[scale_ind, :] + translation_vec

        # 更新位置和缩放因子
        if flag != 'not_found':
            if self.params.get('use_iou_net', True):
                update_scale_flag = self.params.get('update_scale_when_uncertain', True) or flag != 'uncertain'
                if self.params.get('use_classifier', True):
                    self.update_state(new_pos)
                # ATOM中是用W1和W2计算的pos来迭代5次IoUnet,得到精确到bbox坐标
                # DIMP中是用模型训练的filters计算的pos,在IoUNet中迭代5次,得到精确到bbox坐标
                self.refine_target_box(backbone_feat, sample_pos[scale_ind, :], sample_scales[scale_ind], scale_ind, update_scale_flag)
            elif self.params.get('use_classifier', True):
                self.update_state(new_pos, sample_scales[scale_ind])

        # 更新
        update_flag = flag not in ['not_found', 'uncertain']
        hard_negative = (flag == 'hard_negative')
        learning_rate = self.params.get('hard_negative_learning_rate', None) if hard_negative else None

        if update_flag and self.params.get('update_classifier', False):
            # 获得训练样本
            train_x = test_x[scale_ind:scale_ind+1, ...]
            # 创建target_box和空间样本的标签
            target_box = self.get_iounet_box(self.pos, self.target_sz, sample_pos[scale_ind, :], sample_scales[scale_ind])
            # 更新分类模型
            """
            利用DiMP网络,把卷图片用的weights[...]算出来
                其中还有hard_negative:将难样本加入初始化的负例样本中进行训练
                    最后把得到的self.target_filter放入def classify_target中,
                        就是把 self.target_filter和图片卷一下,得到scores
            """
            self.update_classifier(train_x, target_box, learning_rate, s[scale_ind, ...])

        # 设置追踪器位置到IoUNet的位置
        if self.params.get('use_iou_net', True) and flag != 'not_found' and hasattr(self, 'pos_iounet'):
            self.pos = self.pos_iounet.clone()

        score_map = s[scale_ind, ...]
        max_score = torch.max(score_map).item()

        # 可视化和设置显示的debug标志
        self.search_area_box = torch.cat((sample_coords[scale_ind, [1, 0]], sample_coords[scale_ind, [3, 2]] - sample_coords[scale_ind, [1, 0]] - 1))
        self.debug_info['flag' + self.id_str] = flag
        self.debug_info['max_score' + self.id_str] = max_score
        if self.visdom is not None:
            self.visdom.register(score_map, 'heatmap', 2, 'Score Map' + self.id_str)
            self.visdom.register(self.debug_info, 'info_dict', 1, 'Status')
        elif self.params.debug >= 2:
            show_tensor(score_map, 5, title='Max Score = {:.2f}'.format(max_score))

        # 计算输出的BoundingBox,换句话说,就是下一帧track的pos
        new_state = torch.cat((self.pos[[1, 0]] - (self.target_sz[[1, 0]] - 1) / 2, self.target_sz[[1, 0]]))

        if self.params.get('output_not_found_box', False) and flag == 'not_found':
            output_state = [-1, -1, -1, -1]
        else:
            output_state = new_state.tolist()

        out = {
    
    'target_bbox': output_state}
        return out

损失函数和滤波器更新方式

  • 损失函数
    L ( f ) = 1 S t r a i n Σ ( x , c ) ∈ S t r a i n ( ∣ ∣ r ( x × f , c ) ∣ ∣ 2 + ∣ ∣ λ f ∣ ∣ 2 ) L(f) = \frac{1}{S_{train}} \underset {(x,c) \in S_{train}}{\Sigma} (|| r(x \times f, c) ||^2 + || \lambda f||^2) L(f)=Strain1(x,c)StrainΣ(∣∣r(x×f,c)2+∣∣λf2)
    其中 r ( x × f , c ) r(x \times f, c) r(x×f,c)是残差,具体计算方法为:
    r ( x × f , c ) = ν c ⋅ [ m c ⋅ x × f + ( 1 − m c ) m a x ( 0 , x × f ) − c ] r(x \times f, c) = \nu _c \cdot [m_c \cdot x \times f + (1 - m_c)max(0, x \times f) - c] r(x×f,c)=νc[mcx×f+(1mc)max(0,x×f)c]
    其中 ν c \nu _c νc是空间权重, m c m_c mc是mask, 都是根据 d i s t m a p dist_{map} distmap卷积出的结果。
    • d i s t m a p dist_{map} distmap计算方法就是欧式空间距离dist经过函数变化得到的,而dist则是将BoundingBox的坐标中心作为坐标原点,将Featuremap的尺寸作为坐标范围,如图所示:
      在这里插入图片描述 变换函数为:
      y c ( t ) = Σ k = 0 N − 1 ϕ k y ρ k ( ∣ ∣ t − c ∣ ∣ ) y_c (t) = \Sigma^{N-1}_{k=0} \phi^y_k \rho_k (|| t - c||) yc(t)=Σk=0N1ϕkyρk(∣∣tc∣∣)
      其中函数 ρ k ( ∣ ∣ t − c ∣ ∣ ) \rho_k(|| t - c ||) ρk(∣∣tc∣∣)为: ( t − c t-c tc就是上面的dist)
      ρ k ( d ) = { m a x ( 0 , 1 − ∣ d − k △ ∣ △ ) k < N − 1 m a x ( 0 , m i n ( 1 , 1 + d − k △ △ ) ) k = N − 1 \rho_k(d) = \begin{cases} max(0, 1 - \frac{|d - k \triangle|}{\triangle}) & k<N-1 \\ max(0, min(1, 1+\frac{d - k \triangle}{\triangle})) & k=N-1 \end{cases} ρk(d)={ max(0,1dk△∣)max(0,min(1,1+dk))k<N1k=N1

    • ν c \nu _{c} νc计算方法 , 将 d i s t m a p dist_{map} distmap送入卷积层:

    num_bin_dist = 100
    self.spatial_weight_predictor = nn.Conv2d(num_dist_bins, 1, kernel_size=1, bias=False) 
    spatial_weight = self.spatial_weight_predictor(dist_map)
    
    • m c m_c mc的计算方法 , 将 d i s t m a p dist_{map} distmap送入卷积层:
    num_bin_dist = 100
    mask_layers = [nn.Conv2d(num_dist_bins, 1, kernel_size=1, bias=False)]
    if mask_act == 'sigmoid':
    	mask_layers.append(nn.Sigmoid())
    	init_bias = 0.0
    elif mask_act == 'linear':
    	init_bias = 0.5
    else:
    	raise ValueError("激活函数未知")
    # 预测的就是损失中的 mc 被牵制在了[0, 1],还要经过sigmoid
    self.target_mask_predictor = nn.Sequential(*mask_layers)
    target_mask = self.target_mask_predictor(dist_map)
    
  • 梯度下降和更新步长
    L ( f ) L(f) L(f)进行泰勒展开:
    L ( f ) ≈ L ~ ( f ) = 1 2 ( f − f ( i ) ) T Q ( i ) ( f − f ( i ) ) + ( f − f ( i ) ) T ∇ L ( f ( i ) ) + L ( f ( i ) ) L(f) \approx \tilde{L}(f) = \frac{1}{2}(f - f^{(i)})^T Q^{(i)}(f - f^{(i)}) + (f - f^{(i)})^T \nabla L(f^{(i)}) + L(f^{(i)}) L(f)L~(f)=21(ff(i))TQ(i)(ff(i))+(ff(i))TL(f(i))+L(f(i))
    步长 α \alpha α为:
    α = ∇ L ( f ( i ) ) T ∇ L ( f ( i ) ) ∇ L ( f ( i ) ) T Q ( i ) ∇ L ( f ( i ) ) \alpha = \frac{\nabla L(f^{(i)})^T \nabla L(f^{(i)})}{\nabla L(f^{(i)})^T Q^{(i)} \nabla L(f^{(i)})} α=L(f(i))TQ(i)L(f(i))L(f(i))TL(f(i))
    更新方式:
    f ( i + 1 ) = f ( i ) − α ∇ L ( f ( i ) ) f^(i+1) = f^{(i)} - \alpha \nabla L(f^{(i)}) f(i+1)=f(i)αL(f(i))

猜你喜欢

转载自blog.csdn.net/Soonki/article/details/129478349