Skip to content

Commit daf3012

Browse files
committed
Add Data Visualization file
1 parent 81e402e commit daf3012

File tree

1 file changed

+303
-0
lines changed

1 file changed

+303
-0
lines changed

visualize_dataset.py

+303
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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

Comments
 (0)