Detectron原始碼解讀-roidb資料結構
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
, 這是貫穿整個訓練過程的訓練資料, 故我們對此函式進行分析. 該函式程式碼解析如下:
# 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):
# 注意 dataset_name 沒有 's'
# from detectron.datasets.json_dataset import JsonDataset
# 可以看到, roidb 是利用JsonDataset類物件的get_roidb()方法獲取的
# 因此, 我們先在下面看一下這個類的實現細節
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))
return roidb
if isinstance(dataset_names, basestring):
#...
#...
get_roidb()
方法
在上面的函式中我們可以發現, combined_roidb_for_training
函式內部又定義了另一個函式get_roidb()
, 而該函式主要是基於detectron/datasets/json_dataset.py
中的JsonDataset
類及該類的成員方法get_roidb
實現的, 因此, 我們先跳到json_dataset.py
檔案中去看看這個類的內部實現是怎樣的:
# detectron/datasets/json_dataset.py
class JsonDataset(object):
# 這個類的設計主要是基於COCO的json格式資料集
# 當我們需要訓練自己的資料集時, 最好的方式就是將自己的資料集的格式改為
# COCO資料集的json格式, 這樣一來, 我們就無需重寫資料載入程式碼了.
def __init__(self, name):
assert dataset_catalog.contains(name), \
"Unknown dataset name: {}".format(name)
assert...
#...
# 準備資料集的類別資訊
category_ids = self.COCO.getCatIds() # 1~80, 對應80個類
# coco的loadCats函式, 必須指定需要載入的cat的id, 否則返回空列表
# 若指定後, 則返回id對應的類別資訊, 每個類別資訊是一個字典, 包括'name','id','supercategory'三個欄位
# 獲取每個類的名字, person, bicycle,bus等等, 返回的名字在列表中的位置與id在cat_ids列表中的位置一致
categories = [c['name'] for c in sefl.COCO.loadCats(category_ids)]
# 建立類別的name 與 id之間的對應關係, 其中cat_name為key,cat_id為值
self.category_to_id_map = dict(zip(categories, category_ids)) # 注意, 沒有'__background__'
self.classes = ['__background__'] + categories # 將'__background__'新增到categories類別名字列表中
self.num_classes = len(self.classes)
# coco下標最大值為90,但實際上只有80個類, 有的地方跳過了, 因此id不是連續的,
self.json_category_id_to_contiguous_id = {
v: i + 1 # key為coco的非連續id, value為1~80的連續id, 均為整數
for i, v in enumerate(self.COCO.getCatIds())
}
self.contiguous_category_id_to_json_id = {
v: k # key為1~80的連續id, value為coco的非連續id, 均為整數
for k, v in self.json_category_id_to_contiguous_id.items()
}
self._init_keypoints() # 呼叫類內的keypoints初始化方法.
def get_roidb(
self,
gt=False,
proposal_file=None,
min_proposal_size=2,
proposal_limit=-1,
crowd_filter_thresh=0
):
"""
返回json dataset對應的roidb資料, 提供以下四種選項:
- 在roidb中包含gt boxes
- 新增位於proposal file裡面的特定proposals
- 基於最短邊長的proposals過濾器
- 基於群落區域交集的proposals過濾器
"""
assert gt is True or crowd_filter_thresh == 0, \
"Crowd filter threshold must be 0 if gt " \
"annotations are not included."
# 這裡呼叫了COCO的官方API, 關於COCO資料集的結構和標註格式解析, 可以檢視我的另一篇文章
# 沒有指定篩選條件, 獲取資料集標籤中所有的圖片id
image_ids = self.COCO.getImgIds()
image_ids.sort() # 將id按照從小到大的順序排列
# roidb為列表結構, 列表中的每一項是一個字典, 代表著對應imageid的標籤內容.
# 鍵值包括:coco_url, license, width, filename, height, flickr_url, id, date_captured
roidb = copy.deepcopy(self.COCO.loadImgs(image_ids))
for entry in roidb:
# 呼叫了本類的私有函式 _prep_roidb_entry(), entry為字典.
# 主要是為entry賦初值, 佔位符等等, 包含box, segms,等各種欄位, 詳細資訊可以看下面的函式解析
# 注意, 這裡的欄位值都是預測值相關的值, 因此也會局域gt_overlap等欄位
self._prep_roidb_entry(entry)
if gt:
# 如果引數宣告是gt資訊, 則會呼叫_add_gt_annotations
# 訪問標註檔案, 以便新增相關欄位資訊, 具體看下面的相關函式解析
self.debug_timer.tic()
for entry in roidb:
# 注意, 是單獨對每個entry呼叫該函式, 因此每次會載入指定imgid的相關標籤
# 關於_add_gt_annotations函式具體解析可以看後面的部分
self._add_gt_annotations(entry)
logger.debug(
'_add_gt_annotations took {:.3f}s'.
format(self.debug_timer.toc(average=False))
)
if proposal_file is not None:
self.debug_timer.tic()
# 載入proposals檔案到roidb中, 關於此函式的詳細解析可以看後文
self._add_proposals_from_file(
roidb, proposal_file, min_proposal_size, proposal_limit,
crowd_filter_thresh
)
logger.debug(
'_add_proposals_from_file took {:.3f}s'.
format(self.debug_timer.toc(average=False))
)
# 類外部的函式, 用於計算與每個roidb相關的box的類別
_add_class_assignments(roidb)
return roidb
_prep_roidb_entry()
方法
資料準備函式 _prep_roidb_entry()
的實現解析
# 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()
的實現解析
# 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()
# detectron/datasets/json_dataset.py
class JsonDataset(object):
def __init__(...):
#...
def get_roidb(...):
#...
def _prep_roidb_entry(self, entry):
#...
def _add_gt_annotations(self, entry):
#...
#
def _add_proposals_from_file(
self, roidb, proposal_file, min_proposal_size, top_k, crowd_thresh
):
續解combined_roidb_for_training()
方法
接下來, 重新回到剛才detectron/datasets/roidb.py
檔案 的 combined_roidb_for_training
函式中, 繼續往下看:
# 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,