"""
version: 0.0.1
*********************************
Dependencies
- numpy
- PIL
- numba
- opencv
*********************************
References:
- Numpy Documentation: https://numpy.org/doc/
- PIL Documentation: https://pillow.readthedocs.io/en/stable/
- Numba Documentation: https://numba.readthedocs.io/en/stable/
- OpenCV Documentation: https://docs.opencv.org/4.x/
- Voronoi Tessellation: a. https://en.wikipedia.org/wiki/Voronoi_diagram
b. https://www.generativehut.com/post/robots-and-generative-art-and-python-oh-my
c. https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Voronoi.html
- Perlin Noise: https://iq.opengenus.org/perlin-noise/
*********************************
"""
import os
import random
import warnings
import cv2
import numba as nb
import numpy as np
from numba import config
from numba import jit
from PIL import Image
from augraphy.base.augmentation import Augmentation
from augraphy.utilities.meshgenerator import Noise
from augraphy.utilities.slidingwindow import PatternMaker
warnings.filterwarnings("ignore")
[docs]
class VoronoiTessellation(Augmentation):
"""
This script generates a Voronoi Tessellation based on a set of random points in a plane. The tessellation
is visualized by coloring or shading the region around each point with the color or shade of the corresponding
random point. By default, Perlin Noise is added to the distances between each point and its closest random
point to create a smoother, more organic looking tessellation.
The class inherits methods and properties from the Augmentation base class.
:param mult_range: range for amplification factor to generate Perlin noise , default lies between 50 and 80
:type mult_range: tuple (int), optional
:param seed: The seed value for generating the Perlin Noise, default value is 19829813472
:type seed: int, optional
:param num_cells_range: Range for the number of cells used to generate the Voronoi Tessellation. Default
lies between 1000 and 9000.
:type num_cells_range: tuple (int), optional
:param noise_type: If "random", integration of Perlin Noise in the pipeline is randomly selected.
If noise_type is "perlin", perlin noise is added to the background pattern,
otherwise no Perlin Noise is added.Perlin Noise is added to the image to create a smoother,
more organic looking tessellation.
:type noise_type: string, optional
:param background_value: Range for background color assigned to each point
:type background_value: tuple (int)
:param numba_jit: The flag to enable numba jit to speed up the processing in the augmentation.
:type numba_jit: int, optional
:param p: The probability of applying the augmentation to an input image. Default value is 1.0
:type p: float
"""
def __init__(
self,
mult_range=(50, 80),
seed=19829813472,
num_cells_range=(500, 1000),
noise_type="random",
background_value=(200, 255),
numba_jit=1,
p=1,
):
super().__init__(p=p)
self.mult_range = mult_range
self.seed = seed
self.num_cells_range = num_cells_range
self.noise_type = noise_type
self.background_value = background_value
self.numba_jit = numba_jit
config.DISABLE_JIT = bool(1 - numba_jit)
def __repr__(self):
return f"Voronoi Tessellation(amplification_factor_range = {self.mult_range} , seed = {self.seed}, range of random points = {self.num_cells_range}, noise_type={self.noise_type}, background_value = {self.background_value}, numba_jit={self.numba_jit}, p={self.p})"
[docs]
@staticmethod
@jit(nopython=True, cache=True, parallel=True)
def generate_voronoi(width, height, num_cells, nsize, pixel_data, perlin_noise_2d):
"""
Generates Voronoi Tessellation
"""
img_array = np.zeros((width, height), dtype=np.uint8)
for y in nb.prange(width):
for x in nb.prange(height):
dmin = np.hypot(height, width)
for i in nb.prange(num_cells):
d = np.hypot(
(pixel_data[0][i] - x + perlin_noise_2d[0][x][y]),
(pixel_data[1][i] - y + perlin_noise_2d[1][x][y]),
)
if d < dmin:
dmin = d
j = i
nsize[j] += 1
img_array[y][x] = pixel_data[2][j]
return img_array
[docs]
def apply_augmentation(self):
obj_noise = Noise()
perlin_x = np.zeros((self.height, self.width))
perlin_y = np.zeros((self.height, self.width))
if self.perlin:
perlin_x = np.array(
[
[obj_noise.noise2D(x / 100, y / 100) * self.mult for y in range(self.height)]
for x in range(self.width)
],
)
perlin_y = np.array(
[
[
obj_noise.noise2D((x + self.seed) / 100, (y + self.seed) / 100) * self.mult
for y in range(self.height)
]
for x in range(self.width)
],
)
nx = [random.randrange(self.width) for _ in range(self.num_cells)] # x-coordinates of random points
ny = [random.randrange(self.height) for _ in range(self.num_cells)] # y-coordinates of random points
ng = [
random.randrange(self.background_value[0], self.background_value[1]) for _ in range(self.num_cells)
] # go through the number of cells and assign color
nsize = np.zeros(self.num_cells, dtype=np.int32)
img_array = self.generate_voronoi(
self.width,
self.height,
self.num_cells,
nsize,
(nx, ny, ng),
(perlin_x, perlin_y),
)
# try if it is able to save and read image properly, might facing permission issue
try:
image = Image.fromarray(img_array)
image.save(os.getcwd() + "/Voronoi_example.png", "PNG", dpi=(300, 300))
# reads it in a format so that it can be applied as a background pattern to the original image
mesh = cv2.imread(os.getcwd() + "/Voronoi_example.png")
os.remove(os.getcwd() + "/Voronoi_example.png")
except Exception:
mesh = img_array
return mesh
# Applies the Augmentation to input data.
def __call__(self, image, layer=None, force=False):
if force or self.should_run():
result = image.copy()
h, w = result.shape[:2]
if self.noise_type == "random":
self.perlin = random.choice([True, False])
elif self.noise_type == "perlin":
self.perlin = True
else:
self.perlin = False
if self.perlin:
self.width = self.height = random.choice(
[100, 120, 140, 160, 180, 200],
)
lst = [50, 70, 80, 90]
find_random_divisor = (
lambda lst, b: random.choice([x for x in lst if x != 0 and b % x == 0])
if any(x != 0 and b % x == 0 for x in lst)
else 40
)
self.ws = find_random_divisor(
lst,
self.width,
)
else:
self.width = self.height = random.choice(
[200, 210, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400],
)
lst = [100, 120, 140, 150, 160]
find_random_divisor = (
lambda lst, b: random.choice([x for x in lst if x != 0 and b % x == 0])
if any(x != 0 and b % x == 0 for x in lst)
else 40
)
self.ws = find_random_divisor(
lst,
self.width,
)
self.mult = random.randint(self.mult_range[0], self.mult_range[1])
self.num_cells = random.randint(self.num_cells_range[0], self.num_cells_range[1])
voronoi_mesh = self.apply_augmentation()
voronoi_mesh = cv2.resize(voronoi_mesh, (self.ws, self.ws), interpolation=(cv2.INTER_LINEAR))
if len(image.shape) < 3 and len(voronoi_mesh.shape) > 2:
voronoi_mesh = cv2.cvtColor(voronoi_mesh, cv2.COLOR_RGB2GRAY)
elif len(image.shape) > 2 and len(voronoi_mesh.shape) < 3:
voronoi_mesh = cv2.cvtColor(voronoi_mesh, cv2.COLOR_GRAY2BGR)
sw = PatternMaker()
# to ensure the voronoi tessellation covers the whole image,
# original image is padded and voronoi_mesh passes through it like a sliding window
result = sw.make_patterns(result, voronoi_mesh, self.ws)
result = result[self.ws : h + self.ws, self.ws : w + self.ws]
return result