open-vot:PyTorch 实现 Siamese-FC

open-vot 集成了 KCF、SiamFC、GOTURN 等8种跟踪算法,采用模块化设计,利于不同算法的比较及扩展。本文主要介绍其中的 SiamFC 实现。

运行要求

代码运行需安装以下依赖包:

conda install matplotlib shapely
conda install -c conda-forge tensorboardx 

对于 Python3 ,直接安装 urllib3:

conda install urllib3:

而 python2.7 需要参考 pip install urllib2失败 进行以下修改:

#from urllib.request import urlretrieve
import urllib2
    #return urlretrieve(url, filename, _reporthook)
    return urllib2.urlretrieve(url, filename, _reporthook)

模型和数据

预训练模型可直接在项目主页下载:

  • color
  • color+gray

alexnet-owt-4df8aa71.pth 可通过迅雷下载。

数据下载参考:CFNet视频目标跟踪源码运行笔记(2)——training and then tracking。

  • ILSVRC2015_VID.tar.gz
  • cfnet_ILSVRC2015.stats.mat 密码:hqhbwm

VID 训练集拥有3862个片段,平均帧数为290。

代码概述

SiamFC 训练主要涉及到 TrainerSiamFC、TrackerSiamFC 、SiameseNet、Pairwise 和 TransformSiamFC 几个对象。训练示例可参考 TestManagerSiamFC 。

TrackerSiamFC 组织模型训练,TrackerSiamFC 实现了跟踪器训练和推理的功能。SiameseNet 由 AlexNet 基础网络和 XCorr、Adjust2d 附加操作组成,Pairwise 对基本数据集进行封装,从中读取样本对。TransformSiamFC 对数据进行处理。

TrainerSiamFC

Trainer 调用 Tracker。

TrainerSiamFC
TrackerSiamFC

打开文件加载参数,并选择对应模型的参数。

接口设计有 bug,没有传入net_path

    def __init__(self, branch='alexv1', cfg_file=None):
        cfg = {}
        if cfg_file is not None:
            with open(cfg_file, 'r') as f:
                cfg = json.load(f)
            cfg = cfg[branch]

构造一个 TrackerSiamFC 跟踪器。

这里并不需要self.branchnet_path没有赋值。self.tracker.cfg在这里似乎也并不需要。

Logger 继承 tensorboardX 的 SummaryWriter 类。

        self.branch = branch
        self.tracker = TrackerSiamFC(branch=branch, net_path=None, **cfg)
        self.cfg = self.tracker.cfg
        self.logger = Logger(log_dir='logs/siamfc')
        self.cuda = torch.cuda.is_available()

train

Created with Raphaël 2.2.0 train initialize_weights TransformSiamFC multiprocessing.cpu_count ImageNetVID Pairwise DataLoader tracker.step logger.add_text logger.add_scalar tracker.model.module.state_dict logger.add_checkpoint End

initialize_weights 对不同的层参数进行初始化。

这里是一个 bug,初始化应放在 Tracker 中,而这里调用会覆盖已加载的模型。

        tracker = self.tracker
        initialize_weights(tracker.model)
        transform = TransformSiamFC(stats_path, **self.cfg._asdict())

multiprocessing.cpu_count() 返回系统中的CPU数量。此数字不等于当前进程可以使用的 CPU 数量。可以使用len(os.sched_getaffinity(0))获得可用 CPU 的数量。

        epoch_num = self.cfg.epoch_num
        cpu_num = multiprocessing.cpu_count()

vot_dataset没有用到。

        if vot_dir is not None:
            vot_dataset = VOT(vot_dir, return_rect=True, download=True)
        base_dataset = ImageNetVID(vid_dir, return_rect=True)

Pairwise 产生图像对。根据欧式距离生成dataset_train中的标签。
torch.utils.data.DataLoader 数据加载器。组合数据集和采样器,并在数据集上提供单进程或多进程迭代器。


        # training dataset
        dataset_train = Pairwise(
            base_dataset, transform, subset='train')
        dataloader_train = DataLoader(
            dataset_train, batch_size=self.cfg.batch_size, shuffle=True,
            pin_memory=self.cuda, drop_last=True, num_workers=cpu_num)
        # validation dataset
        dataset_val = Pairwise(
            base_dataset, transform, subset='val')
        dataloader_val = DataLoader(
            dataset_val, batch_size=self.cfg.batch_size, shuffle=False,
            pin_memory=self.cuda, drop_last=True, num_workers=cpu_num)
        train_iters = len(dataloader_train)
        val_iters = len(dataloader_val)

训练。

        for epoch in range(epoch_num):
            # training loop
            loss_epoch = 0

            for it, batch in enumerate(dataloader_train):
                loss = tracker.step(batch, update_lr=(it == 0))
                loss_epoch += loss

                # logging
                step = epoch * train_iters + it
                self.logger.add_text('train/iter_loss', '--Epoch: {}/{} Iter: {}/{} Loss: {:.6f}'.format(
                    epoch + 1, epoch_num, it + 1, train_iters, loss), step)
                self.logger.add_scalar('train/iter_loss', loss, step)

            loss_epoch /= train_iters

            # logging
            self.logger.add_text('train/epoch_loss', 'Epoch: {}/{} Loss: {:.6f}'.format(
                epoch + 1, epoch_num, loss_epoch), epoch)
            self.logger.add_scalar('train/epoch_loss', loss_epoch, epoch)

测试。

            # validation loop
            loss_val = 0

            for it, batch in enumerate(dataloader_val):
                loss = tracker.step(batch, backward=False)
                loss_val += loss

            loss_val /= val_iters

            # logging
            self.logger.add_text('train/val_epoch_loss', 'Epoch: {}/{} Val. Loss: {:.6f}'.format(
                epoch + 1, epoch_num, loss_val), epoch)
            self.logger.add_scalar('train/val_epoch_loss', loss_val, epoch)

