def _to_coco(dmet): """ Convert to a coco representation of truth and predictions """ import kwcoco true = kwcoco.CocoDataset() pred = kwcoco.CocoDataset() for node in dmet.classes: # cid = dmet.classes.graph.node[node]['id'] cid = dmet.classes.index(node) supercategory = list(dmet.classes.graph.pred[node]) if len(supercategory) == 0: supercategory = None else: assert len(supercategory) == 1 supercategory = supercategory[0] true.add_category(node, id=cid, supercategory=supercategory) pred.add_category(node, id=cid, supercategory=supercategory) for imgname, gid in dmet._imgname_to_gid.items(): true.add_image(imgname, id=gid) pred.add_image(imgname, id=gid) idx_to_id = { idx: dmet.classes.index(node) for idx, node in enumerate(dmet.classes.idx_to_node) } for gid, pred_dets in dmet.gid_to_pred_dets.items(): pred_boxes = pred_dets.boxes if 'scores' in pred_dets.data: pred_scores = pred_dets.scores else: pred_scores = np.ones(len(pred_dets)) pred_cids = list(ub.take(idx_to_id, pred_dets.class_idxs)) pred_xywh = pred_boxes.to_xywh().data.tolist() for bbox, cid, score in zip(pred_xywh, pred_cids, pred_scores): pred.add_annotation(gid, cid, bbox=bbox, score=score) for gid, true_dets in dmet.gid_to_true_dets.items(): true_boxes = true_dets.boxes if 'weights' in true_dets.data: true_weights = true_dets.weights else: true_weights = np.ones(len(true_boxes)) true_cids = list(ub.take(idx_to_id, true_dets.class_idxs)) true_xywh = true_boxes.to_xywh().data.tolist() for bbox, cid, weight in zip(true_xywh, true_cids, true_weights): true.add_annotation(gid, cid, bbox=bbox, weight=weight) return pred, true
def _ensure_coco(coco): # Map a file path or an in-memory dataset to a CocoDataset import kwcoco import six from os.path import exists if coco is None: return None elif isinstance(coco, six.string_types): fpath = _rectify_fpath(coco) if exists(fpath): with ub.Timer( 'read kwcoco dataset: fpath = {!r}'.format(fpath)): coco = kwcoco.CocoDataset(fpath, autobuild=False) print('building kwcoco index') coco._build_index() else: if not coco.lower().startswith('special:'): import warnings warnings.warn('warning start dataset codes with special:') code = coco else: code = coco.lower()[len('special:'):] coco = kwcoco.CocoDataset.demo(code) else: # print('live dataset') assert isinstance(coco, kwcoco.CocoDataset) return coco
def make_pycocotools_compliant(fpath): import kwcoco import kwimage import ubelt as ub print('Reading fpath = {!r}'.format(fpath)) dset = kwcoco.CocoDataset(fpath) dset._ensure_imgsize(workers=8) for ann in ub.ProgIter(dset.dataset['annotations'], desc='update anns'): if 'iscrowd' not in ann: ann['iscrowd'] = False if 'ignore' not in ann: ann['ignore'] = ann.get('weight', 1.0) < .5 if 'area' not in ann: # Use segmentation if available if 'segmentation' in ann: poly = kwimage.Polygon.from_coco(ann['segmentation'][0]) ann['area'] = float(poly.to_shapely().area) else: x, y, w, h = ann['bbox'] ann['area'] = w * h dset.dump(dset.fpath, newlines=True)
def convert(in_path, out_path): from bioharn.io.viame_csv import ViameCSV dset = kwcoco.CocoDataset() # TODO: ability to map image ids to agree with another coco file csv = ViameCSV(in_path) csv.extend_coco(dset=dset) dset.dump(out_path, newlines=True)
def test_union_with_aux(): from os.path import join test_img1 = { 'id': 1, 'name': 'foo', 'file_name': 'subdir/images/foo.png', 'auxiliary': [{ 'channels': 'ir', 'file_name': 'subdir/aux/foo.png', }] } test_img2 = { 'id': 1, 'name': 'bar', 'file_name': 'images/bar.png', 'auxiliary': [{ 'channels': 'ir', 'file_name': 'aux/foo.png', }] } import kwcoco dset1 = kwcoco.CocoDataset() dset1.add_image(**test_img1) dset1.fpath = join('.', 'dset1', 'data.kwcoco.json') dset2 = kwcoco.CocoDataset() dset2.add_image(**test_img2) dset2.fpath = join('.', 'subdir/dset2', 'data.kwcoco.json') combo = kwcoco.CocoDataset.union(dset1, dset2) assert combo.get_image_fpath(1) == dset1.get_image_fpath(1) assert combo.get_image_fpath(1, channels='ir') == dset1.get_image_fpath( 1, channels='ir') assert combo.get_image_fpath(2) == dset2.get_image_fpath(1) assert combo.get_image_fpath(2, channels='ir') == dset2.get_image_fpath( 1, channels='ir')
def getting_started_existing_dataset(): """ If you want to start using the Python API. Just open IPython and try: """ import kwcoco dset = kwcoco.CocoDataset('<DATASET_NAME>/data.kwcoco.json') """ You can access category, image, and annotation infomation using the index: """ cat = dset.index.cats[1] print('The First Category = {!r}'.format(cat)) img = dset.index.imgs[1] print('The First Image = {!r}'.format(img)) ann = dset.index.anns[1] print('The First Annotation = {!r}'.format(ann))
def main(cls, cmdline=True, **kw): """ CommandLine: xdoctest -m make_coco_from_masks.py MakeCocoFromMasksCLI.main Example: >>> import ubelt as ub >>> cls = MakeCocoFromMasksCLI >>> cmdline = False >>> dpath = _make_intmask_demodata() >>> kw = {'src': join(dpath, '*.png'), 'dst': 'masks.mscoco.json'} >>> fpath = cls.main(cmdline, **kw) >>> # Check validity >>> import kwcoco >>> dset = kwcoco.CocoDataset(fpath) >>> dset._check_integrity() >>> # Ensure we can convert to kwimage and access segmentations >>> dset.annots().detections.data['segmentations'][0].data """ import kwcoco config = cls.CLIConfig(kw, cmdline=cmdline) print('config = {}'.format(ub.repr2(dict(config), nl=2))) serialization_method = config['serialization'] # Initialize an empty COCO dataset coco_dset = kwcoco.CocoDataset() # Path to each mask object mask_fpaths = config['src'] for mask_fpath in mask_fpaths: # I assume each mask corresponds to a single image, but I dont know # what those images should be at the moment. TODO: if this is going # to be a real script, then we should find a nice way of specifying # the correspondences between masks and the images to which they # belong. For now, I'm going to use the mask itself as a dummy # value. img_fpath = mask_fpath image_id = coco_dset.add_image(file_name=img_fpath) # Parse the mask file, and add each object as a new annotation multi_mask = kwimage.imread(mask_fpath) # I recall there is a better opencv of splitting these sort of # masks up into binary masks, maybe it was a connected-component # function? I guess it depends if you want disconnected objects # represented as separte annotations or not. I'm just going to do # the naive thing for now. obj_idxs = np.setdiff1d(np.unique(multi_mask), [0]) for obj_idx in obj_idxs: bin_data = (multi_mask == obj_idx).astype(np.uint8) # Create a kwimage object which has nice `to_coco` methods bin_mask = kwimage.Mask(bin_data, format='c_mask') # We can either save in our coco file as a raster RLE style # mask, or we can use a vector polygon style mask. Either of # the resulting coco_sseg objects is a valid value for an # annotation's "segmentation" field. if serialization_method == 'raster': sseg = bin_mask coco_sseg = sseg.to_coco(style='new') elif serialization_method == 'vector': bin_poly = bin_mask.to_multi_polygon() sseg = bin_poly coco_sseg = sseg.to_coco(style='new') # Now we add this segmentation to the coco dataset as a new # annotation. The annotation needs to know which image it # belongs to, and ideally it has a category and a bounding box # as well. # We can make up a dummy category (note that ensure_category # will not fail if there is a duplicate entry but add_category # will) category_id = coco_dset.ensure_category( 'class_{}'.format(obj_idx)) # We can use the kwimage sseg object to get the bounding box # FIXME: apparently the MultiPolygon object doesnt implement # `to_boxes`, at the moment, so force an inefficient conversion # back to a mask as a hack and use its to_boxes method. # Technically, the bounding box should not be required, but its # a good idea to always include it. bbox = list( sseg.to_mask( dims=multi_mask.shape).to_boxes().to_coco())[0] METHOD1 = False if METHOD1: # We could just add it diretly like this # FIXME: it should be ok to add an annotation without a # category, but it seems like a recent change in kwcoco has # broken that. That will be fixed in the future. annot_id = coco_dset.add_annotation( image_id=image_id, category_id=category_id, bbox=bbox, segmentation=coco_sseg) else: # But lets do it as if we were adding a segmentation to an # existing dataset. In this case we access the # CocoDataset's annotation index structure. # # First add the basic annotation annot_id = coco_dset.add_annotation( image_id=image_id, category_id=category_id, bbox=bbox) # Then use the annotation id to look up its coco-dictionary # representation and simply add the segmentation field ann = coco_dset.anns[annot_id] ann['segmentation'] = coco_sseg # You dont have to set the fpath attr, but I tend to like it coco_dset.fpath = config['dst'] print('Writing to fpath = {}'.format(ub.repr2(coco_dset.fpath, nl=1))) coco_dset.dump(coco_dset.fpath, newlines=True) return coco_dset.fpath
def load_kwcoco_dataset(kwcoco_dataset_id): logger.info('Starting KWCOCO ETL routine') ds_entry = KWCOCOArchive.objects.get(id=kwcoco_dataset_id) if not ds_entry.name: ds_entry.name = os.path.basename(ds_entry.spec_file.name) ds_entry.save(update_fields=[ 'name', ]) # TODO: add a setting like this: workdir = getattr(settings, 'GEODATA_WORKDIR', None) with tempfile.TemporaryDirectory(dir=workdir) as tmpdir: if ds_entry.image_set: # Delete all previously existing data # This should cascade to all the annotations for image in ds_entry.image_set.images.all(): image.file.delete() else: ds_entry.image_set = ImageSet() ds_entry.image_set.name = ds_entry.name ds_entry.image_set.save() ds_entry.save(update_fields=[ 'image_set', ] # noqa: E231 ) # Unarchive the images locally so we can import them when loading the spec # Images could come from a URL, so this is optional if ds_entry.image_archive: with ds_entry.image_archive.file as file_obj: logger.info( f'The KWCOCO image archive: {ds_entry.image_archive}') # Place images in a local directory and keep track of root path # Unzip the contents to the working dir with zipfile.ZipFile(file_obj, 'r') as zip_ref: zip_ref.extractall(tmpdir) logger.info(f'The extracted KWCOCO image archive: {tmpdir}') else: pass # TODO: how should we download data from specified URLs? # Load the KWCOCO JSON spec and make annotations on the images with ds_entry.spec_file.yield_local_path() as file_path: ds = kwcoco.CocoDataset(str(file_path)) # Set the root dir to where the images were extracted / the temp dir # If images are coming from URL, they will download to here ds.img_root = tmpdir # Iterate over images and create an ImageMeta from them. # Any images in the archive that aren't listed in the JSON will be deleted for imgid in ds.imgs.keys(): ak = ds.index.gid_to_aids[imgid] img = ds.imgs[imgid] anns = [ds.anns[k] for k in ak] # Create the Image entry to track each image's location image_file_abs_path = os.path.join(ds.img_root, img['file_name']) name = os.path.basename(image_file_abs_path) file = ChecksumFile() file.file.save(name, open(image_file_abs_path, 'rb')) image, _ = Image.objects.get_or_create(file=file) # Add ImageMeta to ImageSet ds_entry.image_set.images.add(image) # Create annotations that link to that ImageMeta for ann in anns: annotation_entry = Annotation() annotation_entry.image = image try: annotation_entry.label = ds.cats[ ann['category_id']]['name'] except KeyError: pass # annotation_entry.annotator = # annotation_entry.notes = _fill_annotation_segmentation(annotation_entry, ann) logger.info('Done with KWCOCO ETL routine')
def convert_camvid_raw_to_coco(camvid_raw_info): """ Converts the raw camvid format to an MSCOCO based format, ( which lets use use kwcoco's COCO backend). Example: >>> # xdoctest: +REQUIRES(--download) >>> camvid_raw_info = grab_raw_camvid() >>> # test with a reduced set of data >>> del camvid_raw_info['img_paths'][2:] >>> del camvid_raw_info['mask_paths'][2:] >>> dset = convert_camvid_raw_to_coco(camvid_raw_info) >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> plt = kwplot.autoplt() >>> kwplot.figure(fnum=1, pnum=(1, 2, 1)) >>> dset.show_image(gid=1) >>> kwplot.figure(fnum=1, pnum=(1, 2, 2)) >>> dset.show_image(gid=2) """ import re import kwimage import kwcoco print('Converting CamVid to MS-COCO format') dset_root, img_paths, label_path, mask_paths = ub.take( camvid_raw_info, 'dset_root, img_paths, label_path, mask_paths'.split(', ')) img_infos = { 'img_fname': img_paths, 'mask_fname': mask_paths, } keys = list(img_infos.keys()) next_vals = list(zip(*img_infos.values())) image_items = [{k: v for k, v in zip(keys, vals)} for vals in next_vals] dataset = { 'img_root': dset_root, 'images': [], 'categories': [], 'annotations': [], } lines = ub.readfrom(label_path).split('\n') lines = [line for line in lines if line] for line in lines: color_text, name = re.split('\t+', line) r, g, b = map(int, color_text.split(' ')) color = (r, g, b) # Parse the special camvid format cid = (r << 16) + (g << 8) + (b << 0) cat = { 'id': cid, 'name': name, 'color': color, } dataset['categories'].append(cat) for gid, img_item in enumerate(image_items, start=1): img = { 'id': gid, 'file_name': img_item['img_fname'], # nonstandard image field 'segmentation': img_item['mask_fname'], } dataset['images'].append(img) dset = kwcoco.CocoDataset(dataset) dset.rename_categories({'Void': 'background'}) assert dset.name_to_cat['background']['id'] == 0 dset.name_to_cat['background'].setdefault('alias', []).append('Void') if False: _define_camvid_class_hierarcy(dset) if 1: # TODO: Binarize CCs (and efficiently encode if possible) import numpy as np bad_info = [] once = False # Add images dset.remove_annotations(list(dset.index.anns.keys())) for gid, img in ub.ProgIter(dset.imgs.items(), desc='parse label masks'): mask_fpath = join(dset_root, img['segmentation']) rgb_mask = kwimage.imread(mask_fpath, space='rgb') r, g, b = rgb_mask.T.astype(np.int64) cid_mask = np.ascontiguousarray(rgb_to_cid(r, g, b).T) cids = set(np.unique(cid_mask)) - {0} for cid in cids: if cid not in dset.cats: if gid == 618: # Handle a known issue with image 618 c_mask = (cid == cid_mask).astype(np.uint8) total_bad = c_mask.sum() if total_bad < 32: if not once: print( 'gid 618 has a few known bad pixels, ignoring them' ) once = True continue else: raise Exception('more bad pixels than expected') else: raise Exception( 'UNKNOWN cid = {!r} in gid={!r}'.format(cid, gid)) # bad_rgb = cid_to_rgb(cid) # print('bad_rgb = {!r}'.format(bad_rgb)) # print('WARNING UNKNOWN cid = {!r} in gid={!r}'.format(cid, gid)) # bad_info.append({ # 'gid': gid, # 'cid': cid, # }) else: ann = { 'category_id': cid, 'image_id': gid # 'segmentation': mask.to_coco() } assert cid in dset.cats c_mask = (cid == cid_mask).astype(np.uint8) mask = kwimage.Mask(c_mask, 'c_mask') box = kwimage.Boxes([mask.get_xywh()], 'xywh') # box = mask.to_boxes() ann['bbox'] = ub.peek(box.to_coco()) ann['segmentation'] = mask.to_coco() dset.add_annotation(**ann) if 0: bad_cids = [i['cid'] for i in bad_info] print(sorted([c['color'] for c in dataset['categories']])) print(sorted(set([cid_to_rgb(i['cid']) for i in bad_info]))) gid = 618 img = dset.imgs[gid] mask_fpath = join(dset_root, img['segmentation']) rgb_mask = kwimage.imread(mask_fpath, space='rgb') r, g, b = rgb_mask.T.astype(np.int64) cid_mask = np.ascontiguousarray(rgb_to_cid(r, g, b).T) cid_hist = ub.dict_hist(cid_mask.ravel()) bad_cid_hist = {} for cid in bad_cids: bad_cid_hist[cid] = cid_hist.pop(cid) import kwplot kwplot.autompl() kwplot.imshow(rgb_mask) if 0: import kwplot plt = kwplot.autoplt() plt.clf() dset.show_image(1) import xdev gid_list = list(dset.imgs) for gid in xdev.InteractiveIter(gid_list): dset.show_image(gid) xdev.InteractiveIter.draw() dset._build_index() dset._build_hashid() return dset
def grab_coco_camvid(): """ Example: >>> # xdoctest: +REQUIRES(--download) >>> dset = grab_coco_camvid() >>> print('dset = {!r}'.format(dset)) >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> plt = kwplot.autoplt() >>> plt.clf() >>> dset.show_image(gid=1) Ignore: import xdev gid_list = list(dset.imgs) for gid in xdev.InteractiveIter(gid_list): dset.show_image(gid) xdev.InteractiveIter.draw() """ import kwcoco cache_dpath = ub.ensure_app_cache_dir('kwcoco', 'camvid') coco_fpath = join(cache_dpath, 'camvid.mscoco.json') # Need to manually bump this if you make a change to loading SCRIPT_VERSION = 'v4' # Ubelt's stamp-based caches are super cheap and let you take control of # the data format. stamp = ub.CacheStamp('camvid_coco', cfgstr=SCRIPT_VERSION, dpath=cache_dpath, product=coco_fpath, hasher='sha1', verbose=3) if stamp.expired(): camvid_raw_info = grab_raw_camvid() dset = convert_camvid_raw_to_coco(camvid_raw_info) with ub.Timer('dumping MS-COCO dset to: {}'.format(coco_fpath)): dset.dump(coco_fpath) # Mark this process as completed by saving a small file containing the # hash of the "product" you are stamping. stamp.renew() # We can also cache the index build step independently. This uses # ubelt.Cacher, which is pickle based, and writes the actual object to # disk. Each type of caching has its own uses and tradeoffs. cacher = ub.Cacher('prebuilt-coco', cfgstr=SCRIPT_VERSION, dpath=cache_dpath, verbose=3) dset = cacher.tryload() if dset is None: print('Reading coco_fpath = {!r}'.format(coco_fpath)) dset = kwcoco.CocoDataset(coco_fpath, tag='camvid') # Directly save the file to disk. dset._build_index() dset._build_hashid() cacher.save(dset) camvid_dset = dset print('Loaded camvid_dset = {!r}'.format(camvid_dset)) return camvid_dset
def to_coco(self, image_paths=None, video_name=None): """ Translates a kw18 files to a CocoDataset. Note: kw18 does not contain complete information, and as such the returned coco dataset may need to be augmented. Args: image_paths (Dict[int, str], default=None): if specified, maps frame numbers to image file paths. video_name (str, default=None): if specified records the name of the video this kw18 belongs to TODO: - [X] allow kwargs to specify path to frames / videos Example: >>> from kwcoco.kw18 import KW18 >>> from os.path import join >>> import ubelt as ub >>> import kwimage >>> # Prep test data - autogen a demo kw18 and write it to disk >>> dpath = ub.ensure_app_cache_dir('kwcoco/kw18') >>> kw18_fpath = join(dpath, 'test.kw18') >>> KW18.demo().dump(kw18_fpath) >>> # >>> # Load the kw18 file >>> self = KW18.load(kw18_fpath) >>> # Pretend that these image correspond to kw18 frame numbers >>> frame_names = [kwimage.grab_test_image_fpath(k) for k in kwimage.grab_test_image.keys()] >>> frame_ids = sorted(set(self['frame_number'])) >>> image_paths = dict(zip(frame_ids, frame_names)) >>> # >>> # Convert the kw18 to kwcoco and specify paths to images >>> coco_dset = self.to_coco(image_paths=image_paths, video_name='dummy.mp4') >>> # >>> # Now we can draw images >>> canvas = coco_dset.draw_image(1) >>> # xdoctest: +REQUIRES(--draw) >>> kwimage.imwrite('foo.jpg', canvas) >>> # Draw all iamges >>> for gid in coco_dset.imgs.keys(): >>> canvas = coco_dset.draw_image(gid) >>> fpath = join(dpath, 'gid_{}.jpg'.format(gid)) >>> print('write fpath = {!r}'.format(fpath)) >>> kwimage.imwrite(fpath, canvas) """ import kwcoco import ubelt as ub dset = kwcoco.CocoDataset() # kw18s don't have category names, so use ids as proxies unique_category_ids = sorted(set(self['object_type_id'])) for cid in unique_category_ids: dset.ensure_category('class_{}'.format(cid), id=cid) unique_frame_idxs = ub.argunique(self['frame_number']) # kw18 files correspond to one video vidid = 1 dset.add_video(id=vidid, name='unknown_kw18_video') # Index frames of the video for idx in unique_frame_idxs: frame_num = self['frame_number'][idx] timestamp = self['timestamp'][idx] if image_paths and frame_num in image_paths: file_name = image_paths[frame_num] else: file_name = '<unknown_image_{}>'.format(frame_num) dset.add_image(id=frame_num, file_name=file_name, video_id=vidid, frame_index=frame_num, timestamp=timestamp) for rx, row in self.iterrows(): tl_x = row['img_bbox_tl_x'] tl_y = row['img_bbox_tl_y'] br_x = row['img_bbox_br_x'] br_y = row['img_bbox_br_y'] w = br_x - tl_x h = br_y - tl_y bbox = [tl_x, tl_y, w, h] world_loc = (row['world_loc_x'], row['world_loc_y'], row['world_loc_z']) velocity = (row['velocity_x'], row['velocity_y']) kw = {} if 'confidence' in row: kw['score'] = row['confidence'] dset.add_annotation(id=rx, image_id=row['frame_number'], category_id=row['object_type_id'], track_id=row['track_id'], bbox=bbox, area=row['area'], velocity=velocity, world_loc=world_loc, **kw) return dset
def coco_from_viame_csv(csv_fpaths, images=None): @ub.memoize def lazy_image_list(): if images is None: raise Exception('must specify where the image root is') if isdir(images): image_dpath = images all_gpaths = [] import os for root, ds, fs in os.walk(image_dpath): IMG_EXT = {'png', 'jpg', 'jpeg', 'tif', 'tiff'} gpaths = [join(root, f) for f in fs if f.split('.')[-1].lower() in IMG_EXT] if len(gpaths) > 1 and len(ds) != 0: raise Exception('Images must be in a leaf directory') if len(all_gpaths) > 0: raise Exception('Images cannot be nested ATM') all_gpaths += gpaths all_gpaths = sorted(all_gpaths) else: raise NotImplementedError return all_gpaths indexed_images = None import kwcoco dset = kwcoco.CocoDataset() for csv_fpath in csv_fpaths: with open(csv_fpath, 'r') as file: text = file.read() lines = [line.strip() for line in text.split('\n')] lines = [line for line in lines if line and not line.startswith('#')] for line in lines: parts = line.split(',') tid = int(parts[0]) gname = parts[1] frame_index = int(parts[2]) if gname == '': if len(dset.imgs) == 0 or indexed_images: # I GUESS WE ARE SUPPOSED TO GUESS WHAT IMAGE IS WHICH if not indexed_images: indexed_images = lazy_image_list() try: gname = indexed_images[frame_index] except IndexError: continue else: # Also, VIAME-CSV lets the annotations run longer than the # image sequence, so account for that. # Skip this annotation continue tl_x, tl_y, br_x, br_y = map(float, parts[3:7]) w = br_x - tl_x h = br_y - tl_y bbox = [tl_x, tl_y, w, h] score = float(parts[7]) target_len = float(parts[8]) rest = parts[9:] catparts = [] rest_iter = iter(rest) for p in rest_iter: if p.startswith('('): catparts.append(p) final_parts = list(rest_iter) if final_parts: raise NotImplementedError catnames = rest[0::2] catscores = list(map(float, rest[1::2])) cat_to_score = ub.dzip(catnames, catscores) if cat_to_score: catname = ub.argmax(cat_to_score) cid = dset.ensure_category(name=catname) else: cid = None gid = dset.ensure_image(file_name=gname, frame_index=frame_index) kw = {} if target_len >= 0: kw['target_len'] = target_len if score >= 0: kw['score'] = score dset.add_annotation( image_id=gid, category_id=cid, track_id=tid, bbox=bbox, **kw ) return dset
def test_frames_are_in_order(): import kwcoco import ubelt as ub import random def is_sorted(x): return x == sorted(x) total_frames = 30 total_videos = 4 max_frames_per_video = 20 # Seed rng for reproducibility rng = random.Random(926960862) # Initialize empty dataset dset = kwcoco.CocoDataset() # Add some number of videos vidid_pool = [ dset.add_video('vid_{:03d}'.format(vididx)) for vididx in range(total_videos) ] vidid_to_frame_pool = { vidid: ub.oset(range(max_frames_per_video)) for vidid in vidid_pool } # Add some number of frames to the videos in a random order for imgidx in range(total_frames): vidid = rng.choice(vidid_pool) frame_pool = vidid_to_frame_pool[vidid] assert frame_pool, 'ran out of frames' frame_index = rng.choice(frame_pool) frame_pool.remove(frame_index) name = 'img_{:03d}'.format(imgidx) dset.add_image(video_id=vidid, frame_index=frame_index, name=name) # Test that our image ids are always ordered by frame ids vidid_to_gids = dset.index.vidid_to_gids gids_were_in_order = [] for vidid, gids in vidid_to_gids.items(): gids_were_in_order.append(is_sorted(gids)) frame_idxs = [dset.imgs[gid]['frame_index'] for gid in gids] # Note: this check is always valid assert is_sorted(frame_idxs), ( 'images in vidid_to_gids must be sorted by frame_index') # Note: this check has a chance of failing for other params / seeds assert not all(gids_were_in_order), ( 'the probability we randomly have ordered image ids is low, ' 'and 0 when we seed the rng') try: import sqlalchemy # NOQA except Exception: pass else: # Test that the sql view works too sql_dset = dset.view_sql(memory=True) vidid_to_gids = dict(sql_dset.index.vidid_to_gids) gids_were_in_order = [] for vidid, gids in vidid_to_gids.items(): gids_were_in_order.append(is_sorted(gids)) frame_idxs = [dset.imgs[gid]['frame_index'] for gid in gids] # Note: this check is always valid assert is_sorted(frame_idxs), ( 'images in vidid_to_gids must be sorted by frame_index') # Note: this check has a chance of failing for other params / seeds assert not all(gids_were_in_order), ( 'the probability we randomly have ordered image ids is low, ' 'and 0 when we seed the rng')
def eval_detections_cli(**kw): """ CommandLine: xdoctest -m ~/code/netharn/netharn/metrics/detect_metrics.py eval_detections_cli """ import scriptconfig as scfg import kwcoco class EvalDetectionCLI(scfg.Config): default = { 'true': scfg.Path(None, help='true coco dataset'), 'pred': scfg.Path(None, help='predicted coco dataset'), 'out_dpath': scfg.Path('./out', help='output directory') } pass config = EvalDetectionCLI() cmdline = kw.pop('cmdline', True) config.load(kw, cmdline=cmdline) true_coco = kwcoco.CocoDataset(config['true']) pred_coco = kwcoco.CocoDataset(config['pred']) from netharn.metrics.detect_metrics import DetectionMetrics dmet = DetectionMetrics.from_coco(true_coco, pred_coco) voc_info = dmet.score_voc() cls_info = voc_info['perclass'][0] tp = cls_info['tp'] fp = cls_info['fp'] fn = cls_info['fn'] tpr = cls_info['tpr'] ppv = cls_info['ppv'] fp = cls_info['fp'] # Compute the MCC as TN->inf thresh = cls_info['thresholds'] # https://erotemic.wordpress.com/2019/10/23/closed-form-of-the-mcc-when-tn-inf/ mcc_lim = tp / (np.sqrt(fn + tp) * np.sqrt(fp + tp)) f1 = 2 * (ppv * tpr) / (ppv + tpr) draw = False if draw: mcc_idx = mcc_lim.argmax() f1_idx = f1.argmax() import kwplot plt = kwplot.autoplt() kwplot.multi_plot( xdata=thresh, ydata=mcc_lim, xlabel='threshold', ylabel='mcc*', fnum=1, pnum=(1, 4, 1), title='MCC*', color=['blue'], ) plt.plot(thresh[mcc_idx], mcc_lim[mcc_idx], 'r*', markersize=20) plt.plot(thresh[f1_idx], mcc_lim[f1_idx], 'k*', markersize=20) kwplot.multi_plot( xdata=fp, ydata=tpr, xlabel='fp (fa)', ylabel='tpr (pd)', fnum=1, pnum=(1, 4, 2), title='ROC', color=['blue'], ) plt.plot(fp[mcc_idx], tpr[mcc_idx], 'r*', markersize=20) plt.plot(fp[f1_idx], tpr[f1_idx], 'k*', markersize=20) kwplot.multi_plot( xdata=tpr, ydata=ppv, xlabel='tpr (recall)', ylabel='ppv (precision)', fnum=1, pnum=(1, 4, 3), title='PR', color=['blue'], ) plt.plot(tpr[mcc_idx], ppv[mcc_idx], 'r*', markersize=20) plt.plot(tpr[f1_idx], ppv[f1_idx], 'k*', markersize=20) kwplot.multi_plot( xdata=thresh, ydata=f1, xlabel='threshold', ylabel='f1', fnum=1, pnum=(1, 4, 4), title='F1', color=['blue'], ) plt.plot(thresh[mcc_idx], f1[mcc_idx], 'r*', markersize=20) plt.plot(thresh[f1_idx], f1[f1_idx], 'k*', markersize=20)
def convert_voc_to_coco(dpath=None): # TODO: convert segmentation information classes = [ 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor' ] devkit_dpath = ensure_voc_data(dpath=dpath) root = out_dpath = dirname(devkit_dpath) dsets = [] d = _convert_voc_split(devkit_dpath, classes, 'train', 2012, root) dsets.append(d) d = _convert_voc_split(devkit_dpath, classes, 'train', 2007, root) dsets.append(d) d = _convert_voc_split(devkit_dpath, classes, 'val', 2012, root) dsets.append(d) d = _convert_voc_split(devkit_dpath, classes, 'val', 2007, root) dsets.append(d) d = _convert_voc_split(devkit_dpath, classes, 'test', 2007, root) dsets.append(d) if 0: import xdev xdev.view_directory(out_dpath) def reroot_imgs(dset, root): for img in dset.imgs.values(): img['file_name'] = relpath(img['file_name'], root) import kwcoco t1 = kwcoco.CocoDataset(join(out_dpath, 'voc-train-2007.mscoco.json')) t2 = kwcoco.CocoDataset(join(out_dpath, 'voc-train-2012.mscoco.json')) v1 = kwcoco.CocoDataset(join(out_dpath, 'voc-val-2007.mscoco.json')) v2 = kwcoco.CocoDataset(join(out_dpath, 'voc-val-2012.mscoco.json')) t = kwcoco.CocoDataset.union(t1, t2) t.tag = 'voc-train' t.fpath = join(root, t.tag + '.mscoco.json') v = kwcoco.CocoDataset.union(v1, v2) v.tag = 'voc-val' v.fpath = join(root, v.tag + '.mscoco.json') tv = kwcoco.CocoDataset.union(t1, t2, v1, v2) tv.tag = 'voc-trainval' tv.fpath = join(root, tv.tag + '.mscoco.json') print('t.fpath = {!r}'.format(t.fpath)) t.dump(t.fpath, newlines=True) print('v.fpath = {!r}'.format(v.fpath)) v.dump(v.fpath, newlines=True) print('tv.fpath = {!r}'.format(tv.fpath)) tv.dump(tv.fpath, newlines=True) if 0: tv.img_root = root import kwplot kwplot.autompl() tv.show_image(2) dsets = { 'train': t, 'vali': v, 'trainval': tv, } return dsets
def draw_true_and_pred_boxes(true_fpath, pred_fpath, gid, viz_fpath): """ How do you generally visualize gt and predicted bounding boxes together? Example: >>> import kwcoco >>> import ubelt as ub >>> from os.path import join >>> from kwcoco.demo.perterb import perterb_coco >>> # Create a working directory >>> dpath = ub.ensure_app_cache_dir('kwcoco/examples/draw_true_and_pred_boxes') >>> # Lets setup some dummy true data >>> true_dset = kwcoco.CocoDataset.demo('shapes2') >>> true_dset.fpath = join(dpath, 'true_dset.kwcoco.json') >>> true_dset.dump(true_dset.fpath, newlines=True) >>> # Lets setup some dummy predicted data >>> pred_dset = perterb_coco(true_dset, box_noise=100, rng=421) >>> pred_dset.fpath = join(dpath, 'pred_dset.kwcoco.json') >>> pred_dset.dump(pred_dset.fpath, newlines=True) >>> # >>> # We now have our true and predicted data, lets visualize >>> true_fpath = true_dset.fpath >>> pred_fpath = pred_dset.fpath >>> print('dpath = {!r}'.format(dpath)) >>> print('true_fpath = {!r}'.format(true_fpath)) >>> print('pred_fpath = {!r}'.format(pred_fpath)) >>> # Lets choose an image id to visualize and a path to write to >>> gid = 1 >>> viz_fpath = join(dpath, 'viz_{}.jpg'.format(gid)) >>> # The answer to the question is in the logic of this function >>> draw_true_and_pred_boxes(true_fpath, pred_fpath, gid, viz_fpath) """ import kwimage import kwcoco true_dset = kwcoco.CocoDataset(true_fpath) pred_dset = kwcoco.CocoDataset(pred_fpath) if __debug__: # I hope your image ids are aligned between datasets true_img = true_dset.imgs[gid] pred_img = pred_dset.imgs[gid] assert true_img['file_name'] == pred_img['file_name'] # Get the true/pred annotation dictionaries from the chosen image true_aids = true_dset.index.gid_to_aids[gid] pred_aids = pred_dset.index.gid_to_aids[gid] true_anns = [true_dset.index.anns[aid] for aid in true_aids] pred_anns = [pred_dset.index.anns[aid] for aid in pred_aids] # Create Detections from the coco annotation dictionaries true_dets = kwimage.Detections.from_coco_annots(true_anns, dset=true_dset) pred_dets = kwimage.Detections.from_coco_annots(pred_anns, dset=pred_dset) print('true_dets.boxes = {!r}'.format(true_dets.boxes)) print('pred_dets.boxes = {!r}'.format(pred_dets.boxes)) # Load the image fpath = true_dset.get_image_fpath(gid) canvas = kwimage.imread(fpath) # Use kwimage.Detections draw_on method to modify the image canvas = true_dets.draw_on(canvas, color='green') canvas = pred_dets.draw_on(canvas, color='blue') kwimage.imwrite(viz_fpath, canvas)
def _convert_voc_split(devkit_dpath, classes, split, year, root): """ split, year = 'train', 2012 split, year = 'train', 2007 """ import kwcoco import xml.etree.ElementTree as ET dset = kwcoco.CocoDataset(tag='voc-{}-{}'.format(split, year)) for catname in classes: dset.add_category(catname) gpaths, apaths = _read_split_paths(devkit_dpath, split, year) KNOWN = { 'object', 'segmented', 'size', 'source', 'filename', 'folder', 'owner' } for gpath, apath in ub.ProgIter(zip(gpaths, apaths)): tree = ET.parse(apath) troot = tree.getroot() top_level = list(troot) unknown = {e.tag for e in top_level} - KNOWN assert not unknown img = { 'file_name': relpath(gpath, root), 'width': int(tree.find('size').find('width').text), 'height': int(tree.find('size').find('height').text), 'depth': int(tree.find('size').find('depth').text), 'segmented': int(tree.find('segmented').text), 'source': {elem.tag: elem.text for elem in list(tree.find('source'))}, } assert img.pop('depth') == 3 owner = tree.find('owner') if owner is not None: img['owner'] = {elem.tag: elem.text for elem in list(owner)} gid = dset.add_image(**img) for obj in tree.findall('object'): bbox = obj.find('bndbox') # Make pixel indexes 0-based x1 = float(bbox.find('xmin').text) - 1 y1 = float(bbox.find('ymin').text) - 1 x2 = float(bbox.find('xmax').text) - 1 y2 = float(bbox.find('ymax').text) - 1 diffc = obj.find('difficult') difficult = 0 if diffc is None else int(diffc.text) catname = obj.find('name').text.lower().strip() w = x2 - x1 h = y2 - y1 ann = { 'bbox': [x1, y1, w, h], 'category_name': catname, 'difficult': difficult, 'weight': 1.0 - difficult, } cid = dset._alias_to_cat(ann.pop('category_name'))['id'] dset.add_annotation(image_id=gid, category_id=cid, **ann) dset.dump(join(root, dset.tag + '.mscoco.json'), newlines=True) return dset
def main(**kw): """ CommandLine: python $HOME/code/bioharn/dev/kwcoco_to_viame_csv.py \ --src /data/public/Aerial/US_ALASKA_MML_SEALION/2007/sealions_2007_v9.kwcoco.json \ --dst /data/public/Aerial/US_ALASKA_MML_SEALION/2007/sealions_2007_v9.viame.csv """ config = ConvertConfig(default=kw, cmdline=True) import kwcoco import kwimage import ubelt as ub coco_dset = kwcoco.CocoDataset(config['src']) csv_rows = [] for gid, img in ub.ProgIter(coco_dset.imgs.items(), total=coco_dset.n_images): gname = img['file_name'] aids = coco_dset.gid_to_aids[gid] frame_index = img.get('frame_index', 0) # vidid = img.get('video_id', None) for aid in aids: ann = coco_dset.anns[aid] cat = coco_dset.cats[ann['category_id']] catname = cat['name'] # just use annotation id if no tracks tid = ann.get('track_id', aid) # tracked_aids = tid_to_aids.get(tid, [aid]) # track_len = len(tracked_aids) tl_x, tl_y, br_x, br_y = kwimage.Boxes([ann['bbox']], 'xywh').toformat('tlbr').data[0].tolist() score = ann.get('score', 1) row = [ tid, # 1 - Detection or Track Unique ID gname, # 2 - Video or Image String Identifier frame_index, # 3 - Unique Frame Integer Identifier round(tl_x, 3), # 4 - TL-x (top left of the image is the origin: 0,0 round(tl_y, 3), # 5 - TL-y round(br_x, 3), # 6 - BR-x round(br_y, 3), # 7 - BR-y score, # 8 - Auxiliary Confidence (how likely is this actually an object) -1, # 9 - Target Length catname, # 10+ - category name score, # 11+ - category score ] # Optional fields for kp in ann.get('keypoints', []): if 'keypoint_category_id' in kp: cname = coco_dset._resolve_to_kpcat(kp['keypoint_category_id'])['name'] elif 'category_name' in kp: cname = kp['category_name'] elif 'category' in kp: cname = kp['category'] else: raise Exception(str(kp)) kp_x, kp_y = kp['xy'] row.append('(kp) {} {} {}'.format( cname, round(kp_x, 3), round(kp_y, 3))) note_fields = [ 'box_source', 'changelog', 'color', ] for note_key in note_fields: if note_key in ann: row.append('(note) {}: {}'.format(note_key, repr(ann[note_key]).replace(',', '<comma>'))) row = list(map(str, row)) for item in row: if ',' in row: print('BAD row = {!r}'.format(row)) raise Exception('comma is in a row field') row_str = ','.join(row) csv_rows.append(row_str) csv_text = '\n'.join(csv_rows) dst_fpath = config['dst'] print('dst_fpath = {!r}'.format(dst_fpath)) with open(dst_fpath, 'w') as file: file.write(csv_text)