siamRPN论文:High Performance Visual Tracking with Siamese Region Proposal Network
gitHub代码:https://github.com/HonglinChu/SiamTrackers/tree/master/SiamRPN/SiamRPN
论文模型架构:
在此文章中将以代码+注释的形式详解推理过程,即test.py中的代码。
后续有空将会详解训练过程即train.py的代码。
|—1.搭建模型
|—2.跟踪过程:
|——2.1 第一帧:
|——截取exemplar_frame(Img_z)
|——获得初始bbox(需要注意这里x,y值的含义),target_sz ,score_filters_z, reg_filters_z(后面两个用于后续互操作)
|——2.2 非第一帧:
|——截取detection_frame(Img_x)
|——通过Backbone得到score_filters_x, reg_filters_x
|——互相关操纵后得到pred_score, pred_regression(dx,dy,dw,dh)(这里很重要)
|——进行后处理(最重要)
|———分类后处理
|————softmax,得到”前景”概率
|———框偏移后处理
|————将bbox与pred_regression进行逆向计算,得到预测框(非最终的框,需要注意这里x,y值的含义)
|——获得惩罚因子(对应论文公式) 通过惩罚因子修正 ”前景”概率,修正预测框
|——预测框第二次修正,得到最终结果框
|——更新修改下一帧的中心偏差和图片大小偏差
|——返回最终结果框用于画图
|—画出box
|—./bin/test.py
|——SiamRPNTracker类
|———初始超参
|———创建SiamRPNNet()
|————BackBone
|————4个卷积块(conv)
|———初始化创建所有区域建议框
|———初始化创建window
|——ExperimentOTB类
|———OTB类(继承Object)
|——run函数(推理)
|——eport函数(结果)
如果读过前面本人写的Siamfc相关文章:
SiamFC代码讲解,训练过程讲解
SiamFC代码讲解,推理测试讲解
那么阅读本篇文章会比较轻松。(因为希望你了解本人的书写流程)
Siamrpn与Siamfc相比,就是多了一个rpn网络,导致会多一个相关后处理的操作。后处理的相关操作比较复杂,希望本文能讲解清楚。
如果你不了解什么是rpn网络,那么本人推荐先看此篇文章:
(博主:懒人元)RPN 解析
./bin/my_test.py
首先看到Line27,进入到
./siamrpn/tracker.py中
代码详解(注释)
过程简述:
- 超参初始化
- 创建siamfc架构;
- 模型加载;
- 初始transformer
- 初始所有候选区域框
- 初始proposed_box_num个 window(由hanning窗生成,用于最后加权求和)
回到./bin/my_test.py
看到Line33 通过ExperimentOTB类获取数据集,与siamFC类似:
siamFC中相关代码详解(注释)
这里不做赘述
回到./bin/my_test.py
看到Line56 (关键代码)
进入到./experiments/otb.py中run函数
代码详解(注释)
过程简述:
通过for循环遍历dataset
并执行如下操作:
- 创建输出结果.txt(如果不存在的话)
- 进行推理 (最重要)
- 结果保存
当前函数中看到tracker.track(experiments/otb.py—Line55)
这是最关键的一步,点进去,进入到./siamrpn/racker.py下的track函数
ps:需要注意不要进入到siamFC.py下track函数
代码详解(注释)
过程简述:
- 获得第一个框(目标框的参数)
- 如果是第一帧,进行推理初始化(init函数)
- 如果是非第一帧,进行正常推理(update函数)
- 结果框的显示
代码详解(注释)
过程简述:
- 所有初始box 的坐标转换(left,up,w,h)–>(cx,cy,w,h)
- 初始化裁剪图片的中心点坐标 (self.pos)
- 初始化大小(后续会随着过程改变),原始图片大小(self.target_sz;;self.origin_target_sz)
- 初始化exemplar_img
- 初始化score_filters_z, reg_filters_z(self.model.track_init函数)并固定,用于后续互相关
如果非第一帧,跳转进入到update函数(siamfc.py --Line301):
代码详解(注释)
过程简述:
- 获得detection_img
- 经过模型,得到pred_score, pred_regression(dx,dy,dw,dh)
- 后处理
3.1 分类后处理
3.2 框偏移后处理- 获取惩罚因子并更新前景概率
- 获得最优前景概率下的id(index)
- 再次修正框值,得到最终值
- 修改下一帧的中心偏差和图片大小偏差
- 返回Box值,用于后续可显示化
————————————————————————————————————
class SiamRPNTracker:
def __init__(self, model_path,cfg=None,is_deterministic=False):
self.name='SiamRPN'
if cfg:
config.update(cfg)
self.model = SiamRPNNet()
self.is_deterministic = is_deterministic
checkpoint = torch.load(model_path)
if 'model' in checkpoint.keys():
self.model.load_state_dict(torch.load(model_path)['model'])#
else:
self.model.load_state_dict(torch.load(model_path))
self.model = self.model.cuda()
self.model.eval()
self.transforms = transforms.Compose([ToTensor()])
self.anchors = generate_anchors(config.total_stride, ##8
config.anchor_base_size, ##8
config.anchor_scales, ##np.array([8,])
config.anchor_ratios, ##np.array([0.33, 0.5, 1, 2, 3])
config.valid_scope) # ##19
self.window = np.tile(np.outer(np.hanning(config.score_size), np.hanning(config.score_size))[None, :],
[config.anchor_num, 1, 1]).flatten()
##生成5个window, 由汉宁窗生成,汉宁窗是使用加权余弦形成的。
##self.window的值: 越靠近中心,值越大
##用于最后加权求和
"""
np.outer() 的doc说明
Compute the outer product of two vectors.
Given two vectors, a = [a0, a1, ..., aM] and b = [b0, b1, ..., bN], the outer product [1] is:
[[a0*b0 a0*b1 ... a0*bN ]
[a1*b0 .
[ ... .
[aM*b0 aM*bN ]]
"""
对应代码补充:
config.update(参数初始化)
SiamRPNNet类(详细模型搭建代码)
generate_anchor函数
def run(self, tracker, visualize=False):
print('Running tracker %s on %s...' % (
tracker.name, type(self.dataset).__name__))
# loop over the complete dataset
for s, (img_files, anno) in enumerate(self.dataset):
## img_files是list ,保存的是dataset中某一子文件夹中的所有图片路径
##anno 是list ,保存的是dataset中某一子文件夹中的groundtruth路径
seq_name = self.dataset.seq_names[s] ##取第s批数据
print('--Sequence %d/%d: %s' % (s + 1, len(self.dataset), seq_name))
# skip if results exist
record_file = os.path.join(
self.result_dir, tracker.name, '%s.txt' % seq_name)
if os.path.exists(record_file):
print(' Found results, skipping', seq_name)
continue
# tracking loop
boxes, times = tracker.track( ##最重要部分
img_files, anno[0, :], visualize=visualize)
assert len(boxes) == len(anno)
"""
img_files: 保存的是一个文件夹下所有图片的路径
anno[0, :]: 第一张图片的annotation值;目标框的annnotation值(因为siamfc始终实以第一帧图片作为目标框)
visualize: 结果的可视化
"""
# record results
self._record(record_file, boxes, times)
def track(self, img_files, box, visualize=False):
##box: 第一帧,在siamfc中是要跟踪的物体,且后续不会发生变化
frame_num = len(img_files) ##总帧数
boxes = np.zeros((frame_num, 4)) ##准备预测所有框的参数
boxes[0] = box ##获得第一个框(目标框的参数)
times = np.zeros(frame_num) ## 时间,用于后续计算fps
for f, img_file in enumerate(img_files):
img = ops.read_image(img_file)
##img_file 这里传入的是一个img路径
begin = time.time()
if f == 0: ##第一帧
self.init(img, box)
##初始化了很多参数,并固定feature_z 并作为后续的卷积核
else:##不过不是第一帧
boxes[f, :] = self.update(img) ##重要函数,实际推理过程
times[f] = time.time() - begin##耗时
if visualize:
ops.show_image(img, boxes[f, :])
return boxes, times
def init(self, frame, bbox):
""" initialize siamrpn tracker
Args:
frame: an RGB image
bbox: one-based bounding box [left_x, up_y, width, height]
"""
self.bbox = np.array([bbox[0]-1 + (bbox[2]-1) / 2 , bbox[1]-1 + (bbox[3]-1) / 2 , bbox[2], bbox[3]]) #cx,cy,w,h
self.pos = np.array([bbox[0]-1 + (bbox[2]-1) / 2 , bbox[1]-1 + (bbox[3]-1) / 2]) # center x, center y, zero based
## center_pos
##self.pos=self.bbox[:2,...]
self.target_sz = np.array([bbox[2], bbox[3]]) # width, height ## ps,后续会更新
self.origin_target_sz = np.array([bbox[2], bbox[3]])#原始w,h 后续不会更新
# get exemplar img
self.img_mean = np.mean(frame, axis=(0, 1)) ###边界填充像素值
##
exemplar_img, scale_z, _ = get_exemplar_image(frame, self.bbox,config.exemplar_size, config.context_amount, self.img_mean)
## scale_z ==127 / crop_size 未用到
# get exemplar feature
exemplar_img = self.transforms(exemplar_img)[None, :, :, :]#在测试阶段,转换成tensor类型就可以了
self.model.track_init(exemplar_img.cuda()) ##初始score_filters_z, reg_filters_z
对应代码补充:
self.model.track_init()
def update(self, frame):
"""track object based on the previous frame
Args:
frame: an RGB image
Returns:
bbox: tuple of 1-based bounding box(xmin, ymin, xmax, ymax)
"""
instance_img_np, _, _, scale_x = get_instance_image(frame, self.bbox, config.exemplar_size,
config.instance_size,
config.context_amount, self.img_mean)
##scale_x == 271/detection_frame_crop_size 值靠近1
instance_img = self.transforms(instance_img_np)[None, :, :, :]
##model_inference_
pred_score, pred_regression = self.model.track(instance_img.cuda())##互操作 ;;后续会有补充代码说明
# pred_score=1,2x5,19,19 ; pre_regression=1,4x5,19,19
## 前后背景概率;;;;;;;预测框的值:偏移中心点坐标和预测wh
######后处理
### 1. 分类后处理
pred_conf = pred_score.reshape(-1, 2, config.anchor_num * config.score_size * config.score_size).permute(0,2,1)
##1,2*5,19,19---->1,2,5*19*19--->1,5*19*19,2
# 前后背景所有框个数: 5*19*19;;;分类问题
score_pred = F.softmax(pred_conf, dim=2)[0, :, 1].cpu().detach().numpy() # 计算预测分类得分
##score_pred.shape==(1, 2*5*19*19,1) ↑ 只取"物体"的概率 ,这里,默认第一个值是”背景"的概率,第二个值是”物体“的概率
### 2.框偏移后处理
pred_offset = pred_regression.reshape(-1, 4,config.anchor_num * config.score_size * config.score_size).permute(0,2,1)
##1,4*5,19,19---->1,4,5*19*19--->1,5*19*19,4
# 回归问题,,true_box==proposed_box+offset proposed_box是初始固定的,这里回归问题是offset,也就是模型要学习的东西
delta = pred_offset[0].cpu().detach().numpy()
#使用detach()函数来切断一些分支的反向传播;返回一个新的Variable,从当前计算图中分离下来的,但是仍指向原变量的存放位置,不同之处只是requires_grad为false,得到的这个Variable永远不需要计算其梯度,不具有grad。
#即使之后重新将它的requires_grad置为true,它也不会具有梯度grad #这样我们就会继续使用这个新的Variable进行计算,后面当我们进行反向传播时,到该调用detach()的Variable就会停止,不能再继续向前进行传播
box_pred = box_transform_inv(self.anchors, delta) #通过 anchors 和 offset 来获得预测的box ;;;后续会有补充代码说明
###self.anchors: 初始的anchors == x,y,w,h 其中x,y 是相对于图中心的坐标偏移(经过dx,dy计算后);;w,h为候选预测框的大小(经过dw,dh计算后)
###e.g. self.anchors[0]==-72,-72 ,104,32
##因此,这里计算的box_pred x,y也是相对于图中心的坐标偏移
"""
下面如不好理解可以跳过
所以true_box_x = F( center_x + box_pred[0] ) ##其中F 代表某种变换,可能是惩罚因子 ;;其中center_x ==上一时刻的box[0]
true_box_y = F( center_y + box_pred[1] )
true_box_w = G( w + box_pred[2] ) ##其中G 代表某种变换,可能与F相同也可能不同;;功能可能是加权求和
true_box_h = G( y + box_pred[3] )
"""
def change(r):
return np.maximum(r, 1. / r) #np.maximum(x,y): x 和 y 逐位进行比较选择最大值
def sz(w, h):
pad = (w + h) * 0.5 ##(很像模板图像扩充))
sz2 = (w + pad) * (h + pad)
return np.sqrt(sz2)
def sz_wh(wh):
pad = (wh[0] + wh[1]) * 0.5
sz2 = (wh[0] + pad) * (wh[1] + pad)## (模板图像扩充)
return np.sqrt(sz2)
####获得惩罚因子,对应论文中 4.3 公式(13)
s_c = change(sz(box_pred[:, 2], box_pred[:, 3]) / (sz_wh(self.target_sz * scale_x))) # scale penalty
"""
sz_result=sz(box_pred[:, 2], box_pred[:, 3])
sz_wh_result=sz_wh(self.target_sz * scale_x) ##target_sz.shape==(1,2) scale_x近似 127/257
change(sz_result/sz_wh_result)
只有当box_pred[:,2:]===self.target_sz的时候为1 ,其他情况下肯定大于1
"""
r_c = change((self.target_sz[0] / self.target_sz[1]) / (box_pred[:, 2] / box_pred[:, 3])) # ratio penalty
##获取 宽高比率的最大值
penalty = np.exp(-(r_c * s_c - 1.) * config.penalty_k)#尺度惩罚和比例惩罚 获得最终惩罚因子
###penalty_k =0.22
####获得惩罚因子,对应论文中 4.3 公式(13)
pscore = penalty * score_pred#对每一个anchors的分类预测×惩罚因子
pscore = pscore * (1 - config.window_influence) + self.window * config.window_influence #再乘以余弦窗,加重图片中间是物体的概率,削减图片边缘是物体的概率
"""
pscore: 两个权重求和
self.window汉宁窗 大小19*19,越靠近中心点值越大
window_influence = 0.40
增加中心点附近的的像素值为物体的概率
"""
##选择最优框
best_pscore_id = np.argmax(pscore) #得到 最大的得分对应的下标
target = box_pred[best_pscore_id, :] / scale_x #最优物体概率对应id的box进行scale_x(==271/detection_frame_crop_size)缩放
lr = penalty[best_pscore_id] * score_pred[best_pscore_id] * config.lr_box#预测框的学习率
##CONFIG.LR_BOX==0.30 ???推理预测的时候为什么还要涉及到lr???(只影响后续更新 box_w,box_h)
###原因是------用于后续加权修正W,H
####如下步骤是获得真正的box (true_box)
##首先要知道,这里的target的值===x,y,w,h 其中x,y 是相对于图中心的坐标偏移(经过dx,dy计算后);;w,h为候选预测框的大小(经过dw,dh计算后)
res_x = np.clip(target[0] + self.pos[0], 0, frame.shape[1])#w=frame.shape[1]
##等价于np.clip(x+offset_x , 0 , W) 防止cx 越界
##np.clip(a,a_min,a_max)函数 该函数的作用是将数组a中的所有数限定到范围a_min和a_max中。
"""
res: result 最终结果
target== cx,cy,w,h
self.pos== center_x,center_y
"""
res_y = np.clip(target[1] + self.pos[1], 0, frame.shape[0])#h=frame.shape[0]
###宽高加权修正, lr_box的作用
res_w = np.clip(self.target_sz[0] * (1 - lr) + target[2] * lr, config.min_scale * self.origin_target_sz[0],
config.max_scale * self.origin_target_sz[0])
"""
config.min_scale ==0.1 config.max_scale==10
target_sz初始值为 origin_target_sz,后续会进行更新
一般情况下: res_w= self.target_sz[0] * (1 - lr) + target[2] * lr ;;权重求和
"""
res_h = np.clip(self.target_sz[1] * (1 - lr) + target[3] * lr, config.min_scale * self.origin_target_sz[1],
config.max_scale * self.origin_target_sz[1])
###更新信息
self.pos = np.array([res_x, res_y]) #更新之后的坐标
self.target_sz = np.array([res_w, res_h]) #更新框大小
bbox = np.array([res_x, res_y, res_w, res_h])##得到最终预测的box值
self.bbox = ( #cx, cy, w, h
np.clip(bbox[0], 0, frame.shape[1]).astype(np.float64), ##np.clip 在这里是防止越界
np.clip(bbox[1], 0, frame.shape[0]).astype(np.float64),
np.clip(bbox[2], 10, frame.shape[1]).astype(np.float64),
np.clip(bbox[3], 10, frame.shape[0]).astype(np.float64))
#这个用来画图使用
bbox=np.array([# left,up, W, H
self.pos[0] + 1 - (self.target_sz[0]-1) / 2,
self.pos[1] + 1 - (self.target_sz[1]-1) / 2,
self.target_sz[0], self.target_sz[1]])
#return self.bbox, score_pred[best_pscore_id]
"""
可以看到没有修改 feature_z 和 score_filters_z, reg_filters_z
和siamfc一致,只选取第一帧作为模板图(exemplar_img)
"""
return bbox
对应代码补充:
self.model.track
box_transform_inv函数
————————————————————————————————————
class Config:
# dataset related
exemplar_size = 127 # exemplar frame_size
instance_size = 271 #默认271 # detection frame_size;;与论文不同,论文大小是255
context_amount = 0.5 # context amount 图像模板扩充大小
sample_type = 'uniform'
#--------
train_epoch_size = 1000 #
val_epoch_size = 100 #
out_feature = 19 #对应论文,互相关后的尺寸大小;;与论文不同,这里是19,因为detection_frame_size的不同
#max_inter = 80#
eps = 0.01 #
#--------
# training related
#...
# training related
total_stride = 8 ## training related
valid_scope = int((instance_size - exemplar_size) / total_stride + 1)
## (271-127)/8+1 ==18+1==19 等于最后互相关后的尺寸大小
anchor_scales = np.array([8,]) ##Backbone会进行8倍下采样
anchor_ratios = np.array([0.33, 0.5, 1, 2, 3]) ##5个anchor的 尺度大小
anchor_num = len(anchor_scales) * len(anchor_ratios)##==5 论文中对应K的数字
anchor_base_size = 8 ##Backbone会进行8倍下采样
pos_threshold = 0.6
neg_threshold = 0.3
num_pos = 16
num_neg = 48
lamb = 5 # 原始 cls:res = 1:5
save_interval = 1
show_interval = 100#100
show_topK = 3
pretrained_model = './models/alexnet.pth'
# tracking related
gray_ratio = 0.25
blur_ratio = 0.15
score_size = int((instance_size - exemplar_size) / total_stride + 1) ##### (271-127)/8+1 ==18+1==19 等于最后互相关后的尺寸大小
penalty_k =0.22 #0.22 #0.22 #0.16
window_influence = 0.40#0.40 #0.40 #0.40 会创建汉宁窗相关窗口,进行后续的加权求和
lr_box =0.30 #0.30 #0.30
min_scale = 0.1 ##预测框的最小情况比率
max_scale = 10 ##预测框的最大情况比率
def update(self, cfg): ##更新超参数
for k, v in cfg.items():
setattr(self, k, v)
self.score_size = (self.instance_size - self.exemplar_size) //self.total_stride + 1 #
#self.valid_scope = int((self.instance_size - self.exemplar_size) / self.total_stride / 2)#anchor的范围
self.valid_scope= self.score_size
## (271-127)/8+1 ==18+1==19
config = Config()
补充说明:
Config类的超参数不用刻意去记住,等看到代码中存在config.XXX的时候,再去查看。这能进一步去理解代码的含义。
class SiamRPNNet(nn.Module):
def __init__(self, ):
super(SiamRPNNet, self).__init__() ##exemplar尺寸变化情况 detection尺寸变化情况
## conv_O=(I-K+2P)/S+1 maxpool_O=(I-k)/s+1 exemplar_size = 127 instance_size = 271
self.featureExtract = nn.Sequential( ## 127 271
nn.Conv2d(3, 96, 11, stride=2), # stride=2 (127-11)/2+1==59 (271-11)/2+1==131
nn.BatchNorm2d(96), #
nn.MaxPool2d(3, stride=2), # stride=2 默认valid模式, ##(59-3)/2+1==29 (131-3)/2 +1==65
nn.ReLU(inplace=True),
nn.Conv2d(96, 256, 5), ##25 61
nn.BatchNorm2d(256),
nn.MaxPool2d(3, stride=2), # stride=2 ##12 ##30
nn.ReLU(inplace=True),
nn.Conv2d(256, 384, 3), ##10 ##28
nn.BatchNorm2d(384), #
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, 3), ##8 ##26
nn.BatchNorm2d(384),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, 3), ##6 ## 24
nn.BatchNorm2d(256), #
)
self.anchor_num = config.anchor_num #每一个位置有5个anchor
self.input_size = config.instance_size ##config.instance_size==271
self.score_displacement = int((self.input_size - config.exemplar_size) / config.total_stride) ##(271-127)/8 ==18
self.conv_cls1 = nn.Conv2d(256, 256 * 2 * self.anchor_num, kernel_size=3, stride=1, padding=0)
self.conv_r1 = nn.Conv2d(256, 256 * 4 * self.anchor_num, kernel_size=3, stride=1, padding=0)
self.conv_cls2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
self.conv_r2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
self.regress_adjust = nn.Conv2d(4 * self.anchor_num, 4 * self.anchor_num, 1)# ## 1*卷积 用于线性回归后的一个1*1conv操作
def forward(self, template, detection):
N = template.size(0) # N=32 batch==32
template_feature = self.featureExtract(template)#[N,256,6,6] ###exemplar_frame--->exemplar_feature
detection_feature = self.featureExtract(detection)#[N,256,24,24] ##detection_frame--->detection_feature
##self.anchor_num==5
kernel_score = self.conv_cls1(template_feature).view(N, 2 * self.anchor_num, 256, 4, 4) #N,2*5,256,4,4
kernel_regression = self.conv_r1(template_feature).view(N, 4 * self.anchor_num, 256, 4, 4) #N,4*5,256,4,4
conv_score = self.conv_cls2(detection_feature) #N,256,22,22#对齐操作
conv_regression = self.conv_r2(detection_feature)#N,256,22,22
##组卷积 类别分支 互相关操作
conv_scores = conv_score.reshape(1, -1, self.score_displacement + 4, self.score_displacement + 4)#1,Nx256,22,22
score_filters = kernel_score.reshape(-1, 256, 4, 4) # Nx10,256,4,4
pred_score = F.conv2d(conv_scores, score_filters, groups=N).reshape(N, 10, self.score_displacement + 1,self.score_displacement + 1)##groups=N result.shape==1,32*10,19,19
##组卷积 线性回归分支 互相关操作
conv_reg = conv_regression.reshape(1, -1, self.score_displacement + 4, self.score_displacement + 4)##N,256,22,22-->1,N*256,22,22
reg_filters = kernel_regression.reshape(-1, 256, 4, 4)##N,4*5,256,4,4--->N*20,256,4,4
pred_regression = self.regress_adjust(F.conv2d(conv_reg, reg_filters, groups=N).reshape(N, 20, self.score_displacement + 1,self.score_displacement + 1)) ### result.shape==1,32*20,19,19
return pred_score, pred_regression
##补充说明:
self.conv_cls1 = nn.Conv2d(256, 256 * 2 * self.anchor_num, kernel_size=3, stride=1, padding=0)
self.conv_r1 = nn.Conv2d(256, 256 * 4 * self.anchor_num, kernel_size=3, stride=1, padding=0)
self.conv_cls2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
self.conv_r2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
detecition_cls_feature.shape=N,256,22,22 ,则需要reshape为 -1, N*C,22,22 (C=256) [被卷积的(w,h比较大的)的通道为B*C]
exemplar_cls_feature.shape=N,2*5,256,4,4, 则需要reshape为 -1, C,4,4 (C=256) [卷积核(小的wh)的通道数为C]
def generate_anchors(total_stride, base_size, scales, ratios, score_size):
"""
##total_stride=8
##base_size=8
##scales=numpy.array([8.])
##rations=numpy.array([0.33,0.5,1,2,3])
##score_size=19
"""
anchor_num = len(ratios) * len(scales) ##默认为5
anchor = np.zeros((anchor_num, 4), dtype=np.float32) ##
size = base_size * base_size ##64, 19*19下一个像素点相对于原图的大小;也即标准锚框大小
count = 0
for ratio in ratios:
# ws = int(np.sqrt(size * 1.0 / ratio))
ws = int(np.sqrt(size / ratio)) ##根据ration 生成不同的毛框大小
hs = int(ws * ratio)
for scale in scales:
wws = ws * scale
hhs = hs * scale
anchor[count, 0] = 0
anchor[count, 1] = 0
anchor[count, 2] = wws ##初始w
anchor[count, 3] = hhs ##初始h
count += 1
anchor= np.tile(anchor, score_size * score_size)# [5,4]----->[5,4 *score_size*score_size]==[5,4*19*19]
#tile铺平复制
anchor = anchor.reshape((-1, 4))#[5,4*19*19]--->[5*19*19,4] #
##总共有 5*19*19个框
ori = - (score_size // 2) * total_stride ##-19//2 *8 ===-9*8=-72
# the left displacement
xx, yy = np.meshgrid([ori + total_stride * dx for dx in range(score_size)],
[ori + total_stride * dy for dy in range(score_size)])
##获得原始尺寸大小下 相对于中心点的 x,y坐标
xx, yy = np.tile(xx.flatten(), (anchor_num, 1)).flatten(), np.tile(yy.flatten(), (anchor_num, 1)).flatten()
## #tile铺平复制
"""
xx[:20]
>>>
[-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64 72 -72]
yy[:20]
>>>
[-72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -64]
"""
anchor[:, 0], anchor[:, 1] = xx.astype(np.float32), yy.astype(np.float32)
return anchor
##补充说明:
生成的anchor,数据形如:
anchor[0]== -72,-72,104,104。
一定要注意!
一定要注意!
一定要注意!
这里的前面两个值代表的是:原始尺寸大小下 相对于中心点的 x,y坐标
例如这里的anchor[0],应该是第一个像素点的第一个anchor框,即图片左上角的anchor框。
-72,-72则是相对于图像中间的偏移。这里一定要好好理解,后面后处理的时候会用到。
涉及到np.tile函数和np.meshgrid函数,读者可以上网自行查阅,本文只做小demo演示
np.tile函数:
功能,铺平复制
demo:
import numpy as np
a=np.arange(0,20)
a=a.reshape(5,4)
print(a)
print(a.shape)
a=np.tile(a,17*17)
print(a[0,:20])
print(a[1,:20])
print(a.shape)
----------------------
>>>
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]
[16 17 18 19]]
>>>
(5, 4)
>>>
[0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3]
>>>
[4 5 6 7 4 5 6 7 4 5 6 7 4 5 6 7 4 5 6 7]
>>>
(5, 1156)
np.meshgrid函数:
功能:生成网格数据
demo:
import numpy as np
x = np.linspace(1, 4, 4)
y = np.linspace(6, 7, 2)
X, Y = np.meshgrid(x, y)
print(x)
print(y)
print(X)
print(Y)
----------------------
>>>
[1. 2. 3. 4.]
>>>
[6. 7.]
>>>
[[1. 2. 3. 4.]
[1. 2. 3. 4.]]
>>>
[[6. 6. 6. 6.]
[7. 7. 7. 7.]]
def track_init(self, template):
##初始 score_filters_z, reg_filters_z
N = template.size(0) ##N==1
template_feature = self.featureExtract(template)# 输出 [1, 256, 6, 6]
# kernel_score=1,2x5,256,4,4 kernel_regression=1,4x5, 256,4,4
kernel_score = self.conv_cls1(template_feature).view(N, 2 * self.anchor_num, 256, 4, 4) ##后续 分类回归的2k个box
kernel_regression = self.conv_r1(template_feature).view(N, 4 * self.anchor_num, 256, 4, 4)###regression
self.score_filters = kernel_score.reshape(-1, 256, 4, 4) # 2x5, 256, 4, 4
self.reg_filters = kernel_regression.reshape(-1, 256, 4, 4)# 4x5, 256, 4, 4
def track(self, detection):
N = detection.size(0) ##batch推理的时候,默认N==1
detection_feature = self.featureExtract(detection)##推理:feature_x.shape 1,256,24,24
conv_score = self.conv_cls2(detection_feature) ##不清楚可以看network.py下的__init__函数 1,256,22,22
conv_regression = self.conv_r2(detection_feature)##不清楚可以看network.py下的__init__函数 1,256,22,22
conv_scores = conv_score.reshape(1, -1, self.score_displacement + 4, self.score_displacement + 4)
#self.score_displacement==18, conv_score的shape变化: 1,256,22,22---> 1,256,22,22
conv_reg = conv_regression.reshape(1, -1, self.score_displacement + 4, self.score_displacement + 4)
pred_score = F.conv2d(conv_scores, self.score_filters, groups=N).reshape(N, 10, self.score_displacement + 1,self.score_displacement + 1)##组卷积
pred_regression = self.regress_adjust(F.conv2d(conv_reg, self.reg_filters, groups=N).reshape(N, 20, self.score_displacement + 1,self.score_displacement + 1))
return pred_score, pred_regression ## shape : 1,10,19,19 ||| 1,20,19,19
功能:通过给定anchor框和offset(dx,dy,dw,dh)计算预测框的值
对应论文公式:
def box_transform_inv(anchors, offset):
anchor_xctr = anchors[:, :1] #cx
anchor_yctr = anchors[:, 1:2]#cy
anchor_w = anchors[:, 2:3] #w
anchor_h = anchors[:, 3:] #h
offset_x, offset_y, offset_w, offset_h = offset[:, :1], offset[:, 1:2], offset[:, 2:3], offset[:, 3:],
box_cx = anchor_xctr+ anchor_w * offset_x
box_cy = anchor_yctr+ anchor_h * offset_y
box_w = anchor_w * np.exp(offset_w)
box_h = anchor_h * np.exp(offset_h)
box = np.hstack([box_cx, box_cy, box_w, box_h]) #水平方向堆叠 np.vstack竖直方向堆叠
return box
这里只讲解box_cx,box_cy的计算公式,box_w和box_h类似。
首先看论文中的相关公式:
如下图,我们知道了cx1,cy1,w1,h1(蓝色框),这是anchor_box初始创建的。后来通过模型可以得到reg_delta。如何计算得到cx2,cy2呢?
首先,可以知道 cx1+dx=cx2 ;;cy1+dy=cy2;
而 dx=reg_delta[0]*w1;;dy=reg_delta[2]*h1
所以 cx2=cx1+reg_delta[0]*w1;;cy2=cy1+reg_delta[1]*h1
注意!!
这里得到的box_cx,box_cy,box_w,box_h,其中box_cx,box_cy还是相对于中心图片的偏移值,和anchors的前两个值的意义相同。
|—1.搭建模型
|—2.跟踪过程:
|——2.1 第一帧:
|——截取exemplar_frame(Img_z)
|——获得初始bbox(需要注意这里x,y值的含义),target_sz ,score_filters_z, reg_filters_z(后面两个用于后续互操作)
|——2.2 非第一帧:
|——截取detection_frame(Img_x)
|——通过Backbone得到score_filters_x, reg_filters_x
|——互相关操纵后得到pred_score, pred_regression(dx,dy,dw,dh)(这里很重要)
|——进行后处理(最重要)
|———分类后处理
|————softmax,得到”前景”概率
|———框偏移后处理
|————将bbox与pred_regression进行逆向计算,得到预测框(非最终的框,需要注意这里x,y值的含义)
|——获得惩罚因子(对应论文公式) 通过惩罚因子修正 ”前景”概率,修正预测框
|——预测框第二次修正,得到最终结果框
|——更新修改下一帧的中心偏差和图片大小偏差
|——返回最终结果框用于画图
|—画出box
因为本文主要是本人用来做的笔记,顺便进行知识巩固。如果本文对你有所帮助,那么本博客的目的就已经超额完成了。
本人英语水平、阅读论文能力、读写代码能力较为有限。有错误,恳请大佬指正,感谢。
欢迎交流
邮箱:[email protected]