Hồ Chí Minh, 16-08-2023 [Nguyễn Xuân Quang](https://github.com/Tohew), [Võ Duy Nguyên](https://nguyenvd-uit.github.io/), [UIT-Together Research Group](https://uit-together.github.io/) # MMRotate: Installation of Oriented RepPoints ## Mục Lục [TOC] ## Step 1. Cài đặt môi trường ### Step 1.1. Tạo môi trường anaconda và cài đặt PyTorch trên GPU platforms Đặt tên theo cú pháp: Ten_viet tat cua ho va chu lot VD: Nguyen Xuan Quang -> Quangnx ```gherkin= conda create -n Quangnx1 python=3.7 pytorch==1.7.0 cudatoolkit=10.1 torchvision -c pytorch -y ``` ![image](https://hackmd.io/_uploads/S1lAjttqA.png) ### Step 1.2. Kích hoạt môi trường vừa tạo ```gherkin= conda activate Quangnx1 ``` ![image](https://hackmd.io/_uploads/HkQG2tYcC.png) ## Step 2. Cài đặt MMEngine và MMCV sử dụng MIM ```gherkin= pip install openmim mim install mmcv-full mim install mmdet mim uninstall mmcv-full mim install mmcv-full ``` Hình ảnh cài đặt thành công openmim ![image](https://hackmd.io/_uploads/ryMbnYFcR.png) Hình ảnh cài đặt thành công mmcv ![image](https://hackmd.io/_uploads/HJGvnYF90.png) Hình ảnh cài đặt thành công mmdet ![image](https://hackmd.io/_uploads/r1gu2tYq0.png) ## Step 3. Thao tác với MMRotate Truy cập vào thư mục luutru VD: /home/u2301/luutru/ ```gherkin= cd luutru/ ``` Tạo thư mục tương ứng với tên môi trường bên trên ![image](https://hackmd.io/_uploads/BJ8onYFcC.png) ```gherkin= cd Quangnx1/ ``` ### Step 3.1. Cài đặt MMRotate Tại thư mục này thực hiện clone và cài đặt mmdetection ```gherkin= git clone https://github.com/open-mmlab/mmrotate.git cd mmrotate pip install -r requirements/build.txt pip install -v -e . ``` Hình ảnh clone thành công ![image](https://hackmd.io/_uploads/SykA2FK9A.png) ### Step 3.2. Tải file config và checkpoints ```gherkin= mim download mmrotate --config oriented_rcnn_r50_fpn_1x_dota_le90 --dest . ``` Hình ảnh tải thành công file config va checkpoints ![image](https://hackmd.io/_uploads/H1M16tY90.png) ![image](https://hackmd.io/_uploads/SyNbTFFqR.png) ## Verify the installation **Verify the inference demo** ```gherkin= python demo/image_demo.py demo/demo.jpg oriented_rcnn_r50_fpn_1x_dota_le90.py oriented_rcnn_r50_fpn_1x_dota_le90-6d2b2ce0.pth --out-file result.jpg ``` Kết quả được lưu trong thư mục mmrotate Vd: /home/u2301/luutru/UITTogether/mmrotate/ ![image](https://hackmd.io/_uploads/SJM46tK5C.png) Tạo thư mục checkpoints ```gherkin= mkdir ./checkpoints ``` Tải checkpoints của file config oriented_reppoints_r50_fpn_1x_dota_le135 ```gherkin= mim download mmrotate --config oriented_reppoints_r50_fpn_1x_dota_le135 --dest ./checkpoints ``` Hình ảnh tải thành công file checkpoints ![image](https://hackmd.io/_uploads/S1NSaKY9R.png) Tạo thư mục data trong thư mục mmrotate để chứa shortcut dẫn tới thư mục DOTAv1 ![image](https://hackmd.io/_uploads/r1XLTKFcR.png) Sử dụng chung thư mục DOTAv1 nên chúng ta sẽ tạo 1 shortcut dẫn tới thư mục DOTAv1 dùng chung bằng lệnh: ln -s /duong dan toi thu muc goc /duong dan toi thu muc luu shortcut ```gherkin= ln -s /home/cvpr2023/LuuTru/dataset/dotaShipDataset/ /home/cvpr2023/LuuTru/Quangnx1/mmrotate/data/ ``` ![image](https://hackmd.io/_uploads/rJ3wptt5C.png) **Test với dữ liệu DOTAv1** ```gherkin= python ./tools/test.py \ configs/oriented_reppoints/oriented_reppoints_r50_fpn_1x_dota_le135.py \ checkpoints/oriented_reppoints_r50_fpn_1x_dota_le135-ef072de9.pth \ --show-dir work_dirs/vis ``` ## Với bộ dữ liệu SODA-A Phần này sẽ hướng dẫn thực nghiệm phương pháp Oriented RepPoints trên bộ dữ liệu SODA-A ### Step 1: Tải bộ dữ liệu Tải bộ dữ liệu ở đây: [SODA-A](https://shaunyuan22.github.io/SODA/) Bộ dữ liệu được tổ chức như sau: ├── dataset │ ├── SODA-A │ │ ├── Images │ │ │ ├── train │ │ │ ├── val │ │ │ ├── test │ │ ├── Annotations │ │ │ ├── train │ │ │ ├── val │ │ │ ├── test ### Step 2: Split SODA-A Hình ảnh gốc sẽ được cắt thành các hình ảnh nhỏ hơn có kích thước 800 x 800 điểm ảnh với độ dời là 150. ```gherkin= python tools/data/sodaa/sodaa_split.py --base-json sodaa_train.json ``` Trước khi chạy lệnh trên cần đổi đường dẫn của ```gherkin= img_dirs, ann_dirs, save_dir ``` trong file json. VD: File sodaa_train.json sau khi thay đổi sẽ có dạng như sau: ```gherkin= { "nproc": 10, "img_dirs": [ "/storageStudents/nguyenvd/dataset/SODA-A/Images/train/" ], "ann_dirs": [ "/storageStudents/nguyenvd/dataset/SODA-A/Annotations/train/" ], "sizes": [ 800 ], "gaps": [ 150 ], "rates": [ 1.0 ], "img_rate_thr": 0.6, "iof_thr": 0.7, "no_padding": false, "padding_value": [ 104, 116, 124 ], "save_dir": "data/split_sodaa/train/", "save_ext": ".jpg" } ``` Tương tự đối với sodaa_test.json và sodaa_val.json Thư mục sau khi tách nên tổ chức như sau: ├── data │ ├── split_sodaa │ │ ├── Images │ │ │ ├── train │ │ │ ├── val │ │ │ ├── test │ │ ├── Annotations │ │ │ ├── train │ │ │ ├── val │ │ │ ├── test ### Step 3: Tạo file dataset_base của SODA-A Trong thư mục mmrorate/configs/_base_/dataset tạo file "sodaa.py" để các file config chạy trên bộ dữ liệu SODA-A kế thừa từ file này ![image](https://hackmd.io/_uploads/HkTOpFY9R.png) file sodaa.py có dạng như sau: ```gherkin= # dataset settings dataset_type = 'SODAADataset' data_root = "/storageStudents/nguyenvd/quangnx/mmrotate/data/split_sodaa/" 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='RResize', img_scale=(1200, 1200)), dict(type='RRandomFlip', 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=(1200, 1200), flip=False, transforms=[ dict(type='RResize'), dict(type='Normalize', **img_norm_cfg), dict(type='Pad', size_divisor=32), dict(type='DefaultFormatBundle'), dict(type='Collect', keys=['img']) ]) ] data = dict( samples_per_gpu=2, workers_per_gpu=2, train=dict( type=dataset_type, ann_file=data_root + 'Annotations/train/', img_prefix=data_root + 'Images/train/', pipeline=train_pipeline, ori_ann_file="/storageStudents/nguyenvd/dataset/SODA-A/Annotations/train/" ), val=dict( type=dataset_type, ann_file=data_root + 'Annotations/val/', img_prefix=data_root + 'Images/val/', pipeline=test_pipeline, ori_ann_file="/storageStudents/nguyenvd/dataset/SODA-A/Annotations/val/" ), test=dict( type=dataset_type, ann_file=data_root + 'Annotations/test/', img_prefix=data_root + 'Images/test/', pipeline=test_pipeline, ori_ann_file="/storageStudents/nguyenvd/dataset/SODA-A/Annotations/test/" )) ``` Trong đó ```gherkin= data_root: Đường dẫn tới thư mục chứa bộ dữ liệu đã split. ann_file: Đường dẫn tới Annotations đã split. img_prefix: Đường dẫn tới Images đã split. Lưu ý: ori_ann_file: Đường dẫn tới Annotations của bộ dữ liệu gốc. ``` ### Step 4: Tạo file config của phương pháp Oriented RepPoints File config có dạng như sau: ```gherkin= _base_ = [ '../_base_/datasets/sodaa.py', '../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py' ] angle_version = 'le135' norm_cfg = dict(type='GN', num_groups=32, requires_grad=True) model = dict( type='RotatedRepPoints', backbone=dict( type='ResNet', depth=50, num_stages=4, out_indices=(0, 1, 2, 3), frozen_stages=1, zero_init_residual=False, norm_cfg=dict(type='BN', requires_grad=True), norm_eval=True, style='pytorch', init_cfg=dict(type='Pretrained', checkpoint='torchvision://resnet50')), neck=dict( type='FPN', in_channels=[256, 512, 1024, 2048], out_channels=256, start_level=1, add_extra_convs='on_input', num_outs=5, norm_cfg=norm_cfg), bbox_head=dict( type='OrientedRepPointsHead', num_classes=15, in_channels=256, feat_channels=256, point_feat_channels=256, stacked_convs=3, num_points=9, gradient_mul=0.3, point_strides=[8, 16, 32, 64, 128], point_base_scale=2, norm_cfg=norm_cfg, loss_cls=dict( type='FocalLoss', use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), loss_bbox_init=dict(type='ConvexGIoULoss', loss_weight=0.375), loss_bbox_refine=dict(type='ConvexGIoULoss', loss_weight=1.0), loss_spatial_init=dict(type='SpatialBorderLoss', loss_weight=0.05), loss_spatial_refine=dict(type='SpatialBorderLoss', loss_weight=0.1), init_qua_weight=0.2, top_ratio=0.4), # training and testing settings train_cfg=dict( init=dict( assigner=dict(type='ConvexAssigner', scale=4, pos_num=1), allowed_border=-1, pos_weight=-1, debug=False), refine=dict( assigner=dict( type='MaxConvexIoUAssigner', pos_iou_thr=0.1, neg_iou_thr=0.1, min_pos_iou=0, ignore_iof_thr=-1), allowed_border=-1, pos_weight=-1, debug=False)), test_cfg=dict( nms_pre=2000, min_bbox_size=0, score_thr=0.05, nms=dict(iou_thr=0.4), max_per_img=2000)) 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='RResize', img_scale=(1024, 1024)), dict( type='RRandomFlip', flip_ratio=[0.25, 0.25, 0.25], direction=['horizontal', 'vertical', 'diagonal'], version=angle_version), dict(type='Normalize', **img_norm_cfg), dict(type='Pad', size_divisor=32), dict(type='DefaultFormatBundle'), dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']) ] data = dict( train=dict(pipeline=train_pipeline, version=angle_version), val=dict(version=angle_version), test=dict(version=angle_version)) optimizer = dict(lr=0.008) checkpoint_config = dict(interval=1) ``` ### Step 5: Train với file config vừa tạo Để có thể train và hiển thị eval sau mỗi epoch, trước hết cần tạo file sodaa.py trong thư mục mmrorate/datasets và file sodaa_eval.py trong thư mục sodaa_eval. ![image](https://hackmd.io/_uploads/SJ6FpKt9C.png) File sodaa.py có dạng như sau: ```gherkin= import itertools import logging import os import os.path as osp import tempfile import re import time import glob import json import csv import torch import mmcv import numpy as np from collections import defaultdict from functools import partial from multiprocessing import Pool from mmcv.utils import print_log from terminaltables import AsciiTable from mmdet.core import eval_recalls from .builder import DATASETS from mmdet.datasets.custom import CustomDataset from mmcv.ops.nms import nms_rotated from mmrotate.core import poly2obb_np from mmrotate.core.evaluation import eval_rbbox_map from .sodaa_eval.sodaa_eval import SODAAeval import cv2 @DATASETS.register_module() class SODAADataset(CustomDataset): CLASSES = ('airplane', 'helicopter', 'small-vehicle', 'large-vehicle', 'ship', 'container', 'storage-tank', 'swimming-pool', 'windmill') # only foreground categories available def __init__(self, version, ori_ann_file, **kwargs): self.version = version super(SODAADataset, self).__init__(**kwargs) # self.ori_infos = self.load_ori_annotations(ori_ann_file) self.ori_data_infos = self.load_ori_annotations(ori_ann_file) self.cat_ids = self._get_cat_ids() def __len__(self): """Total number of samples of data.""" return len(self.data_infos) def _get_cat_ids(self): cat_ids = dict() for idx, cat in enumerate(self.CLASSES): cat_ids[idx] = cat return cat_ids def load_ori_annotations(self, ori_ann_folder): """ Load annotation info of raw images. """ ann_files = glob.glob(ori_ann_folder + '/*.json') ori_data_infos = [] for ann_file in ann_files: data_info = {} img_name = ann_file.replace('.json', '.jpg').split(os.sep)[-1] data_info['filename'] = img_name data_info['ann'] = {} gt_bboxes = [] gt_labels = [] gt_polygons = [] gt_bboxes_ignore = [] gt_labels_ignore = [] gt_polygons_ignore = [] if os.path.getsize(ann_file) == 0: continue f = json.load(open(ann_file, 'r')) annotations = f['annotations'] for ann in annotations: poly = np.array(ann['poly'], dtype=np.float32) if len(poly) > 8: continue # neglect those objects annotated with more than 8 polygons try: x, y, w, h, a = poly2obb_np(poly, self.version) except: # noqa: E722 continue label = int(ann['category_id']) # 0-index gt_bboxes.append([x, y, w, h, a]) gt_labels.append(label) gt_polygons.append(poly) if gt_bboxes: data_info['ann']['bboxes'] = np.array( gt_bboxes, dtype=np.float32) data_info['ann']['labels'] = np.array( gt_labels, dtype=np.int64) data_info['ann']['polygons'] = np.array( gt_polygons, dtype=np.float32) else: data_info['ann']['bboxes'] = np.zeros((0, 5), dtype=np.float32) data_info['ann']['labels'] = np.array([], dtype=np.int64) data_info['ann']['polygons'] = np.zeros((0, 8), dtype=np.float32) if gt_polygons_ignore: data_info['ann']['bboxes_ignore'] = np.array( gt_bboxes_ignore, dtype=np.float32) data_info['ann']['labels_ignore'] = np.array( gt_labels_ignore, dtype=np.int64) data_info['ann']['polygons_ignore'] = np.array( gt_polygons_ignore, dtype=np.float32) else: data_info['ann']['bboxes_ignore'] = np.zeros( (0, 5), dtype=np.float32) data_info['ann']['labels_ignore'] = np.array( [], dtype=np.int64) data_info['ann']['polygons_ignore'] = np.zeros( (0, 8), dtype=np.float32) ori_data_infos.append(data_info) self.ori_img_ids = [*map(lambda x: x['filename'].split(os.sep)[-1][:-4], ori_data_infos)] return ori_data_infos def get_ori_ann_info(self, idx): """Get annotation by index. Args: idx (int): Index of data. Returns: dict: Annotation info of specified index. """ return self.ori_data_infos[idx]['ann'] def load_annotations(self, ann_folder): """Load annotation from COCO style annotation file. Args: ann_folder (str): Directory that contains annotation file of SODA-A dataset. """ cls_map = {c: i for i, c in enumerate(self.CLASSES)} # 0-index ann_files = glob.glob(ann_folder + '/*.json') data_infos = [] for ann_file in ann_files: data_info = {} img_name = ann_file.replace('.json', '.jpg').split(os.sep)[-1] data_info['filename'] = img_name data_info['ann'] = {} gt_bboxes = [] gt_labels = [] gt_polygons = [] gt_bboxes_ignore = [] gt_labels_ignore = [] gt_polygons_ignore = [] if os.path.getsize(ann_file) == 0: continue f = json.load(open(ann_file, 'r')) annotations = f['annotations'] for ann in annotations: poly = np.array(ann['poly'], dtype=np.float32) try: x, y, w, h, a = poly2obb_np(poly, self.version) except: # noqa: E722 continue label = int(ann['cat_id']) # 0-index trunc = int(ann['trunc']) gt_bboxes.append([x, y, w, h, a]) gt_labels.append(label) gt_polygons.append(poly) if gt_bboxes: data_info['ann']['bboxes'] = np.array( gt_bboxes, dtype=np.float32) data_info['ann']['labels'] = np.array( gt_labels, dtype=np.int64) data_info['ann']['polygons'] = np.array( gt_polygons, dtype=np.float32) else: data_info['ann']['bboxes'] = np.zeros((0, 5), dtype=np.float32) data_info['ann']['labels'] = np.array([], dtype=np.int64) data_info['ann']['polygons'] = np.zeros((0, 8), dtype=np.float32) if gt_polygons_ignore: data_info['ann']['bboxes_ignore'] = np.array( gt_bboxes_ignore, dtype=np.float32) data_info['ann']['labels_ignore'] = np.array( gt_labels_ignore, dtype=np.int64) data_info['ann']['polygons_ignore'] = np.array( gt_polygons_ignore, dtype=np.float32) else: data_info['ann']['bboxes_ignore'] = np.zeros( (0, 5), dtype=np.float32) data_info['ann']['labels_ignore'] = np.array( [], dtype=np.int64) data_info['ann']['polygons_ignore'] = np.zeros( (0, 8), dtype=np.float32) data_infos.append(data_info) self.img_ids = [*map(lambda x: x['filename'].split(os.sep)[-1][:-4], data_infos)] return data_infos def _filter_imgs(self): """Filter images without ground truths.""" valid_inds = [] for i, data_info in enumerate(self.data_infos): if data_info['ann']['labels'].size > 0: valid_inds.append(i) return valid_inds def _set_group_flag(self): """Set flag according to image aspect ratio. All set to 0. """ self.flag = np.zeros(len(self), dtype=np.uint8) def fast_eval_recall(self, results, proposal_nums, iou_thrs, logger=None): gt_bboxes = [] for i in range(len(self.img_ids)): ann_ids = self.sodaa.get_ann_ids(img_ids=self.img_ids[i]) ann_info = self.sodaa.load_anns(ann_ids) if len(ann_info) == 0: gt_bboxes.append(np.zeros((0, 4))) continue bboxes = [] for ann in ann_info: if ann.get('ignore', False) or ann['iscrowd']: continue x1, y1, w, h = ann['bbox'] bboxes.append([x1, y1, x1 + w, y1 + h]) bboxes = np.array(bboxes, dtype=np.float32) if bboxes.shape[0] == 0: bboxes = np.zeros((0, 4)) gt_bboxes.append(bboxes) recalls = eval_recalls( gt_bboxes, results, proposal_nums, iou_thrs, logger=logger) ar = recalls.mean(axis=1) return ar def translate(self, bboxes, x, y): translated = bboxes.copy() translated[..., :2] = translated[..., :2] + \ np.array([x, y], dtype=np.float32) return translated def merge_det(self, results, with_merge=True, nms_iou_thr=0.5, nproc=10, save_dir=None, **kwargs): if mmcv.is_list_of(results, tuple): dets, segms = results else: dets = results # get patch results for evaluating if not with_merge: results = [(data_info['id'], result) for data_info, result in zip(self.data_infos, results)] # TODO: if save_dir is not None: pass return results print('\n>>> Merge detected results of patch for whole image evaluating...') start_time = time.time() collector = defaultdict(list) # ensure data_infos and dets have the same length for data_info, result in zip(self.data_infos, dets): filename = data_info['filename'] x_start, y_start = \ int(filename.split('___')[0].split('__')[-1]), \ int(filename.split('___')[-1].split('.')[0]) ori_name = filename.split('__')[0] # x_start, y_start = data_info['start_coord'] new_result = [] for i, res in enumerate(result): bboxes, scores = res[:, :-1], res[:, [-1]] bboxes = self.translate(bboxes, x_start, y_start) labels = np.zeros((bboxes.shape[0], 1)) + i new_result.append(np.concatenate( [labels, bboxes, scores], axis=1 )) new_result = np.concatenate(new_result, axis=0) collector[ori_name].append(new_result) merge_func = partial(_merge_func, CLASSES=self.CLASSES, iou_thr=nms_iou_thr) if nproc > 1: pool = Pool(nproc) merged_results = pool.map(merge_func, list(collector.items())) pool.close() else: merged_results = list(map(merge_func, list(collector.items()))) # TODO: if save_dir is not None: pass stop_time = time.time() print('Merge results completed, it costs %.1f seconds.'%(stop_time - start_time)) return merged_results def merge_det_dota(self, results, nproc=4): """Merging patch bboxes into full image. Args: results (list): Testing results of the dataset. nproc (int): number of process. Default: 4. """ print('\n>>> Merge detected results of patch for whole image evaluating...') collector = defaultdict(list) for data_info, result in zip(self.data_infos, results): filename = data_info['filename'] x_start, y_start = \ int(filename.split('___')[0].split('__')[-1]), \ int(filename.split('___')[-1].split('.')[0]) ori_name = filename.split('__')[0] new_result = [] for i, res in enumerate(result): bboxes, scores = res[:, :-1], res[:, [-1]] bboxes = bt.translate(bboxes, x_start, y_start) labels = np.zeros((bboxes.shape[0], 1)) + i new_result.append(np.concatenate( [labels, bboxes, scores], axis=1 )) new_result = np.concatenate(new_result, axis=0) collector[ori_name].append(new_result) merge_func = partial(_merge_func, CLASSES=self.CLASSES, iou_thr=0.1) if nproc <= 1: print('Single processing') merged_results = mmcv.track_iter_progress( (map(merge_func, collector.items()), len(collector))) else: print('Multiple processing') merged_results = mmcv.track_parallel_progress( merge_func, list(collector.items()), nproc) return merged_results def poly2obb(self, poly): """Convert polygons to oriented bounding boxes. Args: polys (ndarray): [x0,y0,x1,y1,x2,y2,x3,y3] Returns: obbs (ndarray): [x_ctr,y_ctr,w,h,angle] """ bboxps = np.array(poly).reshape((4, 2)) rbbox = cv2.minAreaRect(bboxps) x, y, w, h, a = rbbox[0][0], rbbox[0][1], rbbox[1][0], rbbox[1][1], rbbox[ 2] # if w < 2 or h < 2: # return a = a / 180 * np.pi if w < h: w, h = h, w a += np.pi / 2 while not np.pi / 2 > a >= -np.pi / 2: if a >= np.pi / 2: a -= np.pi else: a += np.pi assert np.pi / 2 > a >= -np.pi / 2 return x, y, w, h, a def evaluate(self, results, metric='mAP', logger=None, proposal_nums=(100, 300, 1000), iou_thr=None, scale_ranges=None, metric_items=None, nproc=4): """Evaluate the dataset. Args: results (list): Testing results of the dataset. metric (str | list[str]): Metrics to be evaluated. logger (logging.Logger | None | str): Logger used for printing related information during evaluation. Default: None. proposal_nums (Sequence[int]): Proposal number used for evaluating recalls, such as recall@100, recall@1000. Default: (100, 300, 1000). iou_thr (float | list[float]): IoU threshold. It must be a float when evaluating mAP, and can be a list when evaluating recall. Default: 0.5. scale_ranges (list[tuple] | None): Scale ranges for evaluating mAP. Default: None. nproc (int): Processes used for computing TP and FP. Default: 4. """ merged_results = self.merge_det(results, nproc=nproc) merge_idx = [self.ori_img_ids.index(res[0]) for res in merged_results] results = [res[1] for res in merged_results] # exclude `id` for evaluation if not isinstance(metric, str): assert len(metric) == 1 metric = metric[0] allowed_metrics = ['mAP'] if metric not in allowed_metrics: raise KeyError(f'metric {metric} is not supported') annotations = [self.get_ori_ann_info(i) for i in merge_idx] # evaluation if iou_thr is None: iou_thrs = np.linspace( .5, 0.95, int(np.round((0.95 - .5) / .05)) + 1, endpoint=True) eval_results = {} SODAAEval = SODAAeval(annotations, results, numCats=len(self.CLASSES), nproc=nproc) SODAAEval.params.iouThrs = iou_thrs # mapping of cocoEval.stats sodaa_metric_names = { 'AP': 0, 'AP_50': 1, 'AP_75': 2, 'AP_eS': 3, 'AP_rS': 4, 'AP_gS': 5, 'AP_Normal': 6, 'AR@20000': 7, 'AR_eS@20000': 8, 'AR_rS@20000': 9, 'AR_gS@20000': 10, 'AR_Normal@20000': 11 } SODAAEval.evaluate() SODAAEval.accumulate() SODAAEval.summarize() classwise = True if classwise: # Compute per-category AP # Compute per-category AP # from https://github.com/facebookresearch/detectron2/ precisions = SODAAEval.eval['precision'] # precision: (iou, recall, cls, area range, max dets) assert len(self.cat_ids) == precisions.shape[2] results_per_category = [] for catId, catName in self.cat_ids.items(): # area range index 0: all area ranges # max dets index -1: typically 20000 per image precision = precisions[:, :, catId, 0, -1] precision = precision[precision > -1] if precision.size: ap = np.mean(precision) else: ap = float('nan') results_per_category.append( (f'{catName}', f'{float(ap):0.3f}')) num_columns = min(6, len(results_per_category) * 2) results_flatten = list( itertools.chain(*results_per_category)) headers = ['category', 'AP'] * (num_columns // 2) results_2d = itertools.zip_longest(*[ results_flatten[i::num_columns] for i in range(num_columns) ]) table_data = [headers] table_data += [result for result in results_2d] table = AsciiTable(table_data) print_log('\n' + table.table, logger=logger) # TODO: proposal evaluation if metric_items is None: metric_items = [ 'AP', 'AP_50', 'AP_75', 'AP_eS', 'AP_rS', 'AP_gS', 'AP_Normal' ] for metric_item in metric_items: key = f'{metric}_{metric_item}' val = float( f'{SODAAEval.stats[sodaa_metric_names[metric_item]]:.3f}' ) eval_results[key] = val ap = SODAAEval.stats[:7] eval_results[f'{metric}_mAP_copypaste'] = ( f'{ap[0]:.3f} {ap[1]:.3f} {ap[2]:.3f} ' f'{ap[3]:.3f} {ap[4]:.3f} {ap[5]:.3f} ' f'{ap[6]:.3f} ' ) return eval_results def _merge_func(info, CLASSES, iou_thr): img_id, label_dets = info print(img_id) label_dets = np.concatenate(label_dets, axis=0) labels, dets = label_dets[:, 0], label_dets[:, 1:] ori_img_results = [] for i in range(len(CLASSES)): cls_dets = dets[labels == i] if len(cls_dets) == 0: ori_img_results.append(np.empty((0, dets.shape[1]), dtype=np.float32)) continue bboxes, scores = cls_dets[:, :-1], cls_dets[:, [-1]] bboxes = torch.from_numpy(bboxes).to(torch.float32).contiguous() scores = torch.from_numpy(np.squeeze(scores, 1)).to(torch.float32).contiguous() results, inds = nms_rotated(bboxes, scores, iou_thr) # If scores.shape=(N, 1) instead of (N, ), then the results after NMS # will be wrong. Suppose bboxes.shape=[N, 4], the results has shape # of [N**2, 5], results = results.numpy() ori_img_results.append(results) return img_id, ori_img_results def _merge_func_mm(info, CLASSES, iou_thr): """Merging patch bboxes into full image. Args: CLASSES (list): Label category. iou_thr (float): Threshold of IoU. """ img_id, label_dets = info label_dets = np.concatenate(label_dets, axis=0) labels, dets = label_dets[:, 0], label_dets[:, 1:] big_img_results = [] for i in range(len(CLASSES)): if len(dets[labels == i]) == 0: big_img_results.append(dets[labels == i]) else: try: cls_dets = torch.from_numpy(dets[labels == i]).cuda() except: # noqa: E722 cls_dets = torch.from_numpy(dets[labels == i]) nms_dets, keep_inds = nms_rotated(cls_dets[:, :5], cls_dets[:, -1], iou_thr) big_img_results.append(nms_dets.cpu().numpy()) return img_id, big_img_results ``` File sodaa_eval.py có dạng như sau: ```gherkin= __author__ = 'tsungyi' import copy import datetime import time from collections import defaultdict from multiprocessing import Pool from functools import partial import numpy as np import torch from mmcv.ops import box_iou_rotated class SODAAeval: def __init__(self, annotations=None, results=None, numCats=9, iouType='mAP', nproc=10): ''' Initialize CocoEval using coco APIs for gt and dt :param sodaaGt: coco object with ground truth annotations :param sodaaDt: coco object with detection results :return: None ''' if not iouType: print('iouType not specified. use default iouType: mAP') self.annotations = annotations # ground truth self.results = results # detections self.numCats = numCats self.nproc = nproc self.evalImgs = defaultdict( list) # per-image per-category evaluation results [KxAxI] elements self.eval = {} # accumulated evaluation results self._gts = defaultdict(list) # gt for evaluation self._dts = defaultdict(list) # dt for evaluation self.params = SODAAParams(iouType=iouType) # parameters self._paramsEval = {} # parameters for evaluation self.stats = [] # result summarization self.ious = {} # ious between all gts and dts # TODO: get ids if self.annotations is not None: self._getImgAndCatIds() def _getImgAndCatIds(self): self.params.imgIds = [i for i, _ in enumerate(self.annotations)] self.params.catIds = [i for i in range(self.numCats)] def _prepare(self): ''' Prepare ._gts and ._dts for evaluation based on params :return: None ''' p = self.params if p.useCats: # TODO: we do not specify the area sue to no-area split so far. gts = list() insId = 0 for i, imgAnn in enumerate(self.annotations): for j in range(len(imgAnn['labels'])): gt = dict( bbox = imgAnn['bboxes'][j], area = imgAnn['bboxes'][j][2] * imgAnn['bboxes'][j][3], category_id = imgAnn['labels'][j], image_id = i, id = insId, ignore = 0 # no ignore ) gts.append(gt) insId += 1 dts = list() insId = 0 for i, imgRes in enumerate(self.results): for j, catRes in enumerate(imgRes): if len(catRes) == 0: continue bboxes, scores = catRes[:, :5], catRes[:, -1] for k in range(len(scores)): dt = dict( image_id = i, bbox = bboxes[k], score = scores[k], category_id = j, id = insId, area = bboxes[k][2] * bboxes[k][3] ) dts.append(dt) insId += 1 else: # TODO: add class-agnostic evaluation codes pass self._gts = defaultdict(list) # gt for evaluation self._dts = defaultdict(list) # dt for evaluation for gt in gts: self._gts[gt['image_id'], gt['category_id']].append(gt) for dt in dts: self._dts[dt['image_id'], dt['category_id']].append(dt) self.evalImgs = defaultdict( list) # per-image per-category evaluation results self.eval = {} # accumulated evaluation results def evaluate(self): ''' Run per image evaluation on given images and store results (a list of dict) in self.evalImgs :return: None ''' # tic = time.time() p = self.params print('Evaluate annotation type *{}*'.format(p.iouType)) p.imgIds = list(np.unique(p.imgIds)) if p.useCats: p.catIds = list(np.unique(p.catIds)) p.maxDets = sorted(p.maxDets) self.params = p self._prepare() # loop through images, area range, max detection number catIds = p.catIds if p.useCats else [-1] if p.iouType == 'mAP': computeIoU = self.computeIoU else: raise Exception('unknown iouType for iou computation') print('Calculating IoUs...') tic = time.time() self.ious = {(imgId, catId): computeIoU(imgId, catId) for imgId in p.imgIds for catId in catIds} toc = time.time() print('IoU calculation Done (t={:0.2f}s).'.format(toc - tic)) print('Running per image evaluation...') tic = time.time() evaluateImg = self.evaluateImgPartial if self.nproc else self.evaluateImg maxDet = p.maxDets[-1] evaluateImgFunc = partial(evaluateImg) inteLst = [[imgId, catId, areaRng, maxDet] for catId in catIds for areaRng in p.areaRng for imgId in p.imgIds] imgIdLst, catIdLst, areaRngLst, maxDetLst = [], [], [], [] for lst in inteLst: imgIdLst.append(lst[0]) catIdLst.append(lst[1]) areaRngLst.append(lst[2]) maxDetLst.append(lst[3]) if self.nproc > 1: pool = Pool(self.nproc) contents = pool.map(evaluateImgFunc, zip(imgIdLst, catIdLst, areaRngLst, maxDetLst)) pool.close() else: contents = [evaluateImg(imgId, catId, areaRng, maxDet) for catId in catIds for areaRng in p.areaRng for imgId in p.imgIds] toc = time.time() print('DONE (t={:0.2f}s).'.format(toc - tic)) self.evalImgs = [c for c in contents] self._paramsEval = copy.deepcopy(self.params) def computeIoUPartial(self, args): imgId, catId = args p = self.params if p.useCats: gt = self._gts[imgId, catId] dt = self._dts[imgId, catId] else: gt = [_ for cId in p.catIds for _ in self._gts[imgId, cId]] dt = [_ for cId in p.catIds for _ in self._dts[imgId, cId]] if len(gt) == 0 and len(dt) == 0: return [] # sort dt highest score first inds = np.argsort([-d['score'] for d in dt], kind='mergesort') dt = [dt[i] for i in inds] if len(dt) > p.maxDets[-1]: dt = dt[0:p.maxDets[-1]] if p.iouType == 'mAP': g = [g['bbox'] for g in gt] d = [d['bbox'] for d in dt] else: raise Exception('unknown iouType for iou computation') # compute iou between each dt and gt region (rotated rectangle) ious = box_iou_rotated( torch.from_numpy(np.array(d)).float(), torch.from_numpy(np.array(g)).float()).numpy() return ious def computeIoU(self, imgId, catId): p = self.params if p.useCats: gt = self._gts[imgId, catId] dt = self._dts[imgId, catId] else: gt = [_ for cId in p.catIds for _ in self._gts[imgId, cId]] dt = [_ for cId in p.catIds for _ in self._dts[imgId, cId]] if len(gt) == 0 and len(dt) == 0: return [] # sort dt highest score first inds = np.argsort([-d['score'] for d in dt], kind='mergesort') dt = [dt[i] for i in inds] if len(dt) > p.maxDets[-1]: dt = dt[0:p.maxDets[-1]] if p.iouType == 'mAP': g = [g['bbox'] for g in gt] d = [d['bbox'] for d in dt] else: raise Exception('unknown iouType for iou computation') # compute iou between each dt and gt region (rotated rectangle) ious = box_iou_rotated( torch.from_numpy(np.array(d)).float(), torch.from_numpy(np.array(g)).float()).numpy() return ious def evaluateImgPartial(self, args): ''' perform evaluation for single category and image :return: dict (single image results) ''' imgId, catId, aRng, maxDet = args p = self.params if p.useCats: gt = self._gts[imgId, catId] dt = self._dts[imgId, catId] else: gt = [_ for cId in p.catIds for _ in self._gts[imgId, cId]] dt = [_ for cId in p.catIds for _ in self._dts[imgId, cId]] if len(gt) == 0 and len(dt) == 0: return None # TODO: all to 0 for g in gt: if g['ignore'] or (g['area'] < aRng[0] or g['area'] > aRng[1]): g['_ignore'] = 1 else: g['_ignore'] = 0 # sort dt highest score first, sort gt ignore last gtind = np.argsort([g['_ignore'] for g in gt], kind='mergesort') gt = [gt[i] for i in gtind] dtind = np.argsort([-d['score'] for d in dt], kind='mergesort') dt = [dt[i] for i in dtind[0:maxDet]] iscrowd = [0 for o in gt] # [int(o['iscrowd']) for o in gt] # load computed ious ious = self.ious[imgId, catId][:, gtind] if len( self.ious[imgId, catId]) > 0 else self.ious[imgId, catId] T = len(p.iouThrs) G = len(gt) D = len(dt) gtm = np.zeros((T, G)) dtm = np.zeros((T, D)) gtIg = np.array([g['_ignore'] for g in gt]) dtIg = np.zeros((T, D)) if not len(ious) == 0: for tind, t in enumerate(p.iouThrs): for dind, d in enumerate(dt): # information about best match so far (m=-1 -> unmatched) iou = min([t, 1 - 1e-10]) m = -1 for gind, g in enumerate(gt): # if this gt already matched, and not a crowd, continue if gtm[tind, gind] > 0 and not iscrowd[gind]: continue # if dt matched to reg gt, and on ignore gt, stop if m > -1 and gtIg[m] == 0 and gtIg[gind] == 1: break # continue to next gt unless better match made if ious[dind, gind] < iou: continue # if match successful and best so far, store # appropriately iou = ious[dind, gind] m = gind # if match made store id of match for both dt and gt if m == -1: continue dtIg[tind, dind] = gtIg[m] dtm[tind, dind] = gt[m]['id'] gtm[tind, m] = d['id'] # set unmatched detections outside of area range to ignore a = np.array([d['area'] < aRng[0] or d['area'] > aRng[1] for d in dt]).reshape((1, len(dt))) dtIg = np.logical_or(dtIg, np.logical_and(dtm == 0, np.repeat(a, T, 0))) # store results for given image and category return { 'image_id': imgId, 'category_id': catId, 'aRng': aRng, 'maxDet': maxDet, 'dtIds': [d['id'] for d in dt], 'gtIds': [g['id'] for g in gt], 'dtMatches': dtm, 'gtMatches': gtm, 'dtScores': [d['score'] for d in dt], 'gtIgnore': gtIg, 'dtIgnore': dtIg, } def evaluateImg(self, imgId, catId, aRng, maxDet): ''' perform evaluation for single category and image :return: dict (single image results) ''' p = self.params if p.useCats: gt = self._gts[imgId, catId] dt = self._dts[imgId, catId] else: gt = [_ for cId in p.catIds for _ in self._gts[imgId, cId]] dt = [_ for cId in p.catIds for _ in self._dts[imgId, cId]] if len(gt) == 0 and len(dt) == 0: return None # TODO: all to 0 for g in gt: if g['ignore'] or (g['area'] < aRng[0] or g['area'] > aRng[1]): g['_ignore'] = 1 else: g['_ignore'] = 0 # sort dt highest score first, sort gt ignore last gtind = np.argsort([g['_ignore'] for g in gt], kind='mergesort') gt = [gt[i] for i in gtind] dtind = np.argsort([-d['score'] for d in dt], kind='mergesort') dt = [dt[i] for i in dtind[0:maxDet]] iscrowd = [0 for o in gt] # [int(o['iscrowd']) for o in gt] # load computed ious ious = self.ious[imgId, catId][:, gtind] if len( self.ious[imgId, catId]) > 0 else self.ious[imgId, catId] T = len(p.iouThrs) G = len(gt) D = len(dt) gtm = np.zeros((T, G)) dtm = np.zeros((T, D)) gtIg = np.array([g['_ignore'] for g in gt]) dtIg = np.zeros((T, D)) if not len(ious) == 0: for tind, t in enumerate(p.iouThrs): for dind, d in enumerate(dt): # information about best match so far (m=-1 -> unmatched) iou = min([t, 1 - 1e-10]) m = -1 for gind, g in enumerate(gt): # if this gt already matched, and not a crowd, continue if gtm[tind, gind] > 0 and not iscrowd[gind]: continue # if dt matched to reg gt, and on ignore gt, stop if m > -1 and gtIg[m] == 0 and gtIg[gind] == 1: break # continue to next gt unless better match made if ious[dind, gind] < iou: continue # if match successful and best so far, store # appropriately iou = ious[dind, gind] m = gind # if match made store id of match for both dt and gt if m == -1: continue dtIg[tind, dind] = gtIg[m] dtm[tind, dind] = gt[m]['id'] gtm[tind, m] = d['id'] # set unmatched detections outside of area range to ignore a = np.array([d['area'] < aRng[0] or d['area'] > aRng[1] for d in dt]).reshape((1, len(dt))) dtIg = np.logical_or(dtIg, np.logical_and(dtm == 0, np.repeat(a, T, 0))) # store results for given image and category return { 'image_id': imgId, 'category_id': catId, 'aRng': aRng, 'maxDet': maxDet, 'dtIds': [d['id'] for d in dt], 'gtIds': [g['id'] for g in gt], 'dtMatches': dtm, 'gtMatches': gtm, 'dtScores': [d['score'] for d in dt], 'gtIgnore': gtIg, 'dtIgnore': dtIg, } def accumulate(self, p=None): ''' Accumulate per image evaluation results and store the result in self.eval :param p: input params for evaluation :return: None ''' print('Accumulating evaluation results...') tic = time.time() if not self.evalImgs: print('Please run evaluate() first') # allows input customized parameters if p is None: p = self.params p.catIds = p.catIds if p.useCats == 1 else [-1] T = len(p.iouThrs) R = len(p.recThrs) K = len(p.catIds) if p.useCats else 1 A = len(p.areaRng) M = len(p.maxDets) precision = -np.ones( (T, R, K, A, M)) # -1 for the precision of absent categories recall = -np.ones((T, K, A, M)) scores = -np.ones((T, R, K, A, M)) # create dictionary for future indexing _pe = self._paramsEval catIds = _pe.catIds if _pe.useCats else [-1] setK = set(catIds) setA = set(map(tuple, _pe.areaRng)) setM = set(_pe.maxDets) setI = set(_pe.imgIds) # get inds to evaluate k_list = [n for n, k in enumerate(p.catIds) if k in setK] m_list = [m for n, m in enumerate(p.maxDets) if m in setM] a_list = [ n for n, a in enumerate(map(lambda x: tuple(x), p.areaRng)) if a in setA ] i_list = [n for n, i in enumerate(p.imgIds) if i in setI] I0 = len(_pe.imgIds) A0 = len(_pe.areaRng) # retrieve E at each category, area range, and max number of detections for k, k0 in enumerate(k_list): Nk = k0 * A0 * I0 for a, a0 in enumerate(a_list): Na = a0 * I0 for m, maxDet in enumerate(m_list): E = [self.evalImgs[Nk + Na + i] for i in i_list] E = [e for e in E if e is not None] if len(E) == 0: continue dtScores = np.concatenate( [e['dtScores'][0:maxDet] for e in E]) # different sorting method generates slightly different # results. mergesort is used to be consistent as Matlab # implementation. inds = np.argsort(-dtScores, kind='mergesort') dtScoresSorted = dtScores[inds] dtm = np.concatenate( [e['dtMatches'][:, 0:maxDet] for e in E], axis=1)[:, inds] dtIg = np.concatenate( [e['dtIgnore'][:, 0:maxDet] for e in E], axis=1)[:, inds] gtIg = np.concatenate([e['gtIgnore'] for e in E]) npig = np.count_nonzero(gtIg == 0) if npig == 0: continue tps = np.logical_and(dtm, np.logical_not(dtIg)) fps = np.logical_and(np.logical_not(dtm), np.logical_not(dtIg)) tp_sum = np.cumsum(tps, axis=1).astype(dtype=np.float) fp_sum = np.cumsum(fps, axis=1).astype(dtype=np.float) for t, (tp, fp) in enumerate(zip(tp_sum, fp_sum)): tp = np.array(tp) fp = np.array(fp) nd = len(tp) rc = tp / npig pr = tp / (fp + tp + np.spacing(1)) q = np.zeros((R, )) ss = np.zeros((R, )) if nd: recall[t, k, a, m] = rc[-1] else: recall[t, k, a, m] = 0 # numpy is slow without cython optimization for # accessing elements use python array gets significant # speed improvement pr = pr.tolist() q = q.tolist() for i in range(nd - 1, 0, -1): if pr[i] > pr[i - 1]: pr[i - 1] = pr[i] inds = np.searchsorted(rc, p.recThrs, side='left') try: for ri, pi in enumerate(inds): q[ri] = pr[pi] ss[ri] = dtScoresSorted[pi] except: # noqa: E722 pass precision[t, :, k, a, m] = np.array(q) scores[t, :, k, a, m] = np.array(ss) self.eval = { 'params': p, 'counts': [T, R, K, A, M], 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'precision': precision, 'recall': recall, 'scores': scores, } toc = time.time() print('DONE (t={:0.2f}s).'.format(toc - tic)) def summarize(self): ''' Compute and display summary metrics for evaluation results. Note this functin can *only* be applied on the default parameter setting ''' def _summarize(ap=1, iouThr=None, areaRng='all', maxDets=100): p = self.params iStr = '{:<18} {} @ [ IoU={:<9} | area={:>6s} | maxDets={:>3d} ] = {:0.3f}' # noqa: E501 titleStr = 'Average Precision' if ap == 1 else 'Average Recall' typeStr = '(AP)' if ap == 1 else '(AR)' iouStr = '{:0.2f}:{:0.2f}'.format(p.iouThrs[0], p.iouThrs[-1]) \ if iouThr is None else '{:0.2f}'.format(iouThr) aind = [ i for i, aRng in enumerate(p.areaRngLbl) if aRng == areaRng ] mind = [i for i, mDet in enumerate(p.maxDets) if mDet == maxDets] if ap == 1: # dimension of precision: [TxRxKxAxM] s = self.eval['precision'] # IoU if iouThr is not None: t = np.where(iouThr == p.iouThrs)[0] s = s[t] s = s[:, :, :, aind, mind] else: # dimension of recall: [TxKxAxM] s = self.eval['recall'] if iouThr is not None: t = np.where(iouThr == p.iouThrs)[0] s = s[t] s = s[:, :, aind, mind] if len(s[s > -1]) == 0: mean_s = -1 else: mean_s = np.mean(s[s > -1]) txtPth = "./work_dirs/evalRes.txt" txtFile = open(txtPth, 'a+') txtFile.writelines(iStr.format(titleStr, typeStr, iouStr, areaRng, maxDets, mean_s)) txtFile.writelines('\n') txtFile.close() print( iStr.format(titleStr, typeStr, iouStr, areaRng, maxDets, mean_s)) return mean_s def _summarizeDets(): stats = np.zeros((12, )) # AP metric # stats[0] = _summarize(1, areaRng='Small', maxDets=self.params.maxDets[0]) stats[1] = _summarize(1, iouThr=.50, areaRng='Small', maxDets=self.params.maxDets[0]) stats[2] = _summarize(1, iouThr=.75, areaRng='Small', maxDets=self.params.maxDets[0]) stats[3] = _summarize(1, areaRng='eS', maxDets=self.params.maxDets[0]) stats[4] = _summarize(1, areaRng='rS', maxDets=self.params.maxDets[0]) stats[5] = _summarize(1, areaRng='gS', maxDets=self.params.maxDets[0]) stats[6] = _summarize(1, areaRng='Normal', maxDets=self.params.maxDets[0]) # AR metric stats[7] = _summarize(0, areaRng='Small', maxDets=self.params.maxDets[0]) stats[8] = _summarize(0, areaRng='eS', maxDets=self.params.maxDets[0]) stats[9] = _summarize(0, areaRng='rS', maxDets=self.params.maxDets[0]) stats[10] = _summarize(0, areaRng='gS', maxDets=self.params.maxDets[0]) stats[11] = _summarize(0, areaRng='Normal', maxDets=self.params.maxDets[0]) return stats if not self.eval: raise Exception('Please run accumulate() first') iouType = self.params.iouType if iouType == 'mAP': summarize = _summarizeDets else: raise Exception('unknown iouType for iou computation') self.stats = summarize() def __str__(self): self.summarize() class SODAAParams: ''' Params for coco evaluation api ''' def __init__(self, iouType='mAP'): if iouType == 'mAP': self.setDetParams() else: raise Exception('iouType not supported') self.iouType = iouType def setDetParams(self): self.imgIds = [] self.catIds = [] # np.arange causes trouble. the data point on arange is slightly # larger than the true value self.iouThrs = np.linspace(.5, 0.95, int(np.round((0.95 - .5) / .05)) + 1, endpoint=True) self.recThrs = np.linspace(.0, 1.00, int(np.round((1.00 - .0) / .01)) + 1, endpoint=True) # TODO: ensure self.maxDets = [20000] self.areaRng = [[0 ** 2, 32 ** 2], [0 ** 2, 12 ** 2], [12 ** 2, 20 ** 2], [20 ** 2, 32 ** 2], [32 ** 2, 40 * 50]] self.areaRngLbl = ['Small', 'eS', 'rS', 'gS', 'Normal'] self.useCats = 1 ``` #### Đăng ký bộ dữ liệu sodaa Trong file _init_.py, ta thêm lệnh ``` from .sodaa import SODAADataset ``` và trong mảng ```__all__``` ta thêm 'SODAADataset' ### Step 6: Test với file checkpoint sau khi train Sau khi train xong file checkpoint sẽ lưu trong thư mục work_dirs VD: Test với file checkpoint ở epoch thứ 6: ```gherkin= python ./tools/test.py \ configs/oriented_reppoints/oriented_reppoints_r50_fpn_1x_sodaa_le135.py \ work_dirs/oriented_reppoints_r50_fpn_1x_sodaa_le135/epoch_6.pth --eval mAP ``` Tài liệu hướng dẫn dùng cho nhóm [UIT-Together Research Group](https://uit-together.github.io/)