关于ATOM:Accurate Tracking by Overlap Maximization的Pipeline的理解

1.数据流

数据采样方法在“/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.数据采样方法

数据采样函数是"/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

⑥组合输出

data['test_images']data['train_images']图片(单张),在LTRLoader中才会打包出Batch
data['test_anno']采样后经过self._generate_proposals,变换成:
    data['test_proposals']包含16个bbox
    data['proposal_iou']包含16个扰动bbox的IoU
data['train_anno']真实的bbox,不经过self._generate_proposals的bbox

4. 网络模型

重难点:
train_inference_model
图中红色虚线框中的部分是模型训练部分,绿色实线框是在模型推理中通过共轭梯度下降产生的一组filters,整张图就是包含了训练和推理的完整模型图。完整的inference部分如下图:
whole_inference

图中有两个5次迭代,classification的5次迭代对应算法中的红色线框,IoU_predict的对应算法图中的绿色线框,算法图如下
Algorithm

猜你喜欢

转载自blog.csdn.net/Soonki/article/details/129407734
今日推荐