# coding=utf-8
# Copyright 2019 The TensorFlow Datasets Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Common corruptions to images.

Define 12+4 common image corruptions: Gaussian noise, shot noise, impulse_noise,
defocus blur, frosted glass blur, zoom blur, fog, brightness, contrast, elastic,
pixelate, jpeg compression.

4 extra corruptions include gaussian blur, saturate, spatter, and speckle
noise.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import io
import numpy as np
import tensorflow_datasets.public_api as tfds

# /////////////// Corruption Helpers ///////////////


def around_and_astype(x):
  """Round a numpy array, and convert to uint8.

  Args:
    x: numpy array.

  Returns:
    numpy array with dtype uint8.
  """
  return np.around(x).astype(np.uint8)


def disk(radius, alias_blur=0.1, dtype=np.float32):
  """Generating a Gaussian blurring kernel with disk shape.

  Generating a Gaussian blurring kernel with disk shape using cv2 API.

  Args:
    radius: integer, radius of blurring kernel.
    alias_blur: float, standard deviation of Gaussian blurring.
    dtype: data type of kernel

  Returns:
    cv2 object of the Gaussian blurring kernel.
  """
  if radius <= 8:
    length = np.arange(-8, 8 + 1)
    ksize = (3, 3)
  else:
    length = np.arange(-radius, radius + 1)
    ksize = (5, 5)
  x_axis, y_axis = np.meshgrid(length, length)
  aliased_disk = np.array((x_axis**2 + y_axis**2) <= radius**2, dtype=dtype)
  aliased_disk /= np.sum(aliased_disk)
  # supersample disk to antialias
  return tfds.core.lazy_imports.cv2.GaussianBlur(
      aliased_disk, ksize=ksize, sigmaX=alias_blur)


def clipped_zoom(img, zoom_factor):
  """Zoom image with clipping.

  Zoom the central part of the image and clip extra pixels.

  Args:
    img: numpy array, uncorrupted image.
    zoom_factor: numpy array, a sequence of float numbers for zoom factor.

  Returns:
    numpy array, zoomed image after clipping.
  """
  h = img.shape[0]
  ch = int(np.ceil(h / float(zoom_factor)))
  top_h = (h - ch) // 2

  w = img.shape[1]
  cw = int(np.ceil(w / float(zoom_factor)))
  top_w = (w - cw) // 2

  img = tfds.core.lazy_imports.scipy.ndimage.zoom(
      img[top_h:top_h + ch, top_w:top_w + cw], (zoom_factor, zoom_factor, 1),
      order=1)

  # trim off any extra pixels
  trim_top_h = (img.shape[0] - h) // 2
  trim_top_w = (img.shape[1] - w) // 2

  return img[trim_top_h:trim_top_h + h, trim_top_w:trim_top_w + w]


