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)
预训练模型可直接在项目主页下载:
alexnet-owt-4df8aa71.pth 可通过迅雷下载。
数据下载参考:CFNet视频目标跟踪源码运行笔记(2)——training and then tracking。
VID 训练集拥有3862个片段,平均帧数为290。
SiamFC 训练主要涉及到 TrainerSiamFC、TrackerSiamFC 、SiameseNet、Pairwise 和 TransformSiamFC 几个对象。训练示例可参考 TestManagerSiamFC 。
TrackerSiamFC 组织模型训练,TrackerSiamFC 实现了跟踪器训练和推理的功能。SiameseNet 由 AlexNet 基础网络和 XCorr、Adjust2d 附加操作组成,Pairwise 对基本数据集进行封装,从中读取样本对。TransformSiamFC 对数据进行处理。
Trainer 调用 Tracker。
打开文件加载参数,并选择对应模型的参数。
接口设计有 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.branch
,net_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()
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)
对象初始化过程中调用 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()
为什么仅在第一次运行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_graph
为True
。对于标量张量或不需要梯度的张量,不能指定任何值。如果 None 值是可接受的,则此参数是可选的。retain_graph
(bool
,可选)—如果为 False
,将释放用于计算梯度的图。 请注意,几乎在所有情况下都不需要将此选项设置为True
,并且通常可以以更有效的方式解决此问题。默认为create_graph
的值。create_graph
(bool
,可选)—如果为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()
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)
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[tn⋅logσ(xn)+(1−tn)⋅log(1−σ(xn))]
其中 N N N 是批量大小。如果reduce
为True
,那么
ℓ ( 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[pntn⋅logσ(xn)+(1−tn)⋅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)
根据帧数初始化变量。
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 根据初试框初始化跟踪器。
获取目标的中心和宽高。
根据目标面积确定背景扩展大小。
# 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 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 计算x
和z
的相关分数,加惩罚之后寻找最高得分。
# 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_sz
和target_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 根据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
初始化z
和x
,运行网络获得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
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))
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 返回随即的x
和z
的索引。
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 需同时输入z
和x
,没有对z
进行暂存。这样方便训练,而在测试时越过 SiameseNet 直接调用 branch
。
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 模块批量计算x
和z
的互相关。
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 模块在2D 平面上进行处理。
bn
和linear
需要初始化权重参数。
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()
cosine
和euclidean
为自行构造的函数。
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_()
load_siamfc_stats 从文件中加载x
和z
的均值和方差。
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
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_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
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
numpy.random.rand 给定形状的随机值。创建给定形状的数组,并使用来自[0,1]上的均匀分布的随机样本填充它。
如果进行拉伸延展,scale
区间为[1-max_stretch, 1+max_stretch]。而且size
<=patch_sz
。
这里patch_sz
等于out_size
,np.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
检查跟踪器使用的网络类型。两个模型层名不同。
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
定义状态结构体。
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
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
定义数据集列表。
__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