1. 程式人生 > >Detectron原始碼解讀-roidb資料結構

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,