diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c2aa71f..99a3e45d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,8 +69,8 @@ * Changed `_ConcavePolygonRecoverer` to raise warnings instead of throwing exceptions when the underlying search for segment intersection points crashes. #338 -* Added the library to ``conda-forge`` so it can now be installed via - ``conda install imgaug`` (provided the conda-forge channel was added +* Added the library to `conda-forge` so it can now be installed via + `conda install imgaug` (provided the conda-forge channel was added before that). #320 #339 * Changed dependency `opencv-python` to `opencv-python-headless`. This should improve support for some system without GUIs. @@ -148,7 +148,42 @@ similar colors using k-Means clustering. #347 * Added a check script for `UniformColorQuantization` under `checks/check_uniform_color_quantization.py`. #347 - +* Added `imgaug.imgaug.normalize_random_state()`. #348 +* Added `imgaug.augmenters.segmentation._ensure_image_max_size()`. #348 +* Added `imgaug.augmenters.segmentation.PointsSamplerIf`. An interface for + classes used for sampling (usually random) coordinate arrays on images. +* Added `imgaug.augmenters.segmentation._verify_sample_points_images()`. #348 +* Added `imgaug.augmenters.segmentation.RegularGridPointsSampler`. A class + used to generate regular grids of `rows x columns` points on images. #348 +* Added `imgaug.augmenters.segmentation.RelativeRegularGridPointsSampler`. + Similar to `RegularGridPointsSampler`, but number of rows/columns is set + as fractions of image sizes, leading to more rows/columns for larger + images. #348 +* Added `imgaug.augmenters.segmentation.DropoutPointsSampler`. A class + used to randomly drop `p` percent of all coordinates sampled by another + another points sampler. #348 +* Added `imgaug.augmenters.segmentation.UniformPointsSampler`. A class used + to sample `N` points on each image with y-/x-coordinates uniformly sampled + using the corresponding image height/width. #348 +* Added `imgaug.augmenters.segmentation.SubsamplingPointsSampler`. A class + that ensures that another points sampler does not produce more than + `N` points by subsampling a random subset of the produced points if `N` + is exceeded. #348 +* Added `imgaug.augmenters.segmentation.segment_voronoi()`. A function that + converts an image into a voronoi image, i.e. averages the colors within + voronoi cells placed on the image. #348 + * Also added in the same module the functions + `_match_pixels_with_voronoi_cells()`, `_generate_pixel_coords()`, + `_compute_avg_segment_colors()`, `_render_segments()`. +* Added `imgaug.augmenters.segmentation.Voronoi`. An augmenter that converts + an image to a voronoi image. #348 + * Added a check script for `Voronoi` in `checks/check_voronoi.py`. +* Added `imgaug.augmenters.segmentation.UniformVoronoi`, a shortcut for + `Voronoi(UniformPointsSamper)`. #348 +* Added `imgaug.augmenters.segmentation.RegularGridVoronoi`, a shortcut for + `Voronoi(DropoutPointsSampler(RegularGridPointsSampler))`. #348 +* Added `imgaug.augmenters.segmentation.RelativeRegularGridVoronoi`, a shortcut + for `Voronoi(DropoutPointsSampler(RelativeRegularGridPointsSampler))`. #348 ## Fixes diff --git a/checks/check_voronoi.py b/checks/check_voronoi.py new file mode 100644 index 000000000..93be666f7 --- /dev/null +++ b/checks/check_voronoi.py @@ -0,0 +1,34 @@ +from __future__ import print_function, division + +import numpy as np + +import imgaug as ia +import imgaug.augmenters as iaa + + +def main(): + image = ia.quokka_square((256, 256)) + + reggrid_sampler = iaa.DropoutPointsSampler( + iaa.RegularGridPointsSampler(n_rows=50, n_cols=50), + 0.5 + ) + uniform_sampler = iaa.UniformPointsSampler(50*50) + + augs = [ + iaa.Voronoi(points_sampler=reggrid_sampler, p_replace=1.0, + max_size=128), + iaa.Voronoi(points_sampler=uniform_sampler, p_replace=1.0, + max_size=128), + iaa.UniformVoronoi(50*50, p_replace=1.0, max_size=128), + iaa.RegularGridVoronoi(50, 50, p_drop_points=0.4, p_replace=1.0, + max_size=128), + ] + + images = [aug(image=image) for aug in augs] + + ia.imshow(np.hstack(images)) + + +if __name__ == "__main__": + main() diff --git a/imgaug/augmenters/arithmetic.py b/imgaug/augmenters/arithmetic.py index 14166d3e0..981714614 100644 --- a/imgaug/augmenters/arithmetic.py +++ b/imgaug/augmenters/arithmetic.py @@ -1294,6 +1294,7 @@ def Dropout(p=0, per_channel=False, active for ``50`` percent of all images. """ + # TODO add list as an option if ia.is_single_number(p): p2 = iap.Binomial(1 - p) elif ia.is_iterable(p): diff --git a/imgaug/augmenters/segmentation.py b/imgaug/augmenters/segmentation.py index 0fac2672f..77fcc39f6 100644 --- a/imgaug/augmenters/segmentation.py +++ b/imgaug/augmenters/segmentation.py @@ -15,12 +15,20 @@ List of augmenters: * Superpixels + * Voronoi """ from __future__ import print_function, division, absolute_import +from abc import ABCMeta, abstractmethod + import numpy as np -from skimage import segmentation, measure +# use skimage.segmentation instead from ... import segmentation here, +# because otherwise unittest seems to mix up imgaug.augmenters.segmentation +# with skimage.segmentation for whatever reason +import skimage.segmentation +import skimage.measure +import six import six.moves as sm from . import meta @@ -29,6 +37,42 @@ from .. import dtypes as iadt +# TODO merge this into imresize? +def _ensure_image_max_size(image, max_size, interpolation): + """Ensure that images do not exceed a required maximum sidelength. + + This downscales to `max_size` if any side violates that maximum. + The other side is downscaled too so that the aspect ratio is maintained. + + dtype support:: + + See :func:`imgaug.imgaug.imresize_single_image`. + + Parameters + ---------- + image : ndarray + Image to potentially downscale. + + max_size : int + Maximum length of any side of the image. + + interpolation : string or int + See :func:`imgaug.imgaug.imresize_single_image`. + + """ + if max_size is not None: + size = max(image.shape[0], image.shape[1]) + if size > max_size: + resize_factor = max_size / size + new_height = int(image.shape[0] * resize_factor) + new_width = int(image.shape[1] * resize_factor) + image = ia.imresize_single_image( + image, + (new_height, new_width), + interpolation=interpolation) + return image + + # TODO add compactness parameter class Superpixels(meta.Augmenter): """ @@ -64,24 +108,25 @@ class Superpixels(meta.Augmenter): minimum of ( ``imgaug.augmenters.segmentation.Superpixels(image size <= max_size)``, - :func:`imgaug.imgaug.imresize_many_images` + :func:`imgaug.augmenters.segmentation._ensure_image_max_size` ) Parameters ---------- p_replace : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional - Defines for any superpixel the probability that the pixels within - it are replaced by their average color (otherwise, the pixels are not - changed). Examples: + Defines for any segment the probability that the pixels within that + segment are replaced by their average color (otherwise, the pixels + are not changed). + Examples: * A probability of ``0.0`` would mean, that the pixels in no - superpixel are replaced by their average color (image is not + segment are replaced by their average color (image is not changed at all). * A probability of ``0.5`` would mean, that around half of all - superpixels are replaced by their average color. - * A probability of ``1.0`` would mean, that all superpixels are - replaced by their average color (resulting in a standard - superpixel image). + segments are replaced by their average color. + * A probability of ``1.0`` would mean, that all segments are + replaced by their average color (resulting in a voronoi + image). Behaviour based on chosen datatypes for this parameter: @@ -92,7 +137,7 @@ class Superpixels(meta.Augmenter): image. * If a ``StochasticParameter``, it is expected to return values between ``0.0`` and ``1.0`` and will be queried *for each - individual superpixel* to determine whether it is supposed to + individual segment* to determine whether it is supposed to be averaged (``>0.5``) or not (``<=0.5``). Recommended to be some form of ``Binomial(...)``. @@ -112,13 +157,14 @@ class Superpixels(meta.Augmenter): queried to draw one value per image. max_size : int or None, optional - Maximum image size at which the superpixels are generated. + Maximum image size at which the augmentation is performed. If the width or height of an image exceeds this value, it will be - downscaled for the superpixel detection so that the longest side + downscaled before the augmentation so that the longest side matches `max_size`. - This is done to speed up the superpixel algorithm. The final output - (superpixel) image has the same size as the input image. - Use ``None`` to apply no downscaling. + This is done to speed up the process. The final output image has the + same size as the input image. Note that in case `p_replace` is below + ``1.0``, the down-/upscaling will affect the not-replaced pixels too. + Use ``None`` to apply no down-/upscaling. interpolation : int or str, optional Interpolation method to use during downscaling when `max_size` is @@ -204,10 +250,9 @@ def _augment_images(self, images, random_state, parents, hooks): image = images[i] orig_shape = image.shape - image = self._ensure_max_size( - image, self.max_size, self.interpolation) + image = _ensure_image_max_size(image, self.max_size, self.interpolation) - segments = segmentation.slic( + segments = skimage.segmentation.slic( image, n_segments=n_segments_samples[i], compactness=10) image_aug = self._replace_segments(image, segments, replace_samples) @@ -221,20 +266,6 @@ def _augment_images(self, images, random_state, parents, hooks): images[i] = image_aug return images - @classmethod - def _ensure_max_size(cls, image, max_size, interpolation): - if max_size is not None: - size = max(image.shape[0], image.shape[1]) - if size > max_size: - resize_factor = max_size / size - new_height = int(image.shape[0] * resize_factor) - new_width = int(image.shape[1] * resize_factor) - image = ia.imresize_single_image( - image, - (new_height, new_width), - interpolation=interpolation) - return image - @classmethod def _replace_segments(cls, image, segments, replace_samples): min_value, _center_value, max_value = \ @@ -245,7 +276,7 @@ def _replace_segments(cls, image, segments, replace_samples): for c in sm.xrange(nb_channels): # segments+1 here because otherwise regionprops always # misses the last label - regions = measure.regionprops( + regions = skimage.measure.regionprops( segments+1, intensity_image=image[..., c]) for ridx, region in enumerate(regions): # with mod here, because slic can sometimes create more @@ -283,3 +314,1323 @@ def _augment_keypoints(self, keypoints_on_images, random_state, parents, def get_parameters(self): return [self.p_replace, self.n_segments, self.max_size, self.interpolation] + + +# TODO don't average the alpha channel for RGBA? +def segment_voronoi(image, cell_coordinates, replace_mask=None): + """Average colors within voronoi cells of an image. + + Parameters + ---------- + image : ndarray + The image to convert to a voronoi image. May be ``HxW`` or + ``HxWxC``. Note that for ``RGBA`` images the alpha channel + will currently also by averaged. + + cell_coordinates : ndarray + A ``Nx2`` float array containing the center coordinates of voronoi + cells on the image. Values are expected to be in the interval + ``[0.0, height-1.0]`` for the y-axis (x-axis analogous). + If this array contains no coordinate, the image will not be + changed. + + replace_mask : None or ndarray, optional + Boolean mask of the same length as `cell_coordinates`, denoting + for each cell whether its pixels are supposed to be replaced + by the cell's average color (``True``) or left untouched (``False``). + If this is set to ``None``, all cells will be replaced. + + Returns + ------- + ndarray + Voronoi image. + + """ + input_dims = image.ndim + if input_dims == 2: + image = image[..., np.newaxis] + + if len(cell_coordinates) <= 0: + if input_dims == 2: + return image[..., 0] + return image + + height, width = image.shape[0:2] + pixel_coords, ids_of_nearest_cells = \ + _match_pixels_with_voronoi_cells(height, width, cell_coordinates) + cell_colors = _compute_avg_segment_colors( + image, pixel_coords, ids_of_nearest_cells, + len(cell_coordinates)) + + image_aug = _render_segments(image, ids_of_nearest_cells, cell_colors, + replace_mask) + + if input_dims == 2: + return image_aug[..., 0] + return image_aug + + +def _match_pixels_with_voronoi_cells(height, width, cell_coordinates): + # deferred import so that scipy is an optional dependency + from scipy.spatial import cKDTree as KDTree # TODO add scipy for reqs + tree = KDTree(cell_coordinates) + pixel_coords = _generate_pixel_coords(height, width) + pixel_coords_subpixel = pixel_coords.astype(np.float32) + 0.5 + ids_of_nearest_cells = tree.query(pixel_coords_subpixel)[1] + return pixel_coords, ids_of_nearest_cells + + +def _generate_pixel_coords(height, width): + xx, yy = np.meshgrid(np.arange(width), np.arange(height)) + return np.c_[xx.ravel(), yy.ravel()] + + +def _compute_avg_segment_colors(image, pixel_coords, ids_of_nearest_segments, + nb_segments): + nb_channels = image.shape[2] + cell_colors = np.zeros((nb_segments, nb_channels), dtype=np.float64) + cell_counters = np.zeros((nb_segments,), dtype=np.uint32) + + # TODO vectorize + for pixel_coord, id_of_nearest_cell in zip(pixel_coords, + ids_of_nearest_segments): + # pixel_coord is (x,y), so we have to swap it to access the HxW image + pixel_coord_yx = pixel_coord[::-1] + cell_colors[id_of_nearest_cell] += image[tuple(pixel_coord_yx)] + cell_counters[id_of_nearest_cell] += 1 + + # cells without associated pixels can have a count of 0, we clip + # here to 1 as the result for these cells doesn't matter + cell_counters = np.clip(cell_counters, 1, None) + + cell_colors = cell_colors / cell_counters[:, np.newaxis] + + return cell_colors.astype(np.uint8) + + +def _render_segments(image, ids_of_nearest_segments, avg_segment_colors, + replace_mask): + ids_of_nearest_segments = np.copy(ids_of_nearest_segments) + height, width, nb_channels = image.shape + + # without replace_mask we could reduce this down to: + # data = cell_colors[ids_of_nearest_cells, :].reshape( + # (width, height, 3)) + # data = np.transpose(data, (1, 0, 2)) + + keep_mask = (~replace_mask) if replace_mask is not None else None + if keep_mask is None or not np.any(keep_mask): + data = avg_segment_colors[ids_of_nearest_segments, :] + else: + ids_to_keep = np.nonzero(keep_mask)[0] + indices_to_keep = np.where( + np.isin(ids_of_nearest_segments, ids_to_keep))[0] + data = avg_segment_colors[ids_of_nearest_segments, :] + + image_data = image.reshape((height*width, -1)) + data[indices_to_keep] = image_data[indices_to_keep, :] + data = data.reshape((height, width, nb_channels)) + return data + + +# TODO this can be reduced down to a similar problem as Superpixels: +# generate an integer-based class id map of segments, then replace all +# segments with the same class id by the average color within that +# segment +class Voronoi(meta.Augmenter): + """Average colors of an image within Voronoi cells. + + This augmenter performs the following steps: + + 1. Query `points_sampler` to sample random coordinates of cell + centers. On the image. + 2. Estimate for each pixel to which voronoi cell (i.e. segment) + it belongs. Each pixel belongs to the cell with the closest center + coordinate (euclidean distance). + 3. Compute for each cell the average color of the pixels within it. + 4. Replace the pixels of `p_replace` percent of all cells by their + average color. Do not change the pixels of ``(1 - p_replace)`` + percent of all cells. (The percentages are average values over + many images. Some images may get more/less cells replaced by + their average color.) + + This code is very loosely based on + https://codegolf.stackexchange.com/questions/50299/draw-an-image-as-a-voronoi-map/50345#50345 + + dtype support:: + + if (image size <= max_size):: + + * ``uint8``: yes; fully tested + * ``uint16``: no; not tested + * ``uint32``: no; not tested + * ``uint64``: no; not tested + * ``int8``: no; not tested + * ``int16``: no; not tested + * ``int32``: no; not tested + * ``int64``: no; not tested + * ``float16``: no; not tested + * ``float32``: no; not tested + * ``float64``: no; not tested + * ``float128``: no; not tested + * ``bool``: no; not tested + + if (image size > max_size):: + + minimum of ( + ``imgaug.augmenters.segmentation.Voronoi(image size <= max_size)``, + :func:`imgaug.augmenters.segmentation._ensure_image_max_size` + ) + + Parameters + ---------- + points_sampler : PointSamplerIf + A points sampler which will be queried per image to generate the + coordinates of the centers of voronoi cells. + + p_replace : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Defines for any segment the probability that the pixels within that + segment are replaced by their average color (otherwise, the pixels + are not changed). + Examples: + + * A probability of ``0.0`` would mean, that the pixels in no + segment are replaced by their average color (image is not + changed at all). + * A probability of ``0.5`` would mean, that around half of all + segments are replaced by their average color. + * A probability of ``1.0`` would mean, that all segments are + replaced by their average color (resulting in a voronoi + image). + + Behaviour based on chosen datatypes for this parameter: + + * If a number, then that number will always be used. + * If tuple ``(a, b)``, then a random probability will be sampled + from the interval ``[a, b]`` per image. + * If a list, then a random value will be sampled from that list per + image. + * If a ``StochasticParameter``, it is expected to return + values between ``0.0`` and ``1.0`` and will be queried *for each + individual segment* to determine whether it is supposed to + be averaged (``>0.5``) or not (``<=0.5``). + Recommended to be some form of ``Binomial(...)``. + + max_size : int or None, optional + Maximum image size at which the augmentation is performed. + If the width or height of an image exceeds this value, it will be + downscaled before the augmentation so that the longest side + matches `max_size`. + This is done to speed up the process. The final output image has the + same size as the input image. Note that in case `p_replace` is below + ``1.0``, the down-/upscaling will affect the not-replaced pixels too. + Use ``None`` to apply no down-/upscaling. + + interpolation : int or str, optional + Interpolation method to use during downscaling when `max_size` is + exceeded. Valid methods are the same as in + :func:`imgaug.imgaug.imresize_single_image`. + + name : None or str, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + deterministic : bool, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + random_state : None or int or numpy.random.RandomState, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + Examples + -------- + >>> import imgaug.augmenters as iaa + >>> points_sampler = iaa.RegularGridPointsSampler(n_cols=10, n_rows=20) + >>> aug = iaa.Voronoi(points_sampler) + + Creates an augmenter that places a ``10x20`` (``HxW``) grid of cells on + the image and replaces all pixels within each cell by the cell's average + color. The process is performed at an image size not exceeding 128px on + any side. If necessary, the downscaling is performed using linear + interpolation. + + >>> import imgaug.augmenters as iaa + >>> points_sampler = iaa.DropoutPointsSampler( + >>> iaa.RelativeRegularGridPointsSampler( + >>> n_cols_frac=(0.01, 0.1), + >>> n_rows_frac=0.1), + >>> 0.2) + >>> aug = iaa.Voronoi(points_sampler, p_replace=0.9, max_size=None) + + Creates a voronoi augmenter that generates a grid of cells dynamically + adapted to the image size. Larger images get more cells. On the x-axis, + the distance between two cells is ``w * W`` pixels, where ``W`` is the + width of the image and ``w`` is always ``0.1``. On the y-axis, + the distance between two cells is ``h * H`` pixels, where ``H`` is the + height of the image and ``h`` is sampled uniformly from the interval + ``[0.01, 0.1]``. To make the voronoi pattern less regular, about ``20`` + percent of the cell coordinates are randomly dropped (i.e. the remaining + cells grow in size). In contrast to the first example, the image is not + resized (if it was, the sampling would happen *after* the resizing, + which would affect ``W`` and ``H``). Not all voronoi cells are replaced + by their average color, only around ``90`` percent of them. The + remaining ``10`` percent's pixels remain unchanged. + + """ + def __init__(self, points_sampler, p_replace=1.0, max_size=128, + interpolation="linear", + name=None, deterministic=False, random_state=None): + super(Voronoi, self).__init__( + name=name, deterministic=deterministic, random_state=random_state) + + assert isinstance(points_sampler, PointsSamplerIf) + self.points_sampler = points_sampler + + self.p_replace = iap.handle_probability_param( + p_replace, "p_replace", tuple_to_uniform=True, list_to_choice=True) + + self.max_size = max_size + self.interpolation = interpolation + + def _augment_images(self, images, random_state, parents, hooks): + iadt.gate_dtypes(images, + allowed=["uint8"], + disallowed=["bool", + "uint16", "uint32", "uint64", "uint128", + "uint256", + "int8", "int16", "int32", "int64", + "int128", "int256", + "float16", "float32", "float64", + "float96", "float128", "float256"], + augmenter=self) + + rss = ia.derive_random_states(random_state, len(images)) + for i, (image, rs) in enumerate(zip(images, rss)): + images[i] = self._augment_single_image(image, rs) + return images + + def _augment_single_image(self, image, random_state): + rss = ia.derive_random_states(random_state, 2) + orig_shape = image.shape + image = _ensure_image_max_size(image, self.max_size, self.interpolation) + + cell_coordinates = self.points_sampler.sample_points([image], rss[0])[0] + p_replace = self.p_replace.draw_samples((len(cell_coordinates),), + rss[1]) + replace_mask = (p_replace > 0.5) + + image_aug = segment_voronoi(image, cell_coordinates, replace_mask) + + if orig_shape != image_aug.shape: + image_aug = ia.imresize_single_image( + image_aug, + orig_shape[0:2], + interpolation=self.interpolation) + + return image_aug + + def _augment_heatmaps(self, heatmaps, random_state, parents, hooks): + # pylint: disable=no-self-use + return heatmaps + + def _augment_keypoints(self, keypoints_on_images, random_state, parents, + hooks): + # pylint: disable=no-self-use + return keypoints_on_images + + def get_parameters(self): + return [self.points_sampler, self.p_replace, self.max_size, + self.interpolation] + + +class UniformVoronoi(Voronoi): + """Uniformly sample Voronoi cells on images and average colors within them. + + This augmenter is a shortcut for the combination of ``Voronoi`` with + ``UniformPointsSampler``. Hence, it generates a fixed amount of ``N`` + random coordinates of voronoi cells on each image. The cell coordinates + are sampled uniformly using the image height and width as maxima. + + dtype support:: + + See ``imgaug.augmenters.segmentation.Voronoi``. + + Parameters + ---------- + n_points : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Number of points to sample on each image. + + * If a single int, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the discrete interval + ``[a..b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + p_replace : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Defines for any segment the probability that the pixels within that + segment are replaced by their average color (otherwise, the pixels + are not changed). + Examples: + + * A probability of ``0.0`` would mean, that the pixels in no + segment are replaced by their average color (image is not + changed at all). + * A probability of ``0.5`` would mean, that around half of all + segments are replaced by their average color. + * A probability of ``1.0`` would mean, that all segments are + replaced by their average color (resulting in a voronoi + image). + + Behaviour based on chosen datatypes for this parameter: + + * If a number, then that number will always be used. + * If tuple ``(a, b)``, then a random probability will be sampled + from the interval ``[a, b]`` per image. + * If a list, then a random value will be sampled from that list per + image. + * If a ``StochasticParameter``, it is expected to return + values between ``0.0`` and ``1.0`` and will be queried *for each + individual segment* to determine whether it is supposed to + be averaged (``>0.5``) or not (``<=0.5``). + Recommended to be some form of ``Binomial(...)``. + + max_size : int or None, optional + Maximum image size at which the augmentation is performed. + If the width or height of an image exceeds this value, it will be + downscaled before the augmentation so that the longest side + matches `max_size`. + This is done to speed up the process. The final output image has the + same size as the input image. Note that in case `p_replace` is below + ``1.0``, the down-/upscaling will affect the not-replaced pixels too. + Use ``None`` to apply no down-/upscaling. + + interpolation : int or str, optional + Interpolation method to use during downscaling when `max_size` is + exceeded. Valid methods are the same as in + :func:`imgaug.imgaug.imresize_single_image`. + + name : None or str, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + deterministic : bool, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + random_state : None or int or numpy.random.RandomState, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + Examples + -------- + >>> import imgaug.augmenters as iaa + >>> aug = iaa.UniformVoronoi((100, 500)) + + Samples for each image uniformly the number of voronoi cells ``N`` from the + interval ``[100, 500]``. Then generates ``N`` coordinates by sampling + uniformly the x-coordinates from ``[0, W]`` and the y-coordinates from + ``[0, H]``, where ``H`` is the image height and ``W`` the image width. + Then uses these coordinates to group the image pixels into voronoi + cells and averages the colors within them. The process is performed at an + image size not exceeding 128px on any side. If necessary, the downscaling + is performed using linear interpolation. + + >>> import imgaug.augmenters as iaa + >>> aug = iaa.UniformVoronoi(250, p_replace=0.9, max_size=None) + + Same as above, but always samples ``N=250`` cells, replaces only + ``90`` percent of them with their average color (the pixels of the + remaining ``10`` percent are not changed) and performs the transformation + at the original image size. + + """ + def __init__(self, n_points, p_replace=1.0, max_size=128, + interpolation="linear", + name=None, deterministic=False, random_state=None): + super(UniformVoronoi, self).__init__( + points_sampler=UniformPointsSampler(n_points), + p_replace=p_replace, + max_size=max_size, + interpolation=interpolation, + name=name, + deterministic=deterministic, + random_state=random_state + ) + + +class RegularGridVoronoi(Voronoi): + """Sample Voronoi cells from regular grids and color-average them. + + This augmenter is a shortcut for the combination of ``Voronoi``, + ``RegularGridPointsSampler`` and ``DropoutPointsSampler``. Hence, it + generates a regular grid with ``R`` rows and ``C`` columns of coordinates + on each image. Then, it drops ``p`` percent of the ``R*C`` coordinates + to randomize the grid. Each image pixel then belongs to the voronoi + cell with the closest coordinate. + + dtype support:: + + See ``imgaug.augmenters.segmentation.Voronoi``. + + Parameters + ---------- + n_rows : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Number of rows of coordinates to place on each image, i.e. the number + of coordinates on the y-axis. Note that for each image, the sampled + value is clipped to the interval ``[1..H]``, where ``H`` is the image + height. + + * If a single int, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the discrete interval + ``[a..b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + n_cols : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Number of columns of coordinates to place on each image, i.e. the number + of coordinates on the x-axis. Note that for each image, the sampled + value is clipped to the interval ``[1..W]``, where ``W`` is the image + width. + + * If a single int, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the discrete interval + ``[a..b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + p_drop_points : number or tuple of number or imgaug.parameters.StochasticParameter, optional + The probability that a coordinate will be removed from the list + of all sampled coordinates. A value of ``1.0`` would mean that (on + average) ``100`` percent of all coordinates will be dropped, + while ``0.0`` denotes ``0`` percent. Note that this sampler will + always ensure that at least one coordinate is left after the dropout + operation, i.e. even ``1.0`` will only drop all *except one* + coordinate. + + * If a float, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value ``p`` will be sampled from + the interval ``[a, b]`` per image. + * If a ``StochasticParameter``, then this parameter will be used to + determine per coordinate whether it should be *kept* (sampled + value of ``>0.5``) or shouldn't be kept (sampled value of + ``<=0.5``). If you instead want to provide the probability as + a stochastic parameter, you can usually do + ``imgaug.parameters.Binomial(1-p)`` to convert parameter `p` to + a 0/1 representation. + + p_replace : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Defines for any segment the probability that the pixels within that + segment are replaced by their average color (otherwise, the pixels + are not changed). + Examples: + + * A probability of ``0.0`` would mean, that the pixels in no + segment are replaced by their average color (image is not + changed at all). + * A probability of ``0.5`` would mean, that around half of all + segments are replaced by their average color. + * A probability of ``1.0`` would mean, that all segments are + replaced by their average color (resulting in a voronoi + image). + + Behaviour based on chosen datatypes for this parameter: + + * If a number, then that number will always be used. + * If tuple ``(a, b)``, then a random probability will be sampled + from the interval ``[a, b]`` per image. + * If a list, then a random value will be sampled from that list per + image. + * If a ``StochasticParameter``, it is expected to return + values between ``0.0`` and ``1.0`` and will be queried *for each + individual segment* to determine whether it is supposed to + be averaged (``>0.5``) or not (``<=0.5``). + Recommended to be some form of ``Binomial(...)``. + + max_size : int or None, optional + Maximum image size at which the augmentation is performed. + If the width or height of an image exceeds this value, it will be + downscaled before the augmentation so that the longest side + matches `max_size`. + This is done to speed up the process. The final output image has the + same size as the input image. Note that in case `p_replace` is below + ``1.0``, the down-/upscaling will affect the not-replaced pixels too. + Use ``None`` to apply no down-/upscaling. + + interpolation : int or str, optional + Interpolation method to use during downscaling when `max_size` is + exceeded. Valid methods are the same as in + :func:`imgaug.imgaug.imresize_single_image`. + + name : None or str, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + deterministic : bool, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + random_state : None or int or numpy.random.RandomState, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + Examples + -------- + >>> import imgaug.augmenters as iaa + >>> aug = iaa.RegularGridVoronoi(10, 20) + + Places a regular grid of ``10x20`` (``height x width``) coordinates on + each image. Randomly drops on average ``20`` percent of these points + to create a less regular pattern. Then uses the remaining coordinates + to group the image pixels into voronoi cells and averages the colors + within them. The process is performed at an image size not exceeding + 128px on any side. If necessary, the downscaling is performed using + linear interpolation. + + >>> import imgaug.augmenters as iaa + >>> aug = iaa.RegularGridVoronoi( + >>> (10, 30), 20, p_drop_points=0.0, p_replace=0.9, max_size=None) + + Same as above, generates a grid with randomly ``10`` to ``30`` rows, + drops none of the generates points, replaces only ``90`` percent of + the voronoi cells with their average color (the pixels of the remaining + ``10`` percent are not changed) and performs the transformation + at the original image size. + + """ + def __init__(self, n_rows, n_cols, p_drop_points=0.4, p_replace=1.0, + max_size=128, interpolation="linear", + name=None, deterministic=False, random_state=None): + super(RegularGridVoronoi, self).__init__( + points_sampler=DropoutPointsSampler( + RegularGridPointsSampler(n_rows, n_cols), + p_drop_points + ), + p_replace=p_replace, + max_size=max_size, + interpolation=interpolation, + name=name, + deterministic=deterministic, + random_state=random_state + ) + + +class RelativeRegularGridVoronoi(Voronoi): + """Sample Voronoi cells from image-dependent grids and color-average them. + + This augmenter is a shortcut for the combination of ``Voronoi``, + ``RegularGridPointsSampler`` and ``DropoutPointsSampler``. Hence, it + generates a regular grid with ``R`` rows and ``C`` columns of coordinates + on each image. Then, it drops ``p`` percent of the ``R*C`` coordinates + to randomize the grid. Each image pixel then belongs to the voronoi + cell with the closest coordinate. + + **Note**: In contrast to the other Voronoi augmenters, this one uses + ``None`` as the default value for `max_size`, i.e. the color averaging + is always performed at full resolution. This enables the augmenter to + make most use of the added points for larger images. It does however slow + down the augmentation process. + + dtype support:: + + See ``imgaug.augmenters.segmentation.Voronoi``. + + Parameters + ---------- + n_rows_frac : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Relative number of coordinates to place on the y-axis. For a value + ``y`` and image height ``H`` the number of actually placed coordinates + (i.e. computed rows) is given by ``int(round(y*H))``. + Note that for each image, the number of coordinates is clipped to the + interval ``[1,H]``, where ``H`` is the image height. + + * If a single number, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the interval + ``[a, b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + n_cols_frac : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Relative number of coordinates to place on the x-axis. For a value + ``x`` and image height ``W`` the number of actually placed coordinates + (i.e. computed columns) is given by ``int(round(x*W))``. + Note that for each image, the number of coordinates is clipped to the + interval ``[1,W]``, where ``W`` is the image width. + + * If a single number, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the interval + ``[a, b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + p_drop_points : number or tuple of number or imgaug.parameters.StochasticParameter, optional + The probability that a coordinate will be removed from the list + of all sampled coordinates. A value of ``1.0`` would mean that (on + average) ``100`` percent of all coordinates will be dropped, + while ``0.0`` denotes ``0`` percent. Note that this sampler will + always ensure that at least one coordinate is left after the dropout + operation, i.e. even ``1.0`` will only drop all *except one* + coordinate. + + * If a float, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value ``p`` will be sampled from + the interval ``[a, b]`` per image. + * If a ``StochasticParameter``, then this parameter will be used to + determine per coordinate whether it should be *kept* (sampled + value of ``>0.5``) or shouldn't be kept (sampled value of + ``<=0.5``). If you instead want to provide the probability as + a stochastic parameter, you can usually do + ``imgaug.parameters.Binomial(1-p)`` to convert parameter `p` to + a 0/1 representation. + + p_replace : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Defines for any segment the probability that the pixels within that + segment are replaced by their average color (otherwise, the pixels + are not changed). + Examples: + + * A probability of ``0.0`` would mean, that the pixels in no + segment are replaced by their average color (image is not + changed at all). + * A probability of ``0.5`` would mean, that around half of all + segments are replaced by their average color. + * A probability of ``1.0`` would mean, that all segments are + replaced by their average color (resulting in a voronoi + image). + + Behaviour based on chosen datatypes for this parameter: + + * If a number, then that number will always be used. + * If tuple ``(a, b)``, then a random probability will be sampled + from the interval ``[a, b]`` per image. + * If a list, then a random value will be sampled from that list per + image. + * If a ``StochasticParameter``, it is expected to return + values between ``0.0`` and ``1.0`` and will be queried *for each + individual segment* to determine whether it is supposed to + be averaged (``>0.5``) or not (``<=0.5``). + Recommended to be some form of ``Binomial(...)``. + + max_size : int or None, optional + Maximum image size at which the augmentation is performed. + If the width or height of an image exceeds this value, it will be + downscaled before the augmentation so that the longest side + matches `max_size`. + This is done to speed up the process. The final output image has the + same size as the input image. Note that in case `p_replace` is below + ``1.0``, the down-/upscaling will affect the not-replaced pixels too. + Use ``None`` to apply no down-/upscaling. + + interpolation : int or str, optional + Interpolation method to use during downscaling when `max_size` is + exceeded. Valid methods are the same as in + :func:`imgaug.imgaug.imresize_single_image`. + + name : None or str, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + deterministic : bool, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + random_state : None or int or numpy.random.RandomState, optional + See :func:`imgaug.augmenters.meta.Augmenter.__init__`. + + Examples + -------- + >>> import imgaug.augmenters as iaa + >>> aug = iaa.RelativeRegularGridVoronoi(0.01, 0.1) + + Places a regular grid of ``R x C`` coordinates on each image, where + ``R`` is the number of rows and computed as ``R=0.01*H`` with ``H`` being + the height of the input image. ``C`` is the number of columns and + analogously estimated from the image width ``W`` as ``C=0.1*W``. + Larger images will lead to larger ``R`` and ``C`` values. + On average, ``20`` percent of these grid coordinates are randomly + dropped to create a less regular pattern. Then, the remaining coordinates + are used to group the image pixels into voronoi cells and the colors + within them are averaged. + + >>> import imgaug.augmenters as iaa + >>> aug = iaa.RelativeRegularGridVoronoi( + >>> (0.01, 0.1), 0.1, p_drop_points=0.0, p_replace=0.9, max_size=512) + + Same as above, generates a grid with randomly ``R=r*H`` rows, where + ``r`` is sampled uniformly from the interval ``[0.01, 0.1]`` and + ``C=0.1*W`` rows. No points are dropped. The augmenter replaces only + ``90`` percent of the voronoi cells with their average color (the pixels + of the remaining ``10`` percent are not changed). Images larger than + ``512px`` are temporarily downscaled (*before* sampling the grid points) + so that no side exceeds ``512px``. This improves performance, but + degrades the quality of the resulting image. + + """ + + def __init__(self, n_rows_frac, n_cols_frac, p_drop_points=0.4, + p_replace=1.0, max_size=None, interpolation="linear", + name=None, deterministic=False, random_state=None): + super(RelativeRegularGridVoronoi, self).__init__( + points_sampler=DropoutPointsSampler( + RelativeRegularGridPointsSampler(n_rows_frac, n_cols_frac), + p_drop_points + ), + p_replace=p_replace, + max_size=max_size, + interpolation=interpolation, + name=name, + deterministic=deterministic, + random_state=random_state + ) + + +@six.add_metaclass(ABCMeta) +class PointsSamplerIf(object): + """Interface for all point samplers. + + Point samplers return coordinate arrays of shape ``Nx2``. + These coordinates can be used in other augmenters, see e.g. ``Voronoi``. + + """ + + @abstractmethod + def sample_points(self, images, random_state): + """Generate coordinates of points on images. + + Parameters + ---------- + images : ndarray or list of ndarray + One or more images for which to generate points. + If this is a list of arrays, each one of them is expected to + have three dimensions. + If this is an array, it must be four-dimensional and the first + axis is expected to denote the image index. For ``RGB`` images + the array would hence have to be of shape ``(N, H, W, 3)``. + + random_state : None or numpy.random.RandomState or int or float + A random state to use for any probabilistic function required + during the point sampling. + See :func:`imgaug.imgaug.normalize_random_state` for details. + + Returns + ------- + ndarray + An ``(N,2)`` ``float32`` array containing ``(x,y)`` subpixel + coordinates, all of which being within the intervals + ``[0.0, width]`` and ``[0.0, height]``. + + """ + + +def _verify_sample_points_images(images): + assert len(images) > 0, "Expected at least one image, got zero." + if isinstance(images, list): + assert all([ia.is_np_array(image) for image in images]), ( + "Expected list of numpy arrays, got list of types %s." % ( + ", ".join([str(type(image)) for image in images]),)) + assert all([image.ndim == 3 for image in images]), ( + "Expected each image to have three dimensions, " + "got dimensions %s." % ( + ", ".join([str(image.ndim) for image in images]),)) + else: + assert ia.is_np_array(images), ( + "Expected either a list of numpy arrays or a single numpy " + "array of shape NxHxWxC. Got type %s." % (type(images),)) + assert images.ndim == 4, ( + "Expected a four-dimensional array of shape NxHxWxC. " + "Got shape %d dimensions (shape: %s)." % ( + images.ndim, images.shape)) + + +class RegularGridPointsSampler(PointsSamplerIf): + """Sampler that generates a regular grid of coordinates on an image. + + 'Regular grid' here means that on each axis all coordinates have the + same distance from each other. Note that the distance may change between + axis. + + Parameters + ---------- + n_rows : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Number of rows of coordinates to place on each image, i.e. the number + of coordinates on the y-axis. Note that for each image, the sampled + value is clipped to the interval ``[1..H]``, where ``H`` is the image + height. + + * If a single int, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the discrete interval + ``[a..b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + n_cols : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Number of columns of coordinates to place on each image, i.e. the number + of coordinates on the x-axis. Note that for each image, the sampled + value is clipped to the interval ``[1..W]``, where ``W`` is the image + width. + + * If a single int, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the discrete interval + ``[a..b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + Examples + -------- + >>> import imgaug.augmenters as iaa + >>> sampler = iaa.RegularGridPointsSampler( + >>> n_rows=(5, 20), + >>> n_cols=50) + + Creates a point sampler that generates regular grids of points. These grids + contain ``r`` points on the y-axis, where ``r`` is sampled + uniformly from the discrete interval ``[5..20]`` per image. + On the x-axis, the grids always contain ``50`` points. + + """ + + def __init__(self, n_rows, n_cols): + self.n_rows = iap.handle_discrete_param( + n_rows, "n_rows", value_range=(1, None), + tuple_to_uniform=True, list_to_choice=True, allow_floats=False) + self.n_cols = iap.handle_discrete_param( + n_cols, "n_cols", value_range=(1, None), + tuple_to_uniform=True, list_to_choice=True, allow_floats=False) + + def sample_points(self, images, random_state): + random_state = ia.normalize_random_state(random_state) + _verify_sample_points_images(images) + + n_rows_lst, n_cols_lst = self._draw_samples(images, random_state) + return self._generate_point_grids(images, n_rows_lst, n_cols_lst) + + def _draw_samples(self, images, random_state): + rss = ia.derive_random_states(random_state, 2) + n_rows_lst = self.n_rows.draw_samples(len(images), random_state=rss[0]) + n_cols_lst = self.n_cols.draw_samples(len(images), random_state=rss[1]) + return self._clip_rows_and_cols(n_rows_lst, n_cols_lst, images) + + @classmethod + def _clip_rows_and_cols(cls, n_rows_lst, n_cols_lst, images): + heights = np.int32([image.shape[0] for image in images]) + widths = np.int32([image.shape[1] for image in images]) + # We clip intentionally not to H-1 or W-1 here. If e.g. an image has + # a width of 1, we want to get a maximum of 1 column of coordinates. + n_rows_lst = np.clip(n_rows_lst, 1, heights) + n_cols_lst = np.clip(n_cols_lst, 1, widths) + return n_rows_lst, n_cols_lst + + @classmethod + def _generate_point_grids(cls, images, n_rows_lst, n_cols_lst): + grids = [] + for image, n_rows_i, n_cols_i in zip(images, n_rows_lst, n_cols_lst): + grids.append(cls._generate_point_grid(image, n_rows_i, n_cols_i)) + return grids + + @classmethod + def _generate_point_grid(cls, image, n_rows, n_cols): + height, width = image.shape[0:2] + + # We do not have to subtract 1 here from height/width as these are + # subpixel coordinates. Technically, we could also place the cell + # centers outside of the image plane. + if n_rows == 1: + yy = np.float32([float(height)/2]) + else: + yy = np.linspace(0.0, height, num=n_rows) + + if n_cols == 1: + xx = np.float32([float(width)/2]) + else: + xx = np.linspace(0.0, width, num=n_cols) + + xx, yy = np.meshgrid(xx, yy) + grid = np.vstack([xx.ravel(), yy.ravel()]).T + return grid + + def __repr__(self): + return "RegularGridPointsSampler(%s, %s)" % (self.n_rows, self.n_cols) + + def __str__(self): + return self.__repr__() + + +class RelativeRegularGridPointsSampler(PointsSamplerIf): + """Regular grid coordinate sampler; places more points on larger images. + + This is similar to ``RegularGridPointSampler``, but the number of rows + and columns is given as fractions of each image's height and width. + Hence, more coordinates are generated for larger images. + + Parameters + ---------- + n_rows_frac : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Relative number of coordinates to place on the y-axis. For a value + ``y`` and image height ``H`` the number of actually placed coordinates + (i.e. computed rows) is given by ``int(round(y*H))``. + Note that for each image, the number of coordinates is clipped to the + interval ``[1,H]``, where ``H`` is the image height. + + * If a single number, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the interval + ``[a, b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + n_cols_frac : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Relative number of coordinates to place on the x-axis. For a value + ``x`` and image height ``W`` the number of actually placed coordinates + (i.e. computed columns) is given by ``int(round(x*W))``. + Note that for each image, the number of coordinates is clipped to the + interval ``[1,W]``, where ``W`` is the image width. + + * If a single number, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the interval + ``[a, b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + Examples + -------- + >>> import imgaug.augmenters as iaa + >>> sampler = iaa.RelativeRegularGridPointsSampler( + >>> n_rows_frac=(0.01, 0.1), + >>> n_cols_frac=0.2) + + Creates a point sampler that generates regular grids of points. These grids + contain ``round(y*H)`` points on the y-axis, where ``y`` is sampled + uniformly from the interval ``[0.01, 0.1]`` per image and ``H`` is the + image height. On the x-axis, the grids always contain ``0.2*W`` points, + where ``W`` is the image width. + + """ + + def __init__(self, n_rows_frac, n_cols_frac): + eps = 1e-4 + self.n_rows_frac = iap.handle_continuous_param( + n_rows_frac, "n_rows_frac", value_range=(0.0+eps, 1.0), + tuple_to_uniform=True, list_to_choice=True) + self.n_cols_frac = iap.handle_continuous_param( + n_cols_frac, "n_cols_frac", value_range=(0.0+eps, 1.0), + tuple_to_uniform=True, list_to_choice=True) + + def sample_points(self, images, random_state): + random_state = ia.normalize_random_state(random_state) + _verify_sample_points_images(images) + + n_rows, n_cols = self._draw_samples(images, random_state) + return RegularGridPointsSampler._generate_point_grids(images, + n_rows, n_cols) + + def _draw_samples(self, images, random_state): + n_augmentables = len(images) + rss = ia.derive_random_states(random_state, 2) + n_rows_frac = self.n_rows_frac.draw_samples(n_augmentables, + random_state=rss[0]) + n_cols_frac = self.n_cols_frac.draw_samples(n_augmentables, + random_state=rss[1]) + heights = np.int32([image.shape[0] for image in images]) + widths = np.int32([image.shape[1] for image in images]) + + n_rows = np.round(n_rows_frac * heights) + n_cols = np.round(n_cols_frac * widths) + n_rows, n_cols = RegularGridPointsSampler._clip_rows_and_cols( + n_rows, n_cols, images) + + return n_rows.astype(np.int32), n_cols.astype(np.int32) + + def __repr__(self): + return "RelativeRegularGridPointsSampler(%s, %s)" % ( + self.n_rows_frac, self.n_cols_frac) + + def __str__(self): + return self.__repr__() + + +class DropoutPointsSampler(PointsSamplerIf): + """Remove a defined fraction of sampled points. + + Parameters + ---------- + other_points_sampler : PointSamplerIf + Another point sampler that is queried to generate a list of points. + The dropout operation will be applied to that list. + + p_drop : number or tuple of number or imgaug.parameters.StochasticParameter + The probability that a coordinate will be removed from the list + of all sampled coordinates. A value of ``1.0`` would mean that (on + average) ``100`` percent of all coordinates will be dropped, + while ``0.0`` denotes ``0`` percent. Note that this sampler will + always ensure that at least one coordinate is left after the dropout + operation, i.e. even ``1.0`` will only drop all *except one* + coordinate. + + * If a float, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value ``p`` will be sampled from + the interval ``[a, b]`` per image. + * If a ``StochasticParameter``, then this parameter will be used to + determine per coordinate whether it should be *kept* (sampled + value of ``>0.5``) or shouldn't be kept (sampled value of + ``<=0.5``). If you instead want to provide the probability as + a stochastic parameter, you can usually do + ``imgaug.parameters.Binomial(1-p)`` to convert parameter `p` to + a 0/1 representation. + + Examples + -------- + >>> import imgaug.augmenters as iaa + >>> sampler = iaa.DropoutPointsSampler( + >>> iaa.RegularGridPointsSampler(10, 20), + >>> 0.2) + + Creates a point sampler that first generates points following a regular + grid of 10 rows and 20 columns, then randomly drops ``20`` percent of these + points. + + """ + + def __init__(self, other_points_sampler, p_drop): + assert isinstance(other_points_sampler, PointsSamplerIf), ( + "Expected to get an instance of PointsSamplerIf as argument " + "'other_points_sampler', got type %s." % ( + type(other_points_sampler),)) + self.other_points_sampler = other_points_sampler + self.p_drop = self._convert_p_drop_to_inverted_mask_param(p_drop) + + @classmethod + def _convert_p_drop_to_inverted_mask_param(cls, p_drop): + # TODO this is the same as in Dropout, make DRY + # TODO add list as an option + if ia.is_single_number(p_drop): + p_drop = iap.Binomial(1 - p_drop) + elif ia.is_iterable(p_drop): + assert len(p_drop) == 2 + assert p_drop[0] < p_drop[1] + assert 0 <= p_drop[0] <= 1.0 + assert 0 <= p_drop[1] <= 1.0 + p_drop = iap.Binomial(iap.Uniform(1 - p_drop[1], 1 - p_drop[0])) + elif isinstance(p_drop, iap.StochasticParameter): + pass + else: + raise Exception( + "Expected p_drop to be float or int or StochasticParameter, " + "got %s." % (type(p_drop),)) + return p_drop + + def sample_points(self, images, random_state): + random_state = ia.normalize_random_state(random_state) + _verify_sample_points_images(images) + + rss = ia.derive_random_states(random_state, 2) + points_on_images = self.other_points_sampler.sample_points(images, + rss[0]) + drop_masks = self._draw_samples(points_on_images, rss[1]) + return self._apply_dropout_masks(points_on_images, drop_masks) + + def _draw_samples(self, points_on_images, random_state): + rss = ia.derive_random_states(random_state, len(points_on_images)) + drop_masks = [self._draw_samples_for_image(points_on_image, rs) + for points_on_image, rs + in zip(points_on_images, rss)] + return drop_masks + + def _draw_samples_for_image(self, points_on_image, random_state): + drop_samples = self.p_drop.draw_samples((len(points_on_image),), + random_state) + keep_mask = (drop_samples > 0.5) + return keep_mask + + @classmethod + def _apply_dropout_masks(cls, points_on_images, keep_masks): + points_on_images_dropped = [] + for points_on_image, keep_mask in zip(points_on_images, keep_masks): + if len(points_on_image) == 0: + # other sampler didn't provide any points + poi_dropped = points_on_image + else: + if not np.any(keep_mask): + # keep at least one point if all were supposed to be + # dropped + # TODO this could also be moved into its own point sampler, + # like AtLeastOnePoint(...) + idx = (len(points_on_image) - 1) // 2 + keep_mask = np.copy(keep_mask) + keep_mask[idx] = True + poi_dropped = points_on_image[keep_mask, :] + points_on_images_dropped.append(poi_dropped) + return points_on_images_dropped + + def __repr__(self): + return "DropoutPointsSampler(%s, %s)" % (self.other_points_sampler, + self.p_drop) + + def __str__(self): + return self.__repr__() + + +class UniformPointsSampler(PointsSamplerIf): + """Sample points uniformly on images. + + This point sampler generates `n_points` points per image. The x- and + y-coordinates are both sampled from uniform distributions matching the + respective image width and height. + + Parameters + ---------- + n_points : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Number of points to sample on each image. + + * If a single int, then that value will always be used. + * If a tuple ``(a, b)``, then a value from the discrete interval + ``[a..b]`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. + * If a ``StochasticParameter``, then that parameter will be + queried to draw one value per image. + + Examples + -------- + >>> import imgaug.augmenters as iaa + >>> sampler = iaa.UniformPointsSampler(500) + + Creates a point sampler that generates an array of 500 random points for + each input image. The x- and y-coordinates of each point are sampled + from uniform distributions. + + """ + + def __init__(self, n_points): + self.n_points = iap.handle_discrete_param( + n_points, "n_points", value_range=(1, None), + tuple_to_uniform=True, list_to_choice=True, allow_floats=False) + + def sample_points(self, images, random_state): + random_state = ia.normalize_random_state(random_state) + _verify_sample_points_images(images) + + rss = ia.derive_random_states(random_state, 2) + n_points_imagewise = self._draw_samples(len(images), rss[0]) + + n_points_total = np.sum(n_points_imagewise) + n_components_total = 2 * n_points_total + coords_relative = rss[1].uniform(0.0, 1.0, n_components_total) + coords_relative_xy = coords_relative.reshape(n_points_total, 2) + + return self._convert_relative_coords_to_absolute( + coords_relative_xy, n_points_imagewise, images) + + def _draw_samples(self, n_augmentables, random_state): + n_points = self.n_points.draw_samples((n_augmentables,), + random_state=random_state) + n_points_clipped = np.clip(n_points, 1, None) + return n_points_clipped + + @classmethod + def _convert_relative_coords_to_absolute(cls, coords_rel_xy, + n_points_imagewise, images): + coords_absolute = [] + i = 0 + for image, n_points_image in zip(images, n_points_imagewise): + height, width = image.shape[0:2] + xx = coords_rel_xy[i:i+n_points_image, 0] + yy = coords_rel_xy[i:i+n_points_image, 1] + + xx_int = np.clip(np.round(xx * width), 0, width) + yy_int = np.clip(np.round(yy * height), 0, height) + + coords_absolute.append(np.stack([xx_int, yy_int], axis=-1)) + i += n_points_image + return coords_absolute + + def __repr__(self): + return "UniformPointsSampler(%s)" % (self.n_points,) + + def __str__(self): + return self.__repr__() + + +class SubsamplingPointsSampler(PointsSamplerIf): + """Ensure that the number of sampled points is below a maximum. + + This point sampler will sample points from another sampler and + then -- in case more points were generated than an allowed maximum -- + will randomly pick `n_points_max` of these. + + Parameters + ---------- + other_points_sampler : PointsSamplerIf + Another point sampler that is queried to generate a list of points. + The dropout operation will be applied to that list. + + n_points_max : int + Maximum number of allowed points. If `other_points_sampler` generates + more points than this maximum, a random subset of size `n_points_max` + will be selected. + + Examples + -------- + >>> import imgaug.augmenters as iaa + >>> sampler = iaa.SubsamplingPointsSampler( + >>> iaa.RelativeRegularGridPointsSampler(0.1, 0.2), + >>> 50 + >>> ) + + Creates a points sampler that places ``y*H`` points on the y-axis (with + ``y`` being ``0.1`` and ``H`` being an image's height) and ``x*W`` on + the x-axis (analogous). Then, if that number of placed points exceeds + ``50`` (can easily happen for larger images), a random subset of ``50`` + points will be picked and returned. + + """ + + def __init__(self, other_points_sampler, n_points_max): + assert isinstance(other_points_sampler, PointsSamplerIf), ( + "Expected to get an instance of PointsSamplerIf as argument " + "'other_points_sampler', got type %s." % ( + type(other_points_sampler),)) + self.other_points_sampler = other_points_sampler + self.n_points_max = np.clip(n_points_max, -1, None) + if self.n_points_max == 0: + import warnings + warnings.warn("Got n_points_max=0 in SubsamplingPointsSampler. " + "This will result in no points ever getting " + "returned.") + + def sample_points(self, images, random_state): + random_state = ia.normalize_random_state(random_state) + _verify_sample_points_images(images) + + rss = ia.derive_random_states(random_state, len(images) + 1) + points_on_images = self.other_points_sampler.sample_points( + images, rss[-1]) + return [self._subsample(points_on_image, self.n_points_max, rs) + for points_on_image, rs + in zip(points_on_images, rss[:-1])] + + @classmethod + def _subsample(cls, points_on_image, n_points_max, random_state): + if len(points_on_image) <= n_points_max: + return points_on_image + indices = np.arange(len(points_on_image)) + indices_to_keep = random_state.permutation(indices)[0:n_points_max] + return points_on_image[indices_to_keep] + + def __repr__(self): + return "SubsamplingPointsSampler(%s, %d)" % (self.other_points_sampler, + self.n_points_max) + + def __str__(self): + return self.__repr__() + + +# TODO Add points subsampler that drops points close to each other first +# TODO Add poisson points sampler +# TODO Add jitter points sampler that moves points around +# for both see https://codegolf.stackexchange.com/questions/50299/draw-an-image-as-a-voronoi-map/50345#50345 diff --git a/imgaug/imgaug.py b/imgaug/imgaug.py index 2a2e0d570..dfeb5dcb6 100644 --- a/imgaug/imgaug.py +++ b/imgaug/imgaug.py @@ -342,6 +342,34 @@ def seed(seedval): CURRENT_RANDOM_STATE.seed(seedval) +# TODO add tests +def normalize_random_state(random_state): + """ + Normalize various inputs to a numpy random state. + + Parameters + ---------- + random_state : None or numpy.random.RandomState or int or float + RandomState to normalize. + If this is ``None``, the global random state will be returned. + If this is an instance of numpy's ``RandomState``, it will be returned + without any change. If it is anything else it is assumed to be a + seed value and a new ``RandomState`` using that seed will be returned. + + Returns + ------- + numpy.random.RandomState + Normalized random state. + + """ + if random_state is None: + return CURRENT_RANDOM_STATE + elif isinstance(random_state, np.random.RandomState): + return random_state + # seed given + return np.random.RandomState(random_state) + + def current_random_state(): """ Returns the current/global random state of the library. diff --git a/test/augmenters/test_segmentation.py b/test/augmenters/test_segmentation.py index 2ff51af64..14355b7f8 100644 --- a/test/augmenters/test_segmentation.py +++ b/test/augmenters/test_segmentation.py @@ -1,6 +1,7 @@ from __future__ import print_function, division, absolute_import import sys +import warnings # unittest only added in 3.4 self.subTest() if sys.version_info[0] < 3 or sys.version_info[1] < 4: import unittest2 as unittest @@ -240,3 +241,1192 @@ def _allclose(a, b): img_aug = aug.augment_image(img) assert img_aug.dtype == np.dtype(dtype) assert _allclose(img_aug, (7/8)*v2 + (1/8)*v1) + + +class Test_segment_voronoi(unittest.TestCase): + def setUp(self): + reseed() + + def test_cell_coordinates_is_empty_integrationtest(self): + image = np.arange(2*2*3).astype(np.uint8).reshape((2, 2, 3)) + cell_coordinates = np.zeros((0, 2), dtype=np.float32) + replace_mask = np.zeros((0,), dtype=bool) + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + assert np.array_equal(image, image_seg) + + @classmethod + def _test_image_n_channels_integrationtest(cls, nb_channels): + image = np.uint8([ + [0, 1, 200, 201], + [2, 3, 202, 203] + ]) + if nb_channels is not None: + image = np.tile(image[:, :, np.newaxis], (1, 1, nb_channels)) + for c in sm.xrange(nb_channels): + image[..., c] += c + cell_coordinates = np.float32([ + [1.0, 1.0], + [3.0, 1.0] + ]) + replace_mask = np.array([True, True], dtype=bool) + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + pixels1 = image[0:2, 0:2] + pixels2 = image[0:2, 2:4] + avg_color1 = np.average(pixels1.astype(np.float32), axis=(0, 1)) + avg_color2 = np.average(pixels2.astype(np.float32), axis=(0, 1)) + image_expected = np.uint8([ + [avg_color1, avg_color1, avg_color2, avg_color2], + [avg_color1, avg_color1, avg_color2, avg_color2], + ]) + + assert np.array_equal(image_seg, image_expected) + + def test_image_has_no_channels_integrationtest(self): + self._test_image_n_channels_integrationtest(None) + + def test_image_has_one_channel_integrationtest(self): + self._test_image_n_channels_integrationtest(1) + + def test_image_has_three_channels_integrationtest(self): + self._test_image_n_channels_integrationtest(3) + + def test_replace_mask_is_all_false_integrationtest(self): + image = np.uint8([ + [0, 1, 200, 201], + [2, 3, 202, 203] + ]) + cell_coordinates = np.float32([ + [1.0, 1.0], + [3.0, 1.0] + ]) + replace_mask = np.array([False, False], dtype=bool) + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + assert np.array_equal(image_seg, image) + + def test_replace_mask_is_mixed_integrationtest(self): + image = np.uint8([ + [0, 1, 200, 201], + [2, 3, 202, 203] + ]) + cell_coordinates = np.float32([ + [1.0, 1.0], + [3.0, 1.0] + ]) + replace_mask = np.array([False, True], dtype=bool) + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + pixels2 = image[0:2, 2:4] + avg_color2 = np.sum(pixels2).astype(np.float32) / pixels2.size + image_expected = np.uint8([ + [0, 1, avg_color2, avg_color2], + [2, 3, avg_color2, avg_color2], + ]) + assert np.array_equal(image_seg, image_expected) + + def test_replace_mask_is_none_integrationtest(self): + image = np.uint8([ + [0, 1, 200, 201], + [2, 3, 202, 203] + ]) + cell_coordinates = np.float32([ + [1.0, 1.0], + [3.0, 1.0] + ]) + replace_mask = None + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + pixels1 = image[0:2, 0:2] + pixels2 = image[0:2, 2:4] + avg_color1 = np.sum(pixels1).astype(np.float32) / pixels1.size + avg_color2 = np.sum(pixels2).astype(np.float32) / pixels2.size + image_expected = np.uint8([ + [avg_color1, avg_color1, avg_color2, avg_color2], + [avg_color1, avg_color1, avg_color2, avg_color2], + ]) + assert np.array_equal(image_seg, image_expected) + + def test_no_cell_coordinates_provided_and_no_channel_integrationtest(self): + image = np.uint8([ + [0, 1, 200, 201], + [2, 3, 202, 203] + ]) + cell_coordinates = np.zeros((0, 2), dtype=np.float32) + replace_mask = np.zeros((0,), dtype=bool) + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + assert np.array_equal(image_seg, image) + + def test_no_cell_coordinates_provided_and_3_channels_integrationtest(self): + image = np.uint8([ + [0, 1, 200, 201], + [2, 3, 202, 203] + ]) + image = np.tile(image[..., np.newaxis], (1, 1, 3)) + cell_coordinates = np.zeros((0, 2), dtype=np.float32) + replace_mask = np.zeros((0,), dtype=bool) + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + assert np.array_equal(image_seg, image) + + def test_image_with_zero_height(self): + image = np.zeros((0, 4, 3), dtype=np.uint8) + cell_coordinates = np.float32([ + [1.0, 1.0], + [3.0, 1.0] + ]) + replace_mask = np.array([True, True], dtype=bool) + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + assert np.array_equal(image_seg, image) + + def test_image_with_zero_width(self): + image = np.zeros((4, 0, 3), dtype=np.uint8) + cell_coordinates = np.float32([ + [1.0, 1.0], + [3.0, 1.0] + ]) + replace_mask = np.array([True, True], dtype=bool) + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + assert np.array_equal(image_seg, image) + + def test_image_with_zero_size(self): + image = np.zeros((0, 0), dtype=np.uint8) + cell_coordinates = np.float32([ + [1.0, 1.0], + [3.0, 1.0] + ]) + replace_mask = np.array([True, True], dtype=bool) + + image_seg = iaa.segment_voronoi(image, cell_coordinates, replace_mask) + + assert np.array_equal(image_seg, image) + + +class TestVoronoi(unittest.TestCase): + def setUp(self): + reseed() + + def test___init___defaults(self): + sampler = iaa.RegularGridPointsSampler(1, 1) + aug = iaa.Voronoi(sampler) + assert aug.points_sampler is sampler + assert isinstance(aug.p_replace, iap.Deterministic) + assert aug.p_replace.value == 1 + assert aug.max_size == 128 + assert aug.interpolation == "linear" + + def test___init___custom_arguments(self): + sampler = iaa.RegularGridPointsSampler(1, 1) + aug = iaa.Voronoi(sampler, p_replace=0.5, max_size=None, + interpolation="cubic") + assert aug.points_sampler is sampler + assert isinstance(aug.p_replace, iap.Binomial) + assert np.isclose(aug.p_replace.p.value, 0.5) + assert aug.max_size is None + assert aug.interpolation == "cubic" + + def test_max_size_is_none(self): + image = np.zeros((10, 20, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(1, 1) + aug = iaa.Voronoi(sampler, max_size=None) + + mock_imresize = mock.MagicMock() + mock_imresize.return_value = image + + fname = "imgaug.imresize_single_image" + with mock.patch(fname, mock_imresize): + _image_aug = aug(image=image) + + assert mock_imresize.call_count == 0 + + def test_max_size_is_int_image_not_too_large(self): + image = np.zeros((10, 20, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(1, 1) + aug = iaa.Voronoi(sampler, max_size=100) + + mock_imresize = mock.MagicMock() + mock_imresize.return_value = image + + fname = "imgaug.imresize_single_image" + with mock.patch(fname, mock_imresize): + _image_aug = aug(image=image) + + assert mock_imresize.call_count == 0 + + def test_max_size_is_int_image_too_large(self): + image = np.zeros((10, 20, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(1, 1) + aug = iaa.Voronoi(sampler, max_size=10) + + mock_imresize = mock.MagicMock() + mock_imresize.return_value = image + + fname = "imgaug.imresize_single_image" + with mock.patch(fname, mock_imresize): + _image_aug = aug(image=image) + + assert mock_imresize.call_count == 1 + assert mock_imresize.call_args_list[0][0][1] == (5, 10) + + def test_interpolation(self): + image = np.zeros((10, 20, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(1, 1) + aug = iaa.Voronoi(sampler, max_size=10, interpolation="cubic") + + mock_imresize = mock.MagicMock() + mock_imresize.return_value = image + + fname = "imgaug.imresize_single_image" + with mock.patch(fname, mock_imresize): + _image_aug = aug(image=image) + + assert mock_imresize.call_count == 1 + assert mock_imresize.call_args_list[0][1]["interpolation"] == "cubic" + + def test_point_sampler_called(self): + class LoggedPointSampler(iaa.PointsSamplerIf): + def __init__(self, other): + self.other = other + self.call_count = 0 + + def sample_points(self, images, random_state): + self.call_count += 1 + return self.other.sample_points(images, random_state) + + image = np.zeros((10, 20, 3), dtype=np.uint8) + sampler = LoggedPointSampler(iaa.RegularGridPointsSampler(1, 1)) + aug = iaa.Voronoi(sampler) + + _image_aug = aug(image=image) + + assert sampler.call_count == 1 + + def test_point_sampler_returns_no_points_integrationtest(self): + class NoPointsPointSampler(iaa.PointsSamplerIf): + def sample_points(self, images, random_state): + return [np.zeros((0, 2), dtype=np.float32)] + + image = np.zeros((10, 20, 3), dtype=np.uint8) + sampler = NoPointsPointSampler() + aug = iaa.Voronoi(sampler) + + image_aug = aug(image=image) + + assert np.array_equal(image_aug, image) + + @classmethod + def _test_image_with_n_channels(cls, nb_channels): + image = np.zeros((10, 20), dtype=np.uint8) + if nb_channels is not None: + image = image[..., np.newaxis] + image = np.tile(image, (1, 1, nb_channels)) + sampler = iaa.RegularGridPointsSampler(1, 1) + aug = iaa.Voronoi(sampler) + + mock_segment_voronoi = mock.MagicMock() + if nb_channels is None: + mock_segment_voronoi.return_value = image[..., np.newaxis] + else: + mock_segment_voronoi.return_value = image + + fname = "imgaug.augmenters.segmentation.segment_voronoi" + with mock.patch(fname, mock_segment_voronoi): + image_aug = aug(image=image) + + assert image_aug.shape == image.shape + + def test_image_with_no_channels(self): + self._test_image_with_n_channels(None) + + def test_image_with_one_channel(self): + self._test_image_with_n_channels(1) + + def test_image_with_three_channels(self): + self._test_image_with_n_channels(3) + + def test_p_replace_is_zero(self): + image = np.zeros((50, 50), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(50, 50) + aug = iaa.Voronoi(sampler, p_replace=0.0) + + mock_segment_voronoi = mock.MagicMock() + mock_segment_voronoi.return_value = image[..., np.newaxis] + + fname = "imgaug.augmenters.segmentation.segment_voronoi" + with mock.patch(fname, mock_segment_voronoi): + _image_aug = aug(image=image) + + replace_mask = mock_segment_voronoi.call_args_list[0][0][2] + assert not np.any(replace_mask) + + def test_p_replace_is_one(self): + image = np.zeros((50, 50), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(50, 50) + aug = iaa.Voronoi(sampler, p_replace=1.0) + + mock_segment_voronoi = mock.MagicMock() + mock_segment_voronoi.return_value = image[..., np.newaxis] + + fname = "imgaug.augmenters.segmentation.segment_voronoi" + with mock.patch(fname, mock_segment_voronoi): + _image_aug = aug(image=image) + + replace_mask = mock_segment_voronoi.call_args_list[0][0][2] + assert np.all(replace_mask) + + def test_p_replace_is_50_percent(self): + image = np.zeros((200, 200), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(200, 200) + aug = iaa.Voronoi(sampler, p_replace=0.5) + + mock_segment_voronoi = mock.MagicMock() + mock_segment_voronoi.return_value = image[..., np.newaxis] + + fname = "imgaug.augmenters.segmentation.segment_voronoi" + with mock.patch(fname, mock_segment_voronoi): + _image_aug = aug(image=image) + + replace_mask = mock_segment_voronoi.call_args_list[0][0][2] + replace_fraction = np.average(replace_mask.astype(np.float32)) + assert 0.4 <= replace_fraction <= 0.6 + + def test_determinism_integrationtest(self): + image = np.arange(10*20).astype(np.uint8).reshape((10, 20, 1)) + image = np.tile(image, (1, 1, 3)) + image[:, :, 1] += 5 + image[:, :, 2] += 10 + sampler = iaa.DropoutPointsSampler( + iaa.RegularGridPointsSampler((1, 10), (1, 20)), + 0.5 + ) + aug = iaa.Voronoi(sampler, p_replace=(0.0, 1.0)) + aug_det = aug.to_deterministic() + + images_aug_a1 = aug(images=[image] * 50) + images_aug_a2 = aug(images=[image] * 50) + + images_aug_b1 = aug_det(images=[image] * 50) + images_aug_b2 = aug_det(images=[image] * 50) + + same_within_a1 = _all_arrays_identical(images_aug_a1) + same_within_a2 = _all_arrays_identical(images_aug_a2) + + same_within_b1 = _all_arrays_identical(images_aug_b1) + same_within_b2 = _all_arrays_identical(images_aug_b2) + + same_between_a1_a2 = _array_lists_elementwise_identical(images_aug_a1, + images_aug_a2) + same_between_b1_b2 = _array_lists_elementwise_identical(images_aug_b1, + images_aug_b2) + + assert not same_within_a1 + assert not same_within_a2 + assert not same_within_b1 + assert not same_within_b2 + + assert not same_between_a1_a2 + assert same_between_b1_b2 + + def test_get_parameters(self): + sampler = iaa.RegularGridPointsSampler(1, 1) + aug = iaa.Voronoi(sampler, p_replace=0.5, max_size=None, + interpolation="cubic") + params = aug.get_parameters() + assert params[0] is sampler + assert isinstance(params[1], iap.Binomial) + assert np.isclose(params[1].p.value, 0.5) + assert params[2] is None + assert params[3] == "cubic" + + +def _all_arrays_identical(arrs): + if len(arrs) == 1: + return True + + return np.all([np.array_equal(arrs[0], arr_other) + for arr_other in arrs[1:]]) + + +def _array_lists_elementwise_identical(arrs1, arrs2): + return np.all([np.array_equal(arr1, arr2) + for arr1, arr2 in zip(arrs1, arrs2)]) + + +class TestUniformVoronoi(unittest.TestCase): + def test___init___(self): + rs = np.random.RandomState(10) + + mock_voronoi = mock.MagicMock() + mock_voronoi.return_value = mock_voronoi + fname = "imgaug.augmenters.segmentation.Voronoi.__init__" + with mock.patch(fname, mock_voronoi): + _ = iaa.UniformVoronoi( + 100, + p_replace=0.5, + max_size=5, + interpolation="cubic", + name="foo", + deterministic=True, + random_state=rs + ) + + assert mock_voronoi.call_count == 1 + assert isinstance(mock_voronoi.call_args_list[0][1]["points_sampler"], + iaa.UniformPointsSampler) + assert np.isclose(mock_voronoi.call_args_list[0][1]["p_replace"], + 0.5) + assert mock_voronoi.call_args_list[0][1]["max_size"] == 5 + assert mock_voronoi.call_args_list[0][1]["interpolation"] == "cubic" + assert mock_voronoi.call_args_list[0][1]["name"] == "foo" + assert mock_voronoi.call_args_list[0][1]["deterministic"] is True + assert mock_voronoi.call_args_list[0][1]["random_state"] is rs + + def test___init___integrationtest(self): + rs = np.random.RandomState(10) + aug = iaa.UniformVoronoi( + 100, + p_replace=0.5, + max_size=5, + interpolation="cubic", + name=None, + deterministic=True, + random_state=rs + ) + assert aug.points_sampler.n_points.value == 100 + assert np.isclose(aug.p_replace.p.value, 0.5) + assert aug.max_size == 5 + assert aug.interpolation == "cubic" + assert aug.name == "UnnamedUniformVoronoi" + assert aug.deterministic is True + assert aug.random_state is rs + + +class TestRegularGridVoronoi(unittest.TestCase): + def test___init___(self): + rs = np.random.RandomState(10) + + mock_voronoi = mock.MagicMock() + mock_voronoi.return_value = mock_voronoi + fname = "imgaug.augmenters.segmentation.Voronoi.__init__" + with mock.patch(fname, mock_voronoi): + _ = iaa.RegularGridVoronoi( + 10, + 20, + p_drop_points=0.6, + p_replace=0.5, + max_size=5, + interpolation="cubic", + name="foo", + deterministic=True, + random_state=rs + ) + + assert mock_voronoi.call_count == 1 + ps = mock_voronoi.call_args_list[0][1]["points_sampler"] + assert isinstance(ps, iaa.DropoutPointsSampler) + assert isinstance(ps.other_points_sampler, + iaa.RegularGridPointsSampler) + assert np.isclose(ps.p_drop.p.value, 1-0.6) + assert ps.other_points_sampler.n_rows.value == 10 + assert ps.other_points_sampler.n_cols.value == 20 + assert np.isclose(mock_voronoi.call_args_list[0][1]["p_replace"], + 0.5) + assert mock_voronoi.call_args_list[0][1]["max_size"] == 5 + assert mock_voronoi.call_args_list[0][1]["interpolation"] == "cubic" + assert mock_voronoi.call_args_list[0][1]["name"] == "foo" + assert mock_voronoi.call_args_list[0][1]["deterministic"] is True + assert mock_voronoi.call_args_list[0][1]["random_state"] is rs + + def test___init___integrationtest(self): + rs = np.random.RandomState(10) + aug = iaa.RegularGridVoronoi( + 10, + (10, 30), + p_replace=0.5, + max_size=5, + interpolation="cubic", + name=None, + deterministic=True, + random_state=rs + ) + assert np.isclose(aug.points_sampler.p_drop.p.value, 1-0.4) + assert aug.points_sampler.other_points_sampler.n_rows.value == 10 + assert isinstance(aug.points_sampler.other_points_sampler.n_cols, + iap.DiscreteUniform) + assert aug.points_sampler.other_points_sampler.n_cols.a.value == 10 + assert aug.points_sampler.other_points_sampler.n_cols.b.value == 30 + assert np.isclose(aug.p_replace.p.value, 0.5) + assert aug.max_size == 5 + assert aug.interpolation == "cubic" + assert aug.name == "UnnamedRegularGridVoronoi" + assert aug.deterministic is True + assert aug.random_state is rs + + +class TestRelativeRegularGridVoronoi(unittest.TestCase): + def test___init___(self): + rs = np.random.RandomState(10) + + mock_voronoi = mock.MagicMock() + mock_voronoi.return_value = mock_voronoi + fname = "imgaug.augmenters.segmentation.Voronoi.__init__" + with mock.patch(fname, mock_voronoi): + _ = iaa.RelativeRegularGridVoronoi( + 0.1, + 0.2, + p_drop_points=0.6, + p_replace=0.5, + max_size=5, + interpolation="cubic", + name="foo", + deterministic=True, + random_state=rs + ) + + assert mock_voronoi.call_count == 1 + ps = mock_voronoi.call_args_list[0][1]["points_sampler"] + assert isinstance(ps, iaa.DropoutPointsSampler) + assert isinstance(ps.other_points_sampler, + iaa.RelativeRegularGridPointsSampler) + assert np.isclose(ps.p_drop.p.value, 1-0.6) + assert np.isclose(ps.other_points_sampler.n_rows_frac.value, 0.1) + assert np.isclose(ps.other_points_sampler.n_cols_frac.value, 0.2) + assert np.isclose(mock_voronoi.call_args_list[0][1]["p_replace"], + 0.5) + assert mock_voronoi.call_args_list[0][1]["max_size"] == 5 + assert mock_voronoi.call_args_list[0][1]["interpolation"] == "cubic" + assert mock_voronoi.call_args_list[0][1]["name"] == "foo" + assert mock_voronoi.call_args_list[0][1]["deterministic"] is True + assert mock_voronoi.call_args_list[0][1]["random_state"] is rs + + def test___init___integrationtest(self): + rs = np.random.RandomState(10) + aug = iaa.RelativeRegularGridVoronoi( + 0.1, + (0.1, 0.3), + p_replace=0.5, + max_size=5, + interpolation="cubic", + name=None, + deterministic=True, + random_state=rs + ) + + ps = aug.points_sampler + assert np.isclose(aug.points_sampler.p_drop.p.value, 1-0.4) + assert np.isclose(ps.other_points_sampler.n_rows_frac.value, 0.1) + assert isinstance(ps.other_points_sampler.n_cols_frac, iap.Uniform) + assert np.isclose(ps.other_points_sampler.n_cols_frac.a.value, 0.1) + assert np.isclose(ps.other_points_sampler.n_cols_frac.b.value, 0.3) + assert np.isclose(aug.p_replace.p.value, 0.5) + assert aug.max_size == 5 + assert aug.interpolation == "cubic" + assert aug.name == "UnnamedRelativeRegularGridVoronoi" + assert aug.deterministic is True + assert aug.random_state is rs + + +# TODO verify behaviours when image height/width is zero +class TestRegularGridPointSampler(unittest.TestCase): + def setUp(self): + reseed() + + def test___init___(self): + sampler = iaa.RegularGridPointsSampler((1, 10), 20) + assert isinstance(sampler.n_rows, iap.DiscreteUniform) + assert sampler.n_rows.a.value == 1 + assert sampler.n_rows.b.value == 10 + assert sampler.n_cols.value == 20 + + def test_sample_single_point(self): + image = np.zeros((10, 20, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(1, 1) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + assert len(points) == 1 + assert np.allclose(points[0], [10.0, 5.0]) + + def test_sample_points(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(2, 2) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + assert len(points) == 4 + assert np.allclose(points, [ + [0.0, 0.0], + [10.0, 0.0], + [0.0, 10.0], + [10.0, 10.0] + ]) + + def test_sample_points_stochastic(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(1, iap.Choice([1, 2])) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + + matches_single_point = np.allclose(points, [ + [5.0, 5.0] + ]) + matches_two_points = np.allclose(points, [ + [0.0, 5.0], + [10.0, 5.0] + ]) + + assert len(points) in [1, 2] + assert matches_single_point or matches_two_points + + def test_sample_points_cols_is_zero(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(iap.Deterministic(0), 1) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + + matches_single_point = np.allclose(points, [ + [5.0, 5.0] + ]) + + assert len(points) == 1 + assert matches_single_point + + def test_sample_points_rows_is_zero(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(1, iap.Deterministic(0)) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + + matches_single_point = np.allclose(points, [ + [5.0, 5.0] + ]) + + assert len(points) == 1 + assert matches_single_point + + def test_sample_points_rows_is_more_than_image_height(self): + image = np.zeros((1, 1, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(2, 1) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + + matches_single_point = np.allclose(points, [ + [0.5, 0.5] + ]) + + assert len(points) == 1 + assert matches_single_point + + def test_sample_points_cols_is_more_than_image_width(self): + image = np.zeros((1, 1, 3), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler(1, 2) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + + matches_single_point = np.allclose(points, [ + [0.5, 0.5] + ]) + + assert len(points) == 1 + assert matches_single_point + + def test_determinism(self): + image = np.zeros((500, 500, 1), dtype=np.uint8) + sampler = iaa.RegularGridPointsSampler((1, 500), (1, 500)) + points_seed1_1 = sampler.sample_points([image], 1)[0] + points_seed1_2 = sampler.sample_points([image], 1)[0] + points_seed2_1 = sampler.sample_points([image], 2)[0] + + assert points_seed1_1.shape == points_seed1_2.shape + assert points_seed1_1.shape != points_seed2_1.shape + + def test_conversion_to_string(self): + sampler = iaa.RegularGridPointsSampler(10, (10, 30)) + expected = ( + "RegularGridPointsSampler(" + "Deterministic(int 10), " + "DiscreteUniform(Deterministic(int 10), Deterministic(int 30))" + ")" + ) + assert sampler.__str__() == sampler.__repr__() == expected + + +class TestRelativeRegularGridPointSampler(unittest.TestCase): + def setUp(self): + reseed() + + def test___init___(self): + sampler = iaa.RelativeRegularGridPointsSampler((0.1, 0.2), 0.1) + assert isinstance(sampler.n_rows_frac, iap.Uniform) + assert np.isclose(sampler.n_rows_frac.a.value, 0.1) + assert np.isclose(sampler.n_rows_frac.b.value, 0.2) + assert np.isclose(sampler.n_cols_frac.value, 0.1) + + def test_sample_single_point(self): + image = np.zeros((10, 20, 3), dtype=np.uint8) + sampler = iaa.RelativeRegularGridPointsSampler(0.001, 0.001) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + assert len(points) == 1 + assert np.allclose(points[0], [10.0, 5.0]) + + def test_sample_points(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + sampler = iaa.RelativeRegularGridPointsSampler(0.2, 0.2) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + assert len(points) == 4 + assert np.allclose(points, [ + [0.0, 0.0], + [10.0, 0.0], + [0.0, 10.0], + [10.0, 10.0] + ]) + + def test_sample_points_stochastic(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + sampler = iaa.RelativeRegularGridPointsSampler(0.1, + iap.Choice([0.1, 0.2])) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + + matches_single_point = np.allclose(points, [ + [5.0, 5.0] + ]) + matches_two_points = np.allclose(points, [ + [0.0, 5.0], + [10.0, 5.0] + ]) + + assert len(points) in [1, 2] + assert matches_single_point or matches_two_points + + def test_sample_points_cols_is_zero(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + sampler = iaa.RelativeRegularGridPointsSampler(iap.Deterministic(0.001), + 0.1) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + + matches_single_point = np.allclose(points, [ + [5.0, 5.0] + ]) + + assert len(points) == 1 + assert matches_single_point + + def test_sample_points_rows_is_zero(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + sampler = iaa.RelativeRegularGridPointsSampler(0.1, + iap.Deterministic(0.001)) + points = sampler.sample_points([image], np.random.RandomState(1))[0] + + matches_single_point = np.allclose(points, [ + [5.0, 5.0] + ]) + + assert len(points) == 1 + assert matches_single_point + + def test_determinism(self): + image = np.zeros((500, 500, 1), dtype=np.uint8) + sampler = iaa.RelativeRegularGridPointsSampler((0.01, 1.0), (0.1, 1.0)) + points_seed1_1 = sampler.sample_points([image], 1)[0] + points_seed1_2 = sampler.sample_points([image], 1)[0] + points_seed2_1 = sampler.sample_points([image], 2)[0] + + assert points_seed1_1.shape == points_seed1_2.shape + assert points_seed1_1.shape != points_seed2_1.shape + + def test_conversion_to_string(self): + sampler = iaa.RelativeRegularGridPointsSampler(0.01, (0.01, 0.05)) + expected = ( + "RelativeRegularGridPointsSampler(" + "Deterministic(float 0.01000000), " + "Uniform(" + "Deterministic(float 0.01000000), " + "Deterministic(float 0.05000000)" + ")" + ")" + ) + assert sampler.__str__() == sampler.__repr__() == expected + + +class _FixedPointsSampler(iaa.PointsSamplerIf): + def __init__(self, points): + self.points = np.float32(np.copy(points)) + self.last_random_state = None + + def sample_points(self, images, random_state): + self.last_random_state = random_state + return np.tile(self.points[np.newaxis, ...], (len(images), 1)) + + +class TestDropoutPointsSampler(unittest.TestCase): + def setUp(self): + reseed() + + def test___init__(self): + other = iaa.RegularGridPointsSampler(1, 1) + sampler = iaa.DropoutPointsSampler(other, 0.5) + assert sampler.other_points_sampler is other + assert isinstance(sampler.p_drop, iap.Binomial) + assert np.isclose(sampler.p_drop.p.value, 0.5) + + def test_p_drop_is_0_percent(self): + image = np.zeros((1, 1, 3), dtype=np.uint8) + points = np.linspace(0.0, 1000.0, num=100000) + points = np.stack([points, points], axis=-1) + other = _FixedPointsSampler(points) + sampler = iaa.DropoutPointsSampler(other, 0.0) + + observed = sampler.sample_points([image], 1)[0] + + assert np.allclose(observed, points) + + def test_p_drop_is_100_percent(self): + image = np.zeros((1, 1, 3), dtype=np.uint8) + points = np.linspace(0.0+0.9, 1000.0-0.9, num=100000) + points = np.stack([points, points], axis=-1) + other = _FixedPointsSampler(points) + sampler = iaa.DropoutPointsSampler(other, 1.0) + + observed = sampler.sample_points([image], 1)[0] + + eps = 1e-4 + assert len(observed) == 1 + assert 0.0 + 0.9 - eps <= observed[0][0] <= 1000.0 - 0.9 + eps + assert 0.0 + 0.9 - eps <= observed[0][1] <= 1000.0 - 0.9 + eps + + def test_p_drop_is_50_percent(self): + image = np.zeros((1, 1, 3), dtype=np.uint8) + points = np.linspace(0.0+0.9, 1000.0-0.9, num=100000) + points = np.stack([points, points], axis=-1) + other = _FixedPointsSampler(points) + sampler = iaa.DropoutPointsSampler(other, 0.5) + + observed = sampler.sample_points([image], 1)[0] + + assert 50000 - 1000 <= len(observed) <= 50000 + 1000 + + def test_determinism(self): + image = np.zeros((1, 1, 3), dtype=np.uint8) + points = np.linspace(0.0+0.9, 1000.0-0.9, num=100000) + points = np.stack([points, points], axis=-1) + other = _FixedPointsSampler(points) + sampler = iaa.DropoutPointsSampler(other, (0.3, 0.7)) + + observed_s1_1 = sampler.sample_points([image], 1)[0] + observed_s1_2 = sampler.sample_points([image], 1)[0] + observed_s2_1 = sampler.sample_points([image], 2)[0] + + assert np.allclose(observed_s1_1, observed_s1_2) + assert (observed_s1_1.shape != observed_s2_1.shape + or not np.allclose(observed_s1_1, observed_s2_1)) + + def test_random_state_propagates(self): + image = np.zeros((1, 1, 3), dtype=np.uint8) + points = np.linspace(0.0+0.9, 1000.0-0.9, num=1) + points = np.stack([points, points], axis=-1) + other = _FixedPointsSampler(points) + sampler = iaa.DropoutPointsSampler(other, 0.5) + + _ = sampler.sample_points([image], 1)[0] + rs_s1_1 = other.last_random_state + _ = sampler.sample_points([image], 1)[0] + rs_s1_2 = other.last_random_state + _ = sampler.sample_points([image], 2)[0] + rs_s2_1 = other.last_random_state + + # get_state() returns: tuple(str, ndarray of 624 uints, int, int, + # float) + # we compare the non-floats here + all_s1_identical = True + all_s1s2_identical = True + for i in sm.xrange(1, 4): + all_s1_identical = ( + all_s1_identical + and np.array_equal(rs_s1_1.get_state()[i], + rs_s1_2.get_state()[i])) + + all_s1s2_identical = ( + all_s1s2_identical + and np.array_equal(rs_s1_1.get_state()[i], + rs_s2_1.get_state()[i])) + + assert all_s1_identical + assert not all_s1s2_identical + + def test_conversion_to_string(self): + sampler = iaa.DropoutPointsSampler( + iaa.RegularGridPointsSampler(10, 20), + 0.2 + ) + expected = ( + "DropoutPointsSampler(" + "RegularGridPointsSampler(" + "Deterministic(int 10), " + "Deterministic(int 20)" + "), " + "Binomial(Deterministic(float 0.80000000))" + ")" + ) + assert sampler.__str__() == sampler.__repr__() == expected + + +class TestUniformPointsSampler(unittest.TestCase): + def setUp(self): + reseed() + + def test___init__(self): + sampler = iaa.UniformPointsSampler(100) + assert isinstance(sampler.n_points, iap.Deterministic) + assert sampler.n_points.value == 100 + + def test_sampled_points_not_identical(self): + sampler = iaa.UniformPointsSampler(3) + images = [np.zeros((1000, 1000, 3), dtype=np.uint8)] + + points = sampler.sample_points(images, 1)[0] + points_tpls = [tuple(point) for point in points] + n_points = len(points) + n_points_uq = len(set(points_tpls)) + + assert n_points == 3 + assert n_points_uq == 3 + + def test_sampled_points_uniformly_distributed_by_quadrants(self): + # split image into 2x2 quadrants, group all points per quadrant, + # assume that at least around N_points/(2*2) points are in each + # quadrant + sampler = iaa.UniformPointsSampler(10000) + images = [np.zeros((1000, 3000, 1), dtype=np.uint8)] + + points = sampler.sample_points(images, 1)[0] + points_rel = points.astype(np.float32) + points_rel[:, 1] /= 1000 + points_rel[:, 0] /= 3000 + + points_quadrants = np.clip( + np.floor(points_rel * 2), + 0, 1 + ).astype(np.int32) + n_points_per_quadrant = np.zeros((2, 2), dtype=np.int32) + np.add.at( + n_points_per_quadrant, + (points_quadrants[:, 1], points_quadrants[:, 0]), + 1) + + assert np.all(n_points_per_quadrant > 0.8*(10000/4)) + + def test_sampled_points_uniformly_distributed_by_distance_from_origin(self): + # Sample N points, compute distances from origin each axis, + # split into B bins, assume that each bin contains at least around + # N/B points. + sampler = iaa.UniformPointsSampler(10000) + images = [np.zeros((1000, 3000, 1), dtype=np.uint8)] + + points = sampler.sample_points(images, 1)[0] + points_rel = points.astype(np.float32) + points_rel[:, 1] /= 1000 + points_rel[:, 0] /= 3000 + + points_bins = np.clip( + np.floor(points_rel * 10), + 0, 1 + ).astype(np.int32) + + # Don't use euclidean (2d) distance here, but instead axis-wise (1d) + # distance. The euclidean distance leads to non-uniform density of + # distances, because points on the same "circle" have the same + # distance, and there are less points close/far away from the origin + # that fall on the same circle. + points_bincounts_x = np.bincount(points_bins[:, 0]) + points_bincounts_y = np.bincount(points_bins[:, 1]) + + assert np.all(points_bincounts_x > 0.8*(10000/10)) + assert np.all(points_bincounts_y > 0.8*(10000/10)) + + def test_many_images(self): + sampler = iaa.UniformPointsSampler(1000) + images = [ + np.zeros((100, 500, 3), dtype=np.uint8), + np.zeros((500, 100, 1), dtype=np.uint8) + ] + + points = sampler.sample_points(images, 1) + + assert len(points) == 2 + assert len(points[0]) == 1000 + assert len(points[1]) == 1000 + assert not np.allclose(points[0], points[1]) + assert np.any(points[0][:, 1] < 20) + assert np.any(points[0][:, 1] > 0.9*100) + assert np.any(points[0][:, 0] < 20) + assert np.any(points[0][:, 0] > 0.9*500) + assert np.any(points[1][:, 1] < 20) + assert np.any(points[1][:, 1] > 0.9*500) + assert np.any(points[1][:, 0] < 20) + assert np.any(points[1][:, 0] > 0.9*100) + + def test_always_at_least_one_point(self): + sampler = iaa.UniformPointsSampler(iap.Deterministic(0)) + images = [np.zeros((10, 10, 1), dtype=np.uint8)] + + points = sampler.sample_points(images, 1)[0] + + assert len(points) == 1 + + def test_n_points_can_vary_between_calls(self): + sampler = iaa.UniformPointsSampler(iap.Choice([1, 10])) + images = [np.zeros((10, 10, 1), dtype=np.uint8)] + + seen = {1: False, 10: False} + for i in sm.xrange(50): + points = sampler.sample_points(images, i)[0] + seen[len(points)] = True + if all(seen.values()): + break + + assert len(list(seen.keys())) == 2 + assert all(seen.values()) + + def test_n_points_can_vary_between_images(self): + sampler = iaa.UniformPointsSampler(iap.Choice([1, 10])) + images = [ + np.zeros((10, 10, 1), dtype=np.uint8) + for _ in sm.xrange(50)] + + points = sampler.sample_points(images, 1) + point_counts = set([len(points_i) for points_i in points]) + + assert len(points) == 50 + assert len(list(point_counts)) == 2 + assert 1 in point_counts + assert 10 in point_counts + + def test_determinism(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + sampler = iaa.UniformPointsSampler(100) + + observed_s1_1 = sampler.sample_points([image], 1)[0] + observed_s1_2 = sampler.sample_points([image], 1)[0] + observed_s2_1 = sampler.sample_points([image], 2)[0] + + assert np.allclose(observed_s1_1, observed_s1_2) + assert not np.allclose(observed_s1_1, observed_s2_1) + + def test_conversion_to_string(self): + sampler = iaa.UniformPointsSampler(10) + expected = "UniformPointsSampler(Deterministic(int 10))" + assert sampler.__str__() == sampler.__repr__() == expected + + +class TestSubsamplingPointSampler(unittest.TestCase): + def setUp(self): + reseed() + + def test___init__(self): + other = iaa.RegularGridPointsSampler(1, 1) + sampler = iaa.SubsamplingPointsSampler(other, 100) + assert sampler.other_points_sampler is other + assert sampler.n_points_max == 100 + + def test_max_is_zero(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + other = iaa.RegularGridPointsSampler(2, 2) + + with warnings.catch_warnings(record=True) as caught_warnings: + sampler = iaa.SubsamplingPointsSampler(other, 0) + + observed = sampler.sample_points([image], 1)[0] + + assert len(observed) == 0 + assert len(caught_warnings) == 1 + assert "n_points_max=0" in str(caught_warnings[-1].message) + + def test_max_is_above_point_count(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + other = iaa.RegularGridPointsSampler(2, 2) + sampler = iaa.SubsamplingPointsSampler(other, 100) + + observed = sampler.sample_points([image], 1)[0] + + assert len(observed) == 4 + assert np.allclose(observed, [ + [0.0, 0.0], + [10.0, 0.0], + [0.0, 10.0], + [10.0, 10.0] + ]) + + def test_max_is_below_point_count(self): + image = np.zeros((10, 10, 3), dtype=np.uint8) + other = iaa.RegularGridPointsSampler(5, 5) + sampler = iaa.SubsamplingPointsSampler(other, 1000) + + observed = sampler.sample_points([image], 1)[0] + + assert len(observed) == 5*5 + + def test_max_is_sometimes_below_point_count(self): + image = np.zeros((1, 10, 3), dtype=np.uint8) + other = iaa.RegularGridPointsSampler(1, (9, 11)) + sampler = iaa.SubsamplingPointsSampler(other, 1000) + + observed = sampler.sample_points([image] * 100, 1) + counts = [len(observed_i) for observed_i in observed] + counts_uq = set(counts) + + assert 9 in counts_uq + assert 10 in counts_uq + assert 11 not in counts_uq + + def test_random_state_propagates(self): + image = np.zeros((1, 1, 3), dtype=np.uint8) + points = np.linspace(0.0+0.9, 1000.0-0.9, num=1) + points = np.stack([points, points], axis=-1) + other = _FixedPointsSampler(points) + sampler = iaa.SubsamplingPointsSampler(other, 100) + + _ = sampler.sample_points([image], 1)[0] + rs_s1_1 = other.last_random_state + _ = sampler.sample_points([image], 1)[0] + rs_s1_2 = other.last_random_state + _ = sampler.sample_points([image], 2)[0] + rs_s2_1 = other.last_random_state + + # get_state() returns: tuple(str, ndarray of 624 uints, int, int, + # float) + # we compare the non-floats here + all_s1_identical = True + all_s1s2_identical = True + for i in sm.xrange(1, 4): + all_s1_identical = ( + all_s1_identical + and np.array_equal(rs_s1_1.get_state()[i], + rs_s1_2.get_state()[i])) + + all_s1s2_identical = ( + all_s1s2_identical + and np.array_equal(rs_s1_1.get_state()[i], + rs_s2_1.get_state()[i])) + + assert all_s1_identical + assert not all_s1s2_identical + + def test_conversion_to_string(self): + sampler = iaa.SubsamplingPointsSampler( + iaa.RegularGridPointsSampler(10, 20), + 10 + ) + expected = ( + "SubsamplingPointsSampler(" + "RegularGridPointsSampler(" + "Deterministic(int 10), " + "Deterministic(int 20)" + "), " + "10" + ")" + ) + assert sampler.__str__() == sampler.__repr__() == expected