diff --git a/augraphy/augmentations/lib.py b/augraphy/augmentations/lib.py index 7c3079b..c28a872 100644 --- a/augraphy/augmentations/lib.py +++ b/augraphy/augmentations/lib.py @@ -391,8 +391,8 @@ def generate_texture(ysize, xsize, channel, value=255, sigma=1, turbulence=2): return image_output -def generate_edge_texture(oxsize, oysize): - """Generate a mask of edges based texture. +def generate_broken_edge_texture(oxsize, oysize): + """Generate a mask of broken edges based texture. :param oxsize: The width of the output mask. :type oxsize: int @@ -640,7 +640,7 @@ def generate_granular_texture(oxsize, oysize): x_grid, y_grid = np.meshgrid(x_array, y_array) # iterations for adding waves - iterations = random.randint(3, 5) + iterations = random.randint(1, 1) wave_grid = np.zeros((ysize, xsize), dtype="float") for i in range(iterations): @@ -707,6 +707,93 @@ def generate_granular_texture(oxsize, oysize): return wave_grid_output +def generate_curvy_edge_texture(oxsize, oysize): + """Generate a masked of curves based edge texture using FFT. + + :param oxsize: The width of the output texture image. + :type oxsize: int + :param oysize: The height of the output texture image. + :type oysize: int + """ + + # fixed internal resolution + ysize, xsize = 500, 500 + + wave_grid_output = np.zeros((ysize, xsize), dtype="uint8") + + for i in range(random.randint(1, 1)): + # fixed resolution of the wave image + resolution = random.uniform(0.9, 0.95) + + # Create a 2D grid of coordinates + x_array = np.arange(-xsize / 2, xsize / 2) * resolution + y_array = np.arange(-ysize / 2, ysize / 2) * resolution + x_grid, y_grid = np.meshgrid(x_array, y_array) + + wave_grid_fft_shifted = np.zeros((ysize, xsize), dtype="complex") + for i in range(random.randint(1, 1)): + # iterations for adding waves + iterations = random.randint(3, 5) + wave_grid = np.zeros((ysize, xsize), dtype="float") + for i in range(iterations): + + # Calculate the wave height using a sine function + A = np.random.uniform(5, 15) # Amplitude + f = np.random.uniform(0.05, 0.1) # Frequency + p = np.random.uniform(0, 2 * np.pi) # Phase + kx = np.random.uniform(-1, 1) # x-component of wave vector + ky = np.random.uniform(-1, 1) # y-component of wave vector + h_sine = A * np.sin(2 * np.pi * (f * (kx * x_grid + ky * y_grid) - p)) + + # Calculate the wave height using a cosine function + A = np.random.uniform(5, 15) # Amplitude + f = np.random.uniform(0.05, 0.1) # Frequency + p = np.random.uniform(0, 2 * np.pi) # Phase + kx = np.random.uniform(-1, 1) # x-component of wave vector + ky = np.random.uniform(-1, 1) # y-component of wave vector + h_cosine = A * np.cos(2 * np.pi * (f * (kx * x_grid + ky * y_grid) - p)) + + # combine heights from sine and cosine + wave_grid += h_sine + h_cosine + + # Compute the FFT of the wave heights, shift the zero-frequency component to the center and then sum them + wave_grid_fft_shifted += np.fft.fftshift(np.fft.fft2(wave_grid)) + + # unshift the FFT component + new_wave_grid = np.fft.ifft2(np.fft.ifftshift((wave_grid_fft_shifted))) + + # get the real part only + new_wave_grid = np.real(new_wave_grid) + + # scale to 0 -1 + new_wave_grid = (new_wave_grid - new_wave_grid.min()) / (new_wave_grid.max() - new_wave_grid.min()) + + # convert to uint8 + new_wave_grid = np.uint8(new_wave_grid * 255) + + # merge into output + wave_grid_output += new_wave_grid + + # blur to smoothen texture + wave_grid_output = cv2.GaussianBlur(wave_grid_output, (3, 3), 0) + + # remove frequency > 100 + frequency = 100 + wave_grid_output = remove_frequency(wave_grid_output, frequency=frequency) + + # rescale + wave_grid_output = (wave_grid_output - wave_grid_output.min()) / (wave_grid_output.max() - wave_grid_output.min()) + wave_grid_output = np.uint8(wave_grid_output * 255) + + # blur to smoothen trexture + wave_grid_output = cv2.GaussianBlur(wave_grid_output, (9, 9), 0) + + # resize to output size + wave_grid_output = cv2.resize(wave_grid_output, (oxsize, oysize), interpolation=cv2.INTER_LINEAR) + + return wave_grid_output + + def remove_frequency(wave_grid_output, frequency): """Remove image area bigger than the input frequency by using FFT. diff --git a/augraphy/base/paperfactory.py b/augraphy/base/paperfactory.py index 212c0d1..7555fdc 100644 --- a/augraphy/base/paperfactory.py +++ b/augraphy/base/paperfactory.py @@ -8,7 +8,8 @@ from augraphy.augmentations.brightness import Brightness from augraphy.augmentations.colorpaper import ColorPaper from augraphy.augmentations.lib import generate_average_intensity -from augraphy.augmentations.lib import generate_edge_texture +from augraphy.augmentations.lib import generate_broken_edge_texture +from augraphy.augmentations.lib import generate_curvy_edge_texture from augraphy.augmentations.lib import generate_granular_texture from augraphy.augmentations.lib import generate_stains_texture from augraphy.augmentations.lib import generate_strange_texture @@ -154,31 +155,42 @@ def generate_random_texture(self, image): patch_number_height = int(ysize / patch_size) texture = quilt_texture(texture, patch_size, patch_number_width, patch_number_height) - # add edges based texture - texture_edge = generate_edge_texture(texture.shape[1], texture.shape[0]) + # randomize edge type + edge_type = random.choice([0, 1]) - # get mask of edge texture - _, texture_edge_binary = cv2.threshold(texture_edge, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + # generate broken edge texture + if edge_type == 0: - contours, hierarchy = cv2.findContours( - texture_edge_binary, - cv2.RETR_LIST, - cv2.CHAIN_APPROX_NONE, - ) + # add edges based texture + texture_edge = generate_broken_edge_texture(texture.shape[1], texture.shape[0]) - # initialize mask of edge texture - texture_edge_mask = np.zeros_like(texture_edge, dtype="uint8") + # get mask of edge texture + _, texture_edge_binary = cv2.threshold(texture_edge, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) - # find largest inner contour as edge texture - for contour in contours: - x0, y0, width, height = cv2.boundingRect(contour) - area = cv2.contourArea(contour) - if area > (ysize * xsize * 0.6) and width != xsize and height != ysize: - texture_edge_mask = cv2.drawContours(texture_edge_mask, [contour], -1, (255), cv2.FILLED) - break + contours, hierarchy = cv2.findContours( + texture_edge_binary, + cv2.RETR_LIST, + cv2.CHAIN_APPROX_NONE, + ) + + # initialize mask of edge texture + texture_edge_mask = np.zeros_like(texture_edge, dtype="uint8") + + # find largest inner contour as edge texture + for contour in contours: + x0, y0, width, height = cv2.boundingRect(contour) + area = cv2.contourArea(contour) + if area > (ysize * xsize * 0.6) and width != xsize and height != ysize: + texture_edge_mask = cv2.drawContours(texture_edge_mask, [contour], -1, (255), cv2.FILLED) + break - # remove area outside edge texture - texture[texture_edge_mask <= 0] = 0 + # remove area outside edge texture + texture[texture_edge_mask <= 0] = 0 + + # generate curvy edge texture + else: + texture_edge = generate_curvy_edge_texture(texture.shape[1], texture.shape[0]) + texture = cv2.multiply(texture, texture_edge, scale=1 / 255) # randomly crop 1 or 2 side of edge crop_x = int(xsize / 20)