Source code for delayed_image.helpers

import kwimage
import ubelt as ub
import numpy as np
from .util import util_network_text


try:
    import xdev
    profile = xdev.profile
except Exception:
    profile = ub.identity


write_network_text = util_network_text.write_network_text


def _auto_dsize(transform, sub_dsize):
    """
    Returns:
        Tuple[int, int]
    """
    sub_w, sub_h = sub_dsize
    sub_bounds = kwimage.Coords(
        np.array([[0,     0], [sub_w, 0],
                  [0, sub_h], [sub_w, sub_h]])
    )
    bounds = sub_bounds.warp(transform.matrix)
    max_xy = np.ceil(bounds.data.max(axis=0))
    max_x = int(max_xy[0])
    max_y = int(max_xy[1])
    dsize = (max_x, max_y)
    return dsize


def _ensure_valid_dsize(dsize):
    """
    Ensure dsize members are ints or Nones

    Returns:
        None | Tuple[int | None, int | None]
    """
    if dsize is not None:
        if ub.iterable(dsize):
            w, h = dsize
            if w is not None:
                w = int(w)
            if h is not None:
                h = int(h)
            dsize = (w, h)
    return dsize


def _ensure_valid_shape(shape):
    """
    Ensure dsize members are ints or Nones

    Returns:
        Tuple[int, ...]
    """
    return tuple(int(s) for s in shape)


def _largest_shape(shapes):
    """
    Finds maximum over all shapes

    Example:
        >>> shapes = [
        >>>     (10, 20), None, (None, 30), (40, 50, 60, None), (100,)
        >>> ]
        >>> largest = _largest_shape(shapes)
        >>> print('largest = {!r}'.format(largest))
        >>> assert largest == (100, 50, 60, None)
    """
    def _nonemax(a, b):
        if a is None or b is None:
            return a or b
        return max(a, b)
    import itertools as it
    largest = []
    for shape in shapes:
        if shape is not None:
            largest = [
                _nonemax(c1, c2)
                for c1, c2 in it.zip_longest(largest, shape, fillvalue=None)
            ]
    largest = tuple(largest)
    return largest


