YOLACT 、 YOLACT++、YolactEdge小结
作者:互联网
一、YOLACT
论文:https://arxiv.org/abs/1904.02689 yolact
源代码:https://github.com/dbolya/yolact yolact、yolact++
本文的主要贡献:
YOLACT是2019年发表在ICCV上面的一个实时实例分割的模型,它主要是通过两个并行的子网络来实现实例分割的。
(1)Prediction Head分支生成各个anchor的类别置信度、位置回归参数以及mask的掩码系数;
(2)Protonet分支生成一组原型mask。然后将原型mask和mask的掩码系数相乘,从而得到图片中每一个目标物体的mask。论文中还提出了一个新的NMS算法叫Fast-NMS,和传统的NMS算法相比只有轻微的精度损失,但是却大大提升了分割的速度。
YOLACT是一个one-stage模型,它和two-stage的模型(Mask R-CNN)相比起来,速度更快,但是精度稍差一些。YOLACT模型的框架如图所示。
PS:Mask R-CNN,即“先检测后分割”的方法,首先定位到目标物体的边框,然后在边框内分割目标物体。
这篇文章的目标是解决实例分割的实时性问题。通过在现有one-stage目标检测模型的基础上添加mask分支来解决这一问题。与Mask R-CNN等明显使用特征定位步骤的方法不同,在YOLACT中并不存在这一步。
为了达到这一目的,作者将实例分割任务划分为两个更简单的并行任务,通过对这两个任务的结果进行融合来得到最终的实例分割结果。具体如下:
1、Backbone:
YOLACT模型输入的图像大小为550*550,采用的Backbone为ResNet101,源代码中作者还使用了ResNet50和DarkNet53网络结构。ResNet的卷积模块一共有5个从conv1,conv2_x到conv5_x,分别对应图1 YOLACT模型中的C1,C2到C5。YOLACT和SSD一样采用了多尺度的特征图, 从而可以检测到不同尺寸的物体,也就是在大的特征图上检测小的物体,在小的特征图上检测大的物体。
2、FPN:
图1中的P3-P7是FPN网络,它是由C5经过1个卷积层得到P5,然后对P5采用双线性插值使特征图扩大一倍,与经过卷积的C4相加得到P4,再采用同样的方法即可得到P3。再然后,对P5进行卷积和下采样得到P6,对P6进行同样的卷积和下采样得到P7,从而建立FPN网络。接下来是并行的操作。P3 被送入 Protonet,P3-P7 也被同时送到 Prediction Head 中。(采用FPN的好处就是模型学习到特征更丰富,更有利于分割不同大小的目标物体。)
3、Protonet 分支:
Protonet分支的网络结构如下图3所示,它是由由若干卷积层组成。其输入是 P3,其输出的mask维度是 138 * 138 * k (k=32),即 32 个 prototype mask,每个大小是 138 * 138。
4、Prediction Head分支:
Prediction Head分支的网络结构如图4所示,它是在RetinaNet的基础上改进得到的,采用共享卷积网络,从而可以提高速度,达到实时分割的目的。它的输入是 P3-P7 共五个特征图,每个特征图先生成anchor,每个像素点生成3个anchor,比例是 1:1、1:2和2:1。五个特征图的anchor基本边长分别是24、48、96、192和384。基本边长根据不同比例进行调整,确保 anchor 的面积相等。
为了便于理解,接下来以 P3 为例进行解释说明。假设P3的维度是 W3 * H3 * 256,那么它的anchor个数就是 a3 = W3 * H3 * 3。接下来 Prediction Head 为其生成三类输出:
类别置信度,由于 COCO 中共有 81 类(包括背景),所以其维度为 a3 * 81;
位置偏移,维度为 a3 * 4;
mask 置信度,维度为 a3 * 32。
对 P4-P7 进行的操作是相同的,最后将这些结果拼接起来,标记 a = a3 + a4 + a5 + a6 + a7,得到:
全部类别置信度,由于 COCO 中共有 81 类(包括背景),所以其维度为 a * 81;
全部位置偏移,维度为 a * 4;
全部 mask 置信度,维度为 a * 32。
5、Fast NMS:
通过Prediction Head分支网络后会得到很多anchor,可以在anchor的位置加上位置偏移得到RoI位置。由于RoI存在重叠,NMS 是常用的筛选算法,而本文提出了一种新的筛选算法叫Fast-NMS,在保证精度的前提下,减少了筛选的时间。
接下来,通过举例详细介绍Fast NMS算法。假设我们有5个RoI,对于 person这一类,按分类置信度由高到低排序得到b1、b2、b3、b4和 b5。接下来通过矩阵运算得到5个ROI相互之间的IoU,假设结果如下图所示:
可以看出这是一个对称矩阵,接下来将这个对称阵的对角线和下三角元素删掉,得到结果如下图所示:
接下来对每一列取最大值,得到结果[-, 0.8, 0.6, 0.6, 0.4]。假设阈值为0.5,那么IoU超过0.5的RoI需要舍弃掉。根据刚才得到的结果,b2、b3和b4对应的列都超出了阈值,所以这三个RoI会舍去。这样做的原因是,由于每一个元素都是行号小于列号,而序号又是按分类置信度从高到低降序排列的,因此任一元素大于阈值,代表着这一列对应的 RoI 与一个比它置信度高的 RoI 过于重叠了,需要将它舍去。
6、mask系数
典型的基于Anchor的检测模型会为每个Anchor预测4个值用于表征box信息,和C个值用于表征类别得分,共(4+C)个值。YOLACT为 每个Anchor预测(4+C+k)个值,额外k个值即为mask系数。
另外作者认为,为了能够通过线性组合来得到最终想要的mask,能够从最终的mask中减去原型mask是很重要的。换言之就是,mask系数必须有正有负。所以,在mask系数预测时使用了tanh函数进行非线性激活,因为tanh函数的值域是(-1,1).
7、mask合成
通过基本的矩阵乘法配合sigmoid函数来处理两分支的输出,从而合成mask。Prediction Head分支得到的mask coefficient和Protonet分支得到的 prototype mask 做矩阵乘法
其中,P是h×w×k的原型mask集合,C是n×k的系数集合,代表有n个通过NMS和阈值过滤的实例,每个实例对应有k个mask系数。
8、Crop & Threshold
为了改善小目标的分割效果,在推理时会首先根据检测框进行裁剪,再阈值化。而在训练时,会使用ground truth框来进行裁剪,并通过除以对应ground truth框面积来平衡loss尺度。
Crop指的是将边界外的mask清零,训练阶段的边界是ground truth bounding box,评估阶段的边界是预测的bounding box。Threshold指的是以0.5为阈值对生成的 mask进行图像二值化处理。
9、Loss:
YOLACT模型使用的loss是由边框的分类loss、边框的位置回归loss同SSD和mask损失为预测mask和ground truth mask的逐像素二进制交叉熵构成。
其中mask loss在计算时,因为mask的大小是138*138,需要先将原图的mask数据通过双线性插值缩小到这一尺寸。
10、yolact 实现:
(1)数据集,用labelme标注的两类pomeranian和pug,然后生成批量保存的json文件夹。如下图:
文件夹里面是这种形式:
我用的是COCO类型的数据,要把图片信息组织成对应的格式:
对应的转化成json文件的程序为:
import argparse
import json
import matplotlib.pyplot as plt
import skimage.io as io
import cv2
from labelme import utils
import numpy as np
import glob
import PIL.Image
class MyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
else:
return super(MyEncoder, self).default(obj)
class labelme2coco(object):
def __init__(self, labelme_json=[], save_json_path='./train.json'):
'''
:param labelme_json: 所有labelme的json文件路径组成的列表
:param save_json_path: json保存位置
'''
self.labelme_json = labelme_json
self.save_json_path = save_json_path
self.images = []
self.categories = []
self.annotations = []
# self.data_coco = {}
self.label = []
self.annID = 1
self.height = 0
self.width = 0
self.save_json()
def data_transfer(self):
for num, json_file in enumerate(self.labelme_json):
with open(json_file, 'r') as fp:
data = json.load(fp) # 加载json文件
self.images.append(self.image(data, num))
for shapes in data['shapes']:
label = shapes['label']
if label not in self.label:
self.categories.append(self.categorie(label))
self.label.append(label)
points = shapes['points']#这里的point是用rectangle标注得到的,只有两个点,需要转成四个点
points.append([points[0][0],points[1][1]])
points.append([points[1][0],points[0][1]])
self.annotations.append(self.annotation(points, label, num))
self.annID += 1
def image(self, data, num):
image = {}
img = utils.img_b64_to_arr(data['imageData']) # 解析原图片数据
# img=io.imread(data['imagePath']) # 通过图片路径打开图片
# img = cv2.imread(data['imagePath'], 0)
height, width = img.shape[:2]
img = None
image['height'] = height
image['width'] = width
image['id'] = num + 1
image['file_name'] = data['imagePath'].split('\\')[-1] #win
#image['file_name'] = data['imagePath'].split('/')[-1] #linux
self.height = height
self.width = width
return image
def categorie(self, label):
categorie = {}
categorie['supercategory'] = 'component'
categorie['id'] = len(self.label) + 1 # 0 默认为背景
categorie['name'] = label
return categorie
def annotation(self, points, label, num):
annotation = {}
annotation['segmentation'] = [list(np.asarray(points).flatten())]
annotation['iscrowd'] = 0
annotation['image_id'] = num + 1
# annotation['bbox'] = str(self.getbbox(points)) # 使用list保存json文件时报错(不知道为什么)
# list(map(int,a[1:-1].split(','))) a=annotation['bbox'] 使用该方式转成list
annotation['bbox'] = list(map(float, self.getbbox(points)))
annotation['area'] = annotation['bbox'][2] * annotation['bbox'][3]
annotation['category_id'] = self.getcatid(label) #有多个类别时
#annotation['category_id'] = 1 #只有一类时
annotation['id'] = self.annID
return annotation
def getcatid(self, label):
for categorie in self.categories:
if label == categorie['name']:
return categorie['id']
return 1
def getbbox(self, points):
# img = np.zeros([self.height,self.width],np.uint8)
# cv2.polylines(img, [np.asarray(points)], True, 1, lineType=cv2.LINE_AA) # 画边界线
# cv2.fillPoly(img, [np.asarray(points)], 1) # 画多边形 内部像素值为1
polygons = points
mask = self.polygons_to_mask([self.height, self.width], polygons)
return self.mask2box(mask)
def mask2box(self, mask):
'''从mask反算出其边框
mask:[h,w] 0、1组成的图片
1对应对象,只需计算1对应的行列号(左上角行列号,右下角行列号,就可以算出其边框)
'''
# np.where(mask==1)
index = np.argwhere(mask == 1)
rows = index[:, 0]
clos = index[:, 1]
# 解析左上角行列号
left_top_r = np.min(rows) # y
left_top_c = np.min(clos) # x
# 解析右下角行列号
right_bottom_r = np.max(rows)
right_bottom_c = np.max(clos)
# return [(left_top_r,left_top_c),(right_bottom_r,right_bottom_c)]
# return [(left_top_c, left_top_r), (right_bottom_c, right_bottom_r)]
# return [left_top_c, left_top_r, right_bottom_c, right_bottom_r] # [x1,y1,x2,y2]
return [left_top_c, left_top_r, right_bottom_c - left_top_c,
right_bottom_r - left_top_r] # [x1,y1,w,h] 对应COCO的bbox格式
def polygons_to_mask(self, img_shape, polygons):
mask = np.zeros(img_shape, dtype=np.uint8)
mask = PIL.Image.fromarray(mask)
xy = list(map(tuple, polygons))
PIL.ImageDraw.Draw(mask).polygon(xy=xy, outline=1, fill=1)
mask = np.array(mask, dtype=bool)
return mask
def data2coco(self):
data_coco = {}
data_coco['images'] = self.images
data_coco['categories'] = self.categories
data_coco['annotations'] = self.annotations
return data_coco
def save_json(self):
self.data_transfer()
self.data_coco = self.data2coco()
# 保存json文件
json.dump(self.data_coco, open(self.save_json_path, 'w'), indent=4, cls=MyEncoder) # indent=4 更加美观显示
labelme_json = glob.glob('/home/yuxin/labelme_to_coco/pug/pomeranian_pug_json/*.json')
# labelme_json=['./1.json']
labelme2coco(labelme_json, './instances_train2014.json')
instances_train2014.json之后再换成val数据。
(2)修改配置文件config.py:
①类别:
COCO_CLASSES = (‘pomeranian’,‘pug’),原来是coco数据集的80类,这里我的数据集2类 COCO_LABEL_MAP = {1: 1, 2: 2}
②数据集,哪种数据形式(COCO,VOC等),json文件位置:
dataset_base = Config({
'name': 'Base Dataset',
# Training images and annotations
'train_images': './data/coco/pomeranian_pug_train/', #修改train数据存放的位置
'train_info': 'path_to_annotation_file',
# Validation images and annotations.
'valid_images': './data/coco/pomeranian_pug_val/', #修改val数据存放的位置
'valid_info': 'path_to_annotation_file',
# Whether or not to load GT. If this is False, eval.py quantitative evaluation won't work.
'has_gt': True,
# A list of names for each of you classes.
'class_names': COCO_CLASSES,
# COCO class ids aren't sequential, so this is a bandage fix. If your ids aren't sequential,
# provide a map from category_id -> index in class_names + 1 (the +1 is there because it's 1-indexed).
# If not specified, this just assumes category ids start at 1 and increase sequentially.
'label_map': None
})
coco2014_dataset = dataset_base.copy({
'name': 'COCO 2014',
'train_info': './data/coco/annotations/instances_train2014.json',
'valid_info': './data/coco/annotations/instances_val2014.json',
'label_map': COCO_LABEL_MAP
})
③这里我想选择resnet50 backbone训练,则对应的是
yolact_resnet50_config = yolact_base_config.copy({
'name': 'yolact_resnet50',
'backbone': resnet50_backbone.copy({
'selected_layers': list(range(1, 4)),
'pred_scales': yolact_base_config.backbone.pred_scales,
'pred_aspect_ratios': yolact_base_config.backbone.pred_aspect_ratios,
'use_pixel_scales': True,
'preapply_sqrt': False,
'use_square_anchors': True, # This is for backward compatability with a bug
}),
})
回头修改resnet50_backbone,发现需要修改backbone_base的预训练权重的位置:
backbone_base = Config({
'name': 'Base Backbone',
'path': '/home/yuxin/yolact-master/weights', #修改weights路径
'type': object,
'args': tuple(),
'transform': resnet_transform,
'selected_layers': list(),
'pred_scales': list(),
'pred_aspect_ratios': list(),
'use_pixel_scales': False,
'preapply_sqrt': True,
'use_square_anchors': False,
})
回头修改yolact_resnet50_config参数,发现需要修改yolact_base_config:
yolact_base_config = coco_base_config.copy({
'name': 'yolact_base',
# Dataset stuff
#'dataset': coco2017_dataset,
#'num_classes': len(coco2017_dataset.class_names) + 1,
'dataset': coco2014_dataset, #修改默认数据集
'num_classes': len(coco2014_dataset.class_names) + 1, #数据类别
# Image Size
'max_size': 550,
# Training params
#'lr_steps': (280000, 600000, 700000, 750000),
#'max_iter': 800000,
'lr_steps': (200000, 300000, 330000, 380000), #修改训练参数
'max_iter': 100000,
# Backbone Settings
'backbone': resnet101_backbone.copy({
'selected_layers': list(range(1, 4)),
'use_pixel_scales': True,
'preapply_sqrt': False,
'use_square_anchors': True, # This is for backward compatability with a bug
'pred_aspect_ratios': [ [[1, 1/2, 2]] ]*5,
'pred_scales': [[24], [48], [96], [192], [384]],
}),
(3)训练:
$ python train.py --config=yolact_resnet50_config --batch_size 4
在终端运行命令:
(4)测试:
终端输入命令:
$ python eval.py --trained_model=weights/yolact_resnet50_2666_200000.pth --score_threshold=0.5 --top_k=15 --image=pomeranian_test.jpg
图片测试结果:
最后一幅图像是只分割了前景和背景类,没有区分出实例, 作者也提出YOLACT的错误大多是由检测器引起的,比如错误分类或bounding box定位不准。当在画面某个位置存在多个重叠的实例时,则网络可能无法通过自身学习到的原型mask对其进行定位。在这种情况下,会输出更接近前景mask的内容,而不是某些实例的分割。
二、YOLACT++:
论文: https://arxiv.org/abs/1912.06218 Yolact ++
DCN:https://arxiv.org/abs/1703.06211
源代码:https://github.com/dbolya/yolact
DCN:https://github.com/msracver/Deformable-ConvNets
YOLACT++在保证实时性(大于或等于30fps)的前提下,对原版的YOLACT做出几点改进,大幅提升mAP。主要改进有以下几点:
1、在Backbone中引入可变形卷积:
可变形卷积(DCNs)通过使用自由形式的采样代替了传统CNN中使用的刚性网格采样,从而全面刷新了各个检测分割模型的精度。可变形卷积的实现方式如下图所示,从图中我们可以看出,就是在不同的卷积层后面加入一个卷积层来学习输入特征图中每个像素点的位置偏移,然后将学到的X轴和Y轴上的位置偏移加入到输入特征图中,在进行后续的卷积。
可变形卷积和普通卷积的区别的直观感受可以见下图所示,从图中我们可以看出:在顶层的feature map上取两个激活点(分别在大羊和小羊身上),代表的是不同尺度和形状,然后将顶层的feature map经过33的卷积后,在第二层上得到需要抽样的一些点,第二层的feature map接着在经过一个3 3的卷积,在底层上得到需要采样的点。通过对比可以看出,可变形卷积通过学习位置偏移得到的采样点更符合物体本身的形状和尺寸,而标准卷积却是固定的采样点。
在YOLACT++ 中,作者参考Deformable ConvNets v2的思路,将ResNet的C3-C5中的各个标准3x3卷积换成3x3可变性卷积,但没有使用堆叠的可变形卷积模块,因为延迟太高。作者做了三组实验,最后选择每隔3个卷积层,替换一个标准卷积为可变形卷积,这样在时间和性能方面实现trade-off。作者认为DCN在YOLACT上表现良好,有以下两点原因:
( 1 ) DCN通过与目标实例对齐,增强了网络处理具有不同比例,旋转和纵横比的实例的能力;
( 2 )YOLACT是一种one-stage模型,相对于Mask R-CNN 等two-stage模型,YOLACT缺乏一个重采样过程,也没有办法恢复次优采样, 而Mask R-CNN通过ROI Align操作,将各目标对齐到一定区域,可以在一定程度上缓解该问题。
通过加入可变形卷积,mAP提高了1.8,速度慢了8ms。作者认为性能提升的原因有以下几点:
(1)通过和目标实例进行对准,使得网络可以处理不同尺度、旋转角度和比例的实例;
(2)YOLACT本身没有再采样策略,因而一个更好、更灵活的采样策略更重要。
同时作者发现,在引入可变形卷积时需要选择合适的插入位置才能取得性能的提升。
2、优化Prediction Head分支:
由于YOLACT是anchor-based的,所以对anchor设计进行优化。经过实验,选择在每个FPN 层上乘3种大小(1,
2
3
\sqrt[3]{2}
32
、
4
3
\sqrt[3]{4}
34
),这样相当于anchor数量较原来的YOLACT增加了3倍。
YOLACT++受MS R-CNN的启发,高质量的mask并不一定就对应着高的分类置信度,对于模型来说,其分类置信度和预测的掩膜的质量之间是存在差异的。换句话说,以包围框得分来评价mask好坏并不合理,所以在模型后添加了Mask Re-Scoring分支,该分支依据所预测的掩膜与ground-truth的IoU对掩膜进行评分,使用YOLACT生成的裁剪后的原型mask(未作阈值化)作为输入,输出对应每个类别的GT-mask的IoU。
具体说,Fast Mask Re-Scoring网络由6层FCN组成,每一层卷积层之后跟一层ReLU,最后一层为全局池化层。该网络以YOLACT所输出的裁剪过的mask(未经过阈值)为输入,输出mask对于每一个目标种类的IoU。接着,将分类分支预测的类别所对应的mask IoU与相对应的类别置信度的乘积作为该mask最终的分数。
与Mask Scoring R-CNN相比,作者提出的方法有以下不同:
(1)输入为全尺寸的图像的mask(预测框之外的区域值为0),而Mask Scoring R-CNN输入的是RoI repooled的掩膜与来自于掩膜预测分支的特征图所拼接得到的;
(2)没有全连接层,因而本方法更快,加入Fast Mask Re-Scoring分支后时间仅增加了1.2ms。
3、YOLACT++与MS R-CNN的区别:
(1)YOLACT++直接使用全尺寸的mask作为scoring分支的输入,而MS R-CNN使用的是ROI Align后的特征再与其经过mask预测分支计算后的特征拼接后的组成的特征;
(2)YOLACT++的scoring分支没有使用FC层,这使得分割的速度提高。
作者通过对比实验得出:使用YOLACT++中提出的scoring分支单帧耗时只增加了1.2ms,而使用MS R-CNN中的scoring分支则会将单帧耗时提高28ms。主要是由于ROI Align、FC层和拼接操作造成的极大时延。
在YOLACT的基础上,作者进一步进行了如下修改,来得到一个准确度更高的实例分割模型。
(1)在backbonde网络中加入可变形卷积(deformable convolutions);
(2)使用更好的anchor尺度和比例对prediction head进行优化;
(3)加入新的mask re-scoring支路。
最终,在MS COCO上,YOLACT++可以获得34.1mAP和33.5fps的成绩,其精度已经非常接近SOTA模型。
4、对Prediction Head进行优化
YOLACT是基于anchor-based检测器的,因而选择正确的anchor超参数是很重要的。作者尝试了以下两种策略:
(1)保持anchor的尺度不变,增加比例的数目:[1,1/2,2]到[1,1/2,2,1/3,3];
(2)保持比例不变,将每一个FPN等级的尺度数目增加三倍([1×,2(1/3)×,2(2/3)×])。
上述两种方法分别将anchor的数目增加为原来的5/3倍和3倍。
5、YOLACT++实现:
(1)需要先编译DCNv2
命令:python setup.py build develop
报错,是pytorch版本的问题,我的版本是1.5
简单来说就是将src/cuda/dcn_v2_cuda.cu 文件中的THCState_getCurrentStream(state)都替换为c10::cuda::getCurrentCUDAStream()。https://github.com/CharlesShang/DCNv2/pull/58
或者直接像我一样,用1.5的版本直接替换https://github.com/lbin/DCNv2/tree/master
编译:./make.sh
测试DCNv2:python test.py
(2)使用同样的数据集,同样的网络模型,只需用yolact_plus_resnet50_config
(3)训练:
在终端输入命令:
$ python train.py --config=yolact_plus_resnet50_config --batch_size 4
训练后的权重文件都在weights文件夹中(包括预训练权重)。
(4)测试:
在终端运行命令:
$ python eval.py --trained_model=weights/yolact_plus_resnet50_2666_200000.pth --score_threshold=0.5 --top_k=15 --image=pomeranian_test.jpg
三、YOLACT EDGE:
论文:https://arxiv.org/abs/2012.12259
源代码:https://github.com/haotian-liu/yolact_edge
本文提出了YolactEdge实时实例分割方法,可以在小型边缘设备上以实时速度运行。具体来说,在550x550分辨率的图像上,带有ResNet-101主干的YolactEdge在Jetson AGX Xavier上的运行速度高达30.8FPS(在RTX 2080Ti上的运行速度为172.7FPS)。为了实现这一目标,我们对基于图像的最新实时方法YOLACT进行了两项改进:
(1)优化TensorRT,同时谨慎权衡速度和准确性;
(2)利用视频中时间冗余的新型特征扭曲模块。 学习特征,及其在时间维度上的传播、变换,以使得不用对每一帧都完全做特征的计算。
1、TensorRT优化:
TensorRT的优化策略其实是压缩网络权重数据的精度。如下表,原始的模型数据都是FP32(浮点数32),此时速度只有6.6FPS。但是将网络中的各个部分模型参数进行压缩后,精度并没有什么降低的情况下,帧率提高到27帧。
为了量化模型元件到INT8精度,校准步骤是必要的。为此,TensorRT收集每一层激活的直方图生成几个具有不同阈值的量化分布,并使用KL散度将每个量化分布与参考分布进行比较。这个步骤确保模型在转换为INT8精度时损失尽可能小的性能。
上表显示校准数据集大小的影响。文章实验发现使用50或100张图像进行校准在精度和速度方面都足够了。
2、 特征传播
论文其实是针对视频流对象的设计,要是只关心单张图像,TensorRT的优化使速度有了约4x的提升,在处理静态图像时,上面的TensorRT优化策略已经够了, 应该使用YolactEdge的这个版本。然而,在处理视频时可以利用时间冗余来做,下面看下文章对于视频流的设计。
从邻近关键帧转换(即扭曲)特征被证明是一种有效的策略,以减少主干计算,以产生快速的视频Box目标检测器。具体地说,是使用离网光流网络转换所有主干特征。然而,由于光流估计中不可避免的误差,我们发现它不能提供像实例分割这样的像素级任务所需的足够精确的特征。在本研究中提出执行部分特征转换,以改善转换后的特征的品质,同时仍维持快速的运行时间。这里给出一个序列帧的视频,目标是在每一帧
y
i
=
N
(
I
i
)
y_i=N(I_{i})
yi=N(Ii) 中以快速和准确的方式预测每个对象实例的mask。对于视频实例分割网络N,大体上遵循YOLACT速度和准确性权衡的设计理念。具体地说,在每一帧上执行2项并行任务:
(1)生成一组原型mask;
(2)预测每个实例的掩模系数。然后,将原型与掩模系数线性组合,得到最终的掩模。
为了清晰地表示,将N 分解为
N
f
e
a
t
N_{feat}
Nfeat和
N
p
r
e
d
N_{pred}
Npred ,其中
N
f
e
a
t
N_{feat}
Nfeat 表示特征骨干阶段,
N
p
r
e
d
N_{pred}
Npred 表示剩余部分(即用于生成原型掩模的类、box、mask系数和ProtoNet的预测头),它们接受
N
p
r
e
d
N_{pred}
Npred 的输出并进行实例分割预测。
本文有选择地将视频中的帧分为2类:关键帧
I
k
I^k
Ik 和非关键帧
I
n
I^n
In ;模型在这2组框架上只是在Backbone阶段发生了变化。
对于关键帧
I
k
I^k
Ik ,模型计算所有Backbone和金字塔特征(在Fig 1中的C1−C5和P3−P7)。而对于非关键帧只计算一个子集,把其余的利用时间冗余机制进行计算。通过这种方式可以在生成准确预测的同时保持快速运行时之间取得平衡。
本文方法计算的非关键帧只有通过高分辨率 C 3 n C_3^n C3n 级别(例如,跳过 C 4 n C_4^n C4n , C 5 n C_5^n C5n 因此 P 4 n P_4^n P4n , P 5 n P_5^n P5n计算),且仅将低分辨率转换 P 4 k P_4^k P4k / P 5 k P_5^k P5k 特性从之前的关键帧近似 P 4 k P_4^k P4k / P 5 k P_5^k P5k (表示 W 4 k W_4^k W4k / W 5 k W_5^k W5k )在当前非关键帧——如上图所示(右)。它通过与YOLACT相同的方式对 进行向下采样来计算 。通过计算 C 3 n C_3^n C3n 特征和变换后的 W 4 n W_4^n W4n 特征,生成 P 3 n P_3^n P3n 为 P 3 n = C 3 n + u p ( W 4 n ) P_3^n= C_3^n+up(W_4^n) P3n=C3n+up(W4n),其中 u p ( . ) up(.) up(.) 表示上采样。
最后,使用 P 3 n P_3^n P3n 特征来生成像素精确的原型。通过这种方式,可以保留用于生成mask原型的高分辨率细节,因为高分辨率的 C 3 n C_3^n C3n 特征是计算而不是转换的,因此不会受到流量估计中的错误的影响。
重要的是,尽管为每一帧(即关键帧和非关键帧)计算
C
1
−
C
3
C_1-C_3
C1−C3骨干特征,但避免了计算backbone特征中最复杂的部分,因为金字塔网络的不同阶段的计算代价是高度不平衡的。
如表所示,ResNet-101花费66%以上的计算在 ,而一半以上的推理时间被Backbone计算占用。而通过只计算特征金字塔的底层,并转换其余的层可以大大加快速度以达到实时性能
文章提出 空间维度的特征变换改善特征变换的质量。这里对于当前帧只用C3特征(文章说因为C3的结果质量还算可以,消耗小,而文章说C4部分的计算是最耗时间的,当然,因为Resnet101C4部分卷积层最多。。),W4、W5通过前一帧变换得到。而P6、P7通过W4采样得到。一起看下这个变换关系是怎么得到的:
其实就是通过两帧图像通过一个流场估计网络获取两帧的运动变换关系。文章的做法是,重复利用backbone获取到的丰富特征信息(C3),而不是再加一个新的网络。
给定一个非关键帧
I
n
I^n
In 及其前一关键帧
I
k
I^k
Ik ,所设计模型首先将它们之间的物体运动编码为一个2-D流场
M
(
I
k
,
I
n
)
M(I^k,I^n)
M(Ik,In) 。然后利用流场将特征
F
k
=
{
P
4
k
,
P
5
k
}
F^k=\left\{P_4^k,P_5^k\right\}
Fk={P4k,P5k} 从帧
I
k
I^k
Ik变换到与帧
I
n
I^n
In 对齐得到warped特征
F
n
=
{
W
4
n
,
W
5
n
}
=
T
(
F
k
,
M
(
I
k
,
I
n
)
)
F_n=\left\{W_4^n,W_5^n\right\}=T(F^k,M(I^k,I^n))
Fn={W4n,W5n}=T(Fk,M(Ik,In))。
为了进行快速的特征变换,需要有效地估计目标运动。现有的进行流导向特征变换的框架直接采用现成的像素级光流网络进行运动估计。例如,FlowNetS(Fig.2a)在3个阶段执行流量估计:
(1)首先,以原始RGB帧作为输入并计算特征堆栈;
(2)然后,它通过递归上采样和拼接特征映射来细化特征子集,生成既包含高级(大运动)信息又包含精细局部信息(小运动)的粗到细特征;
(3)最后,它使用这些特征来预测最终的Flow Map。
在本算法中,为了节省计算成本没有使用现成的流网络来处理原始RGB帧,而是重新使用所设计骨干网络计算出的特征,它已经产生了一组语义丰富的特征。为此提出了FeatFlowNet(b),它通常遵循FlowNetS架构,但在第1阶段,不是从原始RGB图像输入计算特征堆栈,而是重用来自ResNet骨干(C3)的特征,并使用较少的卷积层。正如实验中所证明的那样,Flow估计网络在同样有效的情况下要快得多。
3、特征Warping
使用FeatFlowNet来估计先前关键帧
I
k
I^k
Ik 和当前非关键帧
I
n
I^n
In 之间的Flow图
M
(
I
k
,
I
n
)
M(I^k,I^n)
M(Ik,In) ,然后通过反向warp将特征从
I
k
I^k
Ik 转换为
I
n
I^n
In :
将
I
n
I^n
In 中的每个像素x 投影到
I
k
I^k
Ik 中为
x
+
δ
x
x + \delta x
x+δx ,其中
δ
x
=
M
x
(
I
k
,
I
n
)
\delta x=M_x(I^k,I^n)
δx=Mx(Ik,In) 。通过双线性插值计算像素值
F
k
−
>
n
(
x
)
=
∑
u
θ
(
u
,
x
+
δ
x
)
F
k
(
x
)
F^{k->n}(x)=\sum_u\theta (u,x+\delta x)F^k(x)
Fk−>n(x)=∑uθ(u,x+δx)Fk(x),其中
θ
\theta
θ 为不同空间位置的双线性插值权值。
4、损失函数
对于实例分割任务,使用与YOLACT相同的损失来训练模型:
分类损失
L
c
l
s
L_{cls}
Lcls ,
Box回归损失
L
b
o
x
=
S
m
o
o
t
h
L
1
L_{box}=Smooth_{L1}
Lbox=SmoothL1 ,
Mask损失
L
s
m
a
r
k
=
B
C
E
(
m
,
m
g
t
)
L_{smark}=BCE(m,m_{gt})
Lsmark=BCE(m,mgt) ,
以及辅助语义分割损失 :
L
a
u
x
L_{aux}
Laux
5、实现
仍在报错,还未攻克。
参考:
https://blog.csdn.net/wh8514/article/details/105520870/
https://zhuanlan.zhihu.com/p/76470432
https://blog.csdn.net/sinat_37532065/article/details/103603919
https://zhuanlan.zhihu.com/p/76470432
https://blog.csdn.net/sinat_37532065/article/details/103603919
https://zhuanlan.zhihu.com/p/77644792
https://zhuanlan.zhihu.com/p/226143186
https://blog.csdn.net/qq_33642342/article/details/103609517
https://blog.csdn.net/qq_36808245/article/details/102731665
https://my.oschina.net/u/3776677/blog/4864481
https://www.pianshen.com/article/69121489242/
https://blog.csdn.net/weixin_43572595/article/details/112236210
https://blog.csdn.net/qq_39056987/article/details/111614535
标签:++,self,mask,json,YolactEdge,YOLACT,特征,卷积 来源: https://blog.csdn.net/yx868yx/article/details/113779675