"""
version: 0.0.1
*********************************
Dependencies
- numpy
- opencv
*********************************
References:
- Scipy Documentation: https://docs.scipy.org/doc/scipy/
- Numpy Documentation: https://numpy.org/doc/
- OpenCV Documentation: https://docs.opencv.org/4.x/
- Delaunay Tessellation: https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Delaunay.html
- Perlin Noise: https://iq.opengenus.org/perlin-noise/
*********************************
"""
import random
import warnings
import cv2
import numpy as np
from scipy import ndimage
from augraphy.base.augmentation import Augmentation
from augraphy.utilities.meshgenerator import Noise
from augraphy.utilities.slidingwindow import PatternMaker
warnings.filterwarnings("ignore")
[docs]
class DelaunayTessellation(Augmentation):
"""
The Delaunay Tessellation is a method of dividing a geometric space
into a set of triangles. This implementation generate a Delaunay Tessellation of an image with Perlin Noise by default to create smoother,
more organic looking tessellations.
The Delaunay Tessellation algorithm is a method of traingulating a set of points in a planar space such that the minimum angle of
each triangle is maximized. This ensures that the triangles are as close to equilateral as possible.
The Delaunay Tessellation is defined as the triangulation of a set of points such that no point is inside the circumcircle of any triangle.
The algorithm works by iteratively adding points to the triangulation and re-triangulating the set of points after each point is added.
The algorithm ensures that the triangulation remains a Delaunay tessellation by checking for the Delaunay condition after each point is added.
The Delaunay Condition states that the circumcircle of each triangle in the triangulation must contain no other points in its interior.
The class inherits methods and properties from the Augmentation base class.
:param n_points_range: Range for the number of triangulating points from 500 to 800. Randomly selected.
:type n_points_range: tuple (int), optional
:param n_horizontal_points_range: Range for the number of points in the horizontal edge, from 500 to 800. The value is randomly selected.
:type n_horizontal_points_range: tuple (int), optional
:param n_vertical_points_range: Range for the number of points in the vertical edge, from 500 to 800. The value is randomly selected.
:type n_vertical_points_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 color_list: A single list contains a collection of colors (in BGR) where the color of the effect will be randomly selected from it.
Use "default" for default color or "random" for random colors.
:type color_list: list, optional
:param color_list_alternate: A single list contains a collection of colors (in BGR) where the alternate color of the effect will be randomly selected from it.
Use "default" for default color or "random" for random colors.
:type color_list_alternate: list, optional
:param p: The probability of applying the augmentation to an input image. Default value is 1.0
:type p: float, optional
"""
def __init__(
self,
n_points_range=(500, 800),
n_horizontal_points_range=(500, 800),
n_vertical_points_range=(500, 800),
noise_type="random",
color_list="default",
color_list_alternate="default",
p=1,
):
super().__init__(p=p)
self.n_points_range = n_points_range # no. of random points generated on the geometric plane
self.n_horizontal_points_range = n_horizontal_points_range # no. of horizontal edge points
self.n_vertical_points_range = n_vertical_points_range # no. of edge vertical points
self.noise_type = noise_type # apply perlin or not
self.color_list = color_list
self.color_list_alternate = color_list_alternate
def __repr__(self):
return f"Delaunay Tessellation range of random points on geometric plane = {self.n_points_range}, range of horizontal edge points = {self.n_horizontal_points_range}, range of vertical edge points = {self.n_vertical_points_range}, noise_type = {self.noise_type}, color_list = {self.color_list}, color_list_alternate = {self.color_list_alternate}"
def _edge_points(self, image):
"""
Generate Random Points on the edge of an document image
:param image: opencv image array
:param length_scale: how far to space out the points in the goemetric
document image plane
:param n_horizontal_points: number of points in the horizontal edge
Leave as None to use length_scale to determine
the value.
:param n_vertical_points: number of points in the vertical edge
Leave as None to use length_scale to determine
the value
:return: array of coordinates
"""
ymax, xmax = image.shape[:2]
if self.n_horizontal_points is None:
self.n_horizontal_points = int(xmax / 200)
if self.n_vertical_points is None:
self.n_vertical_points = int(ymax / 200)
delta_x = 4
delta_y = 4
return np.array(
[[0, 0], [xmax - 1, 0], [0, ymax - 1], [xmax - 1, ymax - 1]]
+ [[delta_x * i, 0] for i in range(1, self.n_horizontal_points)]
+ [[delta_x * i, ymax] for i in range(1, self.n_horizontal_points)]
+ [[0, delta_y * i] for i in range(1, self.n_vertical_points)]
+ [[xmax, delta_y * i] for i in range(1, self.n_vertical_points)]
+ [[xmax - delta_x * i, ymax] for i in range(1, self.n_vertical_points)],
)
[docs]
def apply_augmentation(self):
# Create an empty numpy array of zeros with the given size
img = np.ones((self.height, self.width, 3), np.uint8) * 255
# Define some points to use for the Delaunay triangulation
points = np.array(
[(random.uniform(0, self.width), random.uniform(0, self.height)) for i in range(self.n_points)],
)
points = np.concatenate([points, self._edge_points(img)])
# Perform the Delaunay triangulation on the points
rect = (0, 0, self.width, self.height)
subdiv = cv2.Subdiv2D(rect)
for p in points:
if p[0] >= 0 and p[0] < self.width and p[1] >= 0 and p[1] < self.height:
subdiv.insert((int(p[0]), int(p[1])))
triangles = subdiv.getTriangleList()
triangles = triangles.astype(np.int32)
if self.color_list == "default":
colors = [
(250, 235, 215),
(240, 240, 230),
(253, 245, 230),
(255, 245, 238),
(255, 248, 220),
(248, 248, 255),
(255, 240, 245),
(245, 255, 250),
(255, 250, 250),
(240, 248, 255),
(240, 255, 255),
(240, 255, 240),
(255, 245, 238),
(243, 229, 171),
(250, 250, 210),
]
elif self.color_list == "random":
colors = [[random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)] for _ in range(15)]
else:
colors = self.color_list
if self.color_list_alternate == "default":
alt_colors = [
(255, 255, 240),
(255, 250, 205),
(238, 232, 170),
(255, 255, 224),
(255, 239, 213),
]
elif self.color_list_alternate == "random":
alt_colors = [[random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)] for _ in range(5)]
else:
alt_colors = self.color_list_alternate
# adding perlin noise
if self.perlin:
obj_noise = Noise()
noise = np.array(
[
[obj_noise.noise2D(j / 200, i / 200) * 50 + 200 for j in range(self.width)]
for i in range(self.height)
],
np.float32,
)
# noise = np.array((noise - np.min(noise)) / (np.max(noise) - np.min(noise)) * 255 , np.uint8)
nh, nw = noise.shape
# Convert the blue texture to grayscale
gray_texture = np.dot(noise[..., :3], [0.299, 0.587, 0.114])
white_texture = np.zeros((nh, nw, 3), dtype=np.uint8)
white_texture[..., 0] = gray_texture
white_texture[..., 1] = gray_texture
white_texture[..., 2] = gray_texture
img = cv2.addWeighted(
white_texture,
0.1,
img,
0.9,
0,
) # creating a white texture from the perlin noise mesh
img = ndimage.gaussian_filter(img, sigma=(3, 3, 0), order=0) # applying gaussian filter
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Draw the Delaunay triangulation on the empty numpy array
for t in triangles:
pt1 = (t[0], t[1])
pt2 = (t[2], t[3])
pt3 = (t[4], t[5])
if (
pt1[0]
and pt2[0]
and pt3[0] <= self.width * 0.80
and (pt1[0] and pt2[0] and pt3[0] >= self.width * 0.40)
):
color = colors[np.random.randint(len(colors))] # choose from colors
elif pt1[0] and pt2[0] and pt3[0] <= self.width * 0.40:
color = alt_colors[np.random.randint(len(alt_colors))]
else:
color = alt_colors[np.random.randint(len(alt_colors))]
cv2.fillConvexPoly(img, np.array([pt1, pt2, pt3]), color)
else:
for t in triangles:
pt1 = (t[0], t[1])
pt2 = (t[2], t[3])
pt3 = (t[4], t[5])
if (
pt1[0]
and pt2[0]
and pt3[0] <= self.width * 0.80
and (pt1[0] and pt2[0] and pt3[0] >= self.width * 0.40)
):
color = colors[np.random.randint(len(colors))] # choose from colors
elif pt1[0] and pt2[0] and pt3[0] <= self.width * 0.40:
color = alt_colors[np.random.randint(len(alt_colors))]
else:
color = alt_colors[np.random.randint(len(alt_colors))]
color = colors[np.random.randint(len(colors))] # choose from colors
cv2.fillConvexPoly(img, np.array([pt1, pt2, pt3]), color)
return img
# Applies the Augmentation to input data.
def __call__(self, image, layer=None, force=False):
if force or self.should_run():
self.width = self.height = random.choice(
[400, 480, 500, 600, 640, 720],
) # randomly selecting the width and the height of the background pattern
self.n_points = random.randint(
self.n_points_range[0],
self.n_points_range[1],
) # randomly selecting the number of points in the geometric plane
self.n_horizontal_points = random.randint(
self.n_horizontal_points_range[0],
self.n_horizontal_points_range[1],
) # randomly selecting the edge horizontal points in the goemetric plane
self.n_vertical_points = random.randint(
self.n_vertical_points_range[0],
self.n_vertical_points_range[1],
) # randonly selecting the edge vertical points in the geometric plane
if self.noise_type == "random":
self.perlin = random.choice(
[True, False],
) # randomly select to apply Perlin Noise on top of the Tessellation
elif self.noise_type == "perlin":
self.perlin = True
else:
self.perlin = False
lst = [100, 120, 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,
) # finding the window size for the patch, which will be passed over the original image like a Sliding-Window
result = image.copy()
h, w = result.shape[:2]
delaunay_mesh = self.apply_augmentation()
threshold = self.ws // 20
delaunay_mesh = delaunay_mesh[threshold : h - threshold, threshold : w - threshold]
delaunay_mesh = cv2.resize(delaunay_mesh, (self.ws, self.ws), interpolation=cv2.INTER_LINEAR)
if len(image.shape) < 3:
delaunay_mesh = cv2.cvtColor(delaunay_mesh, cv2.COLOR_RGB2GRAY)
elif len(image.shape) == 3 and image.shape[2] == 1:
delaunay_mesh = cv2.cvtColor(delaunay_mesh, cv2.COLOR_RGB2GRAY)
sw = PatternMaker(alpha=0.49)
result = sw.make_patterns(image=result, mesh_img=delaunay_mesh, window_size=self.ws)
result = result[self.ws : h + self.ws, self.ws : w + self.ws]
return result