diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0fcd8dd573bfe1cd81208876b79bb64dd36717..142f4208fdb49d3cebc9f3fa990054870e02f4ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,16 @@ This changelog follows the specifications detailed in: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), although we have not yet reached a `1.0.0` release. -## Version 0.8.1 - Unreleased +## Version 0.8.2 - Unreleased + +### Deprecated +* Unused code in `utils/util_futures.py` + +### Fixed +* Bug where requesting a slice the same size as an grayscale image did not return a channel dimension + + +## Version 0.8.1 - Released 2025-04-01 ### Added * Added `finalize` argument to `CocoSampler.load_sample` which allows the diff --git a/README.rst b/README.rst index 6fe42f1318e17bc6f0c06f58bcf539bd7cb46e30..eff9b6044375f7dfcc0e51378b13b96b5de29a35 100644 --- a/README.rst +++ b/README.rst @@ -38,6 +38,15 @@ data is to access. However a faster cache can be built at the cost of disk space. Currently we have a "cog" and "npy" backend. Help is wanted to integrate backends for hdf5 and other medical / domain-specific formats. + +This module is best used with +`kwcoco `__, +`kwimage `__, and +`delayed-image `__. +It enables other modules like: +`kwcoco_dataloader `__. + + Installation ------------ @@ -48,9 +57,9 @@ The `ndsampler `_. package can be installe pip install ndsampler -Note that ndsampler depends on `kwimage `_, -where there is a known compatibility issue between `opencv-python `_ -and `opencv-python-headless `_. Please ensure that one +Note that ndsampler depends on `kwimage `__, +where there is a known compatibility issue between `opencv-python `__ +and `opencv-python-headless `__. Please ensure that one or the other (but not both) are installed as well: .. code-block:: bash diff --git a/docs/source/conf.py b/docs/source/conf.py index 1070cd46735de19c6100461a71f3ab452a5e1416..9e92e2feb54c285ad0889122d98ebd57068a9128 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -220,6 +220,8 @@ intersphinx_mapping = { # Requires that the repo have objects.inv 'kwarray': ('https://kwarray.readthedocs.io/en/latest/', None), 'kwimage': ('https://kwimage.readthedocs.io/en/latest/', None), + 'kwcoco': ('https://kwcoco.readthedocs.io/en/latest/', None), + 'delayed_image': ('https://delayed-image.readthedocs.io/en/latest/', None), # 'kwplot': ('https://kwplot.readthedocs.io/en/latest/', None), 'ndsampler': ('https://ndsampler.readthedocs.io/en/latest/', None), 'ubelt': ('https://ubelt.readthedocs.io/en/latest/', None), diff --git a/ndsampler/__init__.py b/ndsampler/__init__.py index 1ae47dd48843464dd816c34bedcf8fd34e6a3f51..34bef68112aed95f122e483828e82b7603703015 100644 --- a/ndsampler/__init__.py +++ b/ndsampler/__init__.py @@ -11,15 +11,19 @@ The ndsampler library | Pypi | https://pypi.org/project/ndsampler | +------------------+---------------------------------------------------------+ +For a quickstart see :mod:`ndsampler.coco_sampler`. -See the gitlab README for more details. +This module is best used with :mod:`kwcoco`, :mod:`kwimage`, and +:mod:`delayed_image`. + +See the `GitLab README `_ for more details. """ __autogen__ = """ mkinit ~/code/ndsampler/ndsampler/__init__.py --diff mkinit ~/code/ndsampler/ndsampler/__init__.py -w """ -__version__ = '0.8.1' +__version__ = '0.8.2' from ndsampler.utils.util_misc import (HashIdentifiable,) diff --git a/ndsampler/abstract_frames.py b/ndsampler/abstract_frames.py index 8f6d9cdbb6881afb923a444cd7fe00a1f34bba9b..00dc3ecb00204ba4db879db277f379d6416fa329 100644 --- a/ndsampler/abstract_frames.py +++ b/ndsampler/abstract_frames.py @@ -3,13 +3,6 @@ Fast access to subregions of images. This implements the core convert-and-cache-as-cog logic, which enables us to read from subregions of images quickly. - - -TODO: - - [X] Implement npy memmap backend - - [X] Implement gdal COG.TIFF backend - - [X] Use as COG if input file is a COG - - [X] Convert to COG if needed """ import numpy as np import ubelt as ub @@ -21,12 +14,6 @@ from ndsampler.utils import util_lru from ndsampler.frame_cache import (_ensure_image_cog, _ensure_image_npy) -try: - from xdev import profile -except Exception: - profile = ub.identity - - class Frames(object): """ Abstract implementation of Frames. @@ -339,7 +326,6 @@ class Frames(object): image_id = self.image_ids[index] return self.load_image(image_id) - @profile def load_region(self, image_id, region=None, channels=ub.NoParam, width=None, height=None): """ @@ -390,12 +376,15 @@ class Frames(object): _lru[image_id] = imgdata return imgdata - @profile def load_image(self, image_id, channels=ub.NoParam, cache=True, noreturn=False): """ Load the image data for a particular image id + Note: + CAREFUL: THIS NEEDS TO MAINTAIN A STABLE API. + OTHER PROJECTS DEPEND ON IT. + Args: image_id (int): the id of the image to load cache (bool, default=True): ensure and return the efficient backend @@ -406,9 +395,6 @@ class Frames(object): This is useful if you simply want to ensure the cached representation. - CAREFUL: THIS NEEDS TO MAINTAIN A STABLE API. - OTHER PROJECTS DEPEND ON IT. - Returns: ArrayLike: an indexable array like representation, possibly memmapped. @@ -651,7 +637,6 @@ class AlignableImageData(object): self.cache_backend = cache_backend self._channel_memcache = {} - @profile def _load_native_channel(self, chan_name, cache=True): """ Load a specific auxiliary channel, optionally caching it @@ -689,7 +674,6 @@ class AlignableImageData(object): _channel_memcache[cache_key] = data return data - @profile def _load_delayed_channel(self, chan_name, cache=True): height = self.pathinfo.get('height', None) width = self.pathinfo.get('width', None) @@ -726,7 +710,6 @@ class AlignableImageData(object): channels = [default_chan] return channels - @profile def _load_prefused_region(self, img_region, channels=ub.NoParam): """ Loads crops from multiple channels in their native coordinate system @@ -773,7 +756,6 @@ class AlignableImageData(object): } return prefused - @profile def _load_fused_region(self, img_region, channels=ub.NoParam): """ Loads crops from multiple channels in aligned base coordinates. diff --git a/ndsampler/category_tree.py b/ndsampler/category_tree.py index 24e9d0c8b9c1294a3e580e6889063a297f125461..60c47157c46f551c7131b4103336817824ffcf3a 100644 --- a/ndsampler/category_tree.py +++ b/ndsampler/category_tree.py @@ -17,8 +17,6 @@ import kwarray import functools import networkx as nx import ubelt as ub -# import torch -# import torch.nn.functional as F import numpy as np from kwcoco import CategoryTree as KWCOCO_CategoryTree # raw category tree diff --git a/ndsampler/coco_dataset.py b/ndsampler/coco_dataset.py index e2b0196eec1ab8cfc8369d4c0ce424a388270c7d..a342c6162e8077d264d0dab7fd2213389399b2c0 100644 --- a/ndsampler/coco_dataset.py +++ b/ndsampler/coco_dataset.py @@ -1,7 +1,5 @@ """ This module has moded to the kwcoco module - -mkinit kwcoco """ from kwcoco.coco_dataset import CocoDataset __all__ = ['CocoDataset'] diff --git a/ndsampler/coco_frames.py b/ndsampler/coco_frames.py index a6592f1bd8b5f1d82f54446440ad78efc8d5d961..58b9f61c188e97f3682170b8654a0d1a0bb26006 100644 --- a/ndsampler/coco_frames.py +++ b/ndsampler/coco_frames.py @@ -5,12 +5,6 @@ import warnings import ubelt as ub -try: - from xdev import profile -except Exception: - profile = ub.identity - - class CocoFrames(abstract_frames.Frames, util_misc.HashIdentifiable): """ wrapper around coco-style dataset to allow for getitem syntax @@ -69,7 +63,6 @@ class CocoFrames(abstract_frames.Frames, util_misc.HashIdentifiable): return super().load_region(image_id, region, width=width, height=height, channels=channels) - @profile def _build_pathinfo(self, image_id): """ Returns: diff --git a/ndsampler/coco_regions.py b/ndsampler/coco_regions.py index 3bc657b753d04d4ea90bd15bb30e0de0ab296e50..5de7e9f66dd323a8b6aeb6b88a665f3327100c11 100644 --- a/ndsampler/coco_regions.py +++ b/ndsampler/coco_regions.py @@ -28,12 +28,6 @@ from ndsampler.utils import util_misc from ndsampler import isect_indexer -try: - from xdev import profile -except Exception: - profile = ub.identity - - class MissingNegativePool(AssertionError): pass @@ -213,7 +207,6 @@ class CocoRegions(Targets, util_misc.HashIdentifiable, ub.NiceRepr): """ return self._lazy_isect_index() - @profile def _lazy_isect_index(self, verbose=None): if self._isect_index is None: # FIXME! Any use of cacher here should be wrapped in an @@ -295,7 +288,6 @@ class CocoRegions(Targets, util_misc.HashIdentifiable, ub.NiceRepr): self._neg_anchors = neg_anchors return self._neg_anchors - @profile def overlapping_aids(self, gid, region, visible_thresh=0.0): """ Finds the other annotations in this image that overlap a region @@ -747,12 +739,9 @@ class CocoRegions(Targets, util_misc.HashIdentifiable, ub.NiceRepr): return cacher -@profile def tabular_coco_targets(dset): """ Transforms COCO box annotations into a tabular form - - _ = xdev.profile_now(tabular_coco_targets)(dset) """ import warnings # TODO: better handling of non-bounding box annotations; ignore for now @@ -818,7 +807,6 @@ def tabular_coco_targets(dset): return targets -@profile def select_positive_regions(targets, window_dims=(300, 300), thresh=0.0, rng=None, verbose=0): """ diff --git a/ndsampler/coco_sampler.py b/ndsampler/coco_sampler.py index 5f167d074203a95156721ef81f511ecc945c3f7d..55b0392ba94edf11ad1bba8092d90ab8447f833b 100644 --- a/ndsampler/coco_sampler.py +++ b/ndsampler/coco_sampler.py @@ -1,6 +1,7 @@ """ The CocoSampler is the ndsampler interface for efficiently sampling windowed -data from a :class:`kwcoco.CocoDataset`. +data from a :class:`kwcoco.CocoDataset`. The following example illustrates +basic usage: CommandLine: xdoctest -m ndsampler.coco_sampler __doc__ --show @@ -82,11 +83,6 @@ from ndsampler.utils import util_misc from delayed_image.channel_spec import FusedChannelSpec from delayed_image.channel_spec import ChannelSpec -try: - from xdev import profile -except Exception: - profile = ub.identity - class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, ub.NiceRepr): @@ -106,7 +102,6 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, details. Defaults to None, which does not do anything fancy. Example: - #print >>> from ndsampler.coco_sampler import * >>> self = CocoSampler.demo('photos') ... @@ -226,6 +221,15 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, """ DEPRECATED, use self.classes instead """ + ub.schedule_deprecation( + 'ndsampler', + 'catgraph', + 'property' + 'Use .classes instead', + deprecate='0.8.2', + error='0.9.0', + remove='1.0.0', + ) if self.regions is None: return None return self.regions.classes @@ -372,8 +376,9 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, keypoints, and segmentation. Defaults to True. target (Dict): Extra target arguments that update the positive target, - like window_dims, pad, etc.... See :func:`load_sample` for - details on allowed keywords. + like window_dims, pad, etc... See + :func:`CocoSampler.load_sample` for details on allowed + keywords. rng (None | int | RandomState): a seed or seeded random number generator. @@ -382,11 +387,14 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, Returns: Dict: sample: dict containing keys + im (ndarray): image data + target (dict): contains the same input items as the input target but additionally specifies inferred information like rel_cx and rel_cy, which gives the center of the target w.r.t the returned **padded** sample. + annots (dict): Dict of aids, cids, and rel/abs boxes """ if index < self.n_positives: @@ -424,10 +432,13 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, Returns: Dict: sample: dict containing keys + im (ndarray): image data + tr (dict): contains the same input items as tr but additionally specifies rel_cx and rel_cy, which gives the center of the target w.r.t the returned **padded** sample. + annots (dict): Dict of aids, cids, and rel/abs boxes Example: @@ -908,7 +919,6 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, target_['vidid'] = target_['video_id'] return target_ - @profile def _infer_target_attributes(self, target, **kwargs): """ Infer unpopulated target attributes @@ -1104,13 +1114,12 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, raise NotImplementedError(ndim) return target_ - @profile def _load_slice(self, target_): """ Called by load_sample after the target dictionary has been resolved. CommandLine: - xdoctest -m ndsampler.coco_sampler CocoSampler._load_slice --profile + xdoctest -m ndsampler.coco_sampler CocoSampler._load_slice Example: >>> # sample an out of bounds target @@ -1247,7 +1256,6 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, sample['tr'] = sample['target'] return sample - @profile def _load_slice_3d(self, target_): """ Breakout the 2d vs 3d logic so they can evolve somewhat independently. @@ -1598,6 +1606,11 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, frame = frame.astype(dtype) else: frame = to_finalize + + # workaround bug where channel dimension is not always returned + # from delayed image if there is no crop operation. + if len(frame.shape) < 3: + frame = frame[..., None] space_frames.append(frame) # warp_sample_from_grid_alt = delayed_crop.get_transform_from(delayed_frame) @@ -1644,7 +1657,6 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, sample['params']['jagged_meta'] = jagged_meta return sample - @profile def _load_slice_2d(self, target): """ Breakout the 2d vs 3d logic so they can evolve somewhat independently. @@ -1777,7 +1789,6 @@ class CocoSampler(abstract_sampler.AbstractSampler, util_misc.HashIdentifiable, } return sample - @profile def _populate_overlap(self, sample, visible_thresh=0.1, with_annots=True, annot_ids=None): """ diff --git a/ndsampler/coerce_data.py b/ndsampler/coerce_data.py index fb64eb7ffd9f0072abec8abc0857074913134328..3857943ef8650472b770e6ad1c82c6f1af7982ef 100644 --- a/ndsampler/coerce_data.py +++ b/ndsampler/coerce_data.py @@ -1,5 +1,5 @@ """ -Moved to netharn +DEPRECATED. This module has moved to netharn. """ import ubelt as ub @@ -25,7 +25,7 @@ def coerce_datasets(config, build_hashid=False, verbose=1): >>> dsets = ndsampler.coerce_data.coerce_datasets(config) >>> print('dsets = {!r}'.format(dsets)) - >>> config = {'datasets': 'special:shapes256'} + >>> config = {'datasets': 'special:shapes8'} >>> ndsampler.coerce_data.coerce_datasets(config) >>> config = { @@ -41,6 +41,12 @@ def coerce_datasets(config, build_hashid=False, verbose=1): >>> 'test_dataset': kwcoco.CocoDataset.demo('photos'), >>> }) """ + ub.schedule_deprecation( + modname='ndsampler', name='coerce_datasets', type='function', + migration='This is unused in ndsampler and will be removed. ' + 'Vendor the function if you need it.', + deprecate='0.8.2', error='0.9.0', remove='1.0.0') + # Ideally the user specifies a standard train/vali/test split def _rectify_fpath(key): fpath = key diff --git a/ndsampler/frame_cache.py b/ndsampler/frame_cache.py index 0348b82152a9f39027bf4845e51600fce5a4acb4..bf9df65c1ceabe6032ee01d37dbfc675d1220832 100644 --- a/ndsampler/frame_cache.py +++ b/ndsampler/frame_cache.py @@ -24,7 +24,6 @@ class CorruptCOG(Exception): pass -# @profile def _cog_cache_write(gpath, cache_gpath, config=None): """ CommandLine: diff --git a/ndsampler/isect_indexer.py b/ndsampler/isect_indexer.py index 4642d058ab4b2e6c9bb37cf568de9a6685f6467d..f8622ba8064abd3d5a29284e9227d1221b511699 100644 --- a/ndsampler/isect_indexer.py +++ b/ndsampler/isect_indexer.py @@ -17,11 +17,6 @@ except ImportError: import pyqtree rtree = None -try: - from xdev import profile -except Exception: - profile = ub.identity - class FrameIntersectionIndex(ub.NiceRepr): """ @@ -89,7 +84,6 @@ class FrameIntersectionIndex(ub.NiceRepr): return self @staticmethod - @profile def _build_index(dset, verbose=0): """ """ @@ -157,7 +151,6 @@ class FrameIntersectionIndex(ub.NiceRepr): qtree.aid_to_ltrb[aid] = ltrb_box return qtrees - @profile def overlapping_aids(self, gid, box): """ Find all annotation-ids within an image that have some overlap with a diff --git a/ndsampler/toydata.py b/ndsampler/toydata.py index 7928c956b0627dbf568360d6879f12868fa884a7..a1ecad833f05c8733097c8e74c75074dd7a73644 100644 --- a/ndsampler/toydata.py +++ b/ndsampler/toydata.py @@ -1,3 +1,6 @@ +""" +DEPRECATE: this functionality has moved to kwcoco +""" import numpy as np from ndsampler import abstract_sampler from ndsampler import category_tree diff --git a/ndsampler/utils/util_futures.py b/ndsampler/utils/util_futures.py index 966fabb1c6a1bd169a3c9b3df6f4e1e7fcf17d68..bf417cdc0e0518a660bdac9a026b2a6f924f1200 100644 --- a/ndsampler/utils/util_futures.py +++ b/ndsampler/utils/util_futures.py @@ -1,166 +1,10 @@ """ -TODO: Use the ubelt variants of these instead. +DEPRECATED + +Use the ubelt variants of these instead. """ -import concurrent.futures from concurrent.futures import as_completed +from ubelt.util_futures import SerialExecutor +from ubelt import Executor __all__ = ['Executor', 'SerialExecutor', 'as_completed'] - - -# class FakeCondition(object): -# def acquire(self): -# pass - -# def release(self): -# pass - - -class SerialFuture( concurrent.futures.Future): - """ - Non-threading / multiprocessing version of future for drop in compatibility - with concurrent.futures. - """ - def __init__(self, func, *args, **kw): - super(SerialFuture, self).__init__() - self.func = func - self.args = args - self.kw = kw - # self._condition = FakeCondition() - self._run_count = 0 - # fake being finished to cause __get_result to be called - self._state = concurrent.futures._base.FINISHED - - def _run(self): - result = self.func(*self.args, **self.kw) - self.set_result(result) - self._run_count += 1 - - def set_result(self, result): - """ - Overrides the implementation to revert to pre python3.8 behavior - """ - with self._condition: - self._result = result - self._state = concurrent.futures._base.FINISHED - for waiter in self._waiters: - waiter.add_result(self) - self._condition.notify_all() - self._invoke_callbacks() - - def _Future__get_result(self): - # overrides private __getresult method - if not self._run_count: - self._run() - return self._result - - -class SerialExecutor(object): - """ - Implements the concurrent.futures API around a single-threaded backend - - Example: - >>> with SerialExecutor() as executor: - >>> futures = [] - >>> for i in range(100): - >>> f = executor.submit(lambda x: x + 1, i) - >>> futures.append(f) - >>> for f in concurrent.futures.as_completed(futures): - >>> assert f.result() > 0 - >>> for i, f in enumerate(futures): - >>> assert i + 1 == f.result() - """ - def __enter__(self): - self.max_workers = 0 - return self - - def __exit__(self, ex_type, ex_value, tb): - pass - - def submit(self, func, *args, **kw): - return SerialFuture(func, *args, **kw) - - def shutdown(self): - pass - - -class Executor(object): - """ - Wrapper around a specific executor. - - Abstracts Serial, Thread, and Process Executor via arguments. - - Args: - mode (str, default='thread'): either thread, serial, or process - max_workers (int, default=0): number of workers. If 0, serial is forced. - """ - - def __init__(self, mode='thread', max_workers=0): - from concurrent import futures - if mode == 'serial' or max_workers == 0: - backend = SerialExecutor() - elif mode == 'thread': - backend = futures.ThreadPoolExecutor(max_workers=max_workers) - elif mode == 'process': - backend = futures.ProcessPoolExecutor(max_workers=max_workers) - else: - raise KeyError(mode) - self.backend = backend - - def __enter__(self): - return self.backend.__enter__() - - def __exit__(self, ex_type, ex_value, tb): - return self.backend.__exit__(ex_type, ex_value, tb) - - def submit(self, func, *args, **kw): - return self.backend.submit(func, *args, **kw) - - def shutdown(self): - return self.backend.shutdown() - - -class JobPool(object): - """ - Abstracts away boilerplate of submitting and collecting jobs - - Example: - >>> def worker(data): - >>> return data + 1 - >>> pool = JobPool('thread', max_workers=16) - >>> import ubelt as ub - >>> with pool: - >>> for data in ub.ProgIter(range(10), desc='submit jobs'): - >>> job = pool.submit(worker, data) - >>> final = [] - >>> for job in ub.ProgIter(pool.as_completed(), total=len(pool), desc='collect jobs'): - >>> info = job.result() - >>> final.append(info) - >>> print('final = {!r}'.format(final)) - """ - def __init__(self, mode='thread', max_workers=0): - self.executor = Executor(mode=mode, max_workers=max_workers) - self.jobs = [] - - def __len__(self): - return len(self.jobs) - - def submit(self, func, *args, **kwargs): - job = self.executor.submit(func, *args, **kwargs) - self.jobs.append(job) - return job - - def __enter__(self): - self.executor.__enter__() - return self - - def __exit__(self, a, b, c): - self.executor.__exit__(a, b, c) - - def as_completed(self): - from concurrent.futures import as_completed - for job in as_completed(self.jobs): - yield job - - def __iter__(self): - for job in self.as_completed(): - yield job diff --git a/ndsampler/utils/util_gdal.py b/ndsampler/utils/util_gdal.py index f6a2b98b8589421836e39239917e10e0573b496a..f7904d5f0338dcc13adfb76ef600d46d3f384bf5 100644 --- a/ndsampler/utils/util_gdal.py +++ b/ndsampler/utils/util_gdal.py @@ -4,13 +4,6 @@ import numpy as np import ubelt as ub -try: - import xdev - profile = xdev.profile -except ImportError: - profile = ub.identity - - def have_gdal(): try: from osgeo import gdal @@ -133,7 +126,6 @@ def _benchmark_cog_conversions(): assert not len(validate(dst_data_fpath)[1]) -@profile def _imwrite_cloud_optimized_geotiff(fpath, data, compress='auto', blocksize=256): """ diff --git a/ndsampler/utils/util_misc.py b/ndsampler/utils/util_misc.py index 7095dcdfb4de62711999b67aae8fe4016dd301cb..110675ce999d564eeef3f1163841f25a056d21bd 100644 --- a/ndsampler/utils/util_misc.py +++ b/ndsampler/utils/util_misc.py @@ -12,19 +12,20 @@ class HashIdentifiable(object): * define `_hashid` Example: - class Base: - def __init__(self): - # commenting the next line removes cooperative inheritance - super().__init__() - self.base = 1 - - class Derived(Base, HashIdentifiable): - def __init__(self): - super().__init__() - self.defived = 1 - - self = Derived() - dir(self) + >>> from ndsampler.utils.util_misc import * # NOQA + >>> class Base: + >>> def __init__(self): + >>> # commenting the next line removes cooperative inheritance + >>> super().__init__() + >>> self.base = 1 + >>> # + >>> class Derived(Base, HashIdentifiable): + >>> def __init__(self): + >>> super().__init__() + >>> self.defived = 1 + >>> # + >>> self = Derived() + >>> assert {'_depends', 'hashid', '_make_hashid'}.issubset(set(dir(self))) """ def __init__(self, **kwargs): super(HashIdentifiable, self).__init__(**kwargs) diff --git a/ndsampler/utils/util_shape.py b/ndsampler/utils/util_shape.py index ccada527edcde9dfa8ba1711016964ced34d850c..b621dffdc271e4dc93e6ccfd05067037807e3bee 100644 --- a/ndsampler/utils/util_shape.py +++ b/ndsampler/utils/util_shape.py @@ -5,12 +5,20 @@ def nestshape(data): """ Examine nested shape of the data + CommandLine: + xdoctest -m ndsampler.utils.util_shape nestshape + Example: >>> data = [np.arange(10), np.arange(13)] >>> nestshape(data) [(10,), (13,)] """ import ubelt as ub + ub.schedule_deprecation( + modname='ndsampler', name='netshape', type='function', + migration='This is unused in ndsampler and will be removed. ' + 'Vendor the function if you need it.', + deprecate='0.8.2', error='0.9.0', remove='1.0.0') def _recurse(d): try: diff --git a/requirements/runtime.txt b/requirements/runtime.txt index fa478e8e6b609a50244c4f2e71d824f4ebbb2598..03cff06f1b9b6cf7596fe69db653c5bbc13d6822 100644 --- a/requirements/runtime.txt +++ b/requirements/runtime.txt @@ -7,6 +7,9 @@ parse >= 1.19.0 xarray>=2023.10.0 ; python_version < '4.0' and python_version >= '3.12' # Python 3.13+ xarray>=0.17.0 ; python_version < '3.12' # Python 3.12- +# Required by xarray==2025.6.0 +typing_extensions>=4.8.0 + # tensorflow requires 1.19.3 numpy>=2.1.0 ; python_version < '4.0' and python_version >= '3.13' # Python 3.13+ numpy>=1.26.0 ; python_version < '3.13' and python_version >= '3.12' # Python 3.12 diff --git a/tests/test_sampling_shapes.py b/tests/test_sampling_shapes.py new file mode 100644 index 0000000000000000000000000000000000000000..48b46ca37189b61f4be2ae29a77deac8a2bc33b2 --- /dev/null +++ b/tests/test_sampling_shapes.py @@ -0,0 +1,54 @@ +def test_whole_frame_sample_shape(): + """ + <0.8.2 had a bug where requesting a full frame did not return data with a + channel dimension. + """ + import kwcoco + import ndsampler + import pytest + try: + import lark # NOQA + except ImportError: + pytest.skip('requires lark') + + dset = kwcoco.CocoDataset.demo('vidshapes1', image_size=128, num_frames=1, sensorchan='gray') + + # Hack away any transforms so we can access the data in its full dims easy + # (todo: would be nice if kwcoco had a way to not generate asset to image warps) + coco_img = dset.coco_image(1) + for asset in coco_img.assets: + asset.pop('warp_aux_to_img', None) + W = coco_img.img['width'] = asset['width'] + H = coco_img.img['height'] = asset['height'] + + sampler = ndsampler.CocoSampler(dset) + + # Sample is the full image frame, no cropping. Previously this failed. + target = { + 'space_slice': (slice(0, H), slice(0, W)), + 'gids': [1], + 'channels': 'gray', + 'verbose_ndsample': 0, + } + # image_fpath = dset.coco_image(1).primary_image_filepath() + # print(f'image_fpath={image_fpath}') + sample = sampler.load_sample(target, with_annots=False) + assert sample['im'].shape == (1, H, W, 1) + + # Also test case where the shape is less + target = { + 'space_slice': (slice(0, H - 2), slice(0, W - 2)), + 'gids': [1], + 'channels': 'gray', + } + sample = sampler.load_sample(target, with_annots=False) + assert sample['im'].shape == (1, H - 2, W - 2, 1) + + # Also test case where the shape is more + target = { + 'space_slice': (slice(0, H + 2), slice(0, W + 2)), + 'gids': [1], + 'channels': 'gray', + } + sample = sampler.load_sample(target, with_annots=False) + assert sample['im'].shape == (1, H + 2, W + 2, 1)