日萌社
人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)
CNN:RCNN、SPPNet、Fast RCNN、Faster RCNN、YOLO V1 V2 V3、SSD、FCN、SegNet、U-Net、DeepLab V1 V2 V3、Mask RCNN
单目标跟踪 Siamese系列网络:SiamFC、SiamRPN、one-shot跟踪、one-shotting单样本学习、DaSiamRPN、SiamRPN++、SiamMask
1.6 test
学习目标:
- 了解网络测试的过程
- 能够实现网络的训练代码编写
网络的测试test主要在tools中test.py中实现,该文件中包含的内容如下图所示:
接下来,我们对以上内容进行介绍。
1.main
main函数是进行网络训练的程序入口,它的执行流程是读取配置信息->设置日志输出到日志文件中->加载网络模型->加载权重文件->加载数据->进行跟踪,在进行跟踪时,还要判断是否进行目标分割,如下图所示:
实现代码如下所示:
def main():
# 获取命令行参数信息
global args, logger, v_id
args = parser.parse_args()
# 获取配置文件中配置信息:主要包括网络结构,超参数等
cfg = load_config(args)
# 初始化logxi信息,并将日志信息输入到磁盘文件中
init_log('global', logging.INFO)
if args.log != "":
add_file_handler('global', args.log, logging.INFO)
# 将相关的配置信息输入到日志文件中
logger = logging.getLogger('global')
logger.info(args)
# setup model
# 加载网络模型架构
if args.arch == 'Custom':
from custom import Custom
model = Custom(anchors=cfg['anchors'])
else:
parser.error('invalid architecture: {}'.format(args.arch))
# 加载网络模型参数
if args.resume:
assert isfile(args.resume), '{} is not a valid file'.format(args.resume)
model = load_pretrain(model, args.resume)
# 使用评估模式,将drop等激活
model.eval()
# 硬件信息
device = torch.device('cuda' if (torch.cuda.is_available() and not args.cpu) else 'cpu')
model = model.to(device)
# 加载数据集 setup dataset
dataset = load_dataset(args.dataset)
# 这三种数据支持掩膜 VOS or VOT?
if args.dataset in ['DAVIS2016', 'DAVIS2017', 'ytb_vos'] and args.mask:
vos_enable = True # enable Mask output
else:
vos_enable = False
total_lost = 0 # VOT
iou_lists = [] # VOS
speed_list = []
# 对数据进行处理
for v_id, video in enumerate(dataset.keys(), start=1):
if args.video != '' and video != args.video:
continue
# true 调用track_vos
if vos_enable:
# 如测试数据是['DAVIS2017', 'ytb_vos']时,会开启多目标跟踪
iou_list, speed = track_vos(model, dataset[video], cfg['hp'] if 'hp' in cfg.keys() else None,
args.mask, args.refine, args.dataset in ['DAVIS2017', 'ytb_vos'], device=device)
iou_lists.append(iou_list)
# False 调用track_vot
else:
lost, speed = track_vot(model, dataset[video], cfg['hp'] if 'hp' in cfg.keys() else None,
args.mask, args.refine, device=device)
total_lost += lost
speed_list.append(speed)
# report final result
if vos_enable:
for thr, iou in zip(thrs, np.mean(np.concatenate(iou_lists), axis=0)):
logger.info('Segmentation Threshold {:.2f} mIoU: {:.3f}'.format(thr, iou))
else:
logger.info('Total Lost: {:d}'.format(total_lost))
logger.info('Mean Speed: {:.2f} FPS'.format(np.mean(speed_list)))
3.get_subwindow_tracking
该函数获取跟踪目标的图像窗口,对目标框进行调整,若目标框在图像之外,则扩充图像,并修改目标框的坐标,其代码如下所示:
def get_subwindow_tracking(im, pos, model_sz, original_sz, avg_chans, out_mode='torch'):
"""
获取跟踪目标的信息(图像窗口)
:param im:跟踪的模板图像
:param pos:目标位置
:param model_sz:模型要求输入的目标尺寸
:param original_sz: 扩展后的目标尺寸
:param avg_chans:图像的平均值
:param out_mode: 输出模式
:return:
"""
if isinstance(pos, float):
# 目标中心点坐标
pos = [pos, pos]
# 目标的尺寸
sz = original_sz
# 图像尺寸
im_sz = im.shape
# 扩展背景后边界到中心的距离
c = (original_sz + 1) / 2
# 判断目标是否超出图像边界,若超出边界则对图像进行填充
context_xmin = round(pos[0] - c)
context_xmax = context_xmin + sz - 1
context_ymin = round(pos[1] - c)
context_ymax = context_ymin + sz - 1
left_pad = int(max(0., -context_xmin))
top_pad = int(max(0., -context_ymin))
right_pad = int(max(0., context_xmax - im_sz[1] + 1))
bottom_pad = int(max(0., context_ymax - im_sz[0] + 1))
# 图像填充使得图像的原点发生变化,计算填充后图像块的坐标
context_xmin = context_xmin + left_pad
context_xmax = context_xmax + left_pad
context_ymin = context_ymin + top_pad
context_ymax = context_ymax + top_pad
# zzp: a more easy speed version
r, c, k = im.shape
# 若进行填充需对目标位置重新赋值
if any([top_pad, bottom_pad, left_pad, right_pad]):
# 生成与填充后图像同样大小的全零数组
te_im = np.zeros((r + top_pad + bottom_pad, c + left_pad + right_pad, k), np.uint8)
# 对原图像区域进行赋值
te_im[top_pad:top_pad + r, left_pad:left_pad + c, :] = im
# 将填充区域赋值为图像的均值
if top_pad:
te_im[0:top_pad, left_pad:left_pad + c, :] = avg_chans
if bottom_pad:
te_im[r + top_pad:, left_pad:left_pad + c, :] = avg_chans
if left_pad:
te_im[:, 0:left_pad, :] = avg_chans
if right_pad:
te_im[:, c + left_pad:, :] = avg_chans
# 根据填充结果修改目标的位置
im_patch_original = te_im[int(context_ymin):int(context_ymax + 1), int(context_xmin):int(context_xmax + 1), :]
else:
im_patch_original = im[int(context_ymin):int(context_ymax + 1), int(context_xmin):int(context_xmax + 1), :]
# 若跟踪目标块的尺寸与模型输入尺寸不同,则利用opencv修改图像尺寸
if not np.array_equal(model_sz, original_sz):
im_patch = cv2.resize(im_patch_original, (model_sz, model_sz))
else:
im_patch = im_patch_original
# cv2.imshow('crop', im_patch)
# cv2.waitKey(0)
# 若输出模式是Torch,则将其通道调换,否则直接输出im_patch
return im_to_torch(im_patch) if out_mode in 'torch' else im_patch
4.generate_anchor
该方法生成目标的anchor,利用anchor对目标进行跟踪,并对anchor的坐标进行修改。
代码如下所示:
def generate_anchor(cfg, score_size):
"""
生成锚点:anchor
:param cfg: anchor的配置信息
:param score_size:分类的评分结果
:return:生成的anchor
"""
# 初始化anchor
anchors = Anchors(cfg)
# 得到生成的anchors
anchor = anchors.anchors
# 得到每一个anchor的左上角和右下角坐标
x1, y1, x2, y2 = anchor[:, 0], anchor[:, 1], anchor[:, 2], anchor[:, 3]
# 将anchor转换为中心点坐标和宽高的形式
anchor = np.stack([(x1+x2)*0.5, (y1+y2)*0.5, x2-x1, y2-y1], 1)
# 获取生成anchor的范围
total_stride = anchors.stride
# 获取锚点的个数
anchor_num = anchor.shape[0]
# 将对锚点组进行广播,并设置其坐标。
anchor = np.tile(anchor, score_size * score_size).reshape((-1, 4))
# 加上ori偏移后,xx和yy以图像中心为原点
ori = - (score_size // 2) * total_stride
xx, yy = np.meshgrid([ori + total_stride * dx for dx in range(score_size)],
[ori + total_stride * dy for dy in range(score_size)])
xx, yy = np.tile(xx.flatten(), (anchor_num, 1)).flatten(), \
np.tile(yy.flatten(), (anchor_num, 1)).flatten()
# 获取anchor
anchor[:, 0], anchor[:, 1] = xx.astype(np.float32), yy.astype(np.float32)
return anchor
5.siamese_init
Siamese_init中创建目标字典state,state中的内容如下图所示:
其中,TrackerConfig是配置信息,net是网络模型,window是惩罚窗口,还包括跟踪目标的系列信息。
代码实现如下所示:
def siamese_init(im, target_pos, target_sz, model, hp=None, device='cpu'):
"""
初始化跟踪器,根据目标的信息构建state 字典
:param im: 当前处理的图像
:param target_pos: 目标的位置
:param target_sz: 目标的尺寸
:param model: 训练好的网络模型
:param hp: 超参数
:param device: 硬件信息
:return: 跟踪器的state字典数据
"""
# 初始化state字典
state = dict()
# 设置图像的宽高
state['im_h'] = im.shape[0]
state['im_w'] = im.shape[1]
# 配置跟踪器的相关参数
p = TrackerConfig()
# 对参数进行更新
p.update(hp, model.anchors)
# 更新参数
p.renew()
# 获取网络模型
net = model
# 根据网络参数对跟踪器的参数进行更新,主要是anchors
p.scales = model.anchors['scales']
p.ratios = model.anchors['ratios']
p.anchor_num = model.anchor_num
# 生成锚点
p.anchor = generate_anchor(model.anchors, p.score_size)
# 图像的平均值
avg_chans = np.mean(im, axis=(0, 1))
# 根据设置的上下文比例,输入z 的宽高及尺寸
wc_z = target_sz[0] + p.context_amount * sum(target_sz)
hc_z = target_sz[1] + p.context_amount * sum(target_sz)
s_z = round(np.sqrt(wc_z * hc_z))
# 初始化跟踪目标 initialize the exemplar
z_crop = get_subwindow_tracking(im, target_pos, p.exemplar_size, s_z, avg_chans)
# 将其转换为Variable可在pythorch中进行反向传播
z = Variable(z_crop.unsqueeze(0))
# 专门处理模板
net.template(z.to(device))
# 设置使用的惩罚窗口
if p.windowing == 'cosine':
# 利用hanning窗的外积生成cosine窗口
window = np.outer(np.hanning(p.score_size), np.hanning(p.score_size))
elif p.windowing == 'uniform':
window = np.ones((p.score_size, p.score_size))
# 每一个anchor都有一个对应的惩罚窗口
window = np.tile(window.flatten(), p.anchor_num)
# 将信息更新到state字典中
state['p'] = p
state['net'] = net
state['avg_chans'] = avg_chans
state['window'] = window
state['target_pos'] = target_pos
state['target_sz'] = target_sz
return state
6.siamese_track
该方法对目标进行追踪,根据siamese_init中获取的目标跟踪框,然后调用track_mask(若进行分割) 或track(不进行分割)进行目标追踪。
1.函数原型
def siamese_track(state, im, mask_enable=False, refine_enable=False, device='cpu', debug=False):
"""
对目标进行跟踪
:param state:目标状态
:param im:跟踪的图像帧
:param mask_enable:是否进行掩膜
:param refine_enable:是否进行特征的融合
:param device:硬件信息
:param debug: 是否进行debug
:return:跟踪目标的状态 state字典
"""
2.目标当前状态
获取目标的当前状态,若进行debug,可将目标的状态绘制在图像上。
# 获取目标状态
p = state['p']
net = state['net']
avg_chans = state['avg_chans']
window = state['window']
target_pos = state['target_pos']
target_sz = state['target_sz']
# 包含周边信息的跟踪框的宽度,高度,尺寸
wc_x = target_sz[1] + p.context_amount * sum(target_sz)
hc_x = target_sz[0] + p.context_amount * sum(target_sz)
s_x = np.sqrt(wc_x * hc_x)
# 模板模型输入框尺寸与跟踪框的比例
scale_x = p.exemplar_size / s_x
# 使用与模板分支相同的比例得到检测区域
d_search = (p.instance_size - p.exemplar_size) / 2
pad = d_search / scale_x
s_x = s_x + 2 * pad
# 对检测框进行扩展,包含周边信息
crop_box = [target_pos[0] - round(s_x) / 2, target_pos[1] - round(s_x) / 2, round(s_x), round(s_x)]
# 若进行debug
if debug:
# 复制图片
im_debug = im.copy()
# 产生crop_box
crop_box_int = np.int0(crop_box)
# 将其绘制在图片上
cv2.rectangle(im_debug, (crop_box_int[0], crop_box_int[1]),
(crop_box_int[0] + crop_box_int[2], crop_box_int[1] + crop_box_int[3]), (255, 0, 0), 2)
# 图片展示
cv2.imshow('search area', im_debug)
cv2.waitKey(0)
3.目标跟踪
根据是否对目标进行分割,调用track_mask或track进行目标追踪。
# 将目标位置按比例转换为要跟踪的目标
x_crop = Variable(get_subwindow_tracking(im, target_pos, p.instance_size, round(s_x), avg_chans).unsqueeze(0))
#调用网络进行目标跟踪
if mask_enable:
# 进行目标分割
score, delta, mask = net.track_mask(x_crop.to(device))
else:
# 只进行目标追踪,不进行分割
score, delta = net.track(x_crop.to(device))
4.分类和回归
目标回归和分类的结果是通过RPN网络实现的,如下图所示:
rpn网络返回的结果并不是真实的检测框的宽高位置等,而是:
其中:delta[0],delta[1],delta[2,],delta[3],是网络模型返回的结果,在以下代码中指等式右侧的delta[0],delta[1],delta[2,],delta[3]。这里我们要求目标框的预测位置,也就是Tx,Ty,Tw,Th(在以下代码中指等式左边的delta[0],delta[1],delta[2,],delta[3]),Ax,Ay,Aw,Ah表示anchor的中心点,宽,高.
以下代码中指p.anchor。在预测中转换为以下公式:
代码如下所示:
# 目标框回归结果(将其转成4*...的样式)
delta = delta.permute(1, 2, 3, 0).contiguous().view(4, -1).data.cpu().numpy()
# 目标分类结果(将其转成2*...的样式)
score = F.softmax(score.permute(1, 2, 3, 0).contiguous().view(2, -1).permute(1, 0), dim=1).data[:,
1].cpu().numpy()
# 计算目标框的中心点坐标,delta[0],delta[1],以及宽delta[2]和高delta[3]。
delta[0, :] = delta[0, :] * p.anchor[:, 2] + p.anchor[:, 0]
delta[1, :] = delta[1, :] * p.anchor[:, 3] + p.anchor[:, 1]
delta[2, :] = np.exp(delta[2, :]) * p.anchor[:, 2]
delta[3, :] = np.exp(delta[3, :]) * p.anchor[:, 3]
5.尺度惩罚
接下来是使用cosine窗口和尺度惩罚选择最优的目标。首先进行尺度抑制,k 是超参数,r表示宽高比,s表示等效边长:
def sz(w, h):
"""
计算等效边长
:param w: 宽
:param h: 高
:return: 等效边长
"""
pad = (w + h) * 0.5
sz2 = (w + pad) * (h + pad)
return np.sqrt(sz2)
def sz_wh(wh):
"""
计算等效边长
:param wh: 宽高的数组
:return: 等效边长
"""
pad = (wh[0] + wh[1]) * 0.5
sz2 = (wh[0] + pad) * (wh[1] + pad)
return np.sqrt(sz2)
下面进行惩罚,并利用非极大值抑制得到最终的目标跟踪框。
# 非极大值抑制
def change(r):
"""
将r和1/r逐位比较取最大值
:param r:
:return:
"""
return np.maximum(r, 1. / r)
# 尺寸惩罚 size penalty
target_sz_in_crop = target_sz*scale_x
s_c = change(sz(delta[2, :], delta[3, :]) / (sz_wh(target_sz_in_crop))) # scale penalty
r_c = change((target_sz_in_crop[0] / target_sz_in_crop[1]) / (delta[2, :] / delta[3, :])) # ratio penalty
# p.penalty_k超参数
penalty = np.exp(-(r_c * s_c - 1) * p.penalty_k)
# 对分类结果进行惩罚
pscore = penalty * score
# cos window (motion model)
# 窗口惩罚:按一定权值叠加一个窗分布值
pscore = pscore * (1 - p.window_influence) + window * p.window_influence
# 获取最优权值的索引
best_pscore_id = np.argmax(pscore)
# 将最优的预测结果映射回原图
pred_in_crop = delta[:, best_pscore_id] / scale_x
# 计算lr
lr = penalty[best_pscore_id] * score[best_pscore_id] * p.lr # lr for OTB
# 计算目标的位置和尺寸:根据预测偏移得到目标位置和尺寸
res_x = pred_in_crop[0] + target_pos[0]
res_y = pred_in_crop[1] + target_pos[1]
res_w = target_sz[0] * (1 - lr) + pred_in_crop[2] * lr
res_h = target_sz[1] * (1 - lr) + pred_in_crop[3] * lr
# 目标的位置和尺寸
target_pos = np.array([res_x, res_y])
target_sz = np.array([res_w, res_h])
6.分割
该部分对目标进行分割,主要是根据是否使用refine模块对模块进行图像分割。
# 若进行分割
if mask_enable:
# 获取最优预测结果的位置索引:np.unravel_index:将平面索引或平面索引数组转换为坐标数组的元组
best_pscore_id_mask = np.unravel_index(best_pscore_id, (5, p.score_size, p.score_size))
delta_x, delta_y = best_pscore_id_mask[2], best_pscore_id_mask[1]
# 是否进行特征融合
if refine_enable:
# 调用track_refine,运行 Refine 模块,由相关特征图上 1×1×256 的特征向量与检测下采样前的特征图得到目标掩膜
mask = net.track_refine((delta_y, delta_x)).to(device).sigmoid().squeeze().view(
p.out_size, p.out_size).cpu().data.numpy()
else:
# 不进行融合时直接生成掩膜数据
mask = mask[0, :, delta_y, delta_x].sigmoid(). \
squeeze().view(p.out_size, p.out_size).cpu().data.numpy()
根据分割的结果,进一步获取目标跟踪的位置和大小。
首先进行图像的仿射变换:
def crop_back(image, bbox, out_sz, padding=-1):
"""
对图像进行仿射变换
:param image: 图像
:param bbox:
:param out_sz: 输出尺寸
:param padding: 是否进行扩展
:return: 仿射变换后的结果
"""
# 构造变换矩阵
# 尺度系数
a = (out_sz[0] - 1) / bbox[2]
b = (out_sz[1] - 1) / bbox[3]
# 平移量
c = -a * bbox[0]
d = -b * bbox[1]
mapping = np.array([[a, 0, c],
[0, b, d]]).astype(np.float)
# 进行仿射变换
crop = cv2.warpAffine(image, mapping, (out_sz[0], out_sz[1]),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT,
borderValue=padding)
return crop
然后对分割结果进行仿射变换后,求其轮廓的最小外接矩形,得到目标的位置,这样我们得到的目标框就会随着目标的运动进行适应性的改变。
# 检测区域框长度与输入模型的大小的比值:缩放系数
s = crop_box[2] / p.instance_size
# 预测的模板区域框
sub_box = [crop_box[0] + (delta_x - p.base_size / 2) * p.total_stride * s,
crop_box[1] + (delta_y - p.base_size / 2) * p.total_stride * s,
s * p.exemplar_size, s * p.exemplar_size]
# 缩放系数
s = p.out_size / sub_box[2]
# 背景框
back_box = [-sub_box[0] * s, -sub_box[1] * s, state['im_w'] * s, state['im_h'] * s]
# 仿射变换
mask_in_img = crop_back(mask, back_box, (state['im_w'], state['im_h']))
# 得到掩膜结果
target_mask = (mask_in_img > p.seg_thr).astype(np.uint8)
# 根据cv2的版本查找轮廓
if cv2.__version__[-5] == '4':
# opencv4中返回的参数只有两个,其他版本有四个
contours, _ = cv2.findContours(target_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
else:
_, contours, _ = cv2.findContours(target_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# 获取轮廓的面积
cnt_area = [cv2.contourArea(cnt) for cnt in contours]
if len(contours) != 0 and np.max(cnt_area) > 100:
# 获取面积最大的轮廓
contour = contours[np.argmax(cnt_area)] # use max area polygon
# 转换为...*2的形式
polygon = contour.reshape(-1, 2)
# pbox = cv2.boundingRect(polygon) # Min Max Rectangle
# 得到最小外接矩形后找到该矩形的四个顶点
prbox = cv2.boxPoints(cv2.minAreaRect(polygon)) # Rotated Rectangle
# box_in_img = pbox
# 获得跟踪框
rbox_in_img = prbox
else: # empty mask
# 根据预测的目标位置和尺寸得到location
location = cxy_wh_2_rect(target_pos, target_sz)
# 得到跟踪框的四个顶点
rbox_in_img = np.array([[location[0], location[1]],
[location[0] + location[2], location[1]],
[location[0] + location[2], location[1] + location[3]],
[location[0], location[1] + location[3]]])
7.跟踪结果
得到目标的位置和大小,并将信息更新到state对象中。
# 得到目标的位置和尺寸
target_pos[0] = max(0, min(state['im_w'], target_pos[0]))
target_pos[1] = max(0, min(state['im_h'], target_pos[1]))
target_sz[0] = max(10, min(state['im_w'], target_sz[0]))
target_sz[1] = max(10, min(state['im_h'], target_sz[1]))
# 更新state对象
state['target_pos'] = target_pos
state['target_sz'] = target_sz
state['score'] = score[best_pscore_id]
state['mask'] = mask_in_img if mask_enable else []
state['ploygon'] = rbox_in_img if mask_enable else []
return state
6.网络测试
进行网络测试时,在终端执行:
cd $SiamMask/experiments/siammask_sharp
bash test_mask_refine.sh config_vot.json SiamMask_VOT.pth VOT2016 0
test_mask_refine.sh中的内容如下:
# 判断是否为空字符串,若为空提示需要输入参数,
if [ -z "$4" ]
then
# echo命令用于字符串的显示,会在终端显示该命令
echo "Need input parameter!"
echo "Usage: bash `basename "$0"` \$CONFIG \$MODEL \$DATASET \$GPUID"
exit
# fi为if语句的结束,相当于end if
fi
# 指明项目的位置,是最上层的位置
ROOT=/Users/yaoxiaoying/Documents/01-工作/03.计算机视觉/03.智慧交通/04.单目标追踪/SiamMask-master
# 设置环境变量
export PYTHONPATH=$ROOT:$PYTHONPATH
# 创建log路径
mkdir -p logs
# 设置参数
config=$1
model=$2
dataset=$3
gpu=$4
# 运行test.py,输入参数有:(1) config file: config_vot.json; (2)mode: SiamMask_VOT.pth; (3)dataset:VOT2016; (4)gpu:0
CUDA_VISIBLE_DEVICES=$gpu python -u $ROOT/tools/test.py \
--config $config \
--resume $model \
--mask --refine \
--dataset $dataset 2>&1 | tee logs/test_$dataset.log
# 2>&1:将错误定向到标准输出
# tee logs/test_$dataset.log:将标准输出的内容添加到log文件中
测试结果:
[2020-02-10 21:02:20,709-rk0-test.py#856] Total Lost: 44
[2020-02-10 21:02:20,710-rk0-test.py#858] Mean Speed: 5.85 FPS
总结:
- 网络测试使用数据对网络的性能进行测试,流程是加载数据,模型,对数据中的目标进行跟踪
- 网络测试代码中主要包括
- siammese_init:跟踪初始化
- siamese_trask:对目标进行跟踪