roidb数据结构
roidb的类型是list
, 其中的每个元素的数据类型都是dict
, roidb列表的长度为数据集的数量(即图片的数量), roidb中每个元素的详细情况如下表所示:
for entry in roidb |
数据类型 | 详细说明 |
---|---|---|
entry['id'] |
int | 代表了当前image的img_id |
entry['file_name'] |
string | 表示当前图片的文件名(带有.jpg后缀) |
entry['dataset'] |
string | 指明所属的数据集? |
entry['image'] |
string | 当前image的文件路径 |
entry['flipped'] |
bool | 当前图片是否进行翻转 |
entry['height'] |
int | 当前图片的高度 |
entry['width'] |
int | 当前图片的宽度 |
entry['has_visible_keypoints'] |
bool | 是否含有关键点 |
entry['boxes'] |
float32, numpy数组(num_objs, 4) | num_objs为当前图片中的目标物体个数, 4代表bbox的坐标 |
entry['segms'] |
二维列表[[],[],…] | 列表中每个元素都还是一个列表, 其中存储着每个物体的ploygon实例标签 |
entry['gt_classes'] |
int32, numpy数组(num_objs) | 指明当前图片中每一个obj的真实类别 |
entry['seg_areas'] |
float32, numpy数组(num_objs) | 代表当前图片中每一个obj的掩膜面积 |
entry['gt_overlaps'] |
float32, scipy.sparse.csr_matrix数据(num_objs, 81) | 代表每一个obj与81个不同类别的overlap |
entry['is_crowd'] |
bool, numpy数组(num_objs) | 代表当前掩膜是否为群落 |
entry[‘box_to_gt_ind_map’] | int32, numpy数组(num_objs) | 该列表存储着box的顺序下标值, 同样是一维数组, 直接拼接,将每一个roi映射到一个index上, index是在entry[‘gt_classes’]>0的rois列表的下标 |
combined_roidb_for_training()
方法
在目标检测类任务中, 有一个很重要的数据结构roidb, 它将作为基本的数据结构在数据队列中存在, Detectron 的数据载入类 RoIDdataLoader
也是将该数据结构作为成员变量使用的, 因此, 有必要对这个数据结构展开分析.
首先, 在运行训练脚本时, 就会调用到 detectron/utils/train.py
中的 train()
函数, 而train()
函数内部又会调用当前文件的add_model_training_inputs()
函数, 在这个函数内部, 就会调用到 detectron/datasets/roidb
文件中的 combined_roidb_for_training()
函数, 该函数的返回值正是roidb
, 这是贯穿整个训练过程的训练数据, 故我们对此函数进行分析. 该函数代码解析如下:
1 | # detectron/datasets/roidb.py |
get_roidb()
方法
在上面的函数中我们可以发现, combined_roidb_for_training
函数内部又定义了另一个函数get_roidb()
, 而该函数主要是基于detectron/datasets/json_dataset.py
中的JsonDataset
类及该类的成员方法get_roidb
实现的, 因此, 我们先跳到json_dataset.py
文件中去看看这个类的内部实现是怎样的:
1 | # detectron/datasets/json_dataset.py |
_prep_roidb_entry()
方法
数据准备函数 _prep_roidb_entry()
的实现解析1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45# detectron/datasets/json_dataset.py
class JsonDataset(object):
def __init__(...):
#...
def get_roidb(...):
#...
# 该函数主要将空的元数据添加到roidb entry中
def _prep_roidb_entry(self, entry):
# entry的'dataset'关键字, 值为self.
entry['dataset'] = self
im_path = os.path.join(self.image_directory, self.image_prefix+entry['file_name'])
assert os.path.exists(im_path), "Image \"{} \" not found".format(im_path)
# entry的'image'关键字, 值为当前imageid对应的image路径
entry['image'] = im_path
entry['flipped'] = False # 禁止反转
entry['has_visible_keypoints'] = False
# 下面entry键的对应值均为空, 暂为占位键
# entry的'boxes'关键字,值为n×4的numpy数组, n代表box的数量,这里暂时为0
entry['boxes'] = np.empty((0,4), dtype=np.float32)
entry['segms'] = [] # entry的'segms'关键字, 值为一个列表,暂时为空
# entry的'gt_classes'关键字, 是个一维数组, 维度与box的数量n对应,暂时为0
entry['gt_classes'] = np.empty((0), dtype=np.int32)
# 代表掩膜的面积, 供n项, 与boxes数目相对
entry['seg_areas'] = np.empty((0), dtype=np.float32)
# TODO, 这里是一个矩阵压缩, 矩阵大小为n×c, c为类别数量, 没太搞懂要压缩成什么?
entry['gt_overlaps'] = scipy.sparse.csr_matrix(
np.empty((0, self.num_classes), dtype=np.float32)
)
# 同样是n行1列, n与boxes数目对应, 表示是否为`一群物体`
entry['is_crowd'] = np.empty((0), dtype=np.bool)
# shape大小与roi相关, 将每一个roi映射到一个index上
# index是在entry['gt_classes']>0的rois列表的下标 TODO还是不太清楚映射关系
entry['box_to_gt_ind_map'] = np.empty((0), dtype=np.int32)
# 关键点信息, 默认情况下不设置
if self.keypoints is not None:
entry['gt_keypoints'] = np.empty(
(0, 3, self.num_keypoints), dtype=np.int32
)
# 删除那些从json file中获取到的不需要的字段
for k in ['date_captured', 'url', 'license', 'file_name']:
if k in entry:
del entry[k]
_add_gt_annotations()
方法
加载标注文件的函数 _add_gt_annotations()
的实现解析1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120# detectron/datasets/json_dataset.py
class JsonDataset(object):
def __init__(...):
#...
def get_roidb(...):
#...
def _prep_roidb_entry(self, entry):
#...
# 该函数将标注文件的元数据添加到roidb entry中
def _add_gt_annotations(self, entry):
# 获取指定imgid的annid列表 (对应多个box)
ann_ids = self.COCO.getAnnIds(imgIds=entry['id'], iscrowd=None)
# 根据annids的id列表, 获取这些id对应的标注信息, objs是一个列表
# 列表中的每一个元素都是一个字典,字典的内容是标注文件中的内容,包含bbox,segmentation等字段
objs = self.COCO.loadAnns(ann_ids)
# 下面的代码会对bboxes进行清洗, 因为有些是无效的数据
valid_objs=[] # 存储有效的objs
valid_segms=[] # 存储有效的segms
width = entry['width'] # 获取entry中的width字段, 代表图片的宽度
height = entry['height'] # 获取entry中的height字段, 代表图片的高度
for obj in objs:
# crowd区域采用RLE编码
# import detectron.utils.segms as segm_utils
# 用于判断当前的segmentation是polygon编码还是rle编码, 前者是列表类型, 后者是字典类型
# 返回True为polygon编码, 返回Fasle为rle编码
if segm_utils.is_poly(obj['segmentation']):
# poly编码必须含有>=3个点才能组成一个多边形, 因此需要>=6个坐标点
# 类似于这样的检查操作只在PLOYGON中存在, 在面对RLE时无需检查, 可以直接接受后面的检查
obj['segmentation'] = [
p for p in obj['segmentation'] if len(p) >=6
]
if obj['area'] < cfg.TRAIN.GT_MIN_AREA:
continue # 如果面积不达标, 则认为该标注无效, 不将其加入valid列表
if 'ignore' in obj and obj['ignore'] == 1:
continue
# import detectron.utils.boxes as box_utils
# 将[x1,y1,w,h]的边框格式转换成[x1,y1,x2,y2]的格式
x1, y1, x2, y2 = box_utils.xywh_to_xyxy(obj['bbox'])
# 将[x1,y1,x2,y2]的边框坐标限制在图片的[width,height]范围内, 防止越界
x1, y1, x2, y2 = box_utils.clip_xyxy_to_image(
x1, y1, x2, y2, height, width
)
if obj['area'] > 0 and x2 > x1 and y2 > y1: # 若数据有效, 则加入到列表当中
obj['clean_bbox'] = [x1, y1, x2, y2]
valid_objs.append(obj)
valid_segms.append(obj['segmentation']) # 将数据的segms存在列表中(RLE/PLOYGON)
num_valid_objs = len(valid_objs) # num_valid_objs持有objs的有效个数
# 注意, 下面的数据内容都被初始化为0
# boxes为 有效objs数×4 的numpy数组, 用来表示每个objs的边框坐标
boxes = np.zeros((num_valid_objs,4), dtype=entry['seg_areas'].dtype)
# 每个objs的真实类别
gt_classes = np.zeros((num_valid_objs), dtype=entry['gt_classes'].dtype)
gt_overlaps = np.zeros( # 形状为 有效objs数×num_class数 的numpy数组, 表示与每个类的IoU大小
(num_valid_objs, self.num_classes),
dtype=entry['gt_overlaps'].dtype
)
# 掩膜面积
seg_areas = np.zeros((num_valid_objs), dtype=entry['seg_areas'].dtype)
# 是否crowd
is_crowd = np.zeros((num_valid_objs), dtype=entry['is_crowd'].dtype)
# 这个是???
box_to_gt_ind_map = np.zeros(
(num_valid_objs), dtype=entry['box_to_gt_ind_map'].dtype
)
if self.keypoints is not None:
gt_keypoints = np.zeros(
(num_valid_objs, 3, self.num_keypoints),
dtype=entry['gt_keypoints'].dtype
)
# 图片是否有可视的关键点?
im_has_visible_keypoints = False
for ix, obj in enumerate(valid_objs):# ix为下标, obj为下标对应元素
# category_id为coco类别id,json_category_id_to_contiguous_id 为字典类型
# 其中, key为coco的非连续id, value为1~80的连续id, 均为整数, 所以这里是将coco的非连续id转换成对应的连续id
cls = self.json_category_id_to_contiguous_id[obj['category_id']]
boxes[ix, :] = obj['clean_box'] # 将当前obj的box填入boxes列表
gt_classes[ix] = cls # 将连续id填入gt_classes列表
seg_areas[ix] = obj['area'] # 将area填入seg_areas列表
is_crowd[ix] = obj['iscrowd']
box_to_gt_ind_map[ix] = ix # 该列表存储着box的顺序下标值
if self.keypoints is not None:
# ...
if obj['iscrowd']:
# 如果当前物体是crowd的话, 则将所有类别的overlap都设置为-1,
# 这样一来在训练的时候, 这些物体都会被排除在外!!
gt_overlaps[ix, :] = -1.0
else:
gt_overlaps[ix, cls] = 1.0 # 仅仅将对应类的overlap设置为1, 其他为0
# 将gt的boxes添加到entry中, 注意axis为0, 则会按照第0维拼接, 即最后是一个n×4的数组
# 注意, entry['boxes']初始时候是空的, 因此这就相当于是只添加了真实的框
entry['boxes'] = np.append(entry['boxes'], boxes, axis=0)
# 由于segms是以列表形式存储, 所以利用列表的extend方法来将valid_segms添加到其中
entry['segms'].extend(valid_segms)
# gt_classes的类型内一维numpy数组(维度为有效obj的数量), 因此这里不用指定axis的值, 直接按照一维数组拼接即可
entry['gt_classes'] = np.append(entry['gt_classes'], gt_classes)
# 同理, 一维numpy数组(维度为有效obj的数量), 无须指定axis的值
entry['seg_areas'] = np.append(entry['seg_areas'], seg_areas)
# gt_overlaps为 num_objs × num_classes的numpy数组, 表示每个obj与任意一个类的重叠度
# 因为entry['gt_overlaps']的类型为scipy.sparse.csr.csr_matrix, 因此这里需要调用toarray方法将其转换成numpy数组, 然后再与gt_overlaps拼接,
#由于entry['gt_overlaps']的维度为 0 × 81 , 因此拼接后的维度为num_objs × num_classes的numpy数组
entry['gt_overlaps'] = np.append(
entry['gt_overlaps'].toarray(), gt_overlaps, axis=0
)
# 再将其包装成scipy.sparse.csr.csr_matrix类型
entry['gt_overlaps'] = scipy.sparse.csr_matrix(entry['gt_overlaps'])
# 一维numpy数组, 可直接拼接
entry['is_crowd'] = np.append(entry['is_crowd'], is_crowd)
# 该列表存储着box的顺序下标值, 同样是一维数组, 直接拼接
entry['box_to_gt_ind_map'] = np.append(
entry['box_to_gt_ind_map'], box_to_gt_ind_map
)
if self.keypoints is not None:
entry['gt_keypoints'] = np.append(
entry['gt_keypoints'], gt_keypoints, axis=0
)
entry['has_visible_keypoints'] = im_has_visible_keypoints
_add_proposals_from_file()
1 | # detectron/datasets/json_dataset.py |
续解combined_roidb_for_training()
方法
接下来, 重新回到刚才detectron/datasets/roidb.py
文件 的 combined_roidb_for_training
函数中, 继续往下看:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42# detectron/datasets/roidb.py
# 加载并连接一个或多个数据集的roidbs, along with optional object proposals
# 每个roidb entry都带有特定的元数据类型, 对其进行准备工作后进行训练
def combined_roidb_for_training(dataset_names, proposal_files):
def get_roidb(dataset_name, proposal_file): # 注意没有 's'
# from detectron.datasets.json_dataset import JsonDataset
# 可以看到, roidb 是利用JsonDataset类对象的get_roidb()方法获取的
# 注意gt参数是True, 所以表明加载的是训练集的真实数据及其标签
ds = JsonDataset(dataset_name)
roidb = ds.get_roidb(
gt=True,
proposal_file=proposal_file,
crowd_filter_thresh=cfg.TRAIN.CROWD_FILTER_THRESH
)
# 如果图片翻转属性为真, 则对加载好以后的数据集进行翻转操作
if cfg.TRAIN.USE_FLIPPED:
logger.info("Appending horizontally-flipped training examples...")
extend_with_flipped_entries(roidb, ds)
logger.info("Loaded dataset: {:s}".format(ds.name))
# 以上, 数据集加载操作完成, 将roidb数据结构返回
return roidb
if isinstance(dataset_names, basestring):
dataset_names=(dataset_names, )
if isinstance(proposal_files, basestring):
proposal_files = (proposal_files, )
if len(proposal_files) == 0:
proposal_files = (None, ) * len(dataset_names)
assert len(dataset_names) == len(proposal_files)
roidbs = [get_roidb(*args) for args in zip(dataset_names, proposal_files)]
roidb = roidbs[0]
for r in roidbs[1:]:
roidb.extend(r)
roidb = filter_for_training(roidb)
logger.info("Computing bounding-box regression targets...")
# 为训练bounding-box 回归其添加必要的information
add_bbox_regression_targets(roidb)
logger.info("done")
_compute_and_log_stats(roidb)
return roidb