|
| 1 | +import matplotlib.pyplot as plt |
| 2 | +import cv2 |
| 3 | +import numpy as np |
| 4 | +import pandas as pd |
| 5 | +import glob |
| 6 | +import os |
| 7 | +import tqdm |
| 8 | +import json |
| 9 | +import copy |
| 10 | +import argparse |
| 11 | + |
| 12 | +from tensorpack.utils import logger, viz |
| 13 | +from tensorpack.utils.timer import timed_operation |
| 14 | +from tensorpack.utils.palette import PALETTE_RGB |
| 15 | + |
| 16 | +from pycocotools import mask as maskUtils |
| 17 | + |
| 18 | +from six.moves import zip |
| 19 | + |
| 20 | + |
| 21 | +class COCODetection(object): |
| 22 | + # handle the weird (but standard) split of train and val |
| 23 | + |
| 24 | + # Not used |
| 25 | + _INSTANCE_TO_BASEDIR = { |
| 26 | + 'valminusminival2014': 'val2014', |
| 27 | + 'minival2014': 'val2014', |
| 28 | + } |
| 29 | + |
| 30 | + COCO_id_to_category_id = {1: 1, 2: 2, 3: 3, 5: 4, 6: 5} |
| 31 | + category_id_to_COCO_id = {v:k for k,v in COCO_id_to_category_id.items()} |
| 32 | + """ |
| 33 | + Mapping from the incontinuous COCO category id to an id in [1, #category] |
| 34 | + For your own dataset, this should usually be an identity mapping. |
| 35 | + """ |
| 36 | + |
| 37 | + def __init__(self, imgdir, annofile): |
| 38 | + self._imgdir = os.path.realpath(imgdir) |
| 39 | + self.name = self._imgdir |
| 40 | + assert os.path.isdir(self._imgdir), self._imgdir |
| 41 | + annotation_file = os.path.realpath(annofile) |
| 42 | + print(os.path.isfile(annotation_file)) |
| 43 | + assert os.path.isfile(annotation_file), annotation_file |
| 44 | + from pycocotools.coco import COCO |
| 45 | + self.coco = COCO(annotation_file) |
| 46 | + logger.info("Instances loaded from {}.".format(annotation_file)) |
| 47 | + |
| 48 | + # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb |
| 49 | + def print_coco_metrics(self, json_file): |
| 50 | + """ |
| 51 | + Args: |
| 52 | + json_file (str): path to the results json file in coco format |
| 53 | + Returns: |
| 54 | + dict: the evaluation metrics |
| 55 | + """ |
| 56 | + from pycocotools.cocoeval import COCOeval |
| 57 | + ret = {} |
| 58 | + cocoDt = self.coco.loadRes(json_file) |
| 59 | + cocoEval = COCOeval(self.coco, cocoDt, 'bbox') |
| 60 | + cocoEval.evaluate() |
| 61 | + cocoEval.accumulate() |
| 62 | + cocoEval.summarize() |
| 63 | + fields = ['IoU=0.5:0.95', 'IoU=0.5', 'IoU=0.75', 'small', 'medium', 'large'] |
| 64 | + for k in range(6): |
| 65 | + ret['mAP(bbox)/' + fields[k]] = cocoEval.stats[k] |
| 66 | + |
| 67 | + json_obj = json.load(open(json_file)) |
| 68 | + if len(json_obj) > 0 and 'segmentation' in json_obj[0]: |
| 69 | + cocoEval = COCOeval(self.coco, cocoDt, 'segm') |
| 70 | + cocoEval.evaluate() |
| 71 | + cocoEval.accumulate() |
| 72 | + cocoEval.summarize() |
| 73 | + for k in range(6): |
| 74 | + ret['mAP(segm)/' + fields[k]] = cocoEval.stats[k] |
| 75 | + return ret |
| 76 | + |
| 77 | + def load(self, add_gt=True, add_mask=False): |
| 78 | + """ |
| 79 | + Args: |
| 80 | + add_gt: whether to add ground truth bounding box annotations to the dicts |
| 81 | + add_mask: whether to also add ground truth mask |
| 82 | +
|
| 83 | + Returns: |
| 84 | + a list of dict, each has keys including: |
| 85 | + 'image_id', 'file_name', |
| 86 | + and (if add_gt is True) 'boxes', 'class', 'is_crowd', and optionally |
| 87 | + 'segmentation'. |
| 88 | + """ |
| 89 | + if add_mask: |
| 90 | + assert add_gt |
| 91 | + with timed_operation('Load Groundtruth Boxes for {}'.format(self.name)): |
| 92 | + img_ids = self.coco.getImgIds() |
| 93 | + img_ids.sort() |
| 94 | + # list of dict, each has keys: height,width,id,file_name |
| 95 | + imgs = self.coco.loadImgs(img_ids) |
| 96 | + |
| 97 | + for img in tqdm.tqdm(imgs): |
| 98 | + img['image_id'] = img.pop('id') |
| 99 | + self._use_absolute_file_name(img) |
| 100 | + if add_gt: |
| 101 | + self._add_detection_gt(img, add_mask) |
| 102 | + return imgs |
| 103 | + |
| 104 | + def _use_absolute_file_name(self, img): |
| 105 | + """ |
| 106 | + Change relative filename to abosolute file name. |
| 107 | + """ |
| 108 | + img['file_name'] = os.path.join( |
| 109 | + self._imgdir, img['file_name']) |
| 110 | + assert os.path.isfile(img['file_name']), img['file_name'] |
| 111 | + |
| 112 | + def _add_detection_gt(self, img, add_mask): |
| 113 | + """ |
| 114 | + Add 'boxes', 'class', 'is_crowd' of this image to the dict, used by detection. |
| 115 | + If add_mask is True, also add 'segmentation' in coco poly format. |
| 116 | + """ |
| 117 | + # ann_ids = self.coco.getAnnIds(imgIds=img['image_id']) |
| 118 | + # objs = self.coco.loadAnns(ann_ids) |
| 119 | + objs = self.coco.imgToAnns[img['image_id']] # equivalent but faster than the above two lines |
| 120 | + |
| 121 | + # clean-up boxes |
| 122 | + valid_objs = [] |
| 123 | + width = img['width'] |
| 124 | + height = img['height'] |
| 125 | + for objid, obj in enumerate(objs): |
| 126 | + if obj.get('ignore', 0) == 1: |
| 127 | + continue |
| 128 | + x1, y1, w, h = obj['bbox'] |
| 129 | + # bbox is originally in float |
| 130 | + # x1/y1 means upper-left corner and w/h means true w/h. This can be verified by segmentation pixels. |
| 131 | + # But we do make an assumption here that (0.0, 0.0) is upper-left corner of the first pixel |
| 132 | + |
| 133 | + x1 = np.clip(float(x1), 0, width) |
| 134 | + y1 = np.clip(float(y1), 0, height) |
| 135 | + w = np.clip(float(x1 + w), 0, width) - x1 |
| 136 | + h = np.clip(float(y1 + h), 0, height) - y1 |
| 137 | + # Require non-zero seg area and more than 1x1 box size |
| 138 | + if obj['area'] > 1 and w > 0 and h > 0 and w * h >= 4: |
| 139 | + obj['bbox'] = [x1, y1, x1 + w, y1 + h] |
| 140 | + valid_objs.append(obj) |
| 141 | + |
| 142 | + if add_mask: |
| 143 | + segs = obj['segmentation'] |
| 144 | + if not isinstance(segs, list): |
| 145 | + assert obj['iscrowd'] == 1 |
| 146 | + obj['segmentation'] = None |
| 147 | + else: |
| 148 | + valid_segs = [np.asarray(p).reshape(-1, 2).astype('float32') for p in segs if len(p) >= 6] |
| 149 | + if len(valid_segs) == 0: |
| 150 | + logger.error("Object {} in image {} has no valid polygons!".format(objid, img['file_name'])) |
| 151 | + elif len(valid_segs) < len(segs): |
| 152 | + logger.warn("Object {} in image {} has invalid polygons!".format(objid, img['file_name'])) |
| 153 | + |
| 154 | + obj['segmentation'] = valid_segs |
| 155 | + |
| 156 | + # all geometrically-valid boxes are returned |
| 157 | + boxes = np.asarray([obj['bbox'] for obj in valid_objs], dtype='float32') # (n, 4) |
| 158 | + cls = np.asarray([ |
| 159 | + self.COCO_id_to_category_id[obj['category_id']] |
| 160 | + for obj in valid_objs], dtype='int32') # (n,) |
| 161 | + is_crowd = np.asarray([obj['iscrowd'] for obj in valid_objs], dtype='int8') |
| 162 | + |
| 163 | + # add the keys |
| 164 | + img['boxes'] = boxes # nx4 |
| 165 | + img['class'] = cls # n, always >0 |
| 166 | + img['is_crowd'] = is_crowd # n, |
| 167 | + if add_mask: |
| 168 | + # also required to be float32 |
| 169 | + img['segmentation'] = [ |
| 170 | + obj['segmentation'] for obj in valid_objs] |
| 171 | + |
| 172 | + def getClassNameFromSample(self, class_id): |
| 173 | + return self.coco.loadCats(self.category_id_to_COCO_id[int(class_id)])[0]["name"] |
| 174 | + |
| 175 | + @staticmethod |
| 176 | + def load_many(basedir, names, add_gt=True, add_mask=False): |
| 177 | + """ |
| 178 | + Load and merges several instance files together. |
| 179 | +
|
| 180 | + Returns the same format as :meth:`COCODetection.load`. |
| 181 | + """ |
| 182 | + if not isinstance(names, (list, tuple)): |
| 183 | + names = [names] |
| 184 | + ret = [] |
| 185 | + for n in names: |
| 186 | + coco = COCODetection(basedir, n) |
| 187 | + ret.extend(coco.load(add_gt, add_mask=add_mask)) |
| 188 | + return ret |
| 189 | + |
| 190 | +def getClassesFromImg(img): |
| 191 | + return img["class"] |
| 192 | + |
| 193 | +def getMasksFromImg(img): |
| 194 | + is_crowd = img['is_crowd'] |
| 195 | + segmentation = copy.deepcopy(img['segmentation']) |
| 196 | + segmentation = [segmentation[k] for k in range(len(segmentation)) if not is_crowd[k]] |
| 197 | + height, width = img['height'], img['width'] |
| 198 | + # Apply augmentation on polygon coordinates. |
| 199 | + # And produce one image-sized binary mask per box. |
| 200 | + masks = [] |
| 201 | + width_height = np.asarray([width, height], dtype=np.float32) |
| 202 | + for polys in segmentation: |
| 203 | + # if not cfg.DATA.ABSOLUTE_COORD: |
| 204 | + # polys = [p * width_height for p in polys] |
| 205 | + # polys = [aug.augment_coords(p, params) for p in polys] |
| 206 | + masks.append(segmentation_to_mask(polys, height, width)) |
| 207 | + masks = np.asarray(masks, dtype='uint8') # values in {0, 1} |
| 208 | + return masks |
| 209 | + |
| 210 | +def genBoxesFromMasks(masks): |
| 211 | + """Compute bounding boxes from masks. |
| 212 | + mask: [num_instances, height, width]. Mask pixels are either 1 or 0. |
| 213 | + Returns: bbox array [num_instances, (y1, x1, y2, x2)]. |
| 214 | + """ |
| 215 | + boxes = np.zeros([masks.shape[0], 4], dtype=np.int32) |
| 216 | + for i in range(masks.shape[0]): |
| 217 | + m = masks[i ,:, :] |
| 218 | + # Bounding box. |
| 219 | + horizontal_indicies = np.where(np.any(m, axis=0))[0] |
| 220 | + vertical_indicies = np.where(np.any(m, axis=1))[0] |
| 221 | + if horizontal_indicies.shape[0]: |
| 222 | + x1, x2 = horizontal_indicies[[0, -1]] |
| 223 | + y1, y2 = vertical_indicies[[0, -1]] |
| 224 | + # x2 and y2 should not be part of the box. Increment by 1. |
| 225 | + x2 += 1 |
| 226 | + y2 += 1 |
| 227 | + else: |
| 228 | + # No mask for this instance. Might happen due to |
| 229 | + # resizing or cropping. Set bbox to zeros |
| 230 | + x1, x2, y1, y2 = 0, 0, 0, 0 |
| 231 | + boxes[i] = np.array([x1, y1, x2, y2]) |
| 232 | + return boxes.astype(np.int32) |
| 233 | + |
| 234 | + |
| 235 | +def segmentation_to_mask(polys, height, width): |
| 236 | + """ |
| 237 | + Convert polygons to binary masks. |
| 238 | + Args: |
| 239 | + polys: a list of nx2 float array. Each array contains many (x, y) coordinates. |
| 240 | + Returns: |
| 241 | + a binary matrix of (height, width) |
| 242 | + """ |
| 243 | + polys = [p.flatten().tolist() for p in polys] |
| 244 | + assert len(polys) > 0, "Polygons are empty!" |
| 245 | + |
| 246 | + import pycocotools.mask as cocomask |
| 247 | + rles = cocomask.frPyObjects(polys, height, width) |
| 248 | + rle = cocomask.merge(rles) |
| 249 | + return cocomask.decode(rle) |
| 250 | + |
| 251 | +def draw_mask(im, mask, box, label, alpha=0.5, color=None): |
| 252 | + """ |
| 253 | + Overlay a mask on top of the image. |
| 254 | + Args: |
| 255 | + im: a 3-channel uint8 image in BGR |
| 256 | + mask: a binary 1-channel image of the same size |
| 257 | + color: if None, will choose automatically |
| 258 | + """ |
| 259 | + if color is None: |
| 260 | + color = PALETTE_RGB[np.random.choice(len(PALETTE_RGB))][::-1] |
| 261 | + im = np.where(np.repeat((mask > 0)[:, :, None], 3, axis=2), |
| 262 | + im * (1 - alpha) + color * alpha, im) |
| 263 | + im = im.astype('uint8') |
| 264 | + color_tuple = tuple([int(c) for c in color]) |
| 265 | + im = viz.draw_boxes(im, box[np.newaxis, :], [label], color=color_tuple) |
| 266 | + return im |
| 267 | + |
| 268 | +def parse_args(): |
| 269 | + parser = argparse.ArgumentParser(description='Code for Harris corner detector tutorial.') |
| 270 | + parser.add_argument('--imagedir', help='Path to dataset images.') |
| 271 | + parser.add_argument('--jsonfile', help='Path to json file.') |
| 272 | + parser.add_argument('--output') |
| 273 | + return parser.parse_args() |
| 274 | + |
| 275 | +def main(): |
| 276 | + args = parse_args() |
| 277 | + output_dir = args.output |
| 278 | + ds = COCODetection(args.imagedir,args.jsonfile) |
| 279 | + imgs = ds.load(add_gt=True, add_mask=True) |
| 280 | + os.makedirs(output_dir, exist_ok=True) |
| 281 | + for img in tqdm.tqdm(imgs): |
| 282 | + # Get masks from "img" (it's actually the image's meta rather than the image itself) |
| 283 | + # I follow the same naming from the Tensorpack's implementation of COCODetection |
| 284 | + masks = getMasksFromImg(img) |
| 285 | + boxes = genBoxesFromMasks(masks) |
| 286 | + classes = getClassesFromImg(img) # Class IDs |
| 287 | + classes = [ds.getClassNameFromSample(clsId) for clsId in classes] # Class names |
| 288 | + file_name = img['file_name'] |
| 289 | + image_id = img['image_id'] |
| 290 | + im = cv2.imread(file_name) |
| 291 | + orig_im = im.copy() |
| 292 | + # Draw masks, boxes and labels |
| 293 | + for i in range(masks.shape[0]): |
| 294 | + im = draw_mask(im, masks[i], boxes[i], str(classes[i])) |
| 295 | + basename = os.path.basename(file_name) |
| 296 | + |
| 297 | + output_path = os.path.join(output_dir, str(image_id) + '_' + basename) |
| 298 | + # merge original image to the image with labels |
| 299 | + im = np.concatenate([orig_im, im], axis=1) |
| 300 | + cv2.imwrite(output_path, im) |
| 301 | + |
| 302 | +if __name__ == '__main__': |
| 303 | + main() |
0 commit comments