code: """
  Augmenters that somehow change the size of the images.
  
  List of augmenters:
  
      * :class:`Resize`
      * :class:`CropAndPad`
      * :class:`Crop`
      * :class:`Pad`
      * :class:`PadToFixedSize`
      * :class:`CenterPadToFixedSize`
      * :class:`CropToFixedSize`
      * :class:`CenterCropToFixedSize`
      * :class:`CropToMultiplesOf`
      * :class:`CenterCropToMultiplesOf`
      * :class:`PadToMultiplesOf`
      * :class:`CenterPadToMultiplesOf`
      * :class:`CropToPowersOf`
      * :class:`CenterCropToPowersOf`
      * :class:`PadToPowersOf`
      * :class:`CenterPadToPowersOf`
      * :class:`CropToAspectRatio`
      * :class:`CenterCropToAspectRatio`
      * :class:`PadToAspectRatio`
      * :class:`CenterPadToAspectRatio`
      * :class:`CropToSquare`
      * :class:`CenterCropToSquare`
      * :class:`PadToSquare`
      * :class:`CenterPadToSquare`
      * :class:`KeepSizeByResize`
  
  """
  from __future__ import print_function, division, absolute_import
  
  import re
  import functools
  
  import numpy as np
  import cv2
  
  import imgaug as ia
  from imgaug.imgaug import _normalize_cv2_input_arr_
  from . import meta
  from .. import parameters as iap
  
  
  def _crop_trbl_to_xyxy(shape, top, right, bottom, left, prevent_zero_size=True):
      if prevent_zero_size:
          top, right, bottom, left = _crop_prevent_zero_size(
              shape[0], shape[1], top, right, bottom, left)
  
      height, width = shape[0:2]
      x1 = left
      x2 = width - right
      y1 = top
      y2 = height - bottom
  
      # these steps prevent negative sizes
      # if x2==x1 or y2==y1 then the output arr has size 0 for the respective axis
      # note that if height/width of arr is zero, then y2==y1 or x2==x1, which
      # is still valid, even if height/width is zero and results in a zero-sized
      # axis
      x2 = max(x2, x1)
      y2 = max(y2, y1)
  
      return x1, y1, x2, y2
  
  
  def _crop_arr_(arr, top, right, bottom, left, prevent_zero_size=True):
      x1, y1, x2, y2 = _crop_trbl_to_xyxy(arr.shape, top, right, bottom, left,
                                          prevent_zero_size=prevent_zero_size)
      return arr[y1:y2, x1:x2, ...]
  
  
  def _crop_and_pad_arr(arr, croppings, paddings, pad_mode="constant",
                        pad_cval=0, keep_size=False):
      height, width = arr.shape[0:2]
  
      image_cr = _crop_arr_(arr, *croppings)
  
      image_cr_pa = pad(
          image_cr,
          top=paddings[0], right=paddings[1],
          bottom=paddings[2], left=paddings[3],
          mode=pad_mode, cval=pad_cval)
  
      if keep_size:
          image_cr_pa = ia.imresize_single_image(image_cr_pa, (height, width))
  
      return image_cr_pa
  
  
  def _crop_and_pad_heatmap_(heatmap, croppings_img, paddings_img,
                             pad_mode="constant", pad_cval=0.0, keep_size=False):
      return _crop_and_pad_hms_or_segmaps_(heatmap, croppings_img,
                                           paddings_img, pad_mode, pad_cval,
                                           keep_size)
  
  
  def _crop_and_pad_segmap_(segmap, croppings_img, paddings_img,
                            pad_mode="constant", pad_cval=0, keep_size=False):
      return _crop_and_pad_hms_or_segmaps_(segmap, croppings_img,
                                           paddings_img, pad_mode, pad_cval,
                                           keep_size)
  
  
  def _crop_and_pad_hms_or_segmaps_(augmentable, croppings_img,
                                    paddings_img, pad_mode="constant",
                                    pad_cval=None, keep_size=False):
      if isinstance(augmentable, ia.HeatmapsOnImage):
          arr_attr_name = "arr_0to1"
          pad_cval = pad_cval if pad_cval is not None else 0.0
      else:
          assert isinstance(augmentable, ia.SegmentationMapsOnImage), (
              "Expected HeatmapsOnImage or SegmentationMapsOnImage, got %s." % (
                  type(augmentable)))
          arr_attr_name = "arr"
          pad_cval = pad_cval if pad_cval is not None else 0
  
      arr = getattr(augmentable, arr_attr_name)
      arr_shape_orig = arr.shape
      augm_shape = augmentable.shape
  
      croppings_proj = _project_size_changes(croppings_img, augm_shape, arr.shape)
      paddings_proj = _project_size_changes(paddings_img, augm_shape, arr.shape)
  
      croppings_proj = _crop_prevent_zero_size(arr.shape[0], arr.shape[1],
                                               *croppings_proj)
  
      arr_cr = _crop_arr_(arr,
                          croppings_proj[0], croppings_proj[1],
                          croppings_proj[2], croppings_proj[3])
      arr_cr_pa = pad(
          arr_cr,
          top=paddings_proj[0], right=paddings_proj[1],
          bottom=paddings_proj[2], left=paddings_proj[3],
          mode=pad_mode,
          cval=pad_cval)
  
      setattr(augmentable, arr_attr_name, arr_cr_pa)
  
      if keep_size:
          augmentable = augmentable.resize(arr_shape_orig[0:2])
      else:
          augmentable.shape = _compute_shape_after_crop_and_pad(
              augmentable.shape, croppings_img, paddings_img)
      return augmentable
  
  
  def _crop_and_pad_kpsoi_(kpsoi, croppings_img, paddings_img, keep_size):
      # using the trbl function instead of croppings_img has the advantage
      # of incorporating prevent_zero_size, dealing with zero-sized input image
      # axis and dealing the negative crop amounts
      x1, y1, _x2, _y2 = _crop_trbl_to_xyxy(kpsoi.shape, *croppings_img)
      crop_left = x1
      crop_top = y1
  
      shape_orig = kpsoi.shape
      shifted = kpsoi.shift_(
          x=-crop_left+paddings_img[3],
          y=-crop_top+paddings_img[0])
      shifted.shape = _compute_shape_after_crop_and_pad(
          shape_orig, croppings_img, paddings_img)
      if keep_size:
          shifted = shifted.on_(shape_orig)
      return shifted
  
  
  def _compute_shape_after_crop_and_pad(old_shape, croppings, paddings):
      x1, y1, x2, y2 = _crop_trbl_to_xyxy(old_shape, *croppings)
      new_shape = list(old_shape)
      new_shape[0] = y2 - y1 + paddings[0] + paddings[2]
      new_shape[1] = x2 - x1 + paddings[1] + paddings[3]
      return tuple(new_shape)
  
  
  def _crop_prevent_zero_size(height, width, crop_top, crop_right, crop_bottom,
                              crop_left):
      remaining_height = height - (crop_top + crop_bottom)
      remaining_width = width - (crop_left + crop_right)
      if remaining_height < 1:
          regain = abs(remaining_height) + 1
          regain_top = regain // 2
          regain_bottom = regain // 2
          if regain_top + regain_bottom < regain:
              regain_top += 1
  
          if regain_top > crop_top:
              diff = regain_top - crop_top
              regain_top = crop_top
              regain_bottom += diff
          elif regain_bottom > crop_bottom:
              diff = regain_bottom - crop_bottom
              regain_bottom = crop_bottom
              regain_top += diff
  
          crop_top = crop_top - regain_top
          crop_bottom = crop_bottom - regain_bottom
  
      if remaining_width < 1:
          regain = abs(remaining_width) + 1
          regain_right = regain // 2
          regain_left = regain // 2
          if regain_right + regain_left < regain:
              regain_right += 1
  
          if regain_right > crop_right:
              diff = regain_right - crop_right
              regain_right = crop_right
              regain_left += diff
          elif regain_left > crop_left:
              diff = regain_left - crop_left
              regain_left = crop_left
              regain_right += diff
  
          crop_right = crop_right - regain_right
          crop_left = crop_left - regain_left
  
      return (
          max(crop_top, 0), max(crop_right, 0), max(crop_bottom, 0),
          max(crop_left, 0))
  
  
  def _project_size_changes(trbl, from_shape, to_shape):
      if from_shape[0:2] == to_shape[0:2]:
          return trbl
  
      height_to = to_shape[0]
      width_to = to_shape[1]
      height_from = from_shape[0]
      width_from = from_shape[1]
  
      top = trbl[0]
      right = trbl[1]
      bottom = trbl[2]
      left = trbl[3]
  
      # Adding/subtracting 1e-4 here helps for the case where a heatmap/segmap
      # is exactly half the size of an image and the size change on an axis is
      # an odd value. Then the projected value would end up being <something>.5
      # and the rounding would always round up to the next integer. If both
      # sides then have the same change, they are both rounded up, resulting
      # in more change than expected.
      # E.g. image height is 8, map height is 4, change is 3 at the top and 3 at
      # the bottom. The changes are projected to 4*(3/8) = 1.5 and both rounded
      # up to 2.0. Hence, the maps are changed by 4 (100% of the map height,
      # vs. 6 for images, which is 75% of the image height).
      top = _int_r(height_to * (top/height_from) - 1e-4)
      right = _int_r(width_to * (right/width_from) + 1e-4)
      bottom = _int_r(height_to * (bottom/height_from) + 1e-4)
      left = _int_r(width_to * (left/width_from) - 1e-4)
  
      return top, right, bottom, left
  
  
  def _int_r(value):
      return int(np.round(value))
  
  
  # TODO somehow integrate this with pad()
  def _handle_pad_mode_param(pad_mode):
      pad_modes_available = {
          "constant", "edge", "linear_ramp", "maximum", "mean", "median",
          "minimum", "reflect", "symmetric", "wrap"}
      if pad_mode == ia.ALL:
          return iap.Choice(list(pad_modes_available))
      if ia.is_string(pad_mode):
          assert pad_mode in pad_modes_available, (
              "Value '%s' is not a valid pad mode. Valid pad modes are: %s." % (
                  pad_mode, ", ".join(pad_modes_available)))
          return iap.Deterministic(pad_mode)
      if isinstance(pad_mode, list):
          assert all([v in pad_modes_available for v in pad_mode]), (
              "At least one in list %s is not a valid pad mode. Valid pad "
              "modes are: %s." % (str(pad_mode), ", ".join(pad_modes_available)))
          return iap.Choice(pad_mode)
      if isinstance(pad_mode, iap.StochasticParameter):
          return pad_mode
      raise Exception(
          "Expected pad_mode to be ia.ALL or string or list of strings or "
          "StochasticParameter, got %s." % (type(pad_mode),))
  
  
  def _handle_position_parameter(position):
      if position == "uniform":
          return iap.Uniform(0.0, 1.0), iap.Uniform(0.0, 1.0)
      if position == "normal":
          return (
              iap.Clip(iap.Normal(loc=0.5, scale=0.35 / 2),
                       minval=0.0, maxval=1.0),
              iap.Clip(iap.Normal(loc=0.5, scale=0.35 / 2),
                       minval=0.0, maxval=1.0)
          )
      if position == "center":
          return iap.Deterministic(0.5), iap.Deterministic(0.5)
      if (ia.is_string(position)
              and re.match(r"^(left|center|right)-(top|center|bottom)$",
                           position)):
          mapping = {"top": 0.0, "center": 0.5, "bottom": 1.0, "left": 0.0,
                     "right": 1.0}
          return (
              iap.Deterministic(mapping[position.split("-")[0]]),
              iap.Deterministic(mapping[position.split("-")[1]])
          )
      if isinstance(position, iap.StochasticParameter):
          return position
      if isinstance(position, tuple):
          assert len(position) == 2, (
              "Expected tuple with two entries as position parameter. "
              "Got %d entries with types %s.." % (
                  len(position), str([type(item) for item in position])))
          for item in position:
              if ia.is_single_number(item) and (item < 0 or item > 1.0):
                  raise Exception(
                      "Both position values must be within the value range "
                      "[0.0, 1.0]. Got type %s with value %.8f." % (
                          type(item), item,))
          position = [iap.Deterministic(item)
                      if ia.is_single_number(item)
                      else item for item in position]
  
          only_sparams = all([isinstance(item, iap.StochasticParameter)
                              for item in position])
          assert only_sparams, (
              "Expected tuple with two entries that are both either "
              "StochasticParameter or float/int. Got types %s." % (
                  str([type(item) for item in position])
              ))
          return tuple(position)
      raise Exception(
          "Expected one of the following as position parameter: string "
          "'uniform', string 'normal', string 'center', a string matching "
          "regex ^(left|center|right)-(top|center|bottom)$, a single "
          "StochasticParameter or a tuple of two entries, both being either "
          "StochasticParameter or floats or int. Got instead type %s with "
          "content '%s'." % (
              type(position),
              (str(position)
               if len(str(position)) < 20
               else str(position)[0:20] + "...")
          )
      )
  
  
  # TODO this is the same as in imgaug.py, make DRY
  def _assert_two_or_three_dims(shape):
      if hasattr(shape, "shape"):
          shape = shape.shape
      assert len(shape) in [2, 3], (
          "Expected image with two or three dimensions, but got %d dimensions "
          "and shape %s." % (len(shape), shape))
  
  
  def pad(arr, top=0, right=0, bottom=0, left=0, mode="constant", cval=0):
      """Pad an image-like array on its top/right/bottom/left side.
  
      This function is a wrapper around :func:`numpy.pad`.
  
      Supported dtypes
      ----------------
  
          * ``uint8``: yes; fully tested (1)
          * ``uint16``: yes; fully tested (1)
          * ``uint32``: yes; fully tested (2) (3)
          * ``uint64``: yes; fully tested (2) (3)
          * ``int8``: yes; fully tested (1)
          * ``int16``: yes; fully tested (1)
          * ``int32``: yes; fully tested (1)
          * ``int64``: yes; fully tested (2) (3)
          * ``float16``: yes; fully tested (2) (3)
          * ``float32``: yes; fully tested (1)
          * ``float64``: yes; fully tested (1)
          * ``float128``: yes; fully tested (2) (3)
          * ``bool``: yes; tested (2) (3)
  
          - (1) Uses ``cv2`` if `mode` is one of: ``"constant"``, ``"edge"``,
                ``"reflect"``, ``"symmetric"``. Otherwise uses ``numpy``.
          - (2) Uses ``numpy``.
          - (3) Rejected by ``cv2``.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray
          Image-like array to pad.
  
      top : int, optional
          Amount of pixels to add to the top side of the image.
          Must be ``0`` or greater.
  
      right : int, optional
          Amount of pixels to add to the right side of the image.
          Must be ``0`` or greater.
  
      bottom : int, optional
          Amount of pixels to add to the bottom side of the image.
          Must be ``0`` or greater.
  
      left : int, optional
          Amount of pixels to add to the left side of the image.
          Must be ``0`` or greater.
  
      mode : str, optional
          Padding mode to use. See :func:`numpy.pad` for details.
          In case of mode ``constant``, the parameter `cval` will be used as
          the ``constant_values`` parameter to :func:`numpy.pad`.
          In case of mode ``linear_ramp``, the parameter `cval` will be used as
          the ``end_values`` parameter to :func:`numpy.pad`.
  
      cval : number or iterable of number, optional
          Value to use for padding if `mode` is ``constant``.
          See :func:`numpy.pad` for details. The cval is expected to match the
          input array's dtype and value range. If an iterable is used, it is
          expected to contain one value per channel. The number of values
          and number of channels are expected to match.
  
      Returns
      -------
      (H',W') ndarray or (H',W',C) ndarray
          Padded array with height ``H'=H+top+bottom`` and width
          ``W'=W+left+right``.
  
      """
      import imgaug.dtypes as iadt
  
      _assert_two_or_three_dims(arr)
      assert all([v >= 0 for v in [top, right, bottom, left]]), (
          "Expected padding amounts that are >=0, but got %d, %d, %d, %d "
          "(top, right, bottom, left)" % (top, right, bottom, left))
  
      is_multi_cval = ia.is_iterable(cval)
  
      if top > 0 or right > 0 or bottom > 0 or left > 0:
          min_value, _, max_value = iadt.get_value_range_of_dtype(arr.dtype)
  
          # without the if here there are crashes for float128, e.g. if
          # cval is an int (just using float(cval) seems to not be accurate
          # enough)
          if arr.dtype.name == "float128":
              cval = np.float128(cval)  # pylint: disable=no-member
  
          if is_multi_cval:
              cval = np.clip(cval, min_value, max_value)
          else:
              cval = max(min(cval, max_value), min_value)
  
          # Note that copyMakeBorder() hangs/runs endlessly if arr has an
          # axis of size 0 and mode is "reflect".
          # Numpy also complains in these cases if mode is not "constant".
          has_zero_sized_axis = any([axis == 0 for axis in arr.shape])
          if has_zero_sized_axis:
              mode = "constant"
  
          mapping_mode_np_to_cv2 = {
              "constant": cv2.BORDER_CONSTANT,
              "edge": cv2.BORDER_REPLICATE,
              "linear_ramp": None,
              "maximum": None,
              "mean": None,
              "median": None,
              "minimum": None,
              "reflect": cv2.BORDER_REFLECT_101,
              "symmetric": cv2.BORDER_REFLECT,
              "wrap": None,
              cv2.BORDER_CONSTANT: cv2.BORDER_CONSTANT,
              cv2.BORDER_REPLICATE: cv2.BORDER_REPLICATE,
              cv2.BORDER_REFLECT_101: cv2.BORDER_REFLECT_101,
              cv2.BORDER_REFLECT: cv2.BORDER_REFLECT
          }
          bad_mode_cv2 = mapping_mode_np_to_cv2.get(mode, None) is None
  
          # these datatypes all simply generate a "TypeError: src data type = X
          # is not supported" error
          bad_datatype_cv2 = (
              arr.dtype.name
              in ["uint32", "uint64", "int64", "float16", "float128", "bool"]
          )
  
          # OpenCV turns the channel axis for arrays with 0 channels to 512
          # TODO add direct test for this. indirectly tested via Pad
          bad_shape_cv2 = (arr.ndim == 3 and arr.shape[-1] == 0)
  
          if not bad_datatype_cv2 and not bad_mode_cv2 and not bad_shape_cv2:
              # convert cval to expected type, as otherwise we get TypeError
              # for np inputs
              kind = arr.dtype.kind
              if is_multi_cval:
                  cval = [float(cval_c) if kind == "f" else int(cval_c)
                          for cval_c in cval]
              else:
                  cval = float(cval) if kind == "f" else int(cval)
  
              if arr.ndim == 2 or arr.shape[2] <= 4:
                  # without this, only the first channel is padded with the cval,
                  # all following channels with 0
                  if arr.ndim == 3 and not is_multi_cval:
                      cval = tuple([cval] * arr.shape[2])
  
                  arr_pad = cv2.copyMakeBorder(
                      _normalize_cv2_input_arr_(arr),
                      top=top, bottom=bottom, left=left, right=right,
                      borderType=mapping_mode_np_to_cv2[mode], value=cval)
                  if arr.ndim == 3 and arr_pad.ndim == 2:
                      arr_pad = arr_pad[..., np.newaxis]
              else:
                  result = []
                  channel_start_idx = 0
                  cval = cval if is_multi_cval else tuple([cval] * arr.shape[2])
                  while channel_start_idx < arr.shape[2]:
                      arr_c = arr[..., channel_start_idx:channel_start_idx+4]
                      cval_c = cval[channel_start_idx:channel_start_idx+4]
                      arr_pad_c = cv2.copyMakeBorder(
                          _normalize_cv2_input_arr_(arr_c),
                          top=top, bottom=bottom, left=left, right=right,
                          borderType=mapping_mode_np_to_cv2[mode], value=cval_c)
                      arr_pad_c = np.atleast_3d(arr_pad_c)
                      result.append(arr_pad_c)
                      channel_start_idx += 4
                  arr_pad = np.concatenate(result, axis=2)
          else:
              # paddings for 2d case
              paddings_np = [(top, bottom), (left, right)]
  
              # add paddings for 3d case
              if arr.ndim == 3:
                  paddings_np.append((0, 0))
  
              if mode == "constant":
                  if arr.ndim > 2 and is_multi_cval:
                      arr_pad_chans = [
                          np.pad(arr[..., c], paddings_np[0:2], mode=mode,
                                 constant_values=cval[c])
                          for c in np.arange(arr.shape[2])]
                      arr_pad = np.stack(arr_pad_chans, axis=-1)
                  else:
                      arr_pad = np.pad(arr, paddings_np, mode=mode,
                                       constant_values=cval)
              elif mode == "linear_ramp":
                  if arr.ndim > 2 and is_multi_cval:
                      arr_pad_chans = [
                          np.pad(arr[..., c], paddings_np[0:2], mode=mode,
                                 end_values=cval[c])
                          for c in np.arange(arr.shape[2])]
                      arr_pad = np.stack(arr_pad_chans, axis=-1)
                  else:
                      arr_pad = np.pad(arr, paddings_np, mode=mode,
                                       end_values=cval)
              else:
                  arr_pad = np.pad(arr, paddings_np, mode=mode)
  
          return arr_pad
      return np.copy(arr)
  
  
  def pad_to_aspect_ratio(arr, aspect_ratio, mode="constant", cval=0,
                          return_pad_amounts=False):
      """Pad an image array on its sides so that it matches a target aspect ratio.
  
      See :func:`~imgaug.imgaug.compute_paddings_for_aspect_ratio` for an
      explanation of how the required padding amounts are distributed per
      image axis.
  
      Supported dtypes
      ----------------
  
      See :func:`~imgaug.augmenters.size.pad`.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray
          Image-like array to pad.
  
      aspect_ratio : float
          Target aspect ratio, given as width/height. E.g. ``2.0`` denotes the
          image having twice as much width as height.
  
      mode : str, optional
          Padding mode to use. See :func:`~imgaug.imgaug.pad` for details.
  
      cval : number, optional
          Value to use for padding if `mode` is ``constant``.
          See :func:`numpy.pad` for details.
  
      return_pad_amounts : bool, optional
          If ``False``, then only the padded image will be returned. If
          ``True``, a ``tuple`` with two entries will be returned, where the
          first entry is the padded image and the second entry are the amounts
          by which each image side was padded. These amounts are again a
          ``tuple`` of the form ``(top, right, bottom, left)``, with each value
          being an ``int``.
  
      Returns
      -------
      (H',W') ndarray or (H',W',C) ndarray
          Padded image as ``(H',W')`` or ``(H',W',C)`` ndarray, fulfilling the
          given `aspect_ratio`.
  
      tuple of int
          Amounts by which the image was padded on each side, given as a
          ``tuple`` ``(top, right, bottom, left)``.
          This ``tuple`` is only returned if `return_pad_amounts` was set to
          ``True``.
  
      """
      pad_top, pad_right, pad_bottom, pad_left = \
          compute_paddings_to_reach_aspect_ratio(arr, aspect_ratio)
      arr_padded = pad(
          arr,
          top=pad_top,
          right=pad_right,
          bottom=pad_bottom,
          left=pad_left,
          mode=mode,
          cval=cval
      )
  
      if return_pad_amounts:
          return arr_padded, (pad_top, pad_right, pad_bottom, pad_left)
      return arr_padded
  
  
  def pad_to_multiples_of(arr, height_multiple, width_multiple, mode="constant",
                          cval=0, return_pad_amounts=False):
      """Pad an image array until its side lengths are multiples of given values.
  
      See :func:`~imgaug.imgaug.compute_paddings_for_aspect_ratio` for an
      explanation of how the required padding amounts are distributed per
      image axis.
  
      Supported dtypes
      ----------------
  
      See :func:`~imgaug.augmenters.size.pad`.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray
          Image-like array to pad.
  
      height_multiple : None or int
          The desired multiple of the height. The computed padding amount will
          reflect a padding that increases the y axis size until it is a multiple
          of this value.
  
      width_multiple : None or int
          The desired multiple of the width. The computed padding amount will
          reflect a padding that increases the x axis size until it is a multiple
          of this value.
  
      mode : str, optional
          Padding mode to use. See :func:`~imgaug.imgaug.pad` for details.
  
      cval : number, optional
          Value to use for padding if `mode` is ``constant``.
          See :func:`numpy.pad` for details.
  
      return_pad_amounts : bool, optional
          If ``False``, then only the padded image will be returned. If
          ``True``, a ``tuple`` with two entries will be returned, where the
          first entry is the padded image and the second entry are the amounts
          by which each image side was padded. These amounts are again a
          ``tuple`` of the form ``(top, right, bottom, left)``, with each value
          being an integer.
  
      Returns
      -------
      (H',W') ndarray or (H',W',C) ndarray
          Padded image as ``(H',W')`` or ``(H',W',C)`` ndarray.
  
      tuple of int
          Amounts by which the image was padded on each side, given as a
          ``tuple`` ``(top, right, bottom, left)``.
          This ``tuple`` is only returned if `return_pad_amounts` was set to
          ``True``.
  
      """
      pad_top, pad_right, pad_bottom, pad_left = \
          compute_paddings_to_reach_multiples_of(
              arr, height_multiple, width_multiple)
      arr_padded = pad(
          arr,
          top=pad_top,
          right=pad_right,
          bottom=pad_bottom,
          left=pad_left,
          mode=mode,
          cval=cval
      )
  
      if return_pad_amounts:
          return arr_padded, (pad_top, pad_right, pad_bottom, pad_left)
      return arr_padded
  
  
  def compute_paddings_to_reach_aspect_ratio(arr, aspect_ratio):
      """Compute pad amounts required to fulfill an aspect ratio.
  
      "Pad amounts" here denotes the number of pixels that have to be added to
      each side to fulfill the desired constraint.
  
      The aspect ratio is given as ``ratio = width / height``.
      Depending on which dimension is smaller (height or width), only the
      corresponding sides (top/bottom or left/right) will be padded.
  
      The axis-wise padding amounts are always distributed equally over the
      sides of the respective axis (i.e. left and right, top and bottom). For
      odd pixel amounts, one pixel will be left over after the equal
      distribution and could be added to either side of the axis. This function
      will always add such a left over pixel to the bottom (y-axis) or
      right (x-axis) side.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray or tuple of int
          Image-like array or shape tuple for which to compute pad amounts.
  
      aspect_ratio : float
          Target aspect ratio, given as width/height. E.g. ``2.0`` denotes the
          image having twice as much width as height.
  
      Returns
      -------
      tuple of int
          Required padding amounts to reach the target aspect ratio, given as a
          ``tuple`` of the form ``(top, right, bottom, left)``.
  
      """
      _assert_two_or_three_dims(arr)
      assert aspect_ratio > 0, (
          "Expected to get an aspect ratio >0, got %.4f." % (aspect_ratio,))
  
      pad_top = 0
      pad_right = 0
      pad_bottom = 0
      pad_left = 0
  
      shape = arr.shape if hasattr(arr, "shape") else arr
      height, width = shape[0:2]
  
      if height == 0:
          height = 1
          pad_bottom += 1
      if width == 0:
          width = 1
          pad_right += 1
  
      aspect_ratio_current = width / height
  
      if aspect_ratio_current < aspect_ratio:
          # image is more vertical than desired, width needs to be increased
          diff = (aspect_ratio * height) - width
          pad_right += int(np.ceil(diff / 2))
          pad_left += int(np.floor(diff / 2))
      elif aspect_ratio_current > aspect_ratio:
          # image is more horizontal than desired, height needs to be increased
          diff = ((1/aspect_ratio) * width) - height
          pad_top += int(np.floor(diff / 2))
          pad_bottom += int(np.ceil(diff / 2))
  
      return pad_top, pad_right, pad_bottom, pad_left
  
  
  def compute_croppings_to_reach_aspect_ratio(arr, aspect_ratio):
      """Compute crop amounts required to fulfill an aspect ratio.
  
      "Crop amounts" here denotes the number of pixels that have to be removed
      from each side to fulfill the desired constraint.
  
      The aspect ratio is given as ``ratio = width / height``.
      Depending on which dimension is smaller (height or width), only the
      corresponding sides (top/bottom or left/right) will be cropped.
  
      The axis-wise padding amounts are always distributed equally over the
      sides of the respective axis (i.e. left and right, top and bottom). For
      odd pixel amounts, one pixel will be left over after the equal
      distribution and could be added to either side of the axis. This function
      will always add such a left over pixel to the bottom (y-axis) or
      right (x-axis) side.
  
      If an aspect ratio cannot be reached exactly, this function will return
      rather one pixel too few than one pixel too many.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray or tuple of int
          Image-like array or shape tuple for which to compute crop amounts.
  
      aspect_ratio : float
          Target aspect ratio, given as width/height. E.g. ``2.0`` denotes the
          image having twice as much width as height.
  
      Returns
      -------
      tuple of int
          Required cropping amounts to reach the target aspect ratio, given as a
          ``tuple`` of the form ``(top, right, bottom, left)``.
  
      """
      _assert_two_or_three_dims(arr)
      assert aspect_ratio > 0, (
          "Expected to get an aspect ratio >0, got %.4f." % (aspect_ratio,))
  
      shape = arr.shape if hasattr(arr, "shape") else arr
      assert shape[0] > 0, (
          "Expected to get an array with height >0, got shape %s." % (shape,))
  
      height, width = shape[0:2]
      aspect_ratio_current = width / height
  
      top = 0
      right = 0
      bottom = 0
      left = 0
  
      if aspect_ratio_current < aspect_ratio:
          # image is more vertical than desired, height needs to be reduced
          # c = H - W/r
          crop_amount = height - (width / aspect_ratio)
          crop_amount = min(crop_amount, height - 1)
          top = int(np.floor(crop_amount / 2))
          bottom = int(np.ceil(crop_amount / 2))
      elif aspect_ratio_current > aspect_ratio:
          # image is more horizontal than desired, width needs to be reduced
          # c = W - Hr
          crop_amount = width - height * aspect_ratio
          crop_amount = min(crop_amount, width - 1)
          left = int(np.floor(crop_amount / 2))
          right = int(np.ceil(crop_amount / 2))
  
      return top, right, bottom, left
  
  
  def compute_paddings_to_reach_multiples_of(arr, height_multiple,
                                             width_multiple):
      """Compute pad amounts until img height/width are multiples of given values.
  
      See :func:`~imgaug.imgaug.compute_paddings_for_aspect_ratio` for an
      explanation of how the required padding amounts are distributed per
      image axis.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray or tuple of int
          Image-like array or shape tuple for which to compute pad amounts.
  
      height_multiple : None or int
          The desired multiple of the height. The computed padding amount will
          reflect a padding that increases the y axis size until it is a multiple
          of this value.
  
      width_multiple : None or int
          The desired multiple of the width. The computed padding amount will
          reflect a padding that increases the x axis size until it is a multiple
          of this value.
  
      Returns
      -------
      tuple of int
          Required padding amounts to reach multiples of the provided values,
          given as a ``tuple`` of the form ``(top, right, bottom, left)``.
  
      """
      def _compute_axis_value(axis_size, multiple):
          if multiple is None:
              return 0, 0
          if axis_size == 0:
              to_pad = multiple
          elif axis_size % multiple == 0:
              to_pad = 0
          else:
              to_pad = multiple - (axis_size % multiple)
          return int(np.floor(to_pad/2)), int(np.ceil(to_pad/2))
  
      _assert_two_or_three_dims(arr)
  
      if height_multiple is not None:
          assert height_multiple > 0, (
              "Can only pad to multiples of 1 or larger, got %d." % (
                  height_multiple,))
      if width_multiple is not None:
          assert width_multiple > 0, (
              "Can only pad to multiples of 1 or larger, got %d." % (
                  width_multiple,))
  
      shape = arr.shape if hasattr(arr, "shape") else arr
      height, width = shape[0:2]
  
      top, bottom = _compute_axis_value(height, height_multiple)
      left, right = _compute_axis_value(width, width_multiple)
  
      return top, right, bottom, left
  
  
  def compute_croppings_to_reach_multiples_of(arr, height_multiple,
                                              width_multiple):
      """Compute croppings to reach multiples of given heights/widths.
  
      See :func:`~imgaug.imgaug.compute_paddings_for_aspect_ratio` for an
      explanation of how the required cropping amounts are distributed per
      image axis.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray or tuple of int
          Image-like array or shape tuple for which to compute crop amounts.
  
      height_multiple : None or int
          The desired multiple of the height. The computed croppings will
          reflect a crop operation that decreases the y axis size until it is
          a multiple of this value.
  
      width_multiple : None or int
          The desired multiple of the width. The computed croppings amount will
          reflect a crop operation that decreases the x axis size until it is
          a multiple of this value.
  
      Returns
      -------
      tuple of int
          Required cropping amounts to reach multiples of the provided values,
          given as a ``tuple`` of the form ``(top, right, bottom, left)``.
  
      """
      def _compute_axis_value(axis_size, multiple):
          if multiple is None:
              return 0, 0
          if axis_size == 0:
              to_crop = 0
          elif axis_size % multiple == 0:
              to_crop = 0
          else:
              to_crop = axis_size % multiple
          return int(np.floor(to_crop/2)), int(np.ceil(to_crop/2))
  
      _assert_two_or_three_dims(arr)
  
      if height_multiple is not None:
          assert height_multiple > 0, (
              "Can only crop to multiples of 1 or larger, got %d." % (
                  height_multiple,))
      if width_multiple is not None:
          assert width_multiple > 0, (
              "Can only crop to multiples of 1 or larger, got %d." % (
                  width_multiple,))
  
      shape = arr.shape if hasattr(arr, "shape") else arr
      height, width = shape[0:2]
  
      top, bottom = _compute_axis_value(height, height_multiple)
      left, right = _compute_axis_value(width, width_multiple)
  
      return top, right, bottom, left
  
  
  def compute_paddings_to_reach_powers_of(arr, height_base, width_base,
                                          allow_zero_exponent=False):
      """Compute paddings to reach powers of given base values.
  
      For given axis size ``S``, padded size ``S'`` (``S' >= S``) and base ``B``
      this function computes paddings that fulfill ``S' = B^E``, where ``E``
      is any exponent from the discrete interval ``[0 .. inf)``.
  
      See :func:`~imgaug.imgaug.compute_paddings_for_aspect_ratio` for an
      explanation of how the required padding amounts are distributed per
      image axis.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray or tuple of int
          Image-like array or shape tuple for which to compute pad amounts.
  
      height_base : None or int
          The desired base of the height.
  
      width_base : None or int
          The desired base of the width.
  
      allow_zero_exponent : bool, optional
          Whether ``E=0`` in ``S'=B^E`` is a valid value. If ``True``, axes
          with size ``0`` or ``1`` will be padded up to size ``B^0=1`` and
          axes with size ``1 < S <= B`` will be padded up to ``B^1=B``.
          If ``False``, the minimum output axis size is always at least ``B``.
  
      Returns
      -------
      tuple of int
          Required padding amounts to fulfill ``S' = B^E`` given as a
          ``tuple`` of the form ``(top, right, bottom, left)``.
  
      """
      def _compute_axis_value(axis_size, base):
          if base is None:
              return 0, 0
          if axis_size == 0:
              to_pad = 1 if allow_zero_exponent else base
          elif axis_size <= base:
              to_pad = base - axis_size
          else:
              # log_{base}(axis_size) in numpy
              exponent = np.log(axis_size) / np.log(base)
  
              to_pad = (base ** int(np.ceil(exponent))) - axis_size
  
          return int(np.floor(to_pad/2)), int(np.ceil(to_pad/2))
  
      _assert_two_or_three_dims(arr)
  
      if height_base is not None:
          assert height_base > 1, (
              "Can only pad to base larger than 1, got %d." % (height_base,))
      if width_base is not None:
          assert width_base > 1, (
              "Can only pad to base larger than 1, got %d." % (width_base,))
  
      shape = arr.shape if hasattr(arr, "shape") else arr
      height, width = shape[0:2]
  
      top, bottom = _compute_axis_value(height, height_base)
      left, right = _compute_axis_value(width, width_base)
  
      return top, right, bottom, left
  
  
  def compute_croppings_to_reach_powers_of(arr, height_base, width_base,
                                           allow_zero_exponent=False):
      """Compute croppings to reach powers of given base values.
  
      For given axis size ``S``, cropped size ``S'`` (``S' <= S``) and base ``B``
      this function computes croppings that fulfill ``S' = B^E``, where ``E``
      is any exponent from the discrete interval ``[0 .. inf)``.
  
      See :func:`~imgaug.imgaug.compute_paddings_for_aspect_ratio` for an
      explanation of how the required cropping amounts are distributed per
      image axis.
  
      .. note::
  
          For axes where ``S == 0``, this function alwayws returns zeros as
          croppings.
  
          For axes where ``1 <= S < B`` see parameter `allow_zero_exponent`.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray or tuple of int
          Image-like array or shape tuple for which to compute crop amounts.
  
      height_base : None or int
          The desired base of the height.
  
      width_base : None or int
          The desired base of the width.
  
      allow_zero_exponent : bool
          Whether ``E=0`` in ``S'=B^E`` is a valid value. If ``True``, axes
          with size ``1 <= S < B`` will be cropped to size ``B^0=1``.
          If ``False``, axes with sizes ``S < B`` will not be changed.
  
      Returns
      -------
      tuple of int
          Required cropping amounts to fulfill ``S' = B^E`` given as a
          ``tuple`` of the form ``(top, right, bottom, left)``.
  
      """
      def _compute_axis_value(axis_size, base):
          if base is None:
              return 0, 0
          if axis_size == 0:
              to_crop = 0
          elif axis_size < base:
              # crop down to B^0 = 1
              to_crop = axis_size - 1 if allow_zero_exponent else 0
          else:
              # log_{base}(axis_size) in numpy
              exponent = np.log(axis_size) / np.log(base)
  
              to_crop = axis_size - (base ** int(exponent))
  
          return int(np.floor(to_crop/2)), int(np.ceil(to_crop/2))
  
      _assert_two_or_three_dims(arr)
  
      if height_base is not None:
          assert height_base > 1, (
              "Can only crop to base larger than 1, got %d." % (height_base,))
      if width_base is not None:
          assert width_base > 1, (
              "Can only crop to base larger than 1, got %d." % (width_base,))
  
      shape = arr.shape if hasattr(arr, "shape") else arr
      height, width = shape[0:2]
  
      top, bottom = _compute_axis_value(height, height_base)
      left, right = _compute_axis_value(width, width_base)
  
      return top, right, bottom, left
  
  
  @ia.deprecated(alt_func="Resize",
                 comment="Resize has the exactly same interface as Scale.")
  def Scale(*args, **kwargs):
      """Augmenter that resizes images to specified heights and widths."""
      # pylint: disable=invalid-name
      return Resize(*args, **kwargs)
  
  
  class Resize(meta.Augmenter):
      """Augmenter that resizes images to specified heights and widths.
  
      Supported dtypes
      ----------------
  
      See :func:`~imgaug.imgaug.imresize_many_images`.
  
      Parameters
      ----------
      size : 'keep' or int or float or tuple of int or tuple of float or list of int or list of float or imgaug.parameters.StochasticParameter or dict
          The new size of the images.
  
              * If this has the string value ``keep``, the original height and
                width values will be kept (image is not resized).
              * If this is an ``int``, this value will always be used as the new
                height and width of the images.
              * If this is a ``float`` ``v``, then per image the image's height
                ``H`` and width ``W`` will be changed to ``H*v`` and ``W*v``.
              * If this is a ``tuple``, it is expected to have two entries
                ``(a, b)``. If at least one of these are ``float`` s, a value
                will be sampled from range ``[a, b]`` and used as the ``float``
                value to resize the image (see above). If both are ``int`` s, a
                value will be sampled from the discrete range ``[a..b]`` and
                used as the integer value to resize the image (see above).
              * If this is a ``list``, a random value from the ``list`` will be
                picked to resize the image. All values in the ``list`` must be
                ``int`` s or ``float`` s (no mixture is possible).
              * If this is a ``StochasticParameter``, then this parameter will
                first be queried once per image. The resulting value will be used
                for both height and width.
              * If this is a ``dict``, it may contain the keys ``height`` and
                ``width`` or the keys ``shorter-side`` and ``longer-side``. Each
                key may have the same datatypes as above and describes the
                scaling on x and y-axis or the shorter and longer axis,
                respectively. Both axis are sampled independently. Additionally,
                one of the keys may have the value ``keep-aspect-ratio``, which
                means that the respective side of the image will be resized so
                that the original aspect ratio is kept. This is useful when only
                resizing one image size by a pixel value (e.g. resize images to
                a height of ``64`` pixels and resize the width so that the
                overall aspect ratio is maintained).
  
      interpolation : imgaug.ALL or int or str or list of int or list of str or imgaug.parameters.StochasticParameter, optional
          Interpolation to use.
  
              * If ``imgaug.ALL``, then a random interpolation from ``nearest``,
                ``linear``, ``area`` or ``cubic`` will be picked (per image).
              * If ``int``, then this interpolation will always be used.
                Expected to be any of the following:
                ``cv2.INTER_NEAREST``, ``cv2.INTER_LINEAR``, ``cv2.INTER_AREA``,
                ``cv2.INTER_CUBIC``
              * If string, then this interpolation will always be used.
                Expected to be any of the following:
                ``nearest``, ``linear``, ``area``, ``cubic``
              * If ``list`` of ``int`` / ``str``, then a random one of the values
                will be picked per image as the interpolation.
              * If a ``StochasticParameter``, then this parameter will be
                queried per image and is expected to return an ``int`` or
                ``str``.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.Resize(32)
  
      Resize all images to ``32x32`` pixels.
  
      >>> aug = iaa.Resize(0.5)
  
      Resize all images to ``50`` percent of their original size.
  
      >>> aug = iaa.Resize((16, 22))
  
      Resize all images to a random height and width within the discrete
      interval ``[16..22]`` (uniformly sampled per image).
  
      >>> aug = iaa.Resize((0.5, 0.75))
  
      Resize all any input image so that its height (``H``) and width (``W``)
      become ``H*v`` and ``W*v``, where ``v`` is uniformly sampled from the
      interval ``[0.5, 0.75]``.
  
      >>> aug = iaa.Resize([16, 32, 64])
  
      Resize all images either to ``16x16``, ``32x32`` or ``64x64`` pixels.
  
      >>> aug = iaa.Resize({"height": 32})
  
      Resize all images to a height of ``32`` pixels and keeps the original
      width.
  
      >>> aug = iaa.Resize({"height": 32, "width": 48})
  
      Resize all images to a height of ``32`` pixels and a width of ``48``.
  
      >>> aug = iaa.Resize({"height": 32, "width": "keep-aspect-ratio"})
  
      Resize all images to a height of ``32`` pixels and resizes the
      x-axis (width) so that the aspect ratio is maintained.
  
      >>> aug = iaa.Resize(
      >>>     {"shorter-side": 224, "longer-side": "keep-aspect-ratio"})
  
      Resize all images to a height/width of ``224`` pixels, depending on which
      axis is shorter and resize the other axis so that the aspect ratio is
      maintained.
  
      >>> aug = iaa.Resize({"height": (0.5, 0.75), "width": [16, 32, 64]})
  
      Resize all images to a height of ``H*v``, where ``H`` is the original
      height and ``v`` is a random value sampled from the interval
      ``[0.5, 0.75]``. The width/x-axis of each image is resized to either
      ``16`` or ``32`` or ``64`` pixels.
  
      >>> aug = iaa.Resize(32, interpolation=["linear", "cubic"])
  
      Resize all images to ``32x32`` pixels. Randomly use either ``linear``
      or ``cubic`` interpolation.
  
      """
  
      def __init__(self, size, interpolation="cubic",
                   seed=None, name=None, **old_kwargs):
          super(Resize, self).__init__(
              seed=seed, name=name, **old_kwargs)
  
          self.size, self.size_order = self._handle_size_arg(size, False)
          self.interpolation = self._handle_interpolation_arg(interpolation)
  
      @classmethod
      def _handle_size_arg(cls, size, subcall):
          def _dict_to_size_tuple(val1, val2):
              kaa = "keep-aspect-ratio"
              not_both_kaa = (val1 != kaa or val2 != kaa)
              assert not_both_kaa, (
                  "Expected at least one value to not be \"keep-aspect-ratio\", "
                  "but got it two times.")
  
              size_tuple = []
              for k in [val1, val2]:
                  if k in ["keep-aspect-ratio", "keep"]:
                      entry = iap.Deterministic(k)
                  else:
                      entry = cls._handle_size_arg(k, True)
                  size_tuple.append(entry)
              return tuple(size_tuple)
  
          def _contains_any_key(dict_, keys):
              return any([key in dict_ for key in keys])
  
          # HW = height, width
          # SL = shorter, longer
          size_order = "HW"
  
          if size == "keep":
              result = iap.Deterministic("keep")
          elif ia.is_single_number(size):
              assert size > 0, "Expected only values > 0, got %s" % (size,)
              result = iap.Deterministic(size)
          elif not subcall and isinstance(size, dict):
              if len(size.keys()) == 0:
                  result = iap.Deterministic("keep")
              elif _contains_any_key(size, ["height", "width"]):
                  height = size.get("height", "keep")
                  width = size.get("width", "keep")
                  result = _dict_to_size_tuple(height, width)
              elif _contains_any_key(size, ["shorter-side", "longer-side"]):
                  shorter = size.get("shorter-side", "keep")
                  longer = size.get("longer-side", "keep")
                  result = _dict_to_size_tuple(shorter, longer)
                  size_order = "SL"
              else:
                  raise ValueError(
                      "Expected dictionary containing no keys, "
                      "the keys \"height\" and/or \"width\", "
                      "or the keys \"shorter-side\" and/or \"longer-side\". "
                      "Got keys: %s." % (str(size.keys()),))
          elif isinstance(size, tuple):
              assert len(size) == 2, (
                  "Expected size tuple to contain exactly 2 values, "
                  "got %d." % (len(size),))
              assert size[0] > 0 and size[1] > 0, (
                  "Expected size tuple to only contain values >0, "
                  "got %d and %d." % (size[0], size[1]))
              if ia.is_single_float(size[0]) or ia.is_single_float(size[1]):
                  result = iap.Uniform(size[0], size[1])
              else:
                  result = iap.DiscreteUniform(size[0], size[1])
          elif isinstance(size, list):
              if len(size) == 0:
                  result = iap.Deterministic("keep")
              else:
                  all_int = all([ia.is_single_integer(v) for v in size])
                  all_float = all([ia.is_single_float(v) for v in size])
                  assert all_int or all_float, (
                      "Expected to get only integers or floats.")
                  assert all([v > 0 for v in size]), (
                      "Expected all values to be >0.")
                  result = iap.Choice(size)
          elif isinstance(size, iap.StochasticParameter):
              result = size
          else:
              raise ValueError(
                  "Expected number, tuple of two numbers, list of numbers, "
                  "dictionary of form "
                  "{'height': number/tuple/list/'keep-aspect-ratio'/'keep', "
                  "'width': <analogous>}, dictionary of form "
                  "{'shorter-side': number/tuple/list/'keep-aspect-ratio'/"
                  "'keep', 'longer-side': <analogous>} "
                  "or StochasticParameter, got %s." % (type(size),)
              )
  
          if subcall:
              return result
          return result, size_order
  
      @classmethod
      def _handle_interpolation_arg(cls, interpolation):
          if interpolation == ia.ALL:
              interpolation = iap.Choice(
                  ["nearest", "linear", "area", "cubic"])
          elif ia.is_single_integer(interpolation):
              interpolation = iap.Deterministic(interpolation)
          elif ia.is_string(interpolation):
              interpolation = iap.Deterministic(interpolation)
          elif ia.is_iterable(interpolation):
              interpolation = iap.Choice(interpolation)
          elif isinstance(interpolation, iap.StochasticParameter):
              pass
          else:
              raise Exception(
                  "Expected int or string or iterable or StochasticParameter, "
                  "got %s." % (type(interpolation),))
          return interpolation
  
      def _augment_batch_(self, batch, random_state, parents, hooks):
          nb_rows = batch.nb_rows
          samples = self._draw_samples(nb_rows, random_state)
  
          if batch.images is not None:
              batch.images = self._augment_images_by_samples(batch.images,
                                                             samples)
  
          if batch.heatmaps is not None:
              # TODO this uses the same interpolation as for images for heatmaps
              #      while other augmenters resort to cubic
              batch.heatmaps = self._augment_maps_by_samples(
                  batch.heatmaps, "arr_0to1", samples)
  
          if batch.segmentation_maps is not None:
              batch.segmentation_maps = self._augment_maps_by_samples(
                  batch.segmentation_maps, "arr",
                  (samples[0], samples[1], [None] * nb_rows))
  
          for augm_name in ["keypoints", "bounding_boxes", "polygons",
                            "line_strings"]:
              augm_value = getattr(batch, augm_name)
              if augm_value is not None:
                  func = functools.partial(
                      self._augment_keypoints_by_samples,
                      samples=samples)
                  cbaois = self._apply_to_cbaois_as_keypoints(augm_value, func)
                  setattr(batch, augm_name, cbaois)
  
          return batch
  
      def _augment_images_by_samples(self, images, samples):
          input_was_array = False
          input_dtype = None
          if ia.is_np_array(images):
              input_was_array = True
              input_dtype = images.dtype
  
          samples_a, samples_b, samples_ip = samples
          result = []
          for i, image in enumerate(images):
              h, w = self._compute_height_width(image.shape, samples_a[i],
                                                samples_b[i], self.size_order)
              image_rs = ia.imresize_single_image(image, (h, w),
                                                  interpolation=samples_ip[i])
              result.append(image_rs)
  
          if input_was_array:
              all_same_size = (len({image.shape for image in result}) == 1)
              if all_same_size:
                  result = np.array(result, dtype=input_dtype)
  
          return result
  
      def _augment_maps_by_samples(self, augmentables, arr_attr_name, samples):
          result = []
          samples_h, samples_w, samples_ip = samples
  
          for i, augmentable in enumerate(augmentables):
              arr = getattr(augmentable, arr_attr_name)
              arr_shape = arr.shape
              img_shape = augmentable.shape
              h_img, w_img = self._compute_height_width(
                  img_shape, samples_h[i], samples_w[i], self.size_order)
              h = int(np.round(h_img * (arr_shape[0] / img_shape[0])))
              w = int(np.round(w_img * (arr_shape[1] / img_shape[1])))
              h = max(h, 1)
              w = max(w, 1)
              if samples_ip[0] is not None:
                  # TODO change this for heatmaps to always have cubic or
                  #      automatic interpolation?
                  augmentable_resize = augmentable.resize(
                      (h, w), interpolation=samples_ip[i])
              else:
                  augmentable_resize = augmentable.resize((h, w))
              augmentable_resize.shape = (h_img, w_img) + img_shape[2:]
              result.append(augmentable_resize)
  
          return result
  
      def _augment_keypoints_by_samples(self, kpsois, samples):
          result = []
          samples_a, samples_b, _samples_ip = samples
          for i, kpsoi in enumerate(kpsois):
              h, w = self._compute_height_width(
                  kpsoi.shape, samples_a[i], samples_b[i], self.size_order)
              new_shape = (h, w) + kpsoi.shape[2:]
              keypoints_on_image_rs = kpsoi.on_(new_shape)
  
              result.append(keypoints_on_image_rs)
  
          return result
  
      def _draw_samples(self, nb_images, random_state):
          rngs = random_state.duplicate(3)
          if isinstance(self.size, tuple):
              samples_h = self.size[0].draw_samples(nb_images,
                                                    random_state=rngs[0])
              samples_w = self.size[1].draw_samples(nb_images,
                                                    random_state=rngs[1])
          else:
              samples_h = self.size.draw_samples(nb_images, random_state=rngs[0])
              samples_w = samples_h
  
          samples_ip = self.interpolation.draw_samples(nb_images,
                                                       random_state=rngs[2])
          return samples_h, samples_w, samples_ip
  
      @classmethod
      def _compute_height_width(cls, image_shape, sample_a, sample_b, size_order):
          imh, imw = image_shape[0:2]
  
          if size_order == 'SL':
              # size order: short, long
              if imh < imw:
                  h, w = sample_a, sample_b
              else:
                  w, h = sample_a, sample_b
          else:
              # size order: height, width
              h, w = sample_a, sample_b
  
          if ia.is_single_float(h):
              assert h > 0, "Expected 'h' to be >0, got %.4f" % (h,)
              h = int(np.round(imh * h))
              h = h if h > 0 else 1
          elif h == "keep":
              h = imh
          if ia.is_single_float(w):
              assert w > 0, "Expected 'w' to be >0, got %.4f" % (w,)
              w = int(np.round(imw * w))
              w = w if w > 0 else 1
          elif w == "keep":
              w = imw
  
          # at least the checks for keep-aspect-ratio must come after
          # the float checks, as they are dependent on the results
          # this is also why these are not written as elifs
          if h == "keep-aspect-ratio":
              h_per_w_orig = imh / imw
              h = int(np.round(w * h_per_w_orig))
          if w == "keep-aspect-ratio":
              w_per_h_orig = imw / imh
              w = int(np.round(h * w_per_h_orig))
  
          return h, w
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.size, self.interpolation, self.size_order]
  
  
  class _CropAndPadSamplingResult(object):
      def __init__(self, crop_top, crop_right, crop_bottom, crop_left,
                   pad_top, pad_right, pad_bottom, pad_left, pad_mode, pad_cval):
          self.crop_top = crop_top
          self.crop_right = crop_right
          self.crop_bottom = crop_bottom
          self.crop_left = crop_left
          self.pad_top = pad_top
          self.pad_right = pad_right
          self.pad_bottom = pad_bottom
          self.pad_left = pad_left
          self.pad_mode = pad_mode
          self.pad_cval = pad_cval
  
      @property
      def croppings(self):
          """Get absolute pixel amounts of croppings as a TRBL tuple."""
          return self.crop_top, self.crop_right, self.crop_bottom, self.crop_left
  
      @property
      def paddings(self):
          """Get absolute pixel amounts of paddings as a TRBL tuple."""
          return self.pad_top, self.pad_right, self.pad_bottom, self.pad_left
  
  
  class CropAndPad(meta.Augmenter):
      """Crop/pad images by pixel amounts or fractions of image sizes.
  
      Cropping removes pixels at the sides (i.e. extracts a subimage from
      a given full image). Padding adds pixels to the sides (e.g. black pixels).
  
      This augmenter will never crop images below a height or width of ``1``.
  
      .. note::
  
          This augmenter automatically resizes images back to their original size
          after it has augmented them. To deactivate this, add the
          parameter ``keep_size=False``.
  
      Supported dtypes
      ----------------
  
      if (keep_size=False):
  
          * ``uint8``: yes; fully tested
          * ``uint16``: yes; tested
          * ``uint32``: yes; tested
          * ``uint64``: yes; tested
          * ``int8``: yes; tested
          * ``int16``: yes; tested
          * ``int32``: yes; tested
          * ``int64``: yes; tested
          * ``float16``: yes; tested
          * ``float32``: yes; tested
          * ``float64``: yes; tested
          * ``float128``: yes; tested
          * ``bool``: yes; tested
  
      if (keep_size=True):
  
          minimum of (
              ``imgaug.augmenters.size.CropAndPad(keep_size=False)``,
              :func:`~imgaug.imgaug.imresize_many_images`
          )
  
      Parameters
      ----------
      px : None or int or imgaug.parameters.StochasticParameter or tuple, optional
          The number of pixels to crop (negative values) or pad (positive values)
          on each side of the image. Either this or the parameter `percent` may
          be set, not both at the same time.
  
              * If ``None``, then pixel-based cropping/padding will not be used.
              * If ``int``, then that exact number of pixels will always be
                cropped/padded.
              * If ``StochasticParameter``, then that parameter will be used for
                each image. Four samples will be drawn per image (top, right,
                bottom, left), unless `sample_independently` is set to ``False``,
                as then only one value will be sampled per image and used for
                all sides.
              * If a ``tuple`` of two ``int`` s with values ``a`` and ``b``,
                then each side will be cropped/padded by a random amount sampled
                uniformly per image and side from the inteval ``[a, b]``. If
                however `sample_independently` is set to ``False``, only one
                value will be sampled per image and used for all sides.
              * If a ``tuple`` of four entries, then the entries represent top,
                right, bottom, left. Each entry may be a single ``int`` (always
                crop/pad by exactly that value), a ``tuple`` of two ``int`` s
                ``a`` and ``b`` (crop/pad by an amount within ``[a, b]``), a
                ``list`` of ``int`` s (crop/pad by a random value that is
                contained in the ``list``) or a ``StochasticParameter`` (sample
                the amount to crop/pad from that parameter).
  
      percent : None or number or imgaug.parameters.StochasticParameter or tuple, optional
          The number of pixels to crop (negative values) or pad (positive values)
          on each side of the image given as a *fraction* of the image
          height/width. E.g. if this is set to ``-0.1``, the augmenter will
          always crop away ``10%`` of the image's height at both the top and the
          bottom (both ``10%`` each), as well as ``10%`` of the width at the
          right and left.
          Expected value range is ``(-1.0, inf)``.
          Either this or the parameter `px` may be set, not both
          at the same time.
  
              * If ``None``, then fraction-based cropping/padding will not be
                used.
              * If ``number``, then that fraction will always be cropped/padded.
              * If ``StochasticParameter``, then that parameter will be used for
                each image. Four samples will be drawn per image (top, right,
                bottom, left). If however `sample_independently` is set to
                ``False``, only one value will be sampled per image and used for
                all sides.
              * If a ``tuple`` of two ``float`` s with values ``a`` and ``b``,
                then each side will be cropped/padded by a random fraction
                sampled uniformly per image and side from the interval
                ``[a, b]``. If however `sample_independently` is set to
                ``False``, only one value will be sampled per image and used for
                all sides.
              * If a ``tuple`` of four entries, then the entries represent top,
                right, bottom, left. Each entry may be a single ``float``
                (always crop/pad by exactly that percent value), a ``tuple`` of
                two ``float`` s ``a`` and ``b`` (crop/pad by a fraction from
                ``[a, b]``), a ``list`` of ``float`` s (crop/pad by a random
                value that is contained in the list) or a ``StochasticParameter``
                (sample the percentage to crop/pad from that parameter).
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          Padding mode to use. The available modes match the numpy padding modes,
          i.e. ``constant``, ``edge``, ``linear_ramp``, ``maximum``, ``median``,
          ``minimum``, ``reflect``, ``symmetric``, ``wrap``. The modes
          ``constant`` and ``linear_ramp`` use extra values, which are provided
          by ``pad_cval`` when necessary. See :func:`~imgaug.imgaug.pad` for
          more details.
  
              * If ``imgaug.ALL``, then a random mode from all available modes
                will be sampled per image.
              * If a ``str``, it will be used as the pad mode for all images.
              * If a ``list`` of ``str``, a random one of these will be sampled
                per image and used as the mode.
              * If ``StochasticParameter``, a random mode will be sampled from
                this parameter per image.
  
      pad_cval : number or tuple of number list of number or imgaug.parameters.StochasticParameter, optional
          The constant value to use if the pad mode is ``constant`` or the end
          value to use if the mode is ``linear_ramp``.
          See :func:`~imgaug.imgaug.pad` for more details.
  
              * If ``number``, then that value will be used.
              * If a ``tuple`` of two ``number`` s and at least one of them is
                a ``float``, then a random number will be uniformly sampled per
                image from the continuous interval ``[a, b]`` and used as the
                value. If both ``number`` s are ``int`` s, the interval is
                discrete.
              * If a ``list`` of ``number``, then a random value will be chosen
                from the elements of the ``list`` and used as the value.
              * If ``StochasticParameter``, a random value will be sampled from
                that parameter per image.
  
      keep_size : bool, optional
          After cropping and padding, the result image will usually have a
          different height/width compared to the original input image. If this
          parameter is set to ``True``, then the cropped/padded image will be
          resized to the input image's size, i.e. the augmenter's output shape
          is always identical to the input shape.
  
      sample_independently : bool, optional
          If ``False`` *and* the values for `px`/`percent` result in exactly
          *one* probability distribution for all image sides, only one single
          value will be sampled from that probability distribution and used for
          all sides. I.e. the crop/pad amount then is the same for all sides.
          If ``True``, four values will be sampled independently, one per side.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CropAndPad(px=(-10, 0))
  
      Crop each side by a random pixel value sampled uniformly per image and
      side from the discrete interval ``[-10..0]``.
  
      >>> aug = iaa.CropAndPad(px=(0, 10))
  
      Pad each side by a random pixel value sampled uniformly per image and
      side from the discrete interval ``[0..10]``. The padding happens by
      zero-padding, i.e. it adds black pixels (default setting).
  
      >>> aug = iaa.CropAndPad(px=(0, 10), pad_mode="edge")
  
      Pad each side by a random pixel value sampled uniformly per image and
      side from the discrete interval ``[0..10]``. The padding uses the
      ``edge`` mode from numpy's pad function, i.e. the pixel colors around
      the image sides are repeated.
  
      >>> aug = iaa.CropAndPad(px=(0, 10), pad_mode=["constant", "edge"])
  
      Similar to the previous example, but uses zero-padding (``constant``) for
      half of the images and ``edge`` padding for the other half.
  
      >>> aug = iaa.CropAndPad(px=(0, 10), pad_mode=ia.ALL, pad_cval=(0, 255))
  
      Similar to the previous example, but uses any available padding mode.
      In case the padding mode ends up being ``constant`` or ``linear_ramp``,
      and random intensity is uniformly sampled (once per image) from the
      discrete interval ``[0..255]`` and used as the intensity of the new
      pixels.
  
      >>> aug = iaa.CropAndPad(px=(0, 10), sample_independently=False)
  
      Pad each side by a random pixel value sampled uniformly once per image
      from the discrete interval ``[0..10]``. Each sampled value is used
      for *all* sides of the corresponding image.
  
      >>> aug = iaa.CropAndPad(px=(0, 10), keep_size=False)
  
      Pad each side by a random pixel value sampled uniformly per image and
      side from the discrete interval ``[0..10]``. Afterwards, do **not**
      resize the padded image back to the input image's size. This will increase
      the image's height and width by a maximum of ``20`` pixels.
  
      >>> aug = iaa.CropAndPad(px=((0, 10), (0, 5), (0, 10), (0, 5)))
  
      Pad the top and bottom by a random pixel value sampled uniformly from the
      discrete interval ``[0..10]``. Pad the left and right analogously by
      a random value sampled from ``[0..5]``. Each value is always sampled
      independently.
  
      >>> aug = iaa.CropAndPad(percent=(0, 0.1))
  
      Pad each side by a random fraction sampled uniformly from the continuous
      interval ``[0.0, 0.10]``. The fraction is sampled once per image and
      side. E.g. a sampled fraction of ``0.1`` for the top side would pad by
      ``0.1*H``, where ``H`` is the height of the input image.
  
      >>> aug = iaa.CropAndPad(
      >>>     percent=([0.05, 0.1], [0.05, 0.1], [0.05, 0.1], [0.05, 0.1]))
  
      Pads each side by either ``5%`` or ``10%``. The values are sampled
      once per side and image.
  
      >>> aug = iaa.CropAndPad(px=(-10, 10))
  
      Sample uniformly per image and side a value ``v`` from the discrete range
      ``[-10..10]``. Then either crop (negative sample) or pad (positive sample)
      the side by ``v`` pixels.
  
      """
  
      def __init__(self, px=None, percent=None, pad_mode="constant", pad_cval=0,
                   keep_size=True, sample_independently=True,
                   seed=None, name=None, **old_kwargs):
          # pylint: disable=invalid-name
          super(CropAndPad, self).__init__(
              seed=seed, name=name, **old_kwargs)
  
          self.mode, self.all_sides, self.top, self.right, self.bottom, \
              self.left = self._handle_px_and_percent_args(px, percent)
  
          self.pad_mode = _handle_pad_mode_param(pad_mode)
          # TODO enable ALL here, like in e.g. Affine
          self.pad_cval = iap.handle_discrete_param(
              pad_cval, "pad_cval", value_range=None, tuple_to_uniform=True,
              list_to_choice=True, allow_floats=True)
  
          self.keep_size = keep_size
          self.sample_independently = sample_independently
  
          # set these to None to use the same values as sampled for the
          # images (not tested)
          self._pad_mode_heatmaps = "constant"
          self._pad_mode_segmentation_maps = "constant"
          self._pad_cval_heatmaps = 0.0
          self._pad_cval_segmentation_maps = 0
  
      @classmethod
      def _handle_px_and_percent_args(cls, px, percent):
          # pylint: disable=invalid-name
          all_sides = None
          top, right, bottom, left = None, None, None, None
  
          if px is None and percent is None:
              mode = "noop"
          elif px is not None and percent is not None:
              raise Exception("Can only pad by pixels or percent, not both.")
          elif px is not None:
              mode = "px"
              all_sides, top, right, bottom, left = cls._handle_px_arg(px)
          else:  # = elif percent is not None:
              mode = "percent"
              all_sides, top, right, bottom, left = cls._handle_percent_arg(
                  percent)
          return mode, all_sides, top, right, bottom, left
  
      @classmethod
      def _handle_px_arg(cls, px):
          # pylint: disable=invalid-name
          all_sides = None
          top, right, bottom, left = None, None, None, None
  
          if ia.is_single_integer(px):
              all_sides = iap.Deterministic(px)
          elif isinstance(px, tuple):
              assert len(px) in [2, 4], (
                  "Expected 'px' given as a tuple to contain 2 or 4 "
                  "entries, got %d." % (len(px),))
  
              def handle_param(p):
                  if ia.is_single_integer(p):
                      return iap.Deterministic(p)
                  if isinstance(p, tuple):
                      assert len(p) == 2, (
                          "Expected tuple of 2 values, got %d." % (len(p)))
                      only_ints = (
                          ia.is_single_integer(p[0])
                          and ia.is_single_integer(p[1]))
                      assert only_ints, (
                          "Expected tuple of integers, got %s and %s." % (
                              type(p[0]), type(p[1])))
                      return iap.DiscreteUniform(p[0], p[1])
                  if isinstance(p, list):
                      assert len(p) > 0, (
                          "Expected non-empty list, but got empty one.")
                      assert all([ia.is_single_integer(val) for val in p]), (
                          "Expected list of ints, got types %s." % (
                              ", ".join([str(type(v)) for v in p])))
                      return iap.Choice(p)
                  if isinstance(p, iap.StochasticParameter):
                      return p
                  raise Exception(
                      "Expected int, tuple of two ints, list of ints or "
                      "StochasticParameter, got type %s." % (type(p),))
  
              if len(px) == 2:
                  all_sides = handle_param(px)
              else:  # len == 4
                  top = handle_param(px[0])
                  right = handle_param(px[1])
                  bottom = handle_param(px[2])
                  left = handle_param(px[3])
          elif isinstance(px, iap.StochasticParameter):
              top = right = bottom = left = px
          else:
              raise Exception(
                  "Expected int, tuple of 4 "
                  "ints/tuples/lists/StochasticParameters or "
                  "StochasticParameter, got type %s." % (type(px),))
          return all_sides, top, right, bottom, left
  
      @classmethod
      def _handle_percent_arg(cls, percent):
          all_sides = None
          top, right, bottom, left = None, None, None, None
  
          if ia.is_single_number(percent):
              assert percent > -1.0, (
                  "Expected 'percent' to be >-1.0, got %.4f." % (percent,))
              all_sides = iap.Deterministic(percent)
          elif isinstance(percent, tuple):
              assert len(percent) in [2, 4], (
                  "Expected 'percent' given as a tuple to contain 2 or 4 "
                  "entries, got %d." % (len(percent),))
  
              def handle_param(p):
                  if ia.is_single_number(p):
                      return iap.Deterministic(p)
                  if isinstance(p, tuple):
                      assert len(p) == 2, (
                          "Expected tuple of 2 values, got %d." % (len(p),))
                      only_numbers = (
                          ia.is_single_number(p[0])
                          and ia.is_single_number(p[1]))
                      assert only_numbers, (
                          "Expected tuple of numbers, got %s and %s." % (
                              type(p[0]), type(p[1])))
                      assert p[0] > -1.0 and p[1] > -1.0, (
                          "Expected tuple of values >-1.0, got %.4f and "
                          "%.4f." % (p[0], p[1]))
                      return iap.Uniform(p[0], p[1])
                  if isinstance(p, list):
                      assert len(p) > 0, (
                          "Expected non-empty list, but got empty one.")
                      assert all([ia.is_single_number(val) for val in p]), (
                          "Expected list of numbers, got types %s." % (
                              ", ".join([str(type(v)) for v in p])))
                      assert all([val > -1.0 for val in p]), (
                          "Expected list of values >-1.0, got values %s." % (
                              ", ".join(["%.4f" % (v,) for v in p])))
                      return iap.Choice(p)
                  if isinstance(p, iap.StochasticParameter):
                      return p
                  raise Exception(
                      "Expected int, tuple of two ints, list of ints or "
                      "StochasticParameter, got type %s." % (type(p),))
  
              if len(percent) == 2:
                  all_sides = handle_param(percent)
              else:  # len == 4
                  top = handle_param(percent[0])
                  right = handle_param(percent[1])
                  bottom = handle_param(percent[2])
                  left = handle_param(percent[3])
          elif isinstance(percent, iap.StochasticParameter):
              top = right = bottom = left = percent
          else:
              raise Exception(
                  "Expected number, tuple of 4 "
                  "numbers/tuples/lists/StochasticParameters or "
                  "StochasticParameter, got type %s." % (type(percent),))
          return all_sides, top, right, bottom, left
  
      def _augment_batch_(self, batch, random_state, parents, hooks):
          shapes = batch.get_rowwise_shapes()
          samples = self._draw_samples(random_state, shapes)
  
          if batch.images is not None:
              batch.images = self._augment_images_by_samples(batch.images,
                                                             samples)
  
          if batch.heatmaps is not None:
              batch.heatmaps = self._augment_maps_by_samples(
                  batch.heatmaps,
                  self._pad_mode_heatmaps, self._pad_cval_heatmaps,
                  samples)
  
          if batch.segmentation_maps is not None:
              batch.segmentation_maps = self._augment_maps_by_samples(
                  batch.segmentation_maps,
                  self._pad_mode_segmentation_maps,
                  self._pad_cval_segmentation_maps, samples)
  
          for augm_name in ["keypoints", "bounding_boxes", "polygons",
                            "line_strings"]:
              augm_value = getattr(batch, augm_name)
              if augm_value is not None:
                  func = functools.partial(
                      self._augment_keypoints_by_samples,
                      samples=samples)
                  cbaois = self._apply_to_cbaois_as_keypoints(augm_value, func)
                  setattr(batch, augm_name, cbaois)
  
          return batch
  
      def _augment_images_by_samples(self, images, samples):
          result = []
          for i, image in enumerate(images):
              samples_i = samples[i]
  
              image_cr_pa = _crop_and_pad_arr(
                  image, samples_i.croppings, samples_i.paddings,
                  samples_i.pad_mode, samples_i.pad_cval, self.keep_size)
  
              result.append(image_cr_pa)
  
          if ia.is_np_array(images):
              if self.keep_size:
                  result = np.array(result, dtype=images.dtype)
              else:
                  nb_shapes = len({image.shape for image in result})
                  if nb_shapes == 1:
                      result = np.array(result, dtype=images.dtype)
  
          return result
  
      def _augment_maps_by_samples(self, augmentables, pad_mode, pad_cval,
                                   samples):
          result = []
          for i, augmentable in enumerate(augmentables):
              samples_img = samples[i]
  
              augmentable = _crop_and_pad_hms_or_segmaps_(
                  augmentable,
                  croppings_img=samples_img.croppings,
                  paddings_img=samples_img.paddings,
                  pad_mode=(pad_mode
                            if pad_mode is not None
                            else samples_img.pad_mode),
                  pad_cval=(pad_cval
                            if pad_cval is not None
                            else samples_img.pad_cval),
                  keep_size=self.keep_size
              )
  
              result.append(augmentable)
  
          return result
  
      def _augment_keypoints_by_samples(self, keypoints_on_images, samples):
          result = []
          for i, keypoints_on_image in enumerate(keypoints_on_images):
              samples_i = samples[i]
  
              kpsoi_aug = _crop_and_pad_kpsoi_(
                  keypoints_on_image, croppings_img=samples_i.croppings,
                  paddings_img=samples_i.paddings, keep_size=self.keep_size)
              result.append(kpsoi_aug)
  
          return result
  
      def _draw_samples(self, random_state, shapes):
          nb_rows = len(shapes)
  
          if self.mode == "noop":
              top = right = bottom = left = np.full((nb_rows,), 0,
                                                    dtype=np.int32)
          else:
              if self.all_sides is not None:
                  if self.sample_independently:
                      samples = self.all_sides.draw_samples(
                          (nb_rows, 4), random_state=random_state)
                      top = samples[:, 0]
                      right = samples[:, 1]
                      bottom = samples[:, 2]
                      left = samples[:, 3]
                  else:
                      sample = self.all_sides.draw_samples(
                          (nb_rows,), random_state=random_state)
                      top = right = bottom = left = sample
              else:
                  top = self.top.draw_samples(
                      (nb_rows,), random_state=random_state)
                  right = self.right.draw_samples(
                      (nb_rows,), random_state=random_state)
                  bottom = self.bottom.draw_samples(
                      (nb_rows,), random_state=random_state)
                  left = self.left.draw_samples(
                      (nb_rows,), random_state=random_state)
  
              if self.mode == "px":
                  # no change necessary for pixel values
                  pass
              elif self.mode == "percent":
                  # percentage values have to be transformed to pixel values
                  shapes_arr = np.array([shape[0:2] for shape in shapes],
                                        dtype=np.float32)
                  heights = shapes_arr[:, 0]
                  widths = shapes_arr[:, 1]
                  top = np.round(heights * top).astype(np.int32)
                  right = np.round(widths * right).astype(np.int32)
                  bottom = np.round(heights * bottom).astype(np.int32)
                  left = np.round(widths * left).astype(np.int32)
              else:
                  raise Exception("Invalid mode")
  
          def _only_above_zero(arr):
              arr = np.copy(arr)
              mask = (arr < 0)
              arr[mask] = 0
              return arr
  
          crop_top = _only_above_zero((-1) * top)
          crop_right = _only_above_zero((-1) * right)
          crop_bottom = _only_above_zero((-1) * bottom)
          crop_left = _only_above_zero((-1) * left)
  
          pad_top = _only_above_zero(top)
          pad_right = _only_above_zero(right)
          pad_bottom = _only_above_zero(bottom)
          pad_left = _only_above_zero(left)
  
          pad_mode = self.pad_mode.draw_samples((nb_rows,),
                                                random_state=random_state)
          pad_cval = self.pad_cval.draw_samples((nb_rows,),
                                                random_state=random_state)
  
          # TODO vectorize this part -- especially return only one instance
          result = []
          for i, shape in enumerate(shapes):
              height, width = shape[0:2]
              crop_top_i, crop_right_i, crop_bottom_i, crop_left_i = \
                  _crop_prevent_zero_size(
                      height, width,
                      crop_top[i], crop_right[i], crop_bottom[i], crop_left[i])
  
              # add here any_crop_y to not warn in case of zero height/width
              # images
              any_crop_y = (crop_top_i > 0 or crop_bottom_i > 0)
              if any_crop_y and crop_top_i + crop_bottom_i >= height:
                  ia.warn(
                      "Expected generated crop amounts in CropAndPad for top and "
                      "bottom image side to be less than the image's height, but "
                      "got %d (top) and %d (bottom) vs. image height %d. This "
                      "will result in an image with output height=1 (if input "
                      "height was >=1) or output height=0 (if input height "
                      "was 0)." % (crop_top_i, crop_bottom_i, height))
  
              # add here any_crop_x to not warn in case of zero height/width
              # images
              any_crop_x = (crop_left_i > 0 or crop_right_i > 0)
              if any_crop_x and crop_left_i + crop_right_i >= width:
                  ia.warn(
                      "Expected generated crop amounts in CropAndPad for left "
                      "and right image side to be less than the image's width, "
                      "but got %d (left) and %d (right) vs. image width %d. "
                      "This will result in an image with output width=1 (if "
                      "input width was >=1) or output width=0 (if input width "
                      "was 0)." % (crop_left_i, crop_right_i, width))
  
              result.append(
                  _CropAndPadSamplingResult(
                      crop_top=crop_top_i,
                      crop_right=crop_right_i,
                      crop_bottom=crop_bottom_i,
                      crop_left=crop_left_i,
                      pad_top=pad_top[i],
                      pad_right=pad_right[i],
                      pad_bottom=pad_bottom[i],
                      pad_left=pad_left[i],
                      pad_mode=pad_mode[i],
                      pad_cval=pad_cval[i]))
          return result
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.all_sides, self.top, self.right, self.bottom, self.left,
                  self.pad_mode, self.pad_cval]
  
  
  class Pad(CropAndPad):
      """Pad images, i.e. adds columns/rows of pixels to them.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropAndPad`.
  
      Parameters
      ----------
      px : None or int or imgaug.parameters.StochasticParameter or tuple, optional
          The number of pixels to pad on each side of the image.
          Expected value range is ``[0, inf)``.
          Either this or the parameter `percent` may be set, not both at the same
          time.
  
              * If ``None``, then pixel-based padding will not be used.
              * If ``int``, then that exact number of pixels will always be
                padded.
              * If ``StochasticParameter``, then that parameter will be used for
                each image. Four samples will be drawn per image (top, right,
                bottom, left), unless `sample_independently` is set to ``False``,
                as then only one value will be sampled per image and used for
                all sides.
              * If a ``tuple`` of two ``int`` s with values ``a`` and ``b``,
                then each side will be padded by a random amount sampled
                uniformly per image and side from the inteval ``[a, b]``. If
                however `sample_independently` is set to ``False``, only one
                value will be sampled per image and used for all sides.
              * If a ``tuple`` of four entries, then the entries represent top,
                right, bottom, left. Each entry may be a single ``int`` (always
                pad by exactly that value), a ``tuple`` of two ``int`` s
                ``a`` and ``b`` (pad by an amount within ``[a, b]``), a
                ``list`` of ``int`` s (pad by a random value that is
                contained in the ``list``) or a ``StochasticParameter`` (sample
                the amount to pad from that parameter).
  
      percent : None or int or float or imgaug.parameters.StochasticParameter or tuple, optional
          The number of pixels to pad
          on each side of the image given as a *fraction* of the image
          height/width. E.g. if this is set to ``0.1``, the augmenter will
          always pad ``10%`` of the image's height at both the top and the
          bottom (both ``10%`` each), as well as ``10%`` of the width at the
          right and left.
          Expected value range is ``[0.0, inf)``.
          Either this or the parameter `px` may be set, not both
          at the same time.
  
              * If ``None``, then fraction-based padding will not be
                used.
              * If ``number``, then that fraction will always be padded.
              * If ``StochasticParameter``, then that parameter will be used for
                each image. Four samples will be drawn per image (top, right,
                bottom, left). If however `sample_independently` is set to
                ``False``, only one value will be sampled per image and used for
                all sides.
              * If a ``tuple`` of two ``float`` s with values ``a`` and ``b``,
                then each side will be padded by a random fraction
                sampled uniformly per image and side from the interval
                ``[a, b]``. If however `sample_independently` is set to
                ``False``, only one value will be sampled per image and used for
                all sides.
              * If a ``tuple`` of four entries, then the entries represent top,
                right, bottom, left. Each entry may be a single ``float``
                (always pad by exactly that fraction), a ``tuple`` of
                two ``float`` s ``a`` and ``b`` (pad by a fraction from
                ``[a, b]``), a ``list`` of ``float`` s (pad by a random
                value that is contained in the list) or a ``StochasticParameter``
                (sample the percentage to pad from that parameter).
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          Padding mode to use. The available modes match the numpy padding modes,
          i.e. ``constant``, ``edge``, ``linear_ramp``, ``maximum``, ``median``,
          ``minimum``, ``reflect``, ``symmetric``, ``wrap``. The modes
          ``constant`` and ``linear_ramp`` use extra values, which are provided
          by ``pad_cval`` when necessary. See :func:`~imgaug.imgaug.pad` for
          more details.
  
              * If ``imgaug.ALL``, then a random mode from all available modes
                will be sampled per image.
              * If a ``str``, it will be used as the pad mode for all images.
              * If a ``list`` of ``str``, a random one of these will be sampled
                per image and used as the mode.
              * If ``StochasticParameter``, a random mode will be sampled from
                this parameter per image.
  
      pad_cval : number or tuple of number list of number or imgaug.parameters.StochasticParameter, optional
          The constant value to use if the pad mode is ``constant`` or the end
          value to use if the mode is ``linear_ramp``.
          See :func:`~imgaug.imgaug.pad` for more details.
  
              * If ``number``, then that value will be used.
              * If a ``tuple`` of two ``number`` s and at least one of them is
                a ``float``, then a random number will be uniformly sampled per
                image from the continuous interval ``[a, b]`` and used as the
                value. If both ``number`` s are ``int`` s, the interval is
                discrete.
              * If a ``list`` of ``number``, then a random value will be chosen
                from the elements of the ``list`` and used as the value.
              * If ``StochasticParameter``, a random value will be sampled from
                that parameter per image.
  
      keep_size : bool, optional
          After padding, the result image will usually have a
          different height/width compared to the original input image. If this
          parameter is set to ``True``, then the padded image will be
          resized to the input image's size, i.e. the augmenter's output shape
          is always identical to the input shape.
  
      sample_independently : bool, optional
          If ``False`` *and* the values for `px`/`percent` result in exactly
          *one* probability distribution for all image sides, only one single
          value will be sampled from that probability distribution and used for
          all sides. I.e. the pad amount then is the same for all sides.
          If ``True``, four values will be sampled independently, one per side.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.Pad(px=(0, 10))
  
      Pad each side by a random pixel value sampled uniformly per image and
      side from the discrete interval ``[0..10]``. The padding happens by
      zero-padding, i.e. it adds black pixels (default setting).
  
      >>> aug = iaa.Pad(px=(0, 10), pad_mode="edge")
  
      Pad each side by a random pixel value sampled uniformly per image and
      side from the discrete interval ``[0..10]``. The padding uses the
      ``edge`` mode from numpy's pad function, i.e. the pixel colors around
      the image sides are repeated.
  
      >>> aug = iaa.Pad(px=(0, 10), pad_mode=["constant", "edge"])
  
      Similar to the previous example, but uses zero-padding (``constant``) for
      half of the images and ``edge`` padding for the other half.
  
      >>> aug = iaa.Pad(px=(0, 10), pad_mode=ia.ALL, pad_cval=(0, 255))
  
      Similar to the previous example, but uses any available padding mode.
      In case the padding mode ends up being ``constant`` or ``linear_ramp``,
      and random intensity is uniformly sampled (once per image) from the
      discrete interval ``[0..255]`` and used as the intensity of the new
      pixels.
  
      >>> aug = iaa.Pad(px=(0, 10), sample_independently=False)
  
      Pad each side by a random pixel value sampled uniformly once per image
      from the discrete interval ``[0..10]``. Each sampled value is used
      for *all* sides of the corresponding image.
  
      >>> aug = iaa.Pad(px=(0, 10), keep_size=False)
  
      Pad each side by a random pixel value sampled uniformly per image and
      side from the discrete interval ``[0..10]``. Afterwards, do **not**
      resize the padded image back to the input image's size. This will increase
      the image's height and width by a maximum of ``20`` pixels.
  
      >>> aug = iaa.Pad(px=((0, 10), (0, 5), (0, 10), (0, 5)))
  
      Pad the top and bottom by a random pixel value sampled uniformly from the
      discrete interval ``[0..10]``. Pad the left and right analogously by
      a random value sampled from ``[0..5]``. Each value is always sampled
      independently.
  
      >>> aug = iaa.Pad(percent=(0, 0.1))
  
      Pad each side by a random fraction sampled uniformly from the continuous
      interval ``[0.0, 0.10]``. The fraction is sampled once per image and
      side. E.g. a sampled fraction of ``0.1`` for the top side would pad by
      ``0.1*H``, where ``H`` is the height of the input image.
  
      >>> aug = iaa.Pad(
      >>>     percent=([0.05, 0.1], [0.05, 0.1], [0.05, 0.1], [0.05, 0.1]))
  
      Pads each side by either ``5%`` or ``10%``. The values are sampled
      once per side and image.
  
      """
  
      def __init__(self, px=None, percent=None, pad_mode="constant", pad_cval=0,
                   keep_size=True, sample_independently=True,
                   seed=None, name=None, **old_kwargs):
          def recursive_validate(value):
              if value is None:
                  return value
              if ia.is_single_number(value):
                  assert value >= 0, "Expected value >0, got %.4f" % (value,)
                  return value
              if isinstance(value, iap.StochasticParameter):
                  return value
              if isinstance(value, tuple):
                  return tuple([recursive_validate(v_) for v_ in value])
              if isinstance(value, list):
                  return [recursive_validate(v_) for v_ in value]
              raise Exception(
                  "Expected None or int or float or StochasticParameter or "
                  "list or tuple, got %s." % (type(value),))
  
          px = recursive_validate(px)
          percent = recursive_validate(percent)
  
          super(Pad, self).__init__(
              px=px,
              percent=percent,
              pad_mode=pad_mode,
              pad_cval=pad_cval,
              keep_size=keep_size,
              sample_independently=sample_independently,
              seed=seed, name=name, **old_kwargs)
  
  
  class Crop(CropAndPad):
      """Crop images, i.e. remove columns/rows of pixels at the sides of images.
  
      This augmenter allows to extract smaller-sized subimages from given
      full-sized input images. The number of pixels to cut off may be defined
      in absolute values or as fractions of the image sizes.
  
      This augmenter will never crop images below a height or width of ``1``.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropAndPad`.
  
      Parameters
      ----------
      px : None or int or imgaug.parameters.StochasticParameter or tuple, optional
          The number of pixels to crop on each side of the image.
          Expected value range is ``[0, inf)``.
          Either this or the parameter `percent` may be set, not both at the same
          time.
  
              * If ``None``, then pixel-based cropping will not be used.
              * If ``int``, then that exact number of pixels will always be
                cropped.
              * If ``StochasticParameter``, then that parameter will be used for
                each image. Four samples will be drawn per image (top, right,
                bottom, left), unless `sample_independently` is set to ``False``,
                as then only one value will be sampled per image and used for
                all sides.
              * If a ``tuple`` of two ``int`` s with values ``a`` and ``b``,
                then each side will be cropped by a random amount sampled
                uniformly per image and side from the inteval ``[a, b]``. If
                however `sample_independently` is set to ``False``, only one
                value will be sampled per image and used for all sides.
              * If a ``tuple`` of four entries, then the entries represent top,
                right, bottom, left. Each entry may be a single ``int`` (always
                crop by exactly that value), a ``tuple`` of two ``int`` s
                ``a`` and ``b`` (crop by an amount within ``[a, b]``), a
                ``list`` of ``int`` s (crop by a random value that is
                contained in the ``list``) or a ``StochasticParameter`` (sample
                the amount to crop from that parameter).
  
      percent : None or int or float or imgaug.parameters.StochasticParameter or tuple, optional
          The number of pixels to crop
          on each side of the image given as a *fraction* of the image
          height/width. E.g. if this is set to ``0.1``, the augmenter will
          always crop ``10%`` of the image's height at both the top and the
          bottom (both ``10%`` each), as well as ``10%`` of the width at the
          right and left.
          Expected value range is ``[0.0, 1.0)``.
          Either this or the parameter `px` may be set, not both
          at the same time.
  
              * If ``None``, then fraction-based cropping will not be
                used.
              * If ``number``, then that fraction will always be cropped.
              * If ``StochasticParameter``, then that parameter will be used for
                each image. Four samples will be drawn per image (top, right,
                bottom, left). If however `sample_independently` is set to
                ``False``, only one value will be sampled per image and used for
                all sides.
              * If a ``tuple`` of two ``float`` s with values ``a`` and ``b``,
                then each side will be cropped by a random fraction
                sampled uniformly per image and side from the interval
                ``[a, b]``. If however `sample_independently` is set to
                ``False``, only one value will be sampled per image and used for
                all sides.
              * If a ``tuple`` of four entries, then the entries represent top,
                right, bottom, left. Each entry may be a single ``float``
                (always crop by exactly that fraction), a ``tuple`` of
                two ``float`` s ``a`` and ``b`` (crop by a fraction from
                ``[a, b]``), a ``list`` of ``float`` s (crop by a random
                value that is contained in the list) or a ``StochasticParameter``
                (sample the percentage to crop from that parameter).
  
      keep_size : bool, optional
          After cropping, the result image will usually have a
          different height/width compared to the original input image. If this
          parameter is set to ``True``, then the cropped image will be
          resized to the input image's size, i.e. the augmenter's output shape
          is always identical to the input shape.
  
      sample_independently : bool, optional
          If ``False`` *and* the values for `px`/`percent` result in exactly
          *one* probability distribution for all image sides, only one single
          value will be sampled from that probability distribution and used for
          all sides. I.e. the crop amount then is the same for all sides.
          If ``True``, four values will be sampled independently, one per side.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.Crop(px=(0, 10))
  
      Crop each side by a random pixel value sampled uniformly per image and
      side from the discrete interval ``[0..10]``.
  
      >>> aug = iaa.Crop(px=(0, 10), sample_independently=False)
  
      Crop each side by a random pixel value sampled uniformly once per image
      from the discrete interval ``[0..10]``. Each sampled value is used
      for *all* sides of the corresponding image.
  
      >>> aug = iaa.Crop(px=(0, 10), keep_size=False)
  
      Crop each side by a random pixel value sampled uniformly per image and
      side from the discrete interval ``[0..10]``. Afterwards, do **not**
      resize the cropped image back to the input image's size. This will decrease
      the image's height and width by a maximum of ``20`` pixels.
  
      >>> aug = iaa.Crop(px=((0, 10), (0, 5), (0, 10), (0, 5)))
  
      Crop the top and bottom by a random pixel value sampled uniformly from the
      discrete interval ``[0..10]``. Crop the left and right analogously by
      a random value sampled from ``[0..5]``. Each value is always sampled
      independently.
  
      >>> aug = iaa.Crop(percent=(0, 0.1))
  
      Crop each side by a random fraction sampled uniformly from the continuous
      interval ``[0.0, 0.10]``. The fraction is sampled once per image and
      side. E.g. a sampled fraction of ``0.1`` for the top side would crop by
      ``0.1*H``, where ``H`` is the height of the input image.
  
      >>> aug = iaa.Crop(
      >>>     percent=([0.05, 0.1], [0.05, 0.1], [0.05, 0.1], [0.05, 0.1]))
  
      Crops each side by either ``5%`` or ``10%``. The values are sampled
      once per side and image.
  
      """
  
      def __init__(self, px=None, percent=None, keep_size=True,
                   sample_independently=True,
                   seed=None, name=None, **old_kwargs):
          def recursive_negate(value):
              if value is None:
                  return value
              if ia.is_single_number(value):
                  assert value >= 0, "Expected value >0, got %.4f." % (value,)
                  return -value
              if isinstance(value, iap.StochasticParameter):
                  return iap.Multiply(value, -1)
              if isinstance(value, tuple):
                  return tuple([recursive_negate(v_) for v_ in value])
              if isinstance(value, list):
                  return [recursive_negate(v_) for v_ in value]
              raise Exception(
                  "Expected None or int or float or StochasticParameter or "
                  "list or tuple, got %s." % (type(value),))
  
          px = recursive_negate(px)
          percent = recursive_negate(percent)
  
          super(Crop, self).__init__(
              px=px,
              percent=percent,
              keep_size=keep_size,
              sample_independently=sample_independently,
              seed=seed, name=name, **old_kwargs)
  
  
  # TODO maybe rename this to PadToMinimumSize?
  # TODO this is very similar to CropAndPad, maybe add a way to generate crop
  #      values imagewise via a callback in in CropAndPad?
  # TODO why is padding mode and cval here called pad_mode, pad_cval but in other
  #      cases mode/cval?
  class PadToFixedSize(meta.Augmenter):
      """Pad images to a predefined minimum width and/or height.
  
      If images are already at the minimum width/height or are larger, they will
      not be padded. Note that this also means that images will not be cropped if
      they exceed the required width/height.
  
      The augmenter randomly decides per image how to distribute the required
      padding amounts over the image axis. E.g. if 2px have to be padded on the
      left or right to reach the required width, the augmenter will sometimes
      add 2px to the left and 0px to the right, sometimes add 2px to the right
      and 0px to the left and sometimes add 1px to both sides. Set `position`
      to ``center`` to prevent that.
  
      Supported dtypes
      ----------------
  
      See :func:`~imgaug.augmenters.size.pad`.
  
      Parameters
      ----------
      width : int or None
          Pad images up to this minimum width.
          If ``None``, image widths will not be altered.
  
      height : int or None
          Pad images up to this minimum height.
          If ``None``, image heights will not be altered.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.CropAndPad.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.CropAndPad.__init__`.
  
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
          Sets the center point of the padding, which determines how the
          required padding amounts are distributed to each side. For a ``tuple``
          ``(a, b)``, both ``a`` and ``b`` are expected to be in range
          ``[0.0, 1.0]`` and describe the fraction of padding applied to the
          left/right (low/high values for ``a``) and the fraction of padding
          applied to the top/bottom (low/high values for ``b``). A padding
          position at ``(0.5, 0.5)`` would be the center of the image and
          distribute the padding equally to all sides. A padding position at
          ``(0.0, 1.0)`` would be the left-bottom and would apply 100% of the
          required padding to the bottom and left sides of the image so that
          the bottom left corner becomes more and more the new image
          center (depending on how much is padded).
  
              * If string ``uniform`` then the share of padding is randomly and
                uniformly distributed over each side.
                Equivalent to ``(Uniform(0.0, 1.0), Uniform(0.0, 1.0))``.
              * If string ``normal`` then the share of padding is distributed
                based on a normal distribution, leading to a focus on the
                center of the images.
                Equivalent to
                ``(Clip(Normal(0.5, 0.45/2), 0, 1),
                Clip(Normal(0.5, 0.45/2), 0, 1))``.
              * If string ``center`` then center point of the padding is
                identical to the image center.
                Equivalent to ``(0.5, 0.5)``.
              * If a string matching regex
                ``^(left|center|right)-(top|center|bottom)$``, e.g. ``left-top``
                or ``center-bottom`` then sets the center point of the padding
                to the X-Y position matching that description.
              * If a tuple of float, then expected to have exactly two entries
                between ``0.0`` and ``1.0``, which will always be used as the
                combination the position matching (x, y) form.
              * If a ``StochasticParameter``, then that parameter will be queried
                once per call to ``augment_*()`` to get ``Nx2`` center positions
                in ``(x, y)`` form (with ``N`` the number of images).
              * If a ``tuple`` of ``StochasticParameter``, then expected to have
                exactly two entries that will both be queried per call to
                ``augment_*()``, each for ``(N,)`` values, to get the center
                positions. First parameter is used for ``x`` coordinates,
                second for ``y`` coordinates.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.PadToFixedSize(width=100, height=100)
  
      For image sides smaller than ``100`` pixels, pad to ``100`` pixels. Do
      nothing for the other edges. The padding is randomly (uniformly)
      distributed over the sides, so that e.g. sometimes most of the required
      padding is applied to the left, sometimes to the right (analogous
      top/bottom).
  
      >>> aug = iaa.PadToFixedSize(width=100, height=100, position="center")
  
      For image sides smaller than ``100`` pixels, pad to ``100`` pixels. Do
      nothing for the other image sides. The padding is always equally
      distributed over the left/right and top/bottom sides.
  
      >>> aug = iaa.PadToFixedSize(width=100, height=100, pad_mode=ia.ALL)
  
      For image sides smaller than ``100`` pixels, pad to ``100`` pixels and
      use any possible padding mode for that. Do nothing for the other image
      sides. The padding is always equally distributed over the left/right and
      top/bottom sides.
  
      >>> aug = iaa.Sequential([
      >>>     iaa.PadToFixedSize(width=100, height=100),
      >>>     iaa.CropToFixedSize(width=100, height=100)
      >>> ])
  
      Pad images smaller than ``100x100`` until they reach ``100x100``.
      Analogously, crop images larger than ``100x100`` until they reach
      ``100x100``. The output images therefore have a fixed size of ``100x100``.
  
      """
  
      def __init__(self, width, height, pad_mode="constant", pad_cval=0,
                   position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(PadToFixedSize, self).__init__(
              seed=seed, name=name, **old_kwargs)
          self.size = (width, height)
  
          # Position of where to pad. The further to the top left this is, the
          # larger the share of pixels that will be added to the top and left
          # sides. I.e. set to (Deterministic(0.0), Deterministic(0.0)) to only
          # add at the top and left, (Deterministic(1.0), Deterministic(1.0))
          # to only add at the bottom right. Analogously (0.5, 0.5) pads equally
          # on both axis, (0.0, 1.0) pads left and bottom, (1.0, 0.0) pads right
          # and top.
          self.position = _handle_position_parameter(position)
  
          self.pad_mode = _handle_pad_mode_param(pad_mode)
          # TODO enable ALL here like in eg Affine
          self.pad_cval = iap.handle_discrete_param(
              pad_cval, "pad_cval", value_range=None, tuple_to_uniform=True,
              list_to_choice=True, allow_floats=True)
  
          # set these to None to use the same values as sampled for the
          # images (not tested)
          self._pad_mode_heatmaps = "constant"
          self._pad_mode_segmentation_maps = "constant"
          self._pad_cval_heatmaps = 0.0
          self._pad_cval_segmentation_maps = 0
  
      def _augment_batch_(self, batch, random_state, parents, hooks):
          # Providing the whole batch to _draw_samples() would not be necessary
          # for this augmenter. The number of rows would be sufficient. This
          # formulation however enables derived augmenters to use rowwise shapes
          # without having to compute them here for this augmenter.
          samples = self._draw_samples(batch, random_state)
  
          if batch.images is not None:
              batch.images = self._augment_images_by_samples(batch.images,
                                                             samples)
  
          if batch.heatmaps is not None:
              batch.heatmaps = self._augment_maps_by_samples(
                  batch.heatmaps, samples, self._pad_mode_heatmaps,
                  self._pad_cval_heatmaps)
  
          if batch.segmentation_maps is not None:
              batch.segmentation_maps = self._augment_maps_by_samples(
                  batch.segmentation_maps, samples, self._pad_mode_heatmaps,
                  self._pad_cval_heatmaps)
  
          for augm_name in ["keypoints", "bounding_boxes", "polygons",
                            "line_strings"]:
              augm_value = getattr(batch, augm_name)
              if augm_value is not None:
                  func = functools.partial(
                      self._augment_keypoints_by_samples,
                      samples=samples)
                  cbaois = self._apply_to_cbaois_as_keypoints(augm_value, func)
                  setattr(batch, augm_name, cbaois)
  
          return batch
  
      def _augment_images_by_samples(self, images, samples):
          result = []
          sizes, pad_xs, pad_ys, pad_modes, pad_cvals = samples
          for i, (image, size) in enumerate(zip(images, sizes)):
              width_min, height_min = size
              height_image, width_image = image.shape[:2]
              paddings = self._calculate_paddings(height_image, width_image,
                                                  height_min, width_min,
                                                  pad_xs[i], pad_ys[i])
  
              image = _crop_and_pad_arr(
                  image, (0, 0, 0, 0), paddings, pad_modes[i], pad_cvals[i],
                  keep_size=False)
  
              result.append(image)
  
          # TODO result is always a list. Should this be converted to an array
          #      if possible (not guaranteed that all images have same size,
          #      some might have been larger than desired height/width)
          return result
  
      def _augment_keypoints_by_samples(self, keypoints_on_images, samples):
          result = []
          sizes, pad_xs, pad_ys, _, _ = samples
          for i, (kpsoi, size) in enumerate(zip(keypoints_on_images, sizes)):
              width_min, height_min = size
              height_image, width_image = kpsoi.shape[:2]
              paddings_img = self._calculate_paddings(height_image, width_image,
                                                      height_min, width_min,
                                                      pad_xs[i], pad_ys[i])
  
              keypoints_padded = _crop_and_pad_kpsoi_(
                  kpsoi, (0, 0, 0, 0), paddings_img,
                  keep_size=False)
  
              result.append(keypoints_padded)
  
          return result
  
      def _augment_maps_by_samples(self, augmentables, samples, pad_mode,
                                   pad_cval):
          sizes, pad_xs, pad_ys, pad_modes, pad_cvals = samples
  
          for i, (augmentable, size) in enumerate(zip(augmentables, sizes)):
              width_min, height_min = size
              height_img, width_img = augmentable.shape[:2]
              paddings_img = self._calculate_paddings(
                  height_img, width_img, height_min, width_min,
                  pad_xs[i], pad_ys[i])
  
              # TODO for the previous method (and likely the new/current one
              #      too):
              #      for 30x30 padded to 32x32 with 15x15 heatmaps this results
              #      in paddings of 1 on each side (assuming
              #      position=(0.5, 0.5)) giving 17x17 heatmaps when they should
              #      be 16x16. Error is due to each side getting projected 0.5
              #      padding which is rounded to 1. This doesn't seem right.
              augmentables[i] = _crop_and_pad_hms_or_segmaps_(
                  augmentables[i],
                  (0, 0, 0, 0),
                  paddings_img,
                  pad_mode=pad_mode if pad_mode is not None else pad_modes[i],
                  pad_cval=pad_cval if pad_cval is not None else pad_cvals[i],
                  keep_size=False)
  
          return augmentables
  
      def _draw_samples(self, batch, random_state):
          nb_images = batch.nb_rows
          rngs = random_state.duplicate(4)
  
          if isinstance(self.position, tuple):
              pad_xs = self.position[0].draw_samples(nb_images,
                                                     random_state=rngs[0])
              pad_ys = self.position[1].draw_samples(nb_images,
                                                     random_state=rngs[1])
          else:
              pads = self.position.draw_samples((nb_images, 2),
                                                random_state=rngs[0])
              pad_xs = pads[:, 0]
              pad_ys = pads[:, 1]
  
          pad_modes = self.pad_mode.draw_samples(nb_images,
                                                 random_state=rngs[2])
          pad_cvals = self.pad_cval.draw_samples(nb_images,
                                                 random_state=rngs[3])
  
          # We return here the sizes even though they are static as it allows
          # derived augmenters to define image-specific heights/widths.
          return [self.size] * nb_images, pad_xs, pad_ys, pad_modes, pad_cvals
  
      @classmethod
      def _calculate_paddings(cls, height_image, width_image,
                              height_min, width_min, pad_xs_i, pad_ys_i):
          pad_top = 0
          pad_right = 0
          pad_bottom = 0
          pad_left = 0
  
          if width_min is not None and width_image < width_min:
              pad_total_x = width_min - width_image
              pad_left = int((1-pad_xs_i) * pad_total_x)
              pad_right = pad_total_x - pad_left
  
          if height_min is not None and height_image < height_min:
              pad_total_y = height_min - height_image
              pad_top = int((1-pad_ys_i) * pad_total_y)
              pad_bottom = pad_total_y - pad_top
  
          return pad_top, pad_right, pad_bottom, pad_left
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.size[0], self.size[1], self.pad_mode, self.pad_cval,
                  self.position]
  
  
  class CenterPadToFixedSize(PadToFixedSize):
      """Pad images equally on all sides up to given minimum heights/widths.
  
      This is an alias for :class:`~imgaug.augmenters.size.PadToFixedSize`
      with ``position="center"``. It spreads the pad amounts equally over
      all image sides, while :class:`~imgaug.augmenters.size.PadToFixedSize`
      by defaults spreads them randomly.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.PadToFixedSize`.
  
      Parameters
      ----------
      width : int or None
          See :func:`PadToFixedSize.__init__`.
  
      height : int or None
          See :func:`PadToFixedSize.__init__`.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`PadToFixedSize.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`PadToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CenterPadToFixedSize(height=20, width=30)
  
      Create an augmenter that pads images up to ``20x30``, with the padded
      rows added *equally* on the top and bottom (analogous for the padded
      columns).
  
      """
  
      def __init__(self, width, height, pad_mode="constant", pad_cval=0,
                   seed=None, name=None, **old_kwargs):
          super(CenterPadToFixedSize, self).__init__(
              width=width, height=height, pad_mode=pad_mode, pad_cval=pad_cval,
              position="center",
              seed=seed, name=name, **old_kwargs)
  
  
  # TODO maybe rename this to CropToMaximumSize ?
  # TODO this is very similar to CropAndPad, maybe add a way to generate crop
  #      values imagewise via a callback in in CropAndPad?
  # TODO add crop() function in imgaug, similar to pad
  class CropToFixedSize(meta.Augmenter):
      """Crop images down to a predefined maximum width and/or height.
  
      If images are already at the maximum width/height or are smaller, they
      will not be cropped. Note that this also means that images will not be
      padded if they are below the required width/height.
  
      The augmenter randomly decides per image how to distribute the required
      cropping amounts over the image axis. E.g. if 2px have to be cropped on
      the left or right to reach the required width, the augmenter will
      sometimes remove 2px from the left and 0px from the right, sometimes
      remove 2px from the right and 0px from the left and sometimes remove 1px
      from both sides. Set `position` to ``center`` to prevent that.
  
      Supported dtypes
      ----------------
  
          * ``uint8``: yes; fully tested
          * ``uint16``: yes; tested
          * ``uint32``: yes; tested
          * ``uint64``: yes; tested
          * ``int8``: yes; tested
          * ``int16``: yes; tested
          * ``int32``: yes; tested
          * ``int64``: yes; tested
          * ``float16``: yes; tested
          * ``float32``: yes; tested
          * ``float64``: yes; tested
          * ``float128``: yes; tested
          * ``bool``: yes; tested
  
      Parameters
      ----------
      width : int or None
          Crop images down to this maximum width.
          If ``None``, image widths will not be altered.
  
      height : int or None
          Crop images down to this maximum height.
          If ``None``, image heights will not be altered.
  
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
           Sets the center point of the cropping, which determines how the
           required cropping amounts are distributed to each side. For a
           ``tuple`` ``(a, b)``, both ``a`` and ``b`` are expected to be in
           range ``[0.0, 1.0]`` and describe the fraction of cropping applied
           to the left/right (low/high values for ``a``) and the fraction
           of cropping applied to the top/bottom (low/high values for ``b``).
           A cropping position at ``(0.5, 0.5)`` would be the center of the
           image and distribute the cropping equally over all sides. A cropping
           position at ``(1.0, 0.0)`` would be the right-top and would apply
           100% of the required cropping to the right and top sides of the image.
  
              * If string ``uniform`` then the share of cropping is randomly
                and uniformly distributed over each side.
                Equivalent to ``(Uniform(0.0, 1.0), Uniform(0.0, 1.0))``.
              * If string ``normal`` then the share of cropping is distributed
                based on a normal distribution, leading to a focus on the center
                of the images.
                Equivalent to
                ``(Clip(Normal(0.5, 0.45/2), 0, 1),
                Clip(Normal(0.5, 0.45/2), 0, 1))``.
              * If string ``center`` then center point of the cropping is
                identical to the image center.
                Equivalent to ``(0.5, 0.5)``.
              * If a string matching regex
                ``^(left|center|right)-(top|center|bottom)$``, e.g.
                ``left-top`` or ``center-bottom`` then sets the center point of
                the cropping to the X-Y position matching that description.
              * If a tuple of float, then expected to have exactly two entries
                between ``0.0`` and ``1.0``, which will always be used as the
                combination the position matching (x, y) form.
              * If a ``StochasticParameter``, then that parameter will be queried
                once per call to ``augment_*()`` to get ``Nx2`` center positions
                in ``(x, y)`` form (with ``N`` the number of images).
              * If a ``tuple`` of ``StochasticParameter``, then expected to have
                exactly two entries that will both be queried per call to
                ``augment_*()``, each for ``(N,)`` values, to get the center
                positions. First parameter is used for ``x`` coordinates,
                second for ``y`` coordinates.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CropToFixedSize(width=100, height=100)
  
      For image sides larger than ``100`` pixels, crop to ``100`` pixels. Do
      nothing for the other sides. The cropping amounts are randomly (and
      uniformly) distributed over the sides of the image.
  
      >>> aug = iaa.CropToFixedSize(width=100, height=100, position="center")
  
      For sides larger than ``100`` pixels, crop to ``100`` pixels. Do nothing
      for the other sides. The cropping amounts are always equally distributed
      over the left/right sides of the image (and analogously for top/bottom).
  
      >>> aug = iaa.Sequential([
      >>>     iaa.PadToFixedSize(width=100, height=100),
      >>>     iaa.CropToFixedSize(width=100, height=100)
      >>> ])
  
      Pad images smaller than ``100x100`` until they reach ``100x100``.
      Analogously, crop images larger than ``100x100`` until they reach
      ``100x100``. The output images therefore have a fixed size of ``100x100``.
  
      """
  
      def __init__(self, width, height, position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(CropToFixedSize, self).__init__(
              seed=seed, name=name, **old_kwargs)
          self.size = (width, height)
  
          # Position of where to crop. The further to the top left this is,
          # the larger the share of pixels that will be cropped from the top
          # and left sides. I.e. set to (Deterministic(0.0), Deterministic(0.0))
          # to only crop at the top and left,
          # (Deterministic(1.0), Deterministic(1.0)) to only crop at the bottom
          # right. Analogously (0.5, 0.5) crops equally on both axis,
          # (0.0, 1.0) crops left and bottom, (1.0, 0.0) crops right and top.
          self.position = _handle_position_parameter(position)
  
      def _augment_batch_(self, batch, random_state, parents, hooks):
          # Providing the whole batch to _draw_samples() would not be necessary
          # for this augmenter. The number of rows would be sufficient. This
          # formulation however enables derived augmenters to use rowwise shapes
          # without having to compute them here for this augmenter.
          samples = self._draw_samples(batch, random_state)
  
          if batch.images is not None:
              batch.images = self._augment_images_by_samples(batch.images,
                                                             samples)
  
          if batch.heatmaps is not None:
              batch.heatmaps = self._augment_maps_by_samples(
                  batch.heatmaps, samples)
  
          if batch.segmentation_maps is not None:
              batch.segmentation_maps = self._augment_maps_by_samples(
                  batch.segmentation_maps, samples)
  
          for augm_name in ["keypoints", "bounding_boxes", "polygons",
                            "line_strings"]:
              augm_value = getattr(batch, augm_name)
              if augm_value is not None:
                  func = functools.partial(
                      self._augment_keypoints_by_samples,
                      samples=samples)
                  cbaois = self._apply_to_cbaois_as_keypoints(augm_value, func)
                  setattr(batch, augm_name, cbaois)
  
          return batch
  
      def _augment_images_by_samples(self, images, samples):
          result = []
          sizes, offset_xs, offset_ys = samples
          for i, (image, size) in enumerate(zip(images, sizes)):
              w, h = size
              height_image, width_image = image.shape[0:2]
  
              croppings = self._calculate_crop_amounts(
                  height_image, width_image, h, w, offset_ys[i], offset_xs[i])
  
              image_cropped = _crop_and_pad_arr(image, croppings, (0, 0, 0, 0),
                                                keep_size=False)
  
              result.append(image_cropped)
  
          return result
  
      def _augment_keypoints_by_samples(self, kpsois, samples):
          result = []
          sizes, offset_xs, offset_ys = samples
          for i, (kpsoi, size) in enumerate(zip(kpsois, sizes)):
              w, h = size
              height_image, width_image = kpsoi.shape[0:2]
  
              croppings_img = self._calculate_crop_amounts(
                  height_image, width_image, h, w, offset_ys[i], offset_xs[i])
  
              kpsoi_cropped = _crop_and_pad_kpsoi_(
                  kpsoi, croppings_img, (0, 0, 0, 0), keep_size=False)
  
              result.append(kpsoi_cropped)
  
          return result
  
      def _augment_maps_by_samples(self, augmentables, samples):
          sizes, offset_xs, offset_ys = samples
          for i, (augmentable, size) in enumerate(zip(augmentables, sizes)):
              w, h = size
              height_image, width_image = augmentable.shape[0:2]
  
              croppings_img = self._calculate_crop_amounts(
                  height_image, width_image, h, w, offset_ys[i], offset_xs[i])
  
              augmentables[i] = _crop_and_pad_hms_or_segmaps_(
                  augmentable, croppings_img, (0, 0, 0, 0), keep_size=False)
  
          return augmentables
  
      @classmethod
      def _calculate_crop_amounts(cls, height_image, width_image,
                                  height_max, width_max,
                                  offset_y, offset_x):
          crop_top = 0
          crop_right = 0
          crop_bottom = 0
          crop_left = 0
  
          if height_max is not None and height_image > height_max:
              crop_top = int(offset_y * (height_image - height_max))
              crop_bottom = height_image - height_max - crop_top
  
          if width_max is not None and width_image > width_max:
              crop_left = int(offset_x * (width_image - width_max))
              crop_right = width_image - width_max - crop_left
  
          return crop_top, crop_right, crop_bottom, crop_left
  
      def _draw_samples(self, batch, random_state):
          nb_images = batch.nb_rows
          rngs = random_state.duplicate(2)
  
          if isinstance(self.position, tuple):
              offset_xs = self.position[0].draw_samples(nb_images,
                                                        random_state=rngs[0])
              offset_ys = self.position[1].draw_samples(nb_images,
                                                        random_state=rngs[1])
          else:
              offsets = self.position.draw_samples((nb_images, 2),
                                                   random_state=rngs[0])
              offset_xs = offsets[:, 0]
              offset_ys = offsets[:, 1]
  
          offset_xs = 1.0 - offset_xs
          offset_ys = 1.0 - offset_ys
  
          # We return here the sizes even though they are static as it allows
          # derived augmenters to define image-specific heights/widths.
          return [self.size] * nb_images, offset_xs, offset_ys
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.size[0], self.size[1], self.position]
  
  
  class CenterCropToFixedSize(CropToFixedSize):
      """Take a crop from the center of each image.
  
      This is an alias for :class:`~imgaug.augmenters.size.CropToFixedSize` with
      ``position="center"``.
  
      .. note::
  
          If images already have a width and/or height below the provided
          width and/or height then this augmenter will do nothing for the
          respective axis. Hence, resulting images can be smaller than the
          provided axis sizes.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropToFixedSize`.
  
      Parameters
      ----------
      width : int or None
          See :func:`CropToFixedSize.__init__`.
  
      height : int or None
          See :func:`CropToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> crop = iaa.CenterCropToFixedSize(height=20, width=10)
  
      Create an augmenter that takes ``20x10`` sized crops from the center of
      images.
  
      """
  
      def __init__(self, width, height,
                   seed=None, name=None, **old_kwargs):
          super(CenterCropToFixedSize, self).__init__(
              width=width, height=height, position="center",
              seed=seed, name=name, **old_kwargs)
  
  
  class CropToMultiplesOf(CropToFixedSize):
      """Crop images down until their height/width is a multiple of a value.
  
      .. note::
  
          For a given axis size ``A`` and multiple ``M``, if ``A`` is in the
          interval ``[0 .. M]``, the axis will not be changed.
          As a result, this augmenter can still produce axis sizes that are
          not multiples of the given values.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropToFixedSize`.
  
      Parameters
      ----------
      width_multiple : int or None
          Multiple for the width. Images will be cropped down until their
          width is a multiple of this value.
          If ``None``, image widths will not be altered.
  
      height_multiple : int or None
          Multiple for the height. Images will be cropped down until their
          height is a multiple of this value.
          If ``None``, image heights will not be altered.
  
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
          See :func:`CropToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CropToMultiplesOf(height_multiple=10, width_multiple=6)
  
      Create an augmenter that crops images to multiples of ``10`` along
      the y-axis (i.e. 10, 20, 30, ...) and to multiples of ``6`` along the
      x-axis (i.e. 6, 12, 18, ...).
      The rows to be cropped will be spread *randomly* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, width_multiple, height_multiple, position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(CropToMultiplesOf, self).__init__(
              width=None, height=None, position=position,
              seed=seed, name=name, **old_kwargs)
          self.width_multiple = width_multiple
          self.height_multiple = height_multiple
  
      def _draw_samples(self, batch, random_state):
          _sizes, offset_xs, offset_ys = super(
              CropToMultiplesOf, self
          )._draw_samples(batch, random_state)
  
          shapes = batch.get_rowwise_shapes()
          sizes = []
          for shape in shapes:
              height, width = shape[0:2]
              croppings = compute_croppings_to_reach_multiples_of(
                  shape,
                  height_multiple=self.height_multiple,
                  width_multiple=self.width_multiple)
  
              # TODO change that
              # note that these are not in the same order as shape tuples
              # in CropToFixedSize
              new_size = (
                  width - croppings[1] - croppings[3],
                  height - croppings[0] - croppings[2]
              )
              sizes.append(new_size)
  
          return sizes, offset_xs, offset_ys
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.width_multiple, self.height_multiple, self.position]
  
  
  class CenterCropToMultiplesOf(CropToMultiplesOf):
      """Crop images equally on all sides until H/W are multiples of given values.
  
      This is the same as :class:`~imgaug.augmenters.size.CropToMultiplesOf`,
      but uses ``position="center"`` by default, which spreads the crop amounts
      equally over all image sides, while
      :class:`~imgaug.augmenters.size.CropToMultiplesOf` by default spreads
      them randomly.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropToFixedSize`.
  
      Parameters
      ----------
      width_multiple : int or None
          See :func:`CropToMultiplesOf.__init__`.
  
      height_multiple : int or None
          See :func:`CropToMultiplesOf.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CenterCropToMultiplesOf(height_multiple=10, width_multiple=6)
  
      Create an augmenter that crops images to multiples of ``10`` along
      the y-axis (i.e. 10, 20, 30, ...) and to multiples of ``6`` along the
      x-axis (i.e. 6, 12, 18, ...).
      The rows to be cropped will be spread *equally* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, width_multiple, height_multiple,
                   seed=None, name=None, **old_kwargs):
          super(CenterCropToMultiplesOf, self).__init__(
              width_multiple=width_multiple,
              height_multiple=height_multiple,
              position="center",
              seed=seed, name=name, **old_kwargs)
  
  
  class PadToMultiplesOf(PadToFixedSize):
      """Pad images until their height/width is a multiple of a value.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.PadToFixedSize`.
  
      Parameters
      ----------
      width_multiple : int or None
          Multiple for the width. Images will be padded until their
          width is a multiple of this value.
          If ``None``, image widths will not be altered.
  
      height_multiple : int or None
          Multiple for the height. Images will be padded until their
          height is a multiple of this value.
          If ``None``, image heights will not be altered.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToFixedSize.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToFixedSize.__init__`.
  
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
          See :func:`PadToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.PadToMultiplesOf(height_multiple=10, width_multiple=6)
  
      Create an augmenter that pads images to multiples of ``10`` along
      the y-axis (i.e. 10, 20, 30, ...) and to multiples of ``6`` along the
      x-axis (i.e. 6, 12, 18, ...).
      The rows to be padded will be spread *randomly* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, width_multiple, height_multiple,
                   pad_mode="constant", pad_cval=0,
                   position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(PadToMultiplesOf, self).__init__(
              width=None, height=None, pad_mode=pad_mode, pad_cval=pad_cval,
              position=position,
              seed=seed, name=name, **old_kwargs)
          self.width_multiple = width_multiple
          self.height_multiple = height_multiple
  
      def _draw_samples(self, batch, random_state):
          _sizes, pad_xs, pad_ys, pad_modes, pad_cvals = super(
              PadToMultiplesOf, self
          )._draw_samples(batch, random_state)
  
          shapes = batch.get_rowwise_shapes()
          sizes = []
          for shape in shapes:
              height, width = shape[0:2]
              paddings = compute_paddings_to_reach_multiples_of(
                  shape,
                  height_multiple=self.height_multiple,
                  width_multiple=self.width_multiple)
  
              # TODO change that
              # note that these are not in the same order as shape tuples
              # in PadToFixedSize
              new_size = (
                  width + paddings[1] + paddings[3],
                  height + paddings[0] + paddings[2]
              )
              sizes.append(new_size)
  
          return sizes, pad_xs, pad_ys, pad_modes, pad_cvals
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.width_multiple, self.height_multiple,
                  self.pad_mode, self.pad_cval,
                  self.position]
  
  
  class CenterPadToMultiplesOf(PadToMultiplesOf):
      """Pad images equally on all sides until H/W are multiples of given values.
  
      This is the same as :class:`~imgaug.augmenters.size.PadToMultiplesOf`, but
      uses ``position="center"`` by default, which spreads the pad amounts
      equally over all image sides, while
      :class:`~imgaug.augmenters.size.PadToMultiplesOf` by default spreads them
      randomly.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.PadToFixedSize`.
  
      Parameters
      ----------
      width_multiple : int or None
          See :func:`PadToMultiplesOf.__init__`.
  
      height_multiple : int or None
          See :func:`PadToMultiplesOf.__init__`.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToMultiplesOf.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToMultiplesOf.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CenterPadToMultiplesOf(height_multiple=10, width_multiple=6)
  
      Create an augmenter that pads images to multiples of ``10`` along
      the y-axis (i.e. 10, 20, 30, ...) and to multiples of ``6`` along the
      x-axis (i.e. 6, 12, 18, ...).
      The rows to be padded will be spread *equally* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, width_multiple, height_multiple,
                   pad_mode="constant", pad_cval=0,
                   seed=None, name=None, **old_kwargs):
          super(CenterPadToMultiplesOf, self).__init__(
              width_multiple=width_multiple,
              height_multiple=height_multiple,
              pad_mode=pad_mode,
              pad_cval=pad_cval,
              position="center",
              seed=seed, name=name, **old_kwargs)
  
  
  class CropToPowersOf(CropToFixedSize):
      """Crop images until their height/width is a power of a base.
  
      This augmenter removes pixels from an axis with size ``S`` leading to the
      new size ``S'`` until ``S' = B^E`` is fulfilled, where ``B`` is a
      provided base (e.g. ``2``) and ``E`` is an exponent from the discrete
      interval ``[1 .. inf)``.
  
      .. note::
  
          This augmenter does nothing for axes with size less than ``B^1 = B``.
          If you have images with ``S < B^1``, it is recommended
          to combine this augmenter with a padding augmenter that pads each
          axis up to ``B``.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropToFixedSize`.
  
      Parameters
      ----------
      width_base : int or None
          Base for the width. Images will be cropped down until their
          width fulfills ``width' = width_base ^ E`` with ``E`` being any
          natural number.
          If ``None``, image widths will not be altered.
  
      height_base : int or None
          Base for the height. Images will be cropped down until their
          height fulfills ``height' = height_base ^ E`` with ``E`` being any
          natural number.
          If ``None``, image heights will not be altered.
  
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
          See :func:`CropToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CropToPowersOf(height_base=3, width_base=2)
  
      Create an augmenter that crops each image down to powers of ``3`` along
      the y-axis (i.e. 3, 9, 27, ...) and powers of ``2`` along the x-axis (i.e.
      2, 4, 8, 16, ...).
      The rows to be cropped will be spread *randomly* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, width_base, height_base, position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(CropToPowersOf, self).__init__(
              width=None, height=None, position=position,
              seed=seed, name=name, **old_kwargs)
          self.width_base = width_base
          self.height_base = height_base
  
      def _draw_samples(self, batch, random_state):
          _sizes, offset_xs, offset_ys = super(
              CropToPowersOf, self
          )._draw_samples(batch, random_state)
  
          shapes = batch.get_rowwise_shapes()
          sizes = []
          for shape in shapes:
              height, width = shape[0:2]
              croppings = compute_croppings_to_reach_powers_of(
                  shape,
                  height_base=self.height_base,
                  width_base=self.width_base)
  
              # TODO change that
              # note that these are not in the same order as shape tuples
              # in CropToFixedSize
              new_size = (
                  width - croppings[1] - croppings[3],
                  height - croppings[0] - croppings[2]
              )
              sizes.append(new_size)
  
          return sizes, offset_xs, offset_ys
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.width_base, self.height_base, self.position]
  
  
  class CenterCropToPowersOf(CropToPowersOf):
      """Crop images equally on all sides until H/W is a power of a base.
  
      This is the same as :class:`~imgaug.augmenters.size.CropToPowersOf`, but
      uses ``position="center"`` by default, which spreads the crop amounts
      equally over all image sides, while
      :class:`~imgaug.augmenters.size.CropToPowersOf` by default spreads them
      randomly.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropToFixedSize`.
  
      Parameters
      ----------
      width_base : int or None
          See :func:`CropToPowersOf.__init__`.
  
      height_base : int or None
          See :func:`CropToPowersOf.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CropToPowersOf(height_base=3, width_base=2)
  
      Create an augmenter that crops each image down to powers of ``3`` along
      the y-axis (i.e. 3, 9, 27, ...) and powers of ``2`` along the x-axis (i.e.
      2, 4, 8, 16, ...).
      The rows to be cropped will be spread *equally* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, width_base, height_base,
                   seed=None, name=None, **old_kwargs):
          super(CenterCropToPowersOf, self).__init__(
              width_base=width_base, height_base=height_base, position="center",
              seed=seed, name=name, **old_kwargs)
  
  
  class PadToPowersOf(PadToFixedSize):
      """Pad images until their height/width is a power of a base.
  
      This augmenter adds pixels to an axis with size ``S`` leading to the
      new size ``S'`` until ``S' = B^E`` is fulfilled, where ``B`` is a
      provided base (e.g. ``2``) and ``E`` is an exponent from the discrete
      interval ``[1 .. inf)``.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.PadToFixedSize`.
  
      Parameters
      ----------
      width_base : int or None
          Base for the width. Images will be padded down until their
          width fulfills ``width' = width_base ^ E`` with ``E`` being any
          natural number.
          If ``None``, image widths will not be altered.
  
      height_base : int or None
          Base for the height. Images will be padded until their
          height fulfills ``height' = height_base ^ E`` with ``E`` being any
          natural number.
          If ``None``, image heights will not be altered.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToFixedSize.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToFixedSize.__init__`.
  
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
          See :func:`PadToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.PadToPowersOf(height_base=3, width_base=2)
  
      Create an augmenter that pads each image to powers of ``3`` along the
      y-axis (i.e. 3, 9, 27, ...) and powers of ``2`` along the x-axis (i.e. 2,
      4, 8, 16, ...).
      The rows to be padded will be spread *randomly* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, width_base, height_base,
                   pad_mode="constant", pad_cval=0,
                   position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(PadToPowersOf, self).__init__(
              width=None, height=None, pad_mode=pad_mode, pad_cval=pad_cval,
              position=position,
              seed=seed, name=name, **old_kwargs)
          self.width_base = width_base
          self.height_base = height_base
  
      def _draw_samples(self, batch, random_state):
          _sizes, pad_xs, pad_ys, pad_modes, pad_cvals = super(
              PadToPowersOf, self
          )._draw_samples(batch, random_state)
  
          shapes = batch.get_rowwise_shapes()
          sizes = []
          for shape in shapes:
              height, width = shape[0:2]
              paddings = compute_paddings_to_reach_powers_of(
                  shape,
                  height_base=self.height_base,
                  width_base=self.width_base)
  
              # TODO change that
              # note that these are not in the same order as shape tuples
              # in PadToFixedSize
              new_size = (
                  width + paddings[1] + paddings[3],
                  height + paddings[0] + paddings[2]
              )
              sizes.append(new_size)
  
          return sizes, pad_xs, pad_ys, pad_modes, pad_cvals
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.width_base, self.height_base,
                  self.pad_mode, self.pad_cval,
                  self.position]
  
  
  class CenterPadToPowersOf(PadToPowersOf):
      """Pad images equally on all sides until H/W is a power of a base.
  
      This is the same as :class:`~imgaug.augmenters.size.PadToPowersOf`, but uses
      ``position="center"`` by default, which spreads the pad amounts equally
      over all image sides, while :class:`~imgaug.augmenters.size.PadToPowersOf`
      by default spreads them randomly.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.PadToFixedSize`.
  
      Parameters
      ----------
      width_base : int or None
          See :func:`PadToPowersOf.__init__`.
  
      height_base : int or None
          See :func:`PadToPowersOf.__init__`.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToPowersOf.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToPowersOf.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CenterPadToPowersOf(height_base=5, width_base=2)
  
      Create an augmenter that pads each image to powers of ``3`` along the
      y-axis (i.e. 3, 9, 27, ...) and powers of ``2`` along the x-axis (i.e. 2,
      4, 8, 16, ...).
      The rows to be padded will be spread *equally* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, width_base, height_base,
                   pad_mode="constant", pad_cval=0,
                   seed=None, name=None, **old_kwargs):
          super(CenterPadToPowersOf, self).__init__(
              width_base=width_base, height_base=height_base,
              pad_mode=pad_mode, pad_cval=pad_cval,
              position="center",
              seed=seed, name=name, **old_kwargs)
  
  
  class CropToAspectRatio(CropToFixedSize):
      """Crop images until their width/height matches an aspect ratio.
  
      This augmenter removes either rows or columns until the image reaches
      the desired aspect ratio given in ``width / height``. The cropping
      operation is stopped once the desired aspect ratio is reached or the image
      side to crop reaches a size of ``1``. If any side of the image starts
      with a size of ``0``, the image will not be changed.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropToFixedSize`.
  
      Parameters
      ----------
      aspect_ratio : number
          The desired aspect ratio, given as ``width/height``. E.g. a ratio
          of ``2.0`` denotes an image that is twice as wide as it is high.
  
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
          See :func:`CropToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CropToAspectRatio(2.0)
  
      Create an augmenter that crops each image until its aspect ratio is as
      close as possible to ``2.0`` (i.e. two times as many pixels along the
      x-axis than the y-axis).
      The rows to be cropped will be spread *randomly* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, aspect_ratio, position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(CropToAspectRatio, self).__init__(
              width=None, height=None, position=position,
              seed=seed, name=name, **old_kwargs)
          self.aspect_ratio = aspect_ratio
  
      def _draw_samples(self, batch, random_state):
          _sizes, offset_xs, offset_ys = super(
              CropToAspectRatio, self
          )._draw_samples(batch, random_state)
  
          shapes = batch.get_rowwise_shapes()
          sizes = []
          for shape in shapes:
              height, width = shape[0:2]
  
              if height == 0 or width == 0:
                  croppings = (0, 0, 0, 0)
              else:
                  croppings = compute_croppings_to_reach_aspect_ratio(
                      shape,
                      aspect_ratio=self.aspect_ratio)
  
              # TODO change that
              # note that these are not in the same order as shape tuples
              # in CropToFixedSize
              new_size = (
                  width - croppings[1] - croppings[3],
                  height - croppings[0] - croppings[2]
              )
              sizes.append(new_size)
  
          return sizes, offset_xs, offset_ys
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.aspect_ratio, self.position]
  
  
  class CenterCropToAspectRatio(CropToAspectRatio):
      """Crop images equally on all sides until they reach an aspect ratio.
  
      This is the same as :class:`~imgaug.augmenters.size.CropToAspectRatio`, but
      uses ``position="center"`` by default, which spreads the crop amounts
      equally over all image sides, while
      :class:`~imgaug.augmenters.size.CropToAspectRatio` by default spreads
      them randomly.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropToFixedSize`.
  
      Parameters
      ----------
      aspect_ratio : number
          See :func:`CropToAspectRatio.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CenterCropToAspectRatio(2.0)
  
      Create an augmenter that crops each image until its aspect ratio is as
      close as possible to ``2.0`` (i.e. two times as many pixels along the
      x-axis than the y-axis).
      The rows to be cropped will be spread *equally* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, aspect_ratio,
                   seed=None, name=None, **old_kwargs):
          super(CenterCropToAspectRatio, self).__init__(
              aspect_ratio=aspect_ratio, position="center",
              seed=seed, name=name, **old_kwargs)
  
  
  class PadToAspectRatio(PadToFixedSize):
      """Pad images until their width/height matches an aspect ratio.
  
      This augmenter adds either rows or columns until the image reaches
      the desired aspect ratio given in ``width / height``.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.PadToFixedSize`.
  
      Parameters
      ----------
      aspect_ratio : number
          The desired aspect ratio, given as ``width/height``. E.g. a ratio
          of ``2.0`` denotes an image that is twice as wide as it is high.
  
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
          See :func:`PadToFixedSize.__init__`.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToFixedSize.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.PadToAspectRatio(2.0)
  
      Create an augmenter that pads each image until its aspect ratio is as
      close as possible to ``2.0`` (i.e. two times as many pixels along the
      x-axis than the y-axis).
      The rows to be padded will be spread *randomly* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, aspect_ratio, pad_mode="constant", pad_cval=0,
                   position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(PadToAspectRatio, self).__init__(
              width=None, height=None, pad_mode=pad_mode, pad_cval=pad_cval,
              position=position,
              seed=seed, name=name, **old_kwargs)
          self.aspect_ratio = aspect_ratio
  
      def _draw_samples(self, batch, random_state):
          _sizes, pad_xs, pad_ys, pad_modes, pad_cvals = super(
              PadToAspectRatio, self
          )._draw_samples(batch, random_state)
  
          shapes = batch.get_rowwise_shapes()
          sizes = []
          for shape in shapes:
              height, width = shape[0:2]
  
              paddings = compute_paddings_to_reach_aspect_ratio(
                  shape,
                  aspect_ratio=self.aspect_ratio)
  
              # TODO change that
              # note that these are not in the same order as shape tuples
              # in PadToFixedSize
              new_size = (
                  width + paddings[1] + paddings[3],
                  height + paddings[0] + paddings[2]
              )
              sizes.append(new_size)
  
          return sizes, pad_xs, pad_ys, pad_modes, pad_cvals
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.aspect_ratio, self.pad_mode, self.pad_cval,
                  self.position]
  
  
  class CenterPadToAspectRatio(PadToAspectRatio):
      """Pad images equally on all sides until H/W matches an aspect ratio.
  
      This is the same as :class:`~imgaug.augmenters.size.PadToAspectRatio`, but
      uses ``position="center"`` by default, which spreads the pad amounts
      equally over all image sides, while
      :class:`~imgaug.augmenters.size.PadToAspectRatio` by default spreads them
      randomly.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.PadToFixedSize`.
  
      Parameters
      ----------
      aspect_ratio : number
          See :func:`PadToAspectRatio.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToAspectRatio.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToAspectRatio.__init__`.
  
      deterministic : bool, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      random_state : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.PadToAspectRatio(2.0)
  
      Create am augmenter that pads each image until its aspect ratio is as
      close as possible to ``2.0`` (i.e. two times as many pixels along the
      x-axis than the y-axis).
      The rows to be padded will be spread *equally* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, aspect_ratio, pad_mode="constant", pad_cval=0,
                   seed=None, name=None, **old_kwargs):
          super(CenterPadToAspectRatio, self).__init__(
              aspect_ratio=aspect_ratio, position="center",
              pad_mode=pad_mode, pad_cval=pad_cval,
              seed=seed, name=name, **old_kwargs)
  
  
  class CropToSquare(CropToAspectRatio):
      """Crop images until their width and height are identical.
  
      This is identical to :class:`~imgaug.augmenters.size.CropToAspectRatio`
      with ``aspect_ratio=1.0``.
  
      Images with axis sizes of ``0`` will not be altered.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropToFixedSize`.
  
      Parameters
      ----------
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
          See :func:`CropToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CropToSquare()
  
      Create an augmenter that crops each image until its square, i.e. height
      and width match.
      The rows to be cropped will be spread *randomly* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(CropToSquare, self).__init__(
              aspect_ratio=1.0, position=position,
              seed=seed, name=name, **old_kwargs)
  
  
  class CenterCropToSquare(CropToSquare):
      """Crop images equally on all sides until their height/width are identical.
  
      In contrast to :class:`~imgaug.augmenters.size.CropToSquare`, this
      augmenter always tries to spread the columns/rows to remove equally over
      both sides of the respective axis to be cropped.
      :class:`~imgaug.augmenters.size.CropToAspectRatio` by default spreads the
      croppings randomly.
  
      This augmenter is identical to :class:`~imgaug.augmenters.size.CropToSquare`
      with ``position="center"``, and thereby the same as
      :class:`~imgaug.augmenters.size.CropToAspectRatio` with
      ``aspect_ratio=1.0, position="center"``.
  
      Images with axis sizes of ``0`` will not be altered.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.CropToFixedSize`.
  
      Parameters
      ----------
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CenterCropToSquare()
  
      Create an augmenter that crops each image until its square, i.e. height
      and width match.
      The rows to be cropped will be spread *equally* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, seed=None, name=None, **old_kwargs):
          super(CenterCropToSquare, self).__init__(
              position="center",
              seed=seed, name=name, **old_kwargs)
  
  
  class PadToSquare(PadToAspectRatio):
      """Pad images until their height and width are identical.
  
      This augmenter is identical to
      :class:`~imgaug.augmenters.size.PadToAspectRatio` with ``aspect_ratio=1.0``.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.PadToFixedSize`.
  
      Parameters
      ----------
      position : {'uniform', 'normal', 'center', 'left-top', 'left-center', 'left-bottom', 'center-top', 'center-center', 'center-bottom', 'right-top', 'right-center', 'right-bottom'} or tuple of float or StochasticParameter or tuple of StochasticParameter, optional
          See :func:`PadToFixedSize.__init__`.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToFixedSize.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToFixedSize.__init__`.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.PadToSquare()
  
      Create an augmenter that pads each image until its square, i.e. height
      and width match.
      The rows to be padded will be spread *randomly* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, pad_mode="constant", pad_cval=0, position="uniform",
                   seed=None, name=None, **old_kwargs):
          super(PadToSquare, self).__init__(
              aspect_ratio=1.0, pad_mode=pad_mode, pad_cval=pad_cval,
              position=position,
              seed=seed, name=name, **old_kwargs)
  
  
  class CenterPadToSquare(PadToSquare):
      """Pad images equally on all sides until their height & width are identical.
  
      This is the same as :class:`~imgaug.augmenters.size.PadToSquare`, but uses
      ``position="center"`` by default, which spreads the pad amounts equally
      over all image sides, while :class:`~imgaug.augmenters.size.PadToSquare`
      by default spreads them randomly. This augmenter is thus also identical to
      :class:`~imgaug.augmenters.size.PadToAspectRatio` with
      ``aspect_ratio=1.0, position="center"``.
  
      Supported dtypes
      ----------------
  
      See :class:`~imgaug.augmenters.size.PadToFixedSize`.
  
      Parameters
      ----------
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      pad_mode : imgaug.ALL or str or list of str or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToAspectRatio.__init__`.
  
      pad_cval : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
          See :func:`~imgaug.augmenters.size.PadToAspectRatio.__init__`.
  
      deterministic : bool, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      random_state : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.CenterPadToSquare()
  
      Create an augmenter that pads each image until its square, i.e. height
      and width match.
      The rows to be padded will be spread *equally* over the top and bottom
      sides (analogous for the left/right sides).
  
      """
  
      def __init__(self, pad_mode="constant", pad_cval=0,
                   seed=None, name=None, **old_kwargs):
          super(CenterPadToSquare, self).__init__(
              pad_mode=pad_mode, pad_cval=pad_cval, position="center",
              seed=seed, name=name, **old_kwargs)
  
  
  class KeepSizeByResize(meta.Augmenter):
      """Resize images back to their input sizes after applying child augmenters.
  
      Combining this with e.g. a cropping augmenter as the child will lead to
      images being resized back to the input size after the crop operation was
      applied. Some augmenters have a ``keep_size`` argument that achieves the
      same goal (if set to ``True``), though this augmenter offers control over
      the interpolation mode and which augmentables to resize (images, heatmaps,
      segmentation maps).
  
      Supported dtypes
      ----------------
  
      See :func:`~imgaug.imgaug.imresize_many_images`.
  
      Parameters
      ----------
      children : Augmenter or list of imgaug.augmenters.meta.Augmenter or None, optional
          One or more augmenters to apply to images. These augmenters may change
          the image size.
  
      interpolation : KeepSizeByResize.NO_RESIZE or {'nearest', 'linear', 'area', 'cubic'} or {cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_AREA, cv2.INTER_CUBIC} or list of str or list of int or StochasticParameter, optional
          The interpolation mode to use when resizing images.
          Can take any value that :func:`~imgaug.imgaug.imresize_single_image`
          accepts, e.g. ``cubic``.
  
              * If this is ``KeepSizeByResize.NO_RESIZE`` then images will not
                be resized.
              * If this is a single ``str``, it is expected to have one of the
                following values: ``nearest``, ``linear``, ``area``, ``cubic``.
              * If this is a single integer, it is expected to have a value
                identical to one of: ``cv2.INTER_NEAREST``,
                ``cv2.INTER_LINEAR``, ``cv2.INTER_AREA``, ``cv2.INTER_CUBIC``.
              * If this is a ``list`` of ``str`` or ``int``, it is expected that
                each ``str``/``int`` is one of the above mentioned valid ones.
                A random one of these values will be sampled per image.
              * If this is a ``StochasticParameter``, it will be queried once per
                call to ``_augment_images()`` and must return ``N`` ``str`` s or
                ``int`` s (matching the above mentioned ones) for ``N`` images.
  
      interpolation_heatmaps : KeepSizeByResize.SAME_AS_IMAGES or KeepSizeByResize.NO_RESIZE or {'nearest', 'linear', 'area', 'cubic'} or {cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_AREA, cv2.INTER_CUBIC} or list of str or list of int or StochasticParameter, optional
          The interpolation mode to use when resizing heatmaps.
          Meaning and valid values are similar to `interpolation`. This
          parameter may also take the value ``KeepSizeByResize.SAME_AS_IMAGES``,
          which will lead to copying the interpolation modes used for the
          corresponding images. The value may also be returned on a per-image
          basis if `interpolation_heatmaps` is provided as a
          ``StochasticParameter`` or may be one possible value if it is
          provided as a ``list`` of ``str``.
  
      interpolation_segmaps : KeepSizeByResize.SAME_AS_IMAGES or KeepSizeByResize.NO_RESIZE or {'nearest', 'linear', 'area', 'cubic'} or {cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_AREA, cv2.INTER_CUBIC} or list of str or list of int or StochasticParameter, optional
          The interpolation mode to use when resizing segmentation maps.
          Similar to `interpolation_heatmaps`.
          **Note**: For segmentation maps, only ``NO_RESIZE`` or nearest
          neighbour interpolation (i.e. ``nearest``) make sense in the vast
          majority of all cases.
  
      seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      name : None or str, optional
          See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  
      **old_kwargs
          Outdated parameters. Avoid using these.
  
      Examples
      --------
      >>> import imgaug.augmenters as iaa
      >>> aug = iaa.KeepSizeByResize(
      >>>     iaa.Crop((20, 40), keep_size=False)
      >>> )
  
      Apply random cropping to input images, then resize them back to their
      original input sizes. The resizing is done using this augmenter instead
      of the corresponding internal resizing operation in ``Crop``.
  
      >>> aug = iaa.KeepSizeByResize(
      >>>     iaa.Crop((20, 40), keep_size=False),
      >>>     interpolation="nearest"
      >>> )
  
      Same as in the previous example, but images are now always resized using
      nearest neighbour interpolation.
  
      >>> aug = iaa.KeepSizeByResize(
      >>>     iaa.Crop((20, 40), keep_size=False),
      >>>     interpolation=["nearest", "cubic"],
      >>>     interpolation_heatmaps=iaa.KeepSizeByResize.SAME_AS_IMAGES,
      >>>     interpolation_segmaps=iaa.KeepSizeByResize.NO_RESIZE
      >>> )
  
      Similar to the previous example, but images are now sometimes resized
      using linear interpolation and sometimes using nearest neighbour
      interpolation. Heatmaps are resized using the same interpolation as was
      used for the corresponding image. Segmentation maps are not resized and
      will therefore remain at their size after cropping.
  
      """
  
      NO_RESIZE = "NO_RESIZE"
      SAME_AS_IMAGES = "SAME_AS_IMAGES"
  
      def __init__(self, children,
                   interpolation="cubic",
                   interpolation_heatmaps=SAME_AS_IMAGES,
                   interpolation_segmaps="nearest",
                   seed=None, name=None, **old_kwargs):
          super(KeepSizeByResize, self).__init__(
              seed=seed, name=name, **old_kwargs)
          self.children = children
  
          def _validate_param(val, allow_same_as_images):
              valid_ips_and_resize = ia.IMRESIZE_VALID_INTERPOLATIONS \
                                    + [KeepSizeByResize.NO_RESIZE]
              if allow_same_as_images and val == self.SAME_AS_IMAGES:
                  return self.SAME_AS_IMAGES
              if val in valid_ips_and_resize:
                  return iap.Deterministic(val)
              if isinstance(val, list):
                  assert len(val) > 0, (
                      "Expected a list of at least one interpolation method. "
                      "Got an empty list.")
                  valid_ips_here = valid_ips_and_resize
                  if allow_same_as_images:
                      valid_ips_here = valid_ips_here \
                                       + [KeepSizeByResize.SAME_AS_IMAGES]
                  only_valid_ips = all([ip in valid_ips_here for ip in val])
                  assert only_valid_ips, (
                      "Expected each interpolations to be one of '%s', got "
                      "'%s'." % (str(valid_ips_here), str(val)))
                  return iap.Choice(val)
              if isinstance(val, iap.StochasticParameter):
                  return val
              raise Exception(
                  "Expected interpolation to be one of '%s' or a list of "
                  "these values or a StochasticParameter. Got type %s." % (
                      str(ia.IMRESIZE_VALID_INTERPOLATIONS), type(val)))
  
          self.children = meta.handle_children_list(children, self.name, "then")
          self.interpolation = _validate_param(interpolation, False)
          self.interpolation_heatmaps = _validate_param(interpolation_heatmaps,
                                                        True)
          self.interpolation_segmaps = _validate_param(interpolation_segmaps,
                                                       True)
  
      def _augment_batch_(self, batch, random_state, parents, hooks):
          with batch.propagation_hooks_ctx(self, hooks, parents):
              images_were_array = None
              if batch.images is not None:
                  images_were_array = ia.is_np_array(batch.images)
              shapes_orig = self._get_shapes(batch)
  
              samples = self._draw_samples(batch.nb_rows, random_state)
  
              batch = self.children.augment_batch_(
                  batch, parents=parents + [self], hooks=hooks)
  
              if batch.images is not None:
                  batch.images = self._keep_size_images(
                      batch.images, shapes_orig["images"], images_were_array,
                      samples)
  
              if batch.heatmaps is not None:
                  # dont use shapes_orig["images"] because they might be None
                  batch.heatmaps = self._keep_size_maps(
                      batch.heatmaps, shapes_orig["heatmaps"],
                      shapes_orig["heatmaps_arr"], samples[1])
  
              if batch.segmentation_maps is not None:
                  # dont use shapes_orig["images"] because they might be None
                  batch.segmentation_maps = self._keep_size_maps(
                      batch.segmentation_maps, shapes_orig["segmentation_maps"],
                      shapes_orig["segmentation_maps_arr"], samples[2])
  
              for augm_name in ["keypoints", "bounding_boxes", "polygons",
                                "line_strings"]:
                  augm_value = getattr(batch, augm_name)
                  if augm_value is not None:
                      func = functools.partial(
                          self._keep_size_keypoints,
                          shapes_orig=shapes_orig[augm_name],
                          interpolations=samples[0])
                      cbaois = self._apply_to_cbaois_as_keypoints(augm_value,
                                                                  func)
                      setattr(batch, augm_name, cbaois)
          return batch
  
      @classmethod
      def _keep_size_images(cls, images, shapes_orig, images_were_array,
                            samples):
          interpolations, _, _ = samples
  
          gen = zip(images, interpolations, shapes_orig)
          result = []
          for image, interpolation, input_shape in gen:
              if interpolation == KeepSizeByResize.NO_RESIZE:
                  result.append(image)
              else:
                  result.append(
                      ia.imresize_single_image(image, input_shape[0:2],
                                               interpolation))
  
          if images_were_array:
              # note here that NO_RESIZE can have led to different shapes
              nb_shapes = len({image.shape for image in result})
              if nb_shapes == 1:
                  result = np.array(result, dtype=images.dtype)
  
          return result
  
      @classmethod
      def _keep_size_maps(cls, augmentables, shapes_orig_images,
                          shapes_orig_arrs, interpolations):
          result = []
          gen = zip(augmentables, interpolations,
                    shapes_orig_arrs, shapes_orig_images)
          for augmentable, interpolation, arr_shape_orig, img_shape_orig in gen:
              if interpolation == "NO_RESIZE":
                  result.append(augmentable)
              else:
                  augmentable = augmentable.resize(
                      arr_shape_orig[0:2], interpolation=interpolation)
                  augmentable.shape = img_shape_orig
                  result.append(augmentable)
  
          return result
  
      @classmethod
      def _keep_size_keypoints(cls, kpsois_aug, shapes_orig, interpolations):
          result = []
          gen = zip(kpsois_aug, interpolations, shapes_orig)
          for kpsoi_aug, interpolation, input_shape in gen:
              if interpolation == KeepSizeByResize.NO_RESIZE:
                  result.append(kpsoi_aug)
              else:
                  result.append(kpsoi_aug.on_(input_shape))
  
          return result
  
      @classmethod
      def _get_shapes(cls, batch):
          result = dict()
          for column in batch.columns:
              result[column.name] = [cell.shape for cell in column.value]
  
          if batch.heatmaps is not None:
              result["heatmaps_arr"] = [
                  cell.arr_0to1.shape for cell in batch.heatmaps]
  
          if batch.segmentation_maps is not None:
              result["segmentation_maps_arr"] = [
                  cell.arr.shape for cell in batch.segmentation_maps]
  
          return result
  
      def _draw_samples(self, nb_images, random_state):
          rngs = random_state.duplicate(3)
          interpolations = self.interpolation.draw_samples((nb_images,),
                                                           random_state=rngs[0])
  
          if self.interpolation_heatmaps == KeepSizeByResize.SAME_AS_IMAGES:
              interpolations_heatmaps = np.copy(interpolations)
          else:
              interpolations_heatmaps = self.interpolation_heatmaps.draw_samples(
                  (nb_images,), random_state=rngs[1]
              )
  
              # Note that `interpolations_heatmaps == self.SAME_AS_IMAGES`
              # works here only if the datatype of the array is such that it
              # may contain strings. It does not work properly for e.g.
              # integer arrays and will produce a single bool output, even
              # for arrays with more than one entry.
              same_as_imgs_idx = [ip == self.SAME_AS_IMAGES
                                  for ip in interpolations_heatmaps]
  
              interpolations_heatmaps[same_as_imgs_idx] = \
                  interpolations[same_as_imgs_idx]
  
          if self.interpolation_segmaps == KeepSizeByResize.SAME_AS_IMAGES:
              interpolations_segmaps = np.copy(interpolations)
          else:
              # TODO This used previously the same seed as the heatmaps part
              #      leading to the same sampled values. Was that intentional?
              #      Doesn't look like it should be that way.
              interpolations_segmaps = self.interpolation_segmaps.draw_samples(
                  (nb_images,), random_state=rngs[2]
              )
  
              # Note that `interpolations_heatmaps == self.SAME_AS_IMAGES`
              # works here only if the datatype of the array is such that it
              # may contain strings. It does not work properly for e.g.
              # integer arrays and will produce a single bool output, even
              # for arrays with more than one entry.
              same_as_imgs_idx = [ip == self.SAME_AS_IMAGES
                                  for ip in interpolations_segmaps]
  
              interpolations_segmaps[same_as_imgs_idx] = \
                  interpolations[same_as_imgs_idx]
  
          return interpolations, interpolations_heatmaps, interpolations_segmaps
  
      def _to_deterministic(self):
          aug = self.copy()
          aug.children = aug.children.to_deterministic()
          aug.deterministic = True
          aug.random_state = self.random_state.derive_rng_()
          return aug
  
      def get_parameters(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
          return [self.interpolation, self.interpolation_heatmaps]
  
      def get_children_lists(self):
          """See :func:`~imgaug.augmenters.meta.Augmenter.get_children_lists`."""
          return [self.children]
  
      def __str__(self):
          pattern = (
              "%s("
              "interpolation=%s, "
              "interpolation_heatmaps=%s, "
              "name=%s, "
              "children=%s, "
              "deterministic=%s"
              ")")
          return pattern % (
              self.__class__.__name__, self.interpolation,
              self.interpolation_heatmaps, self.name, self.children,
              self.deterministic)
   """Collection of basic functions used throughout imgaug."""
  from __future__ import print_function, division, absolute_import
  
  import math
  import numbers
  import sys
  import os
  import json
  import types
  import functools
  # collections.abc exists since 3.3 and is expected to be used for 3.8+
  try:
      from collections.abc import Iterable
  except ImportError:
      from collections import Iterable
  
  import numpy as np
  import cv2
  import imageio
  import six
  import six.moves as sm
  import skimage.draw
  import skimage.measure
  
  
  ALL = "ALL"
  
  FILE_DIR = os.path.dirname(os.path.abspath(__file__))
  
  # filepath to the quokka image, its annotations and depth map
  QUOKKA_FP = os.path.join(FILE_DIR, "quokka.jpg")
  QUOKKA_ANNOTATIONS_FP = os.path.join(FILE_DIR, "quokka_annotations.json")
  QUOKKA_DEPTH_MAP_HALFRES_FP = os.path.join(
      FILE_DIR, "quokka_depth_map_halfres.png")
  
  DEFAULT_FONT_FP = os.path.join(
      os.path.dirname(os.path.abspath(__file__)),
      "DejaVuSans.ttf"
  )
  
  
  # to check if a dtype instance is among these dtypes, use e.g.
  # `dtype.type in  NP_FLOAT_TYPES` do not just use `dtype in NP_FLOAT_TYPES` as
  # that would fail
  NP_FLOAT_TYPES = set(np.sctypes["float"])
  NP_INT_TYPES = set(np.sctypes["int"])
  NP_UINT_TYPES = set(np.sctypes["uint"])
  
  IMSHOW_BACKEND_DEFAULT = "matplotlib"
  
  IMRESIZE_VALID_INTERPOLATIONS = [
      "nearest", "linear", "area", "cubic",
      cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_AREA, cv2.INTER_CUBIC]
  
  
  ###############################################################################
  # Helpers for deprecation
  ###############################################################################
  
  class DeprecationWarning(Warning):  # pylint: disable=redefined-builtin
      """Warning for deprecated calls.
  
      Since python 2.7 DeprecatedWarning is silent by default. So we define
      our own DeprecatedWarning here so that it is not silent by default.
  
      """
  
  
  def warn(msg, category=UserWarning, stacklevel=2):
      """Generate a a warning with stacktrace.
  
      Parameters
      ----------
      msg : str
          The message of the warning.
  
      category : class
          The class of the warning to produce.
  
      stacklevel : int, optional
          How many steps above this function to "jump" in the stacktrace when
          displaying file and line number of the error message.
          Usually ``2``.
  
      """
      import warnings
      warnings.warn(msg, category=category, stacklevel=stacklevel)
  
  
  def warn_deprecated(msg, stacklevel=2):
      """Generate a non-silent deprecation warning with stacktrace.
  
      The used warning is ``imgaug.imgaug.DeprecationWarning``.
  
      Parameters
      ----------
      msg : str
          The message of the warning.
  
      stacklevel : int, optional
          How many steps above this function to "jump" in the stacktrace when
          displaying file and line number of the error message.
          Usually ``2``
  
      """
      warn(msg, category=DeprecationWarning, stacklevel=stacklevel)
  
  
  class deprecated(object):  # pylint: disable=invalid-name
      """Decorator to mark deprecated functions with warning.
  
      Adapted from
      <https://github.com/scikit-image/scikit-image/blob/master/skimage/_shared/utils.py>.
  
      Parameters
      ----------
      alt_func : None or str, optional
          If given, tell user what function to use instead.
  
      behavior : {'warn', 'raise'}, optional
          Behavior during call to deprecated function: ``warn`` means that the
          user is warned that the function is deprecated; ``raise`` means that
          an error is raised.
  
      removed_version : None or str, optional
          The package version in which the deprecated function will be removed.
  
      comment : None or str, optional
          An optional comment that will be appended to the warning message.
  
      """
  
      def __init__(self, alt_func=None, behavior="warn", removed_version=None,
                   comment=None):
          self.alt_func = alt_func
          self.behavior = behavior
          self.removed_version = removed_version
          self.comment = comment
  
      def __call__(self, func):
          alt_msg = None
          if self.alt_func is not None:
              alt_msg = "Use ``%s`` instead." % (self.alt_func,)
  
          rmv_msg = None
          if self.removed_version is not None:
              rmv_msg = "It will be removed in version %s." % (
                  self.removed_version,)
  
          comment_msg = None
          if self.comment is not None and len(self.comment) > 0:
              comment_msg = "%s." % (self.comment.rstrip(". "),)
  
          addendum = " ".join([submsg
                               for submsg
                               in [alt_msg, rmv_msg, comment_msg]
                               if submsg is not None])
  
          @functools.wraps(func)
          def wrapped(*args, **kwargs):
              # getargpec() is deprecated
              # pylint: disable=deprecated-method
  
              # TODO add class name if class method
              import inspect
              # arg_names = func.__code__.co_varnames
  
              # getargspec() was deprecated in py3, but doesn't exist in py2
              if hasattr(inspect, "getfullargspec"):
                  arg_names = inspect.getfullargspec(func)[0]
              else:
                  arg_names = inspect.getargspec(func)[0]
  
              if "self" in arg_names or "cls" in arg_names:
                  main_msg = "Method ``%s.%s()`` is deprecated." % (
                      args[0].__class__.__name__, func.__name__)
              else:
                  main_msg = "Function ``%s()`` is deprecated." % (
                      func.__name__,)
  
              msg = (main_msg + " " + addendum).rstrip(" ").replace("``", "`")
  
              if self.behavior == "warn":
                  warn_deprecated(msg, stacklevel=3)
              elif self.behavior == "raise":
                  raise DeprecationWarning(msg)
              return func(*args, **kwargs)
  
          # modify doc string to display deprecation warning
          doc = "**Deprecated**. " + addendum
          if wrapped.__doc__ is None:
              wrapped.__doc__ = doc
          else:
              wrapped.__doc__ = doc + "\n\n    " + wrapped.__doc__
  
          return wrapped
  
  ###############################################################################
  
  
  def is_np_array(val):
      """Check whether a variable is a numpy array.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is a numpy array. Otherwise ``False``.
  
      """
      # using np.generic here via isinstance(val, (np.ndarray, np.generic))
      # seems to also fire for scalar numpy values even though those are not
      # arrays
      return isinstance(val, np.ndarray)
  
  
  def is_np_scalar(val):
      """Check whether a variable is a numpy scalar.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is a numpy scalar. Otherwise ``False``.
  
      """
      # Note that isscalar() alone also fires for thinks like python strings
      # or booleans.
      # The isscalar() was added to make this function not fire for non-scalar
      # numpy types. Not sure if it is necessary.
      return isinstance(val, np.generic) and np.isscalar(val)
  
  
  def is_single_integer(val):
      """Check whether a variable is an ``int``.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is an ``int``. Otherwise ``False``.
  
      """
      return isinstance(val, numbers.Integral) and not isinstance(val, bool)
  
  
  def is_single_float(val):
      """Check whether a variable is a ``float``.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is a ``float``. Otherwise ``False``.
  
      """
      return (
          isinstance(val, numbers.Real)
          and not is_single_integer(val)
          and not isinstance(val, bool)
      )
  
  
  def is_single_number(val):
      """Check whether a variable is a ``number``, i.e. an ``int`` or ``float``.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is a ``number``. Otherwise ``False``.
  
      """
      return is_single_integer(val) or is_single_float(val)
  
  
  def is_iterable(val):
      """
      Checks whether a variable is iterable.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is an iterable. Otherwise ``False``.
  
      """
      return isinstance(val, Iterable)
  
  
  # TODO convert to is_single_string() or rename is_single_integer/float/number()
  def is_string(val):
      """Check whether a variable is a string.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is a string. Otherwise ``False``.
  
      """
      return isinstance(val, six.string_types)
  
  
  def is_single_bool(val):
      """Check whether a variable is a ``bool``.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is a ``bool``. Otherwise ``False``.
  
      """
      # pylint: disable=unidiomatic-typecheck
      return type(val) == type(True)
  
  
  def is_integer_array(val):
      """Check whether a variable is a numpy integer array.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is a numpy integer array. Otherwise ``False``.
  
      """
      return is_np_array(val) and issubclass(val.dtype.type, np.integer)
  
  
  def is_float_array(val):
      """Check whether a variable is a numpy float array.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is a numpy float array. Otherwise ``False``.
  
      """
      return is_np_array(val) and issubclass(val.dtype.type, np.floating)
  
  
  def is_callable(val):
      """Check whether a variable is a callable, e.g. a function.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` if the variable is a callable. Otherwise ``False``.
  
      """
      # python 3.x with x <= 2 does not support callable(), apparently
      if sys.version_info[0] == 3 and sys.version_info[1] <= 2:
          return hasattr(val, '__call__')
      return callable(val)
  
  
  def is_generator(val):
      """Check whether a variable is a generator.
  
      Parameters
      ----------
      val
          The variable to check.
  
      Returns
      -------
      bool
          ``True`` is the variable is a generator. Otherwise ``False``.
  
      """
      return isinstance(val, types.GeneratorType)
  
  
  def flatten(nested_iterable):
      """Flatten arbitrarily nested lists/tuples.
  
      Code partially taken from https://stackoverflow.com/a/10824420.
  
      Parameters
      ----------
      nested_iterable
          A ``list`` or ``tuple`` of arbitrarily nested values.
  
      Yields
      ------
      any
          All values in `nested_iterable`, flattened.
  
      """
      # don't just check if something is iterable here, because then strings
      # and arrays will be split into their characters and components
      if not isinstance(nested_iterable, (list, tuple)):
          yield nested_iterable
      else:
          for i in nested_iterable:
              if isinstance(i, (list, tuple)):
                  for j in flatten(i):
                      yield j
              else:
                  yield i
  
  
  # TODO no longer used anywhere. deprecate?
  def caller_name():
      """Return the name of the caller, e.g. a function.
  
      Returns
      -------
      str
          The name of the caller as a string
  
      """
      # pylint: disable=protected-access
      return sys._getframe(1).f_code.co_name
  
  
  def seed(entropy=None, seedval=None):
      """Set the seed of imgaug's global RNG.
  
      The global RNG controls most of the "randomness" in imgaug.
  
      The global RNG is the default one used by all augmenters. Under special
      circumstances (e.g. when an augmenter is switched to deterministic mode),
      the global RNG is replaced with a local one. The state of that replacement
      may be dependent on the global RNG's state at the time of creating the
      child RNG.
  
      .. note::
  
          This function is not yet marked as deprecated, but might be in the
          future. The preferred way to seed `imgaug` is via
          :func:`~imgaug.random.seed`.
  
      Parameters
      ----------
      entropy : int
          The seed value to use.
  
      seedval : None or int, optional
          Deprecated.
  
      """
      assert entropy is not None or seedval is not None, (
          "Expected argument 'entropy' or 'seedval' to be not-None, but both"
          "were None.")
  
      if seedval is not None:
          assert entropy is None, (
              "Argument 'seedval' is the outdated name for 'entropy'. Hence, "
              "if it is provided, 'entropy' must be None. Got 'entropy' value "
              "of type %s." % (type(entropy),))
  
          warn_deprecated("Parameter 'seedval' is deprecated. Use "
                          "'entropy' instead.")
          entropy = seedval
  
      import imgaug.random
      imgaug.random.seed(entropy)
  
  
  @deprecated("imgaug.random.normalize_generator")
  def normalize_random_state(random_state):
      """Normalize various inputs to a numpy random generator.
  
      Parameters
      ----------
      random_state : None or int or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.bit_generator.SeedSequence or numpy.random.RandomState
          See :func:`~imgaug.random.normalize_generator`.
  
      Returns
      -------
      numpy.random.Generator or numpy.random.RandomState
          In numpy <=1.16 a ``RandomState``, in 1.17+ a ``Generator`` (even if
          the input was a ``RandomState``).
  
      """
      import imgaug.random
      return imgaug.random.normalize_generator_(random_state)
  
  
  @deprecated("imgaug.random.get_global_rng")
  def current_random_state():
      """Get or create the current global RNG of imgaug.
  
      Note that the first call to this function will create a global RNG.
  
      Returns
      -------
      imgaug.random.RNG
          The global RNG to use.
  
      """
      import imgaug.random
      return imgaug.random.get_global_rng()
  
  
  @deprecated("imgaug.random.convert_seed_to_rng")
  def new_random_state(seed=None, fully_random=False):
      """Create a new numpy random number generator.
  
      Parameters
      ----------
      seed : None or int, optional
          The seed value to use. If ``None`` and `fully_random` is ``False``,
          the seed will be derived from the global RNG. If `fully_random` is
          ``True``, the seed will be provided by the OS.
  
      fully_random : bool, optional
          Whether the seed will be provided by the OS.
  
      Returns
      -------
      numpy.random.Generator or numpy.random.RandomState
          In numpy <=1.16 a ``RandomState``, in 1.17+ a ``Generator``.
          Both are initialized with the provided seed.
  
      """
      # pylint: disable=redefined-outer-name
      import imgaug.random
      if seed is None:
          if fully_random:
              return imgaug.random.RNG.create_fully_random()
          return imgaug.random.RNG.create_pseudo_random_()
      return imgaug.random.RNG(seed)
  
  
  # TODO seems to not be used anywhere anymore
  @deprecated("imgaug.random.convert_seed_to_rng")
  def dummy_random_state():
      """Create a dummy random state using a seed of ``1``.
  
      Returns
      -------
      imgaug.random.RNG
          The new random state.
  
      """
      import imgaug.random
      return imgaug.random.RNG(1)
  
  
  @deprecated("imgaug.random.copy_generator_unless_global_rng")
  def copy_random_state(random_state, force_copy=False):
      """Copy an existing numpy (random number) generator.
  
      Parameters
      ----------
      random_state : numpy.random.Generator or numpy.random.RandomState
          The generator to copy.
  
      force_copy : bool, optional
          If ``True``, this function will always create a copy of every random
          state. If ``False``, it will not copy numpy's default random state,
          but all other random states.
  
      Returns
      -------
      rs_copy : numpy.random.RandomState
          The copied random state.
  
      """
      import imgaug.random
      if force_copy:
          return imgaug.random.copy_generator(random_state)
      return imgaug.random.copy_generator_unless_global_generator(random_state)
  
  
  @deprecated("imgaug.random.derive_generator_")
  def derive_random_state(random_state):
      """Derive a child numpy random generator from another one.
  
      Parameters
      ----------
      random_state : numpy.random.Generator or numpy.random.RandomState
          The generator from which to derive a new child generator.
  
      Returns
      -------
      numpy.random.Generator or numpy.random.RandomState
          In numpy <=1.16 a ``RandomState``, in 1.17+ a ``Generator``.
          In both cases a derived child generator.
  
      """
      import imgaug.random
      return imgaug.random.derive_generator_(random_state)
  
  
  @deprecated("imgaug.random.derive_generators_")
  def derive_random_states(random_state, n=1):
      """Derive child numpy random generators from another one.
  
      Parameters
      ----------
      random_state : numpy.random.Generator or numpy.random.RandomState
          The generator from which to derive new child generators.
  
      n : int, optional
          Number of child generators to derive.
  
      Returns
      -------
      list of numpy.random.Generator or list of numpy.random.RandomState
          In numpy <=1.16 a ``list`` of  ``RandomState`` s,
          in 1.17+ a ``list`` of ``Generator`` s.
          In both cases lists of derived child generators.
  
      """
      import imgaug.random
      return imgaug.random.derive_generators_(random_state, n=n)
  
  
  @deprecated("imgaug.random.advance_generator_")
  def forward_random_state(random_state):
      """Advance a numpy random generator's internal state.
  
      Parameters
      ----------
      random_state : numpy.random.Generator or numpy.random.RandomState
          Generator of which to advance the internal state.
  
      """
      import imgaug.random
      imgaug.random.advance_generator_(random_state)
  
  
  def _quokka_normalize_extract(extract):
      """Generate a normalized rectangle for the standard quokka image.
  
      Parameters
      ----------
      extract : 'square' or tuple of number or imgaug.augmentables.bbs.BoundingBox or imgaug.augmentables.bbs.BoundingBoxesOnImage
          Unnormalized representation of the image subarea to be extracted.
  
              * If ``str`` ``square``, then a squared area
                ``(x: 0 to max 643, y: 0 to max 643)`` will be extracted from
                the image.
              * If a ``tuple``, then expected to contain four ``number`` s
                denoting ``(x1, y1, x2, y2)``.
              * If a :class:`~imgaug.augmentables.bbs.BoundingBox`, then that
                bounding box's area will be extracted from the image.
              * If a :class:`~imgaug.augmentables.bbs.BoundingBoxesOnImage`,
                then expected to contain exactly one bounding box and a shape
                matching the full image dimensions (i.e. ``(643, 960, *)``).
                Then the one bounding box will be used similar to
                ``BoundingBox`` above.
  
      Returns
      -------
      imgaug.augmentables.bbs.BoundingBox
          Normalized representation of the area to extract from the standard
          quokka image.
  
      """
      # TODO get rid of this deferred import
      from imgaug.augmentables.bbs import BoundingBox, BoundingBoxesOnImage
  
      if extract == "square":
          bb = BoundingBox(x1=0, y1=0, x2=643, y2=643)
      elif isinstance(extract, tuple) and len(extract) == 4:
          bb = BoundingBox(x1=extract[0], y1=extract[1],
                           x2=extract[2], y2=extract[3])
      elif isinstance(extract, BoundingBox):
          bb = extract
      elif isinstance(extract, BoundingBoxesOnImage):
          assert len(extract.bounding_boxes) == 1, (
              "Provided BoundingBoxesOnImage instance may currently only "
              "contain a single bounding box.")
          assert extract.shape[0:2] == (643, 960), (
              "Expected BoundingBoxesOnImage instance on an image of shape "
              "(643, 960, ?). Got shape %s." % (extract.shape,))
          bb = extract.bounding_boxes[0]
      else:
          raise Exception(
              "Expected 'square' or tuple of four entries or BoundingBox or "
              "BoundingBoxesOnImage for parameter 'extract', "
              "got %s." % (type(extract),)
          )
      return bb
  
  
  # TODO is this the same as the project functions in augmentables?
  def _compute_resized_shape(from_shape, to_shape):
      """Compute the intended new shape of an image-like array after resizing.
  
      Parameters
      ----------
      from_shape : tuple or ndarray
          Old shape of the array. Usually expected to be a ``tuple`` of form
          ``(H, W)`` or ``(H, W, C)`` or alternatively an array with two or
          three dimensions.
  
      to_shape : None or tuple of ints or tuple of floats or int or float or ndarray
          New shape of the array.
  
              * If ``None``, then `from_shape` will be used as the new shape.
              * If an ``int`` ``V``, then the new shape will be ``(V, V, [C])``,
                where ``C`` will be added if it is part of `from_shape`.
              * If a ``float`` ``V``, then the new shape will be
                ``(H*V, W*V, [C])``, where ``H`` and ``W`` are the old
                height/width.
              * If a ``tuple`` ``(H', W', [C'])`` of ints, then ``H'`` and ``W'``
                will be used as the new height and width.
              * If a ``tuple`` ``(H', W', [C'])`` of floats (except ``C``), then
                ``H'`` and ``W'`` will be used as the new height and width.
              * If a numpy array, then the array's shape will be used.
  
      Returns
      -------
      tuple of int
          New shape.
  
      """
      if is_np_array(from_shape):
          from_shape = from_shape.shape
      if is_np_array(to_shape):
          to_shape = to_shape.shape
  
      to_shape_computed = list(from_shape)
  
      if to_shape is None:
          pass
      elif isinstance(to_shape, tuple):
          assert len(from_shape) in [2, 3]
          assert len(to_shape) in [2, 3]
  
          if len(from_shape) == 3 and len(to_shape) == 3:
              assert from_shape[2] == to_shape[2]
          elif len(to_shape) == 3:
              to_shape_computed.append(to_shape[2])
  
          is_to_s_valid_values = all(
              [v is None or is_single_number(v) for v in to_shape[0:2]])
          assert is_to_s_valid_values, (
              "Expected the first two entries in to_shape to be None or "
              "numbers, got types %s." % (
                  str([type(v) for v in to_shape[0:2]]),))
  
          for i, from_shape_i in enumerate(from_shape[0:2]):
              if to_shape[i] is None:
                  to_shape_computed[i] = from_shape_i
              elif is_single_integer(to_shape[i]):
                  to_shape_computed[i] = to_shape[i]
              else:  # float
                  to_shape_computed[i] = int(np.round(from_shape_i * to_shape[i]))
      elif is_single_integer(to_shape) or is_single_float(to_shape):
          to_shape_computed = _compute_resized_shape(
              from_shape, (to_shape, to_shape))
      else:
          raise Exception(
              "Expected to_shape to be None or ndarray or tuple of floats or "
              "tuple of ints or single int or single float, "
              "got %s." % (type(to_shape),))
  
      return tuple(to_shape_computed)
  
  
  def quokka(size=None, extract=None):
      """Return an image of a quokka as a numpy array.
  
      Parameters
      ----------
      size : None or float or tuple of int, optional
          Size of the output image. Input into
          :func:`~imgaug.imgaug.imresize_single_image`. Usually expected to be a
          ``tuple`` ``(H, W)``, where ``H`` is the desired height and ``W`` is
          the width. If ``None``, then the image will not be resized.
  
      extract : None or 'square' or tuple of number or imgaug.augmentables.bbs.BoundingBox or imgaug.augmentables.bbs.BoundingBoxesOnImage
          Subarea of the quokka image to extract:
  
              * If ``None``, then the whole image will be used.
              * If ``str`` ``square``, then a squared area
                ``(x: 0 to max 643, y: 0 to max 643)`` will be extracted from
                the image.
              * If a ``tuple``, then expected to contain four ``number`` s
                denoting ``(x1, y1, x2, y2)``.
              * If a :class:`~imgaug.augmentables.bbs.BoundingBox`, then that
                bounding box's area will be extracted from the image.
              * If a :class:`~imgaug.augmentables.bbs.BoundingBoxesOnImage`,
                then expected to contain exactly one bounding box and a shape
                matching the full image dimensions (i.e. ``(643, 960, *)``).
                Then the one bounding box will be used similar to
                ``BoundingBox`` above.
  
      Returns
      -------
      (H,W,3) ndarray
          The image array of dtype ``uint8``.
  
      """
      img = imageio.imread(QUOKKA_FP, pilmode="RGB")
      if extract is not None:
          bb = _quokka_normalize_extract(extract)
          img = bb.extract_from_image(img)
      if size is not None:
          shape_resized = _compute_resized_shape(img.shape, size)
          img = imresize_single_image(img, shape_resized[0:2])
      return img
  
  
  def quokka_square(size=None):
      """Return an (square) image of a quokka as a numpy array.
  
      Parameters
      ----------
      size : None or float or tuple of int, optional
          Size of the output image. Input into
          :func:`~imgaug.imgaug.imresize_single_image`. Usually expected to be a
          ``tuple`` ``(H, W)``, where ``H`` is the desired height and ``W`` is
          the width. If ``None``, then the image will not be resized.
  
      Returns
      -------
      (H,W,3) ndarray
          The image array of dtype ``uint8``.
  
      """
      return quokka(size=size, extract="square")
  
  
  def quokka_heatmap(size=None, extract=None):
      """Return a heatmap (here: depth map) for the standard example quokka image.
  
      Parameters
      ----------
      size : None or float or tuple of int, optional
          See :func:`~imgaug.imgaug.quokka`.
  
      extract : None or 'square' or tuple of number or imgaug.augmentables.bbs.BoundingBox or imgaug.augmentables.bbs.BoundingBoxesOnImage
          See :func:`~imgaug.imgaug.quokka`.
  
      Returns
      -------
      imgaug.augmentables.heatmaps.HeatmapsOnImage
          Depth map as an heatmap object. Values close to ``0.0`` denote objects
          that are close to the camera. Values close to ``1.0`` denote objects
          that are furthest away (among all shown objects).
  
      """
      # TODO get rid of this deferred import
      from imgaug.augmentables.heatmaps import HeatmapsOnImage
  
      img = imageio.imread(QUOKKA_DEPTH_MAP_HALFRES_FP, pilmode="RGB")
      img = imresize_single_image(img, (643, 960), interpolation="cubic")
  
      if extract is not None:
          bb = _quokka_normalize_extract(extract)
          img = bb.extract_from_image(img)
      if size is None:
          size = img.shape[0:2]
  
      shape_resized = _compute_resized_shape(img.shape, size)
      img = imresize_single_image(img, shape_resized[0:2])
      img_0to1 = img[..., 0]  # depth map was saved as 3-channel RGB
      img_0to1 = img_0to1.astype(np.float32) / 255.0
      img_0to1 = 1 - img_0to1  # depth map was saved as 0 being furthest away
  
      return HeatmapsOnImage(img_0to1, shape=img_0to1.shape[0:2] + (3,))
  
  
  def quokka_segmentation_map(size=None, extract=None):
      """Return a segmentation map for the standard example quokka image.
  
      Parameters
      ----------
      size : None or float or tuple of int, optional
          See :func:`~imgaug.imgaug.quokka`.
  
      extract : None or 'square' or tuple of number or imgaug.augmentables.bbs.BoundingBox or imgaug.augmentables.bbs.BoundingBoxesOnImage
          See :func:`~imgaug.imgaug.quokka`.
  
      Returns
      -------
      imgaug.augmentables.segmaps.SegmentationMapsOnImage
          Segmentation map object.
  
      """
      # pylint: disable=invalid-name
      # TODO get rid of this deferred import
      from imgaug.augmentables.segmaps import SegmentationMapsOnImage
  
      with open(QUOKKA_ANNOTATIONS_FP, "r") as f:
          json_dict = json.load(f)
  
      xx = []
      yy = []
      for kp_dict in json_dict["polygons"][0]["keypoints"]:
          x = kp_dict["x"]
          y = kp_dict["y"]
          xx.append(x)
          yy.append(y)
  
      img_seg = np.zeros((643, 960, 1), dtype=np.int32)
      rr, cc = skimage.draw.polygon(
          np.array(yy), np.array(xx), shape=img_seg.shape)
      img_seg[rr, cc, 0] = 1
  
      if extract is not None:
          bb = _quokka_normalize_extract(extract)
          img_seg = bb.extract_from_image(img_seg)
  
      segmap = SegmentationMapsOnImage(img_seg, shape=img_seg.shape[0:2] + (3,))
  
      if size is not None:
          shape_resized = _compute_resized_shape(img_seg.shape, size)
          segmap = segmap.resize(shape_resized[0:2])
          segmap.shape = tuple(shape_resized[0:2]) + (3,)
  
      return segmap
  
  
  def quokka_keypoints(size=None, extract=None):
      """Return example keypoints on the standard example quokke image.
  
      The keypoints cover the eyes, ears, nose and paws.
  
      Parameters
      ----------
      size : None or float or tuple of int or tuple of float, optional
          Size of the output image on which the keypoints are placed. If
          ``None``, then the keypoints are not projected to any new size
          (positions on the original image are used). ``float`` s lead to
          relative size changes, ``int`` s to absolute sizes in pixels.
  
      extract : None or 'square' or tuple of number or imgaug.augmentables.bbs.BoundingBox or imgaug.augmentables.bbs.BoundingBoxesOnImage
          Subarea to extract from the image. See :func:`~imgaug.imgaug.quokka`.
  
      Returns
      -------
      imgaug.augmentables.kps.KeypointsOnImage
          Example keypoints on the quokka image.
  
      """
      # TODO get rid of this deferred import
      from imgaug.augmentables.kps import Keypoint, KeypointsOnImage
  
      left, top = 0, 0
      if extract is not None:
          bb_extract = _quokka_normalize_extract(extract)
          left = bb_extract.x1
          top = bb_extract.y1
      with open(QUOKKA_ANNOTATIONS_FP, "r") as f:
          json_dict = json.load(f)
      keypoints = []
      for kp_dict in json_dict["keypoints"]:
          keypoints.append(Keypoint(x=kp_dict["x"] - left, y=kp_dict["y"] - top))
      if extract is not None:
          shape = (bb_extract.height, bb_extract.width, 3)
      else:
          shape = (643, 960, 3)
      kpsoi = KeypointsOnImage(keypoints, shape=shape)
      if size is not None:
          shape_resized = _compute_resized_shape(shape, size)
          kpsoi = kpsoi.on(shape_resized)
      return kpsoi
  
  
  def quokka_bounding_boxes(size=None, extract=None):
      """Return example bounding boxes on the standard example quokke image.
  
      Currently only a single bounding box is returned that covers the quokka.
  
      Parameters
      ----------
      size : None or float or tuple of int or tuple of float, optional
          Size of the output image on which the BBs are placed. If ``None``, then
          the BBs are not projected to any new size (positions on the original
          image are used). ``float`` s lead to relative size changes, ``int`` s
          to absolute sizes in pixels.
  
      extract : None or 'square' or tuple of number or imgaug.augmentables.bbs.BoundingBox or imgaug.augmentables.bbs.BoundingBoxesOnImage
          Subarea to extract from the image. See :func:`~imgaug.imgaug.quokka`.
  
      Returns
      -------
      imgaug.augmentables.bbs.BoundingBoxesOnImage
          Example BBs on the quokka image.
  
      """
      # TODO get rid of this deferred import
      from imgaug.augmentables.bbs import BoundingBox, BoundingBoxesOnImage
  
      left, top = 0, 0
      if extract is not None:
          bb_extract = _quokka_normalize_extract(extract)
          left = bb_extract.x1
          top = bb_extract.y1
      with open(QUOKKA_ANNOTATIONS_FP, "r") as f:
          json_dict = json.load(f)
      bbs = []
      for bb_dict in json_dict["bounding_boxes"]:
          bbs.append(
              BoundingBox(
                  x1=bb_dict["x1"] - left,
                  y1=bb_dict["y1"] - top,
                  x2=bb_dict["x2"] - left,
                  y2=bb_dict["y2"] - top
              )
          )
      if extract is not None:
          shape = (bb_extract.height, bb_extract.width, 3)
      else:
          shape = (643, 960, 3)
      bbsoi = BoundingBoxesOnImage(bbs, shape=shape)
      if size is not None:
          shape_resized = _compute_resized_shape(shape, size)
          bbsoi = bbsoi.on(shape_resized)
      return bbsoi
  
  
  def quokka_polygons(size=None, extract=None):
      """
      Returns example polygons on the standard example quokke image.
  
      The result contains one polygon, covering the quokka's outline.
  
      Parameters
      ----------
      size : None or float or tuple of int or tuple of float, optional
          Size of the output image on which the polygons are placed. If ``None``,
          then the polygons are not projected to any new size (positions on the
          original image are used). ``float`` s lead to relative size changes,
          ``int`` s to absolute sizes in pixels.
  
      extract : None or 'square' or tuple of number or imgaug.augmentables.bbs.BoundingBox or imgaug.augmentables.bbs.BoundingBoxesOnImage
          Subarea to extract from the image. See :func:`~imgaug.imgaug.quokka`.
  
      Returns
      -------
      imgaug.augmentables.polys.PolygonsOnImage
          Example polygons on the quokka image.
  
      """
      # TODO get rid of this deferred import
      from imgaug.augmentables.polys import Polygon, PolygonsOnImage
  
      left, top = 0, 0
      if extract is not None:
          bb_extract = _quokka_normalize_extract(extract)
          left = bb_extract.x1
          top = bb_extract.y1
      with open(QUOKKA_ANNOTATIONS_FP, "r") as f:
          json_dict = json.load(f)
      polygons = []
      for poly_json in json_dict["polygons"]:
          polygons.append(
              Polygon([(point["x"] - left, point["y"] - top)
                       for point in poly_json["keypoints"]])
          )
      if extract is not None:
          shape = (bb_extract.height, bb_extract.width, 3)
      else:
          shape = (643, 960, 3)
      psoi = PolygonsOnImage(polygons, shape=shape)
      if size is not None:
          shape_resized = _compute_resized_shape(shape, size)
          psoi = psoi.on(shape_resized)
      return psoi
  
  
  # TODO change this to some atan2 stuff?
  def angle_between_vectors(v1, v2):
      """Calculcate the angle in radians between vectors `v1` and `v2`.
  
      From
      http://stackoverflow.com/questions/2827393/angles-between-two-n-dimensional-vectors-in-python
  
      Parameters
      ----------
      v1 : (N,) ndarray
          First vector.
  
      v2 : (N,) ndarray
          Second vector.
  
      Returns
      -------
      float
          Angle in radians.
  
      Examples
      --------
      >>> angle_between_vectors(np.float32([1, 0, 0]), np.float32([0, 1, 0]))
      1.570796...
  
      >>> angle_between_vectors(np.float32([1, 0, 0]), np.float32([1, 0, 0]))
      0.0
  
      >>> angle_between_vectors(np.float32([1, 0, 0]), np.float32([-1, 0, 0]))
      3.141592...
  
      """
      # pylint: disable=invalid-name
      length1 = np.linalg.norm(v1)
      length2 = np.linalg.norm(v2)
      v1_unit = (v1 / length1) if length1 > 0 else np.float32(v1) * 0
      v2_unit = (v2 / length2) if length2 > 0 else np.float32(v2) * 0
      return np.arccos(np.clip(np.dot(v1_unit, v2_unit), -1.0, 1.0))
  
  
  # TODO is this used anywhere?
  # TODO this might also be covered by augmentables.utils or
  #      augmentables.polys/lines
  def compute_line_intersection_point(x1, y1, x2, y2, x3, y3, x4, y4):
      """Compute the intersection point of two lines.
  
      Taken from https://stackoverflow.com/a/20679579 .
  
      Parameters
      ----------
      x1 : number
          x coordinate of the first point on line 1.
          (The lines extends beyond this point.)
  
      y1 : number
          y coordinate of the first point on line 1.
          (The lines extends beyond this point.)
  
      x2 : number
          x coordinate of the second point on line 1.
          (The lines extends beyond this point.)
  
      y2 : number
          y coordinate of the second point on line 1.
          (The lines extends beyond this point.)
  
      x3 : number
          x coordinate of the first point on line 2.
          (The lines extends beyond this point.)
  
      y3 : number
          y coordinate of the first point on line 2.
          (The lines extends beyond this point.)
  
      x4 : number
          x coordinate of the second point on line 2.
          (The lines extends beyond this point.)
  
      y4 : number
          y coordinate of the second point on line 2.
          (The lines extends beyond this point.)
  
      Returns
      -------
      tuple of number or bool
          The coordinate of the intersection point as a ``tuple`` ``(x, y)``.
          If the lines are parallel (no intersection point or an infinite number
          of them), the result is ``False``.
  
      """
      # pylint: disable=invalid-name
      def _make_line(point1, point2):
          line_y = (point1[1] - point2[1])
          line_x = (point2[0] - point1[0])
          slope = (point1[0] * point2[1] - point2[0] * point1[1])
          return line_y, line_x, -slope
  
      line1 = _make_line((x1, y1), (x2, y2))
      line2 = _make_line((x3, y3), (x4, y4))
  
      D = line1[0] * line2[1] - line1[1] * line2[0]
      Dx = line1[2] * line2[1] - line1[1] * line2[2]
      Dy = line1[0] * line2[2] - line1[2] * line2[0]
      if D != 0:
          x = Dx / D
          y = Dy / D
          return x, y
      return False
  
  
  # TODO replace by cv2.putText()?
  def draw_text(img, y, x, text, color=(0, 255, 0), size=25):
      """Draw text on an image.
  
      This uses by default DejaVuSans as its font, which is included in this
      library.
  
      dtype support::
  
          * ``uint8``: yes; fully tested
          * ``uint16``: no
          * ``uint32``: no
          * ``uint64``: no
          * ``int8``: no
          * ``int16``: no
          * ``int32``: no
          * ``int64``: no
          * ``float16``: no
          * ``float32``: yes; not tested
          * ``float64``: no
          * ``float128``: no
          * ``bool``: no
  
          TODO check if other dtypes could be enabled
  
      Parameters
      ----------
      img : (H,W,3) ndarray
          The image array to draw text on.
          Expected to be of dtype ``uint8`` or ``float32`` (expected value
          range is ``[0.0, 255.0]``).
  
      y : int
          x-coordinate of the top left corner of the text.
  
      x : int
          y- coordinate of the top left corner of the text.
  
      text : str
          The text to draw.
  
      color : iterable of int, optional
          Color of the text to draw. For RGB-images this is expected to be an
          RGB color.
  
      size : int, optional
          Font size of the text to draw.
  
      Returns
      -------
      (H,W,3) ndarray
          Input image with text drawn on it.
  
      """
      from PIL import (
          Image as PIL_Image,
          ImageDraw as PIL_ImageDraw,
          ImageFont as PIL_ImageFont
      )
  
      assert img.dtype.name in ["uint8", "float32"], (
          "Can currently draw text only on images of dtype 'uint8' or "
          "'float32'. Got dtype %s." % (img.dtype.name,))
  
      input_dtype = img.dtype
      if img.dtype == np.float32:
          img = img.astype(np.uint8)
  
      img = PIL_Image.fromarray(img)
      font = PIL_ImageFont.truetype(DEFAULT_FONT_FP, size)
      context = PIL_ImageDraw.Draw(img)
      context.text((x, y), text, fill=tuple(color), font=font)
      img_np = np.asarray(img)
  
      # PIL/asarray returns read only array
      if not img_np.flags["WRITEABLE"]:
          try:
              # this seems to no longer work with np 1.16 (or was pillow
              # updated?)
              img_np.setflags(write=True)
          except ValueError as ex:
              if "cannot set WRITEABLE flag to True of this array" in str(ex):
                  img_np = np.copy(img_np)
  
      if img_np.dtype != input_dtype:
          img_np = img_np.astype(input_dtype)
  
      return img_np
  
  
  # TODO rename sizes to size?
  def imresize_many_images(images, sizes=None, interpolation=None):
      """Resize each image in a list or array to a specified size.
  
      dtype support::
  
          * ``uint8``: yes; fully tested
          * ``uint16``: yes; tested
          * ``uint32``: no (1)
          * ``uint64``: no (2)
          * ``int8``: yes; tested (3)
          * ``int16``: yes; tested
          * ``int32``: limited; tested (4)
          * ``int64``: no (2)
          * ``float16``: yes; tested (5)
          * ``float32``: yes; tested
          * ``float64``: yes; tested
          * ``float128``: no (1)
          * ``bool``: yes; tested (6)
  
          - (1) rejected by ``cv2.imresize``
          - (2) results too inaccurate
          - (3) mapped internally to ``int16`` when interpolation!="nearest"
          - (4) only supported for interpolation="nearest", other interpolations
                lead to cv2 error
          - (5) mapped internally to ``float32``
          - (6) mapped internally to ``uint8``
  
      Parameters
      ----------
      images : (N,H,W,[C]) ndarray or list of (H,W,[C]) ndarray
          Array of the images to resize.
          Usually recommended to be of dtype ``uint8``.
  
      sizes : float or iterable of int or iterable of float
          The new size of the images, given either as a fraction (a single
          float) or as a ``(height, width)`` ``tuple`` of two integers or as a
          ``(height fraction, width fraction)`` ``tuple`` of two floats.
  
      interpolation : None or str or int, optional
          The interpolation to use during resize.
          If ``int``, then expected to be one of:
  
              * ``cv2.INTER_NEAREST`` (nearest neighbour interpolation)
              * ``cv2.INTER_LINEAR`` (linear interpolation)
              * ``cv2.INTER_AREA`` (area interpolation)
              * ``cv2.INTER_CUBIC`` (cubic interpolation)
  
          If ``str``, then expected to be one of:
  
              * ``nearest`` (identical to ``cv2.INTER_NEAREST``)
              * ``linear`` (identical to ``cv2.INTER_LINEAR``)
              * ``area`` (identical to ``cv2.INTER_AREA``)
              * ``cubic`` (identical to ``cv2.INTER_CUBIC``)
  
          If ``None``, the interpolation will be chosen automatically. For size
          increases, ``area`` interpolation will be picked and for size
          decreases, ``linear`` interpolation will be picked.
  
      Returns
      -------
      (N,H',W',[C]) ndarray
          Array of the resized images.
  
      Examples
      --------
      >>> import imgaug as ia
      >>> images = np.zeros((2, 8, 16, 3), dtype=np.uint8)
      >>> images_resized = ia.imresize_many_images(images, 2.0)
      >>> images_resized.shape
      (2, 16, 32, 3)
  
      Convert two RGB images of height ``8`` and width ``16`` to images of
      height ``2*8=16`` and width ``2*16=32``.
  
      >>> images_resized = ia.imresize_many_images(images, (2.0, 4.0))
      >>> images_resized.shape
      (2, 16, 64, 3)
  
      Convert two RGB images of height ``8`` and width ``16`` to images of
      height ``2*8=16`` and width ``4*16=64``.
  
      >>> images_resized = ia.imresize_many_images(images, (16, 32))
      >>> images_resized.shape
      (2, 16, 32, 3)
  
      Converts two RGB images of height ``8`` and width ``16`` to images of
      height ``16`` and width ``32``.
  
      """
      # pylint: disable=too-many-statements
  
      # we just do nothing if the input contains zero images
      # one could also argue that an exception would be appropriate here
      if len(images) == 0:
          return images
  
      # verify that sizes contains only values >0
      if is_single_number(sizes) and sizes <= 0:
          raise ValueError(
              "If 'sizes' is given as a single number, it is expected to "
              "be >= 0, got %.8f." % (sizes,))
  
      # change after the validation to make the above error messages match the
      # original input
      if is_single_number(sizes):
          sizes = (sizes, sizes)
      else:
          assert len(sizes) == 2, (
              "If 'sizes' is given as a tuple, it is expected be a tuple of two "
              "entries, got %d entries." % (len(sizes),))
          assert all([is_single_number(val) and val >= 0 for val in sizes]), (
              "If 'sizes' is given as a tuple, it is expected be a tuple of two "
              "ints or two floats, each >= 0, got types %s with values %s." % (
                  str([type(val) for val in sizes]), str(sizes)))
  
      # if input is a list, call this function N times for N images
      # but check beforehand if all images have the same shape, then just
      # convert to a single array and de-convert afterwards
      if isinstance(images, list):
          nb_shapes = len({image.shape for image in images})
          if nb_shapes == 1:
              return list(imresize_many_images(
                  np.array(images), sizes=sizes, interpolation=interpolation))
  
          return [
              imresize_many_images(
                  image[np.newaxis, ...],
                  sizes=sizes,
                  interpolation=interpolation)[0, ...]
              for image in images]
  
      shape = images.shape
      assert images.ndim in [3, 4], "Expected array of shape (N, H, W, [C]), " \
                                    "got shape %s" % (str(shape),)
      nb_images = shape[0]
      height_image, width_image = shape[1], shape[2]
      nb_channels = shape[3] if images.ndim > 3 else None
  
      height_target, width_target = sizes[0], sizes[1]
      height_target = (int(np.round(height_image * height_target))
                       if is_single_float(height_target)
                       else height_target)
      width_target = (int(np.round(width_image * width_target))
                      if is_single_float(width_target)
                      else width_target)
  
      if height_target == height_image and width_target == width_image:
          return np.copy(images)
  
      # return empty array if input array contains zero-sized axes
      # note that None==0 is not True (for case nb_channels=None)
      if 0 in [height_target, width_target, nb_channels]:
          shape_out = tuple([shape[0], height_target, width_target]
                            + list(shape[3:]))
          return np.zeros(shape_out, dtype=images.dtype)
  
      # place this after the (h==h' and w==w') check so that images with
      # zero-sized don't result in errors if the aren't actually resized
      # verify that all input images have height/width > 0
      has_zero_size_axes = any([axis == 0 for axis in images.shape[1:]])
      assert not has_zero_size_axes, (
          "Cannot resize images, because at least one image has a height and/or "
          "width and/or number of channels of zero. "
          "Observed shapes were: %s." % (
              str([image.shape for image in images]),))
  
      inter = interpolation
      assert inter is None or inter in IMRESIZE_VALID_INTERPOLATIONS, (
          "Expected 'interpolation' to be None or one of %s. Got %s." % (
              ", ".join(
                  [str(valid_ip) for valid_ip in IMRESIZE_VALID_INTERPOLATIONS]
              ),
              str(inter)
          )
      )
      if inter is None:
          if height_target > height_image or width_target > width_image:
              inter = cv2.INTER_AREA
          else:
              inter = cv2.INTER_LINEAR
      elif inter in ["nearest", cv2.INTER_NEAREST]:
          inter = cv2.INTER_NEAREST
      elif inter in ["linear", cv2.INTER_LINEAR]:
          inter = cv2.INTER_LINEAR
      elif inter in ["area", cv2.INTER_AREA]:
          inter = cv2.INTER_AREA
      else:  # if ip in ["cubic", cv2.INTER_CUBIC]:
          inter = cv2.INTER_CUBIC
  
      # TODO find more beautiful way to avoid circular imports
      from . import dtypes as iadt
      if inter == cv2.INTER_NEAREST:
          iadt.gate_dtypes(
              images,
              allowed=["bool",
                       "uint8", "uint16",
                       "int8", "int16", "int32",
                       "float16", "float32", "float64"],
              disallowed=["uint32", "uint64", "uint128", "uint256",
                          "int64", "int128", "int256",
                          "float96", "float128", "float256"],
              augmenter=None)
      else:
          iadt.gate_dtypes(
              images,
              allowed=["bool",
                       "uint8", "uint16",
                       "int8", "int16",
                       "float16", "float32", "float64"],
              disallowed=["uint32", "uint64", "uint128", "uint256",
                          "int32", "int64", "int128", "int256",
                          "float96", "float128", "float256"],
              augmenter=None)
  
      result_shape = (nb_images, height_target, width_target)
      if nb_channels is not None:
          result_shape = result_shape + (nb_channels,)
      result = np.zeros(result_shape, dtype=images.dtype)
      for i, image in enumerate(images):
          input_dtype = image.dtype
          input_dtype_name = input_dtype.name
  
          if input_dtype_name == "bool":
              image = image.astype(np.uint8) * 255
          elif input_dtype_name == "int8" and inter != cv2.INTER_NEAREST:
              image = image.astype(np.int16)
          elif input_dtype_name == "float16":
              image = image.astype(np.float32)
  
          if nb_channels is not None and nb_channels > 512:
              channels = [
                  cv2.resize(image[..., c], (width_target, height_target),
                             interpolation=inter) for c in sm.xrange(nb_channels)]
              result_img = np.stack(channels, axis=-1)
          else:
              result_img = cv2.resize(
                  image, (width_target, height_target), interpolation=inter)
  
          assert result_img.dtype.name == image.dtype.name, (
              "Expected cv2.resize() to keep the input dtype '%s', but got "
              "'%s'. This is an internal error. Please report." % (
                  image.dtype.name, result_img.dtype.name
              )
          )
  
          # cv2 removes the channel axis if input was (H, W, 1)
          # we re-add it (but only if input was not (H, W))
          if (len(result_img.shape) == 2 and nb_channels is not None
                  and nb_channels == 1):
              result_img = result_img[:, :, np.newaxis]
  
          if input_dtype_name == "bool":
              result_img = result_img > 127
          elif input_dtype_name == "int8" and inter != cv2.INTER_NEAREST:
              # TODO somehow better avoid circular imports here
              from . import dtypes as iadt
              result_img = iadt.restore_dtypes_(result_img, np.int8)
          elif input_dtype_name == "float16":
              # TODO see above
              from . import dtypes as iadt
              result_img = iadt.restore_dtypes_(result_img, np.float16)
          result[i] = result_img
      return result
  
  
  def _assert_two_or_three_dims(shape):
      if hasattr(shape, "shape"):
          shape = shape.shape
      assert len(shape) in [2, 3], (
          "Expected image with two or three dimensions, but got %d dimensions "
          "and shape %s." % (len(shape), shape))
  
  
  def imresize_single_image(image, sizes, interpolation=None):
      """Resize a single image.
  
      dtype support::
  
          See :func:`~imgaug.imgaug.imresize_many_images`.
  
      Parameters
      ----------
      image : (H,W,C) ndarray or (H,W) ndarray
          Array of the image to resize.
          Usually recommended to be of dtype ``uint8``.
  
      sizes : float or iterable of int or iterable of float
          See :func:`~imgaug.imgaug.imresize_many_images`.
  
      interpolation : None or str or int, optional
          See :func:`~imgaug.imgaug.imresize_many_images`.
  
      Returns
      -------
      (H',W',C) ndarray or (H',W') ndarray
          The resized image.
  
      """
      _assert_two_or_three_dims(image)
  
      grayscale = False
      if image.ndim == 2:
          grayscale = True
          image = image[:, :, np.newaxis]
  
      rs = imresize_many_images(
          image[np.newaxis, :, :, :], sizes, interpolation=interpolation)
      if grayscale:
          return rs[0, :, :, 0]
      return rs[0, ...]
  
  
  def pool(arr, block_size, func, pad_mode="constant", pad_cval=0,
           preserve_dtype=True, cval=None):
      """Resize an array by pooling values within blocks.
  
      dtype support::
  
          * ``uint8``: yes; fully tested
          * ``uint16``: yes; tested
          * ``uint32``: yes; tested (2)
          * ``uint64``: no (1)
          * ``int8``: yes; tested
          * ``int16``: yes; tested
          * ``int32``: yes; tested (2)
          * ``int64``: no (1)
          * ``float16``: yes; tested
          * ``float32``: yes; tested
          * ``float64``: yes; tested
          * ``float128``: yes; tested (2)
          * ``bool``: yes; tested
  
          - (1) results too inaccurate (at least when using np.average as func)
          - (2) Note that scikit-image documentation says that the wrapped
                pooling function converts inputs to ``float64``. Actual tests
                showed no indication of that happening (at least when using
                preserve_dtype=True).
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray
          Image-like array to pool. Ideally of datatype ``float64``.
  
      block_size : int or tuple of int
          Spatial size of each group of values to pool, aka kernel size.
  
            * If a single ``int``, then a symmetric block of that size along
              height and width will be used.
            * If a ``tuple`` of two values, it is assumed to be the block size
              along height and width of the image-like, with pooling happening
              per channel.
            * If a ``tuple`` of three values, it is assumed to be the block size
              along height, width and channels.
  
      func : callable
          Function to apply to a given block in order to convert it to a single
          number, e.g. :func:`numpy.average`, :func:`numpy.min`,
          :func:`numpy.max`.
  
      pad_mode : str, optional
          Padding mode to use if the array cannot be divided by `block_size`
          without remainder. See :func:`~imgaug.imgaug.pad` for details.
  
      pad_cval : number, optional
          Value to use for padding if `mode` is ``constant``.
          See :func:`numpy.pad` for details.
  
      preserve_dtype : bool, optional
          Whether to convert the array back to the input datatype if it is
          changed away from that in the pooling process.
  
      cval : None or number, optional
          Deprecated. Old name for `pad_cval`.
  
      Returns
      -------
      (H',W') ndarray or (H',W',C') ndarray
          Array after pooling.
  
      """
      # TODO find better way to avoid circular import
      from . import dtypes as iadt
      from .augmenters import size as iasize
  
      if arr.size == 0:
          return np.copy(arr)
  
      iadt.gate_dtypes(arr,
                       allowed=["bool",
                                "uint8", "uint16", "uint32",
                                "int8", "int16", "int32",
                                "float16", "float32", "float64", "float128"],
                       disallowed=["uint64", "uint128", "uint256",
                                   "int64", "int128", "int256",
                                   "float256"],
                       augmenter=None)
  
      if cval is not None:
          warn_deprecated("`cval` is a deprecated argument in pool(). "
                          "Use `pad_cval` instead.")
          pad_cval = cval
  
      _assert_two_or_three_dims(arr)
  
      is_valid_int = is_single_integer(block_size) and block_size >= 1
      is_valid_tuple = is_iterable(block_size) and len(block_size) in [2, 3] \
          and [is_single_integer(val) and val >= 1 for val in block_size]
      assert is_valid_int or is_valid_tuple, (
          "Expected argument 'block_size' to be a single integer >0 or "
          "a tuple of 2 or 3 values with each one being >0. Got %s." % (
              str(block_size)))
  
      if is_single_integer(block_size):
          block_size = [block_size, block_size]
      if len(block_size) < arr.ndim:
          block_size = list(block_size) + [1]
  
      # We use custom padding here instead of the one from block_reduce(),
      # because (1) it is expected to be faster and (2) it allows us more
      # flexibility wrt to padding modes.
      arr = iasize.pad_to_multiples_of(
          arr,
          height_multiple=block_size[0],
          width_multiple=block_size[1],
          mode=pad_mode,
          cval=pad_cval
      )
  
      input_dtype = arr.dtype
  
      arr_reduced = skimage.measure.block_reduce(arr, tuple(block_size), func,
                                                 cval=cval)
      if preserve_dtype and arr_reduced.dtype.name != input_dtype.name:
          arr_reduced = arr_reduced.astype(input_dtype)
      return arr_reduced
  
  
  # TODO does OpenCV have a faster avg pooling method?
  def avg_pool(arr, block_size, pad_mode="reflect", pad_cval=128,
               preserve_dtype=True, cval=None):
      """Resize an array using average pooling.
  
      Defaults to ``pad_mode="reflect"`` to ensure that padded values do not
      affect the average.
  
      dtype support::
  
          See :func:`~imgaug.imgaug.pool`.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray
          Image-like array to pool.
          See :func:`~imgaug.imgaug.pool` for details.
  
      block_size : int or tuple of int or tuple of int
          Size of each block of values to pool.
          See :func:`~imgaug.imgaug.pool` for details.
  
      pad_mode : str, optional
          Padding mode to use if the array cannot be divided by `block_size`
          without remainder.
          See :func:`~imgaug.imgaug.pad` for details.
  
      pad_cval : number, optional
          Padding value.
          See :func:`~imgaug.imgaug.pool` for details.
  
      preserve_dtype : bool, optional
          Whether to preserve the input array dtype.
          See  :func:`~imgaug.imgaug.pool` for details.
  
      cval : None or number, optional
          Deprecated. Old name for `pad_cval`.
  
      Returns
      -------
      (H',W') ndarray or (H',W',C') ndarray
          Array after average pooling.
  
      """
      return pool(arr, block_size, np.average, pad_mode=pad_mode,
                  pad_cval=pad_cval, preserve_dtype=preserve_dtype, cval=cval)
  
  
  def max_pool(arr, block_size, pad_mode="edge", pad_cval=0,
               preserve_dtype=True, cval=None):
      """Resize an array using max-pooling.
  
      Defaults to ``pad_mode="edge"`` to ensure that padded values do not affect
      the maximum, even if the dtype was something else than ``uint8``.
  
      dtype support::
  
          See :func:`~imgaug.imgaug.pool`.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray
          Image-like array to pool.
          See :func:`~imgaug.imgaug.pool` for details.
  
      block_size : int or tuple of int or tuple of int
          Size of each block of values to pool.
          See :func:`~imgaug.imgaug.pool` for details.
  
      pad_mode : str, optional
          Padding mode to use if the array cannot be divided by `block_size`
          without remainder.
          See :func:`~imgaug.imgaug.pad` for details.
  
      pad_cval : number, optional
          Padding value.
          See :func:`~imgaug.imgaug.pool` for details.
  
      preserve_dtype : bool, optional
          Whether to preserve the input array dtype.
          See  :func:`~imgaug.imgaug.pool` for details.
  
      cval : None or number, optional
          Deprecated. Old name for `pad_cval`.
  
      Returns
      -------
      (H',W') ndarray or (H',W',C') ndarray
          Array after max-pooling.
  
      """
      return pool(arr, block_size, np.max, pad_mode=pad_mode,
                  pad_cval=pad_cval, preserve_dtype=preserve_dtype, cval=cval)
  
  
  def min_pool(arr, block_size, pad_mode="edge", pad_cval=255,
               preserve_dtype=True):
      """Resize an array using min-pooling.
  
      Defaults to ``pad_mode="edge"`` to ensure that padded values do not affect
      the minimum, even if the dtype was something else than ``uint8``.
  
      dtype support::
  
          See :func:`~imgaug.imgaug.pool`.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray
          Image-like array to pool.
          See :func:`~imgaug.imgaug.pool` for details.
  
      block_size : int or tuple of int or tuple of int
          Size of each block of values to pool.
          See :func:`~imgaug.imgaug.pool` for details.
  
      pad_mode : str, optional
          Padding mode to use if the array cannot be divided by `block_size`
          without remainder.
          See :func:`~imgaug.imgaug.pad` for details.
  
      pad_cval : number, optional
          Padding value.
          See :func:`~imgaug.imgaug.pool` for details.
  
      preserve_dtype : bool, optional
          Whether to preserve the input array dtype.
          See  :func:`~imgaug.imgaug.pool` for details.
  
      Returns
      -------
      (H',W') ndarray or (H',W',C') ndarray
          Array after min-pooling.
  
      """
      return pool(arr, block_size, np.min, pad_mode=pad_mode, pad_cval=pad_cval,
                  preserve_dtype=preserve_dtype)
  
  
  def median_pool(arr, block_size, pad_mode="reflect", pad_cval=128,
                  preserve_dtype=True):
      """Resize an array using median-pooling.
  
      Defaults to ``pad_mode="reflect"`` to ensure that padded values do not
      affect the average.
  
      dtype support::
  
          See :func:`~imgaug.imgaug.pool`.
  
      Parameters
      ----------
      arr : (H,W) ndarray or (H,W,C) ndarray
          Image-like array to pool.
          See :func:`~imgaug.imgaug.pool` for details.
  
      block_size : int or tuple of int or tuple of int
          Size of each block of values to pool.
          See :func:`~imgaug.imgaug.pool` for details.
  
      pad_mode : str, optional
          Padding mode to use if the array cannot be divided by `block_size`
          without remainder.
          See :func:`~imgaug.imgaug.pad` for details.
  
      pad_cval : number, optional
          Padding value.
          See :func:`~imgaug.imgaug.pool` for details.
  
      preserve_dtype : bool, optional
          Whether to preserve the input array dtype.
          See  :func:`~imgaug.imgaug.pool` for details.
  
      Returns
      -------
      (H',W') ndarray or (H',W',C') ndarray
          Array after min-pooling.
  
      """
      return pool(arr, block_size, np.median, pad_mode=pad_mode,
                  pad_cval=pad_cval, preserve_dtype=preserve_dtype)
  
  
  def draw_grid(images, rows=None, cols=None):
      """Combine multiple images into a single grid-like image.
  
      Calling this function with four images of the same shape and ``rows=2``,
      ``cols=2`` will combine the four images to a single image array of shape
      ``(2*H, 2*W, C)``, where ``H`` is the height of any of the images
      (analogous ``W``) and ``C`` is the number of channels of any image.
  
      Calling this function with four images of the same shape and ``rows=4``,
      ``cols=1`` is analogous to calling :func:`numpy.vstack` on the images.
  
      dtype support::
  
          * ``uint8``: yes; fully tested
          * ``uint16``: yes; fully tested
          * ``uint32``: yes; fully tested
          * ``uint64``: yes; fully tested
          * ``int8``: yes; fully tested
          * ``int16``: yes; fully tested
          * ``int32``: yes; fully tested
          * ``int64``: yes; fully tested
          * ``float16``: yes; fully tested
          * ``float32``: yes; fully tested
          * ``float64``: yes; fully tested
          * ``float128``: yes; fully tested
          * ``bool``: yes; fully tested
  
      Parameters
      ----------
      images : (N,H,W,3) ndarray or iterable of (H,W,3) array
          The input images to convert to a grid.
  
      rows : None or int, optional
          The number of rows to show in the grid.
          If ``None``, it will be automatically derived.
  
      cols : None or int, optional
          The number of cols to show in the grid.
          If ``None``, it will be automatically derived.
  
      Returns
      -------
      (H',W',3) ndarray
          Image of the generated grid.
  
      """
      nb_images = len(images)
      assert nb_images > 0, "Expected to get at least one image, got none."
  
      if is_np_array(images):
          assert images.ndim == 4, (
              "Expected to get an array of four dimensions denoting "
              "(N, H, W, C), got %d dimensions and shape %s." % (
                  images.ndim, images.shape))
      else:
          assert is_iterable(images), (
              "Expected to get an iterable of ndarrays, "
              "got %s." % (type(images),))
          assert all([is_np_array(image) for image in images]), (
              "Expected to get an iterable of ndarrays, "
              "got types %s." % (
                  ", ".join([str(type(image)) for image in images],)))
          assert all([image.ndim == 3 for image in images]), (
              "Expected to get images with three dimensions. Got shapes %s." % (
                  ", ".join([str(image.shape) for image in images])))
          assert len({image.dtype.name for image in images}) == 1, (
              "Expected to get images with the same dtypes, got dtypes %s." % (
                  ", ".join([image.dtype.name for image in images])))
          assert len({image.shape[-1] for image in images}) == 1, (
              "Expected to get images with the same number of channels, "
              "got shapes %s." % (
                  ", ".join([str(image.shape) for image in images])))
  
      cell_height = max([image.shape[0] for image in images])
      cell_width = max([image.shape[1] for image in images])
      nb_channels = images[0].shape[2]
  
      if rows is None and cols is None:
          rows = cols = int(math.ceil(math.sqrt(nb_images)))
      elif rows is not None:
          cols = int(math.ceil(nb_images / rows))
      elif cols is not None:
          rows = int(math.ceil(nb_images / cols))
      assert rows * cols >= nb_images, (
          "Expected rows*cols to lead to at least as many cells as there were "
          "images provided, but got %d rows, %d cols (=%d cells) for %d "
          "images. " % (rows, cols, rows*cols, nb_images))
  
      width = cell_width * cols
      height = cell_height * rows
      dtype = images.dtype if is_np_array(images) else images[0].dtype
      grid = np.zeros((height, width, nb_channels), dtype=dtype)
      cell_idx = 0
      for row_idx in sm.xrange(rows):
          for col_idx in sm.xrange(cols):
              if cell_idx < nb_images:
                  image = images[cell_idx]
                  cell_y1 = cell_height * row_idx
                  cell_y2 = cell_y1 + image.shape[0]
                  cell_x1 = cell_width * col_idx
                  cell_x2 = cell_x1 + image.shape[1]
                  grid[cell_y1:cell_y2, cell_x1:cell_x2, :] = image
              cell_idx += 1
  
      return grid
  
  
  def show_grid(images, rows=None, cols=None):
      """Combine multiple images into a single image and plot the result.
  
      This will show a window of the results of :func:`~imgaug.imgaug.draw_grid`.
  
      dtype support::
  
          minimum of (
              :func:`~imgaug.imgaug.draw_grid`,
              :func:`~imgaug.imgaug.imshow`
          )
  
      Parameters
      ----------
      images : (N,H,W,3) ndarray or iterable of (H,W,3) array
          See :func:`~imgaug.imgaug.draw_grid`.
  
      rows : None or int, optional
          See :func:`~imgaug.imgaug.draw_grid`.
  
      cols : None or int, optional
          See :func:`~imgaug.imgaug.draw_grid`.
  
      """
      grid = draw_grid(images, rows=rows, cols=cols)
      imshow(grid)
  
  
  def imshow(image, backend=IMSHOW_BACKEND_DEFAULT):
      """Show an image in a window.
  
      dtype support::
  
          * ``uint8``: yes; not tested
          * ``uint16``: ?
          * ``uint32``: ?
          * ``uint64``: ?
          * ``int8``: ?
          * ``int16``: ?
          * ``int32``: ?
          * ``int64``: ?
          * ``float16``: ?
          * ``float32``: ?
          * ``float64``: ?
          * ``float128``: ?
          * ``bool``: ?
  
      Parameters
      ----------
      image : (H,W,3) ndarray
          Image to show.
  
      backend : {'matplotlib', 'cv2'}, optional
          Library to use to show the image. May be either matplotlib or
          OpenCV ('cv2'). OpenCV tends to be faster, but apparently causes more
          technical issues.
  
      """
      assert backend in ["matplotlib", "cv2"], (
          "Expected backend 'matplotlib' or 'cv2', got %s." % (backend,))
  
      if backend == "cv2":
          image_bgr = image
          if image.ndim == 3 and image.shape[2] in [3, 4]:
              image_bgr = image[..., 0:3][..., ::-1]
  
          win_name = "imgaug-default-window"
          cv2.namedWindow(win_name, cv2.WINDOW_NORMAL)
          cv2.imshow(win_name, image_bgr)
          cv2.waitKey(0)
          cv2.destroyWindow(win_name)
      else:
          # import only when necessary (faster startup; optional dependency;
          # less fragile -- see issue #225)
          import matplotlib.pyplot as plt
  
          dpi = 96
          h, w = image.shape[0] / dpi, image.shape[1] / dpi
          # if the figure is too narrow, the footer may appear and make the fig
          # suddenly wider (ugly)
          w = max(w, 6)
  
          fig, ax = plt.subplots(figsize=(w, h), dpi=dpi)
          fig.canvas.set_window_title("imgaug.imshow(%s)" % (image.shape,))
          # cmap=gray is automatically only activate for grayscale images
          ax.imshow(image, cmap="gray")
          plt.show()
  
  
  def do_assert(condition, message="Assertion failed."):
      """Assert that a ``condition`` holds or raise an ``Exception`` otherwise.
  
      This was added because `assert` statements are removed in optimized code.
      It replaced `assert` statements throughout the library, but that was
      reverted again for readability and performance reasons.
  
      Parameters
      ----------
      condition : bool
          If ``False``, an exception is raised.
  
      message : str, optional
          Error message.
  
      """
      if not condition:
          raise AssertionError(str(message))
  
  
  def _normalize_cv2_input_arr_(arr):
      flags = arr.flags
      if not flags["OWNDATA"]:
          arr = np.copy(arr)
          flags = arr.flags
      if not flags["C_CONTIGUOUS"]:
          arr = np.ascontiguousarray(arr)
      return arr
  
  
  def apply_lut(image, table):
      """Map an input image to a new one using a lookup table.
  
      dtype support::
  
          See :func:`~imgaug.imgaug.apply_lut_`.
  
      Parameters
      ----------
      image : ndarray
          See :func:`~imgaug.imgaug.apply_lut_`.
  
      table : ndarray or list of ndarray
          See :func:`~imgaug.imgaug.apply_lut_`.
  
      Returns
      -------
      ndarray
          Image after mapping via lookup table.
  
      """
      return apply_lut_(np.copy(image), table)
  
  
  # TODO make this function compatible with short max sized images, probably
  #      isn't right now
  def apply_lut_(image, table):
      """Map an input image in-place to a new one using a lookup table.
  
      dtype support::
  
          * ``uint8``: yes; fully tested
          * ``uint16``: no
          * ``uint32``: no
          * ``uint64``: no
          * ``int8``: no
          * ``int16``: no
          * ``int32``: no
          * ``int64``: no
          * ``float16``: no
          * ``float32``: no
          * ``float64``: no
          * ``float128``: no
          * ``bool``: no
  
      Parameters
      ----------
      image : ndarray
          Image of dtype ``uint8`` and shape ``(H,W)`` or ``(H,W,C)``.
  
      table : ndarray or list of ndarray
          Table of dtype ``uint8`` containing the mapping from old to new
          values. Either a ``list`` of ``C`` ``(256,)`` arrays or a single
          array of shape ``(256,)`` or ``(256, C)`` or ``(1, 256, C)``.
          In case of ``(256,)`` the same table is used for all channels,
          otherwise a channelwise table is used and ``C`` is expected to match
          the number of channels.
  
      Returns
      -------
      ndarray
          Image after mapping via lookup table.
          This *might* be the same array instance as provided via `image`.
  
      """
  
      image_shape_orig = image.shape
      nb_channels = 1 if len(image_shape_orig) == 2 else image_shape_orig[-1]
  
      if 0 in image_shape_orig:
          return image
  
      image = _normalize_cv2_input_arr_(image)
  
      # [(256,), (256,), ...] => (256, C)
      if isinstance(table, list):
          assert len(table) == nb_channels, (
              "Expected to get %d tables (one per channel), got %d instead." % (
                  nb_channels, len(table)))
          table = np.stack(table, axis=-1)
  
      # (256, C) => (1, 256, C)
      if table.shape == (256, nb_channels):
          table = table[np.newaxis, :, :]
  
      assert table.shape == (256,) or table.shape == (1, 256, nb_channels), (
          "Expected 'table' to be any of the following: "
          "A list of C (256,) arrays, an array of shape (256,), an array of "
          "shape (256, C), an array of shape (1, 256, C). Transformed 'table' "
          "up to shape %s for image with shape %s (C=%d)." % (
              table.shape, image_shape_orig, nb_channels))
  
      if nb_channels > 512:
          if table.shape == (256,):
              table = np.tile(table[np.newaxis, :, np.newaxis],
                              (1, 1, nb_channels))
  
          subluts = []
          for group_idx in np.arange(int(np.ceil(nb_channels / 512))):
              c_start = group_idx * 512
              c_end = c_start + 512
              subluts.append(apply_lut_(image[:, :, c_start:c_end],
                                        table[:, :, c_start:c_end]))
  
          return np.concatenate(subluts, axis=2)
  
      assert image.dtype.name == "uint8", (
          "Expected uint8 image, got dtype %s." % (image.dtype.name,))
      assert table.dtype.name == "uint8", (
          "Expected uint8 table, got dtype %s." % (table.dtype.name,))
  
      image = cv2.LUT(image, table, dst=image)
      return image
  
  
  class HooksImages(object):
      """Class to intervene with image augmentation runs.
  
      This is e.g. useful to dynamically deactivate some augmenters.
  
      Parameters
      ----------
      activator : None or callable, optional
          A function that gives permission to execute an augmenter.
          The expected interface is::
  
              ``f(images, augmenter, parents, default)``
  
          where ``images`` are the input images to augment, ``augmenter`` is the
          instance of the augmenter to execute, ``parents`` are previously
          executed augmenters and ``default`` is an expected default value to be
          returned if the activator function does not plan to make a decision
          for the given inputs.
  
      propagator : None or callable, optional
          A function that gives permission to propagate the augmentation further
          to the children of an augmenter. This happens after the activator.
          In theory, an augmenter may augment images itself (if allowed by the
          activator) and then execute child augmenters afterwards (if allowed by
          the propagator). If the activator returned ``False``, the propagation
          step will never be executed.
          The expected interface is::
  
              ``f(images, augmenter, parents, default)``
  
          with all arguments having identical meaning to the activator.
  
      preprocessor : None or callable, optional
          A function to call before an augmenter performed any augmentations.
          The interface is:
  
              ``f(images, augmenter, parents)``
  
          with all arguments having identical meaning to the activator.
          It is expected to return the input images, optionally modified.
  
      postprocessor : None or callable, optional
          A function to call after an augmenter performed augmentations.
          The interface is the same as for the `preprocessor`.
  
      Examples
      --------
      >>> import numpy as np
      >>> import imgaug as ia
      >>> import imgaug.augmenters as iaa
      >>> seq = iaa.Sequential([
      >>>     iaa.GaussianBlur(3.0, name="blur"),
      >>>     iaa.Dropout(0.05, name="dropout"),
      >>>     iaa.Affine(translate_px=-5, name="affine")
      >>> ])
      >>> images = [np.zeros((10, 10), dtype=np.uint8)]
      >>>
      >>> def activator(images, augmenter, parents, default):
      >>>     return False if augmenter.name in ["blur", "dropout"] else default
      >>>
      >>> seq_det = seq.to_deterministic()
      >>> images_aug = seq_det.augment_images(images)
      >>> heatmaps = [np.random.rand(*(3, 10, 10))]
      >>> heatmaps_aug = seq_det.augment_images(
      >>>     heatmaps,
      >>>     hooks=ia.HooksImages(activator=activator)
      >>> )
  
      This augments images and their respective heatmaps in the same way.
      The heatmaps however are only modified by ``Affine``, not by
      ``GaussianBlur`` or ``Dropout``.
  
      """
  
      def __init__(self, activator=None, propagator=None, preprocessor=None,
                   postprocessor=None):
          self.activator = activator
          self.propagator = propagator
          self.preprocessor = preprocessor
          self.postprocessor = postprocessor
  
      def is_activated(self, images, augmenter, parents, default):
          """Estimate whether an augmenter may be executed.
  
          This also affects propagation of data to child augmenters.
  
          Returns
          -------
          bool
              If ``True``, the augmenter may be executed.
              Otherwise ``False``.
  
          """
          if self.activator is None:
              return default
          return self.activator(images, augmenter, parents, default)
  
      def is_propagating(self, images, augmenter, parents, default):
          """Estimate whether an augmenter may call its children.
  
          This function decides whether an augmenter with children is allowed
          to call these in order to further augment the inputs.
          Note that if the augmenter itself performs augmentations (before/after
          calling its children), these may still be executed, even if this
          method returns ``False``.
  
          Returns
          -------
          bool
              If ``True``, the augmenter may propagate data to its children.
              Otherwise ``False``.
  
          """
          if self.propagator is None:
              return default
          return self.propagator(images, augmenter, parents, default)
  
      def preprocess(self, images, augmenter, parents):
          """Preprocess input data per augmenter before augmentation.
  
          Returns
          -------
          (N,H,W,C) ndarray or (N,H,W) ndarray or list of (H,W,C) ndarray or list of (H,W) ndarray
              The input images, optionally modified.
  
          """
          if self.preprocessor is None:
              return images
          return self.preprocessor(images, augmenter, parents)
  
      def postprocess(self, images, augmenter, parents):
          """Postprocess input data per augmenter after augmentation.
  
          Returns
          -------
          (N,H,W,C) ndarray or (N,H,W) ndarray or list of (H,W,C) ndarray or list of (H,W) ndarray
              The input images, optionally modified.
  
          """
          if self.postprocessor is None:
              return images
          return self.postprocessor(images, augmenter, parents)
  
  
  class HooksHeatmaps(HooksImages):
      """Class to intervene with heatmap augmentation runs.
  
      This is e.g. useful to dynamically deactivate some augmenters.
  
      This class is currently the same as the one for images. This may or may
      not change in the future.
  
      """
  
  
  class HooksKeypoints(HooksImages):
      """Class to intervene with keypoint augmentation runs.
  
      This is e.g. useful to dynamically deactivate some augmenters.
  
      This class is currently the same as the one for images. This may or may
      not change in the future.
  
      """
  
  
  #####################################################################
  # Create classes/functions that were moved to other files and create
  # DeprecatedWarnings when they are called.
  #####################################################################
  
  def _mark_moved_class_or_function(class_name_old, module_name_new,
                                    class_name_new):
      # pylint: disable=redefined-outer-name
      class_name_new = (class_name_new
                        if class_name_new is not None
                        else class_name_old)
  
      def _func(*args, **kwargs):
          import importlib
          warn_deprecated(
              "Using imgaug.imgaug.%s is deprecated. Use %s.%s instead." % (
                  class_name_old, module_name_new, class_name_new
              ))
          module = importlib.import_module(module_name_new)
          return getattr(module, class_name_new)(*args, **kwargs)
  
      return _func
  
  
  MOVED = [
      ("Keypoint", "imgaug.augmentables.kps", None),
      ("KeypointsOnImage", "imgaug.augmentables.kps", None),
      ("BoundingBox", "imgaug.augmentables.bbs", None),
      ("BoundingBoxesOnImage", "imgaug.augmentables.bbs", None),
      ("Polygon", "imgaug.augmentables.polys", None),
      ("PolygonsOnImage", "imgaug.augmentables.polys", None),
      ("MultiPolygon", "imgaug.augmentables.polys", None),
      ("_ConcavePolygonRecoverer", "imgaug.augmentables.polys", None),
      ("HeatmapsOnImage", "imgaug.augmentables.heatmaps", None),
      ("SegmentationMapsOnImage", "imgaug.augmentables.segmaps", None),
      ("Batch", "imgaug.augmentables.batches", None),
      ("BatchLoader", "imgaug.multicore", None),
      ("BackgroundAugmenter", "imgaug.multicore", None),
      ("compute_geometric_median", "imgaug.augmentables.kps", None),
      ("_convert_points_to_shapely_line_string", "imgaug.augmentables.polys",
       None),
      ("_interpolate_point_pair", "imgaug.augmentables.polys", None),
      ("_interpolate_points", "imgaug.augmentables.polys", None),
      ("_interpolate_points_by_max_distance", "imgaug.augmentables.polys", None),
      ("pad", "imgaug.augmenters.size", None),
      ("pad_to_aspect_ratio", "imgaug.augmenters.size", None),
      ("pad_to_multiples_of", "imgaug.augmenters.size", None),
      ("compute_paddings_for_aspect_ratio", "imgaug.augmenters.size",
       "compute_paddings_to_reach_aspect_ratio"),
      ("compute_paddings_to_reach_multiples_of", "imgaug.augmenters.size", None),
      ("compute_paddings_to_reach_exponents_of", "imgaug.augmenters.size", None)
  ]
  
  for class_name_old, module_name_new, class_name_new in MOVED:
      locals()[class_name_old] = _mark_moved_class_or_function(
          class_name_old, module_name_new, class_name_new)
  
apis: numpy.ceil numpy.stack numpy.floor numpy.pad numpy.copy numpy.arange numpy.clip numpy.log numpy.float128 numpy.round numpy.concatenate numpy.full numpy.array numpy.atleast_3d
apis: numpy.tile numpy.ceil numpy.zeros numpy.dot numpy.round numpy.stack numpy.concatenate numpy.float32 numpy.asarray numpy.copy matplotlib.pyplot.subplots matplotlib.pyplot.show numpy.array numpy.ascontiguousarray numpy.isscalar numpy.linalg.norm