Source code for augraphy.augmentations.inkshifter

"""
version: 0.0.1


Dependencies
*******************************************************************************
    - opencv
    - numpy


Documentation
********************************************************************************

    - Handwriter Repository: https://github.com/sherlockdoyle/Handwriter/tree/main
    - Noise Generation: https://pvigier.github.io/2018/06/13/perlin-noise-numpy.html
    - OpenCV remap() function : https://docs.opencv.org/3.4/d1/da0/tutorial_remap.html
    - Opencv meshgrid() function: https://numpy.org/doc/stable/reference/generated/numpy.meshgrid.html

"""
import random

import cv2
import numpy as np

from augraphy.base.augmentation import Augmentation


[docs] class InkShifter(Augmentation): def __init__( self, text_shift_scale_range=(18, 27), text_shift_factor_range=(1, 4), text_fade_range=(0, 2), blur_kernel_size=(5, 5), blur_sigma=0, noise_type="random", p=1.0, ): """ InkShifter augmentation shifts and displaces the image using noise maps. :param text_shift_scale_range (tuple): Range for the text shift scale. :param: text_shift_factor_range (tuple): Range for the text shift factor. :param: text_fade_range (tuple): Range for the text fade. :param: noise_type (str): Type of noise to use ("random", "perlin", or None). :param p (float): Probability of applying the augmentation. """ super().__init__(p=p) self.text_shift_scale_range = text_shift_scale_range self.text_shift_factor_range = text_shift_factor_range self.text_fade_range = text_fade_range self.noise_type = noise_type self.blur_kernel_size = blur_kernel_size self.blur_sigma = blur_sigma def __repr(self): return f"InkShifter: text_shift_scale_range = {self.text_shift_scale_range}, text_shift_factor_range = {self.text_shift_factor_range}, text_fade_range = {self.text_fade_range}, noise_type = {self.noise_type}, blur_kernel_size = {self.blur_kernel_size}, blur_sigma = {self.blur_sigma}"
[docs] def displace_image(self, img, mapx, mapy, fill=(255, 255, 255)): """ Apply displacement map to an image. :param img: Input Image :param mapx (numpy.ndarray): x-componet of the displacement map :param mapy (numpy.ndarray): y component of the displacement map :param fill: Fill value of the pixels outside the image """ gridx, gridy = np.meshgrid( np.arange(img.shape[1], dtype=np.float32), np.arange(img.shape[0], dtype=np.float32), ) if mapx is None: mapx = gridx else: mapx += gridx if mapy is None: mapy = gridy else: mapy += gridy return cv2.remap(img, mapx, mapy, cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT, borderValue=fill)
[docs] def noise_map(self, shape, res=(64, 64)): """ Generate a noise map based on Perlin Noise :param shape(tuple): Desired shape of the perlin noise map :param res(tuple): Resolution of the noise map """ orig_shape = shape shape = np.ceil(shape[0] / res[0]) * res[0], np.ceil(shape[1] / res[1]) * res[1] d0, d1 = shape[0] // res[0], shape[1] // res[1] angles = 2 * np.pi * np.random.rand(res[0] + 1, res[1] + 1) grad = np.dstack((np.cos(angles), np.sin(angles))) gysize, gxsize = grad.shape[:2] grid = np.mgrid[: res[0] : res[0] / shape[0], : res[1] : res[1] / shape[1]].transpose(1, 2, 0) % 1 # grid y size is larger after the ceil rounding, prune it if grid.shape[0] > (gysize - 1) * d0: difference = int(abs(grid.shape[0] - (gysize - 1) * d0)) grid = grid[:-difference, :] # grid y size is smaller after the ceil rounding, pad it elif grid.shape[0] < (gysize - 1) * d0: difference = int(abs(grid.shape[0] - (gysize - 1) * d0)) grid = np.pad( grid, # (top, bottom), (left, right) pad_width=((0, difference), (0, 0), (0, 0)), mode="edge", ) # grid x size is larger after the ceil rounding, prune it if grid.shape[1] > (gxsize - 1) * d1: difference = int(abs(grid.shape[1] - (gxsize - 1) * d1)) grid = grid[:, :-difference] # grid x size is smaller after the ceil rounding, pad it elif grid.shape[1] < (gxsize - 1) * d1: difference = int(abs(grid.shape[1] - (gxsize - 1) * d1)) grid = np.pad( grid, # (top, bottom), (left, right) pad_width=((0, 0), (0, difference), (0, 0)), mode="edge", ) n00 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1])) * grad[:-1, :-1].repeat(d0, 0).repeat(d1, 1), 2) n10 = np.sum(np.dstack((grid[:, :, 0] - 1, grid[:, :, 1])) * grad[1:, :-1].repeat(d0, 0).repeat(d1, 1), 2) n01 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1] - 1)) * grad[:-1, 1:].repeat(d0, 0).repeat(d1, 1), 2) n11 = np.sum(np.dstack((grid[:, :, 0] - 1, grid[:, :, 1] - 1)) * grad[1:, 1:].repeat(d0, 0).repeat(d1, 1), 2) t = 6 * grid**5 - 15 * grid**4 + 10 * grid**3 n0 = (1 - t[:, :, 0]) * n00 + t[:, :, 0] * n10 n1 = (1 - t[:, :, 0]) * n01 + t[:, :, 0] * n11 noise = (np.sqrt(2) * ((1 - t[:, :, 1]) * n0 + t[:, :, 1] * n1))[: orig_shape[0], : orig_shape[1]].astype( np.float32, ) noise_blurred = cv2.GaussianBlur(noise, self.blur_kernel_size, self.blur_sigma) return noise_blurred
[docs] def noise_map_fractal(self, shape, res=(64, 64), octaves=1, persistence=0.5): """ Generate a fractal noise map :param shape(tuple): desired shape of the fractal noise map :param res(tuple): resolution of the noise map :param octaves(int): Number of octaves in the fractal noise :param persistence (float): Persistence value for the fractal nois """ noise = np.zeros(shape) frequency = 1 amplitude = 1 for _ in range(octaves): noise += amplitude * self.noise_map(shape, (frequency * res[0], frequency * res[1])) frequency *= 2 amplitude *= persistence return noise.astype("float32")
[docs] def put_fading(self, img, fade, f=0.5): """ Apply fading effect to the image :param img(numpy.ndarray): input image :param fade(numpy.ndarray): fade values :param f(float): Fading factor """ fade -= fade.min() fade /= fade.max() fade += (1 - fade) * f return (255 - (255 - img) * fade.reshape((fade.shape[0], fade.shape[1], 1))).astype(np.uint8)
def __call__(self, image, layer=None, force=None): if force or self.should_run(): h, w, _ = image.shape text_shift_scale = random.randint(self.text_shift_scale_range[0], self.text_shift_scale_range[1]) text_shift_factor = random.randint(self.text_shift_factor_range[0], self.text_shift_factor_range[1]) if self.noise_type == "random": perlin_noise = random.choice([True, False]) elif self.noise_type == "perlin": perlin_noise = True if self.noise_type == "fractal": perlin_noise = False else: perlin_noise = True if perlin_noise: noisemap_x = self.noise_map((h, w), (text_shift_scale, text_shift_scale)) noisemap_y = self.noise_map((h, w), (text_shift_scale, text_shift_scale)) amp = random.random() disp_img = self.displace_image( image, -amp * text_shift_factor * noisemap_x, text_shift_factor * noisemap_y, ) else: noisemap_x = self.noise_map_fractal((h, w), (text_shift_scale, text_shift_scale)) noisemap_y = self.noise_map_fractal((h, w), (text_shift_scale, text_shift_scale)) amp = random.random() disp_img = self.displace_image( image, -amp * text_shift_factor * noisemap_x, text_shift_factor * noisemap_y, ) return disp_img