diff --git a/CHANGELOG.md b/CHANGELOG.md index aee32a015..d64bd7f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,21 @@ and `multicore.Pool.imap_batches_unordered()` to control the maximum number of batches in the background augmentation pipeline (allows to limit maximum RAM demands). -* Increased `max_distance` thresholds for `almost_equals()`, `exterior_almost_equals()` and `coords_almost_equals()` in `Polygon` and `LineString` from `1e-6` to `1e-4`. +* Increased `max_distance` thresholds for `almost_equals()`, + `exterior_almost_equals()` and `coords_almost_equals()` in `Polygon` and + `LineString` from `1e-6` to `1e-4`. This should fix false-negative problems related to float inaccuracies. * Added module `imgaug.augmenters.edges`. -* Added interface `BinaryImageColorizerIf` to `imgaug.augmenters.edges`, which +* Added interface `augmenters.edges.BinaryImageColorizerIf`, which contains the interface for classes used to convert binary images to RGB images. -* Added `RandomColorsBinaryImageColorizer` to `imgaug.augmenters.edges`, which +* Added `augmenters.edges.RandomColorsBinaryImageColorizer`, which converts binary images to RGB images by sampling uniformly RGB colors for `True` and `False` values. -* Added augmenter `Canny`, which applies canny edge detection with alpha +* Added `augmenters.edges.Canny`, which applies canny edge detection with alpha blending and random coloring to images. -* Renamed `imgaug/external/poly_point_isect.py` to `imgaug/external/poly_point_isect_py3.py.bak`. +* Renamed `imgaug/external/poly_point_isect.py` to + `imgaug/external/poly_point_isect_py3.py.bak`. The file is in the library only for completeness and contains python3 syntax. `poly_point_isect_py2py3.py` is actually used. * Added dtype gating to `dtypes.clip_()`. @@ -25,23 +28,52 @@ * Added `augmenters.pooling.MaxPooling`. #317 * Added `augmenters.pooling.MinPooling`. #317 * Added `augmenters.pooling.MedianPooling`. #317 -* Refactored `augmenters/weather.py` (general code and docstring cleanup). +* `augmenters.color.AddToHueAndSaturation` + * [rarely breaking] Refactored `AddToHueAndSaturation` to clean it up. + Re-running old code with the same seeds will now produce different + images. #319 + * [rarely breaking] The `value` parameter is now interpreted by the + augmenter to return first the hue and then the saturation value to add, + instead of the other way round. + (This shouldn't affect anybody.) #319 + * [rarely breaking] Added `value_hue` and `value_saturation` arguments, + which allow to set individual parameters for hue and saturation + instead of having to use one parameter for both (they may not be set + if `value` is already set). + This changes the order of arguments of the augmenter and code that relied + on that order will now break. + This also changes the output of + `AddToHueAndSaturation.get_parameters()`. #319 +* Added `augmenters.color.AddToHue`, a shortcut for + `AddToHueAndSaturation(value_hue=...)`. #319 +* Added `augmenters.color.AddToSaturation`, a shortcut for + `AddToHueAndSaturation(value_saturation=...)`. #319 +* Added `augmenters.color.WithHueAndSaturation`. #319 +* Added `augmenters.color.MultiplyHueAndSaturation`. #319 +* Added `augmenters.color.MultiplyHue`. #319 +* Added `augmenters.color.MultiplySaturation`. #319 +* Refactored `augmenters/weather.py` (general code and docstring cleanup). #336 + ## Fixes * Fixed an issue with `Polygon.clip_out_of_image()`, which would lead to exceptions if a polygon had overlap with an image, but not a single one of its points was inside that image plane. -* Fixed `imgaug.multicore` falsely not accepting `imgaug.augmentables.batches.UnnormalizedBatch`. +* Fixed `multicore` methods falsely not accepting + `augmentables.batches.UnnormalizedBatch`. * `Rot90` now uses subpixel-based coordinate remapping. - I.e. any coordinate `(x, y)` will be mapped to `(H-y, x)` for a rotation by 90deg. + I.e. any coordinate `(x, y)` will be mapped to `(H-y, x)` for a rotation by + 90deg. Previously, an integer-based remapping to `(H-y-1, x)` was used. Coordinates are e.g. used by keypoints, bounding boxes or polygons. -* `Invert` - * `[rarely breaking]` If `min_value` and/or `max_value` arguments were set, `uint64` is no longer a valid input array dtype for `Invert`. +* `augmenters.arithmetic.Invert` + * [rarely breaking] If `min_value` and/or `max_value` arguments were + set, `uint64` is no longer a valid input array dtype for `Invert`. This is due to a conversion to `float64` resulting in loss of resolution. * Fixed `Invert` in rare cases restoring dtypes improperly. -* Fixed `dtypes.gate_dtypes()` crashing if the input was one or more numpy scalars instead of numpy arrays or dtypes. +* Fixed `dtypes.gate_dtypes()` crashing if the input was one or more numpy + scalars instead of numpy arrays or dtypes. # 0.2.9 diff --git a/checks/check_add_to_hue_and_saturation.py b/checks/check_add_to_hue_and_saturation.py index 6f67f5496..e8994b046 100644 --- a/checks/check_add_to_hue_and_saturation.py +++ b/checks/check_add_to_hue_and_saturation.py @@ -31,6 +31,12 @@ def main(): images_aug = iaa.AddToHueAndSaturation(value=(-255, 255), per_channel=True).augment_images([image] * 64) ia.imshow(ia.draw_grid(images_aug)) + image = ia.quokka_square((128, 128)) + images_aug = [] + images_aug.extend(iaa.AddToHue().augment_images([image] * 10)) + images_aug.extend(iaa.AddToSaturation().augment_images([image] * 10)) + ia.imshow(ia.draw_grid(images_aug, rows=2)) + if __name__ == "__main__": main() diff --git a/checks/check_multiply_hue_and_saturation.py b/checks/check_multiply_hue_and_saturation.py new file mode 100644 index 000000000..3c6e2c4f0 --- /dev/null +++ b/checks/check_multiply_hue_and_saturation.py @@ -0,0 +1,37 @@ +from __future__ import print_function, division + +import numpy as np + +import imgaug as ia +from imgaug import augmenters as iaa + + +def main(): + image = ia.quokka_square((128, 128)) + images_aug = [] + + for mul in np.linspace(0.0, 2.0, 10): + aug = iaa.MultiplyHueAndSaturation(mul) + image_aug = aug.augment_image(image) + images_aug.append(image_aug) + + for mul_hue in np.linspace(0.0, 5.0, 10): + aug = iaa.MultiplyHueAndSaturation(mul_hue=mul_hue) + image_aug = aug.augment_image(image) + images_aug.append(image_aug) + + for mul_saturation in np.linspace(0.0, 5.0, 10): + aug = iaa.MultiplyHueAndSaturation(mul_saturation=mul_saturation) + image_aug = aug.augment_image(image) + images_aug.append(image_aug) + + ia.imshow(ia.draw_grid(images_aug, rows=3)) + + images_aug = [] + images_aug.extend(iaa.MultiplyHue().augment_images([image] * 10)) + images_aug.extend(iaa.MultiplySaturation().augment_images([image] * 10)) + ia.imshow(ia.draw_grid(images_aug, rows=2)) + + +if __name__ == "__main__": + main() diff --git a/checks/check_with_hue_and_saturation.py b/checks/check_with_hue_and_saturation.py new file mode 100644 index 000000000..21dcb5d73 --- /dev/null +++ b/checks/check_with_hue_and_saturation.py @@ -0,0 +1,22 @@ +from __future__ import print_function, division +import imgaug as ia +import imgaug.augmenters as iaa + + +def main(): + image = ia.quokka_square(size=(128, 128)) + images = [] + + for i in range(15): + aug = iaa.WithHueAndSaturation(iaa.WithChannels(0, iaa.Add(i*20))) + images.append(aug.augment_image(image)) + + for i in range(15): + aug = iaa.WithHueAndSaturation(iaa.WithChannels(1, iaa.Add(i*20))) + images.append(aug.augment_image(image)) + + ia.imshow(ia.draw_grid(images, rows=2)) + + +if __name__ == "__main__": + main() diff --git a/imgaug/augmenters/color.py b/imgaug/augmenters/color.py index 0ade7c87f..1d0b7437f 100644 --- a/imgaug/augmenters/color.py +++ b/imgaug/augmenters/color.py @@ -1,5 +1,5 @@ """ -Augmenters that apply color space oriented changes. +Augmenters that affect image colors or image colorspaces. Do not import directly from this file, as the categorization is not final. Use instead :: @@ -17,7 +17,13 @@ * InColorspace (deprecated) * WithColorspace + * WithHueAndSaturation + * MultiplyHueAndSaturation + * MultiplyHue + * MultiplySaturation * AddToHueAndSaturation + * AddToHue + * AddToSaturation * ChangeColorspace * Grayscale @@ -30,16 +36,18 @@ from . import meta from . import blend +from . import arithmetic import imgaug as ia from .. import parameters as iap from .. import dtypes as iadt @ia.deprecated(alt_func="WithColorspace") -def InColorspace(to_colorspace, from_colorspace="RGB", children=None, name=None, deterministic=False, - random_state=None): +def InColorspace(to_colorspace, from_colorspace="RGB", children=None, + name=None, deterministic=False, random_state=None): """Convert images to another colorspace.""" - return WithColorspace(to_colorspace, from_colorspace, children, name, deterministic, random_state) + return WithColorspace(to_colorspace, from_colorspace, children, name, + deterministic, random_state) class WithColorspace(meta.Augmenter): @@ -89,6 +97,7 @@ class WithColorspace(meta.Augmenter): Examples -------- + >>> import imgaug.augmenters as iaa >>> aug = iaa.WithColorspace(to_colorspace="HSV", from_colorspace="RGB", >>> children=iaa.WithChannels(0, iaa.Add(10))) @@ -97,9 +106,10 @@ class WithColorspace(meta.Augmenter): """ - def __init__(self, to_colorspace, from_colorspace="RGB", children=None, name=None, deterministic=False, - random_state=None): - super(WithColorspace, self).__init__(name=name, deterministic=deterministic, random_state=random_state) + def __init__(self, to_colorspace, from_colorspace="RGB", children=None, + name=None, deterministic=False, random_state=None): + super(WithColorspace, self).__init__( + name=name, deterministic=deterministic, random_state=random_state) self.to_colorspace = to_colorspace self.from_colorspace = from_colorspace @@ -107,7 +117,8 @@ def __init__(self, to_colorspace, from_colorspace="RGB", children=None, name=Non def _augment_images(self, images, random_state, parents, hooks): result = images - if hooks is None or hooks.is_propagating(images, augmenter=self, parents=parents, default=True): + if hooks is None or hooks.is_propagating(images, augmenter=self, + parents=parents, default=True): result = ChangeColorspace( to_colorspace=self.to_colorspace, from_colorspace=self.from_colorspace @@ -125,7 +136,8 @@ def _augment_images(self, images, random_state, parents, hooks): def _augment_heatmaps(self, heatmaps, random_state, parents, hooks): result = heatmaps - if hooks is None or hooks.is_propagating(heatmaps, augmenter=self, parents=parents, default=True): + if hooks is None or hooks.is_propagating(heatmaps, augmenter=self, + parents=parents, default=True): result = self.children.augment_heatmaps( result, parents=parents + [self], @@ -133,9 +145,12 @@ def _augment_heatmaps(self, heatmaps, random_state, parents, hooks): ) return result - def _augment_keypoints(self, keypoints_on_images, random_state, parents, hooks): + def _augment_keypoints(self, keypoints_on_images, random_state, parents, + hooks): result = keypoints_on_images - if hooks is None or hooks.is_propagating(keypoints_on_images, augmenter=self, parents=parents, default=True): + if hooks is None or hooks.is_propagating(keypoints_on_images, + augmenter=self, + parents=parents, default=True): result = self.children.augment_keypoints( result, parents=parents + [self], @@ -157,18 +172,548 @@ def get_children_lists(self): return [self.children] def __str__(self): - return "WithColorspace(from_colorspace=%s, to_colorspace=%s, name=%s, children=[%s], deterministic=%s)" % ( - self.from_colorspace, self.to_colorspace, self.name, self.children, self.deterministic) + return ( + "WithColorspace(from_colorspace=%s, " + "to_colorspace=%s, name=%s, children=[%s], deterministic=%s)" % ( + self.from_colorspace, self.to_colorspace, self.name, + self.children, self.deterministic) + ) + + +# TODO Merge this into WithColorspace? A bit problematic due to int16 +# conversion that would make WithColorspace less flexible. +# TODO add option to choose overflow behaviour for hue and saturation channels, +# e.g. clip, modulo or wrap +class WithHueAndSaturation(meta.Augmenter): + """ + Apply child augmenters to hue and saturation channels. + + This augumenter takes an image in a source colorspace, converts + it to HSV, extracts the H (hue) and S (saturation) channels, + applies the provided child augmenters to these channels + and finally converts back to the original colorspace. + + The image array generated by this augmenter and provided to its children + is in ``int16`` (**sic!** only augmenters that can handle ``int16`` arrays + can be children!). The hue channel is mapped to the value + range ``[0, 255]``. Before converting back to the source colorspace, the + saturation channel's values are clipped to ``[0, 255]``. A modulo operation + is applied to the hue channel's values, followed by a mapping from + ``[0, 255]`` to ``[0, 180]`` (and finally the colorspace conversion). + + dtype support:: + + * ``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 + + Parameters + ---------- + from_colorspace : str, optional + See :func:`imgaug.augmenters.ChangeColorspace.__init__`. + + children : None or Augmenter or list of Augmenters, optional + See :func:`imgaug.augmenters.ChangeColorspace.__init__`. + + 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.WithHueAndSaturation(iaa.WithChannels(0, iaa.Add(10))) + + This creates an augmenter that will add 10 to the hue value in HSV + colorspace. It automatically accounts for the hue being in angular + representation, i.e. if the angle goes beyond 360deg, it will start again + at 0deg. The colorspace is finally converted back to RGB (the default + setting). + + >>> import imgaug.augmenters as iaa + >>> aug = iaa.WithHueAndSaturation([ + >>> iaa.WithChannels(0, iaa.Add((-30, 10))), + >>> iaa.WithChannels(1, [ + >>> iaa.Multiply((0.5, 1.5)), + >>> iaa.LinearContrast((0.75, 1.25)) + >>> ]) + >>> ]) + + Creates an augmenter that adds a random value sampled from uniformly + from the range ``[-30, 10]`` to the hue and multiplies the saturation + by a random factor sampled uniformly from ``[0.5, 1.5]``. It also + modifies the contrast of the saturation channel. After these steps, + the HSV image is converted back to RGB. + + """ + + def __init__(self, children=None, from_colorspace="RGB", name=None, + deterministic=False, random_state=None): + super(WithHueAndSaturation, self).__init__( + name=name, deterministic=deterministic, random_state=random_state) + + self.children = meta.handle_children_list(children, self.name, "then") + self.from_colorspace = from_colorspace + + # this dtype needs to be able to go beyond [0, 255] to e.g. accomodate + # for Add or Multiply + self._internal_dtype = np.int16 + + def _augment_images(self, images, random_state, parents, hooks): + iadt.gate_dtypes( + images, + allowed=["uint8"], + disallowed=[ + "bool", + "uint16", "uint32", "uint64", "uint128", "uint256", + "int32", "int64", "int128", "int256", + "float16", "float32", "float64", "float96", "float128", + "float256"], + augmenter=self) + + result = images + if hooks is None or hooks.is_propagating(images, augmenter=self, + parents=parents, default=True): + # RGB (or other source colorspace) -> HSV + images_hsv = ChangeColorspace( + to_colorspace=ChangeColorspace.HSV, + from_colorspace=self.from_colorspace + ).augment_images(images) + + # HSV -> HS + hue_and_sat = [] + for image_hsv in images_hsv: + image_hsv = image_hsv.astype(np.int16) + # project hue from [0,180] to [0,255] so that child augmenters + # can assume the same value range for all channels + hue = ( + (image_hsv[:, :, 0].astype(np.float32) / 180.0) * 255.0 + ).astype(self._internal_dtype) + saturation = image_hsv[:, :, 1] + hue_and_sat.append(np.stack([hue, saturation], axis=-1)) + if ia.is_np_array(images_hsv): + hue_and_sat = np.stack(hue_and_sat, axis=0) + + # apply child augmenters to HS + hue_and_sat_aug = self.children.augment_images( + images=hue_and_sat, + parents=parents + [self], + hooks=hooks + ) + + # postprocess augmented HS int16 data + # hue: modulo to [0, 255] then project to [0, 360/2] + # saturation: clip to [0, 255] + # + convert to uint8 + # + re-attach V channel to HS + hue_and_sat_proj = [] + for i, hs_aug in enumerate(hue_and_sat_aug): + hue_aug = hs_aug[:, :, 0] + sat_aug = hs_aug[:, :, 1] + hue_aug = ( + (np.mod(hue_aug, 255).astype(np.float32) / 255.0) * (360/2) + ).astype(np.uint8) + sat_aug = iadt.clip_(sat_aug, 0, 255).astype(np.uint8) + hue_and_sat_proj.append( + np.stack([hue_aug, sat_aug, images_hsv[i][:, :, 2]], + axis=-1) + ) + if ia.is_np_array(hue_and_sat_aug): + hue_and_sat_proj = np.uint8(hue_and_sat_proj) + + # HSV -> RGB (or whatever the source colorspace was) + result = ChangeColorspace( + to_colorspace=self.from_colorspace, + from_colorspace=ChangeColorspace.HSV + ).augment_images(hue_and_sat_proj) + return result + + def _augment_heatmaps(self, heatmaps, random_state, parents, hooks): + result = heatmaps + if hooks is None or hooks.is_propagating(heatmaps, augmenter=self, + parents=parents, default=True): + result = self.children.augment_heatmaps( + result, + parents=parents + [self], + hooks=hooks, + ) + return result + + def _augment_keypoints(self, keypoints_on_images, random_state, parents, + hooks): + result = keypoints_on_images + if hooks is None or hooks.is_propagating(keypoints_on_images, + augmenter=self, + parents=parents, default=True): + result = self.children.augment_keypoints( + result, + parents=parents + [self], + hooks=hooks, + ) + return result + + def _to_deterministic(self): + aug = self.copy() + aug.children = aug.children.to_deterministic() + aug.deterministic = True + aug.random_state = ia.derive_random_state(self.random_state) + return aug + + def get_parameters(self): + return [self.from_colorspace] + + def get_children_lists(self): + return [self.children] + + def __str__(self): + return ( + "WithHueAndSaturation(from_colorspace=%s, " + "name=%s, children=[%s], deterministic=%s)" % ( + self.from_colorspace, self.name, + self.children, self.deterministic) + ) + + +def MultiplyHueAndSaturation(mul=None, mul_hue=None, mul_saturation=None, + per_channel=False, from_colorspace="RGB", + name=None, deterministic=False, + random_state=None): + """ + Augmenter that multiplies hue and saturation by random values. + + The augmenter first transforms images to HSV colorspace, then multiplies + the pixel values in the H and S channels and afterwards converts back to + RGB. + + This augmenter is a wrapper around ``WithHueAndSaturation``. + The performance is expected to be worse than the one + of ``AddToHueAndSaturation``. + + dtype support:: + + See `imgaug.augmenters.color.WithHueAndSaturation`. + + Parameters + ---------- + mul : None or number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Multiplier with which to multiply all hue *and* saturation values of + all pixels. + It is expected to be in the range ``-10.0`` to ``+10.0``. + Note that values of ``0.0`` or lower will remove all saturation. + + * If this is ``None``, `mul_hue` and/or `mul_saturation` + may be set to values other than ``None``. + * If a number, then that multiplier will be used for all images. + * If a tuple ``(a, b)``, then a value from the continuous + range ``[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 a value will be sampled from that + parameter per image. + + mul_hue : None or number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Multiplier with which to multiply all hue values. + This is expected to be in the range ``-10.0`` to ``+10.0`` and will + automatically be projected to an angular representation using + ``(hue/255) * (360/2)`` (OpenCV's hue representation is in the + range ``[0, 180]`` instead of ``[0, 360]``). + Only this or `mul` may be set, not both. + + * If this and `mul_saturation` are both ``None``, `mul` may + be set to a non-``None`` value. + * If a number, then that multiplier will be used for all images. + * If a tuple ``(a, b)``, then a value from the continuous + range ``[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 a value will be sampled from that + parameter per image. + + mul_saturation : None or number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Multiplier with which to multiply all saturation values. + It is expected to be in the range ``0.0`` to ``+10.0``. + Only this or `mul` may be set, not both. + + * If this and `mul_hue` are both ``None``, `mul` may + be set to a non-``None`` value. + * If a number, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value from the continuous + range ``[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 a value will be sampled from that + parameter per image. + + per_channel : bool or float, optional + Whether to sample per image only one value from `mul` and use it for + both hue and saturation (``False``) or to sample independently one + value for hue and one for saturation (``True``). + If this value is a float ``p``, then for ``p`` percent of all images + `per_channel` will be treated as ``True``, otherwise as ``False``. + + This parameter has no effect is `mul_hue` and/or `mul_saturation` + are used instead of `value`. + + from_colorspace : str, optional + See :func:`imgaug.augmenters.color.ChangeColorspace.__init__`. + + 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.MultiplyHueAndSaturation((0.5, 1.5), per_channel=True) + + Multiplies the hue and saturation with random values between 0.5 and 1.5 + (independently per channel and the same value for all pixels within + that channel). The hue will be automatically projected to an angular + representation. + + """ + if mul is not None: + assert mul_hue is None, ( + "`mul_hue` may not be set if `mul` is set. " + "It is set to: %s (type: %s)." % ( + str(mul_hue), type(mul_hue)) + ) + assert mul_saturation is None, ( + "`mul_saturation` may not be set if `mul` is set. " + "It is set to: %s (type: %s)." % ( + str(mul_saturation), type(mul_saturation)) + ) + mul = iap.handle_continuous_param( + mul, "mul", value_range=(-10.0, 10.0), tuple_to_uniform=True, + list_to_choice=True) + else: + if mul_hue is not None: + mul_hue = iap.handle_continuous_param( + mul_hue, "mul_hue", value_range=(-10.0, 10.0), + tuple_to_uniform=True, list_to_choice=True) + if mul_saturation is not None: + mul_saturation = iap.handle_continuous_param( + mul_saturation, "mul_saturation", value_range=(0.0, 10.0), + tuple_to_uniform=True, list_to_choice=True) + + if name is None: + name = "Unnamed%s" % (ia.caller_name(),) + + if random_state is None: + rss = [None] * 5 + else: + rss = ia.derive_random_states(random_state, 5) + + children = [] + if mul is not None: + children.append( + arithmetic.Multiply( + mul, + per_channel=per_channel, + name="%s-Multiply" % (name,), + random_state=rss[0], + deterministic=deterministic + ) + ) + else: + if mul_hue is not None: + children.append( + meta.WithChannels( + 0, + arithmetic.Multiply( + mul_hue, + name="%s-MultiplyHue" % (name,), + random_state=rss[0], + deterministic=deterministic + ), + name="%s-WithChannelsHue" % (name,), + random_state=rss[1], + deterministic=deterministic + ) + ) + if mul_saturation is not None: + children.append( + meta.WithChannels( + 1, + arithmetic.Multiply( + mul_saturation, + name="%s-MultiplySaturation" % (name,), + random_state=rss[2], + deterministic=deterministic + ), + name="%s-WithChannelsSaturation" % (name,), + random_state=rss[3], + deterministic=deterministic + ) + ) + + if children: + return WithHueAndSaturation( + children, + from_colorspace=from_colorspace, + name=name, + random_state=rss[4], + deterministic=deterministic + ) + + # mul, mul_hue and mul_saturation were all None + return meta.Noop(name=name, random_state=rss[4], + deterministic=deterministic) + + +def MultiplyHue(mul=(-1.0, 1.0), from_colorspace="RGB", name=None, + deterministic=False, random_state=None): + """ + Augmenter that multiplies the hue of images by random values. + + The augmenter first transforms images to HSV colorspace, then multiplies + the pixel values in the H channel and afterwards converts back to + RGB. + + This augmenter is a shortcut for ``MultiplyHueAndSaturation(mul_hue=...)``. + + dtype support:: + + See `imgaug.augmenters.color.MultiplyHueAndSaturation`. + + Parameters + ---------- + mul : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Multiplier with which to multiply all hue values. + This is expected to be in the range ``-10.0`` to ``+10.0`` and will + automatically be projected to an angular representation using + ``(hue/255) * (360/2)`` (OpenCV's hue representation is in the + range ``[0, 180]`` instead of ``[0, 360]``). + Only this or `mul` may be set, not both. + + * If a number, then that multiplier will be used for all images. + * If a tuple ``(a, b)``, then a value from the continuous + range ``[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 a value will be sampled from that + parameter per image. + + from_colorspace : str, optional + See :func:`imgaug.augmenters.color.ChangeColorspace.__init__`. + + 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.MultiplyHue((0.5, 1.5)) + + Multiplies the hue with random values between 0.5 and 1.5. + The hue will be automatically projected to an angular representation. + + """ + if name is None: + name = "Unnamed%s" % (ia.caller_name(),) + return MultiplyHueAndSaturation(mul_hue=mul, + from_colorspace=from_colorspace, + name=name, + deterministic=deterministic, + random_state=random_state) + + +def MultiplySaturation(mul=(0.0, 3.0), from_colorspace="RGB", name=None, + deterministic=False, random_state=None): + """ + Augmenter that multiplies the saturation of images by random values. + + The augmenter first transforms images to HSV colorspace, then multiplies + the pixel values in the H channel and afterwards converts back to + RGB. + + This augmenter is a shortcut for + ``MultiplyHueAndSaturation(mul_saturation=...)``. + + dtype support:: + + See `imgaug.augmenters.color.MultiplyHueAndSaturation`. + + Parameters + ---------- + mul : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional + Multiplier with which to multiply all saturation values. + It is expected to be in the range ``0.0`` to ``+10.0``. + + * If a number, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value from the continuous + range ``[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 a value will be sampled from that + parameter per image. + + from_colorspace : str, optional + See :func:`imgaug.augmenters.color.ChangeColorspace.__init__`. + + 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.MultiplySaturation((0.5, 1.5)) + + Multiplies the saturation with random values between 0.5 and 1.5. + + """ + if name is None: + name = "Unnamed%s" % (ia.caller_name(),) + return MultiplyHueAndSaturation(mul_saturation=mul, + from_colorspace=from_colorspace, + name=name, + deterministic=deterministic, + random_state=random_state) # TODO removed deterministic and random_state here as parameters, because this # function creates multiple child augmenters. not sure if this is sensible # (give them all the same random state instead?) -# TODO this is for now deactivated, because HSV images returned by opencv have value range 0-180 for the hue channel -# and are supposed to be angular representations, i.e. if values go below 0 or above 180 they are supposed to overflow -# to 180 and 0 +# TODO this is for now deactivated, because HSV images returned by opencv have +# value range 0-180 for the hue channel +# and are supposed to be angular representations, i.e. if values go below +# 0 or above 180 they are supposed to overflow +# to 180 and 0 """ -def AddToHueAndSaturation(value=0, per_channel=False, from_colorspace="RGB", channels=[0, 1], name=None): # pylint: disable=locally-disabled, dangerous-default-value, line-too-long +def AddToHueAndSaturation(value=0, per_channel=False, from_colorspace="RGB", + channels=[0, 1], name=None): # pylint: disable=locally-disabled, dangerous-default-value, line-too-long "" Augmenter that transforms images into HSV space, selects the H and S channels and then adds a given range of values to these. @@ -218,8 +763,11 @@ class AddToHueAndSaturation(meta.Augmenter): """ Augmenter that increases/decreases hue and saturation by random values. - The augmenter first transforms images to HSV colorspace, then adds random values to the H and S channels - and afterwards converts back to RGB. + The augmenter first transforms images to HSV colorspace, then adds random + values to the H and S channels and afterwards converts back to RGB. + + This augmenter is faster than using ``WithHueAndSaturation`` in combination + with ``Add``. TODO add float support @@ -241,146 +789,457 @@ class AddToHueAndSaturation(meta.Augmenter): Parameters ---------- - value : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional - See :func:`imgaug.augmenters.arithmetic.Add.__init__()`. + value : None or int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Value to add to the hue *and* saturation of all pixels. + It is expected to be in the range ``-255`` to ``+255``. + + * If this is ``None``, `value_hue` and/or `value_saturation` + may be set to values other than ``None``. + * If an integer, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value from the discrete + range ``[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 a value will be sampled from that + parameter per image. + + value_hue : None or int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Value to add to the hue of all pixels. + This is expected to be in the range ``-255`` to ``+255`` and will + automatically be projected to an angular representation using + ``(hue/255) * (360/2)`` (OpenCV's hue representation is in the + range ``[0, 180]`` instead of ``[0, 360]``). + Only this or `value` may be set, not both. + + * If this and `value_saturation` are both ``None``, `value` may + be set to a non-``None`` value. + * If an integer, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value from the discrete + range ``[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 a value will be sampled from that + parameter per image. + + value_saturation : None or int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Value to add to the saturation of all pixels. + It is expected to be in the range ``-255`` to ``+255``. + Only this or `value` may be set, not both. + + * If this and `value_hue` are both ``None``, `value` may + be set to a non-``None`` value. + * If an integer, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value from the discrete + range ``[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 a value will be sampled from that + parameter per image. per_channel : bool or float, optional - See :func:`imgaug.augmenters.arithmetic.Add.__init__()`. + Whether to sample per image only one value from `value` and use it for + both hue and saturation (``False``) or to sample independently one + value for hue and one for saturation (``True``). + If this value is a float ``p``, then for ``p`` percent of all images + `per_channel` will be treated as ``True``, otherwise as ``False``. + + This parameter has no effect is `value_hue` and/or `value_saturation` + are used instead of `value`. from_colorspace : str, optional See :func:`imgaug.augmenters.color.ChangeColorspace.__init__()`. - channels : int or list of int or None, optional - See :func:`imgaug.augmenters.meta.WithChannels.__init__()`. - 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 -------- - >>> aug = AddToHueAndSaturation((-20, 20), per_channel=True) + >>> import imgaug.augmenters as iaa + >>> aug = iaa.AddToHueAndSaturation((-20, 20), per_channel=True) Adds random values between -20 and 20 to the hue and saturation (independently per channel and the same value for all pixels within - that channel). + that channel). The hue will be automatically projected to an angular + representation. """ _LUT_CACHE = None - def __init__(self, value=0, per_channel=False, from_colorspace="RGB", name=None, deterministic=False, - random_state=None): - super(AddToHueAndSaturation, self).__init__(name=name, deterministic=deterministic, random_state=random_state) - - self.value = iap.handle_discrete_param(value, "value", value_range=(-255, 255), tuple_to_uniform=True, - list_to_choice=True, allow_floats=False) - self.per_channel = iap.handle_probability_param(per_channel, "per_channel") - - # we don't change these in a modified to_deterministic() here, because they are called in _augment_images() - # with random states - self.colorspace_changer = ChangeColorspace(from_colorspace=from_colorspace, to_colorspace="HSV") - self.colorspace_changer_inv = ChangeColorspace(from_colorspace="HSV", to_colorspace=from_colorspace) + def __init__(self, value=None, value_hue=None, value_saturation=None, + per_channel=False, from_colorspace="RGB", + name=None, deterministic=False, random_state=None): + super(AddToHueAndSaturation, self).__init__( + name=name, deterministic=deterministic, random_state=random_state) + + self.value = self._handle_value_arg(value, value_hue, value_saturation) + self.value_hue = self._handle_value_hue_arg(value_hue) + self.value_saturation = self._handle_value_saturation_arg( + value_saturation) + self.per_channel = iap.handle_probability_param(per_channel, + "per_channel") + + # we don't change these in a modified to_deterministic() here, + # because they are called in _augment_images() with random states + self.colorspace_changer = ChangeColorspace( + from_colorspace=from_colorspace, to_colorspace="HSV") + self.colorspace_changer_inv = ChangeColorspace( + from_colorspace="HSV", to_colorspace=from_colorspace) self.backend = "cv2" # precompute tables for cv2.LUT if self.backend == "cv2" and self._LUT_CACHE is None: - self._LUT_CACHE = (np.zeros((256*2, 256), dtype=np.int8), - np.zeros((256*2, 256), dtype=np.int8)) - value_range = np.arange(0, 256, dtype=np.int16) - # this could be done slightly faster by vectorizing the loop - for i in sm.xrange(-255, 255+1): - table_hue = np.mod(value_range + i, 180) - table_saturation = np.clip(value_range + i, 0, 255) - self._LUT_CACHE[0][i, :] = table_hue - self._LUT_CACHE[1][i, :] = table_saturation + self._LUT_CACHE = self._generate_lut_table() + + def _draw_samples(self, augmentables, random_state): + nb_images = len(augmentables) + rss = ia.derive_random_states(random_state, 2) + + if self.value is not None: + per_channel = self.per_channel.draw_samples( + (nb_images,), random_state=rss[0]) + per_channel = (per_channel > 0.5) + + samples = self.value.draw_samples( + (nb_images, 2), random_state=rss[1]).astype(np.int32) + assert (-255 <= samples[0, 0] <= 255), ( + "Expected values sampled from `value` in AddToHueAndSaturation " + "to be in range [-255, 255], but got %.8f." % (samples[0, 0]) + ) + + samples_hue = samples[:, 0] + samples_saturation = np.copy(samples[:, 0]) + samples_saturation[per_channel] = samples[per_channel, 1] + else: + if self.value_hue is not None: + samples_hue = self.value_hue.draw_samples( + (nb_images,), random_state=rss[0]).astype(np.int32) + else: + samples_hue = np.zeros((nb_images,), dtype=np.int32) + + if self.value_saturation is not None: + samples_saturation = self.value_saturation.draw_samples( + (nb_images,), random_state=rss[1]).astype(np.int32) + else: + samples_saturation = np.zeros((nb_images,), dtype=np.int32) + + # project hue to angular representation + # OpenCV uses range [0, 180] for the hue + samples_hue = ( + (samples_hue.astype(np.float32) / 255.0) * (360/2) + ).astype(np.int32) + + return samples_hue, samples_saturation def _augment_images(self, images, random_state, parents, hooks): + iadt.gate_dtypes( + images, + allowed=["uint8"], + disallowed=[ + "bool", + "uint16", "uint32", "uint64", "uint128", "uint256", + "int32", "int64", "int128", "int256", + "float16", "float32", "float64", "float96", "float128", + "float256"], + augmenter=self) + input_dtypes = iadt.copy_dtypes_for_restore(images, force_list=True) result = images - nb_images = len(images) - # surprisingly, placing this here seems to be slightly slower than placing it inside the loop + # surprisingly, placing this here seems to be slightly slower than + # placing it inside the loop # if isinstance(images_hsv, list): # images_hsv = [img.astype(np.int32) for img in images_hsv] # else: # images_hsv = images_hsv.astype(np.int32) rss = ia.derive_random_states(random_state, 3) - images_hsv = self.colorspace_changer._augment_images(images, rss[0], parents + [self], hooks) - samples = self.value.draw_samples((nb_images, 2), random_state=rss[1]).astype(np.int32) - samples_hue = ((samples.astype(np.float32) / 255.0) * (360/2)).astype(np.int32) - per_channel = self.per_channel.draw_samples((nb_images,), random_state=rss[2]) - rs_inv = random_state - - ia.do_assert(-255 <= samples[0, 0] <= 255) + images_hsv = self.colorspace_changer._augment_images( + images, rss[0], parents + [self], hooks) + samples = self._draw_samples(images, rss[1]) + hues = samples[0] + saturations = samples[1] + rs_inv = rss[2] # this is needed if no cache for LUT is used: # value_range = np.arange(0, 256, dtype=np.int16) - gen = enumerate(zip(images_hsv, samples, samples_hue, per_channel)) - for i, (image_hsv, samples_i, samples_hue_i, per_channel_i) in gen: + gen = enumerate(zip(images_hsv, hues, saturations)) + for i, (image_hsv, hue_i, saturation_i) in gen: assert image_hsv.dtype.name == "uint8" - sample_saturation = samples_i[0] - if per_channel_i > 0.5: - sample_hue = samples_hue_i[1] - else: - sample_hue = samples_hue_i[0] - if self.backend == "cv2": - # this has roughly the same speed as the numpy backend for 64x64 and is about 25% faster for 224x224 - - # code without using cache: - # table_hue = np.mod(value_range + sample_hue, 180) - # table_saturation = np.clip(value_range + sample_saturation, 0, 255) - - # table_hue = table_hue.astype(np.uint8, copy=False) - # table_saturation = table_saturation.astype(np.uint8, copy=False) - - # image_hsv[..., 0] = cv2.LUT(image_hsv[..., 0], table_hue) - # image_hsv[..., 1] = cv2.LUT(image_hsv[..., 1], table_saturation) - - # code with using cache (at best maybe 10% faster for 64x64): - image_hsv[..., 0] = cv2.LUT(image_hsv[..., 0], self._LUT_CACHE[0][int(sample_hue)]) - image_hsv[..., 1] = cv2.LUT(image_hsv[..., 1], self._LUT_CACHE[1][int(sample_saturation)]) + image_hsv = self._transform_image_cv2( + image_hsv, hue_i, saturation_i) else: - image_hsv = image_hsv.astype(np.int16) # int16 seems to be slightly faster than int32 - # np.mod() works also as required here for negative values - image_hsv[..., 0] = np.mod(image_hsv[..., 0] + sample_hue, 180) - image_hsv[..., 1] = np.clip(image_hsv[..., 1] + sample_saturation, 0, 255) + image_hsv = self._transform_image_numpy( + image_hsv, hue_i, saturation_i) image_hsv = image_hsv.astype(input_dtypes[i]) - # the inverse colorspace changer has a deterministic output (always , so that can - # always provide it the same random state as input - image_rgb = self.colorspace_changer_inv._augment_images([image_hsv], rs_inv, parents + [self], hooks)[0] + # the inverse colorspace changer has a deterministic output + # (always , so that can always provide it the + # same random state as input + image_rgb = self.colorspace_changer_inv._augment_images( + [image_hsv], rs_inv, parents + [self], hooks)[0] result[i] = image_rgb return result + def _transform_image_cv2(self, image_hsv, hue, saturation): + # this has roughly the same speed as the numpy backend + # for 64x64 and is about 25% faster for 224x224 + + # code without using cache: + # table_hue = np.mod(value_range + sample_hue, 180) + # table_saturation = np.clip(value_range + sample_saturation, 0, 255) + + # table_hue = table_hue.astype(np.uint8, copy=False) + # table_saturation = table_saturation.astype(np.uint8, copy=False) + + # image_hsv[..., 0] = cv2.LUT(image_hsv[..., 0], table_hue) + # image_hsv[..., 1] = cv2.LUT(image_hsv[..., 1], table_saturation) + + # code with using cache (at best maybe 10% faster for 64x64): + image_hsv[..., 0] = cv2.LUT( + image_hsv[..., 0], self._LUT_CACHE[0][int(hue)]) + image_hsv[..., 1] = cv2.LUT( + image_hsv[..., 1], self._LUT_CACHE[1][int(saturation)]) + return image_hsv + + @classmethod + def _transform_image_numpy(cls, image_hsv, hue, saturation): + # int16 seems to be slightly faster than int32 + image_hsv = image_hsv.astype(np.int16) + # np.mod() works also as required here for negative values + image_hsv[..., 0] = np.mod(image_hsv[..., 0] + hue, 180) + image_hsv[..., 1] = np.clip( + image_hsv[..., 1] + saturation, 0, 255) + return image_hsv + 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): + 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.value, self.per_channel] + return [self.value, self.value_hue, self.value_saturation, + self.per_channel] + + @classmethod + def _handle_value_arg(cls, value, value_hue, value_saturation): + if value is not None: + assert value_hue is None, ( + "`value_hue` may not be set if `value` is set. " + "It is set to: %s (type: %s)." % ( + str(value_hue), type(value_hue)) + ) + assert value_saturation is None, ( + "`value_saturation` may not be set if `value` is set. " + "It is set to: %s (type: %s)." % ( + str(value_saturation), type(value_saturation)) + ) + return iap.handle_discrete_param( + value, "value", value_range=(-255, 255), tuple_to_uniform=True, + list_to_choice=True, allow_floats=False) + + return None + + @classmethod + def _handle_value_hue_arg(cls, value_hue): + if value_hue is not None: + # we don't have to verify here that value is None, as the + # exclusivity was already ensured in _handle_value_arg() + return iap.handle_discrete_param( + value_hue, "value_hue", value_range=(-255, 255), + tuple_to_uniform=True, list_to_choice=True, allow_floats=False) + + return None + + @classmethod + def _handle_value_saturation_arg(cls, value_saturation): + if value_saturation is not None: + # we don't have to verify here that value is None, as the + # exclusivity was already ensured in _handle_value_arg() + return iap.handle_discrete_param( + value_saturation, "value_saturation", value_range=(-255, 255), + tuple_to_uniform=True, list_to_choice=True, allow_floats=False) + return None + + @classmethod + def _generate_lut_table(cls): + table = (np.zeros((256*2, 256), dtype=np.int8), + np.zeros((256*2, 256), dtype=np.int8)) + value_range = np.arange(0, 256, dtype=np.int16) + # this could be done slightly faster by vectorizing the loop + for i in sm.xrange(-255, 255+1): + table_hue = np.mod(value_range + i, 180) + table_saturation = np.clip(value_range + i, 0, 255) + table[0][i, :] = table_hue + table[1][i, :] = table_saturation + return table + + +def AddToHue(value=(-255, 255), from_colorspace="RGB", name=None, + deterministic=False, random_state=None): + """ + Add random values to the hue of images. + + The augmenter first transforms images to HSV colorspace, then adds random + values to the H channel and afterwards converts back to RGB. + + If you want to change both the hue and the saturation, it is recommended + to use ``AddToHueAndSaturation`` as otherwise the image will be + converted twice to HSV and back to RGB. + + This augmenter is a shortcut for ``AddToHueAndSaturation(value_hue=...)``. + + dtype support:: + + See `imgaug.augmenters.color.AddToHueAndSaturation`. + + Parameters + ---------- + value : None or int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Value to add to the hue of all pixels. + This is expected to be in the range ``-255`` to ``+255`` and will + automatically be projected to an angular representation using + ``(hue/255) * (360/2)`` (OpenCV's hue representation is in the + range ``[0, 180]`` instead of ``[0, 360]``). + + * If an integer, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value from the discrete + range ``[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 a value will be sampled from that + parameter per image. + + from_colorspace : str, optional + See :func:`imgaug.augmenters.color.ChangeColorspace.__init__()`. + + 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.AddToHue((-20, 20)) + + Samples random values from the discrete uniform range ``[-20..20]``, + converts them to angular representation and adds them to the hue, i.e. + to the H channel in HSV colorspace. + + """ + if name is None: + name = "Unnamed%s" % (ia.caller_name(),) + + return AddToHueAndSaturation( + value_hue=value, + from_colorspace=from_colorspace, + name=name, + deterministic=deterministic, + random_state=random_state) + + +def AddToSaturation(value=(-75, 75), from_colorspace="RGB", name=None, + deterministic=False, random_state=None): + """ + Add random values to the saturation of images. + + The augmenter first transforms images to HSV colorspace, then adds random + values to the S channel and afterwards converts back to RGB. + + If you want to change both the hue and the saturation, it is recommended + to use ``AddToHueAndSaturation`` as otherwise the image will be + converted twice to HSV and back to RGB. + + This augmenter is a shortcut for + ``AddToHueAndSaturation(value_saturation=...)``. + + dtype support:: + + See `imgaug.augmenters.color.AddToHueAndSaturation`. + + Parameters + ---------- + value : None or int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional + Value to add to the saturation of all pixels. + It is expected to be in the range ``-255`` to ``+255``. + + * If an integer, then that value will be used for all images. + * If a tuple ``(a, b)``, then a value from the discrete + range ``[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 a value will be sampled from that + parameter per image. + + from_colorspace : str, optional + See :func:`imgaug.augmenters.color.ChangeColorspace.__init__()`. + + 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.AddToSaturation((-20, 20)) + + Samples random values from the discrete uniform range ``[-20..20]``, + and adds them to the saturation, i.e. to the S channel in HSV colorspace. + + """ + if name is None: + name = "Unnamed%s" % (ia.caller_name(),) + + return AddToHueAndSaturation( + value_saturation=value, + from_colorspace=from_colorspace, + name=name, + deterministic=deterministic, + random_state=random_state) # TODO tests -# Note: Not clear whether this class will be kept (for anything aside from grayscale) -# other colorspaces dont really make sense and they also might not work correctly -# due to having no clearly limited range (like 0-255 or 0-1) -# TODO rename to ChangeColorspace3D and then introduce ChangeColorspace, which does not enforce 3d images? +# Note: Not clear whether this class will be kept (for anything aside from +# grayscale) +# other colorspaces dont really make sense and they also might not work +# correctly due to having no clearly limited range (like 0-255 or 0-1) +# TODO rename to ChangeColorspace3D and then introduce ChangeColorspace, which +# does not enforce 3d images? class ChangeColorspace(meta.Augmenter): """ Augmenter to change the colorspace of images. - NOTE: This augmenter is not tested. Some colorspaces might work, others might not. + **Note**: This augmenter is not tested. Some colorspaces might work, others + might not. - NOTE: This augmenter tries to project the colorspace value range on 0-255. It outputs dtype=uint8 images. + **Note**: This augmenter tries to project the colorspace value range on + 0-255. It outputs dtype=uint8 images. TODO check dtype support @@ -404,8 +1263,10 @@ class ChangeColorspace(meta.Augmenter): ---------- to_colorspace : str or list of str or imgaug.parameters.StochasticParameter The target colorspace. - Allowed strings are: ``RGB``, ``BGR``, ``GRAY``, ``CIE``, ``YCrCb``, ``HSV``, ``HLS``, ``Lab``, ``Luv``. - These are also accessible via ``ChangeColorspace.``, e.g. ``ChangeColorspace.YCrCb``. + Allowed strings are: ``RGB``, ``BGR``, ``GRAY``, ``CIE``, ``YCrCb``, + ``HSV``, ``HLS``, ``Lab``, ``Luv``. + These are also accessible via ``ChangeColorspace.``, + e.g. ``ChangeColorspace.YCrCb``. * If a string, it must be among the allowed colorspaces. * If a list, it is expected to be a list of strings, each one @@ -425,8 +1286,8 @@ class ChangeColorspace(meta.Augmenter): old image is visible. * If an int or float, exactly that value will be used. - * If a tuple ``(a, b)``, a random value from the range ``a <= x <= b`` will - be sampled per image. + * If a tuple ``(a, b)``, a random value from the range + ``a <= x <= b`` will be sampled per image. * If a list, then a random value will be sampled from that list per image. * If a StochasticParameter, a value will be sampled from the @@ -453,7 +1314,8 @@ class ChangeColorspace(meta.Augmenter): Lab = "Lab" Luv = "Luv" COLORSPACES = {RGB, BGR, GRAY, CIE, YCrCb, HSV, HLS, Lab, Luv} - # TODO access cv2 COLOR_ variables directly instead of indirectly via dictionary mapping + # TODO access cv2 COLOR_ variables directly instead of indirectly via + # dictionary mapping CV_VARS = { # RGB "RGB2BGR": cv2.COLOR_RGB2BGR, @@ -480,42 +1342,54 @@ class ChangeColorspace(meta.Augmenter): "HLS2RGB": cv2.COLOR_HLS2RGB, "HLS2BGR": cv2.COLOR_HLS2BGR, # Lab - "Lab2RGB": cv2.COLOR_Lab2RGB if hasattr(cv2, "COLOR_Lab2RGB") else cv2.COLOR_LAB2RGB, - "Lab2BGR": cv2.COLOR_Lab2BGR if hasattr(cv2, "COLOR_Lab2BGR") else cv2.COLOR_LAB2BGR + "Lab2RGB": ( + cv2.COLOR_Lab2RGB + if hasattr(cv2, "COLOR_Lab2RGB") else cv2.COLOR_LAB2RGB), + "Lab2BGR": ( + cv2.COLOR_Lab2BGR + if hasattr(cv2, "COLOR_Lab2BGR") else cv2.COLOR_LAB2BGR) } - def __init__(self, to_colorspace, from_colorspace="RGB", alpha=1.0, name=None, deterministic=False, - random_state=None): - super(ChangeColorspace, self).__init__(name=name, deterministic=deterministic, random_state=random_state) + def __init__(self, to_colorspace, from_colorspace="RGB", alpha=1.0, + name=None, deterministic=False, random_state=None): + super(ChangeColorspace, self).__init__( + name=name, deterministic=deterministic, random_state=random_state) # TODO somehow merge this with Alpha augmenter? - self.alpha = iap.handle_continuous_param(alpha, "alpha", value_range=(0, 1.0), tuple_to_uniform=True, - list_to_choice=True) + self.alpha = iap.handle_continuous_param( + alpha, "alpha", value_range=(0, 1.0), tuple_to_uniform=True, + list_to_choice=True) if ia.is_string(to_colorspace): ia.do_assert(to_colorspace in ChangeColorspace.COLORSPACES) self.to_colorspace = iap.Deterministic(to_colorspace) elif ia.is_iterable(to_colorspace): - ia.do_assert(all([ia.is_string(colorspace) for colorspace in to_colorspace])) - ia.do_assert(all([(colorspace in ChangeColorspace.COLORSPACES) for colorspace in to_colorspace])) + ia.do_assert(all([ia.is_string(colorspace) + for colorspace in to_colorspace])) + ia.do_assert(all([(colorspace in ChangeColorspace.COLORSPACES) + for colorspace in to_colorspace])) self.to_colorspace = iap.Choice(to_colorspace) elif isinstance(to_colorspace, iap.StochasticParameter): self.to_colorspace = to_colorspace else: - raise Exception("Expected to_colorspace to be string, list of strings or StochasticParameter, got %s." % ( - type(to_colorspace),)) + raise Exception("Expected to_colorspace to be string, list of " + "strings or StochasticParameter, got %s." % ( + type(to_colorspace),)) self.from_colorspace = from_colorspace ia.do_assert(self.from_colorspace in ChangeColorspace.COLORSPACES) ia.do_assert(from_colorspace != ChangeColorspace.GRAY) - self.eps = 0.001 # epsilon value to check if alpha is close to 1.0 or 0.0 + # epsilon value to check if alpha is close to 1.0 or 0.0 + self.eps = 0.001 def _augment_images(self, images, random_state, parents, hooks): result = images nb_images = len(images) - alphas = self.alpha.draw_samples((nb_images,), random_state=ia.copy_random_state(random_state)) - to_colorspaces = self.to_colorspace.draw_samples((nb_images,), random_state=ia.copy_random_state(random_state)) + alphas = self.alpha.draw_samples( + (nb_images,), random_state=ia.copy_random_state(random_state)) + to_colorspaces = self.to_colorspace.draw_samples( + (nb_images,), random_state=ia.copy_random_state(random_state)) for i in sm.xrange(nb_images): alpha = alphas[i] to_colorspace = to_colorspaces[i] @@ -527,34 +1401,39 @@ def _augment_images(self, images, random_state, parents, hooks): if alpha == 0 or self.from_colorspace == to_colorspace: pass # no change necessary else: - # some colorspaces here should use image/255.0 according to the docs, - # but at least for conversion to grayscale that results in errors, - # ie uint8 is expected + # some colorspaces here should use image/255.0 according to + # the docs, but at least for conversion to grayscale that + # results in errors, ie uint8 is expected if image.ndim != 3: import warnings warnings.warn( "Received an image with %d dimensions in " - "ChangeColorspace._augment_image(), but expected 3 dimensions, i.e. shape " + "ChangeColorspace._augment_image(), but expected 3 " + "dimensions, i.e. shape " "(height, width, channels)." % (image.ndim,) ) elif image.shape[2] != 3: import warnings warnings.warn( "Received an image with shape (H, W, C) and C=%d in " - "ChangeColorspace._augment_image(). Expected C to usually be 3 -- any " - "other value will likely result in errors. (Note that this function is " - "e.g. called during grayscale conversion and hue/saturation " + "ChangeColorspace._augment_image(). Expected C to " + "usually be 3 -- any other value will likely result in " + "errors. (Note that this function is e.g. called " + "during grayscale conversion and hue/saturation " "changes.)" % (image.shape[2],) ) - if self.from_colorspace in [ChangeColorspace.RGB, ChangeColorspace.BGR]: - from_to_var_name = "%s2%s" % (self.from_colorspace, to_colorspace) + if self.from_colorspace in [ChangeColorspace.RGB, + ChangeColorspace.BGR]: + from_to_var_name = "%s2%s" % ( + self.from_colorspace, to_colorspace) from_to_var = ChangeColorspace.CV_VARS[from_to_var_name] img_to_cs = cv2.cvtColor(image, from_to_var) else: # convert to RGB - from_to_var_name = "%s2%s" % (self.from_colorspace, ChangeColorspace.RGB) + from_to_var_name = "%s2%s" % ( + self.from_colorspace, ChangeColorspace.RGB) from_to_var = ChangeColorspace.CV_VARS[from_to_var_name] img_rgb = cv2.cvtColor(image, from_to_var) @@ -562,16 +1441,19 @@ def _augment_images(self, images, random_state, parents, hooks): img_to_cs = img_rgb else: # convert from RGB to desired target colorspace - from_to_var_name = "%s2%s" % (ChangeColorspace.RGB, to_colorspace) + from_to_var_name = "%s2%s" % ( + ChangeColorspace.RGB, to_colorspace) from_to_var = ChangeColorspace.CV_VARS[from_to_var_name] img_to_cs = cv2.cvtColor(img_rgb, from_to_var) - # this will break colorspaces that have values outside 0-255 or 0.0-1.0 + # this will break colorspaces that have values outside 0-255 + # or 0.0-1.0 # TODO dont convert to uint8 if ia.is_integer_array(img_to_cs): img_to_cs = np.clip(img_to_cs, 0, 255).astype(np.uint8) else: - img_to_cs = np.clip(img_to_cs * 255, 0, 255).astype(np.uint8) + img_to_cs = np.clip(img_to_cs * 255, 0, 255).astype( + np.uint8) # for grayscale: covnert from (H, W) to (H, W, 3) if len(img_to_cs.shape) == 2: @@ -583,9 +1465,12 @@ def _augment_images(self, images, random_state, parents, hooks): return images 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): + def _augment_keypoints(self, keypoints_on_images, random_state, parents, + hooks): + # pylint: disable=no-self-use return keypoints_on_images def get_parameters(self): @@ -593,11 +1478,13 @@ def get_parameters(self): # TODO rename to Grayscale3D and add Grayscale that keeps the image at 1D? -def Grayscale(alpha=0, from_colorspace="RGB", name=None, deterministic=False, random_state=None): +def Grayscale(alpha=0, from_colorspace="RGB", name=None, deterministic=False, + random_state=None): """ Augmenter to convert images to their grayscale versions. - NOTE: Number of output channels is still 3, i.e. this augmenter just "removes" color. + NOTE: Number of output channels is still 3, i.e. this augmenter just + "removes" color. TODO check dtype support @@ -626,15 +1513,17 @@ def Grayscale(alpha=0, from_colorspace="RGB", name=None, deterministic=False, ra old image is visible. * If a number, exactly that value will always be used. - * If a tuple ``(a, b)``, a random value from the range ``a <= x <= b`` will - be sampled per image. - * If a list, then a random value will be sampled from that list per image. + * If a tuple ``(a, b)``, a random value from the range + ``a <= x <= b`` will be sampled per image. + * If a list, then a random value will be sampled from that list + per image. * If a StochasticParameter, a value will be sampled from the parameter per image. from_colorspace : str, optional The source colorspace (of the input images). - Allowed strings are: ``RGB``, ``BGR``, ``GRAY``, ``CIE``, ``YCrCb``, ``HSV``, ``HLS``, ``Lab``, ``Luv``. + Allowed strings are: ``RGB``, ``BGR``, ``GRAY``, ``CIE``, ``YCrCb``, + ``HSV``, ``HLS``, ``Lab``, ``Luv``. See :func:`imgaug.augmenters.color.ChangeColorspace.__init__`. name : None or str, optional @@ -648,13 +1537,15 @@ def Grayscale(alpha=0, from_colorspace="RGB", name=None, deterministic=False, ra Examples -------- + >>> import imgaug.augmenters as iaa >>> aug = iaa.Grayscale(alpha=1.0) - creates an augmenter that turns images to their grayscale versions. + Creates an augmenter that turns images to their grayscale versions. + >>> import imgaug.augmenters as iaa >>> aug = iaa.Grayscale(alpha=(0.0, 1.0)) - creates an augmenter that turns images to their grayscale versions with + Creates an augmenter that turns images to their grayscale versions with an alpha value in the range ``0 <= alpha <= 1``. An alpha value of 0.5 would mean, that the output image is 50 percent of the input image and 50 percent of the grayscale image (i.e. 50 percent of color removed). @@ -663,5 +1554,9 @@ def Grayscale(alpha=0, from_colorspace="RGB", name=None, deterministic=False, ra if name is None: name = "Unnamed%s" % (ia.caller_name(),) - return ChangeColorspace(to_colorspace=ChangeColorspace.GRAY, alpha=alpha, from_colorspace=from_colorspace, - name=name, deterministic=deterministic, random_state=random_state) + return ChangeColorspace(to_colorspace=ChangeColorspace.GRAY, + alpha=alpha, + from_colorspace=from_colorspace, + name=name, + deterministic=deterministic, + random_state=random_state) diff --git a/imgaug/parameters.py b/imgaug/parameters.py index b1e29b6f6..a7eb94c7f 100644 --- a/imgaug/parameters.py +++ b/imgaug/parameters.py @@ -1540,6 +1540,10 @@ def __str__(self): return "Divide(%s, %s, %s)" % (str(self.other_param), str(self.val), self.elementwise) +# TODO sampling (N,) from something like 10+Uniform(0, 1) will return +# N times the same value as (N,) values will be sampled from 10, but only +# one from Uniform() unless elementwise=True is explicitly set. That +# seems unintuitive. How can this be prevented? class Add(StochasticParameter): """ Parameter to add to other parameter's results. @@ -1568,6 +1572,7 @@ class Add(StochasticParameter): Converts a uniform range ``[0.0, 1.0)`` to ``[1.0, 2.0)``. """ + def __init__(self, other_param, val, elementwise=False): super(Add, self).__init__() diff --git a/test/augmenters/test_color.py b/test/augmenters/test_color.py index 2cc81571b..11a441735 100644 --- a/test/augmenters/test_color.py +++ b/test/augmenters/test_color.py @@ -1,6 +1,18 @@ from __future__ import print_function, division, absolute_import import time +import itertools +import sys +# unittest only added in 3.4 self.subTest() +if sys.version_info[0] < 3 or sys.version_info[1] < 4: + import unittest2 as unittest +else: + import unittest +# unittest.mock is not available in 2.7 (though unittest2 might contain it?) +try: + import unittest.mock as mock +except ImportError: + import mock import matplotlib matplotlib.use('Agg') # fix execution of tests involving matplotlib on travis @@ -8,7 +20,10 @@ import six.moves as sm import cv2 +import imgaug as ia from imgaug import augmenters as iaa +from imgaug import parameters as iap +import imgaug.augmenters.meta as meta from imgaug.testutils import reseed @@ -16,7 +31,6 @@ def main(): time_start = time.time() # TODO WithColorspace - test_AddToHueAndSaturation() # TODO ChangeColorspace test_Grayscale() @@ -24,11 +38,505 @@ def main(): print("<%s> Finished without errors in %.4fs." % (__file__, time_end - time_start,)) -def test_AddToHueAndSaturation(): - reseed() +class TestWithHueAndSaturation(unittest.TestCase): + def setUp(self): + reseed() + + def test___init__(self): + child = iaa.Noop() + aug = iaa.WithHueAndSaturation(child, from_colorspace="BGR") + assert isinstance(aug.children, list) + assert len(aug.children) == 1 + assert aug.children[0] is child + assert aug.from_colorspace == "BGR" + + aug = iaa.WithHueAndSaturation([child]) + assert isinstance(aug.children, list) + assert len(aug.children) == 1 + assert aug.children[0] is child + assert aug.from_colorspace == "RGB" + + def test_augment_images(self): + def do_return_images(images, parents, hooks): + assert images[0].dtype.name == "int16" + return images + + aug_mock = mock.MagicMock(spec=meta.Augmenter) + aug_mock.augment_images.side_effect = do_return_images + aug = iaa.WithHueAndSaturation(aug_mock) + + image = np.zeros((4, 4, 3), dtype=np.uint8) + image_aug = aug.augment_images([image])[0] + assert image_aug.dtype.name == "uint8" + assert np.array_equal(image_aug, image) + assert aug_mock.augment_images.call_count == 1 + + def test_augment_images__hue(self): + def augment_images(images, random_state, parents, hooks): + assert images[0].dtype.name == "int16" + images = np.copy(images) + images[..., 0] += 10 + return images + + aug = iaa.WithHueAndSaturation(iaa.Lambda(func_images=augment_images)) + + # example image + image = np.arange(0, 255).reshape((1, 255, 1)).astype(np.uint8) + image = np.tile(image, (1, 1, 3)) + image[..., 0] += 0 + image[..., 1] += 1 + image[..., 2] += 2 + + # compute expected output + image_hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + image_hsv = image_hsv.astype(np.int16) + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/180)*255).astype(np.int16) + image_hsv[..., 0] += 10 + image_hsv[..., 0] = np.mod(image_hsv[..., 0], 255) + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/255)*180).astype(np.int16) + image_hsv = image_hsv.astype(np.uint8) + image_expected = cv2.cvtColor(image_hsv, cv2.COLOR_HSV2RGB) + assert not np.array_equal(image_expected, image) + + # augment and verify + images_aug = aug.augment_images(np.stack([image, image], axis=0)) + assert ia.is_np_array(images_aug) + for image_aug in images_aug: + assert image_aug.shape == (1, 255, 3) + assert np.array_equal(image_aug, image_expected) + + def test_augment_images__saturation(self): + def augment_images(images, random_state, parents, hooks): + assert images[0].dtype.name == "int16" + images = np.copy(images) + images[..., 1] += 10 + return images + + aug = iaa.WithHueAndSaturation(iaa.Lambda(func_images=augment_images)) + + # example image + image = np.arange(0, 255).reshape((1, 255, 1)).astype(np.uint8) + image = np.tile(image, (1, 1, 3)) + image[..., 0] += 0 + image[..., 1] += 1 + image[..., 2] += 2 + + # compute expected output + image_hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + image_hsv = image_hsv.astype(np.int16) + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/180)*255).astype(np.int16) + image_hsv[..., 1] += 10 + image_hsv[..., 1] = np.clip(image_hsv[..., 1], 0, 255) + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/255)*180).astype(np.int16) + image_hsv = image_hsv.astype(np.uint8) + image_expected = cv2.cvtColor(image_hsv, cv2.COLOR_HSV2RGB) + assert not np.array_equal(image_expected, image) + + # augment and verify + images_aug = aug.augment_images(np.stack([image, image], axis=0)) + assert ia.is_np_array(images_aug) + for image_aug in images_aug: + assert image_aug.shape == (1, 255, 3) + assert np.array_equal(image_aug, image_expected) + + def test_augment_heatmaps(self): + from imgaug.augmentables.heatmaps import HeatmapsOnImage + + def do_return_augmentables(heatmaps, parents, hooks): + return heatmaps + + aug_mock = mock.MagicMock(spec=meta.Augmenter) + aug_mock.augment_heatmaps.side_effect = do_return_augmentables + hm = np.ones((8, 12, 1), dtype=np.float32) + hmoi = HeatmapsOnImage(hm, shape=(16, 24, 3)) + + aug = iaa.WithHueAndSaturation(aug_mock) + hmoi_aug = aug.augment_heatmaps(hmoi) + assert hmoi_aug.shape == (16, 24, 3) + assert hmoi_aug.arr_0to1.shape == (8, 12, 1) + + assert aug_mock.augment_heatmaps.call_count == 1 + + def test_augment_keypoints(self): + from imgaug.augmentables.kps import Keypoint, KeypointsOnImage + + def do_return_augmentables(keypoints_on_images, parents, hooks): + return keypoints_on_images + + aug_mock = mock.MagicMock(spec=meta.Augmenter) + aug_mock.augment_keypoints.side_effect = do_return_augmentables + kpsoi = KeypointsOnImage.from_xy_array(np.float32([ + [0, 0], + [5, 1] + ]), shape=(16, 24, 3)) + + aug = iaa.WithHueAndSaturation(aug_mock) + kpsoi_aug = aug.augment_keypoints(kpsoi) + assert kpsoi_aug.shape == (16, 24, 3) + assert kpsoi.keypoints[0].x == 0 + assert kpsoi.keypoints[0].y == 0 + assert kpsoi.keypoints[1].x == 5 + assert kpsoi.keypoints[1].y == 1 + + assert aug_mock.augment_keypoints.call_count == 1 + + def test__to_deterministic(self): + aug = iaa.WithHueAndSaturation([iaa.Noop()], from_colorspace="BGR") + aug_det = aug.to_deterministic() + + assert not aug.deterministic # ensure copy + assert not aug.children[0].deterministic + + assert aug_det.deterministic + assert isinstance(aug_det.children[0], iaa.Noop) + assert aug_det.children[0].deterministic + + def test_get_parameters(self): + aug = iaa.WithHueAndSaturation([iaa.Noop()], from_colorspace="BGR") + assert aug.get_parameters()[0] == "BGR" + + def test_get_children_lists(self): + child = iaa.Noop() + aug = iaa.WithHueAndSaturation(child) + children_lists = aug.get_children_lists() + assert len(children_lists) == 1 + assert len(children_lists[0]) == 1 + assert children_lists[0][0] is child + + child = iaa.Noop() + aug = iaa.WithHueAndSaturation([child]) + children_lists = aug.get_children_lists() + assert len(children_lists) == 1 + assert len(children_lists[0]) == 1 + assert children_lists[0][0] is child + + def test___str__(self): + child = iaa.Sequential([iaa.Noop(name="foo")]) + aug = iaa.WithHueAndSaturation(child) + observed = aug.__str__() + expected = ( + "WithHueAndSaturation(" + "from_colorspace=RGB, " + "name=UnnamedWithHueAndSaturation, " + "children=[%s], " + "deterministic=False" + ")" % (child.__str__(),) + ) + assert observed == expected + + +class TestMultiplyHueAndSaturation(unittest.TestCase): + def setUp(self): + reseed() + + def test_returns_correct_objects__mul(self): + aug = iaa.MultiplyHueAndSaturation( + (0.9, 1.1), per_channel=True) + assert isinstance(aug, iaa.WithHueAndSaturation) + assert isinstance(aug.children, iaa.Sequential) + assert len(aug.children) == 1 + assert isinstance(aug.children[0], iaa.Multiply) + assert isinstance(aug.children[0].mul, iap.Uniform) + assert np.isclose(aug.children[0].mul.a.value, 0.9) + assert np.isclose(aug.children[0].mul.b.value, 1.1) + assert isinstance(aug.children[0].per_channel, iap.Deterministic) + assert aug.children[0].per_channel.value == 1 + + def test_returns_correct_objects__mul_hue(self): + aug = iaa.MultiplyHueAndSaturation(mul_hue=(0.9, 1.1)) + assert isinstance(aug, iaa.WithHueAndSaturation) + assert isinstance(aug.children, iaa.Sequential) + assert len(aug.children) == 1 + assert isinstance(aug.children[0], iaa.WithChannels) + assert aug.children[0].channels == [0] + assert len(aug.children[0].children) == 1 + assert isinstance(aug.children[0].children[0], iaa.Multiply) + assert isinstance(aug.children[0].children[0].mul, iap.Uniform) + assert np.isclose(aug.children[0].children[0].mul.a.value, 0.9) + assert np.isclose(aug.children[0].children[0].mul.b.value, 1.1) + + def test_returns_correct_objects__mul_saturation(self): + aug = iaa.MultiplyHueAndSaturation(mul_saturation=(0.9, 1.1)) + assert isinstance(aug, iaa.WithHueAndSaturation) + assert isinstance(aug.children, iaa.Sequential) + assert len(aug.children) == 1 + assert isinstance(aug.children[0], iaa.WithChannels) + assert aug.children[0].channels == [1] + assert len(aug.children[0].children) == 1 + assert isinstance(aug.children[0].children[0], iaa.Multiply) + assert isinstance(aug.children[0].children[0].mul, iap.Uniform) + assert np.isclose(aug.children[0].children[0].mul.a.value, 0.9) + assert np.isclose(aug.children[0].children[0].mul.b.value, 1.1) + + def test_returns_correct_objects__mul_hue_and_mul_saturation(self): + aug = iaa.MultiplyHueAndSaturation(mul_hue=(0.9, 1.1), + mul_saturation=(0.8, 1.2)) + assert isinstance(aug, iaa.WithHueAndSaturation) + assert isinstance(aug.children, iaa.Sequential) + assert len(aug.children) == 2 + + assert isinstance(aug.children[0], iaa.WithChannels) + assert aug.children[0].channels == [0] + assert len(aug.children[0].children) == 1 + assert isinstance(aug.children[0].children[0], iaa.Multiply) + assert isinstance(aug.children[0].children[0].mul, iap.Uniform) + assert np.isclose(aug.children[0].children[0].mul.a.value, 0.9) + assert np.isclose(aug.children[0].children[0].mul.b.value, 1.1) + + assert isinstance(aug.children[1], iaa.WithChannels) + assert aug.children[1].channels == [1] + assert len(aug.children[0].children) == 1 + assert isinstance(aug.children[1].children[0], iaa.Multiply) + assert isinstance(aug.children[1].children[0].mul, iap.Uniform) + assert np.isclose(aug.children[1].children[0].mul.a.value, 0.8) + assert np.isclose(aug.children[1].children[0].mul.b.value, 1.2) + + def test_augment_images__mul(self): + aug = iaa.MultiplyHueAndSaturation(1.5) + + # example image + image = np.arange(0, 255).reshape((1, 255, 1)).astype(np.uint8) + image = np.tile(image, (1, 1, 3)) + image[..., 0] += 0 + image[..., 1] += 5 + image[..., 2] += 10 + + # compute expected output + image_hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + image_hsv = image_hsv.astype(np.int16) # simulate WithHueAndSaturation + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/180)*255).astype(np.int16) + image_hsv = image_hsv.astype(np.float32) # simulate Multiply + + image_hsv[..., 0] *= 1.5 + image_hsv[..., 1] *= 1.5 + image_hsv = np.round(image_hsv).astype(np.int16) + + image_hsv[..., 0] = np.mod(image_hsv[..., 0], 255) + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/255)*180).astype(np.int16) + image_hsv[..., 1] = np.clip(image_hsv[..., 1], 0, 255) + + image_hsv = image_hsv.astype(np.uint8) + image_expected = cv2.cvtColor(image_hsv, cv2.COLOR_HSV2RGB) + assert not np.array_equal(image_expected, image) + + # augment and verify + images_aug = aug.augment_images(np.stack([image, image], axis=0)) + assert ia.is_np_array(images_aug) + for image_aug in images_aug: + assert image_aug.shape == (1, 255, 3) + diff = np.abs(image_aug.astype(np.int16) - image_expected) + assert np.all(diff <= 1) + + def test_augment_images__mul_hue(self): + # this is almost identical to test_augment_images__mul + # only + # aug = ... + # and + # image_hsv[...] *= 1.2 + # have been changed + + aug = iaa.MultiplyHueAndSaturation(mul_hue=1.5) # changed over __mul + + # example image + image = np.arange(0, 255).reshape((1, 255, 1)).astype(np.uint8) + image = np.tile(image, (1, 1, 3)) + image[..., 0] += 0 + image[..., 1] += 5 + image[..., 2] += 10 + + # compute expected output + image_hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + image_hsv = image_hsv.astype(np.int16) # simulate WithHueAndSaturation + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/180)*255).astype(np.int16) + image_hsv = image_hsv.astype(np.float32) # simulate Multiply + + image_hsv[..., 0] *= 1.5 + image_hsv[..., 1] *= 1.0 # changed over __mul + image_hsv = np.round(image_hsv).astype(np.int16) + + image_hsv[..., 0] = np.mod(image_hsv[..., 0], 255) + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/255)*180).astype(np.int16) + image_hsv[..., 1] = np.clip(image_hsv[..., 1], 0, 255) + + image_hsv = image_hsv.astype(np.uint8) + image_expected = cv2.cvtColor(image_hsv, cv2.COLOR_HSV2RGB) + assert not np.array_equal(image_expected, image) + + # augment and verify + images_aug = aug.augment_images(np.stack([image, image], axis=0)) + assert ia.is_np_array(images_aug) + for image_aug in images_aug: + assert image_aug.shape == (1, 255, 3) + diff = np.abs(image_aug.astype(np.int16) - image_expected) + assert np.all(diff <= 1) + + def test_augment_images__mul_saturation(self): + # this is almost identical to test_augment_images__mul + # only + # aug = ... + # and + # image_hsv[...] *= 1.2 + # have been changed + + aug = iaa.MultiplyHueAndSaturation(mul_saturation=1.5) # changed + + # example image + image = np.arange(0, 255).reshape((1, 255, 1)).astype(np.uint8) + image = np.tile(image, (1, 1, 3)) + image[..., 0] += 0 + image[..., 1] += 5 + image[..., 2] += 10 + + # compute expected output + image_hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + image_hsv = image_hsv.astype(np.int16) # simulate WithHueAndSaturation + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/180)*255).astype(np.int16) + image_hsv = image_hsv.astype(np.float32) # simulate Multiply + + image_hsv[..., 0] *= 1.0 # changed over __mul + image_hsv[..., 1] *= 1.5 + image_hsv = np.round(image_hsv).astype(np.int16) + + image_hsv[..., 0] = np.mod(image_hsv[..., 0], 255) + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/255)*180).astype(np.int16) + image_hsv[..., 1] = np.clip(image_hsv[..., 1], 0, 255) + + image_hsv = image_hsv.astype(np.uint8) + image_expected = cv2.cvtColor(image_hsv, cv2.COLOR_HSV2RGB) + assert not np.array_equal(image_expected, image) + + # augment and verify + images_aug = aug.augment_images(np.stack([image, image], axis=0)) + assert ia.is_np_array(images_aug) + for image_aug in images_aug: + assert image_aug.shape == (1, 255, 3) + diff = np.abs(image_aug.astype(np.int16) - image_expected) + assert np.all(diff <= 1) + + def test_augment_images__mul_hue_and_mul_saturation(self): + # this is almost identical to test_augment_images__mul + # only + # aug = ... + # and + # image_hsv[...] *= 1.2 + # have been changed + + aug = iaa.MultiplyHueAndSaturation(mul_hue=1.5, + mul_saturation=1.6) # changed + + # example image + image = np.arange(0, 255).reshape((1, 255, 1)).astype(np.uint8) + image = np.tile(image, (1, 1, 3)) + image[..., 0] += 0 + image[..., 1] += 5 + image[..., 2] += 10 - # interestingly, when using this RGB2HSV and HSV2RGB conversion from skimage, the results - # differ quite a bit from the cv2 ones + # compute expected output + image_hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + image_hsv = image_hsv.astype(np.int16) # simulate WithHueAndSaturation + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/180)*255).astype(np.int16) + image_hsv = image_hsv.astype(np.float32) # simulate Multiply + + image_hsv[..., 0] *= 1.5 + image_hsv[..., 1] *= 1.6 # changed over __mul + image_hsv = np.round(image_hsv).astype(np.int16) + + image_hsv[..., 0] = np.mod(image_hsv[..., 0], 255) + image_hsv[..., 0] = ( + (image_hsv[..., 0].astype(np.float32)/255)*180).astype(np.int16) + image_hsv[..., 1] = np.clip(image_hsv[..., 1], 0, 255) + + image_hsv = image_hsv.astype(np.uint8) + image_expected = cv2.cvtColor(image_hsv, cv2.COLOR_HSV2RGB) + assert not np.array_equal(image_expected, image) + + # augment and verify + images_aug = aug.augment_images(np.stack([image, image], axis=0)) + assert ia.is_np_array(images_aug) + for image_aug in images_aug: + assert image_aug.shape == (1, 255, 3) + diff = np.abs(image_aug.astype(np.int16) - image_expected) + assert np.all(diff <= 1) + + def test_augment_images__deterministic(self): + rs = np.random.RandomState(1) + images = rs.randint(0, 255, size=(32, 4, 4, 3), dtype=np.uint8) + + for deterministic in [False, True]: + aug = iaa.MultiplyHueAndSaturation(mul=(0.1, 5.0), + deterministic=deterministic) + images_aug1 = aug.augment_images(images) + images_aug2 = aug.augment_images(images) + equal = np.array_equal(images_aug1, images_aug2) + if deterministic: + assert equal + else: + assert not equal + + aug = iaa.MultiplyHueAndSaturation(mul_hue=(0.1, 5.0), + mul_saturation=(0.1, 5.0), + deterministic=deterministic) + images_aug1 = aug.augment_images(images) + images_aug2 = aug.augment_images(images) + equal = np.array_equal(images_aug1, images_aug2) + if deterministic: + assert equal + else: + assert not equal + + +class TestMultiplyToHue(unittest.TestCase): + def test_returns_correct_class(self): + # this test is practically identical to + # TestMultiplyToHueAndSaturation.test_returns_correct_objects__mul_hue + aug = iaa.MultiplyHue((0.9, 1.1)) + assert isinstance(aug, iaa.WithHueAndSaturation) + assert isinstance(aug.children, iaa.Sequential) + assert len(aug.children) == 1 + assert isinstance(aug.children[0], iaa.WithChannels) + assert aug.children[0].channels == [0] + assert len(aug.children[0].children) == 1 + assert isinstance(aug.children[0].children[0], iaa.Multiply) + assert isinstance(aug.children[0].children[0].mul, iap.Uniform) + assert np.isclose(aug.children[0].children[0].mul.a.value, 0.9) + assert np.isclose(aug.children[0].children[0].mul.b.value, 1.1) + + +class TestMultiplyToSaturation(unittest.TestCase): + def test_returns_correct_class(self): + # this test is practically identical to + # TestMultiplyToHueAndSaturation + # .test_returns_correct_objects__mul_saturation + aug = iaa.MultiplySaturation((0.9, 1.1)) + assert isinstance(aug, iaa.WithHueAndSaturation) + assert isinstance(aug.children, iaa.Sequential) + assert len(aug.children) == 1 + assert isinstance(aug.children[0], iaa.WithChannels) + assert aug.children[0].channels == [1] + assert len(aug.children[0].children) == 1 + assert isinstance(aug.children[0].children[0], iaa.Multiply) + assert isinstance(aug.children[0].children[0].mul, iap.Uniform) + assert np.isclose(aug.children[0].children[0].mul.a.value, 0.9) + assert np.isclose(aug.children[0].children[0].mul.b.value, 1.1) + + +class TestAddToHueAndSaturation(unittest.TestCase): + def setUp(self): + reseed() + + # interestingly, when using this RGB2HSV and HSV2RGB conversion from + # skimage, the results differ quite a bit from the cv2 ones """ def _add_hue_saturation(img, value): img_hsv = color.rgb2hsv(img / 255.0) @@ -36,70 +544,308 @@ def _add_hue_saturation(img, value): return color.hsv2rgb(img_hsv) * 255 """ - def _add_hue_saturation(img, value): + @classmethod + def _add_hue_saturation(cls, img, value=None, value_hue=None, + value_saturation=None): + if value is not None: + assert value_hue is None and value_saturation is None + else: + assert value_hue is not None or value_saturation is not None + + if value is not None: + value_hue = value + value_saturation = value + else: + value_hue = value_hue or 0 + value_saturation = value_saturation or 0 + img_hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) img_hsv = img_hsv.astype(np.int32) - img_hsv[..., 0] = np.mod(img_hsv[..., 0] + (value/255.0) * (360/2), 180) - img_hsv[..., 1] = np.clip(img_hsv[..., 1] + value, 0, 255) + img_hsv[..., 0] = np.mod( + img_hsv[..., 0] + (value_hue/255.0) * (360/2), 180) + img_hsv[..., 1] = np.clip( + img_hsv[..., 1] + value_saturation, 0, 255) img_hsv = img_hsv.astype(np.uint8) return cv2.cvtColor(img_hsv, cv2.COLOR_HSV2RGB) - base_img = np.zeros((2, 2, 3), dtype=np.uint8) - base_img[..., 0] += 20 - base_img[..., 1] += 40 - base_img[..., 2] += 60 + def test___init__(self): + aug = iaa.AddToHueAndSaturation((-20, 20)) + assert isinstance(aug.value, iap.DiscreteUniform) + assert aug.value.a.value == -20 + assert aug.value.b.value == 20 + assert aug.value_hue is None + assert aug.value_saturation is None + assert isinstance(aug.per_channel, iap.Deterministic) + assert aug.per_channel.value == 0 - for per_channel in [False, True]: - for backend in ["cv2", "numpy"]: - aug = iaa.AddToHueAndSaturation(0, per_channel=per_channel) - aug.backend = backend - observed = aug.augment_image(base_img) - expected = base_img - assert np.allclose(observed, expected) + def test___init___value_none(self): + aug = iaa.AddToHueAndSaturation(value_hue=(-20, 20), + value_saturation=[0, 5, 10]) + assert aug.value is None + assert isinstance(aug.value_hue, iap.DiscreteUniform) + assert isinstance(aug.value_saturation, iap.Choice) + assert aug.value_hue.a.value == -20 + assert aug.value_hue.b.value == 20 + assert aug.value_saturation.a == [0, 5, 10] + assert isinstance(aug.per_channel, iap.Deterministic) + assert aug.per_channel.value == 0 - aug = iaa.AddToHueAndSaturation(30, per_channel=per_channel) - aug.backend = backend - observed = aug.augment_image(base_img) - expected = _add_hue_saturation(base_img, 30) - diff = np.abs(observed.astype(np.float32) - expected) - assert np.all(diff <= 1) + def test___init___per_channel(self): + aug = iaa.AddToHueAndSaturation(per_channel=0.5) + assert aug.value is None + assert aug.value_hue is None + assert aug.value_saturation is None + assert isinstance(aug.per_channel, iap.Binomial) + assert np.isclose(aug.per_channel.p.value, 0.5) - aug = iaa.AddToHueAndSaturation(255, per_channel=per_channel) - aug.backend = backend - observed = aug.augment_image(base_img) - expected = _add_hue_saturation(base_img, 255) - diff = np.abs(observed.astype(np.float32) - expected) - assert np.all(diff <= 1) + def test_augment_images(self): + base_img = np.zeros((2, 2, 3), dtype=np.uint8) + base_img[..., 0] += 20 + base_img[..., 1] += 40 + base_img[..., 2] += 60 + + gen = itertools.product([False, True], ["cv2", "numpy"]) + for per_channel, backend in gen: + with self.subTest(per_channel=per_channel, backend=backend): + aug = iaa.AddToHueAndSaturation(0, per_channel=per_channel) + aug.backend = backend + observed = aug.augment_image(base_img) + expected = base_img + assert np.allclose(observed, expected) + + aug = iaa.AddToHueAndSaturation(30, per_channel=per_channel) + aug.backend = backend + observed = aug.augment_image(base_img) + expected = self._add_hue_saturation(base_img, 30) + diff = np.abs(observed.astype(np.float32) - expected) + assert np.all(diff <= 1) + + aug = iaa.AddToHueAndSaturation(255, per_channel=per_channel) + aug.backend = backend + observed = aug.augment_image(base_img) + expected = self._add_hue_saturation(base_img, 255) + diff = np.abs(observed.astype(np.float32) - expected) + assert np.all(diff <= 1) + + aug = iaa.AddToHueAndSaturation(-255, per_channel=per_channel) + aug.backend = backend + observed = aug.augment_image(base_img) + expected = self._add_hue_saturation(base_img, -255) + diff = np.abs(observed.astype(np.float32) - expected) + assert np.all(diff <= 1) + + def test_augment_images__different_hue_and_saturation__no_per_channel(self): + base_img = np.zeros((2, 2, 3), dtype=np.uint8) + base_img[..., 0] += 20 + base_img[..., 1] += 40 + base_img[..., 2] += 60 + + class _DummyParam(iap.StochasticParameter): + def _draw_samples(self, size, random_state): + arr = np.float32([10, 20]) + return np.tile(arr[np.newaxis, :], (size[0], 1)) + + aug = iaa.AddToHueAndSaturation(value=_DummyParam(), per_channel=False) + img_expected = self._add_hue_saturation(base_img, value=10) + img_observed = aug.augment_image(base_img) + + assert np.array_equal(img_observed, img_expected) + + def test_augment_images__different_hue_and_saturation__per_channel(self): + base_img = np.zeros((2, 2, 3), dtype=np.uint8) + base_img[..., 0] += 20 + base_img[..., 1] += 40 + base_img[..., 2] += 60 + + class _DummyParam(iap.StochasticParameter): + def _draw_samples(self, size, random_state): + arr = np.float32([10, 20]) + return np.tile(arr[np.newaxis, :], (size[0], 1)) + + aug = iaa.AddToHueAndSaturation(value=_DummyParam(), per_channel=True) + img_expected = self._add_hue_saturation( + base_img, value_hue=10, value_saturation=20) + img_observed = aug.augment_image(base_img) + + assert np.array_equal(img_observed, img_expected) - aug = iaa.AddToHueAndSaturation(-255, per_channel=per_channel) - aug.backend = backend + def test_augment_images__different_hue_and_saturation__mixed_perchan(self): + base_img = np.zeros((2, 2, 3), dtype=np.uint8) + base_img[..., 0] += 20 + base_img[..., 1] += 40 + base_img[..., 2] += 60 + + class _DummyParamValue(iap.StochasticParameter): + def _draw_samples(self, size, random_state): + arr = np.float32([10, 20]) + return np.tile(arr[np.newaxis, :], (size[0], 1)) + + class _DummyParamPerChannel(iap.StochasticParameter): + def _draw_samples(self, size, random_state): + assert size == (3,) + return np.float32([1.0, 0.0, 1.0]) + + aug = iaa.AddToHueAndSaturation( + value=_DummyParamValue(), per_channel=_DummyParamPerChannel()) + + img_expected1 = self._add_hue_saturation( + base_img, value_hue=10, value_saturation=20) + img_expected2 = self._add_hue_saturation( + base_img, value_hue=10, value_saturation=10) + img_expected3 = self._add_hue_saturation( + base_img, value_hue=10, value_saturation=20) + + img_observed1, img_observed2, img_observed3, = \ + aug.augment_images([base_img] * 3) + + assert np.array_equal(img_observed1, img_expected1) + assert np.array_equal(img_observed2, img_expected2) + assert np.array_equal(img_observed3, img_expected3) + + def test_augment_images__list_as_value(self): + base_img = np.zeros((2, 2, 3), dtype=np.uint8) + base_img[..., 0] += 20 + base_img[..., 1] += 40 + base_img[..., 2] += 60 + + aug = iaa.AddToHueAndSaturation([0, 10, 20]) + base_img = base_img[0:1, 0:1, :] + expected_imgs = [ + iaa.AddToHueAndSaturation(0).augment_image(base_img), + iaa.AddToHueAndSaturation(10).augment_image(base_img), + iaa.AddToHueAndSaturation(20).augment_image(base_img) + ] + + assert not np.array_equal(expected_imgs[0], expected_imgs[1]) + assert not np.array_equal(expected_imgs[1], expected_imgs[2]) + assert not np.array_equal(expected_imgs[0], expected_imgs[2]) + nb_iterations = 300 + seen = dict([(i, 0) for i, _ in enumerate(expected_imgs)]) + for _ in sm.xrange(nb_iterations): observed = aug.augment_image(base_img) - expected = _add_hue_saturation(base_img, -255) - diff = np.abs(observed.astype(np.float32) - expected) - assert np.all(diff <= 1) + for i, expected_img in enumerate(expected_imgs): + if np.allclose(observed, expected_img): + seen[i] += 1 + assert np.sum(list(seen.values())) == nb_iterations + n_exp = nb_iterations / 3 + n_exp_tol = nb_iterations * 0.1 + assert all([n_exp - n_exp_tol < v < n_exp + n_exp_tol + for v in seen.values()]) - aug = iaa.AddToHueAndSaturation([0, 10, 20]) - base_img = base_img[0:1, 0:1, :] - expected_imgs = [ - iaa.AddToHueAndSaturation(0).augment_image(base_img), - iaa.AddToHueAndSaturation(10).augment_image(base_img), - iaa.AddToHueAndSaturation(20).augment_image(base_img) - ] - - assert not np.array_equal(expected_imgs[0], expected_imgs[1]) - assert not np.array_equal(expected_imgs[1], expected_imgs[2]) - assert not np.array_equal(expected_imgs[0], expected_imgs[2]) - nb_iterations = 300 - seen = dict([(i, 0) for i, _ in enumerate(expected_imgs)]) - for _ in sm.xrange(nb_iterations): - observed = aug.augment_image(base_img) - for i, expected_img in enumerate(expected_imgs): - if np.allclose(observed, expected_img): - seen[i] += 1 - assert np.sum(list(seen.values())) == nb_iterations - n_exp = nb_iterations / 3 - n_exp_tol = nb_iterations * 0.1 - assert all([n_exp - n_exp_tol < v < n_exp + n_exp_tol for v in seen.values()]) + def test_augment_images__value_hue(self): + base_img = np.zeros((2, 2, 3), dtype=np.uint8) + base_img[..., 0] += 20 + base_img[..., 1] += 40 + base_img[..., 2] += 60 + + class _DummyParam(iap.StochasticParameter): + def _draw_samples(self, size, random_state): + return np.float32([10, 20, 30]) + + aug = iaa.AddToHueAndSaturation(value_hue=_DummyParam()) + + img_expected1 = self._add_hue_saturation(base_img, value_hue=10) + img_expected2 = self._add_hue_saturation(base_img, value_hue=20) + img_expected3 = self._add_hue_saturation(base_img, value_hue=30) + + img_observed1, img_observed2, img_observed3 = \ + aug.augment_images([base_img] * 3) + + assert np.array_equal(img_observed1, img_expected1) + assert np.array_equal(img_observed2, img_expected2) + assert np.array_equal(img_observed3, img_expected3) + + def test_augment_images__value_saturation(self): + base_img = np.zeros((2, 2, 3), dtype=np.uint8) + base_img[..., 0] += 20 + base_img[..., 1] += 40 + base_img[..., 2] += 60 + + class _DummyParam(iap.StochasticParameter): + def _draw_samples(self, size, random_state): + return np.float32([10, 20, 30]) + + aug = iaa.AddToHueAndSaturation(value_saturation=_DummyParam()) + + img_expected1 = self._add_hue_saturation(base_img, value_saturation=10) + img_expected2 = self._add_hue_saturation(base_img, value_saturation=20) + img_expected3 = self._add_hue_saturation(base_img, value_saturation=30) + + img_observed1, img_observed2, img_observed3 = \ + aug.augment_images([base_img] * 3) + + assert np.array_equal(img_observed1, img_expected1) + assert np.array_equal(img_observed2, img_expected2) + assert np.array_equal(img_observed3, img_expected3) + + def test_augment_images__value_hue_and_value_saturation(self): + base_img = np.zeros((2, 2, 3), dtype=np.uint8) + base_img[..., 0] += 20 + base_img[..., 1] += 40 + base_img[..., 2] += 60 + + class _DummyParam(iap.StochasticParameter): + def _draw_samples(self, size, random_state): + return np.float32([10, 20, 30]) + + aug = iaa.AddToHueAndSaturation(value_hue=_DummyParam(), + value_saturation=_DummyParam()+40) + + img_expected1 = self._add_hue_saturation(base_img, value_hue=10, + value_saturation=40+10) + img_expected2 = self._add_hue_saturation(base_img, value_hue=20, + value_saturation=40+20) + img_expected3 = self._add_hue_saturation(base_img, value_hue=30, + value_saturation=40+30) + + img_observed1, img_observed2, img_observed3 = \ + aug.augment_images([base_img] * 3) + + assert np.array_equal(img_observed1, img_expected1) + assert np.array_equal(img_observed2, img_expected2) + assert np.array_equal(img_observed3, img_expected3) + + def test_get_parameters(self): + aug = iaa.AddToHueAndSaturation((-20, 20), per_channel=0.5) + params = aug.get_parameters() + assert isinstance(params[0], iap.DiscreteUniform) + assert params[0].a.value == -20 + assert params[0].b.value == 20 + assert params[1] is None + assert params[2] is None + assert isinstance(params[3], iap.Binomial) + assert np.isclose(params[3].p.value, 0.5) + + def test_get_parameters_value_hue_and_value_saturation(self): + aug = iaa.AddToHueAndSaturation(value_hue=(-20, 20), + value_saturation=5) + params = aug.get_parameters() + assert params[0] is None + assert isinstance(params[1], iap.DiscreteUniform) + assert params[1].a.value == -20 + assert params[1].b.value == 20 + assert isinstance(params[2], iap.Deterministic) + assert params[2].value == 5 + assert isinstance(params[3], iap.Deterministic) + assert params[3].value == 0 + + +class TestAddToHue(unittest.TestCase): + def test_returns_correct_class(self): + aug = iaa.AddToHue((-20, 20)) + assert isinstance(aug, iaa.AddToHueAndSaturation) + assert isinstance(aug.value_hue, iap.DiscreteUniform) + assert aug.value_hue.a.value == -20 + assert aug.value_hue.b.value == 20 + + +class TestAddToSaturation(unittest.TestCase): + def test_returns_correct_class(self): + aug = iaa.AddToSaturation((-20, 20)) + assert isinstance(aug, iaa.AddToHueAndSaturation) + assert isinstance(aug.value_saturation, iap.DiscreteUniform) + assert aug.value_saturation.a.value == -20 + assert aug.value_saturation.b.value == 20 def test_Grayscale():