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