@profile
def _swap_warp_after_crop(root_region_bounds, tf_leaf_to_root):
    r"""
    Given a warp followed by a crop, compute the corresponding crop followed by
    a warp.

    Given a region in a "root" image and a trasnform between that "root" and
    some "leaf" image, compute the appropriate quantized region in the "leaf"
    image and the adjusted transformation between that root and leaf.

    Args:
        root_region_bounds (kwimage.Polygon):
            region representing the crop that happens after the warp

        tf_leaf_to_root (kwimage.Affine):
            the warp that happens before the input crop

    Returns:
        Tuple[Tuple[slice, slice], kwimage.Affine]:
            leaf_crop_slices - the crop that happens before the warp
            tf_newleaf_to_newroot - warp that happens after the crop.

    Example:
        >>> from delayed_image.helpers import *  # NOQA
        >>> region_slices = (slice(33, 100), slice(22, 62))
        >>> region_shape = (100, 100, 1)
        >>> root_region_box = kwimage.Boxes.from_slice(region_slices, shape=region_shape)
        >>> root_region_bounds = root_region_box.to_polygons()[0]
        >>> tf_leaf_to_root = kwimage.Affine.affine(scale=7).matrix
        >>> slices, tf_new = _swap_warp_after_crop(root_region_bounds, tf_leaf_to_root)
        >>> print('tf_new =\n{!r}'.format(tf_new))
        >>> print('slices = {!r}'.format(slices))

    Example:
        >>> region_slices = (slice(0, 8), slice(0, 8))
        >>> region_shape = (100, 100, 1)
        >>> root_region_box = kwimage.Boxes.from_slice(region_slices, shape=region_shape)
        >>> root_region_bounds = root_region_box.to_polygons()[0]
        >>> tf_leaf_to_root = kwimage.Affine.affine(scale=2.0001).matrix
        >>> slices, tf_new = _swap_warp_after_crop(root_region_bounds, tf_leaf_to_root)
        >>> print('tf_new =\n{!r}'.format(tf_new))
        >>> print('slices = {!r}'.format(slices))

    Example:
        >>> region_slices = (slice(0, 8), slice(0, 8))
        >>> region_shape = (100, 100, 1)
        >>> root_region_box = kwimage.Boxes.from_slice(region_slices, shape=region_shape)
        >>> root_region_bounds = root_region_box.to_polygons()[0]
        >>> tf_leaf_to_root = kwimage.Affine.affine(scale=0.4).matrix
        >>> slices, tf_new = _swap_warp_after_crop(root_region_bounds, tf_leaf_to_root)
        >>> print('tf_new =\n{!r}'.format(tf_new))
        >>> print('slices = {!r}'.format(slices))
    """
    # Transform the region bounds into the sub-image space
    tf_leaf_to_root = kwimage.Affine.coerce(tf_leaf_to_root)
    tf_root_to_leaf = tf_leaf_to_root.inv()
    tf_root_to_leaf = tf_root_to_leaf.__array__()
    leaf_region_bounds = root_region_bounds.warp(tf_root_to_leaf)
    leaf_region_box = leaf_region_bounds.box().to_ltrb()

    # Quantize to a region that is possible to sample from
    leaf_crop_box = leaf_region_box.quantize(inplace=True)

    # is this ok?
    # leaf_crop_box = leaf_crop_box.clip(0, 0, None, None, inplace=True)
    # Optimized clip
    ltrb = leaf_crop_box.data
    np.clip(ltrb, 0, None, out=ltrb)

    # Because we sampled a large quantized region, we need to modify the
    # transform to nudge it a bit to the left, undoing the quantization,
    # which has a bit of extra padding on the left, before applying the
    # final transform.
    # subpixel_offset = leaf_region_box.data[0, 0:2]
    crop_offset = leaf_crop_box.data[0:2]
    root_offset = root_region_bounds.exterior.data.min(axis=0)

    # TODO: could optimize this logic
    tf_root_to_newroot = kwimage.Affine.translate(offset=-root_offset).matrix
    tf_newleaf_to_leaf = kwimage.Affine.translate(offset=crop_offset).matrix

    # Resample the smaller region to align it with the root region
    # Note: The right most transform is applied first
    tf_newleaf_to_newroot = (
        tf_root_to_newroot @
        tf_leaf_to_root @
        tf_newleaf_to_leaf
    )

    lt_x, lt_y, rb_x, rb_y = leaf_crop_box.data

    if 1:
        # Candidate fix
        root_region_box = root_region_bounds.box()
        old_w = root_region_box.width
        old_h = root_region_box.height
        leaf_w = leaf_region_box.width
        leaf_h = leaf_region_box.height

        # TODO: test the case where old_w or old_h are zero
        # Attempt to work around issue #4
        # Not sure what the consequences of handing the issue this way are but
        # it seems to work.
        ZERO_WORKAROUND = 1

        if ZERO_WORKAROUND and old_w == 0:
            padw = 0
        else:
            padw = int(np.ceil(leaf_w / old_w))

        if ZERO_WORKAROUND and old_h == 0:
            padh = 0
        else:
            padh = int(np.ceil(leaf_h / old_h))

    else:
        padw, padh = 1, 1
    # padw, padh = 0, 0

    # # leaf_crop_slices = (slice(lt_y, rb_y), slice(lt_x, rb_x))
    # print('root_region_bounds = {}'.format(ub.urepr(root_region_bounds, nl=1)))
    # print('leaf_region_bounds = {}'.format(ub.urepr(leaf_region_bounds, nl=1)))
    # print('tf_leaf_to_root = {}'.format(ub.urepr(tf_leaf_to_root, nl=1)))

    # Need to pad add a bit more to the end of the crop because we are going to
    # warp afterwards. This fixes at least one case of off-by-one error.
    leaf_crop_slices = (slice(lt_y, rb_y + padh), slice(lt_x, rb_x + padw))

    return leaf_crop_slices, tf_newleaf_to_newroot


