diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d0e21194aa5604a9197451a6821c3ef90794be7..d18c6182e150f2dbc0aa048dfa13f1647e09f8fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,21 @@ We are currently working on porting this changelog to the specifications in This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Version 0.8.6 - Unreleased +## Version 0.8.7 - Unreleased + + +### Changed +* `CocoDataset.conform` now returns a reference to `self` + +### Added +* Support segmentations in legacy `COCO.loadRes` + +### Fixed +* Perterb no longer fails if segmentations are not present +* Subset now correctly handles tracks + + +## Version 0.8.6 - Released 2024-11-30 ### Added * `segmentation_metrics` now has the ability to dump components of its visualization for more flexible figure aggregation. diff --git a/kwcoco/__init__.py b/kwcoco/__init__.py index 8ce2c157cf046adcc660387a117fe397ffa2d0e5..7dab3aaa7d04315c507f9fcdc4ad8d25b6491993 100644 --- a/kwcoco/__init__.py +++ b/kwcoco/__init__.py @@ -269,7 +269,7 @@ Testing: """ -__version__ = '0.8.6' +__version__ = '0.8.7' __submodules__ = { diff --git a/kwcoco/cli/coco_subset.py b/kwcoco/cli/coco_subset.py index 9b7a7a12cbbadd07fe4df981b888e02df0277b15..00129331a70f8be0c8be03f0f1b4c5669347cd1f 100644 --- a/kwcoco/cli/coco_subset.py +++ b/kwcoco/cli/coco_subset.py @@ -62,10 +62,10 @@ class CocoSubsetCLI(scfg.DataConfig): A json query (via the jq spec) that specifies which videos belong in the subset. Note, this is a passed as the body of the following jq query format string to filter valid ids - '.videos[] | select({select_images}) | .id'. + '.videos[] | select({select_videos}) | .id'. Examples for this argument are as follows: - '.file_name | startswith("foo")' will select only videos + '.name | startswith("foo")' will select only videos where the name starts with foo. Only applicable for dataset that contain videos. diff --git a/kwcoco/coco_dataset.py b/kwcoco/coco_dataset.py index be285524b7bdae1f4754333e9da7d7c1b1541a01..628eca3d283c1dda5a799e196a874344225f62f4 100644 --- a/kwcoco/coco_dataset.py +++ b/kwcoco/coco_dataset.py @@ -2562,6 +2562,9 @@ class MixinCocoStats: workers (int): number of parallel jobs for IO tasks + Returns: + Self: the inplace modified kwcoco dataset + Example: >>> import kwcoco >>> dset = kwcoco.CocoDataset.demo('shapes8') @@ -2667,6 +2670,9 @@ class MixinCocoStats: ann['segmentation'] = kwimage.MultiPolygon.from_shapely(fixed_mpoly).to_coco(style='orig') else: raise + # from numbers import Number + # if len(segm) and isinstance(segm[0], Number): + # segm = [segm] if 'keypoints' in ann: import kwimage @@ -2674,6 +2680,7 @@ class MixinCocoStats: # each category, currently it is arbitrary pts = kwimage.Points.from_coco(ann['keypoints'], classes=kpcats) ann['keypoints'] = pts.to_coco(style='orig') + return self def validate(self, **config): """ @@ -7124,6 +7131,8 @@ class CocoDataset(AbstractCocoDataset, MixinCocoAddRemove, MixinCocoStats, >>> sub_dset = self.subset(gids, copy=True) >>> assert len(sub_dset.index.videos) == 1 >>> assert len(self.index.videos) == 2 + >>> assert len(sub_dset.index.tracks) == 2 + >>> assert len(self.index.tracks) == 4 Example: >>> import kwcoco @@ -7174,9 +7183,14 @@ class CocoDataset(AbstractCocoDataset, MixinCocoAddRemove, MixinCocoStats, sub_aids = sorted([aid for gid in chosen_gids for aid in self.index.gid_to_aids.get(gid, [])]) new_dataset['annotations'] = list(ub.take(self.index.anns, sub_aids)) - new_dataset['img_root'] = self.dataset.get('img_root', None) - # TODO: handle tracks table. + if 'tracks' in self.dataset: + sub_track_ids = sorted(set( + ann.get('track_id', None) + for ann in new_dataset['annotations']) - {None}) + new_dataset['tracks'] = list(self.tracks(sub_track_ids).objs_iter()) + + new_dataset['img_root'] = self.dataset.get('img_root', None) if copy: from copy import deepcopy diff --git a/kwcoco/compat_dataset.py b/kwcoco/compat_dataset.py index 1c91c67ef25dd87b68c16082e93003798d802900..a5ac4427865a0bf221584d3a40b9cce7575d694c 100644 --- a/kwcoco/compat_dataset.py +++ b/kwcoco/compat_dataset.py @@ -247,21 +247,45 @@ class COCO(CocoDataset): """ aids = [ann['id'] for ann in anns] self.show_image(aids=aids, show_boxes=draw_bbox) - # raise NotImplementedError def loadRes(self, resFile): """ Load result file and return a result api object. Args: - resFile (str): file name of result file + resFile (str | ndarray | List[Dict]): + file name of result file or something else that resolves to a + json list of annotation dictionaries corresponding to + predictions. Returns: - object: res result api object + COCO: res result api object + + Example: + >>> from kwcoco.compat_dataset import * # NOQA + >>> import kwcoco + >>> from kwcoco.demo.perterb import perterb_coco + >>> truth = kwcoco.CocoDataset.demo('shapes8').conform(legacy=True) + >>> self = COCO(truth.dataset) + >>> dpath = ub.Path.appdir('kwcoco/tests/compat').ensuredir() + >>> pred = perterb_coco(truth) + >>> # Get a type of input loadRes accepts + >>> anns = pred.dataset['annotations'] + >>> # This function handles 4 different types of result annotations + >>> capn_anns = [(ub.udict(ann) | {'caption': 'stuff'}) & {'caption', 'image_id'} for ann in anns] + >>> bbox_anns = [ub.udict(ann) & {'bbox', 'image_id', 'category_id',} for ann in anns] + >>> sseg_anns = [ub.udict(ann) & {'segmentation', 'image_id', 'category_id'} for ann in anns] + >>> kpts_anns = [ub.udict(ann) & {'keypoints', 'image_id', 'category_id'} for ann in anns] + >>> # Ensure we can get a result object for each + >>> res = self.loadRes(capn_anns) + >>> res = self.loadRes(bbox_anns) + >>> res = self.loadRes(sseg_anns) + >>> res = self.loadRes(kpts_anns) """ import json import time import copy + import kwimage res = COCO() res.dataset['images'] = [img for img in self.dataset['images']] @@ -300,25 +324,31 @@ class COCO(CocoDataset): res.dataset['categories'] = copy.deepcopy( self.dataset['categories']) for id, ann in enumerate(anns): - # now only support compressed RLE format as segmentation - # results - raise NotImplementedError('havent ported mask results yet') + # Unlike the original, using kwimage will support multiple + # segmentation formats, even in legacy mode. + sseg = kwimage.Segmentation.coerce(ann['segmentation']) # ann['area'] = maskUtils.area(ann['segmentation']) - # if 'bbox' not in ann: - # ann['bbox'] = maskUtils.toBbox(ann['segmentation']) + if 'bbox' not in ann: + ann['bbox'] = sseg.to_multi_polygon().box().to_coco() + # ann['bbox'] = maskUtils.toBbox(ann['segmentation']) ann['id'] = id + 1 ann['iscrowd'] = 0 elif 'keypoints' in anns[0]: res.dataset['categories'] = copy.deepcopy( self.dataset['categories']) for id, ann in enumerate(anns): - s = ann['keypoints'] - x = s[0::3] - y = s[1::3] - x0, x1, y0, y1 = np.min(x), np.max(x), np.min(y), np.max(y) - ann['area'] = (x1 - x0) * (y1 - y0) + s = kwimage.Points.coerce(ann['keypoints']).to_coco() + if len(s): + x = s[0::3] + y = s[1::3] + x0, x1, y0, y1 = np.min(x), np.max(x), np.min(y), np.max(y) + ann['area'] = (x1 - x0) * (y1 - y0) + ann['bbox'] = [x0, y0, x1 - x0, y1 - y0] + else: + ann['area'] = 0 + ann['bbox'] = [0, 0, 0, 0] + print('Warning: annotation missing keypoints') ann['id'] = id + 1 - ann['bbox'] = [x0, y0, x1 - x0, y1 - y0] print('DONE (t={:0.2f}s)'.format(time.time() - tic)) res.dataset['annotations'] = anns @@ -368,7 +398,7 @@ class COCO(CocoDataset): Convert annotation which can be polygons, uncompressed RLE to RLE. Returns: - kwimage.Mask + Dict: containing the size and counts to define the RLE. Note: * This requires the C-extensions for kwimage to be installed (i.e. @@ -390,16 +420,17 @@ class COCO(CocoDataset): >>> self.conform(legacy=True) >>> orig = self._aspycoco().annToRLE(self.anns[1]) """ - from kwimage.structs.segmentation import _coerce_coco_segmentation + import kwimage aid = ann['id'] ann = self.anns[aid] t = self.imgs[ann['image_id']] h, w = t['height'], t['width'] data = ann['segmentation'] dims = (h, w) - sseg = _coerce_coco_segmentation(data, dims) + sseg = kwimage.Segmentation.coerce(data, dims=dims) try: - rle = sseg.to_mask(dims=dims).to_bytes_rle().data + mask = sseg.to_mask(dims=dims) + rle = mask.to_bytes_rle().data except NotImplementedError: raise NotImplementedError(( 'kwimage does not seem to have required ' @@ -433,12 +464,12 @@ class COCO(CocoDataset): >>> kwplot.imshow(diff) >>> kwplot.show_if_requested() """ - from kwimage.structs.segmentation import _coerce_coco_segmentation + import kwimage aid = ann['id'] ann = self.anns[aid] data = ann['segmentation'] dims = None - sseg = _coerce_coco_segmentation(data, dims) + sseg = kwimage.Segmentation.coerce(data, dims=dims) try: mask = sseg.to_mask() except Exception: diff --git a/kwcoco/demo/perterb.py b/kwcoco/demo/perterb.py index 9f8c4dd44c9d6de7a2c0f67bd11af515f955d1b9..6f4ea7d4f23b1debc3b73ff46f63959a8290a54f 100644 --- a/kwcoco/demo/perterb.py +++ b/kwcoco/demo/perterb.py @@ -12,8 +12,12 @@ def perterb_coco(coco_dset, **kwargs): cls_noise (int, default=0): null_pred (bool, default=False): with_probs (bool, default=False): + with_heatmaps (bool): default false + verbose (int): score_noise (float, default=0.2): hacked (int, default=1): + n_fp(int): num false positives + n_fn(int): num false negatives Example: >>> from kwcoco.demo.perterb import * # NOQA @@ -39,6 +43,7 @@ def perterb_coco(coco_dset, **kwargs): Ignore: import xdev + # broken due to argspec from kwcoco.demo.perterb import perterb_coco # NOQA defaultkw = xdev.get_func_kwargs(perterb_coco) for k, v in defaultkw.items(): @@ -134,7 +139,9 @@ def perterb_coco(coco_dset, **kwargs): old_cxywh = kwimage.Boxes([old_bbox], 'xywh').to_cxywh() new_cxywh = kwimage.Boxes([new_bbox], 'xywh').to_cxywh() - old_sseg = kwimage.Segmentation.coerce(ann['segmentation']) + old_sseg = ann.get('segmentation', None) + if old_sseg is not None: + old_sseg = kwimage.Segmentation.coerce(old_sseg) # Compute the transform of the box so we can modify the # other attributes (TODO: we could use a random affine transform @@ -142,10 +149,15 @@ def perterb_coco(coco_dset, **kwargs): offset = new_cxywh.data[0, 0:2] - old_cxywh.data[0, 0:2] scale = new_cxywh.data[0, 2:4] / old_cxywh.data[0, 2:4] old_to_new = kwimage.Affine.coerce(offset=offset, scale=scale) - new_sseg = old_sseg.warp(old_to_new) + + if old_sseg is not None: + new_sseg = old_sseg.warp(old_to_new) + else: + new_sseg = None # Overwrite the data - ann['segmentation'] = new_sseg.to_coco(style='new') + if new_sseg is not None: + ann['segmentation'] = new_sseg.to_coco(style='new') ann['bbox'] = [new_x, new_y, new_w, new_h] ann['score'] = float(true_score_RV(1)[0])