1# Copyright 2019 The TensorFlow Authors. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ==============================================================================
15"""Keras image preprocessing layers."""
16# pylint: disable=g-classes-have-attributes
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import print_function
20
21import numpy as np
22
23from tensorflow.python.eager import context
24from tensorflow.python.compat import compat
25from tensorflow.python.framework import dtypes
26from tensorflow.python.framework import ops
27from tensorflow.python.framework import tensor_shape
28from tensorflow.python.framework import tensor_util
29from tensorflow.python.keras import backend as K
30from tensorflow.python.keras.engine import base_preprocessing_layer
31from tensorflow.python.keras.engine.base_preprocessing_layer import PreprocessingLayer
32from tensorflow.python.keras.engine.input_spec import InputSpec
33from tensorflow.python.keras.utils import control_flow_util
34from tensorflow.python.ops import array_ops
35from tensorflow.python.ops import check_ops
36from tensorflow.python.ops import control_flow_ops
37from tensorflow.python.ops import gen_image_ops
38from tensorflow.python.ops import image_ops
39from tensorflow.python.ops import math_ops
40from tensorflow.python.ops import stateful_random_ops
41from tensorflow.python.ops import stateless_random_ops
42from tensorflow.python.util.tf_export import keras_export
43
44ResizeMethod = image_ops.ResizeMethod
45
46_RESIZE_METHODS = {
47    'bilinear': ResizeMethod.BILINEAR,
48    'nearest': ResizeMethod.NEAREST_NEIGHBOR,
49    'bicubic': ResizeMethod.BICUBIC,
50    'area': ResizeMethod.AREA,
51    'lanczos3': ResizeMethod.LANCZOS3,
52    'lanczos5': ResizeMethod.LANCZOS5,
53    'gaussian': ResizeMethod.GAUSSIAN,
54    'mitchellcubic': ResizeMethod.MITCHELLCUBIC
55}
56
57H_AXIS = 1
58W_AXIS = 2
59
60
61def check_fill_mode_and_interpolation(fill_mode, interpolation):
62  if fill_mode not in {'reflect', 'wrap', 'constant', 'nearest'}:
63    raise NotImplementedError(
64        'Unknown `fill_mode` {}. Only `reflect`, `wrap`, '
65        '`constant` and `nearest` are supported.'.format(fill_mode))
66  if interpolation not in {'nearest', 'bilinear'}:
67    raise NotImplementedError('Unknown `interpolation` {}. Only `nearest` and '
68                              '`bilinear` are supported.'.format(interpolation))
69
70
71@keras_export('keras.layers.experimental.preprocessing.Resizing')
72class Resizing(PreprocessingLayer):
73  """Image resizing layer.
74
75  Resize the batched image input to target height and width. The input should
76  be a 4-D tensor in the format of NHWC.
77
78  Args:
79    height: Integer, the height of the output shape.
80    width: Integer, the width of the output shape.
81    interpolation: String, the interpolation method. Defaults to `bilinear`.
82      Supports `bilinear`, `nearest`, `bicubic`, `area`, `lanczos3`, `lanczos5`,
83      `gaussian`, `mitchellcubic`
84    name: A string, the name of the layer.
85  """
86
87  def __init__(self,
88               height,
89               width,
90               interpolation='bilinear',
91               name=None,
92               **kwargs):
93    self.target_height = height
94    self.target_width = width
95    self.interpolation = interpolation
96    self._interpolation_method = get_interpolation(interpolation)
97    self.input_spec = InputSpec(ndim=4)
98    super(Resizing, self).__init__(name=name, **kwargs)
99    base_preprocessing_layer.keras_kpl_gauge.get_cell('Resizing').set(True)
100
101  def call(self, inputs):
102    outputs = image_ops.resize_images_v2(
103        images=inputs,
104        size=[self.target_height, self.target_width],
105        method=self._interpolation_method)
106    return outputs
107
108  def compute_output_shape(self, input_shape):
109    input_shape = tensor_shape.TensorShape(input_shape).as_list()
110    return tensor_shape.TensorShape(
111        [input_shape[0], self.target_height, self.target_width, input_shape[3]])
112
113  def get_config(self):
114    config = {
115        'height': self.target_height,
116        'width': self.target_width,
117        'interpolation': self.interpolation,
118    }
119    base_config = super(Resizing, self).get_config()
120    return dict(list(base_config.items()) + list(config.items()))
121
122
123@keras_export('keras.layers.experimental.preprocessing.CenterCrop')
124class CenterCrop(PreprocessingLayer):
125  """Crop the central portion of the images to target height and width.
126
127  Input shape:
128    4D tensor with shape:
129    `(samples, height, width, channels)`, data_format='channels_last'.
130
131  Output shape:
132    4D tensor with shape:
133    `(samples, target_height, target_width, channels)`.
134
135  If the input height/width is even and the target height/width is odd (or
136  inversely), the input image is left-padded by 1 pixel.
137
138  Args:
139    height: Integer, the height of the output shape.
140    width: Integer, the width of the output shape.
141    name: A string, the name of the layer.
142  """
143
144  def __init__(self, height, width, name=None, **kwargs):
145    self.target_height = height
146    self.target_width = width
147    self.input_spec = InputSpec(ndim=4)
148    super(CenterCrop, self).__init__(name=name, **kwargs)
149    base_preprocessing_layer.keras_kpl_gauge.get_cell('CenterCrop').set(True)
150
151  def call(self, inputs):
152    inputs_shape = array_ops.shape(inputs)
153    img_hd = inputs_shape[H_AXIS]
154    img_wd = inputs_shape[W_AXIS]
155    img_hd_diff = img_hd - self.target_height
156    img_wd_diff = img_wd - self.target_width
157    checks = []
158    checks.append(
159        check_ops.assert_non_negative(
160            img_hd_diff,
161            message='The crop height {} should not be greater than input '
162            'height.'.format(self.target_height)))
163    checks.append(
164        check_ops.assert_non_negative(
165            img_wd_diff,
166            message='The crop width {} should not be greater than input '
167            'width.'.format(self.target_width)))
168    with ops.control_dependencies(checks):
169      bbox_h_start = math_ops.cast(img_hd_diff / 2, dtypes.int32)
170      bbox_w_start = math_ops.cast(img_wd_diff / 2, dtypes.int32)
171      bbox_begin = array_ops.stack([0, bbox_h_start, bbox_w_start, 0])
172      bbox_size = array_ops.stack(
173          [-1, self.target_height, self.target_width, -1])
174      outputs = array_ops.slice(inputs, bbox_begin, bbox_size)
175      return outputs
176
177  def compute_output_shape(self, input_shape):
178    input_shape = tensor_shape.TensorShape(input_shape).as_list()
179    return tensor_shape.TensorShape(
180        [input_shape[0], self.target_height, self.target_width, input_shape[3]])
181
182  def get_config(self):
183    config = {
184        'height': self.target_height,
185        'width': self.target_width,
186    }
187    base_config = super(CenterCrop, self).get_config()
188    return dict(list(base_config.items()) + list(config.items()))
189
190
191@keras_export('keras.layers.experimental.preprocessing.RandomCrop')
192class RandomCrop(PreprocessingLayer):
193  """Randomly crop the images to target height and width.
194
195  This layer will crop all the images in the same batch to the same cropping
196  location.
197  By default, random cropping is only applied during training. At inference
198  time, the images will be first rescaled to preserve the shorter side, and
199  center cropped. If you need to apply random cropping at inference time,
200  set `training` to True when calling the layer.
201
202  Input shape:
203    4D tensor with shape:
204    `(samples, height, width, channels)`, data_format='channels_last'.
205
206  Output shape:
207    4D tensor with shape:
208    `(samples, target_height, target_width, channels)`.
209
210  Args:
211    height: Integer, the height of the output shape.
212    width: Integer, the width of the output shape.
213    seed: Integer. Used to create a random seed.
214    name: A string, the name of the layer.
215  """
216
217  def __init__(self, height, width, seed=None, name=None, **kwargs):
218    self.height = height
219    self.width = width
220    self.seed = seed
221    self._rng = make_generator(self.seed)
222    self.input_spec = InputSpec(ndim=4)
223    super(RandomCrop, self).__init__(name=name, **kwargs)
224    base_preprocessing_layer.keras_kpl_gauge.get_cell('RandomCrop').set(True)
225
226  def call(self, inputs, training=True):
227    if training is None:
228      training = K.learning_phase()
229
230    def random_cropped_inputs():
231      """Cropped inputs with stateless random ops."""
232      input_shape = array_ops.shape(inputs)
233      crop_size = array_ops.stack(
234          [input_shape[0], self.height, self.width, input_shape[3]])
235      check = control_flow_ops.Assert(
236          math_ops.reduce_all(input_shape >= crop_size),
237          [self.height, self.width])
238      with ops.control_dependencies([check]):
239        limit = input_shape - crop_size + 1
240        offset = stateless_random_ops.stateless_random_uniform(
241            array_ops.shape(input_shape),
242            dtype=crop_size.dtype,
243            maxval=crop_size.dtype.max,
244            seed=self._rng.make_seeds()[:, 0]) % limit
245        return array_ops.slice(inputs, offset, crop_size)
246
247    # TODO(b/143885775): Share logic with Resize and CenterCrop.
248    def resize_and_center_cropped_inputs():
249      """Deterministically resize to shorter side and center crop."""
250      input_shape = array_ops.shape(inputs)
251      input_height_t = input_shape[H_AXIS]
252      input_width_t = input_shape[W_AXIS]
253      ratio_cond = (input_height_t / input_width_t > (self.height / self.width))
254      # pylint: disable=g-long-lambda
255      resized_height = control_flow_util.smart_cond(
256          ratio_cond,
257          lambda: math_ops.cast(self.width * input_height_t / input_width_t,
258                                input_height_t.dtype), lambda: self.height)
259      resized_width = control_flow_util.smart_cond(
260          ratio_cond, lambda: self.width,
261          lambda: math_ops.cast(self.height * input_width_t / input_height_t,
262                                input_width_t.dtype))
263      # pylint: enable=g-long-lambda
264      resized_inputs = image_ops.resize_images_v2(
265          images=inputs, size=array_ops.stack([resized_height, resized_width]))
266
267      img_hd_diff = resized_height - self.height
268      img_wd_diff = resized_width - self.width
269      bbox_h_start = math_ops.cast(img_hd_diff / 2, dtypes.int32)
270      bbox_w_start = math_ops.cast(img_wd_diff / 2, dtypes.int32)
271      bbox_begin = array_ops.stack([0, bbox_h_start, bbox_w_start, 0])
272      bbox_size = array_ops.stack([-1, self.height, self.width, -1])
273      outputs = array_ops.slice(resized_inputs, bbox_begin, bbox_size)
274      return outputs
275
276    output = control_flow_util.smart_cond(training, random_cropped_inputs,
277                                          resize_and_center_cropped_inputs)
278    original_shape = inputs.shape.as_list()
279    batch_size, num_channels = original_shape[0], original_shape[3]
280    output_shape = [batch_size] + [self.height, self.width] + [num_channels]
281    output.set_shape(output_shape)
282    return output
283
284  def compute_output_shape(self, input_shape):
285    input_shape = tensor_shape.TensorShape(input_shape).as_list()
286    return tensor_shape.TensorShape(
287        [input_shape[0], self.height, self.width, input_shape[3]])
288
289  def get_config(self):
290    config = {
291        'height': self.height,
292        'width': self.width,
293        'seed': self.seed,
294    }
295    base_config = super(RandomCrop, self).get_config()
296    return dict(list(base_config.items()) + list(config.items()))
297
298
299@keras_export('keras.layers.experimental.preprocessing.Rescaling')
300class Rescaling(PreprocessingLayer):
301  """Multiply inputs by `scale` and adds `offset`.
302
303  For instance:
304
305  1. To rescale an input in the `[0, 255]` range
306  to be in the `[0, 1]` range, you would pass `scale=1./255`.
307
308  2. To rescale an input in the `[0, 255]` range to be in the `[-1, 1]` range,
309  you would pass `scale=1./127.5, offset=-1`.
310
311  The rescaling is applied both during training and inference.
312
313  Input shape:
314    Arbitrary.
315
316  Output shape:
317    Same as input.
318
319  Args:
320    scale: Float, the scale to apply to the inputs.
321    offset: Float, the offset to apply to the inputs.
322    name: A string, the name of the layer.
323  """
324
325  def __init__(self, scale, offset=0., name=None, **kwargs):
326    self.scale = scale
327    self.offset = offset
328    super(Rescaling, self).__init__(name=name, **kwargs)
329    base_preprocessing_layer.keras_kpl_gauge.get_cell('Rescaling').set(True)
330
331  def call(self, inputs):
332    dtype = self._compute_dtype
333    scale = math_ops.cast(self.scale, dtype)
334    offset = math_ops.cast(self.offset, dtype)
335    return math_ops.cast(inputs, dtype) * scale + offset
336
337  def compute_output_shape(self, input_shape):
338    return input_shape
339
340  def get_config(self):
341    config = {
342        'scale': self.scale,
343        'offset': self.offset,
344    }
345    base_config = super(Rescaling, self).get_config()
346    return dict(list(base_config.items()) + list(config.items()))
347
348
349HORIZONTAL = 'horizontal'
350VERTICAL = 'vertical'
351HORIZONTAL_AND_VERTICAL = 'horizontal_and_vertical'
352
353
354@keras_export('keras.layers.experimental.preprocessing.RandomFlip')
355class RandomFlip(PreprocessingLayer):
356  """Randomly flip each image horizontally and vertically.
357
358  This layer will flip the images based on the `mode` attribute.
359  During inference time, the output will be identical to input. Call the layer
360  with `training=True` to flip the input.
361
362  Input shape:
363    4D tensor with shape:
364    `(samples, height, width, channels)`, data_format='channels_last'.
365
366  Output shape:
367    4D tensor with shape:
368    `(samples, height, width, channels)`, data_format='channels_last'.
369
370  Attributes:
371    mode: String indicating which flip mode to use. Can be "horizontal",
372      "vertical", or "horizontal_and_vertical". Defaults to
373      "horizontal_and_vertical". "horizontal" is a left-right flip and
374      "vertical" is a top-bottom flip.
375    seed: Integer. Used to create a random seed.
376    name: A string, the name of the layer.
377  """
378
379  def __init__(self,
380               mode=HORIZONTAL_AND_VERTICAL,
381               seed=None,
382               name=None,
383               **kwargs):
384    super(RandomFlip, self).__init__(name=name, **kwargs)
385    base_preprocessing_layer.keras_kpl_gauge.get_cell('RandomFlip').set(True)
386    self.mode = mode
387    if mode == HORIZONTAL:
388      self.horizontal = True
389      self.vertical = False
390    elif mode == VERTICAL:
391      self.horizontal = False
392      self.vertical = True
393    elif mode == HORIZONTAL_AND_VERTICAL:
394      self.horizontal = True
395      self.vertical = True
396    else:
397      raise ValueError('RandomFlip layer {name} received an unknown mode '
398                       'argument {arg}'.format(name=name, arg=mode))
399    self.seed = seed
400    self._rng = make_generator(self.seed)
401    self.input_spec = InputSpec(ndim=4)
402
403  def call(self, inputs, training=True):
404    if training is None:
405      training = K.learning_phase()
406
407    def random_flipped_inputs():
408      flipped_outputs = inputs
409      if self.horizontal:
410        flipped_outputs = image_ops.random_flip_left_right(
411            flipped_outputs, self.seed)
412      if self.vertical:
413        flipped_outputs = image_ops.random_flip_up_down(flipped_outputs,
414                                                        self.seed)
415      return flipped_outputs
416
417    output = control_flow_util.smart_cond(training, random_flipped_inputs,
418                                          lambda: inputs)
419    output.set_shape(inputs.shape)
420    return output
421
422  def compute_output_shape(self, input_shape):
423    return input_shape
424
425  def get_config(self):
426    config = {
427        'mode': self.mode,
428        'seed': self.seed,
429    }
430    base_config = super(RandomFlip, self).get_config()
431    return dict(list(base_config.items()) + list(config.items()))
432
433
434# TODO(tanzheny): Add examples, here and everywhere.
435@keras_export('keras.layers.experimental.preprocessing.RandomTranslation')
436class RandomTranslation(PreprocessingLayer):
437  """Randomly translate each image during training.
438
439  Args:
440    height_factor: a float represented as fraction of value, or a tuple of size
441      2 representing lower and upper bound for shifting vertically. A negative
442      value means shifting image up, while a positive value means shifting image
443      down. When represented as a single positive float, this value is used for
444      both the upper and lower bound. For instance, `height_factor=(-0.2, 0.3)`
445      results in an output shifted by a random amount in the range [-20%, +30%].
446      `height_factor=0.2` results in an output height shifted by a random amount
447      in the range [-20%, +20%].
448    width_factor: a float represented as fraction of value, or a tuple of size 2
449      representing lower and upper bound for shifting horizontally. A negative
450      value means shifting image left, while a positive value means shifting
451      image right. When represented as a single positive float, this value is
452      used for both the upper and lower bound. For instance,
453      `width_factor=(-0.2, 0.3)` results in an output shifted left by 20%, and
454      shifted right by 30%. `width_factor=0.2` results in an output height
455      shifted left or right by 20%.
456    fill_mode: Points outside the boundaries of the input are filled according
457      to the given mode (one of `{'constant', 'reflect', 'wrap', 'nearest'}`).
458      - *reflect*: `(d c b a | a b c d | d c b a)` The input is extended by
459        reflecting about the edge of the last pixel.
460      - *constant*: `(k k k k | a b c d | k k k k)` The input is extended by
461        filling all values beyond the edge with the same constant value k = 0.
462      - *wrap*: `(a b c d | a b c d | a b c d)` The input is extended by
463        wrapping around to the opposite edge.
464      - *nearest*: `(a a a a | a b c d | d d d d)` The input is extended by the
465        nearest pixel.
466    interpolation: Interpolation mode. Supported values: "nearest", "bilinear".
467    seed: Integer. Used to create a random seed.
468    name: A string, the name of the layer.
469    fill_value: a float represents the value to be filled outside the boundaries
470      when `fill_mode` is "constant".
471  Input shape:
472    4D tensor with shape: `(samples, height, width, channels)`,
473      data_format='channels_last'.
474  Output shape:
475    4D tensor with shape: `(samples, height, width, channels)`,
476      data_format='channels_last'.
477  Raise:
478    ValueError: if either bound is not between [0, 1], or upper bound is less
479      than lower bound.
480  """
481
482  def __init__(self,
483               height_factor,
484               width_factor,
485               fill_mode='reflect',
486               interpolation='bilinear',
487               seed=None,
488               name=None,
489               fill_value=0.0,
490               **kwargs):
491    self.height_factor = height_factor
492    if isinstance(height_factor, (tuple, list)):
493      self.height_lower = height_factor[0]
494      self.height_upper = height_factor[1]
495    else:
496      self.height_lower = -height_factor
497      self.height_upper = height_factor
498    if self.height_upper < self.height_lower:
499      raise ValueError('`height_factor` cannot have upper bound less than '
500                       'lower bound, got {}'.format(height_factor))
501    if abs(self.height_lower) > 1. or abs(self.height_upper) > 1.:
502      raise ValueError('`height_factor` must have values between [-1, 1], '
503                       'got {}'.format(height_factor))
504
505    self.width_factor = width_factor
506    if isinstance(width_factor, (tuple, list)):
507      self.width_lower = width_factor[0]
508      self.width_upper = width_factor[1]
509    else:
510      self.width_lower = -width_factor
511      self.width_upper = width_factor
512    if self.width_upper < self.width_lower:
513      raise ValueError('`width_factor` cannot have upper bound less than '
514                       'lower bound, got {}'.format(width_factor))
515    if abs(self.width_lower) > 1. or abs(self.width_upper) > 1.:
516      raise ValueError('`width_factor` must have values between [-1, 1], '
517                       'got {}'.format(width_factor))
518
519    check_fill_mode_and_interpolation(fill_mode, interpolation)
520
521    self.fill_mode = fill_mode
522    self.fill_value = fill_value
523    self.interpolation = interpolation
524    self.seed = seed
525    self._rng = make_generator(self.seed)
526    self.input_spec = InputSpec(ndim=4)
527    super(RandomTranslation, self).__init__(name=name, **kwargs)
528    base_preprocessing_layer.keras_kpl_gauge.get_cell('RandomTranslation').set(
529        True)
530
531  def call(self, inputs, training=True):
532    if training is None:
533      training = K.learning_phase()
534
535    def random_translated_inputs():
536      """Translated inputs with random ops."""
537      inputs_shape = array_ops.shape(inputs)
538      batch_size = inputs_shape[0]
539      h_axis, w_axis = H_AXIS, W_AXIS
540      img_hd = math_ops.cast(inputs_shape[h_axis], dtypes.float32)
541      img_wd = math_ops.cast(inputs_shape[w_axis], dtypes.float32)
542      height_translate = self._rng.uniform(
543          shape=[batch_size, 1],
544          minval=self.height_lower,
545          maxval=self.height_upper,
546          dtype=dtypes.float32)
547      height_translate = height_translate * img_hd
548      width_translate = self._rng.uniform(
549          shape=[batch_size, 1],
550          minval=self.width_lower,
551          maxval=self.width_upper,
552          dtype=dtypes.float32)
553      width_translate = width_translate * img_wd
554      translations = math_ops.cast(
555          array_ops.concat([width_translate, height_translate], axis=1),
556          dtype=dtypes.float32)
557      return transform(
558          inputs,
559          get_translation_matrix(translations),
560          interpolation=self.interpolation,
561          fill_mode=self.fill_mode,
562          fill_value=self.fill_value)
563
564    output = control_flow_util.smart_cond(training, random_translated_inputs,
565                                          lambda: inputs)
566    output.set_shape(inputs.shape)
567    return output
568
569  def compute_output_shape(self, input_shape):
570    return input_shape
571
572  def get_config(self):
573    config = {
574        'height_factor': self.height_factor,
575        'width_factor': self.width_factor,
576        'fill_mode': self.fill_mode,
577        'fill_value': self.fill_value,
578        'interpolation': self.interpolation,
579        'seed': self.seed,
580    }
581    base_config = super(RandomTranslation, self).get_config()
582    return dict(list(base_config.items()) + list(config.items()))
583
584
585def get_translation_matrix(translations, name=None):
586  """Returns projective transform(s) for the given translation(s).
587
588  Args:
589    translations: A matrix of 2-element lists representing [dx, dy] to translate
590      for each image (for a batch of images).
591    name: The name of the op.
592
593  Returns:
594    A tensor of shape (num_images, 8) projective transforms which can be given
595      to `transform`.
596  """
597  with K.name_scope(name or 'translation_matrix'):
598    num_translations = array_ops.shape(translations)[0]
599    # The translation matrix looks like:
600    #     [[1 0 -dx]
601    #      [0 1 -dy]
602    #      [0 0 1]]
603    # where the last entry is implicit.
604    # Translation matrices are always float32.
605    return array_ops.concat(
606        values=[
607            array_ops.ones((num_translations, 1), dtypes.float32),
608            array_ops.zeros((num_translations, 1), dtypes.float32),
609            -translations[:, 0, None],
610            array_ops.zeros((num_translations, 1), dtypes.float32),
611            array_ops.ones((num_translations, 1), dtypes.float32),
612            -translations[:, 1, None],
613            array_ops.zeros((num_translations, 2), dtypes.float32),
614        ],
615        axis=1)
616
617
618def transform(images,
619              transforms,
620              fill_mode='reflect',
621              fill_value=0.0,
622              interpolation='bilinear',
623              output_shape=None,
624              name=None):
625  """Applies the given transform(s) to the image(s).
626
627  Args:
628    images: A tensor of shape (num_images, num_rows, num_columns, num_channels)
629      (NHWC), (num_rows, num_columns, num_channels) (HWC), or (num_rows,
630      num_columns) (HW). The rank must be statically known (the shape is not
631      `TensorShape(None)`.
632    transforms: Projective transform matrix/matrices. A vector of length 8 or
633      tensor of size N x 8. If one row of transforms is [a0, a1, a2, b0, b1, b2,
634      c0, c1], then it maps the *output* point `(x, y)` to a transformed *input*
635      point `(x', y') = ((a0 x + a1 y + a2) / k, (b0 x + b1 y + b2) / k)`, where
636      `k = c0 x + c1 y + 1`. The transforms are *inverted* compared to the
637      transform mapping input points to output points. Note that gradients are
638      not backpropagated into transformation parameters.
639    fill_mode: Points outside the boundaries of the input are filled according
640      to the given mode (one of `{'constant', 'reflect', 'wrap', 'nearest'}`).
641    fill_value: a float represents the value to be filled outside the boundaries
642      when `fill_mode` is "constant".
643    interpolation: Interpolation mode. Supported values: "nearest", "bilinear".
644    output_shape: Output dimesion after the transform, [height, width]. If None,
645      output is the same size as input image.
646    name: The name of the op.  ## Fill mode.
647  Behavior for each valid value is as follows:  reflect (d c b a | a b c d | d c
648    b a) The input is extended by reflecting about the edge of the last pixel.
649    constant (k k k k | a b c d | k k k k) The input is extended by filling all
650    values beyond the edge with the same constant value k = 0.  wrap (a b c d |
651    a b c d | a b c d) The input is extended by wrapping around to the opposite
652    edge.  nearest (a a a a | a b c d | d d d d) The input is extended by the
653    nearest pixel.
654  Input shape:
655    4D tensor with shape: `(samples, height, width, channels)`,
656      data_format='channels_last'.
657  Output shape:
658    4D tensor with shape: `(samples, height, width, channels)`,
659      data_format='channels_last'.
660
661  Returns:
662    Image(s) with the same type and shape as `images`, with the given
663    transform(s) applied. Transformed coordinates outside of the input image
664    will be filled with zeros.
665
666  Raises:
667    TypeError: If `image` is an invalid type.
668    ValueError: If output shape is not 1-D int32 Tensor.
669  """
670  with K.name_scope(name or 'transform'):
671    if output_shape is None:
672      output_shape = array_ops.shape(images)[1:3]
673      if not context.executing_eagerly():
674        output_shape_value = tensor_util.constant_value(output_shape)
675        if output_shape_value is not None:
676          output_shape = output_shape_value
677
678    output_shape = ops.convert_to_tensor_v2_with_dispatch(
679        output_shape, dtypes.int32, name='output_shape')
680
681    if not output_shape.get_shape().is_compatible_with([2]):
682      raise ValueError('output_shape must be a 1-D Tensor of 2 elements: '
683                       'new_height, new_width, instead got '
684                       '{}'.format(output_shape))
685
686    fill_value = ops.convert_to_tensor_v2_with_dispatch(
687        fill_value, dtypes.float32, name='fill_value')
688
689    if compat.forward_compatible(2020, 8, 5):
690      return gen_image_ops.ImageProjectiveTransformV3(
691          images=images,
692          output_shape=output_shape,
693          fill_value=fill_value,
694          transforms=transforms,
695          fill_mode=fill_mode.upper(),
696          interpolation=interpolation.upper())
697
698    return gen_image_ops.ImageProjectiveTransformV2(
699        images=images,
700        output_shape=output_shape,
701        transforms=transforms,
702        fill_mode=fill_mode.upper(),
703        interpolation=interpolation.upper())
704
705
706def get_rotation_matrix(angles, image_height, image_width, name=None):
707  """Returns projective transform(s) for the given angle(s).
708
709  Args:
710    angles: A scalar angle to rotate all images by, or (for batches of images) a
711      vector with an angle to rotate each image in the batch. The rank must be
712      statically known (the shape is not `TensorShape(None)`).
713    image_height: Height of the image(s) to be transformed.
714    image_width: Width of the image(s) to be transformed.
715    name: The name of the op.
716
717  Returns:
718    A tensor of shape (num_images, 8). Projective transforms which can be given
719      to operation `image_projective_transform_v2`. If one row of transforms is
720       [a0, a1, a2, b0, b1, b2, c0, c1], then it maps the *output* point
721       `(x, y)` to a transformed *input* point
722       `(x', y') = ((a0 x + a1 y + a2) / k, (b0 x + b1 y + b2) / k)`,
723       where `k = c0 x + c1 y + 1`.
724  """
725  with K.name_scope(name or 'rotation_matrix'):
726    x_offset = ((image_width - 1) - (math_ops.cos(angles) *
727                                     (image_width - 1) - math_ops.sin(angles) *
728                                     (image_height - 1))) / 2.0
729    y_offset = ((image_height - 1) - (math_ops.sin(angles) *
730                                      (image_width - 1) + math_ops.cos(angles) *
731                                      (image_height - 1))) / 2.0
732    num_angles = array_ops.shape(angles)[0]
733    return array_ops.concat(
734        values=[
735            math_ops.cos(angles)[:, None],
736            -math_ops.sin(angles)[:, None],
737            x_offset[:, None],
738            math_ops.sin(angles)[:, None],
739            math_ops.cos(angles)[:, None],
740            y_offset[:, None],
741            array_ops.zeros((num_angles, 2), dtypes.float32),
742        ],
743        axis=1)
744
745
746@keras_export('keras.layers.experimental.preprocessing.RandomRotation')
747class RandomRotation(PreprocessingLayer):
748  """Randomly rotate each image.
749
750  By default, random rotations are only applied during training.
751  At inference time, the layer does nothing. If you need to apply random
752  rotations at inference time, set `training` to True when calling the layer.
753
754  Input shape:
755    4D tensor with shape:
756    `(samples, height, width, channels)`, data_format='channels_last'.
757
758  Output shape:
759    4D tensor with shape:
760    `(samples, height, width, channels)`, data_format='channels_last'.
761
762  Attributes:
763    factor: a float represented as fraction of 2pi, or a tuple of size 2
764      representing lower and upper bound for rotating clockwise and
765      counter-clockwise. A positive values means rotating counter clock-wise,
766      while a negative value means clock-wise. When represented as a single
767      float, this value is used for both the upper and lower bound. For
768      instance, `factor=(-0.2, 0.3)` results in an output rotation by a random
769      amount in the range `[-20% * 2pi, 30% * 2pi]`. `factor=0.2` results in an
770      output rotating by a random amount in the range `[-20% * 2pi, 20% * 2pi]`.
771    fill_mode: Points outside the boundaries of the input are filled according
772      to the given mode (one of `{'constant', 'reflect', 'wrap', 'nearest'}`).
773      - *reflect*: `(d c b a | a b c d | d c b a)` The input is extended by
774        reflecting about the edge of the last pixel.
775      - *constant*: `(k k k k | a b c d | k k k k)` The input is extended by
776        filling all values beyond the edge with the same constant value k = 0.
777      - *wrap*: `(a b c d | a b c d | a b c d)` The input is extended by
778        wrapping around to the opposite edge.
779      - *nearest*: `(a a a a | a b c d | d d d d)` The input is extended by the
780        nearest pixel.
781    interpolation: Interpolation mode. Supported values: "nearest", "bilinear".
782    seed: Integer. Used to create a random seed.
783    name: A string, the name of the layer.
784    fill_value: a float represents the value to be filled outside the boundaries
785      when `fill_mode` is "constant".
786  Raise:
787    ValueError: if either bound is not between [0, 1], or upper bound is less
788      than lower bound.
789  """
790
791  def __init__(self,
792               factor,
793               fill_mode='reflect',
794               interpolation='bilinear',
795               seed=None,
796               name=None,
797               fill_value=0.0,
798               **kwargs):
799    self.factor = factor
800    if isinstance(factor, (tuple, list)):
801      self.lower = factor[0]
802      self.upper = factor[1]
803    else:
804      self.lower = -factor
805      self.upper = factor
806    if self.upper < self.lower:
807      raise ValueError('Factor cannot have negative values, '
808                       'got {}'.format(factor))
809    check_fill_mode_and_interpolation(fill_mode, interpolation)
810    self.fill_mode = fill_mode
811    self.fill_value = fill_value
812    self.interpolation = interpolation
813    self.seed = seed
814    self._rng = make_generator(self.seed)
815    self.input_spec = InputSpec(ndim=4)
816    super(RandomRotation, self).__init__(name=name, **kwargs)
817    base_preprocessing_layer.keras_kpl_gauge.get_cell('RandomRotation').set(
818        True)
819
820  def call(self, inputs, training=True):
821    if training is None:
822      training = K.learning_phase()
823
824    def random_rotated_inputs():
825      """Rotated inputs with random ops."""
826      inputs_shape = array_ops.shape(inputs)
827      batch_size = inputs_shape[0]
828      img_hd = math_ops.cast(inputs_shape[H_AXIS], dtypes.float32)
829      img_wd = math_ops.cast(inputs_shape[W_AXIS], dtypes.float32)
830      min_angle = self.lower * 2. * np.pi
831      max_angle = self.upper * 2. * np.pi
832      angles = self._rng.uniform(
833          shape=[batch_size], minval=min_angle, maxval=max_angle)
834      return transform(
835          inputs,
836          get_rotation_matrix(angles, img_hd, img_wd),
837          fill_mode=self.fill_mode,
838          fill_value=self.fill_value,
839          interpolation=self.interpolation)
840
841    output = control_flow_util.smart_cond(training, random_rotated_inputs,
842                                          lambda: inputs)
843    output.set_shape(inputs.shape)
844    return output
845
846  def compute_output_shape(self, input_shape):
847    return input_shape
848
849  def get_config(self):
850    config = {
851        'factor': self.factor,
852        'fill_mode': self.fill_mode,
853        'fill_value': self.fill_value,
854        'interpolation': self.interpolation,
855        'seed': self.seed,
856    }
857    base_config = super(RandomRotation, self).get_config()
858    return dict(list(base_config.items()) + list(config.items()))
859
860
861@keras_export('keras.layers.experimental.preprocessing.RandomZoom')
862class RandomZoom(PreprocessingLayer):
863  """Randomly zoom each image during training.
864
865  Args:
866    height_factor: a float represented as fraction of value, or a tuple of size
867      2 representing lower and upper bound for zooming vertically. When
868      represented as a single float, this value is used for both the upper and
869      lower bound. A positive value means zooming out, while a negative value
870      means zooming in. For instance, `height_factor=(0.2, 0.3)` result in an
871      output zoomed out by a random amount in the range [+20%, +30%].
872      `height_factor=(-0.3, -0.2)` result in an output zoomed in by a random
873      amount in the range [+20%, +30%].
874    width_factor: a float represented as fraction of value, or a tuple of size 2
875      representing lower and upper bound for zooming horizontally. When
876      represented as a single float, this value is used for both the upper and
877      lower bound. For instance, `width_factor=(0.2, 0.3)` result in an output
878      zooming out between 20% to 30%. `width_factor=(-0.3, -0.2)` result in an
879      output zooming in between 20% to 30%. Defaults to `None`, i.e., zooming
880      vertical and horizontal directions by preserving the aspect ratio.
881    fill_mode: Points outside the boundaries of the input are filled according
882      to the given mode (one of `{'constant', 'reflect', 'wrap', 'nearest'}`).
883      - *reflect*: `(d c b a | a b c d | d c b a)` The input is extended by
884        reflecting about the edge of the last pixel.
885      - *constant*: `(k k k k | a b c d | k k k k)` The input is extended by
886        filling all values beyond the edge with the same constant value k = 0.
887      - *wrap*: `(a b c d | a b c d | a b c d)` The input is extended by
888        wrapping around to the opposite edge.
889      - *nearest*: `(a a a a | a b c d | d d d d)` The input is extended by the
890        nearest pixel.
891    interpolation: Interpolation mode. Supported values: "nearest", "bilinear".
892    seed: Integer. Used to create a random seed.
893    name: A string, the name of the layer.
894    fill_value: a float represents the value to be filled outside the boundaries
895      when `fill_mode` is "constant".
896  Example:  >>> input_img = np.random.random((32, 224, 224, 3)) >>> layer =
897    tf.keras.layers.experimental.preprocessing.RandomZoom(.5, .2) >>> out_img =
898    layer(input_img) >>> out_img.shape TensorShape([32, 224, 224, 3])
899  Input shape:
900    4D tensor with shape: `(samples, height, width, channels)`,
901      data_format='channels_last'.
902  Output shape:
903    4D tensor with shape: `(samples, height, width, channels)`,
904      data_format='channels_last'.
905  Raise:
906    ValueError: if lower bound is not between [0, 1], or upper bound is
907      negative.
908  """
909
910  def __init__(self,
911               height_factor,
912               width_factor=None,
913               fill_mode='reflect',
914               interpolation='bilinear',
915               seed=None,
916               name=None,
917               fill_value=0.0,
918               **kwargs):
919    self.height_factor = height_factor
920    if isinstance(height_factor, (tuple, list)):
921      self.height_lower = height_factor[0]
922      self.height_upper = height_factor[1]
923    else:
924      self.height_lower = -height_factor
925      self.height_upper = height_factor
926
927    if abs(self.height_lower) > 1. or abs(self.height_upper) > 1.:
928      raise ValueError('`height_factor` must have values between [-1, 1], '
929                       'got {}'.format(height_factor))
930
931    self.width_factor = width_factor
932    if width_factor is not None:
933      if isinstance(width_factor, (tuple, list)):
934        self.width_lower = width_factor[0]
935        self.width_upper = width_factor[1]
936      else:
937        self.width_lower = -width_factor  # pylint: disable=invalid-unary-operand-type
938        self.width_upper = width_factor
939
940      if self.width_lower < -1. or self.width_upper < -1.:
941        raise ValueError('`width_factor` must have values larger than -1, '
942                         'got {}'.format(width_factor))
943
944    check_fill_mode_and_interpolation(fill_mode, interpolation)
945
946    self.fill_mode = fill_mode
947    self.fill_value = fill_value
948    self.interpolation = interpolation
949    self.seed = seed
950    self._rng = make_generator(self.seed)
951    self.input_spec = InputSpec(ndim=4)
952    super(RandomZoom, self).__init__(name=name, **kwargs)
953    base_preprocessing_layer.keras_kpl_gauge.get_cell('RandomZoom').set(True)
954
955  def call(self, inputs, training=True):
956    if training is None:
957      training = K.learning_phase()
958
959    def random_zoomed_inputs():
960      """Zoomed inputs with random ops."""
961      inputs_shape = array_ops.shape(inputs)
962      batch_size = inputs_shape[0]
963      img_hd = math_ops.cast(inputs_shape[H_AXIS], dtypes.float32)
964      img_wd = math_ops.cast(inputs_shape[W_AXIS], dtypes.float32)
965      height_zoom = self._rng.uniform(
966          shape=[batch_size, 1],
967          minval=1. + self.height_lower,
968          maxval=1. + self.height_upper)
969      if self.width_factor is not None:
970        width_zoom = self._rng.uniform(
971            shape=[batch_size, 1],
972            minval=1. + self.width_lower,
973            maxval=1. + self.width_upper)
974      else:
975        width_zoom = height_zoom
976      zooms = math_ops.cast(
977          array_ops.concat([width_zoom, height_zoom], axis=1),
978          dtype=dtypes.float32)
979      return transform(
980          inputs,
981          get_zoom_matrix(zooms, img_hd, img_wd),
982          fill_mode=self.fill_mode,
983          fill_value=self.fill_value,
984          interpolation=self.interpolation)
985
986    output = control_flow_util.smart_cond(training, random_zoomed_inputs,
987                                          lambda: inputs)
988    output.set_shape(inputs.shape)
989    return output
990
991  def compute_output_shape(self, input_shape):
992    return input_shape
993
994  def get_config(self):
995    config = {
996        'height_factor': self.height_factor,
997        'width_factor': self.width_factor,
998        'fill_mode': self.fill_mode,
999        'fill_value': self.fill_value,
1000        'interpolation': self.interpolation,
1001        'seed': self.seed,
1002    }
1003    base_config = super(RandomZoom, self).get_config()
1004    return dict(list(base_config.items()) + list(config.items()))
1005
1006
1007def get_zoom_matrix(zooms, image_height, image_width, name=None):
1008  """Returns projective transform(s) for the given zoom(s).
1009
1010  Args:
1011    zooms: A matrix of 2-element lists representing [zx, zy] to zoom for each
1012      image (for a batch of images).
1013    image_height: Height of the image(s) to be transformed.
1014    image_width: Width of the image(s) to be transformed.
1015    name: The name of the op.
1016
1017  Returns:
1018    A tensor of shape (num_images, 8). Projective transforms which can be given
1019      to operation `image_projective_transform_v2`. If one row of transforms is
1020       [a0, a1, a2, b0, b1, b2, c0, c1], then it maps the *output* point
1021       `(x, y)` to a transformed *input* point
1022       `(x', y') = ((a0 x + a1 y + a2) / k, (b0 x + b1 y + b2) / k)`,
1023       where `k = c0 x + c1 y + 1`.
1024  """
1025  with K.name_scope(name or 'zoom_matrix'):
1026    num_zooms = array_ops.shape(zooms)[0]
1027    # The zoom matrix looks like:
1028    #     [[zx 0 0]
1029    #      [0 zy 0]
1030    #      [0 0 1]]
1031    # where the last entry is implicit.
1032    # Zoom matrices are always float32.
1033    x_offset = ((image_width - 1.) / 2.0) * (1.0 - zooms[:, 0, None])
1034    y_offset = ((image_height - 1.) / 2.0) * (1.0 - zooms[:, 1, None])
1035    return array_ops.concat(
1036        values=[
1037            zooms[:, 0, None],
1038            array_ops.zeros((num_zooms, 1), dtypes.float32),
1039            x_offset,
1040            array_ops.zeros((num_zooms, 1), dtypes.float32),
1041            zooms[:, 1, None],
1042            y_offset,
1043            array_ops.zeros((num_zooms, 2), dtypes.float32),
1044        ],
1045        axis=1)
1046
1047
1048@keras_export('keras.layers.experimental.preprocessing.RandomContrast')
1049class RandomContrast(PreprocessingLayer):
1050  """Adjust the contrast of an image or images by a random factor.
1051
1052  Contrast is adjusted independently for each channel of each image during
1053  training.
1054
1055  For each channel, this layer computes the mean of the image pixels in the
1056  channel and then adjusts each component `x` of each pixel to
1057  `(x - mean) * contrast_factor + mean`.
1058
1059  Input shape:
1060    4D tensor with shape:
1061    `(samples, height, width, channels)`, data_format='channels_last'.
1062
1063  Output shape:
1064    4D tensor with shape:
1065    `(samples, height, width, channels)`, data_format='channels_last'.
1066
1067  Attributes:
1068    factor: a positive float represented as fraction of value, or a tuple of
1069      size 2 representing lower and upper bound. When represented as a single
1070      float, lower = upper. The contrast factor will be randomly picked between
1071      [1.0 - lower, 1.0 + upper].
1072    seed: Integer. Used to create a random seed.
1073    name: A string, the name of the layer.
1074  Raise:
1075    ValueError: if lower bound is not between [0, 1], or upper bound is
1076      negative.
1077  """
1078
1079  def __init__(self, factor, seed=None, name=None, **kwargs):
1080    self.factor = factor
1081    if isinstance(factor, (tuple, list)):
1082      self.lower = factor[0]
1083      self.upper = factor[1]
1084    else:
1085      self.lower = self.upper = factor
1086    if self.lower < 0. or self.upper < 0. or self.lower > 1.:
1087      raise ValueError('Factor cannot have negative values or greater than 1.0,'
1088                       ' got {}'.format(factor))
1089    self.seed = seed
1090    self.input_spec = InputSpec(ndim=4)
1091    super(RandomContrast, self).__init__(name=name, **kwargs)
1092    base_preprocessing_layer.keras_kpl_gauge.get_cell('RandomContrast').set(
1093        True)
1094
1095  def call(self, inputs, training=True):
1096    if training is None:
1097      training = K.learning_phase()
1098
1099    def random_contrasted_inputs():
1100      return image_ops.random_contrast(inputs, 1. - self.lower, 1. + self.upper,
1101                                       self.seed)
1102
1103    output = control_flow_util.smart_cond(training, random_contrasted_inputs,
1104                                          lambda: inputs)
1105    output.set_shape(inputs.shape)
1106    return output
1107
1108  def compute_output_shape(self, input_shape):
1109    return input_shape
1110
1111  def get_config(self):
1112    config = {
1113        'factor': self.factor,
1114        'seed': self.seed,
1115    }
1116    base_config = super(RandomContrast, self).get_config()
1117    return dict(list(base_config.items()) + list(config.items()))
1118
1119
1120@keras_export('keras.layers.experimental.preprocessing.RandomHeight')
1121class RandomHeight(PreprocessingLayer):
1122  """Randomly vary the height of a batch of images during training.
1123
1124  Adjusts the height of a batch of images by a random factor. The input
1125  should be a 4-D tensor in the "channels_last" image data format.
1126
1127  By default, this layer is inactive during inference.
1128
1129  Args:
1130    factor: A positive float (fraction of original height), or a tuple of size 2
1131      representing lower and upper bound for resizing vertically. When
1132      represented as a single float, this value is used for both the upper and
1133      lower bound. For instance, `factor=(0.2, 0.3)` results in an output with
1134      height changed by a random amount in the range `[20%, 30%]`.
1135      `factor=(-0.2, 0.3)` results in an output with height changed by a random
1136      amount in the range `[-20%, +30%]. `factor=0.2` results in an output with
1137      height changed by a random amount in the range `[-20%, +20%]`.
1138    interpolation: String, the interpolation method. Defaults to `bilinear`.
1139      Supports `bilinear`, `nearest`, `bicubic`, `area`, `lanczos3`, `lanczos5`,
1140      `gaussian`, `mitchellcubic`
1141    seed: Integer. Used to create a random seed.
1142    name: A string, the name of the layer.
1143  Input shape:
1144    4D tensor with shape: `(samples, height, width, channels)`
1145      (data_format='channels_last').
1146  Output shape:
1147    4D tensor with shape: `(samples, random_height, width, channels)`.
1148  """
1149
1150  def __init__(self,
1151               factor,
1152               interpolation='bilinear',
1153               seed=None,
1154               name=None,
1155               **kwargs):
1156    self.factor = factor
1157    if isinstance(factor, (tuple, list)):
1158      self.height_lower = factor[0]
1159      self.height_upper = factor[1]
1160    else:
1161      self.height_lower = -factor
1162      self.height_upper = factor
1163
1164    if self.height_upper < self.height_lower:
1165      raise ValueError('`factor` cannot have upper bound less than '
1166                       'lower bound, got {}'.format(factor))
1167    if self.height_lower < -1. or self.height_upper < -1.:
1168      raise ValueError('`factor` must have values larger than -1, '
1169                       'got {}'.format(factor))
1170    self.interpolation = interpolation
1171    self._interpolation_method = get_interpolation(interpolation)
1172    self.input_spec = InputSpec(ndim=4)
1173    self.seed = seed
1174    self._rng = make_generator(self.seed)
1175    super(RandomHeight, self).__init__(name=name, **kwargs)
1176    base_preprocessing_layer.keras_kpl_gauge.get_cell('RandomHeight').set(True)
1177
1178  def call(self, inputs, training=True):
1179    if training is None:
1180      training = K.learning_phase()
1181
1182    def random_height_inputs():
1183      """Inputs height-adjusted with random ops."""
1184      inputs_shape = array_ops.shape(inputs)
1185      img_hd = math_ops.cast(inputs_shape[H_AXIS], dtypes.float32)
1186      img_wd = inputs_shape[W_AXIS]
1187      height_factor = self._rng.uniform(
1188          shape=[],
1189          minval=(1.0 + self.height_lower),
1190          maxval=(1.0 + self.height_upper))
1191      adjusted_height = math_ops.cast(height_factor * img_hd, dtypes.int32)
1192      adjusted_size = array_ops.stack([adjusted_height, img_wd])
1193      output = image_ops.resize_images_v2(
1194          images=inputs, size=adjusted_size, method=self._interpolation_method)
1195      original_shape = inputs.shape.as_list()
1196      output_shape = [original_shape[0]] + [None] + original_shape[2:4]
1197      output.set_shape(output_shape)
1198      return output
1199
1200    return control_flow_util.smart_cond(training, random_height_inputs,
1201                                        lambda: inputs)
1202
1203  def compute_output_shape(self, input_shape):
1204    input_shape = tensor_shape.TensorShape(input_shape).as_list()
1205    return tensor_shape.TensorShape(
1206        [input_shape[0], None, input_shape[2], input_shape[3]])
1207
1208  def get_config(self):
1209    config = {
1210        'factor': self.factor,
1211        'interpolation': self.interpolation,
1212        'seed': self.seed,
1213    }
1214    base_config = super(RandomHeight, self).get_config()
1215    return dict(list(base_config.items()) + list(config.items()))
1216
1217
1218@keras_export('keras.layers.experimental.preprocessing.RandomWidth')
1219class RandomWidth(PreprocessingLayer):
1220  """Randomly vary the width of a batch of images during training.
1221
1222  Adjusts the width of a batch of images by a random factor. The input
1223  should be a 4-D tensor in the "channels_last" image data format.
1224
1225  By default, this layer is inactive during inference.
1226
1227  Args:
1228    factor: A positive float (fraction of original height), or a tuple of size 2
1229      representing lower and upper bound for resizing vertically. When
1230      represented as a single float, this value is used for both the upper and
1231      lower bound. For instance, `factor=(0.2, 0.3)` results in an output with
1232      width changed by a random amount in the range `[20%, 30%]`. `factor=(-0.2,
1233      0.3)` results in an output with width changed by a random amount in the
1234      range `[-20%, +30%]. `factor=0.2` results in an output with width changed
1235      by a random amount in the range `[-20%, +20%]`.
1236    interpolation: String, the interpolation method. Defaults to `bilinear`.
1237      Supports `bilinear`, `nearest`, `bicubic`, `area`, `lanczos3`, `lanczos5`,
1238      `gaussian`, `mitchellcubic`
1239    seed: Integer. Used to create a random seed.
1240    name: A string, the name of the layer.
1241  Input shape:
1242    4D tensor with shape: `(samples, height, width, channels)`
1243      (data_format='channels_last').
1244  Output shape:
1245    4D tensor with shape: `(samples, height, random_width, channels)`.
1246  """
1247
1248  def __init__(self,
1249               factor,
1250               interpolation='bilinear',
1251               seed=None,
1252               name=None,
1253               **kwargs):
1254    self.factor = factor
1255    if isinstance(factor, (tuple, list)):
1256      self.width_lower = factor[0]
1257      self.width_upper = factor[1]
1258    else:
1259      self.width_lower = -factor
1260      self.width_upper = factor
1261    if self.width_upper < self.width_lower:
1262      raise ValueError('`factor` cannot have upper bound less than '
1263                       'lower bound, got {}'.format(factor))
1264    if self.width_lower < -1. or self.width_upper < -1.:
1265      raise ValueError('`factor` must have values larger than -1, '
1266                       'got {}'.format(factor))
1267    self.interpolation = interpolation
1268    self._interpolation_method = get_interpolation(interpolation)
1269    self.input_spec = InputSpec(ndim=4)
1270    self.seed = seed
1271    self._rng = make_generator(self.seed)
1272    super(RandomWidth, self).__init__(name=name, **kwargs)
1273    base_preprocessing_layer.keras_kpl_gauge.get_cell('RandomWidth').set(True)
1274
1275  def call(self, inputs, training=True):
1276    if training is None:
1277      training = K.learning_phase()
1278
1279    def random_width_inputs():
1280      """Inputs width-adjusted with random ops."""
1281      inputs_shape = array_ops.shape(inputs)
1282      img_hd = inputs_shape[H_AXIS]
1283      img_wd = math_ops.cast(inputs_shape[W_AXIS], dtypes.float32)
1284      width_factor = self._rng.uniform(
1285          shape=[],
1286          minval=(1.0 + self.width_lower),
1287          maxval=(1.0 + self.width_upper))
1288      adjusted_width = math_ops.cast(width_factor * img_wd, dtypes.int32)
1289      adjusted_size = array_ops.stack([img_hd, adjusted_width])
1290      output = image_ops.resize_images_v2(
1291          images=inputs, size=adjusted_size, method=self._interpolation_method)
1292      original_shape = inputs.shape.as_list()
1293      output_shape = original_shape[0:2] + [None] + [original_shape[3]]
1294      output.set_shape(output_shape)
1295      return output
1296
1297    return control_flow_util.smart_cond(training, random_width_inputs,
1298                                        lambda: inputs)
1299
1300  def compute_output_shape(self, input_shape):
1301    input_shape = tensor_shape.TensorShape(input_shape).as_list()
1302    return tensor_shape.TensorShape(
1303        [input_shape[0], input_shape[1], None, input_shape[3]])
1304
1305  def get_config(self):
1306    config = {
1307        'factor': self.factor,
1308        'interpolation': self.interpolation,
1309        'seed': self.seed,
1310    }
1311    base_config = super(RandomWidth, self).get_config()
1312    return dict(list(base_config.items()) + list(config.items()))
1313
1314
1315def make_generator(seed=None):
1316  """Creates a random generator.
1317
1318  Args:
1319    seed: the seed to initialize the generator. If None, the generator will be
1320      initialized non-deterministically.
1321
1322  Returns:
1323    A generator object.
1324  """
1325  if seed:
1326    return stateful_random_ops.Generator.from_seed(seed)
1327  else:
1328    return stateful_random_ops.Generator.from_non_deterministic_state()
1329
1330
1331def get_interpolation(interpolation):
1332  interpolation = interpolation.lower()
1333  if interpolation not in _RESIZE_METHODS:
1334    raise NotImplementedError(
1335        'Value not recognized for `interpolation`: {}. Supported values '
1336        'are: {}'.format(interpolation, _RESIZE_METHODS.keys()))
1337  return _RESIZE_METHODS[interpolation]
1338