@profile
def _swap_crop_after_warp(inner_region, outer_transform):
    r"""
    Given a crop followed by a warp (usually an overview), compute the
    corresponding warp followed by a crop followed by a small correction warp.

    Note that in general it is not possible to ensure the crop is the last
    operation, there may need to be a small warp after it.

    However, this is generally only useful when the warp being pushed early in
    the operation chain corresponds to an overview, and often - but not always
    - the final warp will simply be the identity.

    Args:
        inner_region (kwimage.Polygon):
            region representing the crop that happens before the warp

        outer_transform (kwimage.Affine):
            the warp that happens after the input crop

    Returns:
        Tuple[kwimage.Affine, Tuple[slice, slice], kwimage.Affine]:

            new_inner_warp - the new warp to happen before the crop

            outer_crop - the new crop after the main warp

            new_outer_warp - a small subpixel alignment warp to happen last

    Example:
        >>> from delayed_image.helpers import *  # NOQA
        >>> region_slices = (slice(33, 100), slice(22, 62))
        >>> region_shape = (100, 100, 1)
        >>> inner_region = kwimage.Boxes.from_slice(region_slices)
        >>> inner_region = inner_region.to_polygons()[0]
        >>> outer_transform = kwimage.Affine.affine(scale=1/4)
        >>> new_inner_warp, outer_crop, new_outer_warp = _swap_crop_after_warp(inner_region, outer_transform)
        >>> print('new_inner_warp = {}'.format(ub.urepr(new_inner_warp, nl=1)))
        >>> print('outer_crop = {}'.format(ub.urepr(outer_crop, nl=1)))
        >>> print('new_outer_warp = {}'.format(ub.urepr(new_outer_warp, nl=1)))
    """
    # Find where the inner region maps to after the transform is applied
    outer_region = inner_region.warp(outer_transform)

    # Transform the region bounds into the sub-image space
    outer_box = outer_region.box().to_ltrb()

    # Quantize to a region that is possible to sample from
    outer_crop_box = outer_box.quantize()

    # is this ok?
    outer_crop_box = outer_crop_box.clip(0, 0, None, None)

    # Because the new crop might not be perfectly aligned, we might need to
    # nudge it a bit after we crop out its bounds.
    crop_offset = outer_crop_box.data[0:2]
    outer_offset = outer_region.exterior.data.min(axis=0)

    # Compute the extra transform that will realign the quantized croped data
    # with the original warped inner crop.
    tf_crop_to_box = kwimage.Affine.affine(
        offset=crop_offset - outer_offset
    )

    lt_x, lt_y, rb_x, rb_y = outer_crop_box.data
    outer_crop = (slice(lt_y, rb_y), slice(lt_x, rb_x))
    new_outer_warp = tf_crop_to_box

    # The inner warp will be the same as the original outer warp.
    new_inner_warp = outer_transform

    return new_inner_warp, outer_crop, new_outer_warp


