import random
import cv2
import numpy as np
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from skimage.morphology import thin
from sklearn.datasets import make_blobs
from augraphy.augmentations.brightness import Brightness
from augraphy.augmentations.lib import add_noise
from augraphy.augmentations.lib import binary_threshold
from augraphy.augmentations.lib import rotate_image
from augraphy.augmentations.lib import sobel
[docs]
class InkGenerator:
"""Core object to generate different inks effect.
:param ink_type: Types of ink, select from "pencil", "pen", "marker" or "highlighter".
:type ink_type: string
:param ink_draw_method: Content of ink generation, select from "lines" or "text".
:param ink_draw_method: string
:param ink_draw_iterations: Tuple of ints determining the drawing iterations
:param ink_draw_iterations: int
:param ink_location: Tuple of ints determining location of ink drawings.
Or use "random: for random line location.
:type ink_location: tuple or string
:param ink_background: Background of ink generation.
:param ink_background: numpy array
:param ink_background_size: Tuple of ints (height, width) or (height, width, channels)
determining the size of new background for ink generation.
A new background will be created only if ink_background is not provided.
:param ink_background_size: tuple
:param ink_background_color: Tuple of ints (BGR) determining the color of background.
:type ink_background_color: tuple
:param ink_color: Tuple of ints (BGR) determining the color of ink.
:type ink_color: tuple
:param ink_min_brightness: Flag to enable min brightness in the generated ink.
:type ink_min_brightness: int
:param ink_min_brightness_value_range: Pair of ints determining the range for min brightness value in the generated ink.
:type ink_min_brightness_value_range: tuple
:param ink_draw_size_range: Pair of floats determining the range for
the size of the ink drawing.
:type ink_draw_size_range: tuple
:param ink_thickness_range: Pair of floats determining the range for the thickness of the generated ink.
:type scribbles_thickness_range: tuple
:param ink_brightness_change: A list of value change for the brightness of the ink.
If more than one value is provided, the final value will be randomly selected.
:type ink_brightness_change: list
:param ink_skeletonize: Flag to enable skeletonization in the generated drawings.
:type ink_skeletonize: int
:param ink_skeletonize_iterations_range: Pair of ints determining the number of iterations in skeletonization process.
:type ink_skeletonize_iterations_range: int
:param ink_text: Text value of ink generation, valid only if ink_draw_method is "text".
:param ink_text: string
:param ink_text_font: List contain paths to font types. Valid only if ink content is "text".
:type ink_text_font: list
:param ink_text_rotate_range: Tuple of ints to determine rotation angle of "text" based drawings.
:type ink_text_rotate_range: tuple
:param ink_lines_coordinates: A list contain coordinates of line.
:type ink_lines_coordinates: list
:param ink_lines_stroke_count_range: Pair of floats determining the range for
the number of created lines.
:type ink_lines_stroke_count_range: tuple
"""
def __init__(
self,
ink_type,
ink_draw_method,
ink_draw_iterations,
ink_location,
ink_background,
ink_background_size,
ink_background_color,
ink_color,
ink_min_brightness,
ink_min_brightness_value_range,
ink_draw_size_range,
ink_thickness_range,
ink_brightness_change,
ink_skeletonize,
ink_skeletonize_iterations_range,
ink_text,
ink_text_font,
ink_text_rotate_range,
ink_lines_coordinates,
ink_lines_stroke_count_range,
):
self.ink_type = ink_type
self.ink_draw_method = ink_draw_method
self.ink_draw_iterations = ink_draw_iterations
self.ink_location = ink_location
self.ink_background = ink_background
self.ink_background_size = ink_background_size
self.ink_background_color = ink_background_color
self.ink_color = ink_color
self.ink_min_brightness = ink_min_brightness
self.ink_min_brightness_value_range = ink_min_brightness_value_range
self.ink_draw_size_range = ink_draw_size_range
self.ink_thickness_range = ink_thickness_range
self.ink_brightness_change = ink_brightness_change
self.ink_skeletonize = ink_skeletonize
self.ink_skeletonize_iterations_range = ink_skeletonize_iterations_range
self.ink_text = ink_text
self.ink_text_font = ink_text_font
self.ink_text_rotate_range = ink_text_rotate_range
self.ink_lines_coordinates = ink_lines_coordinates
self.ink_lines_stroke_count_range = ink_lines_stroke_count_range
[docs]
def apply_brightness(self, image):
"""Brighten image based on the minimum brightness value by using Brightness augmentation.
:param image: The image to be brighten.
:type image: numpy.array (numpy.uint8)
"""
# get location of intensity < min intensity
min_intensity = random.randint(self.ink_min_brightness_value_range[0], self.ink_min_brightness_value_range[1])
image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
y_location, x_location = np.where(image_hsv[:, :, 2] < min_intensity)
# if there's location where intensity < min intensity, apply brightness
if len(y_location) > 0:
image_min_intensity = min(image_hsv[:, :, 2][y_location, x_location])
if image_min_intensity > 0 and image_min_intensity < min_intensity:
brighten_ratio = abs(image_min_intensity - min_intensity) / image_min_intensity
brighten_min = 1 + brighten_ratio
brighten_max = 1 + brighten_ratio + 0.5
brightness = Brightness(brightness_range=(brighten_min, brighten_max))
image = brightness(image)
return image
[docs]
def apply_pencil_effect(self, ink_image, ink_background):
"""Apply foreground image with pencil effect to background image.
:param ink_image: Image with pencil drawings.
:type ink_image: numpy.array (numpy.uint8)
:param image: The background image.
:type image: numpy.array (numpy.uint8)
"""
# create pencil effect by changing some pixels into white background
gray_image = cv2.cvtColor(ink_image, cv2.COLOR_BGR2GRAY)
noise_mask = add_noise(gray_image, (1.0, 1.0), (0, 2), 4)
for i in range(ink_image.shape[2]):
ink_image[:, :, i][noise_mask == 0] = 255
ink_image = cv2.GaussianBlur(ink_image, (3, 3), 0)
# add some brightness in value channel of hsv image
hsv = cv2.cvtColor(ink_image, cv2.COLOR_BGR2HSV)
hsv = np.array(hsv, dtype=np.float64)
hsv[:, :, 2] += random.choice(self.ink_brightness_change)
hsv[:, :, 2][hsv[:, :, 2] > 255] = 255
hsv = np.array(hsv, dtype=np.uint8)
ink_image = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
return cv2.multiply(ink_image, ink_background, scale=1 / 255)
[docs]
def apply_pen_effect(self, ink_image, ink_background):
"""Apply foreground image with pen effect to background image.
:param ink_image: Image with pen drawings.
:type ink_image: numpy.array (numpy.uint8)
:param image: The background image.
:type image: numpy.array (numpy.uint8)
"""
return cv2.multiply(ink_image, ink_background, scale=1 / 255)
[docs]
def apply_marker_effect(self, ink_image, ink_background):
"""Apply foreground image with marker effect to background image.
:param ink_image: Image with marker drawings.
:type ink_image: numpy.array (numpy.uint8)
:param image: The background image.
:type image: numpy.array (numpy.uint8)
"""
gray_image = cv2.cvtColor(ink_image, cv2.COLOR_BGR2GRAY)
ink_image = gray_image.copy()
# kernel size depends on thickness
kernel_size = random.randint(self.ink_thickness_range[0], self.ink_thickness_range[1])
# minimum size of marker effect
min_size = 2
# kernel for jagged effect
kernel_jagged = np.zeros((min_size + kernel_size, min_size + kernel_size), dtype="uint8")
kernel_jagged[int((min_size + kernel_size) / 2), :] = 1
kernel_jagged[:, int((min_size + kernel_size) / 2)] = 1
# erode image and get jagged edge
random_image = np.random.randint(0, 255, size=ink_image.shape[:2], dtype="uint8")
image_jagged = np.full(ink_image.shape, fill_value=255, dtype="uint8")
indices = np.logical_and(gray_image < 255, random_image < 128)
image_jagged[indices] = 0
image_jagged = cv2.erode(image_jagged, kernel_jagged, iterations=2)
# erode ink image , but with lesser iteration
ink_image = cv2.erode(ink_image, kernel_jagged, iterations=1)
# add white noise in the borders
image_white_noise = np.full(ink_image.shape, fill_value=255, dtype="uint8")
kernel_white_noise = np.ones((max(min_size, kernel_size), max(min_size, kernel_size)), dtype="uint8")
random_image2 = np.random.randint(0, 255, size=ink_image.shape[:2], dtype="uint8")
indices_white_noise = np.logical_and(gray_image < 255, random_image2 < 16)
image_white_noise[indices_white_noise] = 0
image_white_noise = cv2.erode(image_white_noise, kernel_white_noise, iterations=2)
image_jagged[image_white_noise < 255] = 255
# merge jagged and eroded ink image
ink_image[ink_image < 255] = 0
ink_image[ink_image > 0] = 1
ink_image = np.multiply(image_jagged, ink_image)
# add white noise at the center of the ink
# this value control white noise at the center, the higher the value, the lower the white noise at the center
center_white_noise_threshold = random.randint(200, 225)
image_sobel = sobel(gray_image)
image_sobel[random_image2 < center_white_noise_threshold] = 0
ink_image[image_sobel > 0] = 255
# update color
ink_image_gray = ink_image
ink_image = cv2.cvtColor(ink_image, cv2.COLOR_GRAY2BGR)
ink_image[ink_image_gray < 255] = self.ink_color
# Last step, blur image for better effect
ink_image = cv2.GaussianBlur(ink_image, (3, 3), 0)
return cv2.multiply(ink_image, ink_background, scale=1 / 255)
[docs]
def generate_edges(self, image, repeat=0, randomize=0):
"""Generate edges based on image gradients.
:param image: Image with lines ot text drawings.
:type image: numpy.array (numpy.uint8)
:param repeat: Number of recursive edge generation iterations.
:type repeat: int, optional
:param randomize: Number of recursive edge generation iterations.
:type randomize: int, optional
"""
gx, gy = np.gradient(image, edge_order=1)
if randomize:
image_edges = (gx * random.choice([1, -1])) + (gy * random.choice([1, -1]))
image_edges[image_edges < 0] = 0
else:
image_edges = abs(gx) + abs(gy)
image_edges[image_edges > 0] = 1
if repeat:
image_edges = self.generate_edges(image_edges, repeat=repeat - 1)
return image_edges
[docs]
def generate_noise_clusters(self, image, n_clusters=(200, 200), n_samples=(300, 300), std_range=(5, 10)):
"""Generate clusters of noise with make_blobs.
:param image: Image with lines ot text drawings.
:type image: numpy.array (numpy.uint8)
:param n_clusters: Tuple of ints determining number of noise clusters.
:type n_clusters: tuple, optional
:param n_samples: Tuple of ints determining number of noise sample in each cluster.
:type n_samples: tuple, optional
:param std_range: Tuple of ints determining spreadness of noise clusters.
:type std_range: int, optional
"""
# generate random clusters, samples and std
samples = [
random.randint(n_samples[0], n_samples[1]) for _ in range(random.randint(n_clusters[0], n_clusters[1]))
]
std = random.randint(std_range[0], std_range[1])
# generate clusters of blobs
generated_points, point_group = make_blobs(
n_samples=samples,
center_box=(0, max(image.shape)),
cluster_std=std,
n_features=2,
)
# index of invalid points (beyond image borders)
ind_delete = np.logical_or.reduce(
(
generated_points[:, 0] < 0,
generated_points[:, 1] < 0,
generated_points[:, 0] > image.shape[1] - 1,
generated_points[:, 1] > image.shape[0] - 1,
),
)
# delete invalid points
generated_points_x = np.delete(generated_points[:, 0], ind_delete.reshape(ind_delete.shape[0]), axis=0).astype(
"int",
)
generated_points_y = np.delete(generated_points[:, 1], ind_delete.reshape(ind_delete.shape[0]), axis=0).astype(
"int",
)
# add noise into image
image_noise = np.zeros_like(image, dtype="uint8")
image_noise[generated_points_y, generated_points_x] = 1
return image_noise
[docs]
def apply_highlighter_effect(self, ink_image, ink_background):
"""Apply foreground image with highlighter effect to background image.
:param ink_image: Image with highlighter drawings.
:type ink_image: numpy.array (numpy.uint8)
:param ink_background: The background image.
:type ink_background: numpy.array (numpy.uint8)
"""
gray_image = cv2.cvtColor(ink_image, cv2.COLOR_BGR2GRAY)
ink_image = gray_image.copy()
# kernel size depends on thickness
kernel_size = random.randint(self.ink_thickness_range[0], self.ink_thickness_range[1])
# minimum size of highligther effect
min_size = 3
# kernel with diagonal lines to produce highlighter effect
kernel_diagonal = np.zeros((min_size + kernel_size, min_size + kernel_size), dtype="uint8")
for i in range(min_size + kernel_size):
kernel_diagonal[i, i] = 1
if min_size + kernel_size > 2:
mid_location = int((min_size + kernel_size) / 2)
kernel_diagonal[mid_location, mid_location] = 0
# dilate image to make it thicker
ink_image = cv2.erode(ink_image, kernel_diagonal, iterations=2)
# effect at the core center of ink
image_edges_center = self.generate_edges(gray_image, repeat=2, randomize=1)
# effect at the borders
image_edges_border = self.generate_edges(ink_image, repeat=2, randomize=1)
# effect with single or multiple lines
image_edges_lines = self.generate_edges(gray_image, repeat=0, randomize=1)
n_clusters = (200 + kernel_size * 10, 250 + kernel_size * 10)
n_samples = (200 + kernel_size * 5, 250 + kernel_size * 5)
std_range = (3 + np.ceil(kernel_size / 5), 7 + np.ceil(kernel_size / 5))
image_noise_center = self.generate_noise_clusters(gray_image, n_clusters, n_samples, std_range)
image_noise_border = self.generate_noise_clusters(gray_image, n_clusters, n_samples, std_range)
# white line as noise
if self.ink_draw_method == "lines":
indices = np.logical_and(image_edges_lines > 0, (image_noise_border + image_noise_center) > 0)
ink_image[indices] = 255
# effect at center line
indices = np.logical_and(image_edges_center > 0, image_noise_center > 0)
ink_image[indices] = 255
# effect at borders
indices = np.logical_and(image_edges_border > 0, image_noise_border > 0)
ink_image[indices] = 255
# convert back image to BGR and add color
ink_image_gray = ink_image
ink_image = cv2.cvtColor(ink_image, cv2.COLOR_GRAY2BGR)
ink_image[ink_image_gray < 255] = self.ink_color
# Last step, blur image for better effect
ink_image = cv2.GaussianBlur(ink_image, (3, 3), 0)
return cv2.multiply(ink_image, ink_background, scale=1 / 255)
[docs]
def generate_lines(self, ink_background):
"""Generated lines drawing in background image.
:param ink_backgrounde: The background image.
:type ink_background: numpy.array (numpy.uint8)
"""
# ink background is the max size
max_height, max_width = ink_background.shape[:2]
ink_draw_iterations = random.randint(
self.ink_draw_iterations[0],
self.ink_draw_iterations[1],
)
# background across all iterations
combined_background = np.full((max_height, max_width, 3), fill_value=255, dtype="uint8")
for i in range(ink_draw_iterations):
# background of lines image
lines_background = np.full((max_height, max_width, 3), fill_value=255, dtype="uint8")
# each stroke count
ink_lines_stroke_count = random.randint(
self.ink_lines_stroke_count_range[0],
self.ink_lines_stroke_count_range[1],
)
if self.ink_lines_coordinates == "random":
# get size of foreground
size = random.randint(max(self.ink_draw_size_range[0], 30), max(40, self.ink_draw_size_range[1]))
xsize = ysize = min([size, max_height, max_width])
else:
# if coordinates are provided, all lines will be drew at one time
ink_lines_stroke_count = 1
xpoint_min = max_width
ypoint_min = max_height
xpoint_max = 0
ypoint_max = 0
for points in self.ink_lines_coordinates:
xpoints = points[:, 0]
ypoints = points[:, 1]
xpoint_min = min(xpoint_min, min(xpoints))
ypoint_min = min(ypoint_min, min(ypoints))
xpoint_max = max(xpoint_max, max(xpoints))
ypoint_max = max(ypoint_max, max(ypoints))
# add offset, to prevent cut off of thicken drawing at the edges of image
offset = 50
xsize = xpoint_max - xpoint_min + (offset * 2)
ysize = ypoint_max - ypoint_min + (offset * 2)
# reset coordinates so that it starts at min coordinates
ink_lines_coordinates = []
for points in self.ink_lines_coordinates:
points_new = []
xpoints = points[:, 0]
ypoints = points[:, 1]
for xpoint, ypoint in zip(xpoints, ypoints):
points_new.append([xpoint - xpoint_min + offset, ypoint - ypoint_min + offset])
ink_lines_coordinates.append(np.array(points_new))
# fixed ink location if lines coordinates are provided
self.ink_location = (xpoint_min, ypoint_min)
if self.ink_location == "random":
# random paste location
xstart = random.randint(0, max(1, max_width - xsize - 1))
ystart = random.randint(0, max(1, max_height - ysize - 1))
else:
xstart, ystart = self.ink_location
xstart = max(0, xstart - offset)
ystart = max(0, ystart - offset)
if xstart < 0:
xstart = 0
elif xstart + xsize >= max_width:
xsize = max_width - xstart
if ystart < 0:
ystart = 0
elif ystart + ysize >= max_height:
ysize = max_height - ystart
# create each stroke of lines
for i in range(ink_lines_stroke_count):
# generate lines thickness
ink_thickness = random.randint(
self.ink_thickness_range[0],
self.ink_thickness_range[1],
)
# min thickness of highlighter is 2
if self.ink_type == "highlighter":
ink_thickness = max(2, ink_thickness)
# foreground of line image
line_image = np.full((ysize, xsize, 3), fill_value=255, dtype="uint8")
if self.ink_lines_coordinates == "random":
x = np.array(
[
random.randint(5, xsize - 25),
random.randint(5, xsize - 25),
random.randint(5, xsize - 25),
random.randint(5, xsize - 25),
random.randint(5, xsize - 25),
],
)
y = np.array(
[
random.randint(5, ysize - 25),
random.randint(5, ysize - 25),
random.randint(5, ysize - 25),
random.randint(5, ysize - 25),
random.randint(5, ysize - 25),
],
)
start_stop = [
random.randint(5, ysize // 2),
random.randint(ysize // 2, ysize - 5),
]
# Initilaize y axis
lspace = np.linspace(min(start_stop), max(start_stop))
# calculate the coefficients.
z = np.polyfit(x, y, 2)
# calculate x axis
line_fitx = z[0] * lspace**2 + z[1] * lspace + z[2]
verts = np.array(list(zip(line_fitx.astype(int), lspace.astype(int))))
ink_lines_coordinates = [verts]
# get a patch of background
line_background = lines_background[ystart : ystart + ysize, xstart : xstart + xsize]
# draw lines
cv2.polylines(
line_image,
ink_lines_coordinates,
False,
self.ink_color,
thickness=ink_thickness,
)
# apply line image with ink effect to background
line_background = self.apply_ink_effect(line_image, line_background)
# reassign background patch to background
lines_background[ystart : ystart + ysize, xstart : xstart + xsize] = line_background
# reduce transparency for highligther
if self.ink_type == "highlighter":
lines_background = cv2.addWeighted(
lines_background,
0.5,
np.full_like(lines_background, fill_value=255, dtype="uint8"),
0.5,
0,
)
# combine backgrounds in each iteration
combined_background = cv2.multiply(lines_background, combined_background, scale=1 / 255)
# skeletonize image (optional)
if self.ink_skeletonize:
binary_image = cv2.cvtColor(255 - combined_background, cv2.COLOR_BGR2GRAY)
binary_image[binary_image > 0] = 1
max_iter = random.randint(
self.ink_skeletonize_iterations_range[0],
self.ink_skeletonize_iterations_range[1],
)
thin_mask = thin(binary_image, max_iter=max_iter) * 1
for i in range(3):
combined_background[:, :, i][thin_mask == 0] = 255
# brighten image to reach minimum brightness (optional)
if self.ink_min_brightness:
combined_background = self.apply_brightness(combined_background)
# merge image with lines with ink background
image_output = cv2.multiply(combined_background, ink_background, scale=1 / 255)
return image_output
[docs]
def apply_ink_effect(self, foreground_image, background_image):
"""Function to apply various ink effect.
:param foreground_image: Foreground image with lines or text.
:type foreground_image: numpy.array (numpy.uint8)
:param background_image: The background image.
:type background_image: numpy.array (numpy.uint8)
"""
# make sure both images are in a same size
bysize, bxsize = background_image.shape[:2]
if foreground_image.shape[0] != bysize or foreground_image.shape[1] != bxsize:
foreground_image = cv2.resize(foreground_image, (bxsize, bysize), interpolation=cv2.INTER_AREA)
# pencil
if self.ink_type == "pencil":
image_merged = self.apply_pencil_effect(foreground_image, background_image)
# pen
elif self.ink_type == "pen":
image_merged = self.apply_pen_effect(foreground_image, background_image)
# marker
elif self.ink_type == "marker":
image_merged = self.apply_marker_effect(foreground_image, background_image)
# highlighter
else:
image_merged = self.apply_highlighter_effect(foreground_image, background_image)
return image_merged
[docs]
def generate_text(self, ink_background):
"""Generated texts drawing in background image.
:param ink_backgrounde: The background image.
:type ink_background: numpy.array (numpy.uint8)
"""
# ink background is the max size
max_height, max_width = ink_background.shape[:2]
ink_draw_iterations = random.randint(
self.ink_draw_iterations[0],
self.ink_draw_iterations[1],
)
# background across all iterations
combined_background = np.full((max_height, max_width, 3), fill_value=255, dtype="uint8")
for i in range(ink_draw_iterations):
# foreground and background of text image
texts_image = np.full((max_height, max_width, 3), fill_value=255, dtype="uint8")
texts_background = np.full((max_height, max_width, 3), fill_value=255, dtype="uint8")
# convert image to PIL
texts_image_PIL = Image.fromarray(texts_image)
draw = ImageDraw.Draw(texts_image_PIL)
# set font and size
font = ImageFont.truetype(
random.choice(self.ink_text_font),
size=int(random.randint(self.ink_draw_size_range[0], self.ink_draw_size_range[1]) / 4),
)
if self.ink_text == "random":
text = random.choice(["DEMO", "APPROVED", "CHECKED", "ORIGINAL", "COPY", "CONFIDENTIAL"])
else:
text = self.ink_text
# thickness of text
ink_thickness = random.randint(
self.ink_thickness_range[0],
self.ink_thickness_range[1],
)
# draw text
draw.text(
(int(max_width / 2), int(max_height / 2)),
text,
font=font,
stroke_width=ink_thickness,
fill=self.ink_color,
)
# convert it back to numpy array
texts_image = np.array(texts_image_PIL)
# rotate image
texts_image = rotate_image(
texts_image,
random.randint(self.ink_text_rotate_range[0], self.ink_text_rotate_range[1]),
)
# resize to make sure rotated image size is consistent
texts_image = cv2.resize(texts_image, (max_width, max_height), interpolation=cv2.INTER_AREA)
# remove additional blank area
binary_image = binary_threshold(texts_image, threshold_method="threshold_otsu", threshold_arguments={})
coordinates = cv2.findNonZero(255 - binary_image)
x, y, xsize, ysize = cv2.boundingRect(coordinates)
# minimum size
xsize = max(5, xsize)
ysize = max(5, ysize)
texts_image = texts_image[y : y + ysize, x : x + xsize]
if self.ink_location == "random":
# random paste location
xstart = random.randint(0, max(0, max_width - xsize - 1))
ystart = random.randint(0, max(0, max_height - ysize - 1))
else:
xstart, ystart = self.ink_location
if xstart < 0:
xstart = 0
elif xstart + xsize >= max_width:
xstart = max_width - xsize - 1
if ystart < 0:
ystart = 0
elif ystart + ysize >= max_height:
ystart = max_height - ysize - 1
text_background = texts_background[ystart : ystart + ysize, xstart : xstart + xsize]
# apply foreground image to background
text_background = self.apply_ink_effect(texts_image, text_background)
texts_background[ystart : ystart + ysize, xstart : xstart + xsize] = text_background
# reduce transparency for highligther
if self.ink_type == "highlighter":
texts_background = cv2.addWeighted(
texts_background,
0.5,
np.full_like(texts_background, fill_value=255, dtype="uint8"),
0.5,
0,
)
# combine backgrounds in each iteration
combined_background = cv2.multiply(texts_background, combined_background, scale=1 / 255)
# skeletonize image (optional)
if self.ink_skeletonize:
binary_image = cv2.cvtColor(255 - combined_background, cv2.COLOR_BGR2GRAY)
binary_image[binary_image > 0] = 1
max_iter = random.randint(
self.ink_skeletonize_iterations_range[0],
self.ink_skeletonize_iterations_range[1],
)
thin_mask = thin(binary_image, max_iter=max_iter) * 1
for i in range(3):
combined_background[:, :, i][thin_mask == 0] = 255
# brighten image to reach minimum brightness (optional)
if self.ink_min_brightness:
combined_background = self.apply_brightness(combined_background)
# merge image with texts with ink background
image_output = cv2.multiply(combined_background, ink_background, scale=1 / 255)
return image_output
[docs]
def generate_ink(
self,
ink_type=None,
ink_draw_method=None,
ink_draw_iterations=None,
ink_location=None,
ink_background=None,
ink_background_size=None,
ink_background_color=None,
ink_color=None,
ink_min_brightness=None,
ink_min_brightness_value_range=None,
ink_draw_size_range=None,
ink_thickness_range=None,
ink_brightness_change=None,
ink_skeletonize=None,
ink_text=None,
ink_text_font=None,
ink_text_rotate_range=None,
ink_lines_coordinates=None,
ink_lines_curvy=None,
ink_lines_stroke_count_range=None,
):
"""Main function to print ink into the background.
:param ink_type: Types of ink, select from "pencil", "pen", "marker" or "highlighter".
:type ink_type: string, optional
:param ink_draw_method: Content of ink generation, select from "lines" or "text".
:param ink_draw_method: string, optional
:param ink_draw_iterations: Tuple of ints determining the drawing iterations
:param ink_draw_iterations: int, optional
:param ink_location: Tuple of ints determining location of ink drawings.
Or use "random: for random line location.
:type ink_location: tuple or string, optional
:param ink_background: Background of ink generation.
:param ink_background: numpy array, optional
:param ink_background_size: Tuple of ints (height, width) or (height, width, channels)
determining the size of new background for ink generation.
A new background will be created only if ink_background is not provided.
:param ink_background_size: tuple, optional
:param ink_background_color: Tuple of ints (BGR) determining the color of background.
:type ink_background_color: tuple, optional
:param ink_color: Tuple of ints (BGR) determining the color of ink.
:type ink_color: tuple, optional
:param ink_min_brightness: Flag to enable min brightness in the generated ink.
:type ink_min_brightness: int, optional
:param ink_min_brightness_value_range: Pair of ints determining the range for min brightness value in the generated ink.
:type ink_min_brightness_value_range: tuple, optional
:param ink_draw_size_range: Pair of floats determining the range for
the size of the ink drawing.
:type ink_draw_size_range: tuple, optional
:param ink_thickness_range: Pair of floats determining the range for the thickness of the created ink.
:type scribbles_thickness_range: tuple, optional
:param ink_brightness_change: A list of value change for the brightness of the ink.
If more than one value is provided, the final value will be randomly selected.
:type ink_brightness_change: list, optional
:param ink_skeletonize: Flag to enable skeletonization in the generated drawings.
:type ink_skeletonize: int, optional
:param ink_skeletonize_iterations_range: Pair of ints determining the number of iterations in skeletonization process.
:type ink_skeletonize_iterations_range: int, optional
:param ink_text: Text value of ink generation, valid only if ink_draw_method is "text".
:param ink_text: string, optional
:param ink_text_font: List contain paths to font types. Valid only if ink content is "text".
:type ink_text_font: list, optional
:param ink_text_rotate_range: Tuple of ints to determine rotation angle of "text" based drawings.
:type ink_text_rotate_range: tuple, optional
:param ink_lines_coordinates: A list contain coordinates of line.
:type ink_lines_coordinates: list, optional
:param ink_lines_stroke_count_range: Pair of floats determining the range for
the number of created lines.
:type ink_lines_stroke_count_range: tuple, optional
"""
# If input is not None, replace self parameters
if ink_type is not None:
self.ink_type = ink_type
if ink_draw_method is not None:
self.ink_draw_method = ink_draw_method
if ink_draw_iterations is not None:
self.ink_draw_iterations = ink_draw_iterations
if ink_location is not None:
self.ink_location = ink_location
if ink_background is not None:
self.ink_background = ink_background
if ink_background_size is not None:
self.ink_background_size = ink_background_size
if ink_background_color is not None:
self.ink_background_color = ink_background_color
if ink_color is not None:
self.ink_color = ink_color
if ink_min_brightness is not None:
self.ink_min_brightness = ink_min_brightness
if ink_min_brightness_value_range is not None:
self.ink_min_brightness_value_range = ink_min_brightness_value_range
if ink_draw_size_range is not None:
self.ink_draw_size_range = ink_draw_size_range
if ink_thickness_range is not None:
self.ink_thickness_range = ink_thickness_range
if ink_brightness_change is not None:
self.ink_brightness_change = ink_brightness_change
if ink_skeletonize is not None:
self.ink_skeletonize = ink_skeletonize
if ink_text is not None:
self.ink_text = ink_text
if ink_text_font is not None:
self.ink_text_font = ink_text_font
if ink_text_rotate_range is not None:
self.ink_text_rotate_range = ink_text_rotate_range
if ink_lines_coordinates is not None:
self.ink_lines_coordinates = ink_lines_coordinates
if ink_lines_curvy is not None:
self.ink_lines_curvy = ink_lines_curvy
if ink_lines_stroke_count_range is not None:
self.ink_lines_stroke_count_range = ink_lines_stroke_count_range
# retrieve or create background
if isinstance(self.ink_background, np.ndarray):
ink_background = self.ink_background.copy()
else:
ink_background = np.full(self.ink_background_size, fill_value=self.ink_background_color, dtype="uint8")
# check if provided background is grayscale
if len(ink_background.shape) < 3:
background_is_grayscale = 1
else:
background_is_grayscale = 0
# change to BGR for further processing
if background_is_grayscale:
ink_background = cv2.cvtColor(ink_background, cv2.COLOR_GRAY2BGR)
# generate ink effect
if self.ink_draw_method == "lines":
image_output = self.generate_lines(ink_background)
else:
image_output = self.generate_text(ink_background)
# return image in a same format
if background_is_grayscale:
image_output = cv2.cvtColor(image_output, cv2.COLOR_BGR2GRAY)
return image_output