From 8a355940ed12af1a6b69d3c92dfcdf1babfa1cf1 Mon Sep 17 00:00:00 2001 From: divyasreepat Date: Thu, 11 May 2023 18:00:43 -0700 Subject: [PATCH] Added RandomZoom to preprocessing layers (#149) * added random Zoom * added seed * update docstring * added name * added name --- keras_core/layers/__init__.py | 1 + .../layers/preprocessing/random_zoom.py | 116 ++++++++++++++++++ .../layers/preprocessing/random_zoom_test.py | 83 +++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 keras_core/layers/preprocessing/random_zoom.py create mode 100644 keras_core/layers/preprocessing/random_zoom_test.py diff --git a/keras_core/layers/__init__.py b/keras_core/layers/__init__.py index e6bf40ac0..91f672dc9 100644 --- a/keras_core/layers/__init__.py +++ b/keras_core/layers/__init__.py @@ -77,6 +77,7 @@ from keras_core.layers.preprocessing.random_contrast import RandomContrast from keras_core.layers.preprocessing.random_crop import RandomCrop from keras_core.layers.preprocessing.random_flip import RandomFlip from keras_core.layers.preprocessing.random_translation import RandomTranslation +from keras_core.layers.preprocessing.random_zoom import RandomZoom from keras_core.layers.preprocessing.rescaling import Rescaling from keras_core.layers.preprocessing.resizing import Resizing from keras_core.layers.preprocessing.string_lookup import StringLookup diff --git a/keras_core/layers/preprocessing/random_zoom.py b/keras_core/layers/preprocessing/random_zoom.py new file mode 100644 index 000000000..273d0565f --- /dev/null +++ b/keras_core/layers/preprocessing/random_zoom.py @@ -0,0 +1,116 @@ +import numpy as np +import tensorflow as tf + +from keras_core import backend +from keras_core.api_export import keras_core_export +from keras_core.layers.layer import Layer + + +@keras_core_export("keras_core.layers.RandomZoom") +class RandomZoom(Layer): + """A preprocessing layer which randomly zooms images during training. + + This layer will randomly zoom in or out on each axis of an image + independently, filling empty space according to `fill_mode`. + + Input pixel values can be of any range (e.g. `[0., 1.)` or `[0, 255]`) and + of integer or floating point dtype. + By default, the layer will output floats. + + Args: + height_factor: a float represented as fraction of value, + or a tuple of size 2 representing lower and upper bound + for zooming vertically. When represented as a single float, + this value is used for both the upper and + lower bound. A positive value means zooming out, + while a negative value + means zooming in. For instance, `height_factor=(0.2, 0.3)` + result in an output zoomed out by a random amount + in the range `[+20%, +30%]`. + `height_factor=(-0.3, -0.2)` result in an output zoomed + in by a random amount in the range `[+20%, +30%]`. + width_factor: a float represented as fraction of value, + or a tuple of size 2 representing lower and upper bound + for zooming horizontally. When + represented as a single float, this value is used + for both the upper and + lower bound. For instance, `width_factor=(0.2, 0.3)` + result in an output + zooming out between 20% to 30%. + `width_factor=(-0.3, -0.2)` result in an + output zooming in between 20% to 30%. `None` means + i.e., zooming vertical and horizontal directions + by preserving the aspect ratio. Defaults to `None`. + fill_mode: Points outside the boundaries of the input are + filled according to the given mode + (one of `{"constant", "reflect", "wrap", "nearest"}`). + - *reflect*: `(d c b a | a b c d | d c b a)` + The input is extended by reflecting about + the edge of the last pixel. + - *constant*: `(k k k k | a b c d | k k k k)` + The input is extended by filling all values beyond + the edge with the same constant value k = 0. + - *wrap*: `(a b c d | a b c d | a b c d)` The input is extended by + wrapping around to the opposite edge. + - *nearest*: `(a a a a | a b c d | d d d d)` + The input is extended by the nearest pixel. + interpolation: Interpolation mode. Supported values: `"nearest"`, + `"bilinear"`. + seed: Integer. Used to create a random seed. + fill_value: a float represents the value to be filled outside + the boundaries when `fill_mode="constant"`. + + Example: + + >>> input_img = np.random.random((32, 224, 224, 3)) + >>> layer = keras_core.layers.RandomZoom(.5, .2) + >>> out_img = layer(input_img) + + Input shape: + 3D (unbatched) or 4D (batched) tensor with shape: + `(..., height, width, channels)`, in `"channels_last"` format. + + Output shape: + 3D (unbatched) or 4D (batched) tensor with shape: + `(..., height, width, channels)`, in `"channels_last"` format. + """ + + def __init__( + self, + height_factor, + width_factor=None, + fill_mode="reflect", + interpolation="bilinear", + seed=None, + fill_value=0.0, + name=None, + **kwargs, + ): + super().__init__(name=name, **kwargs) + self.seed = seed or backend.random.make_default_seed() + self.layer = tf.keras.layers.RandomZoom( + height_factor=height_factor, + width_factor=width_factor, + fill_mode=fill_mode, + interpolation=interpolation, + seed=self.seed, + name=name, + fill_value=fill_value, + **kwargs, + ) + + def call(self, inputs, training=True): + if not isinstance(inputs, (tf.Tensor, np.ndarray, list, tuple)): + inputs = tf.convert_to_tensor(np.array(inputs)) + outputs = self.layer.call(inputs) + if backend.backend() != "tensorflow": + outputs = backend.convert_to_tensor(outputs) + return outputs + + def compute_output_shape(self, input_shape): + return tuple(self.layer.compute_output_shape(input_shape)) + + def get_config(self): + config = self.layer.get_config() + config.update({"seed": self.seed}) + return config diff --git a/keras_core/layers/preprocessing/random_zoom_test.py b/keras_core/layers/preprocessing/random_zoom_test.py new file mode 100644 index 000000000..6516616d9 --- /dev/null +++ b/keras_core/layers/preprocessing/random_zoom_test.py @@ -0,0 +1,83 @@ +import numpy as np +from absl.testing import parameterized + +from keras_core import backend +from keras_core import layers +from keras_core import testing + + +class RandomZoomTest(testing.TestCase, parameterized.TestCase): + @parameterized.named_parameters( + ("random_zoom_in_4_by_6", -0.4, -0.6), + ("random_zoom_in_2_by_3", -0.2, -0.3), + ("random_zoom_in_tuple_factor", (-0.4, -0.5), (-0.2, -0.3)), + ("random_zoom_out_4_by_6", 0.4, 0.6), + ("random_zoom_out_2_by_3", 0.2, 0.3), + ("random_zoom_out_tuple_factor", (0.4, 0.5), (0.2, 0.3)), + ) + def test_random_zoom(self, height_factor, width_factor): + self.run_layer_test( + layers.RandomZoom, + init_kwargs={ + "height_factor": height_factor, + "width_factor": width_factor, + }, + input_shape=(2, 3, 4), + expected_output_shape=(2, 3, 4), + supports_masking=False, + ) + + def test_random_zoom_out_correctness(self): + input_image = np.reshape(np.arange(0, 25), (1, 5, 5, 1)) + expected_output = np.asarray( + [ + [0, 0, 0, 0, 0], + [0, 5, 7, 9, 0], + [0, 10, 12, 14, 0], + [0, 20, 22, 24, 0], + [0, 0, 0, 0, 0], + ] + ) + expected_output = backend.convert_to_tensor( + np.reshape(expected_output, (1, 5, 5, 1)) + ) + self.run_layer_test( + layers.RandomZoom, + init_kwargs={ + "height_factor": (0.5, 0.5), + "width_factor": (0.8, 0.8), + "interpolation": "nearest", + "fill_mode": "constant", + }, + input_shape=None, + input_data=input_image, + expected_output=expected_output, + supports_masking=False, + ) + + def test_random_zoom_in_correctness(self): + input_image = np.reshape(np.arange(0, 25), (1, 5, 5, 1)) + expected_output = np.asarray( + [ + [6, 7, 7, 8, 8], + [11, 12, 12, 13, 13], + [11, 12, 12, 13, 13], + [16, 17, 17, 18, 18], + [16, 17, 17, 18, 18], + ] + ) + expected_output = backend.convert_to_tensor( + np.reshape(expected_output, (1, 5, 5, 1)) + ) + self.run_layer_test( + layers.RandomZoom, + init_kwargs={ + "height_factor": (-0.5, -0.5), + "width_factor": (-0.5, -0.5), + "interpolation": "nearest", + }, + input_shape=None, + input_data=input_image, + expected_output=expected_output, + supports_masking=False, + )