...
 
Commits (25)
......@@ -525,6 +525,15 @@ deploy/py2.py3-none-any:
# todo, can we tag the branch here as well?
# note TWINE_USERNAME and TWINE_PASSWORD are protected variables only available on master and release branch
- twine upload --username $TWINE_USERNAME --password $TWINE_PASSWORD --sign $BDIST_WHEEL_PATH.asc $BDIST_WHEEL_PATH
# Have the server git-tag the release and push the tags
- VERSION=$(python -c "import setup; print(setup.VERSION)")
# do sed twice to handle the case of https clone with and without a read token
- URL_HOST=$(git remote get-url origin | sed -e 's|https\?://.*@||g' | sed -e 's|https\?://||g')
- echo "URL_HOST = $URL_HOST"
- git config user.email "ci@gitlab.kitware.com"
- git config user.name "Gitlab-CI"
- git tag $VERSION -m "tarball tag $VERSION"
- git push --tags "https://${GIT_PUSH_TOKEN}@${URL_HOST}"
only:
refs:
......
......@@ -4,6 +4,21 @@ This changelog follows the specifications detailed in: [Keep a Changelog](https:
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.6.2 - Unreleased
### Added
* `draw_line_segments_on_image`
* Boxes.scale now accepts `about` keyword arg (can use to scale about center).
* Boxes.warp now accepts matrices and does inexact corner warping
* kwimage structures `warp` function now accepts a generic callable for mapping array-based points.
### Changed
* Renamed `_rectify_interpolation` to `_coerce_interpolation`. Old function is deprecated and removed in the future.
* `_coerce_interpolation` now accepts strings for fallback interpolation flags.
* `Detections.from_coco_annots` now returns classes as ndsampler.CategoryTree when possible
## Version 0.6.1 -
### Added
......
......@@ -4,7 +4,7 @@ mkinit ~/code/kwimage/kwimage/structs/__init__.py --relative -w --nomod
mkinit ~/code/kwimage/kwimage/__init__.py --relative --nomod -w
"""
__version__ = '0.6.1'
__version__ = '0.6.2'
from .algo import (available_nms_impls, daq_spatial_nms, non_max_supression,)
from .im_alphablend import (ensure_alpha_channel, overlay_alpha_images,
......@@ -16,8 +16,9 @@ from .im_core import (atleast_3channels, ensure_float01, ensure_uint255,
from .im_cv2 import (convert_colorspace, gaussian_patch, imresize, imscale,)
from .im_demodata import (grab_test_image, grab_test_image_fpath,)
from .im_draw import (draw_boxes_on_image, draw_clf_on_image,
draw_text_on_image, draw_vector_field, make_heatmask,
make_orimask, make_vector_field,)
draw_line_segments_on_image, draw_text_on_image,
draw_vector_field, make_heatmask, make_orimask,
make_vector_field,)
from .im_filter import (fourier_mask, radial_fourier_mask,)
from .im_io import (imread, imwrite, load_image_shape,)
from .im_runlen import (decode_run_length, encode_run_length, rle_translate,)
......@@ -25,19 +26,20 @@ from .im_stack import (stack_images, stack_images_grid,)
from .structs import (Boxes, Coords, Detections, Heatmap, Mask, MaskList,
MultiPolygon, Points, PointsList, Polygon, PolygonList,
Segmentation, SegmentationList, smooth_prob,)
from .util_warp import (TORCH_GRID_SAMPLE_HAS_ALIGN, subpixel_accum,
subpixel_align, subpixel_getvalue, subpixel_maximum,
subpixel_minimum, subpixel_set, subpixel_setvalue,
subpixel_slice, subpixel_translate, warp_points,
warp_tensor,)
from .util_warp import (TORCH_GRID_SAMPLE_HAS_ALIGN, add_homog, remove_homog,
subpixel_accum, subpixel_align, subpixel_getvalue,
subpixel_maximum, subpixel_minimum, subpixel_set,
subpixel_setvalue, subpixel_slice, subpixel_translate,
warp_points, warp_tensor,)
__all__ = ['BASE_COLORS', 'Boxes', 'CSS4_COLORS', 'Color', 'Coords',
'Detections', 'Heatmap', 'Mask', 'MaskList', 'MultiPolygon',
'Points', 'PointsList', 'Polygon', 'PolygonList', 'Segmentation',
'SegmentationList', 'TABLEAU_COLORS', 'TORCH_GRID_SAMPLE_HAS_ALIGN',
'XKCD_COLORS', 'atleast_3channels', 'available_nms_impls',
'convert_colorspace', 'daq_spatial_nms', 'decode_run_length',
'draw_boxes_on_image', 'draw_clf_on_image', 'draw_text_on_image',
'XKCD_COLORS', 'add_homog', 'atleast_3channels',
'available_nms_impls', 'convert_colorspace', 'daq_spatial_nms',
'decode_run_length', 'draw_boxes_on_image', 'draw_clf_on_image',
'draw_line_segments_on_image', 'draw_text_on_image',
'draw_vector_field', 'encode_run_length', 'ensure_alpha_channel',
'ensure_float01', 'ensure_uint255', 'fourier_mask',
'gaussian_patch', 'grab_test_image', 'grab_test_image_fpath',
......@@ -45,9 +47,9 @@ __all__ = ['BASE_COLORS', 'Boxes', 'CSS4_COLORS', 'Color', 'Coords',
'make_channels_comparable', 'make_heatmask', 'make_orimask',
'make_vector_field', 'non_max_supression', 'num_channels',
'overlay_alpha_images', 'overlay_alpha_layers',
'radial_fourier_mask', 'rle_translate', 'smooth_prob',
'stack_images', 'stack_images_grid', 'subpixel_accum',
'subpixel_align', 'subpixel_getvalue', 'subpixel_maximum',
'subpixel_minimum', 'subpixel_set', 'subpixel_setvalue',
'subpixel_slice', 'subpixel_translate', 'warp_points',
'warp_tensor']
'radial_fourier_mask', 'remove_homog', 'rle_translate',
'smooth_prob', 'stack_images', 'stack_images_grid',
'subpixel_accum', 'subpixel_align', 'subpixel_getvalue',
'subpixel_maximum', 'subpixel_minimum', 'subpixel_set',
'subpixel_setvalue', 'subpixel_slice', 'subpixel_translate',
'warp_points', 'warp_tensor']
......@@ -209,7 +209,7 @@ class _NMS_Impls():
from torchvision import _C as C # NOQA
import torchvision
_funcs['torchvision'] = torchvision.ops.nms
except ImportError as ex:
except (ImportError, UnicodeDecodeError) as ex:
warnings.warn(
'optional torchvision C nms is not available: {}'.format(
str(ex)))
......
......@@ -8,6 +8,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
import cv2
import six
import numpy as np
import numbers
from . import im_core
......@@ -20,7 +21,7 @@ _CV2_INTERPOLATION_TYPES = {
}
def _rectify_interpolation(interpolation, default=cv2.INTER_LANCZOS4,
def _coerce_interpolation(interpolation, default=cv2.INTER_LANCZOS4,
grow_default=cv2.INTER_LANCZOS4,
shrink_default=cv2.INTER_AREA, scale=None):
"""
......@@ -44,25 +45,64 @@ def _rectify_interpolation(interpolation, default=cv2.INTER_LANCZOS4,
Returns:
int: flag specifying interpolation type that can be passed to
functions like cv2.resize, cv2.warpAffine, etc...
Example:
>>> flag = _coerce_interpolation('linear')
>>> assert flag == cv2.INTER_LINEAR
>>> flag = _coerce_interpolation(cv2.INTER_LINEAR)
>>> assert flag == cv2.INTER_LINEAR
>>> flag = _coerce_interpolation('auto', default='lanczos')
>>> assert flag == cv2.INTER_LANCZOS4
>>> flag = _coerce_interpolation(None, default='lanczos')
>>> assert flag == cv2.INTER_LANCZOS4
>>> flag = _coerce_interpolation('auto', shrink_default='area', scale=0.1)
>>> assert flag == cv2.INTER_AREA
>>> flag = _coerce_interpolation('auto', grow_default='cubic', scale=10.)
>>> assert flag == cv2.INTER_CUBIC
>>> # xdoctest: +REQUIRES(module:pytest)
>>> import pytest
>>> with pytest.raises(TypeError):
>>> _coerce_interpolation(3.4)
>>> import pytest
>>> with pytest.raises(KeyError):
>>> _coerce_interpolation('foobar')
"""
if interpolation is None:
# Handle auto-defaulting
if interpolation is None or interpolation == 'auto':
if scale is None:
return default
interpolation = default
else:
if scale >= 1:
return grow_default
interpolation = grow_default
else:
return shrink_default
interpolation = shrink_default
elif isinstance(interpolation, six.text_type):
# Handle coercion from string to cv2 integer flag
if isinstance(interpolation, six.text_type):
try:
return _CV2_INTERPOLATION_TYPES[interpolation]
except KeyError:
print('Valid values for interpolation are {}'.format(
list(_CV2_INTERPOLATION_TYPES.keys())))
raise
raise KeyError(
'Invalid interpolation value={!r}. '
'Valid strings for interpolation are {}'.format(
interpolation, list(_CV2_INTERPOLATION_TYPES.keys())))
elif isinstance(interpolation, numbers.Integral):
return int(interpolation)
else:
return interpolation
raise TypeError(
'Invalid interpolation value={!r}. '
'Type must be int or string but got {!r}'.format(
interpolation, type(interpolation)))
def _rectify_interpolation(*args, **kwargs):
if False:
# TODO: Enable warning once internals are switched over
import warnings
warnings.warn(
'_rectify_interpolation is deprecated use _coerce_interpolation',
DeprecationWarning)
return _coerce_interpolation(*args, **kwargs)
def imscale(img, scale, interpolation=None, return_scale=False):
......@@ -117,7 +157,7 @@ def imscale(img, scale, interpolation=None, return_scale=False):
new_scale = new_w / w, new_h / h
new_dsize = (new_w, new_h)
interpolation = _rectify_interpolation(interpolation)
interpolation = _coerce_interpolation(interpolation)
new_img = cv2.resize(img, new_dsize, interpolation=interpolation)
if return_scale:
......@@ -286,7 +326,7 @@ def imresize(img, scale=None, dsize=None, max_dim=None, min_dim=None,
left, top = offset
right, bot = target_size - (embed_size + offset)
interpolation = _rectify_interpolation(
interpolation = _coerce_interpolation(
interpolation, scale=equal_sxy)
embed_dsize = tuple(embed_size)
......@@ -310,7 +350,7 @@ def imresize(img, scale=None, dsize=None, max_dim=None, min_dim=None,
old_dsize = (old_w, old_h)
new_dsize = (int(np.round(new_w)), int(np.round(new_h)))
new_scale = np.array(new_dsize) / np.array(old_dsize)
interpolation = _rectify_interpolation(
interpolation = _coerce_interpolation(
interpolation, scale=new_scale.min())
new_img = cv2.resize(img, new_dsize, interpolation=interpolation)
if return_info:
......
......@@ -293,6 +293,116 @@ def draw_boxes_on_image(img, boxes, color='blue', thickness=1,
return img2
def draw_line_segments_on_image(
img, pts1, pts2, color='blue', colorspace='rgb', thickness=1,
**kwargs):
"""
Draw line segments between pts1 and pts2 on an image.
Args:
pts1 (ndarray): xy coordinates of starting points
pts2 (ndarray): corresponding xy coordinates of ending points
color (str | List):
color code or a list of colors for each line segment
colorspace (str, default='rgb'): colorspace of image
thickness (int, default=1)
lineType (int, default=cv2.LINE_AA) option for cv2.line
Returns:
ndarray: the modified image (inplace if possible)
Example:
>>> from kwimage.im_draw import * # NOQA
>>> pts1 = np.array([[2, 0], [2, 20], [2.5, 30]])
>>> pts2 = np.array([[10, 5], [30, 28], [100, 50]])
>>> img = np.ones((100, 100, 3), dtype=np.uint8) * 255
>>> color = 'blue'
>>> colorspace = 'rgb'
>>> img2 = draw_line_segments_on_image(img, pts1, pts2, thickness=2)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl() # xdoc: +SKIP
>>> kwplot.figure(doclf=True, fnum=1)
>>> kwplot.imshow(img2)
Example:
>>> import kwimage
>>> pts1 = kwimage.Points.random(10).scale(512).xy
>>> pts2 = kwimage.Points.random(10).scale(512).xy
>>> img = np.ones((512, 512, 3), dtype=np.uint8) * 255
>>> color = kwimage.Color.distinct(10)
>>> img2 = kwimage.draw_line_segments_on_image(img, pts1, pts2, color=color)
>>> # xdoc: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl() # xdoc: +SKIP
>>> kwplot.figure(doclf=True, fnum=1)
>>> kwplot.imshow(img2)
"""
import cv2
# color = kwimage.Color(color)._forimage(img, colorspace)
num = len(pts1)
colors = _broadcast_colors(color, num, img, colorspace)
if 'lineType' not in kwargs:
kwargs['lineType'] = cv2.LINE_AA
pts1_ = pts1.tolist()
pts2_ = pts2.tolist()
for xy1, xy2, col in zip(pts1_, pts2_, colors):
xy1 = tuple(map(int, xy1))
xy2 = tuple(map(int, xy2))
cv2.line(img, xy1, xy2, color=col, thickness=thickness, **kwargs)
return img
def _broadcast_colors(color, num, img, colorspace):
"""
Determine if color applies a single color to all ``num`` items, or if it is
a list of colors for each item. Return as a list of colors for each item.
TODO:
- [ ] add as classmethod of kwimage.Color
Example:
>>> img = (np.random.rand(512, 512, 3) * 255).astype(np.uint8)
>>> colorspace = 'rgb'
>>> color = color_str_list = ['red', 'green', 'blue']
>>> color_str = 'red'
>>> num = 3
>>> print(_broadcast_colors(color_str_list, num, img, colorspace))
>>> print(_broadcast_colors(color_str, num, img, colorspace))
>>> colors_tuple_list = _broadcast_colors(color_str_list, num, img, colorspace)
>>> print(_broadcast_colors(colors_tuple_list, num, img, colorspace))
>>> #
>>> # FIXME: This case seems broken
>>> colors_ndarray_list = np.array(_broadcast_colors(color_str_list, num, img, colorspace))
>>> print(_broadcast_colors(colors_ndarray_list, num, img, colorspace))
"""
# Note there is an ambiguity when num=3 and color=[int, int, int]
# that must be resolved by checking num channels in the image
import kwimage
import ubelt as ub
import numbers
needs_broadcast = True # assume the list wasnt given by default
if ub.iterable(color):
first = ub.peek(color)
if len(color) == num:
if len(color) <= 4 and isinstance(first, numbers.Number):
# ambiguous case, interpret as a single broadcastable color
needs_broadcast = True
else:
# This is the only case we dont need broadcast
needs_broadcast = False
if needs_broadcast:
color = kwimage.Color(color)._forimage(img, colorspace)
colors = [color] * num
else:
colors = [kwimage.Color(c)._forimage(img, colorspace) for c in color]
return colors
def make_heatmask(probs, cmap='plasma', with_alpha=1.0, space='rgb',
dsize=None):
"""
......@@ -330,7 +440,9 @@ def make_heatmask(probs, cmap='plasma', with_alpha=1.0, space='rgb',
heatmask[:, :, 3] = (probs * with_alpha) # assign probs to alpha channel
if dsize is not None:
import cv2
heatmask = cv2.resize(heatmask, tuple(dsize), interpolation=cv2.INTER_NEAREST)
heatmask = cv2.resize(
heatmask, tuple(dsize),
interpolation=cv2.INTER_NEAREST)
return heatmask
......
This diff is collapsed.
......@@ -293,9 +293,10 @@ class Coords(_generic.Spatial, ub.NiceRepr):
Generalized coordinate transform.
Args:
transform (GeometricTransform | ArrayLike | Augmenter):
scikit-image tranform, a transformation matrix, or
an imgaug Augmenter.
transform (GeometricTransform | ArrayLike | Augmenter | callable):
scikit-image tranform, a 3x3 transformation matrix,
an imgaug Augmenter, or generic callable which transforms
an NxD ndarray.
input_dims (Tuple): shape of the image these objects correspond to
(only needed / used when transform is an imgaug augmenter)
......@@ -325,15 +326,32 @@ class Coords(_generic.Spatial, ub.NiceRepr):
>>> assert np.all(self.warp(np.eye(3)).data == self.data)
>>> assert np.all(self.warp(np.eye(2)).data == self.data)
Ignore:
>>> # xdoctest: +SKIP
Doctest:
>>> # xdoctest: +REQUIRES(module:osr)
>>> import osr
>>> wgs84_crs = osr.SpatialReference()
>>> wgs84_crs.ImportFromEPSG(4326)
>>> transform = osr.CoordinateTransformation(wgs84_crs, wgs84_crs)
>>> dst_crs = osr.SpatialReference()
>>> dst_crs.ImportFromEPSG(2927)
>>> transform = osr.CoordinateTransformation(wgs84_crs, dst_crs)
>>> self = Coords.random(10, rng=0)
>>> new = self.warp(transform)
>>> assert np.all(new.data == self.data)
>>> assert np.all(new.data != self.data)
>>> # Alternative using generic func
>>> def _gdal_coord_tranform(pts):
... return np.array([transform.TransformPoint(x, y, 0)[0:2]
... for x, y in pts])
>>> alt = self.warp(_gdal_coord_tranform)
>>> assert np.all(alt.data != self.data)
>>> assert np.all(alt.data == new.data)
Doctest:
>>> # can use a generic function
>>> def func(xy):
... return np.zeros_like(xy)
>>> self = Coords.random(10, rng=0)
>>> assert np.all(self.warp(func).data == 0)
"""
import kwimage
impl = self._impl
......@@ -350,9 +368,9 @@ class Coords(_generic.Spatial, ub.NiceRepr):
except ImportError:
import warnings
warnings.warn('imgaug is not installed')
raise TypeError(type(transform))
if isinstance(transform, imgaug.augmenters.Augmenter):
return new._warp_imgaug(transform, input_dims, inplace=True)
else:
if isinstance(transform, imgaug.augmenters.Augmenter):
return new._warp_imgaug(transform, input_dims, inplace=True)
### Try to accept GDAL tranforms ###
try:
......@@ -369,11 +387,16 @@ class Coords(_generic.Spatial, ub.NiceRepr):
new_pts.append((x, y))
new.data = np.array(new_pts, dtype=new.data.dtype)
return new
### Try to accept generic callable transforms ###
if callable(transform):
new.data = transform(new.data)
return new
raise TypeError(type(transform))
new.data = kwimage.warp_points(matrix, new.data)
return new
# @profile
def _warp_imgaug(self, augmenter, input_dims, inplace=False):
"""
Warps by applying an augmenter from the imgaug library
......@@ -695,3 +718,11 @@ class Coords(_generic.Spatial, ub.NiceRepr):
collections.append(col)
ax.add_collection(col)
return collections
if __name__ == '__main__':
"""
CommandLine:
python -m kwimage.structs.coords all
"""
import xdoctest
xdoctest.doctest_module(__file__)
......@@ -664,6 +664,10 @@ class Detections(ub.NiceRepr, _DetAlgoMixin, _DetDrawMixin):
import kwimage
cnames = None
if dset is not None:
try:
classes = dset.object_categories()
except Exception:
pass
cats = dset.dataset['categories']
kp_classes = dset.keypoint_categories()
else:
......@@ -991,7 +995,10 @@ class Detections(ub.NiceRepr, _DetAlgoMixin, _DetDrawMixin):
"""
sortx = self.scores.argsort()
if reverse:
sortx = sortx[::-1]
if torch.is_tensor(sortx):
sortx = torch.flip(sortx, dims=(0,))
else:
sortx = sortx[::-1]
return sortx
def sort(self, reverse=True):
......
......@@ -29,15 +29,20 @@ Notes:
semantics which are w/h.
"""
import os
import cv2
import copy
import six
import numpy as np
import ubelt as ub
import itertools as it
import warnings
from . import _generic
KWIMAGE_DISABLE_IMPORT_WARNINGS = os.environ.get('KWIMAGE_DISABLE_IMPORT_WARNINGS', '')
class _Mask_Backends():
# TODO: could make this prettier
def __init__(self):
......@@ -45,9 +50,6 @@ class _Mask_Backends():
def _lazy_init(self):
_funcs = {}
import os
import warnings
val = os.environ.get('KWIMAGE_DISABLE_C_EXTENSIONS', '').lower()
DISABLE_C_EXTENSIONS = val in {'true', 'on', 'yes', '1'}
......@@ -56,17 +58,19 @@ class _Mask_Backends():
from pycocotools import _mask
_funcs['pycoco'] = _mask
except ImportError as ex:
warnings.warn(
'optional module pycocotools is not available: {}'.format(
str(ex)))
if not KWIMAGE_DISABLE_IMPORT_WARNINGS:
warnings.warn(
'optional module pycocotools is not available: {}'.format(
str(ex)))
if not DISABLE_C_EXTENSIONS:
try:
from kwimage.structs._mask_backend import cython_mask
_funcs['kwimage'] = cython_mask
except ImportError as ex:
warnings.warn(
'optional mask_backend is not available: {}'.format(str(ex)))
if not KWIMAGE_DISABLE_IMPORT_WARNINGS:
warnings.warn(
'optional mask_backend is not available: {}'.format(str(ex)))
self._funcs = _funcs
self._valid = frozenset(self._funcs.keys())
......@@ -77,8 +81,8 @@ class _Mask_Backends():
valid = ub.oset(prefs) & set(self._funcs)
if not valid:
import warnings
warnings.warn('no valid mask backend')
if not KWIMAGE_DISABLE_IMPORT_WARNINGS:
warnings.warn('no valid mask backend')
return None, None
key = ub.peek(valid)
func = self._funcs[key]
......@@ -87,11 +91,11 @@ class _Mask_Backends():
_backends = _Mask_Backends()
# cython_mask = _backends.get_backend(['pycoco', 'kwimage'])
backend_key, cython_mask = _backends.get_backend(['kwimage', 'pycoco'])
# print('backend_key = {!r}'.format(backend_key))
# cython_mask = _backends.get_backend([])
# cython_backend = _backends.get_backend(['pycoco'])
@ub.memoize
def _lazy_mask_backend():
backend_key, cython_mask = _backends.get_backend(['kwimage', 'pycoco'])
return cython_mask
__all__ = ['Mask', 'MaskList']
......@@ -188,6 +192,9 @@ class _MaskConversionMixin(object):
"""
if self.format == MaskFormat.BYTES_RLE:
return self.copy() if copy else self
cython_mask = _lazy_mask_backend()
if self.format == MaskFormat.ARRAY_RLE:
h, w = self.data['size']
if self.data.get('order', 'F') != 'F':
......@@ -256,6 +263,7 @@ class _MaskConversionMixin(object):
else:
# NOTE: inefficient, could be improved
self = self.to_bytes_rle(copy=False)
cython_mask = _lazy_mask_backend()
if cython_mask is None:
raise NotImplementedError('pure python version')
f_mask = cython_mask.decode([self.data])[:, :, 0]
......@@ -337,6 +345,7 @@ class _MaskConstructorMixin(object):
if isinstance(polygons, np.ndarray):
polygons = [polygons]
flat_polys = [np.array(ps).ravel() for ps in polygons]
cython_mask = _lazy_mask_backend()
if cython_mask is None:
raise NotImplementedError('pure python version')
encoded = cython_mask.frPoly(flat_polys, h, w)
......@@ -766,6 +775,7 @@ class Mask(ub.NiceRepr, _MaskConversionMixin, _MaskConstructorMixin,
new = cls(new_data, MaskFormat.C_MASK)
elif format == MaskFormat.BYTES_RLE:
datas = [item.to_bytes_rle().data for item in items]
cython_mask = _lazy_mask_backend()
if cython_mask is None:
raise NotImplementedError('pure python version')
new_data = cython_mask.merge(datas, intersect=0)
......@@ -796,6 +806,7 @@ class Mask(ub.NiceRepr, _MaskConversionMixin, _MaskConstructorMixin,
"""
cls = self.__class__ if isinstance(self, Mask) else Mask
rle_datas = [item.to_bytes_rle().data for item in it.chain([self], others)]
cython_mask = _lazy_mask_backend()
if cython_mask is None:
raise NotImplementedError('pure python version')
encoded = cython_mask.merge(rle_datas, intersect=1)
......@@ -825,6 +836,7 @@ class Mask(ub.NiceRepr, _MaskConversionMixin, _MaskConstructorMixin,
150
"""
self = self.to_bytes_rle()
cython_mask = _lazy_mask_backend()
if cython_mask is None:
raise NotImplementedError('pure python version')
return cython_mask.area([self.data])[0]
......@@ -863,6 +875,7 @@ class Mask(ub.NiceRepr, _MaskConversionMixin, _MaskConstructorMixin,
"""
# import kwimage
self = self.to_bytes_rle()
cython_mask = _lazy_mask_backend()
if cython_mask is None:
raise NotImplementedError('pure python version')
xywh = cython_mask.toBbox([self.data])[0]
......@@ -1132,6 +1145,7 @@ class Mask(ub.NiceRepr, _MaskConversionMixin, _MaskConstructorMixin,
# I'm not sure what passing `pyiscrowd` actually does here
# TODO: determine what `pyiscrowd` does, and document it.
pyiscrowd = np.array([0], dtype=np.uint8)
cython_mask = _lazy_mask_backend()
if cython_mask is None:
raise NotImplementedError('pure python version')
iou = cython_mask.iou([item1], [item2], pyiscrowd)[0, 0]
......
......@@ -82,9 +82,10 @@ class _PointsWarpMixin:
Generalized coordinate transform.
Args:
transform (GeometricTransform | ArrayLike | Augmenter):
scikit-image tranform, a 3x3 transformation matrix, or
an imgaug Augmenter.
transform (GeometricTransform | ArrayLike | Augmenter | callable):
scikit-image tranform, a 3x3 transformation matrix,
an imgaug Augmenter, or generic callable which transforms
an NxD ndarray.
input_dims (Tuple): shape of the image these objects correspond to
(only needed / used when transform is an imgaug augmenter)
......@@ -125,6 +126,9 @@ class _PointsWarpMixin:
tf = transform
if isinstance(tf, np.ndarray):
tf = skimage.transform.AffineTransform(matrix=transform)
elif callable(tf):
raise NotImplementedError(
'callables cant transform linear data_to_img yet')
inv_tf = skimage.transform.AffineTransform(matrix=tf._inv_matrix)
# new.meta['tf_data_to_img'] = new.meta['tf_data_to_img'] + inv_tf
new.meta['tf_data_to_img'] = inv_tf + new.meta['tf_data_to_img']
......@@ -460,6 +464,8 @@ class Points(_generic.Spatial, _PointsWarpMixin):
def draw(self, color='blue', ax=None, alpha=None, radius=1, **kwargs):
"""
TODO: can use kwplot.draw_points
Example:
>>> # xdoc: +REQUIRES(module:kwplot)
>>> from kwimage.structs.points import * # NOQA
......
......@@ -139,9 +139,10 @@ class _PolyWarpMixin:
Generalized coordinate transform.
Args:
transform (GeometricTransform | ArrayLike | Augmenter):
scikit-image tranform, a 3x3 transformation matrix, or
an imgaug Augmenter.
transform (GeometricTransform | ArrayLike | Augmenter | callable):
scikit-image tranform, a 3x3 transformation matrix,
an imgaug Augmenter, or generic callable which transforms
an NxD ndarray.
input_dims (Tuple): shape of the image these objects correspond to
(only needed / used when transform is an imgaug augmenter)
......
......@@ -75,6 +75,14 @@ def _coordinate_grid(dims, align_corners=False):
return pixel_coords
def warp_image(inputs, mat, **kw):
import kwarray
# _impl = kwarray.ArrayAPI.coerce(inputs)
inputs = kwarray.atleast_nd(inputs, 3)
tensor = inputs.transpose(2, 0, 1)
return warp_tensor(tensor, mat, **kw)
def warp_tensor(inputs, mat, output_dims, mode='bilinear',
padding_mode='zeros', isinv=False, ishomog=None,
align_corners=False, new_mode=False):
......@@ -1285,7 +1293,7 @@ def _warp_tensor_cv2(inputs, mat, output_dims, mode='linear', ishomog=None):
return outputs
def warp_points(matrix, pts):
def warp_points(matrix, pts, homog_mode='divide'):
"""
Warp ND points / coordinates using a transformation matrix.
......@@ -1305,6 +1313,10 @@ def warp_points(matrix, pts):
points. The leading axis may take any shape, but usually, shape
will be [N x D] where N is the number of points.
homog_mode (str, default='divide'):
what to do for homogenous coordinates. Can either divide, keep, or
drop.
Retrns:
new_pts (ArrayLike): the points after being transformed by the matrix
......@@ -1351,6 +1363,7 @@ def warp_points(matrix, pts):
if len(matrix.shape) != 2:
raise ValueError('matrix must have 2 dimensions')
new_shape = pts.shape
D = pts.shape[-1] # the trailing axis is the point dimensionality
D1, D2 = matrix.shape
......@@ -1372,13 +1385,85 @@ def warp_points(matrix, pts):
new_pts_T = impl.matmul(matrix, new_pts_T)
if D != D1:
# remove homogenous coordinates (unless the matrix was affine with the
# last row was ommitted)
new_pts_T = new_pts_T[0:D] / new_pts_T[-1:]
if homog_mode == 'divide':
# remove homogenous coordinates (unless the matrix was affine with
# the last row was ommitted)
new_pts_T = new_pts_T[0:D] / new_pts_T[-1:]
elif homog_mode == 'drop':
# FIXME: the drop mode probably doesn't correspond to anything real
# and thus should be removed
new_pts_T = new_pts_T[0:D]
elif homog_mode == 'keep':
new_pts_T = new_pts_T
new_shape = pts.shape[0:-1] + (D1,)
else:
raise KeyError(homog_mode)
# Return the warped points with the same shape as the input
new_pts = impl.T(new_pts_T)
new_pts = impl.view(new_pts, pts.shape)
new_pts = impl.view(new_pts, new_shape)
return new_pts
def remove_homog(pts, mode='divide'):
"""
Remove homogenous coordinate to a point array.
This is a convinience function, it is not particularly efficient.
SeeAlso:
cv2.convertPointsFromHomogeneous
Example:
>>> homog_pts = np.random.rand(10, 3)
>>> remove_homog(homog_pts, 'divide')
>>> remove_homog(homog_pts, 'drop')
"""
impl = kwarray.ArrayAPI.coerce(pts)
D = pts.shape[-1] # the trailing axis is the point dimensionality
new_D = D - 1
pts_T = impl.T(impl.view(pts, (-1, D)))
if mode == 'divide':
new_pts_T = pts_T[0:new_D] / pts_T[-1:]
elif mode == 'drop':
# FIXME: the drop mode probably doesn't correspond to anything real
# and thus should be removed
new_pts_T = pts_T[0:new_D]
else:
raise KeyError(mode)
new_pts = impl.T(new_pts_T)
return new_pts
def add_homog(pts):
"""
Add a homogenous coordinate to a point array
This is a convinience function, it is not particularly efficient.
SeeAlso:
cv2.convertPointsToHomogeneous
Example:
>>> pts = np.random.rand(10, 2)
>>> add_homog(pts)
Benchmark:
>>> import timerit
>>> ti = timerit.Timerit(1000, bestof=10, verbose=2)
>>> pts = np.random.rand(1000, 2)
>>> for timer in ti.reset('kwimage'):
>>> with timer:
>>> kwimage.add_homog(pts)
>>> for timer in ti.reset('cv2'):
>>> with timer:
>>> cv2.convertPointsToHomogeneous(pts)
>>> # cv2 is 4x faster, but has more restrictive inputs
"""
import kwarray
impl = kwarray.ArrayAPI.coerce(pts)
new_pts = impl.cat([
pts, impl.ones(pts.shape[0:-1] + (1,), dtype=pts.dtype)], axis=-1)
return new_pts
......
......@@ -4,9 +4,9 @@
pip install -r requirements.txt
# Install in developer mode
#pip install -e . --verbose
# For some reason there is a bug with using pip and skbuild
# Calling setup.py directly seems to work though
python setup.py clean
python setup.py develop
#python setup.py develop
pip install -e .