在 VOT 上测试。self.track 应该是tracker.track。不过这里与 track 的接口并不一致。

            # tracking loop if vot_dir is available
            if vot_dir is not None:
                self.track(vot_dir, visualize=False)

添加检查点。
torch.nn.Module.state_dict 返回包含模块整个状态的字典。包括参数和持久缓冲区(例如,运行平均值)。键是对应的参数和缓冲区名称。
add_checkpoint 保存字典到文件。

            # add checkpoint
            self.logger.add_checkpoint(
                'siamfc', self.tracker.model.module.state_dict(),
                (epoch + 1) // 100 + 1)

TrackerSiamFC(object)

对象初始化过程中调用 setup_model 和 setup_optimizer 函数。
self.name并不需要。

    def __init__(self, branch='alexv2', net_path=None, **kargs):
        self.name = 'SiamFC'
        self.parse_args(**kargs)
        self.cuda = torch.cuda.is_available()
        self.device = torch.device('cuda:0' if self.cuda else 'cpu')
        self.setup_model(branch, net_path)
        self.setup_optimizer()

step

Created with Raphaël 2.2.0 step scheduler.step model.train optimizer.zero_grad torch.set_grad_enabled model criterion loss.backward optimizer.step End

为什么仅在第一次运行self.scheduler.step()

torch.optim.lr_scheduler.StepLR 将每个参数组的学习速率设置为每个step_size epoch 按 gamma 衰减的初始 lr。当last_epoch = -1时,将初始 lr 设置为 lr。

这里self.scheduler.step()的调用应放在调用 step 的循环之外,否则就变成了每次迭代调整学习率。

        if backward:
            if update_lr:
                self.scheduler.step()
            self.model.train()
        else:
            self.model.eval()

torch.Tensor.to 执行 Tensor dtype 和(或)设备转换。 从self.to(*args, **kwargs)的参数推断出 torch.dtype 和 torch.device。

如果self Tensor 已经有正确的 torch.dtype 和 torch.device,则返回self。否则,返回的张量是具有所需 torch.dtype 和 torch.device 的self的副本。

函数传入4个数据。

        z, x, labels, weights = \
            batch[0].to(self.device), batch[1].to(self.device), \
            batch[2].to(self.device), batch[3].to(self.device)

torch.optim.Optimizer.zero_grad 清除所有优化 torch.Tensor 的梯度。

torch.autograd.set_grad_enabled 上下文管理器,将梯度计算设置为打开或关闭。

依次运行前向->损失函数->反向->优化器。

torch.Tensor.backward 计算当前张量关于图叶子的梯度。

使用链式法则差分图。如果张量是非标量的(即其数据具有多于一个元素)并且需要梯度,则该函数还需要指定gradient。它应该是类型和位置匹配的张量,包含差分函数关于自身的梯度。

此函数在叶子中累积梯度 - 您可能需要在调用之前将它们归零。
参数:

  • gradient(Tensor或 None)—梯度相关张量。如果是张量,它将自动转换为不需要求梯度的张量,除非create_graphTrue。对于标量张量或不需要梯度的张量,不能指定任何值。如果 None 值是可接受的,则此参数是可选的。
  • retain_graphbool,可选)—如果为 False,将释放用于计算梯度的图。 请注意,几乎在所有情况下都不需要将此选项设置为True,并且通常可以以更有效的方式解决此问题。默认为create_graph的值。
  • create_graphbool,可选)—如果为True,将构造派生图,允许计算更高阶的导数。默认为False

torch.optim.Optimizer.step 执行单个优化步骤(参数更新)。

torch.Tensor.item 以标准 Python 数返回此张量的值。这仅适用于具有一个元素的张量。对于其他情况,请参阅tolist()。此操作不可差分。

        self.optimizer.zero_grad()
        with torch.set_grad_enabled(backward):
            pred = self.model(z, x)
            loss = self.criterion(pred, labels, weights)
            if backward:
                loss.backward()
                self.optimizer.step()

        return loss.item()

setup_optimizer

torch.nn.Module.named_parameters 返回模块参数的迭代器,同时产生参数名称和参数本身。
.0表示 conv,.1表示 bn。

