From da807797e12721b92a1699a82e26882fec094083 Mon Sep 17 00:00:00 2001 From: lvzhangcheng Date: Mon, 25 Oct 2021 20:45:07 +0800 Subject: [PATCH] add natural robustness methods --- mindarmour/natural_robustness/__init__.py | 16 + mindarmour/natural_robustness/natural_noise.py | 910 +++++++++++++++++++++++++ 2 files changed, 926 insertions(+) create mode 100644 mindarmour/natural_robustness/__init__.py create mode 100644 mindarmour/natural_robustness/natural_noise.py diff --git a/mindarmour/natural_robustness/__init__.py b/mindarmour/natural_robustness/__init__.py new file mode 100644 index 0000000..439f375 --- /dev/null +++ b/mindarmour/natural_robustness/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +""" +This package include methods to generate natural perturbation samples. +""" diff --git a/mindarmour/natural_robustness/natural_noise.py b/mindarmour/natural_robustness/natural_noise.py new file mode 100644 index 0000000..ce38176 --- /dev/null +++ b/mindarmour/natural_robustness/natural_noise.py @@ -0,0 +1,910 @@ +# Copyright 2019 Huawei Technologies Co., Ltd +# +# 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. +""" +Image transform +""" +import os +import math +import numpy as np +import cv2 +from perlin_numpy import generate_fractal_noise_2d + +from mindarmour.utils._check_param import check_param_multi_types, check_param_in_range, check_numpy_param, \ + check_int_positive, check_param_type, check_value_non_negative +from mindarmour.utils.logger import LogUtil + +LOGGER = LogUtil.get_instance() +TAG = 'Image Transformation' + + +class Contrast: + """ + Contrast of an image. + + Args: + alpha(Union[float, int]): Control the contrast of an image. If 1.0, + gives the original image. If 0, gives a gray image. Default: 1. + beta(Union[float, int]): delta added to alpha. Default: 0. + """ + + def __init__(self, alpha=1, beta=0): + super(Contrast, self).__init__() + self.set_params(alpha) + self.beta = check_param_multi_types('factor', beta, [int, float]) + + def set_params(self, alpha=1, auto_param=False): + """ + Set contrast parameters. + + Args: + alpha (Union[float, int]): Control the contrast of an image. If 1.0 + gives the original image. If 0 gives a gray image. Default: 1. + auto_param (bool): True if auto generate parameters. Default: False. + """ + if auto_param: + self.alpha = np.random.uniform(-5, 5) + else: + self.alpha = check_param_multi_types('factor', alpha, [int, float]) + + def __call__(self, image): + """ + Transform the image. + + Args: + image (numpy.ndarray): Original image to be transformed. + + Returns: + numpy.ndarray, transformed image. + """ + image = check_numpy_param('image', image) + new_img = cv2.convertScaleAbs(image, alpha=self.alpha, beta=self.beta) + return new_img + + +class GaussianBlur: + """ + Blurs the image using Gaussian blur filter. + + Args: + ksize(Union[list, tuple]): Blur radius, 0 means no blur. Default: 0. + """ + + def __init__(self, ksize=(5, 5)): + super(GaussianBlur, self).__init__() + ksize = check_param_multi_types('ksize', ksize, [list, tuple]) + self.ksize = tuple(ksize) + + def __call__(self, image): + """ + Transform the image. + + Args: + image (numpy.ndarray): Original image to be transformed. + + Returns: + numpy.ndarray, transformed image. + """ + image = check_numpy_param('image', image) + new_img = cv2.GaussianBlur(image, self.ksize, 0) + return new_img + + +class SaltAndPepperNoise: + """ + Add noise of an image. + + Args: + factor (float): factor is the ratio of pixels to add noise. + If 0 gives the original image. Default 0. + """ + + def __init__(self, factor=0): + super(SaltAndPepperNoise, self).__init__() + self.set_params(factor) + + def set_params(self, factor=0, auto_param=False): + """ + Set noise parameters. + + Args: + factor (Union[float, int]): factor is the ratio of pixels to + add noise. If 0 gives the original image. Default 0. + auto_param (bool): True if auto generate parameters. Default: False. + """ + if auto_param: + self.factor = np.random.uniform(0, 1) + else: + self.factor = check_param_multi_types('factor', factor, [int, float]) + + def __call__(self, image): + """ + Transform the image. + + Args: + image (numpy.ndarray): Original image to be transformed. + + Returns: + numpy.ndarray, transformed image. + """ + image = check_numpy_param('image', image) + ori_dtype = image.dtype + noise = np.random.uniform(low=-1, high=1, size=(image.shape[0], image.shape[1])) + trans_image = np.copy(image) + threshold = 1 - self.factor + trans_image[noise < -threshold] = (0, 0, 0) + trans_image[noise > threshold] = (255, 255, 255) + return trans_image.astype(ori_dtype) + + +class Translate: + """ + Translate an image. + + Args: + x_bias (Union[int, float]): X-direction translation, x = x + x_bias*image_length. + Default: 0. + y_bias (Union[int, float]): Y-direction translation, y = y + y_bias*image_wide. + Default: 0. + """ + + def __init__(self, x_bias=0, y_bias=0): + super(Translate, self).__init__() + self.set_params(x_bias, y_bias) + + def set_params(self, x_bias=0, y_bias=0, auto_param=False): + """ + Set translate parameters. + + Args: + x_bias (Union[float, int]): X-direction translation, and x_bias should be in range of (-1, 1). Default: 0. + y_bias (Union[float, int]): Y-direction translation, and y_bias should be in range of (-1, 1). Default: 0. + auto_param (bool): True if auto generate parameters. Default: False. + """ + x_bias = check_param_in_range('x_bias', x_bias, -1, 1) + y_bias = check_param_in_range('y_bias', y_bias, -1, 1) + self.auto_param = auto_param + if auto_param: + self.x_bias = np.random.uniform(-0.3, 0.3) + self.y_bias = np.random.uniform(-0.3, 0.3) + else: + self.x_bias = check_param_multi_types('x_bias', x_bias, [int, float]) + self.y_bias = check_param_multi_types('y_bias', y_bias, [int, float]) + + def __call__(self, image): + """ + Transform the image. + + Args: + image(numpy.ndarray): Original image to be transformed. + + Returns: + numpy.ndarray, transformed image. + """ + image = check_numpy_param('image', image) + h, w = image.shape[:2] + matrix = np.array([[1, 0, self.x_bias * w], [0, 1, self.y_bias * h]], dtype=np.float) + new_img = cv2.warpAffine(image, matrix, (w, h)) + return new_img + + +class Scale: + """ + Scale an image in the middle. + + Args: + factor_x (Union[float, int]): Rescale in X-direction, x=factor_x*x. + Default: 1. + factor_y (Union[float, int]): Rescale in Y-direction, y=factor_y*y. + Default: 1. + """ + + def __init__(self, factor_x=1, factor_y=1): + super(Scale, self).__init__() + self.set_params(factor_x, factor_y) + + def set_params(self, factor_x=1, factor_y=1, auto_param=False): + + """ + Set scale parameters. + + Args: + factor_x (Union[float, int]): Rescale in X-direction, x=factor_x*x. + Default: 1. + factor_y (Union[float, int]): Rescale in Y-direction, y=factor_y*y. + Default: 1. + auto_param (bool): True if auto generate parameters. Default: False. + """ + if auto_param: + self.factor_x = np.random.uniform(0.7, 3) + self.factor_y = np.random.uniform(0.7, 3) + else: + self.factor_x = check_param_multi_types('factor_x', factor_x, [int, float]) + self.factor_y = check_param_multi_types('factor_y', factor_y, [int, float]) + + def __call__(self, image): + """ + Transform the image. + + Args: + image(numpy.ndarray): Original image to be transformed. + + Returns: + numpy.ndarray, transformed image. + """ + image = check_numpy_param('image', image) + h, w = image.shape[:2] + matrix = np.array([[self.factor_x, 0, 0], [0, self.factor_y, 0]], dtype=np.float) + new_img = cv2.warpAffine(image, matrix, (w, h)) + return new_img + + +class Shear: + """ + Shear an image, for each pixel (x, y) in the sheared image, the new value is + taken from a position (x+factor_x*y, factor_y*x+y) in the origin image. Then + the sheared image will be rescaled to fit original size. + + Args: + factor_x (Union[float, int]): Shear factor of horizontal direction. + Default: 0. + factor_y (Union[float, int]): Shear factor of vertical direction. + Default: 0. + + """ + + def __init__(self, factor, director='horizonal'): + super(Shear, self).__init__() + self.factor = factor + self.director = director + + def __call__(self, image): + """ + Transform the image. + + Args: + image(numpy.ndarray): Original image to be transformed. + + Returns: + numpy.ndarray, transformed image. + """ + h, w = image.shape[:2] + if self.director == 'horizontal': + matrix = np.array([[1, self.factor, 0], [0, 1, 0]], dtype=np.float) + nw = int(w + self.factor * h) + nh = h + else: + matrix = np.array([[1, 0, 0], [self.factor, 1, 0]], dtype=np.float) + nw = w + nh = int(h + self.factor * w) + new_img = cv2.warpAffine(image, matrix, (nw, nh)) + new_img = cv2.resize(new_img, (w, h)) + return new_img + + +class Rotate: + """ + Rotate an image of degrees counter clockwise around its center. + + Args: + angle(Union[float, int]): Degrees counter clockwise. Default: 0. + """ + + def __init__(self, angle=0): + super(Rotate, self).__init__() + self.set_params(angle) + + def set_params(self, angle=0, auto_param=False): + """ + Set rotate parameters. + + Args: + angle(Union[float, int]): Degrees counter clockwise. Default: 0. + auto_param (bool): True if auto generate parameters. Default: False. + """ + if auto_param: + self.angle = np.random.uniform(0, 360) + else: + self.angle = check_param_multi_types('angle', angle, [int, float]) + + def __call__(self, image): + """ + Transform the image. + + Args: + image(numpy.ndarray): Original image to be transformed. + + Returns: + numpy.ndarray, rotated image. + """ + image = check_numpy_param('image', image) + h, w = image.shape[:2] + center = (w // 2, h // 2) + matrix = cv2.getRotationMatrix2D(center, -self.angle, 1.0) + cos = np.abs(matrix[0, 0]) + sin = np.abs(matrix[0, 1]) + + # 计算图像旋转后的新边界 + nw = int((h * sin) + (w * cos)) + nh = int((h * cos) + (w * sin)) + + # 调整旋转矩阵的移动距离(t_{x}, t_{y}) + matrix[0, 2] += (nw / 2) - center[0] + matrix[1, 2] += (nh / 2) - center[1] + rotate = cv2.warpAffine(image, matrix, (nw, nh)) + rotate = cv2.resize(rotate, (w, h)) + return rotate + + +class Perspective: + """ + Shear an image, for each pixel (x, y) in the sheared image, the new value is + taken from a position (x+factor_x*y, factor_y*x+y) in the origin image. Then + the sheared image will be rescaled to fit original size. + + Args: + factor_x (Union[float, int]): Shear factor of horizontal direction. + Default: 0. + factor_y (Union[float, int]): Shear factor of vertical direction. + Default: 0. + + """ + + def __init__(self, ori_pos, dst_pos): + super(Perspective, self).__init__() + ori_pos = check_param_type('ori_pos', ori_pos, list) + dst_pos = check_param_type('dst_pos', dst_pos, list) + self.ori_pos = np.float32(ori_pos) + self.dst_pos = np.float32(dst_pos) + + def __call__(self, image): + """ + Transform the image. + + Args: + image(numpy.ndarray): Original image to be transformed. + + Returns: + numpy.ndarray, transformed image. + """ + image = check_numpy_param('image', image) + h, w = image.shape[:2] + matrix = cv2.getPerspectiveTransform(self.ori_pos, self.dst_pos) + new_img = cv2.warpPerspective(image, matrix, (w, h)) + return new_img + + +class MotionBlur: + """ + Motion blur for a given image. + + Args: + degree (int): Degree of blur. This value must be positive. Default: 5. + angle: (union[float, int]): Direction of motion blur. Angle=0 means up and down motion blur. Angle is + counterclockwise. + + Example: + >>> img = cv2.imread('1.png') + >>> img = np.array(img) + >>> angle = 0 + >>> degree = 5 + >>> trans = MotionBlur(degree=degree, angle=angle) + >>> new_img = trans(img) + """ + + def __init__(self, degree=5, angle=45): + super(MotionBlur, self).__init__() + self.degree = check_int_positive('degree', degree) + self.degree = check_param_multi_types('degree', degree, [float, int]) + self.angle = angle - 45 + + def __call__(self, image): + """ + Motion blur for a given image. + + Args: + image (numpy.ndarray): Original image. + + Returns: + numpy.ndarray, image after motion blur. + """ + image = check_numpy_param('image', image) + matrix = cv2.getRotationMatrix2D((self.degree / 2, self.degree / 2), self.angle, 1) + motion_blur_kernel = np.diag(np.ones(self.degree)) + motion_blur_kernel = cv2.warpAffine(motion_blur_kernel, matrix, (self.degree, self.degree)) + motion_blur_kernel = motion_blur_kernel / self.degree + blurred = cv2.filter2D(image, -1, motion_blur_kernel) + # convert to uint8 + cv2.normalize(blurred, blurred, 0, 255, cv2.NORM_MINMAX) + blurred = np.array(blurred, dtype=np.uint8) + return blurred + + +class GradientBlur: + """ + Gradient blur. + + Args: + point(union[tuple, list]): 2D coordinate of the Blur center point. + kernel_num(int): Number of blur kernels. Default: 5. + center(bool): Blurred or clear at the center of a specified point. Default: True. + + Example: + >>> img = cv2.imread('xx.png') + >>> img = np.array(img) + >>> number = 5 + >>> h, w = img.shape[:2] + >>> point = (int(h / 5), int(w / 5)) + >>> center = True + >>> trans = GradientBlur(point, number, center) + >>> new_img = trans(img) + """ + + def __init__(self, point, kernel_num=3, center=True): + super(GradientBlur).__init__() + point = check_param_multi_types('point', point, [list, tuple]) + self.point = tuple(point) + self.kernel_num = check_int_positive('kernel_num', kernel_num) + self.center = check_param_type('center', center, bool) + + def __call__(self, image): + """ + + Args: + image(numpy.ndarray): Original image. + + Returns: + numpy.ndarray, gradient blurred image. + """ + image = check_numpy_param('image', image) + w, h = image.shape[:2] + mask = np.zeros(image.shape, dtype=np.uint8) + masks = [] + radius = max(w - self.point[0], self.point[0], h - self.point[1], self.point[1]) + radius = int(radius / self.kernel_num) + for i in range(self.kernel_num): + circle = cv2.circle(mask.copy(), self.point, radius * (1 + i), (1, 1, 1), -1) + masks.append(circle) + blurs = [] + for i in range(3, 3 + 2 * self.kernel_num, 2): + ksize = (i, i) + blur = cv2.GaussianBlur(image, ksize, 0) + blurs.append(blur) + + dst = image.copy() + if self.center: + for i in range(self.kernel_num): + dst = masks[i] * dst + (1 - masks[i]) * blurs[i] + else: + for i in range(self.kernel_num - 1, -1, -1): + dst = masks[i] * blurs[self.kernel_num - 1 - i] + (1 - masks[i]) * dst + return dst + + +def _circle_gradient_mask(img_src, color_start, color_end, scope=0.5, point=None): + """ + Generate circle gradient mask. + + Args: + img_src(numpy.ndarray): Source image. + color_start(union([tuple, list])): Color of circle gradient center. + color_end(union([tuple, list])): Color of circle gradient edge. + scope(float): + point(union([tuple, list]): center point. + + Returns: + numpy.ndarray, gradients mask. + """ + if not isinstance(img_src, np.ndarray): + raise TypeError('`src` must be numpy.ndarray type, but got {0}.'.format(type(img_src))) + + height, width = img_src.shape[:2] + + if point is None: + point = (height // 2, width // 2) + x, y = point + + # upper left + bound_upper_left = math.ceil(math.sqrt(x ** 2 + y ** 2)) + # upper right + bound_upper_right = math.ceil(math.sqrt(height ** 2 + (width - y) ** 2)) + # lower left + bound_lower_left = math.ceil(math.sqrt((height - x) ** 2 + y ** 2)) + # lower right + bound_lower_right = math.ceil(math.sqrt((height - x) ** 2 + (width - y) ** 2)) + + radius = max(bound_lower_left, bound_lower_right, bound_upper_left, bound_upper_right) * scope + + # img_grad = np.zeros_like(img_src, dtype=np.uint8) + img_grad = np.ones_like(img_src, dtype=np.uint8) * max(color_end) + # opencv use BGR format + grad_b = float(color_end[0] - color_start[0]) / radius + grad_g = float(color_end[1] - color_start[1]) / radius + grad_r = float(color_end[2] - color_start[2]) / radius + + for i in range(height): + for j in range(width): + distance = math.ceil(math.sqrt((x - i) ** 2 + (y - j) ** 2)) + if distance >= radius: + continue + img_grad[i, j, 0] = color_start[0] + distance * grad_b + img_grad[i, j, 1] = color_start[1] + distance * grad_g + img_grad[i, j, 2] = color_start[2] + distance * grad_r + + return img_grad + + +def _line_gradient_mask(image, start_pos=None, start_color=(0, 0, 0), end_color=(255, 255, 255), mode='horizontal'): + """ + Generate liner gradient mask. + + Args: + image(numpy.ndarray): Original image. + mode(str): mode must be in ['horizontal', 'vertical']. + """ + h, w = image.shape[:2] + if start_pos is None: + start_pos = 0.5 + else: + if mode == 'horizontal': + start_pos = start_pos[0] / h + else: + start_pos = start_pos[1] / w + start_color = np.array(start_color) + end_color = np.array(end_color) + if mode == 'horizontal': + w_l = int(w * start_pos) + w_r = w - w_l + if w_l > w_r: + r_end_color = (end_color - start_color) / start_pos * (1 - start_pos) + start_color + left = np.linspace(end_color, start_color, w_l) + right = np.linspace(start_color, r_end_color, w_r) + else: + l_end_color = (end_color - start_color) / (1 - start_pos) * start_pos + start_color + left = np.linspace(l_end_color, start_color, w_l) + right = np.linspace(start_color, end_color, w_r) + line = np.concatenate((left, right), axis=0) + mask = np.reshape(np.tile(line, (h, 1)), (h, w, 3)) + mask = np.array(mask, dtype=np.uint8) + else: + # 'vertical' + h_t = int(h * start_pos) + h_b = h - h_t + if h_t > h_b: + b_end_color = (end_color - start_color) / start_pos * (1 - start_pos) + start_color + top = np.linspace(end_color, start_color, h_t) + bottom = np.linspace(start_color, b_end_color, h_b) + else: + t_end_color = (end_color - start_color) / (1 - start_pos) * start_pos + start_color + top = np.linspace(t_end_color, start_color, h_t) + bottom = np.linspace(start_color, end_color, h_b) + line = np.concatenate((top, bottom), axis=0) + mask = np.reshape(np.tile(line, (w, 1)), (w, h, 3)) + mask = np.transpose(mask, [1, 0, 2]) + mask = np.array(mask, dtype=np.uint8) + return mask + + +class GradientLuminance: + """ + Gradient adjusts the luminance of picture. + + Args: + color_start(union[tuple, list]): Color of gradient center. Default:(0, 0, 0). + color_end(union[tuple, list]): Color of gradient edge. Default:(255, 255, 255). + start_point(union[tuple, list]): 2D coordinate of gradient center. + scope(float): Range of the gradient. A larger value indicates a larger gradient range. Default: 0.3. + bright_rate(float): Control brightness of . A larger value indicates a larger gradient range. Default: 0.3. + pattern(str): Dark or light, this value must be in ['light', 'dark']. + mode(str): Gradient mode, value must be in ['circle', 'horizontal', 'vertical']. + + Examples: + >>> img = cv2.imread('x.png') + >>> height, width = img.shape[:2] + >>> point = (height // 4, width // 2) + >>> start = (255, 255, 255) + >>> end = (0, 0, 0) + >>> scope = 0.3 + >>> trans = GradientLuminance(start, end, point, scope, pattern='light', mode='circle') + >>> img_new = trans(img) + """ + + def __init__(self, color_start, color_end, start_point, scope=0.5, pattern='light', bright_rate=0.3, mode='circle'): + self.color_start = check_param_multi_types('color_start', color_start, [list, tuple]) + self.color_end = check_param_multi_types('color_end', color_end, [list, tuple]) + self.start_point = check_param_multi_types('start_point', start_point, [list, tuple]) + self.scope = check_value_non_negative('scope', scope) + self.bright_rate = check_param_type('bright_rate', bright_rate, float) + self.bright_rate = check_param_in_range('bright_rate', bright_rate, 0, 1) + + if pattern in ['light', 'dark']: + self.pattern = pattern + else: + msg = "Value of param pattern must be in ['light', 'dark']" + LOGGER.error(TAG, msg) + raise ValueError(msg) + if mode in ['circle', 'horizontal', 'vertical']: + self.mode = mode + else: + msg = "Value of param mode must be in ['circle', 'horizontal', 'vertical']" + LOGGER.error(TAG, msg) + raise ValueError(msg) + + def __call__(self, image): + image = check_numpy_param('image', image) + if self.mode == 'circle': + mask = _circle_gradient_mask(image, self.color_start, self.color_end, self.scope, self.start_point) + else: + mask = _line_gradient_mask(image, self.start_point, self.color_start, self.color_end, mode=self.mode) + + if self.pattern == 'light': + img_new = cv2.addWeighted(image, 1, mask, self.bright_rate, 0.0) + else: + img_new = cv2.addWeighted(image, self.bright_rate, mask, 1 - self.bright_rate, 0.0) + return img_new + + +class Perlin: + """ + Add perlin noise to given image. + + Args: + ratio(float): Noise density. Default: 0.3. + shade(float): The degree of background shade color. Default: 0.1. + + Examples: + >>> img = cv2.imread('xx.png') + >>> img = np.array(img) + >>> shade = 0.1 + >>> ratio = 0.2 + >>> trans = Perlin(ratio, shade) + >>> new_img = trans(img) + """ + + def __init__(self, ratio, shade=0.1): + super(Perlin).__init__() + ratio = check_param_type('ratio', ratio, float) + ratio = check_param_in_range('ratio', ratio, 0, 1) + if ratio > 0.7: + self.ratio = 7 + else: + self.ratio = int(ratio * 10) + shade = check_param_type('shade', shade, float) + self.shade = check_param_in_range('shade', shade, 0, 1) + + def __call__(self, image): + """ + Args: + image(numpy.ndarray): Original image. + + Returns: + numpy.ndarray, image with perlin noise. + """ + image = check_numpy_param('image', image) + noise = generate_fractal_noise_2d((1024, 1024), (2 ** self.ratio, 2 ** self.ratio), 4) + noise[noise < 0] = 0 + noise[noise > 1] = 1 + back = np.array((1 - noise) * 255, dtype=np.uint8) + back = cv2.resize(back, (image.shape[1], image.shape[0])) + back = np.resize(np.repeat(back, 3), image.shape) + dst = cv2.addWeighted(image, 1 - self.shade, back, self.shade, 0) + return dst + + +class BackShadow: + """ + Add back ground picture to given image. + + Args: + template_path(str): Path of template pictures file. + shade(float): TWeight of background. Default: 0.1. + + Examples: + >>> img = cv2.imread('xx.png') + >>> img = np.array(img) + >>> template_path = 'template/leaf' + >>> shade = 0.2 + >>> trans = BackShadow(template_path, shade=shade) + >>> new_img = trans(img) + """ + + def __init__(self, template_path, shade=0.1): + super(BackShadow).__init__() + if os.path.exists(template_path): + self.template_path = template_path + else: + msg = "Template_path is not exist" + LOGGER.error(TAG, msg) + raise ValueError(msg) + shade = check_param_type('shade', shade, float) + self.shade = check_param_in_range('shade', shade, 0, 1) + + def __call__(self, image): + """ + Args: + image(numpy.ndarray): Original image. + + Returns: + numpy.ndarray, image with background shadow. + """ + image = check_numpy_param('image', image) + file = os.listdir(self.template_path) + file_path = os.path.join(self.template_path, np.random.choice(file)) + shadow = cv2.imread(file_path) + beta = 0 + shadow = cv2.resize(shadow, (image.shape[1], image.shape[0])) + dst = cv2.addWeighted(image, 1 - self.shade, shadow, self.shade, beta) + return dst + + +class NaturalNoise: + """ + Add natural noise to an image. + + Args: + ratio(float): Noise density. Default: 0.0002. + k_x_range(union[list, tuple]): Value range of the noise block length. + k_y_range(union[list, tuple]): Value range of the noise block width. + + Examples: + >>> img = cv2.imread('xx.png') + >>> img = np.array(img) + >>> k_x_range=(1, 5) + >>> k_y_range=(3, 25) + >>> trans = NaturalNoise(range=0.0002) + >>> new_img = trans(img) + + """ + + def __init__(self, ratio=0.0002, k_x_range=(1, 5), k_y_range=(3, 25)): + super(NaturalNoise).__init__() + self.ratio = check_param_type('ratio', ratio, float) + k_x_range = check_param_multi_types('k_x_range', k_x_range, [list, tuple]) + k_y_range = check_param_multi_types('k_y_range', k_y_range, [list, tuple]) + self.k_x_range = tuple(k_x_range) + self.k_y_range = tuple(k_y_range) + + def __call__(self, image): + """ + Add natural noise to given image. + Args: + image(numpy.ndarray): Original image. + + Returns: + numpy.ndarray, image with natural noise. + """ + image = check_numpy_param('image', image) + randon_range = 100 + w, h = image.shape[:2] + dst = np.ones((w, h, 3), dtype=np.uint8) * 255 + for _ in range(5): + noise = np.ones((w, h, 3), dtype=np.uint8) * 255 + rate = self.ratio / 5 + mask = np.random.uniform(size=(w, h)) < rate + noise[mask] = np.random.randint(0, randon_range) + + k_x, k_y = np.random.randint(*self.k_x_range), np.random.randint(*self.k_y_range) + kernel = np.ones((k_x, k_y), np.uint8) + erode = cv2.erode(noise, kernel, iterations=1) + dst = erode * (erode < randon_range) + dst * (1 - erode < randon_range) + # Add black point + for _ in range(np.random.randint(k_x * k_y / 2)): + x = np.random.randint(-k_x, k_x) + y = np.random.randint(-k_y, k_y) + matrix = np.array([[1, 0, y], [0, 1, x]], dtype=np.float) + affine = cv2.warpAffine(noise, matrix, (h, w)) + dst = affine * (affine < randon_range) + dst * (1 - affine < randon_range) + # Add white point + for _ in range(int(k_x * k_y / 2)): + x = np.random.randint(-k_x / 2 - 1, k_x / 2 + 1) + y = np.random.randint(-k_y / 2 - 1, k_y / 2 + 1) + matrix = np.array([[1, 0, y], [0, 1, x]], dtype=np.float) + affine = cv2.warpAffine(noise, matrix, (h, w)) + white = affine < randon_range + dst[white] = 255 + + mask = dst < randon_range + dst = image * (1 - mask) + dst * mask + dst = np.array(dst, dtype=np.uint8) + return dst + + +class Curve: + """ + Curve picture using sin method. + + Args: + curves(union[float, int]): Divide width to curves of `2*math.pi`, which means how many curve cycles. + depth(union[float, int]): Amplitude of sin method. + mode(str): Direction of deformation. Optional value is 'vertical' or 'horizontal'. + + Examples: + >>> img = cv2.imread('x.png') + >>> curves =1 + >>> depth = 10 + >>> trans = Curve(curves, depth, mode='vertical') + >>> img_new = trans(img) + """ + + def __init__(self, curves=10, depth=10, mode='vertical'): + super(Curve).__init__() + self.curves = check_value_non_negative('curves', curves) + self.depth = check_value_non_negative('depth', depth) + if mode in ['vertical', 'horizontal']: + self.mode = mode + else: + msg = "Value of param mode must be in ['vertical', 'horizontal']" + LOGGER.error(TAG, msg) + raise ValueError(msg) + + def __call__(self, image): + """ + Curve picture using sin method. + + Args: + image(numpy.ndarray): Original image. + + Returns: + numpy.ndarray, curved image. + """ + image = check_numpy_param('image', image) + if self.mode == 'vertical': + image = np.transpose(image, [1, 0, 2]) + heights, widths = image.shape[:2] + + src_x = np.zeros((heights, widths), np.float32) + src_y = np.zeros((heights, widths), np.float32) + + for y in range(heights): + for x in range(widths): + src_x[y, x] = x + src_y[y, x] = y + self.depth * math.sin(x / (widths / self.curves / 2 / math.pi)) + img_new = cv2.remap(image, src_x, src_y, cv2.INTER_LINEAR) + if self.mode == 'vertical': + img_new = np.transpose(img_new, [1, 0, 2]) + return img_new + + +class BackgroundWord: + """ + Overlay the background image on the original image. + + Args: + shade(float): Weight of background. + back(numpy.ndarray): Background Image. If none, mean background image is the as original image. + + Examples: + >>> img = cv2.imread('x.png') + >>> back = cv2.imread('x.png') + >>> shade=0.2 + >>> trans = BackgroundWord(shade, back) + >>> img_new = trans(img) + """ + + def __init__(self, shade=0.1, back=None): + super(BackgroundWord).__init__() + self.shade = shade + self.back = back + + def __call__(self, image): + image = check_numpy_param('image', image) + beta = 0 + width, height = image.shape[:2] + x = np.random.randint(0, int(width / 5)) + y = np.random.randint(0, int(height / 5)) + matrix = np.array([[1, 0, y], [0, 1, x]], dtype=np.float) + affine = cv2.warpAffine(image.copy(), matrix, (height, width)) + back = image.copy() + back[x:, y:] = affine[x:, y:] + dst = cv2.addWeighted(image, 1 - self.shade, back, self.shade, beta) + return dst