def plasma_fractal(mapsize=512, wibbledecay=3):
  """Generate a heightmap using diamond-square algorithm.

  Modification of the algorithm in
  https://github.com/FLHerne/mapgen/blob/master/diamondsquare.py

  Args:
    mapsize: side length of the heightmap, must be a power of two.
    wibbledecay: integer, decay factor.

  Returns:
    numpy 2d array, side length 'mapsize', of floats in [0,255].
  """
  if mapsize & (mapsize - 1) != 0:
    raise ValueError('mapsize must be a power of two.')
  maparray = np.empty((mapsize, mapsize), dtype=np.float_)
  maparray[0, 0] = 0
  stepsize = mapsize
  wibble = 100

  def wibbledmean(array):
    return array / 4 + wibble * np.random.uniform(-wibble, wibble, array.shape)

  def fillsquares():
    """For each square, calculate middle value as mean of points + wibble."""
    cornerref = maparray[0:mapsize:stepsize, 0:mapsize:stepsize]
    squareaccum = cornerref + np.roll(cornerref, shift=-1, axis=0)
    squareaccum += np.roll(squareaccum, shift=-1, axis=1)
    maparray[stepsize // 2:mapsize:stepsize,
             stepsize // 2:mapsize:stepsize] = wibbledmean(squareaccum)

  def filldiamonds():
    """For each diamond, calculate middle value as meanof points + wibble."""
    mapsize = maparray.shape[0]
    drgrid = maparray[stepsize // 2:mapsize:stepsize,
                      stepsize // 2:mapsize:stepsize]
    ulgrid = maparray[0:mapsize:stepsize, 0:mapsize:stepsize]
    ldrsum = drgrid + np.roll(drgrid, 1, axis=0)
    lulsum = ulgrid + np.roll(ulgrid, -1, axis=1)
    ltsum = ldrsum + lulsum
    maparray[0:mapsize:stepsize,
             stepsize // 2:mapsize:stepsize] = wibbledmean(ltsum)
    tdrsum = drgrid + np.roll(drgrid, 1, axis=1)
    tulsum = ulgrid + np.roll(ulgrid, -1, axis=0)
    ttsum = tdrsum + tulsum
    maparray[stepsize // 2:mapsize:stepsize,
             0:mapsize:stepsize] = wibbledmean(ttsum)

  while stepsize >= 2:
    fillsquares()
    filldiamonds()
    stepsize //= 2
    wibble /= wibbledecay

  maparray -= maparray.min()
  return maparray / maparray.max()


# /////////////// End Corruption Helpers ///////////////

# /////////////// Corruptions ///////////////


def gaussian_noise(x, severity=1):
  """Gaussian noise corruption to images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Added Gaussian noise.
  """
  c = [.08, .12, 0.18, 0.26, 0.38][severity - 1]
  x = np.array(x) / 255.
  x_clip = np.clip(x + np.random.normal(size=x.shape, scale=c), 0, 1) * 255
  return around_and_astype(x_clip)


def shot_noise(x, severity=1):
  """Shot noise corruption to images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Added shot noise.
  """
  c = [60, 25, 12, 5, 3][severity - 1]
  x = np.array(x) / 255.
  x_clip = np.clip(np.random.poisson(x * c) / float(c), 0, 1) * 255
  return around_and_astype(x_clip)


def impulse_noise(x, severity=1):
  """Impulse noise corruption to images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Added impulse noise.
  """
  c = [.03, .06, .09, 0.17, 0.27][severity - 1]
  x = tfds.core.lazy_imports.skimage.util.random_noise(
      np.array(x) / 255., mode='s&p', amount=c)
  x_clip = np.clip(x, 0, 1) * 255
  return around_and_astype(x_clip)


def defocus_blur(x, severity=1):
  """Defocus blurring to images.

  Apply defocus blurring to images using Gaussian kernel.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied defocus blur.
  """
  c = [(3, 0.1), (4, 0.5), (6, 0.5), (8, 0.5), (10, 0.5)][severity - 1]
  x = np.array(x) / 255.
  kernel = disk(radius=c[0], alias_blur=c[1])
  channels = []
  for d in range(3):
    channels.append(tfds.core.lazy_imports.cv2.filter2D(x[:, :, d], -1, kernel))
  channels = np.array(channels).transpose((1, 2, 0))  # 3x224x224 -> 224x224x3
  x_clip = np.clip(channels, 0, 1) * 255
  return around_and_astype(x_clip)


def frosted_glass_blur(x, severity=1):
  """Frosted glass blurring to images.

  Apply frosted glass blurring to images by shuffling pixels locally.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied frosted glass blur.
  """
  # sigma, max_delta, iterations
  c = [(0.7, 1, 2), (0.9, 2, 1), (1, 2, 3), (1.1, 3, 2),
       (1.5, 4, 2)][severity - 1]
  x = np.uint8(
      tfds.core.lazy_imports.skimage.filters.gaussian(
          np.array(x) / 255., sigma=c[0], multichannel=True) * 255)

  # locally shuffle pixels
  for _ in range(c[2]):
    for h in range(x.shape[0] - c[1], c[1], -1):
      for w in range(x.shape[1] - c[1], c[1], -1):
        dx, dy = np.random.randint(-c[1], c[1], size=(2,))
        h_prime, w_prime = h + dy, w + dx
        # swap
        x[h, w], x[h_prime, w_prime] = x[h_prime, w_prime], x[h, w]
  x_clip = np.clip(
      tfds.core.lazy_imports.skimage.filters.gaussian(
          x / 255., sigma=c[0], multichannel=True), 0, 1)
  x_clip *= 255
  return around_and_astype(x_clip)


def zoom_blur(x, severity=1):
  """Zoom blurring to images.

  Applying zoom blurring to images by zooming the central part of the images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied zoom blur.
  """
  c = [
      np.arange(1, 1.11, 0.01),
      np.arange(1, 1.16, 0.01),
      np.arange(1, 1.21, 0.02),
      np.arange(1, 1.26, 0.02),
      np.arange(1, 1.31, 0.03)
  ][severity - 1]
  x = (np.array(x) / 255.).astype(np.float32)
  out = np.zeros_like(x)
  for zoom_factor in c:
    out += clipped_zoom(x, zoom_factor)
  x = (x + out) / (len(c) + 1)
  x_clip = np.clip(x, 0, 1) * 255
  return around_and_astype(x_clip)


def fog(x, severity=1):
  """Fog corruption to images.

  Adding fog to images. Fog is generated by diamond-square algorithm.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Added fog.
  """
  c = [(1.5, 2), (2., 2), (2.5, 1.7), (2.5, 1.5), (3., 1.4)][severity - 1]
  x = np.array(x) / 255.
  max_val = x.max()
  mapsize = 512
  shape = x.shape
  max_length = max(shape[0], shape[1])
  if max_length > mapsize:
    mapsize = 2**int(np.ceil(np.log2(float(max_length))))
  tmp = plasma_fractal(mapsize=mapsize, wibbledecay=c[1])
  tmp = tmp[:x.shape[0], :x.shape[1]]
  tmp = tmp[..., np.newaxis]
  x += c[0] * tmp
  x_clip = np.clip(x * max_val / (max_val + c[0]), 0, 1) * 255
  return around_and_astype(x_clip)


def brightness(x, severity=1):
  """Change brightness of images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Changed brightness.
  """
  c = [.1, .2, .3, .4, .5][severity - 1]

  x = np.array(x) / 255.
  x = tfds.core.lazy_imports.skimage.color.rgb2hsv(x)
  x[:, :, 2] = np.clip(x[:, :, 2] + c, 0, 1)
  x = tfds.core.lazy_imports.skimage.color.hsv2rgb(x)
  x_clip = np.clip(x, 0, 1) * 255
  return around_and_astype(x_clip)


def contrast(x, severity=1):
  """Change contrast of images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Changed contrast.
  """
  c = [0.4, .3, .2, .1, .05][severity - 1]

  x = np.array(x) / 255.
  means = np.mean(x, axis=(0, 1), keepdims=True)
  x_clip = np.clip((x - means) * c + means, 0, 1) * 255
  return around_and_astype(x_clip)


def elastic(x, severity=1):
  """Conduct elastic transform to images.

  Elastic transform is performed on small patches of the images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied elastic transform.
  """
  c = [(244 * 2, 244 * 0.7, 244 * 0.1), (244 * 2, 244 * 0.08, 244 * 0.2),
       (244 * 0.05, 244 * 0.01, 244 * 0.02),
       (244 * 0.07, 244 * 0.01, 244 * 0.02),
       (244 * 0.12, 244 * 0.01, 244 * 0.02)][severity - 1]

  image = np.array(x, dtype=np.float32) / 255.
  shape = image.shape
  shape_size = shape[:2]

  # random affine
  center_square = np.float32(shape_size) // 2
  square_size = min(shape_size) // 3
  pts1 = np.float32([
      center_square + square_size,
      [center_square[0] + square_size, center_square[1] - square_size],
      center_square - square_size
  ])
  pts2 = pts1 + np.random.uniform(
      -c[2], c[2], size=pts1.shape).astype(np.float32)
  affine_trans = tfds.core.lazy_imports.cv2.getAffineTransform(pts1, pts2)
  image = tfds.core.lazy_imports.cv2.warpAffine(
      image,
      affine_trans,
      shape_size[::-1],
      borderMode=tfds.core.lazy_imports.cv2.BORDER_REFLECT_101)

  dx = (tfds.core.lazy_imports.skimage.filters.gaussian(
      np.random.uniform(-1, 1, size=shape[:2]),
      c[1],
      mode='reflect',
      truncate=3) * c[0]).astype(np.float32)
  dy = (tfds.core.lazy_imports.skimage.filters.gaussian(
      np.random.uniform(-1, 1, size=shape[:2]),
      c[1],
      mode='reflect',
      truncate=3) * c[0]).astype(np.float32)
  dx, dy = dx[..., np.newaxis], dy[..., np.newaxis]

  x, y, z = np.meshgrid(
      np.arange(shape[1]), np.arange(shape[0]), np.arange(shape[2]))
  indices = np.reshape(y + dy,
                       (-1, 1)), np.reshape(x + dx,
                                            (-1, 1)), np.reshape(z, (-1, 1))
  x_clip = np.clip(
      tfds.core.lazy_imports.scipy.ndimage.interpolation.map_coordinates(
          image, indices, order=1, mode='reflect').reshape(shape), 0, 1) * 255
  return around_and_astype(x_clip)


def pixelate(x, severity=1):
  """Pixelate images.

  Conduct pixelating corruptions to images by first shrinking the images and
  then resizing to original size.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied pixelating
    corruption.
  """
  c = [0.6, 0.5, 0.4, 0.3, 0.25][severity - 1]
  shape = x.shape
  x = tfds.core.lazy_imports.PIL_Image.fromarray(x.astype(np.uint8))
  x = x.resize((int(shape[1] * c), int(shape[0] * c)))
  x = x.resize((shape[1], shape[0]))
  return np.asarray(x)


def jpeg_compression(x, severity=1):
  """Conduct jpeg compression to images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied jpeg compression.
  """
  c = [25, 18, 15, 10, 7][severity - 1]
  x = tfds.core.lazy_imports.PIL_Image.fromarray(x.astype(np.uint8))
  output = io.BytesIO()
  x.save(output, 'JPEG', quality=c)
  output.seek(0)
  x = tfds.core.lazy_imports.PIL_Image.open(output)
  return np.asarray(x)


# /////////////// Extra Corruptions ///////////////


def gaussian_blur(x, severity=1):
  """Apply gaussian blur to images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied gaussian blur.
  """
  c = [1, 2, 3, 4, 6][severity - 1]

  x = tfds.core.lazy_imports.gaussian(
      np.array(x) / 255., sigma=c, multichannel=True)
  x = np.clip(x, 0, 1) * 255

  return around_and_astype(x)


def saturate(x, severity=1):
  """Increase saturation of images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied saturation.
  """
  c = [(0.3, 0), (0.1, 0), (2, 0), (5, 0.1), (20, 0.2)][severity - 1]

  x = np.array(x) / 255.
  x = tfds.core.lazy_imports.skimage.color.rgb2hsv(x)
  x[:, :, 1] = np.clip(x[:, :, 1] * c[0] + c[1], 0, 1)
  x = tfds.core.lazy_imports.skimage.color.hsv2rgb(x)
  x = np.clip(x, 0, 1) * 255

  return around_and_astype(x)


def spatter(x, severity=1):
  """Apply spatter to images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied spatter.
  """
  cv2 = tfds.core.lazy_imports.cv2
  skimage = tfds.core.lazy_imports.skimage
  c = [(0.65, 0.3, 4, 0.69, 0.6, 0), (0.65, 0.3, 3, 0.68, 0.6, 0),
       (0.65, 0.3, 2, 0.68, 0.5, 0), (0.65, 0.3, 1, 0.65, 1.5, 1),
       (0.67, 0.4, 1, 0.65, 1.5, 1)][severity - 1]
  x = np.array(x, dtype=np.float32) / 255.

  liquid_layer = np.random.normal(size=x.shape[:2], loc=c[0], scale=c[1])

  liquid_layer = skimage.filters.gaussian(liquid_layer, sigma=c[2])
  liquid_layer[liquid_layer < c[3]] = 0
  if c[5] == 0:
    liquid_layer = (liquid_layer * 255).astype(np.uint8)
    dist = 255 - cv2.Canny(liquid_layer, 50, 150)
    dist = cv2.distanceTransform(dist, cv2.DIST_L2, 5)
    _, dist = cv2.threshold(dist, 20, 20, cv2.THRESH_TRUNC)
    dist = cv2.blur(dist, (3, 3)).astype(np.uint8)
    dist = cv2.equalizeHist(dist)
    #     ker = np.array([[-1,-2,-3],[-2,0,0],[-3,0,1]], dtype=np.float32)
    #     ker -= np.mean(ker)
    ker = np.array([[-2, -1, 0], [-1, 1, 1], [0, 1, 2]])
    dist = cv2.filter2D(dist, cv2.CV_8U, ker)
    dist = cv2.blur(dist, (3, 3)).astype(np.float32)

    m = cv2.cvtColor(liquid_layer * dist, cv2.COLOR_GRAY2BGRA)
    m /= np.max(m, axis=(0, 1))
    m *= c[4]

    # water is pale turqouise
    color = np.concatenate(
        (175 / 255. * np.ones_like(m[..., :1]), 238 / 255. *
         np.ones_like(m[..., :1]), 238 / 255. * np.ones_like(m[..., :1])),
        axis=2)

    color = cv2.cvtColor(color, cv2.COLOR_BGR2BGRA)
    x = cv2.cvtColor(x, cv2.COLOR_BGR2BGRA)

    x = cv2.cvtColor(np.clip(x + m * color, 0, 1), cv2.COLOR_BGRA2BGR) * 255
  else:
    m = np.where(liquid_layer > c[3], 1, 0)
    m = skimage.filters.gaussian(m.astype(np.float32), sigma=c[4])
    m[m < 0.8] = 0
    #         m = np.abs(m) ** (1/c[4])

    # mud brown
    color = np.concatenate(
        (63 / 255. * np.ones_like(x[..., :1]), 42 / 255. *
         np.ones_like(x[..., :1]), 20 / 255. * np.ones_like(x[..., :1])),
        axis=2)

    color *= m[..., np.newaxis]
    x *= (1 - m[..., np.newaxis])

    x = np.clip(x + color, 0, 1) * 255
  return around_and_astype(x)


def speckle_noise(x, severity=1):
  """Apply speckle noise to images.

  Args:
    x: numpy array, uncorrupted image, assumed to have uint8 pixel in [0,255].
    severity: integer, severity of corruption.

  Returns:
    numpy array, image with uint8 pixels in [0,255]. Applied speckle noise.
  """
  c = [.15, .2, 0.35, 0.45, 0.6][severity - 1]

  x = np.array(x) / 255.
  x = np.clip(x + x * np.random.normal(size=x.shape, scale=c), 0, 1) * 255
  return around_and_astype(x)