这里应检查param.requires_grad,不应将冻结的参数加入到列表中。

        params = []
        for name, param in self.model.named_parameters():
            lr = self.cfg.initial_lr
            weight_decay = self.cfg.weight_decay
            if '.0' in name:  # conv
                if 'weight' in name:
                    lr *= self.cfg.lr_mult_conv_weight
                    weight_decay *= 1
                elif 'bias' in name:
                    lr *= self.cfg.lr_mult_conv_bias
                    weight_decay *= 0
            elif '.1' in name or 'bn' in name:  # bn
                if 'weight' in name:
                    lr *= self.cfg.lr_mult_bn_weight
                    weight_decay *= 0
                elif 'bias' in name:
                    lr *= self.cfg.lr_mult_bn_bias
                    weight_decay *= 0
            elif 'linear' in name:
                if 'weight' in name:
                    lr *= self.cfg.lr_mult_linear_weight
                    weight_decay *= 1
                elif 'bias' in name:
                    lr *= self.cfg.lr_mult_linear_bias
                    weight_decay *= 0
            params.append({
                'params': param,
                'initial_lr': lr,
                'weight_decay': weight_decay})
        self.optimizer = optim.SGD(
            params, lr=self.cfg.initial_lr,
            weight_decay=self.cfg.weight_decay)
        gamma = (self.cfg.final_lr / self.cfg.initial_lr) ** \
            (1 / (self.cfg.epoch_num // self.cfg.step_size))
        self.scheduler = StepLR(self.optimizer, self.cfg.step_size, gamma=gamma)
        self.criterion = BCEWeightedLoss().to(self.device)

BCEWeightedLoss

BCEWeightedLoss
binary_cross_entropy_with_logits
    def __init__(self):
        super(BCEWeightedLoss, self).__init__()

torch.nn.functional.binary_cross_entropy_with_logits 测量目标和输出logits之间的二进制交叉熵的函数。
torch.nn.BCEWithLogitsLoss 将 Sigmoid 层和 BCELoss 组合在一个单独的类中。 这个版本在数值上比使用普通的 Sigmoid 后跟 BCELoss 更稳定,因为通过将操作组合成一个层,我们利用 log-sum-exp 技巧来实现数值稳定性。
损失可以描述为:

ℓ ( x , y ) = L = { l 1 , … , l N } ⊤ \ell(x, y) = L = \{l_1,\dots,l_N\}^\top (x,y)=L={l1,,lN}
l n = − w n [ t n ⋅ log ⁡ σ ( x n ) + ( 1 − t n ) ⋅ log ⁡ ( 1 − σ ( x n ) ) ] l_n = - w_n \left[ t_n \cdot \log \sigma(x_n)+ (1 - t_n) \cdot \log (1 - \sigma(x_n)) \right] ln=wn[tnlogσ(xn)+(1tn)log(1σ(xn))]

其中 N N N 是批量大小。如果reduceTrue,那么
ℓ ( x , y ) = { mean ⁡ ( L ) , if    size_average = True , sum ⁡ ( L ) , if    size_average = False . \ell(x, y) = \begin{cases} \operatorname{mean}(L), & \text{if}\; \text{size\_average} = \text{True},\\ \operatorname{sum}(L), & \text{if}\; \text{size\_average} = \text{False}. \end{cases} (x,y)={mean(L),sum(L),ifsize_average=True,ifsize_average=False.
这用于测量例如自动编码器中的重建误差。注意,目标 t i t_i ti 应该是0到1之间的数字。

通过在正例中添加权重,可以权衡召回和精确度。在这种情况下,损失可以描述为:
ℓ ( x , y ) = L = { l 1 , … , l N } ⊤ \ell(x, y) = L = \{l_1,\dots,l_N\}^\top (x,y)=L={l1,,lN}
l n = − w n [ p n t n ⋅ log ⁡ σ ( x n ) + ( 1 − t n ) ⋅ log ⁡ ( 1 − σ ( x n ) ) ] , l_n = - w_n \left[ p_n t_n \cdot \log \sigma(x_n)+ (1 - t_n) \cdot \log (1 - \sigma(x_n)) \right], ln=wn[pntnlogσ(xn)+(1tn)log(1σ(xn))],
其中 p n p_n pn n n n 类的正权重。 p n > 1 p_n>1 pn>1 增加召回率, p n < 1 pn<1 pn<1 增加精度。
或者,如果数据集包含单个类的100个正数和300个负数示例,则该类的 pos_weight 应该等于 300 100 = 3 \frac{300}{100}=3 100300=3。损失将表现为数据集包含数学: 3 × 100 = 300 3\times 100=300 3×100=300 个正例。

    def forward(self, input, target, weight=None):
        return F.binary_cross_entropy_with_logits(
            input, target, weight, size_average=True)

track

track
init
update

根据帧数初始化变量。

        frame_num = len(img_files)
        bndboxes = np.zeros((frame_num, 4))
        bndboxes[0, :] = init_rect
        speed_fps = np.zeros(frame_num)

进入循环,使用 Python Imaging Library 读取图像。

        for f, img_file in enumerate(img_files):
            image = Image.open(img_file)
            if image.mode == 'L':
                image = image.convert('RGB')

第1帧进行初始化,后续进行位置预测。

            start_time = time.time()
            if f == 0:
                self.init(image, init_rect)
            else:
                bndboxes[f, :] = self.update(image)
            elapsed_time = time.time() - start_time
            speed_fps[f] = elapsed_time

init

Created with Raphaël 2.2.0 init _deduce_network_params crop _extract_feature End

init 根据初试框初始化跟踪器。
获取目标的中心和宽高。
根据目标面积确定背景扩展大小。

        # initialize parameters
        self.center = init_rect[:2] + init_rect[2:] / 2
        self.target_sz = init_rect[2:]
        context = self.cfg.context * self.target_sz.sum()
        self.z_sz = np.sqrt((self.target_sz + context).prod())
        self.x_sz = self.z_sz * self.cfg.search_sz / self.cfg.exemplar_sz

_deduce_network_params 推断出得分图大小和网络的总步长。
计算上采样后的得分图大小。

        self.scale_factors = self.cfg.scale_step ** np.linspace(
            -(self.cfg.scale_num // 2),
            self.cfg.scale_num // 2, self.cfg.scale_num)
        self.score_sz, self.total_stride = self._deduce_network_params(
            self.cfg.exemplar_sz, self.cfg.search_sz)
        self.final_score_sz = self.cfg.response_up * (self.score_sz - 1) + 1

构造一个与得分图等大的 hanning 窗。

        hann_1d = np.expand_dims(np.hanning(
            self.final_score_sz), axis=0)
        self.penalty = np.transpose(hann_1d) * hann_1d
        self.penalty = self.penalty / self.penalty.sum()

截取模板图像并提取特征。

        # extract template features
        crop_z = crop(image, self.center, self.z_sz,
                      out_size=self.cfg.exemplar_sz)
        self.z = self._extract_feature(crop_z)

update

Created with Raphaël 2.2.0 update image _crop _extract_feature _calc_score x_sz, center, target_sz, z, z_sz End

更新多尺度测试的尺寸数组。

        # update scaled sizes
        scaled_exemplar = self.scale_factors * self.z_sz
        scaled_search_area = self.scale_factors * self.x_sz
        scaled_target = self.scale_factors[:, np.newaxis] * self.target_sz

_crop 截取图像块并进行填充和缩放。
_calc_score 计算xz的相关分数,加惩罚之后寻找最高得分。

        # locate target
        crops_x = self._crop(image, self.center, scaled_search_area,
                             out_size=self.cfg.search_sz)
        x = self._extract_feature(crops_x)
        score, scale_id = self._calc_score(self.z, x)

更新x_sztarget_sz
_locate_target 计算目标中心位置。

        self.x_sz = (1 - self.cfg.scale_lr) * self.x_sz + \
            self.cfg.scale_lr * scaled_search_area[scale_id]
        self.center = self._locate_target(self.center, score, self.final_score_sz,
                                          self.total_stride, self.cfg.search_sz,
                                          self.cfg.response_up, self.x_sz)
        self.target_sz = (1 - self.cfg.scale_lr) * self.target_sz + \
            self.cfg.scale_lr * scaled_target[scale_id]

如果设置了模板学习率则更新模板特征,否则仅更新z_sz 似乎应与论文中不同。这里z_sz 似乎应与z同步。

        # update the template
        # self.z_sz = (1 - self.cfg.scale_lr) * self.z_sz + \
        #     self.cfg.scale_lr * scaled_exemplar[scale_id]
        if self.cfg.z_lr > 0:
            crop_z = crop(image, self.center, self.z_sz,
                          out_size=self.cfg.exemplar_sz)
            new_z = self._extract_feature(crop_z)
            self.z = (1 - self.cfg.z_lr) * self.z + \
                self.cfg.z_lr * new_z
        self.z_sz = (1 - self.cfg.scale_lr) * self.z_sz + \
            self.cfg.scale_lr * scaled_exemplar[scale_id]

numpy.concatenate 沿现有轴加入一系列数组。
返回目标框的对角线坐标。

        return np.concatenate([
            self.center - self.target_sz / 2, self.target_sz])

_crop

_crop 根据sizes传入的尺寸从图像中裁剪出图像块。

numpy.tile 通过按照reps给出的次数重复A来构造数组。
如果reps的长度为d,则结果的维度为max(d, A.ndim)
如果A.ndim < d,则通过预先添加新轴将A提升为d维。 因此,形状(3,)的阵列被提升为(1, 3)用于2-D复制,或形状(1, 1, 3)用于3-D复制。如果这不是所需的行为,请在调用此函数之前手动将A提升为d维。

如果A.ndim> d,则通过前填1来将reps提升为A.ndim。因此,对于形状A(2, 3, 4, 5),(2, 2)的reps被视为(1, 1, 2, 2)。

注意:尽管可以使用 tile 进行广播,但强烈建议使用 numpy 的广播操作和功能。

如果sizes仅为一个值,则将其构造为行向量。

        sizes = np.array(sizes)
        if sizes.ndim == 1:
            sizes = np.tile(sizes, (2, 1)).T

求尺度数组中的最大值,依此截取图像块。

        max_size = np.max(sizes, axis=0)
        anchor_patch = crop(image, center, max_size, padding=padding)

计算每个尺寸相对于图像块的偏移,依此截取。

        patches = []
        for i, size in enumerate(sizes):
            if np.all(size == max_size):
                patch = anchor_patch
            else:
                offset = (max_size - size) / 2
                patch = anchor_patch.crop((
                    int(offset[0]),
                    int(offset[1]),
                    int(offset[0] + round(size[0])),
                    int(offset[1] + round(size[1]))))
            if out_size is not None:
                patch = patch.resize((out_size, out_size), Image.BILINEAR)
            patches.append(patch)

如果仅有一个尺寸,修改patches的类型。

        if len(sizes) == 1:
            patches = patches[0]

        return patches

_deduce_network_params

初始化zx,运行网络获得score_sz。实际运行的话成本过高,应通过计算获得。

        z = torch.zeros(1, 3, exemplar_sz, exemplar_sz).to(self.device)
        x = torch.zeros(1, 3, search_sz, search_sz).to(self.device)
        with torch.set_grad_enabled(False):
            self.model.eval()
            y = self.model(z, x)
        score_sz = y.size(-1)

计算网络总步长。

        total_stride = 1
        for m in self.model.modules():
            if isinstance(m, (nn.Conv2d, nn.MaxPool2d)):
                stride = m.stride[0] if isinstance(
                    m.stride, tuple) else m.stride
                total_stride *= stride

        return score_sz, total_stride

_extract_feature

torchvision.transforms.functional.to_tensor 将 PIL 图像或 numpy.ndarray 转换为张量。

        if isinstance(image, Image.Image):
            image = (255.0 * TF.to_tensor(image)).unsqueeze(0)
        elif isinstance(image, (list, tuple)):
            image = 255.0 * torch.stack([TF.to_tensor(c) for c in image])
        else:
            raise Exception('Incorrect input type: {}'.format(type(image)))

torch.autograd.set_grad_enabled 是上下文管理器,将梯度计算设置为打开或关闭。
eval() 设置模块为评估模式。这仅对某些模块有影响。有关其在训练/评估模式中的行为的详细信息,请参阅特定模块的文档,例如: Dropout,BatchNorm等。

        with torch.set_grad_enabled(False):
            self.branch.eval()
            return self.branch(image.to(self.device))

Pairwise

torch.utils.data.Dataset 表示数据集的抽象类。所有其他数据集都应该对其进行子类化。所有子类都应覆盖__len__,它提供数据集的大小,__getitem__,支持整数索引,范围从0到len(self)(不包含)。
pairs_per_video=25会影响对训练集的大小估计。

    def __init__(self, base_dataset, transform=None, pairs_per_video=25,
                 frame_range=100, causal=False, return_index=False,
                 rand_choice=True, subset='train', train_ratio=0.95):
        super(Pairwise, self).__init__()
        assert subset in ['train', 'val']
        self.base_dataset = base_dataset
        self.transform = transform
        self.pairs_per_video = pairs_per_video
        self.frame_range = frame_range
        self.causal = causal
        self.return_index = return_index
        self.rand_choice = rand_choice

base_dataset是 ImageNetVID 的返回值。

        n = len(self.base_dataset)
        split = int(n * train_ratio)
        split = np.clip(split, 10, n - 10)
        if subset == 'train':
            self.indices = np.arange(0, split, dtype=int)
            self.indices = np.tile(self.indices, pairs_per_video)
        elif subset == 'val':
            self.indices = np.arange(split, n, dtype=int)

__getitem__

检查索引超出。

        if index >= len(self):
            raise IndexError('list index out of range')

numpy.random.choice 从给定的1-D 阵列生成随机样本。

        if self.rand_choice:
            index = np.random.choice(self.indices)
        else:
            index = self.indices[index]
        img_files, anno = self.base_dataset[index]

_sample_pair 返回随即的xz的索引。

        rand_z, rand_x = self._sample_pair(len(img_files))
        img_z = Image.open(img_files[rand_z])
        img_x = Image.open(img_files[rand_x])
        if img_z.mode == 'L':
            img_z = img_z.convert('RGB')
            img_x = img_x.convert('RGB')
        bndbox_z = anno[rand_z, :]
        bndbox_x = anno[rand_x, :]

构造item元组。

        if self.return_index:
            item = (img_z, img_x, bndbox_z, bndbox_x, rand_z, rand_x)
        else:
            item = (img_z, img_x, bndbox_z, bndbox_x)

TransformSiamFC 对图像进行处理,主要包括切图和生成标签。

        if self.transform is not None:
            return self.transform(*item)
        else:
            return item

SiameseNet

SiameseNet 需同时输入zx,没有对z进行暂存。这样方便训练,而在测试时越过 SiameseNet 直接调用 branch

Created with Raphaël 2.2.0 SiameseNet x, z branch xcorr norm out End
    def __init__(self, branch, norm='bn'):
        super(SiameseNet, self).__init__()
        self.branch = branch
        self.norm = Adjust2d(norm=norm)
        self.xcorr = XCorr()

    def forward(self, z, x):
        assert z.size()[:2] == x.size()[:2]
        z = self.branch(z)
        x = self.branch(x)
        out = self.xcorr(z, x)
        out = self.norm(out, z, x)

        return out

XCorr

XCorr 模块批量计算xz的互相关。
torch.cat 在给定维度中连接给定的seq张量序列。所有张量必须具有相同的形状(在连接维度中除外)或为空。


    def __init__(self):
        super(XCorr, self).__init__()

    def forward(self, z, x):
        out = []
        for i in range(z.size(0)):
            out.append(F.conv2d(x[i, :].unsqueeze(0),
                                z[i, :].unsqueeze(0)))

        return torch.cat(out, dim=0)

Adjust2d

Adjust2d 模块在2D 平面上进行处理。
bnlinear需要初始化权重参数。

    def __init__(self, norm='bn'):
        super(Adjust2d, self).__init__()
        assert norm in [None, 'bn', 'cosine', 'euclidean', 'linear']
        self.norm = norm
        if norm == 'bn':
            self.bn = nn.BatchNorm2d(1)
        elif norm == 'linear':
            self.linear = nn.Conv2d(1, 1, 1, bias=True)
        self._initialize_weights()

cosineeuclidean为自行构造的函数。

    def forward(self, out, z=None, x=None):
        if self.norm == 'bn':
            out = self.bn(out)
        elif self.norm == 'linear':
            out = self.linear(out)
        elif self.norm == 'cosine':
            n, k = out.size(0), z.size(-1)
            norm_z = torch.sqrt(
                torch.pow(z, 2).view(n, -1).sum(1)).view(n, 1, 1, 1)
            norm_x = torch.sqrt(
                k * k * F.avg_pool2d(torch.pow(x, 2), k, 1).sum(1, keepdim=True))
            out = out / (norm_z * norm_x + 1e-32)
            out = (out + 1) / 2
        elif self.norm == 'euclidean':
            n, k = out.size(0), z.size(-1)
            sqr_z = torch.pow(z, 2).view(n, -1).sum(1).view(n, 1, 1, 1)
            sqr_x = k * k * \
                F.avg_pool2d(torch.pow(x, 2), k, 1).sum(1, keepdim=True)
            out = out + sqr_z + sqr_x
            out = out.clamp(min=1e-32).sqrt()
        elif self.norm == None:
            out = out

        return out
    def _initialize_weights(self):
        if self.norm == 'bn':
            self.bn.weight.data.fill_(1)
            self.bn.bias.data.zero_()
        elif self.norm == 'linear':
            self.linear.weight.data.fill_(1e-3)
            self.linear.bias.data.zero_()

TransformSiamFC

Created with Raphaël 2.2.0 __call__ _crop _create_labels _acquire_augment torchvision.transforms.functional.to_tensor torch.from_numpy End

load_siamfc_stats 从文件中加载xz的均值和方差。

    def __init__(self, stats_path=None, **kargs):
        self.parse_args(**kargs)
        self.stats = None
        if stats_path:
            self.stats = load_siamfc_stats(stats_path)

根据参数设置属性。

    def parse_args(self, **kargs):
        # default branch is AlexNetV1
        default_args = {
            'exemplar_sz': 127,
            'search_sz': 255,
            'score_sz': 17,
            'context': 0.5,
            'r_pos': 16,
            'r_neg': 0,
            'total_stride': 8,
            'ignore_label': -100,
            # augmentation parameters
            'aug_translate': True,
            'max_translate': 4,
            'aug_stretch': True,
            'max_stretch': 0.05,
            'aug_color': True}

        for key, val in default_args.items():
            if key in kargs:
                setattr(self, key, kargs[key])
            else:
                setattr(self, key, val)

_crop 截取图像块并进行填充和缩放。
_create_labels

    def __call__(self, img_z, img_x, bndbox_z, bndbox_x):
        crop_z = self._crop(img_z, bndbox_z, self.exemplar_sz)
        crop_x = self._crop(img_x, bndbox_x, self.search_sz)
        labels, weights = self._create_labels()

_acquire_augment

        crop_z = self._acquire_augment(
            crop_z, self.exemplar_sz, self.stats.rgb_variance_z)
        crop_x = self._acquire_augment(
            crop_x, self.search_sz, self.stats.rgb_variance_x)

F.to_tensor 将 PIL Image 转为 [0,1] 之间的值。

        crop_z = (255.0 * F.to_tensor(crop_z)).float()
        crop_x = (255.0 * F.to_tensor(crop_x)).float()
        labels = torch.from_numpy(labels).float()
        weights = torch.from_numpy(weights).float()

        return crop_z, crop_x, labels, weights

_crop

bndbox格式为[x1,y1,x2,y2],类型为 np.array。

        center = bndbox[:2] + bndbox[2:] / 2
        size = bndbox[2:]

背景为宽高和的一半。计算拓展面积,参照exemplar_sz计算图像块大小。

        context = self.context * size.sum()
        patch_sz = out_size / self.exemplar_sz * \
            np.sqrt((size + context).prod())

crop_pil 处理裁剪中的填充问题。

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

_create_labels

_create_logisticloss_labels 生成大小为score_sz的标签,半径r_pos范围内的标签为正,其余为负。

        labels = self._create_logisticloss_labels()

weights使正负损失均衡。

        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

_create_logisticloss_labels

r_pos为正样本半径,r_neg为负样本半径。

        label_sz = self.score_sz
        r_pos = self.r_pos / self.total_stride
        r_neg = self.r_neg / self.total_stride
        labels = np.zeros((label_sz, label_sz))

标签值为0-1。

        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.ignore_label
                else:
                    labels[r, c] = 0

        return labels

_acquire_augment

numpy.random.rand 给定形状的随机值。创建给定形状的数组,并使用来自[0,1]上的均匀分布的随机样本填充它。
如果进行拉伸延展,scale区间为[1-max_stretch, 1+max_stretch]。而且size<=patch_sz
这里patch_sz等于out_sizenp.minimum似乎有问题。
acquire_augment 处理与之相同。

        center = (out_size // 2, out_size // 2)
        patch_sz = np.asarray(patch.size)

        if self.aug_stretch:
            scale = (1 + self.max_stretch * (-1 + 2 * np.random.rand(2)))
            size = np.round(np.minimum(out_size * scale, patch_sz))
        else:
            size = patch_sz

如果进行平移增强,计算平移范围。
size<patch_sz,意味着进行拉伸的情况下平移才生效。

        if self.aug_translate:
            mx, my = np.minimum(
                self.max_translate, np.floor((patch_sz - size) / 2))
            rx = np.random.randint(-mx, mx) if mx > 0 else 0
            ry = np.random.randint(-my, my) if my > 0 else 0
            dx = center[0] - size[0] // 2 + rx
            dy = center[1] - size[1] // 2 + ry
        else:
            dx = center[0] - size[0] // 2
            dy = center[1] - size[1] // 2

        patch = patch.crop((
            int(dx), int(dy),
            int(dx + round(size[0])),
            int(dy + round(size[1]))))
        patch = patch.resize((out_size, out_size), Image.NEAREST)

numpy.random.randn 从“标准正态”分布中返回一个(或多个)样本。
如果使用颜色增强,减去一个随机颜色值。

        if self.aug_color:
            offset = np.reshape(np.dot(
                rgb_variance, np.random.randn(3)), (1, 1, 3))
            out = Image.fromarray(np.uint8(patch - offset))
        else:
            out = patch

        return out

load_siamfc_from_matconvnet

检查跟踪器使用的网络类型。两个模型层名不同。

    assert isinstance(model.branch, (AlexNetV1, AlexNetV2))
    if isinstance(model.branch, AlexNetV1):
        p_conv = 'conv'
        p_bn = 'bn'
        p_adjust = 'adjust_'
    elif isinstance(model.branch, AlexNetV2):
        p_conv = 'br_conv'
        p_bn = 'br_bn'
        p_adjust = 'fin_adjust_bn'

load_matconvnet 从文件中读取到参数名和值的列表。
conv1f 为卷积核的信息,conv1b 为卷积的 bias 信息。

    params_names_list, params_values_list = load_matconvnet(filename)
    params_values_list = [torch.from_numpy(p) for p in params_values_list]
    for l, p in enumerate(params_values_list):
        param_name = params_names_list[l]
        if 'conv' in param_name and param_name[-1] == 'f':
            p = p.permute(3, 2, 0, 1)
        p = torch.squeeze(p)
        params_values_list[l] = p

构造网络元组。

    net = (
        model.branch.conv1,
        model.branch.conv2,
        model.branch.conv3,
        model.branch.conv4,
        model.branch.conv5)

layer[0]为卷积。

    for l, layer in enumerate(net):
        layer[0].weight.data[:] = params_values_list[
            params_names_list.index('%s%df' % (p_conv, l + 1))]
        layer[0].bias.data[:] = params_values_list[
            params_names_list.index('%s%db' % (p_conv, l + 1))]

如果不是最后一个卷积层,加载 BN 的参数。

        if l < len(net) - 1:
            layer[1].weight.data[:] = params_values_list[
                params_names_list.index('%s%dm' % (p_bn, l + 1))]
            layer[1].bias.data[:] = params_values_list[
                params_names_list.index('%s%db' % (p_bn, l + 1))]

            bn_moments = params_values_list[
                params_names_list.index('%s%dx' % (p_bn, l + 1))]
            layer[1].running_mean[:] = bn_moments[:, 0]
            layer[1].running_var[:] = bn_moments[:, 1] ** 2

如果是最后一个卷积层,根据norm的类型加载相应参数。

        elif model.norm.norm == 'bn':
            model.norm.bn.weight.data[:] = params_values_list[
                params_names_list.index('%sm' % p_adjust)]
            model.norm.bn.bias.data[:] = params_values_list[
                params_names_list.index('%sb' % p_adjust)]

            bn_moments = params_values_list[
                params_names_list.index('%sx' % p_adjust)]
            model.norm.bn.running_mean[:] = bn_moments[0]
            model.norm.bn.running_var[:] = bn_moments[1] ** 2
        elif model.norm.norm == 'linear':
            model.norm.linear.weight.data[:] = params_values_list[
                params_names_list.index('%sf' % p_adjust)]
            model.norm.linear.bias.data[:] = params_values_list[
                params_names_list.index('%sb' % p_adjust)]

    return model

load_siamfc_stats

定义状态结构体。

    Stats = namedtuple('Stats', [
        'rgb_mean_z',
        'rgb_variance_z',
        'rgb_mean_x',
        'rgb_variance_x'])

读取 mat 文件。

 mat = h5py.File(stats_path, mode='r')

numpy.linalg.eig 计算正方形阵列的特征值和右特征向量。

    rgb_mean_z = mat['z']['rgbMean'][:]
    d, v = np.linalg.eig(mat['z']['rgbCovariance'][:])
    rgb_variance_z = 0.1 * np.dot(np.sqrt(np.diag(d)), v.T)

    rgb_mean_x = mat['x']['rgbMean'][:]
    d, v = np.linalg.eig(mat['z']['rgbCovariance'][:])
    rgb_variance_x = 0.1 * np.dot(np.sqrt(np.diag(d)), v.T)
    stats = Stats(
        rgb_mean_z,
        rgb_variance_z,
        rgb_mean_x,
        rgb_variance_x)

    return stats

ImageNetVID

    def __init__(self, root_dir, return_rect=False,
                 subset='train', rand_choice=True, download=False):
        r'''TODO: make the track_id sampling deterministic
        '''
        super(ImageNetVID, self).__init__()
        self.root_dir = root_dir
        self.return_rect = return_rect
        self.rand_choice = rand_choice
        if download:
            self._download(self.root_dir)

        if not self._check_integrity():
            raise Exception('Dataset not found or corrupted. ' +
                            'You can use download=True to download it.')

glob.glob 返回与pathname匹配的可能为空的路径名列表,路径名必须是包含路径规范的字符串。pathname可以是绝对的(如/usr/src/Python-1.5/Makefile)或 相对的(如../../Tools/*/*.gif),也可以包含 shell 样式的通配符。 结果中包含损坏的符号链接(如在 shell 中)。

        if subset == 'val':
            self.seq_dirs = sorted(glob.glob(os.path.join(
                self.root_dir, 'Data/VID/val/ILSVRC2015_val_*')))
            self.seq_names = [os.path.basename(s) for s in self.seq_dirs]
            self.anno_dirs = [os.path.join(
                self.root_dir, 'Annotations/VID/val', s) for s in self.seq_names]
        elif subset == 'train':
            self.seq_dirs = sorted(glob.glob(os.path.join(
                self.root_dir, 'Data/VID/train/ILSVRC*/ILSVRC*')))
            self.seq_names = [os.path.basename(s) for s in self.seq_dirs]
            self.anno_dirs = [os.path.join(
                self.root_dir, 'Annotations/VID/train',
                *s.split('/')[-2:]) for s in self.seq_dirs]
        else:
            raise Exception('Unknown subset.')

__getitem__

检查index是否在序列名列表中。

        if isinstance(index, six.string_types):
            if not index in self.seq_names:
                raise Exception('Sequence {} not found.'.format(index))
            index = self.seq_names.index(index)
        elif self.rand_choice:
            index = np.random.randint(len(self.seq_names))

读取 xml 文件中的’object’字段。

        anno_files = sorted(glob.glob(
            os.path.join(self.anno_dirs[index], '*.xml')))
        objects = [ET.ElementTree(file=f).findall('object')
                   for f in anno_files]
        # choose the track id randomly
        track_ids, counts = np.unique([obj.find(
            'trackid').text for group in objects for obj in group], return_counts=True)
        track_id = random.choice(track_ids[counts >= 2])
        anno = []
        for f, group in enumerate(objects):
            for obj in group:
                if not obj.find('trackid').text == track_id:
                    continue
                frames.append(f)
                anno.append([
                    int(obj.find('bndbox/xmin').text),
                    int(obj.find('bndbox/ymin').text),
                    int(obj.find('bndbox/xmax').text),
                    int(obj.find('bndbox/ymax').text)])
        img_files = [os.path.join(
            self.seq_dirs[index], '%06d.JPEG' % f) for f in frames]
        anno = np.array(anno)
        if self.return_rect:
            anno[:, 2:] = anno[:, 2:] - anno[:, :2] + 1

        return img_files, anno

OTB

定义数据集列表。

    __otb13_seqs = ['Basketball', 'Bolt', 'Boy', 'Car4', 'CarDark',
                    'CarScale', 'Coke', 'Couple', 'Crossing', 'David',
                    'David2', 'David3', 'Deer', 'Dog1', 'Doll', 'Dudek',
                    'FaceOcc1', 'FaceOcc2', 'Fish', 'FleetFace',
                    'Football', 'Football1', 'Freeman1', 'Freeman3',
                    'Freeman4', 'Girl', 'Ironman', 'Jogging', 'Jumping',
                    'Lemming', 'Liquor', 'Matrix', 'Mhyang', 'MotorRolling',
                    'MountainBike', 'Shaking', 'Singer1', 'Singer2',
                    'Skating1', 'Skiing', 'Soccer', 'Subway', 'Suv',
                    'Sylvester', 'Tiger1', 'Tiger2', 'Trellis', 'Walking',
                    'Walking2', 'Woman']

    __tb50_seqs = ['Basketball', 'Biker', 'Bird1', 'BlurBody', 'BlurCar2',
                   'BlurFace', 'BlurOwl', 'Bolt', 'Box', 'Car1', 'Car4',
                   'CarDark', 'CarScale', 'ClifBar', 'Couple', 'Crowds',
                   'David', 'Deer', 'Diving', 'DragonBaby', 'Dudek',
                   'Football', 'Freeman4', 'Girl', 'Human3', 'Human4',
                   'Human6', 'Human9', 'Ironman', 'Jump', 'Jumping',
                   'Liquor', 'Matrix', 'MotorRolling', 'Panda', 'RedTeam',
                   'Shaking', 'Singer2', 'Skating1', 'Skating2', 'Skiing',
                   'Soccer', 'Surfer', 'Sylvester', 'Tiger2', 'Trellis',
                   'Walking', 'Walking2', 'Woman']

    __tb100_seqs = ['Bird2', 'BlurCar1', 'BlurCar3', 'BlurCar4', 'Board',
                    'Bolt2', 'Boy', 'Car2', 'Car24', 'Coke', 'Coupon',
                    'Crossing', 'Dancer', 'Dancer2', 'David2', 'David3',
                    'Dog', 'Dog1', 'Doll', 'FaceOcc1', 'FaceOcc2', 'Fish',
                    'FleetFace', 'Football1', 'Freeman1', 'Freeman3',
                    'Girl2', 'Gym', 'Human2', 'Human5', 'Human7', 'Human8',
                    'Jogging', 'KiteSurf', 'Lemming', 'Man', 'Mhyang',
                    'MountainBike', 'Rubik', 'Singer1', 'Skater',
                    'Skater2', 'Subway', 'Suv', 'Tiger1', 'Toy', 'Trans',
                    'Twinnings', 'Vase']
    __otb15_seqs = __tb50_seqs + __tb100_seqs

    __version_dict = {
        2013: __otb13_seqs,
        2015: __otb15_seqs,
        'otb2013': __otb13_seqs,
        'otb2015': __otb15_seqs,
        'tb50': __tb50_seqs,
'tb100': __tb100_seqs}

__init__

检查版本。_check_integrity 获取路径下的子文件夹,检查是否都存在。
chain.from_iterable chain() 的替代构造函数。获取来自延迟计算的单个可迭代参数的链式输入。

    def __init__(self, root_dir, version=2015, download=True):
        super(OTB, self).__init__()
        assert version in self.__version_dict

        self.root_dir = root_dir
        self.version = version
        if download:
            self._download(root_dir, version)
        self._check_integrity(root_dir, version)

        valid_seqs = self.__version_dict[version]
        self.anno_files = list(chain.from_iterable(glob.glob(
            os.path.join(root_dir, s, 'groundtruth*.txt')) for s in valid_seqs))
        # remove empty annotation files
        # (e.g., groundtruth_rect.1.txt of Human4)
        self.anno_files = self._filter_files(self.anno_files)
        self.seq_dirs = [os.path.dirname(f) for f in self.anno_files]
        self.seq_names = [os.path.basename(d) for d in self.seq_dirs]
        # rename repeated sequence names
        # (e.g., Jogging and Skating2)
self.seq_names = self._rename_seqs(self.seq_names)

__getitem__

        if isinstance(index, six.string_types):
            if not index in self.seq_names:
                raise Exception('Sequence {} not found.'.format(index))
            index = self.seq_names.index(index)

        img_files = sorted(glob.glob(
            os.path.join(self.seq_dirs[index], 'img/*.jpg')))

        # special sequences
        # (visit http://cvlab.hanyang.ac.kr/tracker_benchmark/index.html for detail)
        seq_name = self.seq_names[index]
        if seq_name.lower() == 'david':
            img_files = img_files[300-1:770]
        elif seq_name.lower() == 'football1':
            img_files = img_files[:74]
        elif seq_name.lower() == 'freeman3':
            img_files = img_files[:460]
        elif seq_name.lower() == 'freeman4':
            img_files = img_files[:283]
        elif seq_name.lower() == 'diving':
            img_files = img_files[:215]

        # to deal with different delimeters
        with open(self.anno_files[index], 'r') as f:
            anno = np.loadtxt(io.StringIO(f.read().replace(',', ' ')))
        assert len(img_files) == len(anno)
        assert anno.shape[1] == 4

return img_files, anno

参考文献

  • huanglianghua/siamfc-pytorch
  • huanglianghua/open-vot
  • torrvision/siamfc-tf
  • zlj199502/siamfc_pytorch
  • PyTorch(六)——梯度反向传递(BackPropogate)的理解
  • PyTorch入门学习(二):Autogard之自动求梯度
  • Task 4 CNN back-propagation 反向传播算法
  • CFNet视频目标跟踪源码运行笔记(2)——training and then tracking
  • Loading weights from pretrained model with different module names
  • MatConvNet实现深度学习
  • pytorch如何使用多块gpu?
  • PyTorch使用tensorboardX
  • rafellerc/Pytorch-SiamFC

你可能感兴趣的:(VisualTracking,PyTorch,DeepLearning)