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)