From f53942f7f68e7b36c43babd797e020a7f536097d Mon Sep 17 00:00:00 2001 From: joncrall Date: Sat, 30 Nov 2024 19:01:51 -0500 Subject: [PATCH 1/8] Start branch for 0.8.7 --- CHANGELOG.md | 5 ++++- kwcoco/__init__.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d0e2119..0b81d531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,10 @@ 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 + + +## 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 8ce2c157..7dab3aaa 100644 --- a/kwcoco/__init__.py +++ b/kwcoco/__init__.py @@ -269,7 +269,7 @@ Testing: """ -__version__ = '0.8.6' +__version__ = '0.8.7' __submodules__ = { -- GitLab From 92d1e63b89e2c582fe51c627ac0c1776426ced8a Mon Sep 17 00:00:00 2001 From: joncrall Date: Sun, 1 Dec 2024 14:37:41 -0500 Subject: [PATCH 2/8] feat: support segmentations in legacy loadRes --- CHANGELOG.md | 7 ++++++ kwcoco/coco_dataset.py | 7 ++++++ kwcoco/compat_dataset.py | 53 +++++++++++++++++++++++++++++----------- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b81d531..0851408d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Version 0.8.7 - Unreleased +### Changed +* `CocoDataset.conform` now returns a reference to `self` + +### Added +* Support segmentations in legacy `COCO.loadRes` + + ## Version 0.8.6 - Released 2024-11-30 ### Added diff --git a/kwcoco/coco_dataset.py b/kwcoco/coco_dataset.py index be285524..cd4c32ab 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): """ diff --git a/kwcoco/compat_dataset.py b/kwcoco/compat_dataset.py index 1c91c67e..01f3428a 100644 --- a/kwcoco/compat_dataset.py +++ b/kwcoco/compat_dataset.py @@ -247,21 +247,40 @@ 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) + >>> anns = pred.dataset['annotations'] + >>> 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] + >>> 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 +319,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 -- GitLab From 5cef571b37e4651297d64987b2b40d0aaa3e4024 Mon Sep 17 00:00:00 2001 From: joncrall Date: Sun, 1 Dec 2024 14:48:05 -0500 Subject: [PATCH 3/8] refactor: use kwimage.Segmentation.coerce instead of private method --- kwcoco/compat_dataset.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/kwcoco/compat_dataset.py b/kwcoco/compat_dataset.py index 01f3428a..a5ac4427 100644 --- a/kwcoco/compat_dataset.py +++ b/kwcoco/compat_dataset.py @@ -269,10 +269,15 @@ class COCO(CocoDataset): >>> 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) @@ -393,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. @@ -415,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 ' @@ -458,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: -- GitLab From 7ff80231a9de1aa48b665dd9e379f9a096ca880a Mon Sep 17 00:00:00 2001 From: joncrall Date: Tue, 10 Dec 2024 17:39:06 -0500 Subject: [PATCH 4/8] fix: perterb no longer fails when segmentation is not available --- CHANGELOG.md | 3 +++ kwcoco/demo/perterb.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0851408d..90eeadc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added * Support segmentations in legacy `COCO.loadRes` +### Fixed +* Perterb no longer fails if segmentations are not present + ## Version 0.8.6 - Released 2024-11-30 diff --git a/kwcoco/demo/perterb.py b/kwcoco/demo/perterb.py index 9f8c4dd4..56d0990b 100644 --- a/kwcoco/demo/perterb.py +++ b/kwcoco/demo/perterb.py @@ -134,7 +134,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 +144,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]) -- GitLab From 59ac409f166543385d9c831f9b08711f6d2876ea Mon Sep 17 00:00:00 2001 From: joncrall Date: Wed, 11 Dec 2024 19:51:16 -0500 Subject: [PATCH 5/8] fix: fixed incorrect docs for kwcoco subset --- kwcoco/cli/coco_subset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kwcoco/cli/coco_subset.py b/kwcoco/cli/coco_subset.py index 9b7a7a12..00129331 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. -- GitLab From 4df7d4c0ad7d53eee139976d76656e08e94a8189 Mon Sep 17 00:00:00 2001 From: "jon.crall" Date: Mon, 16 Dec 2024 21:23:15 -0500 Subject: [PATCH 6/8] fix: handle tracks in subset --- CHANGELOG.md | 1 + kwcoco/coco_dataset.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90eeadc4..d18c6182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixed * Perterb no longer fails if segmentations are not present +* Subset now correctly handles tracks ## Version 0.8.6 - Released 2024-11-30 diff --git a/kwcoco/coco_dataset.py b/kwcoco/coco_dataset.py index cd4c32ab..2c8bcd7a 100644 --- a/kwcoco/coco_dataset.py +++ b/kwcoco/coco_dataset.py @@ -7131,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 @@ -7181,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'])) + 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 -- GitLab From 21bb6dfdd87391ca640a7d1b4469f826c90ae9ac Mon Sep 17 00:00:00 2001 From: "jon.crall" Date: Wed, 18 Dec 2024 12:05:34 -0500 Subject: [PATCH 7/8] fix: ignore None in track union --- kwcoco/coco_dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kwcoco/coco_dataset.py b/kwcoco/coco_dataset.py index 2c8bcd7a..628eca3d 100644 --- a/kwcoco/coco_dataset.py +++ b/kwcoco/coco_dataset.py @@ -7187,7 +7187,7 @@ class CocoDataset(AbstractCocoDataset, MixinCocoAddRemove, MixinCocoStats, if 'tracks' in self.dataset: sub_track_ids = sorted(set( ann.get('track_id', None) - for ann in new_dataset['annotations'])) + 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) -- GitLab From d5a01d396da44f705397f315add2c7eaa6133a09 Mon Sep 17 00:00:00 2001 From: "jon.crall" Date: Wed, 18 Dec 2024 12:12:30 -0500 Subject: [PATCH 8/8] docs: add missing args to perterb docs --- kwcoco/demo/perterb.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/kwcoco/demo/perterb.py b/kwcoco/demo/perterb.py index 56d0990b..6f4ea7d4 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(): -- GitLab