[docs] def dequantize(quant_data, quantization): """ Helper for dequantization Args: quant_data (ndarray): data to dequantize quantization (Dict[str, Any]): quantization information dictionary to undo. Expected keys are: orig_dtype (str) orig_min (float) orig_max (float) quant_min (float) quant_max (float) nodata (None | int) Returns: ndarray : dequantized data Example: >>> quant_data = (np.random.rand(4, 4) * 256).astype(np.uint8) >>> quantization = { >>> 'orig_dtype': 'float32', >>> 'orig_min': 0, >>> 'orig_max': 1, >>> 'quant_min': 0, >>> 'quant_max': 255, >>> 'nodata': None, >>> } >>> dequantize(quant_data, quantization) Example: >>> quant_data = np.ones((4, 4), dtype=np.uint8) >>> quantization = { >>> 'orig_dtype': 'float32', >>> 'orig_min': 0, >>> 'orig_max': 1, >>> 'quant_min': 1, >>> 'quant_max': 1, >>> 'nodata': None, >>> } >>> dequantize(quant_data, quantization) """ orig_dtype = quantization.get('orig_dtype', 'float32') orig_min = quantization.get('orig_min', 0) orig_max = quantization.get('orig_max', 1) quant_min = quantization.get('quant_min', 0) quant_max = quantization['quant_max'] nodata = quantization.get('nodata', None) orig_extent = orig_max - orig_min quant_extent = quant_max - quant_min if quant_extent == 0: scale = 0 else: scale = (orig_extent / quant_extent) dequant = quant_data.astype(orig_dtype) dequant = (dequant - quant_min) * scale + orig_min if nodata is not None: mask = quant_data == nodata dequant[mask] = np.nan return dequant
[docs] def quantize_float01(imdata, old_min=0, old_max=1, quantize_dtype=np.int16): """ Note: Setting old_min / old_max indicates the possible extend of the input data (and it will be clipped to it). It does not mean that the input data has to have those min and max values, but it should be between them. Example: >>> from delayed_image.helpers import * # NOQA >>> # Test error when input is not nicely between 0 and 1 >>> imdata = (np.random.randn(32, 32, 3) - 1.) * 2.5 >>> quant1, quantization1 = quantize_float01(imdata, old_min=0, old_max=1) >>> recon1 = dequantize(quant1, quantization1) >>> error1 = np.abs((recon1 - imdata)).sum() >>> print('error1 = {!r}'.format(error1)) >>> # >>> for i in range(1, 20): >>> print('i = {!r}'.format(i)) >>> quant2, quantization2 = quantize_float01(imdata, old_min=-i, old_max=i) >>> recon2 = dequantize(quant2, quantization2) >>> error2 = np.abs((recon2 - imdata)).sum() >>> print('error2 = {!r}'.format(error2)) Example: >>> # Test dequantize with uint8 >>> from delayed_image.helpers import dequantize >>> imdata = np.random.randn(32, 32, 3) >>> quant1, quantization1 = quantize_float01(imdata, old_min=0, old_max=1, quantize_dtype=np.uint8) >>> recon1 = dequantize(quant1, quantization1) >>> error1 = np.abs((recon1 - imdata)).sum() >>> print('error1 = {!r}'.format(error1)) Example: >>> # Test quantization with different signed / unsigned combos >>> print(quantize_float01(None, 0, 1, np.int16)) >>> print(quantize_float01(None, 0, 1, np.int8)) >>> print(quantize_float01(None, 0, 1, np.uint8)) >>> print(quantize_float01(None, 0, 1, np.uint16)) """ # old_min = 0 # old_max = 1 quantize_iinfo = np.iinfo(quantize_dtype) quantize_max = quantize_iinfo.max if quantize_iinfo.kind == 'u': # Unsigned quantize quantize_nan = 0 quantize_min = 1 elif quantize_iinfo.kind == 'i': # Signed quantize quantize_min = 0 quantize_nan = max(-9999, quantize_iinfo.min) quantization = { 'orig_min': old_min, 'orig_max': old_max, 'quant_min': quantize_min, 'quant_max': quantize_max, 'nodata': quantize_nan, } old_extent = (old_max - old_min) new_extent = (quantize_max - quantize_min) quant_factor = new_extent / old_extent if imdata is not None: invalid_mask = np.isnan(imdata) new_imdata = (imdata.clip(old_min, old_max) - old_min) * quant_factor + quantize_min new_imdata = new_imdata.astype(quantize_dtype) new_imdata[invalid_mask] = quantize_nan else: new_imdata = None return new_imdata, quantization
[docs] class mkslice_cls: """ Helper to make slice syntax easier to construct Example: >>> from delayed_image.helpers import mkslice_cls >>> m = mkslice_cls() >>> m[0:3] slice(0, 3, None) >>> m[0:3, 0:5] (slice(0, 3, None), slice(0, 5, None)) >>> m()[0:3, 0:5] (slice(0, 3, None), slice(0, 5, None)) """ def __class_getitem__(self, index): # Doesnt exist in older Python versions return index def __getitem__(self, index): return index def __call__(self): return self
mkslice = mkslice_cls()