MMDetection-数据准备
簡介
在本專欄的上一篇文章中,介紹了統籌控制整個MMDetection工作pipeline的配置文件,它是非常重要的。但是,其實也可以發現,配置文件里其實是一系列的鍵值對,這些類或者方法的具體定義其實都在mmdet這個文件夾下,它是MMDetection的核心。而熟悉PyTorch的都知道,數據和模型是整個工作流的兩個核心配置,本文和后一篇文章就針對MMDetection的數據準備和模型展開介紹。
數據集
數據集格式
首先,我們需要知道,目標檢測的兩個基準數據集(COCO和VOC)使用了兩種不同的數據集存放格式(data format),關于這兩種格式的具體介紹,我這里不細說了,不太了解的可以參考我的博文。而MMDetection其實已經對幾種常用的數據集格式進行了PyTorch的Dataset的封裝,COCO和VOC當然在其中。
自定義Dataset
熟悉PyTorch的都知道,我們使用沒有出現過的自定義數據集(或者數據集格式),就要構建對應的Dataset類,這個類其實是對數據集存儲格式解析、標注文件讀取、圖像文件讀取等流程定義的一套操作,這些類定義在mmdet文件夾下的datasets目錄下,如mmdet/datasets/voc.py,就是定義了對PASCAL VOC數據集的處理。
下面以VOC數據集為例,不妨直接來看其源碼,內容如下(我這里略去了在數據集上進行mAP評估的函數)。可以看到它的主要函數都繼承自XMLDataset,這是因為VOC數據集是以XML文件的形式存儲標注的,為了復用單獨將XMLDataset獨立出來。
@DATASETS.register_module() class VOCDataset(XMLDataset):CLASSES = ('aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car','cat', 'chair', 'cow', 'diningtable', 'dog', 'horse','motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train','tvmonitor')def __init__(self, **kwargs):super(VOCDataset, self).__init__(**kwargs)if 'VOC2007' in self.img_prefix:self.year = 2007elif 'VOC2012' in self.img_prefix:self.year = 2012else:raise ValueError('Cannot infer dataset year from img_prefix')來看下面這個XMLDataset的具體實現,我這里省略部分函數的具體實現,其中最為核心的是兩個函數load_annotations和get_ann_info,前者讀取所有圖像的id和size信息以列表信息存儲,后者則根據圖像的id具體來讀取圖像的所有目標邊界框信息和邊界框類別標簽。
@DATASETS.register_module() class XMLDataset(CustomDataset):def __init__(self, min_size=None, **kwargs):assert self.CLASSES or kwargs.get('classes', None), 'CLASSES in `XMLDataset` can not be None.'super(XMLDataset, self).__init__(**kwargs)self.cat2label = {cat: i for i, cat in enumerate(self.CLASSES)}self.min_size = min_sizedef load_annotations(self, ann_file):data_infos = []img_ids = mmcv.list_from_file(ann_file)for img_id in img_ids:filename = f'JPEGImages/{img_id}.jpg'xml_path = osp.join(self.img_prefix, 'Annotations',f'{img_id}.xml')tree = ET.parse(xml_path)root = tree.getroot()size = root.find('size')if size is not None:width = int(size.find('width').text)height = int(size.find('height').text)else:img_path = osp.join(self.img_prefix, 'JPEGImages','{}.jpg'.format(img_id))img = Image.open(img_path)width, height = img.sizedata_infos.append(dict(id=img_id, filename=filename, width=width, height=height))return data_infosdef _filter_imgs(self, min_size=32):passdef get_ann_info(self, idx):img_id = self.data_infos[idx]['id']xml_path = osp.join(self.img_prefix, 'Annotations', f'{img_id}.xml')tree = ET.parse(xml_path)root = tree.getroot()bboxes = []labels = []bboxes_ignore = []labels_ignore = []for obj in root.findall('object'):name = obj.find('name').textif name not in self.CLASSES:continuelabel = self.cat2label[name]difficult = int(obj.find('difficult').text)bnd_box = obj.find('bndbox')# TODO: check whether it is necessary to use int# Coordinates may be float typebbox = [int(float(bnd_box.find('xmin').text)),int(float(bnd_box.find('ymin').text)),int(float(bnd_box.find('xmax').text)),int(float(bnd_box.find('ymax').text))]ignore = Falseif self.min_size:assert not self.test_modew = bbox[2] - bbox[0]h = bbox[3] - bbox[1]if w < self.min_size or h < self.min_size:ignore = Trueif difficult or ignore:bboxes_ignore.append(bbox)labels_ignore.append(label)else:bboxes.append(bbox)labels.append(label)if not bboxes:bboxes = np.zeros((0, 4))labels = np.zeros((0, ))else:bboxes = np.array(bboxes, ndmin=2) - 1labels = np.array(labels)if not bboxes_ignore:bboxes_ignore = np.zeros((0, 4))labels_ignore = np.zeros((0, ))else:bboxes_ignore = np.array(bboxes_ignore, ndmin=2) - 1labels_ignore = np.array(labels_ignore)ann = dict(bboxes=bboxes.astype(np.float32),labels=labels.astype(np.int64),bboxes_ignore=bboxes_ignore.astype(np.float32),labels_ignore=labels_ignore.astype(np.int64))return anndef get_cat_ids(self, idx):pass但是上面兩個依此繼承的Dataset類并沒有出現我們PyTorch中最常出現的__getitem__()函數,它其實在XMLDataset繼承的CustomDataset中具體實現了,這個類也是MMDetection中所有自定義數據集類的根源,它其實定義了通用的目標檢測數據集處理的內容,包括__getitem__(idx)這個函數,按照id去讀取對應的標注和圖像(這里的圖像處理會經過一個稱為pipeline的處理,在MMDetection中,pipeline定義了一個序列的對圖像的處理過程)。
上面說了這么多,又以MMDetection對VOC數據集封裝的Dataset為例簡要介紹Dataset結構,就是想說明,若你要使用全新的數據集就需要為其定義符合CustomDataset接口的類,或者將現有數據集轉換為MMDetection已經支持的這幾種格式(如COCO和PASCAL VOC)或中間格式(middle format)。 官方推薦的是這種數據集轉換的方法,可選離線轉換或者在線轉換兩種方式,前者的意思是通過一個腳本重新組織數據集本地文件的格式,后者則是手寫一個新的Dataset類自定義其中讀取數據集部分的代碼,當訓練時一批一批的進行格式轉換。在MMDetection中,推薦離線轉為COCO格式,這樣一勞永逸并且用戶只需要修改配置文件中的數據集標注路徑和類別列表即可,而且,在MMDetection中,只支持COCO格式數據集的mask AP評估。
COCO基本標注格式如下,其中的必要項及其含義可以參考我關于COCO數據集格式解析的博客。不過,也不用擔心轉換數據集腳本的麻煩,大多數開源數據集都會提供轉換的工具箱代碼的,使用標注工具自己標注的數據集處理方式網上也有很多開源代碼。在MMDetection中,建議將離線數據集轉換的腳本放在根目錄下的tools/dataset_converters/目錄下,該目錄下官方已經預定義了將VOC數據集和Cityscapes數據集轉化為COCO格式的腳本。
'images': [{'file_name': 'COCO_val2014_000000001268.jpg','height': 427,'width': 640,'id': 1268},... ],'annotations': [{'segmentation': [[192.81,247.09,...219.03,249.06]], # if you have mask labels'area': 1035.749,'iscrowd': 0,'image_id': 1268,'bbox': [192.81, 224.8, 74.73, 33.43],'category_id': 16,'id': 42986},... ],'categories': [{'id': 0, 'name': 'car'},]自定義數據準備
我這里通過官方的腳本通過命令python tools/dataset_converters/pascal_voc.py /VOC/VOCdevkit/ -o /VOC/VOCdevkit/ --out-format coco將VOC數據集轉換為了COCO格式(由于VOC2012測試集沒有標注,因而需要修改官方腳本測試集轉換的部分),得到了一個合適的COCO格式的自定義數據集(只不過我這里自定義數據集以VOC為例),生成了如下的COCO格式的標注文件。
voc0712_train.json voc0712_trainval.json voc0712_val.json voc07_test.json voc07_train.json voc07_trainval.json voc07_val.json voc12_train.json voc12_trainval.json voc12_val.json到這里,我們就完成了數據集的預處理,現在要想使用該數據集訓練,還需要兩個步驟:修改自定義數據集的配置文件和檢查自定義數據集的標注。
先是修改配置文件,我們需要在配置文件中修改三處,第一處是dataset_type字段,將其修改為轉換為的預定于數據集類型,如CocoDataset;第二處是classes字段,這是一個所有目標類別的字符串列表(需要注意的是這里也未必需要所有類別,也可以是所有類別的子集上進行訓練,也是通過這個classes字段控制),配合這個修改,data字段下具體的train、val和test字段也要修改classes和文件目錄;第三處是model字段部分的num_classes數值,如VOC應該設置為20。例如,對于上面COCO格式的VOC,我們編寫的配置文件如下。
_base_ = ['../_base_/models/faster_rcnn_r50_fpn.py','../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py' ]model = dict(roi_head=dict(bbox_head=dict(num_classes=20)))dataset_type = 'CocoDataset' classes = ('aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat','chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person','pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor' ) data_root = '自己的數據集根目錄' img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) train_pipeline = [dict(type='LoadImageFromFile'),dict(type='LoadAnnotations', with_bbox=True),dict(type='Resize', img_scale=(1000, 600), keep_ratio=True),dict(type='RandomFlip', flip_ratio=0.5),dict(type='Normalize', **img_norm_cfg),dict(type='Pad', size_divisor=32),dict(type='DefaultFormatBundle'),dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']), ] test_pipeline = [dict(type='LoadImageFromFile'),dict(type='MultiScaleFlipAug',img_scale=(1000, 600),flip=False,transforms=[dict(type='Resize', keep_ratio=True),dict(type='RandomFlip'),dict(type='Normalize', **img_norm_cfg),dict(type='Pad', size_divisor=32),dict(type='ImageToTensor', keys=['img']),dict(type='Collect', keys=['img']),]) ] data = dict(samples_per_gpu=2,workers_per_gpu=2,train=dict(type=dataset_type,classes=classes,ann_file=data_root + 'voc0712_train.json',img_prefix=data_root,pipeline=train_pipeline),val=dict(type=dataset_type,classes=classes,ann_file=data_root + 'voc0712_val.json',img_prefix=data_root,pipeline=test_pipeline),test=dict(type=dataset_type,classes=classes,ann_file=data_root + 'voc0712_val.json',img_prefix=data_root,pipeline=test_pipeline)) evaluation = dict(interval=1, metric='bbox')接著,我們應該檢查自定義數據集的標注是否合法,因為標注不準確,一切的訓練都是無效的。下面是一個合法的COCO標注格式示例。我們也可以通過官方提供的簡單的可視化腳本來檢查標注讀下來是否正確,具體為執行python tools/misc/browse_dataset.py config.py命令即可,通過可視化的如下圖結果,可以確定標注應該是沒什么問題的。
'annotations': [{'segmentation': [[192.81,247.09,...219.03,249.06]], # if you have mask labels'area': 1035.749,'iscrowd': 0,'image_id': 1268,'bbox': [192.81, 224.8, 74.73, 33.43],'category_id': 16,'id': 42986},... ],# MMDetection automatically maps the uncontinuous `id` to the continuous label indices. 'categories': [{'id': 1, 'name': 'a'}, {'id': 3, 'name': 'b'}, {'id': 4, 'name': 'c'}, {'id': 16, 'name': 'd'}, {'id': 17, 'name': 'e'},]官方教程這里還介紹了一種可用的中間格式數據集(middle format),我個人覺得這個用途不是特別廣泛,所以這里就跳過了。
數據集包裝
MMDetection還支持在現有數據集的基礎上進行包裝從而對現有數據集進行混合或者修改分布,目前MMDetection支持三種包裝方式,分別如下。
- RepeatDataset: 簡單重復整個數據集
- ClassBalancedDataset: 以類別平衡的方式重復數據集
- ConcatDataset: 級聯數據集
對第一個RepeatDataset的使用方式也很簡單,假定原始數據集為A,那么為了重復只需要給定重復次數times即可,應當構建如下的data.train字段。
dataset_A_train = dict(type='RepeatDataset',times=N,dataset=dict( # This is the original config of Dataset_Atype='Dataset_A',...pipeline=train_pipeline))對第二個ClassBalancedDataset,這是一種基于類別頻率的重復數據集的方式,使用該方法要求定義的Dataset類實現了get_cat_ids(idx)方法,該數據集構建需要指定閾值oversample_thr即可,它會修改原始數據集使得類別接近平衡,示例如下。
dataset_A_train = dict(type='ClassBalancedDataset',oversample_thr=1e-3,dataset=dict( # This is the original config of Dataset_Atype='Dataset_A',...pipeline=train_pipeline))最后,重點提一下關鍵的ConcatDataset,我們之前的VOC0712其實就是兩個數據集級聯而成的,數據集的級聯有三種情況,分別敘述。
-
若需要級聯的數據集標注是同種格式如VOC2007和VOC2012,則可以采用如下方式級聯,這樣構建的數據集默認在各個子數據集上評估,如果要在整個級聯后的數據集上評估,應當將下面的separate_eval設為False。
dataset_A_train = dict(type='Dataset_A',ann_file = ['anno_file_1', 'anno_file_2'],pipeline=train_pipeline,separate_eval=True, ) -
若需要級聯的數據集格式不同,需要類似下面這種方式進行級聯,它同樣和上面一樣支持separate_eval的修改。
dataset_A_train = dict() dataset_B_train = dict()data = dict(imgs_per_gpu=2,workers_per_gpu=2,train = [dataset_A_train,dataset_B_train],val = dataset_A_val,test = dataset_A_test) -
同樣也支持如下顯式的級聯數據集,
dataset_A_val = dict() dataset_B_val = dict()data = dict(imgs_per_gpu=2,workers_per_gpu=2,train=dataset_A_train,val=dict(type='ConcatDataset',datasets=[dataset_A_val, dataset_B_val],separate_eval=False))
數據管道
在MMDetection中,數據準備pipeline和數據集的定義是解耦開的,一個Dataset定義了如何處理標注文件而一個Pipeline則定義了一套準備數據字典(這個數據字典就是Dataset對象提供的item)的步驟,因此它通常是一個操作序列,每一步的操作都輸入一個字典輸出一個新字典用于下一步的轉換。
上圖是一個經典的pipeline,藍框是操作名,隨著數據在pipeline內的流動,每個操作會向原有的字典中添加新的鍵(圖中的綠色部分)或者更新已有的鍵(圖中的黃色部分)。
這些所有的pipeline中的操作分為數據加載、預處理、格式化和測試時數據增強,
下面是一個經典的Faster R-CNN的pipeline配置,具體操作的參數和函數用途及對字典的修改可以查看官網教程。
img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) train_pipeline = [dict(type='LoadImageFromFile'),dict(type='LoadAnnotations', with_bbox=True),dict(type='Resize', img_scale=(1333, 800), keep_ratio=True),dict(type='RandomFlip', flip_ratio=0.5),dict(type='Normalize', **img_norm_cfg),dict(type='Pad', size_divisor=32),dict(type='DefaultFormatBundle'),dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']), ] test_pipeline = [dict(type='LoadImageFromFile'),dict(type='MultiScaleFlipAug',img_scale=(1333, 800),flip=False,transforms=[dict(type='Resize', keep_ratio=True),dict(type='RandomFlip'),dict(type='Normalize', **img_norm_cfg),dict(type='Pad', size_divisor=32),dict(type='ImageToTensor', keys=['img']),dict(type='Collect', keys=['img']),]) ]當然,pipeline也是支持自定義的,它以字典作為輸入也以字典作為輸出,同樣要遵循注冊(registry)機制才可用,分以下三步。
定義my_pipeline.py。
from mmdet.datasets import PIPELINES@PIPELINES.register_module() class MyTransform:def __call__(self, results):results['dummy'] = Truereturn results ``**導入自定義的pipeline。**```python from .my_pipeline import MyTransform在配置文件中加入該操作。
img_norm_cfg = dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) train_pipeline = [dict(type='LoadImageFromFile'),dict(type='LoadAnnotations', with_bbox=True),dict(type='Resize', img_scale=(1333, 800), keep_ratio=True),dict(type='RandomFlip', flip_ratio=0.5),dict(type='Normalize', **img_norm_cfg),dict(type='Pad', size_divisor=32),dict(type='MyTransform'),dict(type='DefaultFormatBundle'),dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']), ]總結
本文主要介紹了MMDetetion的數據準備相關的內容,數據是模型訓練的關鍵,這部分對應的官方文檔這里給出鏈接。最后,如果我的文章對你有所幫助,歡迎點贊收藏評論一鍵三連,你的支持是我不懈創作的動力。
總結
以上是生活随笔為你收集整理的MMDetection-数据准备的全部內容,希望文章能夠幫你解決所遇到的問題。