Commit 6bcf8f54 authored by Jon Crall's avatar Jon Crall
Browse files

Merge branch 'dev/0.2.13' into 'master'

Start branch for 0.2.13

See merge request !33
parents 83b83523 bbbd5d92
Pipeline #251235 passed with stages
in 5 minutes and 34 seconds
......@@ -5,12 +5,26 @@ 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.2.12 - Unreleased
## Version 0.2.13 - Unreleased
### Added
* Add `.images` to `Videos` 1D object.
* Initial `copy_assets` behavior for kwcoco subset.
### Changed
* Improved speed of repeated calls to FusedChannelSpec.coerce and normalize
### Fixed
* Fixed bug in delayed image where nans did not correctly change size when warped
* Fixed bug in delayed image where warps were not applied correctly to concatenated objects
## Version 0.2.12 - Released 2021-09-22
### Added
* Initial implementation of shorthand channels
* Parametarized `max_speed` of toydata objects
* Parameterized `max_speed` of toydata objects
* Add `combine_kwcoco_measures` function
* Add new API methods to ChannelSpec objects
......
......@@ -33,14 +33,16 @@ Python, this data structure is reasonably efficient.
>>> import kwcoco
>>> import json
>>> # Create demo data
>>> demo = CocoDataset.demo()
>>> demo = kwcoco.CocoDataset.demo()
>>> # Reroot can switch between absolute / relative-paths
>>> demo.reroot(absolute=True)
>>> # could also use demo.dump / demo.dumps, but this is more explicit
>>> text = json.dumps(demo.dataset)
>>> with open('demo.json', 'w') as file:
>>> file.write(text)
>>> # Read from disk
>>> self = CocoDataset('demo.json')
>>> self = kwcoco.CocoDataset('demo.json')
>>> # Add data
>>> cid = self.add_category('Cat')
......@@ -61,6 +63,7 @@ Python, this data structure is reasonably efficient.
>>> # Inspect data
>>> # xdoctest: +REQUIRES(module:kwplot)
>>> import kwplot
>>> kwplot.autompl()
>>> self.show_image(gid=1)
......@@ -92,14 +95,14 @@ Python, this data structure is reasonably efficient.
[[37, 6, 230, 240], [124, 96, 45, 18]]
>>> # built in conversions to efficient kwimage array DataStructures
>>> print(ub.repr2(annots.detections.data))
>>> print(ub.repr2(annots.detections.data, sv=1))
{
'boxes': <Boxes(xywh,
array([[ 37., 6., 230., 240.],
[124., 96., 45., 18.]], dtype=float32))>,
'class_idxs': np.array([5, 3], dtype=np.int64),
'keypoints': <PointsList(n=2) at 0x7f07eda33220>,
'segmentations': <PolygonList(n=2) at 0x7f086365aa60>,
'class_idxs': [5, 3],
'keypoints': <PointsList(n=2)>,
'segmentations': <PolygonList(n=2)>,
}
>>> gids = list(self.imgs.keys())
......@@ -258,7 +261,7 @@ The logic of this init is generated via:
mkinit ~/code/kwcoco/kwcoco/__init__.py
"""
__version__ = '0.2.12'
__version__ = '0.2.13'
__submodules__ = ['coco_dataset', 'abstract_coco_dataset']
......
......@@ -24,13 +24,14 @@ TODO:
TODO:
- [ ]: Use FusedChannelSpec as a member of ChannelSpec
- [ ]: Handle special slice suffix for length calculations
- [x]: Use FusedChannelSpec as a member of ChannelSpec
- [x]: Handle special slice suffix for length calculations
"""
import ubelt as ub
import six
import functools
import abc
import functools
import six
import ubelt as ub
import warnings
class BaseChannelSpec(ub.NiceRepr):
......@@ -154,13 +155,16 @@ class FusedChannelSpec(BaseChannelSpec):
"""
_alias_lut = {
'rgb': 'r|g|b',
'rgba': 'r|g|b|a',
'dxdy': 'dx|dy',
'fxfy': 'fx|fy',
'rgb': ['r', 'g', 'b'],
'rgba': ['r', 'g', 'b', 'a'],
'dxdy': ['dx', 'dy'],
'fxfy': ['fx', 'fy'],
}
_size_lut = {k: v.count('|') + 1 for k, v in _alias_lut.items()}
# Efficiency memorization of coerced string codes
_memo = {}
_size_lut = {k: len(v) for k, v in _alias_lut.items()}
def __init__(self, parsed, _is_normalized=False):
self.parsed = parsed
......@@ -168,9 +172,8 @@ class FusedChannelSpec(BaseChannelSpec):
self._is_normalized = _is_normalized
def __len__(self):
import warnings
if not self._is_normalized:
warnings.warn(ub.paragraph(
text = ub.paragraph(
'''
Length Definition for unormalized FusedChannelSpec is in flux.
......@@ -179,7 +182,8 @@ class FusedChannelSpec(BaseChannelSpec):
atomic codes. Currently it returns the number "unnormalized"
atomic codes. Normalizing the FusedChannelSpec object or using
"numel" will supress this warning.
'''))
''')
warnings.warn(text)
return len(self.parsed)
def __getitem__(self, index):
......@@ -219,13 +223,20 @@ class FusedChannelSpec(BaseChannelSpec):
>>> FusedChannelSpec.coerce(3)
>>> FusedChannelSpec.coerce(FusedChannelSpec(['a']))
"""
try:
# Efficiency hack
return cls._memo[data]
except (KeyError, TypeError):
pass
if isinstance(data, list):
self = cls(data)
elif isinstance(data, str):
self = cls.parse(data)
cls._memo[data] = self
elif isinstance(data, int):
# we know the number of channels, but not their names
self = cls(['u{}'.format(i) for i in range(data)])
cls._memo[data] = self
elif isinstance(data, cls):
self = data
elif isinstance(data, ChannelSpec):
......@@ -240,7 +251,6 @@ class FusedChannelSpec(BaseChannelSpec):
raise TypeError('unknown type {}'.format(type(data)))
return self
@ub.memoize_method
def normalize(self):
"""
Replace aliases with explicit single-band-per-code specs
......@@ -263,9 +273,11 @@ class FusedChannelSpec(BaseChannelSpec):
return self
norm_parsed = []
needed_normalization = False
for v in self.parsed:
if v in self._alias_lut:
norm_parsed.extend(self._alias_lut.get(v).split('|'))
norm_parsed.extend(self._alias_lut.get(v))
needed_normalization = True
else:
if ':' in v:
root, *slice_args = v.split(':')
......@@ -280,11 +292,15 @@ class FusedChannelSpec(BaseChannelSpec):
raise NotImplementedError
for idx in range(start, stop, step):
norm_parsed.append('{}.{}'.format(root, idx))
needed_normalization = True
else:
norm_parsed.append(v)
# self._alias_lut.get(v, v).split('|')
# norm_parsed = list(ub.flatten(
# for v in self.parsed))
if not needed_normalization:
# If we went through the normalized process and we didn't need it
# update ourself so we don't redo the work.
self._is_normalized = True
return self
normed = FusedChannelSpec(norm_parsed, _is_normalized=True)
return normed
......@@ -315,7 +331,7 @@ class FusedChannelSpec(BaseChannelSpec):
size_list = []
for v in self.parsed:
if v in self._alias_lut:
num = self._alias_lut.get(v).count('|') + 1
num = len(self._alias_lut.get(v))
else:
if ':' in v:
root, *slice_args = v.split(':')
......
......@@ -67,7 +67,9 @@ class CocoSubsetCLI(object):
Only applicable for dataset that contain videos.
Requries the "jq" python library is installed.
'''))
''')),
'copy_assets': scfg.Value(False, help='if True copy the assests to the new bundle directory'),
# TODO: Add more filter criteria
......@@ -118,6 +120,42 @@ class CocoSubsetCLI(object):
new_dset.fpath = config['dst']
print('Writing new_dset.fpath = {!r}'.format(new_dset.fpath))
new_dset.fpath = new_dset.fpath
if config['copy_assets']:
# Create a copy of the data, (currently only works for relative
# kwcoco files)
from os.path import join, dirname
import shutil
# new_dset.reroot(new_dset.bundle_dpath, old_prefix=dset.bundle_dpath)
tocopy = []
dstdirs = set()
for gid, new_img in new_dset.index.imgs.items():
old_img = dset.index.imgs[gid]
if new_img.get('file_name', None) is not None:
new_fpath = join(new_dset.bundle_dpath, new_img['file_name'])
old_fpath = join(dset.bundle_dpath, old_img['file_name'])
dstdirs.add(dirname(new_fpath))
tocopy.append((old_fpath, new_fpath))
new_aux_list = new_img.get('auxiliary', [])
old_aux_list = old_img.get('auxiliary', [])
for old_aux, new_aux in zip(old_aux_list, new_aux_list):
new_fpath = join(new_dset.bundle_dpath, new_aux['file_name'])
old_fpath = join(dset.bundle_dpath, old_aux['file_name'])
dstdirs.add(dirname(new_fpath))
tocopy.append((old_fpath, new_fpath))
# Ensure directories
for dpath in dstdirs:
ub.ensuredir(dpath)
pool = ub.JobPool(max_workers=4)
for src, dst in tocopy:
pool.submit(shutil.copy2, src, dst)
for future in pool.as_completed():
future.result()
new_dset.dump(new_dset.fpath, newlines=True)
......
......@@ -320,14 +320,14 @@ class MixinCocoAccessors(object):
"video" for loading in video space.
TODO:
- [ ] Currently can only take all or none of the channels from each
- [X] Currently can only take all or none of the channels from each
base-image / auxiliary dict. For instance if the main image is
r|g|b you can't just select g|b at the moment.
- [ ] The order of the channels in the delayed load should
- [X] The order of the channels in the delayed load should
match the requested channel order.
- [ ] TODO: add nans to bands that don't exist or throw an error
- [X] TODO: add nans to bands that don't exist or throw an error
Example:
>>> import kwcoco
......@@ -371,56 +371,40 @@ class MixinCocoAccessors(object):
>>> print('delayed = {!r}'.format(delayed))
>>> print('delayed.finalize() = {!r}'.format(delayed.finalize(as_xarray=True)))
"""
from kwcoco.util.util_delayed_poc import DelayedLoad, DelayedChannelConcat
from kwcoco.util.util_delayed_poc import DelayedChannelConcat
from kwimage.transform import Affine
from kwcoco.channel_spec import FusedChannelSpec
bundle_dpath = self.bundle_dpath
requested = channels
if requested is not None:
requested = FusedChannelSpec.coerce(requested)
def _delay_load_imglike(obj):
info = {}
fname = obj.get('file_name', None)
channels_ = obj.get('channels', None)
if channels_ is not None:
channels_ = FusedChannelSpec.coerce(channels_).normalize()
info['channels'] = channels_
width = obj.get('width', None)
height = obj.get('height', None)
if height is not None and width is not None:
info['dsize'] = dsize = (width, height)
else:
info['dsize'] = None
if fname is not None:
info['fpath'] = fpath = join(bundle_dpath, fname)
info['chan'] = DelayedLoad(fpath, channels=channels_, dsize=dsize)
return info
img = self.index.imgs[gid]
# obj = img
info = img_info = _delay_load_imglike(img)
chan_list = []
if info.get('chan', None) is not None:
# Get info about the primary image and check if its channels are
# requested (if it even has any)
info = img_info = self._delay_load_imglike(img)
if info.get('chan_construct', None) is not None:
include_flag = requested is None
if not include_flag:
if requested.intersection(info['channels']):
include_flag = True
if include_flag:
chan_list.append(info.get('chan', None))
chncls, chnkw = info['chan_construct']
chan = chncls(**chnkw)
chan_list.append(chan)
for aux in img.get('auxiliary', []):
info = _delay_load_imglike(aux)
aux_to_img = Affine.coerce(aux.get('warp_aux_to_img', None))
chan = info['chan']
info = self._delay_load_imglike(aux)
include_flag = requested is None
if not include_flag:
if requested.intersection(info['channels']):
include_flag = True
if include_flag:
aux_to_img = Affine.coerce(aux.get('warp_aux_to_img', None))
chncls, chnkw = info['chan_construct']
chan = chncls(**chnkw)
chan = chan.delayed_warp(
aux_to_img, dsize=img_info['dsize'])
chan_list.append(chan)
......@@ -454,6 +438,33 @@ class MixinCocoAccessors(object):
return delayed
def _delay_load_imglike(self, obj):
"""
Helper function for delayed_load
"""
from kwcoco.util.util_delayed_poc import DelayedLoad
from kwcoco.channel_spec import FusedChannelSpec
info = {}
fname = obj.get('file_name', None)
channels_ = obj.get('channels', None)
if channels_ is not None:
channels_ = FusedChannelSpec.coerce(channels_)
channels_ = channels_.normalize()
info['channels'] = channels_
width = obj.get('width', None)
height = obj.get('height', None)
if height is not None and width is not None:
info['dsize'] = dsize = (width, height)
else:
info['dsize'] = None
if fname is not None:
bundle_dpath = self.bundle_dpath
info['fpath'] = fpath = join(bundle_dpath, fname)
# Delaying this gives us a small speed boost
info['chan_construct'] = (DelayedLoad, dict(
fpath=fpath, channels=channels_, dsize=dsize))
return info
def load_image(self, gid_or_img, channels=None):
"""
Reads an image from disk and
......
......@@ -332,6 +332,19 @@ class Videos(ObjectList1D):
def __init__(self, ids, dset):
super().__init__(ids, dset, 'videos')
@property
def images(self):
"""
Example:
>>> import kwcoco
>>> self = kwcoco.CocoDataset.demo('vidshapes8').videos()
>>> print(self.images)
<ImageGroups(n=8, m=2.0, s=0.0)>
"""
return ImageGroups(
[self._dset.images(vidid=vidid) for vidid in self._ids],
self._dset)
class Images(ObjectList1D):
"""
......
......@@ -105,8 +105,10 @@ Example:
>>> subcrop.finalize()
>>> #
>>> msi_crop = delayed.delayed_crop((slice(10, 80), slice(30, 50)))
>>> subdata = msi_crop.delayed_warp(kwimage.Affine.scale(3), dsize='auto').take_channels('B11|B1')
>>> subdata.finalize()
>>> msi_warp = msi_crop.delayed_warp(kwimage.Affine.scale(3), dsize='auto')
>>> subdata = msi_warp.take_channels('B11|B1')
>>> final = subdata.finalize()
>>> assert final.shape == (210, 60, 2)
Example:
......@@ -270,6 +272,7 @@ class DelayedImageOperation(DelayedVisionOperation):
Operations that pertain only to images
"""
@profile
def delayed_crop(self, region_slices):
"""
Create a new delayed image that performs a crop in the transformed
......@@ -369,7 +372,7 @@ class DelayedImageOperation(DelayedVisionOperation):
def delayed_warp(self, transform, dsize=None):
"""
Delayedly transform the underlying data.
Delayed transform the underlying data.
Note:
this deviates from kwimage warp functions because instead of
......@@ -442,6 +445,7 @@ class DelayedIdentity(DelayedImageOperation):
# Hack
yield DelayedWarp(self, Affine(None), dsize=self.dsize)
@profile
def finalize(self):
final = self.sub_data
final = kwarray.atleast_nd(final, 3, front=False)
......@@ -457,6 +461,17 @@ class DelayedNans(DelayedImageOperation):
region_slices = (slice(5, 10), slice(1, 12))
delayed = self.delayed_crop(region_slices)
Example:
>>> from kwcoco.util.util_delayed_poc import * # NOQA
>>> dsize = (307, 311)
>>> c1 = DelayedNans(dsize=dsize, channels=channel_spec.FusedChannelSpec.coerce('foo'))
>>> c2 = DelayedLoad.demo('astro', dsize=dsize).load_shape(True)
>>> cat = DelayedChannelConcat([c1, c2])
>>> warped_cat = cat.delayed_warp(kwimage.Affine.scale(1.07), dsize=(328, 332))
>>> warped_cat.finalize()
#>>> cropped = warped_cat.delayed_crop((slice(0, 300), slice(0, 100)))
#>>> cropped.finalize().shape
"""
def __init__(self, dsize=None, channels=None):
self.meta = {}
......@@ -497,8 +512,13 @@ class DelayedNans(DelayedImageOperation):
def _optimize_paths(self, **kwargs):
# DEBUG_PRINT('DelayedLoad._optimize_paths')
# hack
yield DelayedWarp(self, Affine(None), dsize=self.dsize)
# if 'dsize' in kwargs:
# dsize = tuple(kwargs['dsize'])
# else:
dsize = self.dsize
yield DelayedWarp(self, Affine(None), dsize=dsize)
@profile
def finalize(self, **kwargs):
if 'dsize' in kwargs:
shape = tuple(kwargs['dsize'])[::-1] + (self.num_bands,)
......@@ -625,7 +645,11 @@ class DelayedLoad(DelayedImageOperation):
def _optimize_paths(self, **kwargs):
# DEBUG_PRINT('DelayedLoad._optimize_paths')
# hack
yield DelayedWarp(self, Affine(None), dsize=self.dsize)
# if 'dsize' in kwargs:
# dsize = tuple(kwargs['dsize'])
# else:
dsize = self.dsize
yield DelayedWarp(self, Affine(None), dsize=dsize)
# raise AssertionError('hack so this is not called')
def load_shape(self, use_channel_heuristic=False):
......@@ -676,6 +700,7 @@ class DelayedLoad(DelayedImageOperation):
def fpath(self):
return self.meta.get('fpath', None)
@profile
def finalize(self, **kwargs):
final = self.cache.get('final', None)
if final is None:
......@@ -790,6 +815,7 @@ class DelayedLoad(DelayedImageOperation):
new_dsize = (new_xstop - new_xstart,
new_ystop - new_ystart)
# TODO: it might be ok to remove this line
assert self._immediates['dsize'] is None, 'does not handle'
new = self.__class__(
......@@ -878,6 +904,7 @@ class DelayedLoad(DelayedImageOperation):
return new
@ub.memoize
def have_gdal():
try:
from osgeo import gdal
......@@ -1005,6 +1032,7 @@ class LazyGDalFrameFile(ub.NiceRepr):
from os.path import basename
return '.../' + basename(self.fpath)
@profile
def __getitem__(self, index):
"""
References:
......@@ -1204,7 +1232,7 @@ class DelayedFrameConcat(DelayedVideoOperation):
num_bands = nband_cands[0]
else:
raise ValueError(
'components must all have the same delayed size')
'components must all have the same delayed size: got {}'.format(nband_cands))
self.num_bands = num_bands
self.num_frames = len(self.frames)
self.meta = {
......@@ -1226,6 +1254,7 @@ class DelayedFrameConcat(DelayedVideoOperation):
w, h = self.dsize
return (self.num_frames, h, w, self.num_bands)
@profile
def finalize(self, **kwargs):
"""
Execute the final transform
......@@ -1301,6 +1330,32 @@ class DelayedFrameConcat(DelayedVideoOperation):
new = DelayedFrameConcat(new_frames)
return new
def delayed_warp(self, transform, dsize=None):
"""
Delayed transform the underlying data.
Note:
this deviates from kwimage warp functions because instead of
"output_dims" (specified in c-style shape) we specify dsize (w, h).
Returns:
DelayedWarp : new delayed transform a chained transform
"""
# warped = DelayedWarp(self, transform=transform, dsize=dsize)
# return warped
if dsize is None:
dsize = self.dsize
elif isinstance(dsize, str):
if dsize == 'auto':
dsize = _auto_dsize(transform, self.dsize)
new_frames = []
for frame in self.frames:
new_frame = frame.delayed_warp(transform, dsize=dsize)
new_frames.append(new_frame)
new = DelayedFrameConcat(new_frames)
return new
class DelayedChannelConcat(DelayedImageOperation):
"""
......@@ -1347,10 +1402,12 @@ class DelayedChannelConcat(DelayedImageOperation):
raise ValueError('No components to concatenate')
self.components = components
if dsize is None:
print('self.components = {!r}'.format(self.components))
dsize_cands = [comp.dsize for comp in self.components]
if not ub.allsame(dsize_cands):
raise ValueError(
'components must all have the same delayed size')
# 'components must all have the same delayed size')
'components must all have the same delayed size: got {}'.format(dsize_cands))
dsize = dsize_cands[0]
self.dsize = dsize
self.num_bands = sum(comp.num_bands for comp in self.components)
......@@ -1395,6 +1452,7 @@ class DelayedChannelConcat(DelayedImageOperation):
w, h = self.dsize
return (h, w, self.num_bands)
@profile
def finalize(self, **kwargs):
"""
Execute the final transform
......@@ -1408,9 +1466,34 @@ class DelayedChannelConcat(DelayedImageOperation):
import xarray as xr
final = xr.concat(stack, dim='c')
else:
for a in stack:
print('a = {!r}'.format(a.shape))
final = np.concatenate(stack, axis=2)
return final
def delayed_warp(self, transform, dsize=None):
"""
Delayed transform the underlying data.
Note:
this deviates from kwimage warp functions because instead of
"output_dims" (specified in c-style shape) we specify dsize (w, h).
Returns:
DelayedWarp : new delayed transform a chained transform
"""
if dsize is None:
dsize = self.dsize
elif isinstance(dsize, str):
if dsize == 'auto':
dsize = _auto_dsize(transform, self.dsize)
new_parts = []
for part in self.components:
new_frame = part.delayed_warp(transform, dsize=dsize)
new_parts.append(new_frame)
new = DelayedChannelConcat(new_parts)
return new