import math
import random
import cv2
import numpy as np
from numba import config
from numba import jit
from augraphy.augmentations.lib import make_white_transparent
[docs]
class OverlayBuilder:
"""Takes an input image, a number of times to duplicate that image, an image
on which to overlay the result of this, and a page position, then produces
an overlayable image with the input image copied that many times across
the edge of the page or at random location or at the center of image.
:param overlay_types: Types of overlay method.
:type overlay_types: string
:param foreground: The image to overlay on the background document.
:type foreground: numpy array
:param background: The document.
:type background: numpy array
:param ntimes: Number copies of the foreground image to draw.
:type ntimes: int, optional
:param nscales: Scales of foreground image size.
:type nscales: tuple, optional
:param edge: Which edge of the page the foreground copies should be
placed on. Selections included left, right, top, bottom, enter, random.
:type edge: string, optional
:param edge_offset: How far from the edge of the page to draw the copies.
:type edge_offset: int, optional
:param alpha: Alpha value for overlay methods that uses alpha in the blending.
:type alpha: float, optional
:param ink_color: Ink color value for ink_to_paper overlay type.
:type ink_color: int, optional
"""
def __init__(
self,
overlay_types,
foreground,
background,
ntimes=1,
nscales=(1, 1),
edge="center",
edge_offset=0,
alpha=0.3,
ink_color=-1,
):
self.overlay_types = overlay_types
self.foreground = foreground
self.background = background
self.ntimes = ntimes
self.nscales = nscales
self.edge = edge
self.edge_offset = max(0, edge_offset) # prevent negative
self.alpha = alpha
self.ink_color = ink_color
# set valid edge type
if edge not in ["center", "random", "left", "right", "top", "bottom"]:
self.edge = "center"
# most of the blending methods are adapted here: https://github.com/flrs/blend_modes
# set valid overlay types
if overlay_types not in [
"ink_to_paper",
"min",
"max",
"mix",
"normal",
"lighten",
"darken",
"addition",
"subtract",
"difference",
"screen",
"dodge",
"multiply",
"divide",
"hard_light",
"grain_extract",
"grain_merge",
"overlay",
"FFT",
]:
self.overlay_types = "mix"
[docs]
def compute_offsets(self, foreground):
"""Determine where to place the foreground image copies
:param foreground: The image to overlay on the background document.
:type foreground: numpy array
"""
xdim = self.background.shape[1]
ydim = self.background.shape[0]
img_width = foreground.shape[1]
img_height = foreground.shape[0]
remaining_width = xdim - (self.ntimes * img_width)
remaining_height = ydim - (self.ntimes * img_height)
# max to prevent negative offset
offset_width = max(0, math.floor(remaining_width / (self.ntimes + 1)))
offset_height = max(0, math.floor(remaining_height / (self.ntimes + 1)))
return offset_width, offset_height
[docs]
def check_size(self, img_foreground, img_background, center=None):
"""Check the fitting size of foreground to background
:param img_foreground: The image to overlay on the background document.
:type img_foreground: numpy array
:param img_background: The background document.
:type img_background: numpy array
:param center: Center coordinate (x,y) of the overlaying process.
:type center: tuple
"""
# background size
ysize_background, xsize_background = img_background.shape[:2]
# get center x and y
center_x, center_y = center
# foreground size
ysize_foreground, xsize_foreground = img_foreground.shape[:2]
# center point of foreground
ysize_half_foreground, xsize_half_foreground = int(ysize_foreground / 2), int(
xsize_foreground / 2,
)
# if foreground size is > background size, crop only the fitting size
if center_y - ysize_half_foreground < 0 and center_y + ysize_half_foreground > ysize_background:
img_foreground = img_foreground[
-(center_y - ysize_half_foreground) : ysize_foreground
- (center_y + ysize_half_foreground - ysize_background),
:,
]
# new size after cropping
# foreground size
ysize_foreground, xsize_foreground = img_foreground.shape[:2]
# center point of foreground
ysize_half_foreground, xsize_half_foreground = (
int(
ysize_foreground / 2,
),
int(xsize_foreground / 2),
)
if center_x - xsize_half_foreground < 0 and center_x + xsize_half_foreground > xsize_background:
img_foreground = img_foreground[
:,
-(center_x - xsize_half_foreground) : xsize_foreground
- (center_x + xsize_half_foreground - xsize_background),
]
# new size after cropping
# foreground size
ysize_foreground, xsize_foreground = img_foreground.shape[:2]
# center point of foreground
ysize_half_foreground, xsize_half_foreground = (
int(
ysize_foreground / 2,
),
int(xsize_foreground / 2),
)
# to prevent having no overlap between foreground and background image
# check width max size
if center_x - xsize_half_foreground >= xsize_background:
# at least 10 pixel overlapping area
center_x = xsize_background + xsize_half_foreground - 10
# check width min size
elif center_x + xsize_half_foreground < 0:
# at least 10 pixel overlapping area
center_x = 10 - xsize_half_foreground
# check height max size
if center_y - ysize_half_foreground >= ysize_background:
# at least 10 pixel overlapping area
center_y = ysize_background + ysize_half_foreground - 10
# check height min size
elif center_y + ysize_half_foreground < 0:
# at least 10 pixel overlapping area
center_y = 10 - ysize_half_foreground
# if foreground x exceed background width
if center_x + xsize_half_foreground > xsize_background:
# get new patch image to not exceed background width
img_foreground = img_foreground[:, : -(center_x + xsize_half_foreground - xsize_background)]
# get new foreground size
ysize_foreground, xsize_foreground = img_foreground.shape[:2]
# half new foreground size
ysize_half_foreground, xsize_half_foreground = (
int(
ysize_foreground / 2,
),
int(xsize_foreground / 2),
)
# update new center
center = [xsize_background - xsize_half_foreground, center[1]]
# if foreground x < 0
if center_x - xsize_half_foreground < 0:
# get new patch image to not exceed background width
img_foreground = img_foreground[:, abs(center_x - xsize_half_foreground) :]
# get new foreground size
ysize_foreground, xsize_foreground = img_foreground.shape[:2]
# half new foreground size
ysize_half_foreground, xsize_half_foreground = (
int(
ysize_foreground / 2,
),
int(xsize_foreground / 2),
)
# update new center
center = [xsize_half_foreground, center[1]]
# if foreground y exceed background height
if center_y + ysize_half_foreground > ysize_background:
# get new patch image to not exceed background width
img_foreground = img_foreground[: -(center_y + ysize_half_foreground - ysize_background), :]
# get new foreground size
ysize_foreground, xsize_foreground = img_foreground.shape[:2]
# half new foreground size
ysize_half_foreground, xsize_half_foreground = (
int(
ysize_foreground / 2,
),
int(xsize_foreground / 2),
)
# update new center
center = [center[0], ysize_background - ysize_half_foreground]
# if foreground y < 0
if center_y - ysize_half_foreground < 0:
# get new patch image to not exceed background width
img_foreground = img_foreground[abs(center_y - ysize_half_foreground) :, :]
# get new foreground size
ysize_foreground, xsize_foreground = img_foreground.shape[:2]
# half new foreground size
ysize_half_foreground, xsize_half_foreground = (
int(
ysize_foreground / 2,
),
int(xsize_foreground / 2),
)
# update new center
center = [center[0], ysize_half_foreground]
return img_foreground, center
[docs]
@staticmethod
@jit(nopython=True, cache=True)
def compose_alpha(img_alpha_background, img_alpha_foreground, alpha):
"""Calculate alpha composition ratio between two images.
:param img_alpha_background: The background image alpha layer.
:type img_alpha_background: numpy array
:param img_alpha_foreground: The foreground image alpha layer.
:type img_alpha_foreground: numpy array
:param alpha: Alpha value for the blending process.
:type alpha: float
"""
comp_alpha = np.minimum(img_alpha_background, img_alpha_foreground) * alpha
new_alpha = img_alpha_background + (1.0 - img_alpha_foreground) * comp_alpha
ratio = comp_alpha / new_alpha
return ratio
[docs]
def fft_blend_single_channel(self, image1, image2, random_mask):
"""Blend images using random mask and fft transform.
:param image1: The background image.
:type image1: numpy array
:param image2: The foreground image.
:type image2: numpy array
:param random_mask: Mask with random value for FFT blending method.
:type random_mask: numpy array
"""
# apply Fast Fourier Transform (FFT) to the images
image1_fft = np.fft.fft2(image1)
image2_fft = np.fft.fft2(image2)
# merge fft based on random_mask
combined_fft = (image1_fft * random_mask) + (image2_fft * random_mask)
# apply Inverse Fast Fourier Transform (IFFT) to the combined spectrum
image_merged = np.fft.ifft2(combined_fft).real
# scale between 0 - 255
image_merged = (image_merged - np.min(image_merged)) * (255.0 / (np.max(image_merged) - np.min(image_merged)))
# convert the image to uint8 data type
image_merged = image_merged.astype(np.uint8)
return image_merged
[docs]
def fft_blend(
self,
overlay_background,
image1,
image2,
xstart,
xend,
ystart,
yend,
):
"""Blend images using saturation and value channel of images in hsv channel.
:param overlay_background: Background image.
:type overlay_background: numpy array
:param image1: A patch of background image.
:type image1: numpy array
:param image2: Foreground_image.
:type image2: numpy array
:param xstart: x start point of the image patch.
:type xstart: int
:param xend: x end point of the image patch.
:type xend: int
:param ystart: y start point of the image patch.
:type ystart: int
:param yend: y end point of the image patch.
:type yend: int
"""
if len(image1.shape) < 3:
is_gray = 1
image1 = cv2.cvtColor(image1, cv2.COLOR_GRAY2BGR)
else:
is_gray = 0
if len(image2.shape) < 3:
image2 = cv2.cvtColor(image2, cv2.COLOR_GRAY2BGR)
ysize, xsize = image1.shape[:2]
random_mask = np.random.randint(0, 2, size=(ysize, xsize)).astype("float")
# convert into hsv channel
image_hsv1 = cv2.cvtColor(image1, cv2.COLOR_BGR2HSV)
image_hsv2 = cv2.cvtColor(image2, cv2.COLOR_BGR2HSV)
# merge saturation and value channel
image_hsv1[:, :, 1] = self.fft_blend_single_channel(image_hsv1[:, :, 1], image_hsv2[:, :, 1], random_mask)
image_hsv1[:, :, 2] = self.fft_blend_single_channel(image_hsv1[:, :, 2], image_hsv2[:, :, 2], random_mask)
# convert back to bgr
image_blended = cv2.cvtColor(image_hsv1, cv2.COLOR_HSV2BGR)
if is_gray:
image_blended = cv2.cvtColor(image_blended, cv2.COLOR_BGR2GRAY)
overlay_background[ystart:yend, xstart:xend] = image_blended
[docs]
def ink_to_paper_blend(
self,
overlay_background,
base,
new_foreground,
xstart,
xend,
ystart,
yend,
):
"""Apply blending using default ink to paper printing method.
:param overlay_background: Background image.
:type overlay_background: numpy array
:param base: A patch of background image.
:type base: numpy array
:param new_foreground: Foreground_image.
:type new_foreground: numpy array
:param xstart: x start point of the image patch.
:type xstart: int
:param xend: x end point of the image patch.
:type xend: int
:param ystart: y start point of the image patch.
:type ystart: int
:param yend: y end point of the image patch.
:type yend: int
"""
foreground = make_white_transparent(new_foreground, self.ink_color)
# Split out the transparency mask from the colour info
overlay_img = foreground[:, :, :3] # Grab the BRG planes
overlay_mask = foreground[:, :, 3:] # And the alpha plane
# Turn the single channel alpha masks into three channel, so we can use them as weights
if len(foreground.shape) > 2:
overlay_mask = cv2.cvtColor(overlay_mask, cv2.COLOR_GRAY2BGR)
# Again calculate the inverse mask
background_mask = 255 - overlay_mask
# Convert background to 3 channels if they are in single channel
if len(base.shape) < 3:
base = cv2.cvtColor(base, cv2.COLOR_GRAY2BGR)
# Create a masked out face image, and masked out overlay
# We convert the images to floating point in range 0.0 - 1.0
background_part = (base * (1.0 / 255.0)) * (background_mask * (1 / 255.0))
overlay_part = (overlay_img * (1.0 / 255.0)) * (overlay_mask * (1.0 / 255.0))
# And finally just add them together, and rescale it back to an 8bit integer image
image_printed = np.uint8(
cv2.addWeighted(background_part, 255.0, overlay_part, 255.0, 0.0),
)
overlay_background[ystart:yend, xstart:xend] = image_printed
return overlay_background
[docs]
def mix_blend(
self,
overlay_background,
new_foreground,
center,
fg_height,
fg_width,
):
"""Apply blending using cv2.seamlessClone.
:param overlay_background: The background image.
:type overlay_background: numpy array
:param new_foreground: The foreground iamge of overlaying process.
:type new_foreground: numpy array
:param center: Center coordinate (x,y) of the overlaying process.
:type center: tuple
:param fg_height: Height of foreground image.
:type fg_height: int
:param fg_width: Width of foreground image.
:type fg_width: int
"""
img_mask = np.ones((fg_height, fg_width), dtype="uint8") * 255
# convert gray to bgr (seamlessClone need bgr format)
if len(new_foreground.shape) < 3:
new_foreground = cv2.cvtColor(new_foreground, cv2.COLOR_GRAY2BGR)
if len(overlay_background.shape) < 3:
overlay_background = cv2.cvtColor(overlay_background, cv2.COLOR_GRAY2BGR)
overlay_background = cv2.seamlessClone(
new_foreground,
overlay_background,
img_mask,
center,
cv2.MIXED_CLONE,
)
# convert from bgr back to gray
if len(new_foreground.shape) < 3:
overlay_background = cv2.cvtColor(new_foreground, cv2.COLOR_BGR2GRAY)
return overlay_background
[docs]
def min_max_blend(
self,
base,
base_gray,
new_foreground,
new_foreground_gray,
fg_height,
fg_width,
):
"""Apply blending using min or max gray value.
:param base: Background image.
:type base: numpy array
:param base_gray: Background image in grayscale.
:type base_gray: numpy array
:param new_foreground: Foreground_image.
:type new_foreground: numpy array
:param new_foreground_gray: Foreground image in grayscale.
:type new_foreground_gray: numpy array
:param fg_height: Height of foreground image.
:type fg_height: int
:param fg_width: Width of foreground image.
:type fg_width: int
"""
if self.overlay_types == "min":
indices = new_foreground_gray < base_gray
else:
indices = new_foreground_gray > base_gray
# for colour image
if len(base.shape) > 2:
for i in range(base.shape[2]):
base[:, :, i][indices] = new_foreground[:, :, i][indices]
# for grayscale
else:
base[indices] = new_foreground_gray[indices]
[docs]
def normal_blend(
self,
overlay_background,
base,
new_foreground,
xstart,
xend,
ystart,
yend,
alpha,
):
"""Apply blending using input alpha value (normal method).
:param overlay_background: Background image.
:type overlay_background: numpy array
:param base: A patch of background image.
:type base: numpy array
:param new_foreground: Foreground_image.
:type new_foreground: numpy array
:param xstart: x start point of the image patch.
:type xstart: int
:param xend: x end point of the image patch.
:type xend: int
:param ystart: y start point of the image patch.
:type ystart: int
:param yend: y end point of the image patch.
:type yend: int
:param alpha: Alpha value of the foreground.
:type alpha: float
"""
# convert to float (0-1)
base_norm = base / 255.0
foreground_norm = new_foreground / 255.0
# get alpha layer from base if there is any
if len(base_norm.shape) > 3:
base_alpha = (base_norm[:, :, 3] * 255).astype("uint8")
base_alpha = cv2.cvtColor(base_alpha, cv2.COLOR_GRAY2BGR) / 255
else:
base_alpha = 1
# blend by alpha value
img_blended = (foreground_norm * self.alpha) + (base_norm * base_alpha * (1 - alpha))
# normalized by alpha value
img_blended_norm = img_blended / (self.alpha + (base_alpha * (1 - alpha)))
# convert blended image back to uint8
img_blended_norm = (img_blended_norm * 255.0).astype("uint8")
# add patch of blended image back to background
overlay_background[ystart:yend, xstart:xend] = img_blended_norm
[docs]
def various_blend(
self,
overlay_background,
base,
new_foreground,
xstart,
xend,
ystart,
yend,
):
"""Apply blending using input alpha value (multiple methods).
:param overlay_background: Background image.
:type overlay_background: numpy array
:param base: A patch of background image.
:type base: numpy array
:param new_foreground: Foreground_image.
:type new_foreground: numpy array
:param xstart: x start point of the image patch.
:type xstart: int
:param xend: x end point of the image patch.
:type xend: int
:param ystart: y start point of the image patch.
:type ystart: int
:param yend: y end point of the image patch.
:type yend: int
"""
# convert to float (0-1)
base_norm = base / 255.0
foreground_norm = new_foreground / 255.0
check_alpha_ratio = 0
# get alpha layer (if any)
if len(base_norm.shape) > 3 and len(foreground_norm.shape) > 3:
img_base_alpha = base_norm[:, :, 3]
img_foreground_alpha = foreground_norm[:, :, 3]
check_alpha_ratio = 1
# compose alpha ratio from background and foreground alpha value
ratio = self.compose_alpha(img_base_alpha, img_foreground_alpha, self.alpha)
# remove infinity value due to zero division
ratio[ratio == np.inf] = 0
# compute alpha value
if self.overlay_types == "lighten":
comp_value = np.maximum(base_norm[:, :, :3], foreground_norm[:, :, :3])
elif self.overlay_types == "darken":
comp_value = np.minimum(base_norm[:, :, :3], foreground_norm[:, :, :3])
elif self.overlay_types == "addition":
comp_value = base_norm[:, :, :3] + foreground_norm[:, :, :3]
elif self.overlay_types == "subtract":
comp_value = base_norm[:, :, :3] - foreground_norm[:, :, :3]
elif self.overlay_types == "difference":
comp_value = abs(base_norm[:, :, :3] - foreground_norm[:, :, :3])
elif self.overlay_types == "screen":
comp_value = 1.0 - (1.0 - base_norm[:, :, :3]) * (1.0 - foreground_norm[:, :, :3])
elif self.overlay_types == "dodge":
# prevent zero division
divisor = 1.0 - foreground_norm[:, :, :3]
divisor[divisor == 0.0] = 1.0
comp_value = np.minimum(
base_norm[:, :, :3] / divisor,
1.0,
)
elif self.overlay_types == "multiply":
comp_value = np.clip(
base_norm[:, :, :3] * foreground_norm[:, :, :3],
0.0,
1.0,
)
elif self.overlay_types == "divide":
comp_value = np.minimum(
(256.0 / 255.0 * base_norm[:, :, :3]) / (1.0 / 255.0 + foreground_norm[:, :, :3]),
1.0,
)
elif self.overlay_types == "hard_light":
base_greater = np.greater(base_norm[:, :, :3], 0.5)
foreground_greater = np.greater(foreground_norm[:, :, :3], 0.5)
min_element = np.minimum(
base_norm[:, :, :3] * (foreground_norm[:, :, :3] * 2.0),
1.0,
)
inverse_min_element = np.minimum(
1.0 - ((1.0 - base_norm[:, :, :3]) * (1.0 - (foreground_norm[:, :, :3] - 0.5) * 2.0)),
1.0,
)
comp_value = (base_greater * inverse_min_element) + (np.logical_not(foreground_greater) * min_element)
elif self.overlay_types == "grain_extract":
comp_value = np.clip(
base_norm[:, :, :3] - foreground_norm[:, :, :3] + 0.5,
0.0,
1.0,
)
elif self.overlay_types == "grain_merge":
comp_value = np.clip(
base_norm[:, :, :3] + foreground_norm[:, :, :3] - 0.5,
0.0,
1.0,
)
elif self.overlay_types == "overlay":
base_less = np.less(base_norm[:, :, :3], 0.5)
base_greater_equal = np.greater_equal(base_norm[:, :, :3], 0.5)
base_foreground_product = 2 * base_norm[:, :, :3] * foreground_norm[:, :, :3]
inverse_base_foreground_product = 1 - (2 * (1 - base_norm[:, :, :3]) * (1 - foreground_norm[:, :, :3]))
comp_value = (base_less * base_foreground_product) + (base_greater_equal * inverse_base_foreground_product)
# apply alpha ratio only if both images have alpha layer
if check_alpha_ratio:
# get reshaped ratio
ratio_rs = np.reshape(
np.repeat(ratio, 3),
(base_norm.shape[0], base_norm.shape[1], 3),
)
# blend image
if self.overlay_types == "addition" or self.overlay_types == "subtract":
# clip value for addition or subtract
img_blended = np.clip(
(comp_value * ratio_rs) + (base_norm * (1.0 - ratio_rs)),
0.0,
1.0,
)
else:
img_blended = self.apply_ratio(comp_value, base_norm, ratio_rs)
else:
img_blended = comp_value
# get blended image in uint8
img_blended = (img_blended * 255).astype("uint8")
# add patch of blended image back to background
overlay_background[ystart:yend, xstart:xend] = img_blended
[docs]
@staticmethod
@jit(nopython=True, cache=True)
def apply_ratio(comp_value, base_norm, ratio_rs):
"""Function to apply alpha ratio to both foreground and background image
:param comp_value: The resulting image from blending process.
:type comP_value: numpy array
:param base_norm: The background.
:type base_norm: numpy array
:param ratio_rs: Alpha ratio for each pixel.
:type ratio_rs: numpy array
"""
return (comp_value * ratio_rs) + (base_norm * (1.0 - ratio_rs))
[docs]
def apply_overlay(
self,
overlay_background,
offset_width,
offset_height,
ystart,
yend,
xstart,
xend,
):
"""Applies overlay from foreground to background.
:param overlay_background: Background image.
:type overlay_background: numpy array
:param offset_width: Offset width value to the overlay process.
:type offset_width: int
:param offset_height: Offset height value to the overlay process.
:type offset_height: int
:param ystart: y start point of the overlaying process.
:type ystart: int
:param yend: y end point of the overlaying process.
:type yend: int
:param xstart: x start point of the overlaying process.
:type xstart: int
:param xend: x end point of the overlaying process.
:type xend: int
"""
# get bgr and gray of background
if len(overlay_background.shape) > 2:
overlay_background_gray = cv2.cvtColor(
overlay_background,
cv2.COLOR_BGR2GRAY,
)
else:
overlay_background_gray = overlay_background
overlay_background = cv2.cvtColor(overlay_background, cv2.COLOR_GRAY2BGR)
if isinstance(self.foreground, list):
for i, current_foreground in enumerate(self.foreground):
# get bgr and gray of foreground
if len(current_foreground.shape) < 3:
self.foreground[i] = cv2.cvtColor(
current_foreground,
cv2.COLOR_GRAY2BGR,
)
fg_height, fg_width = self.foreground[0].shape[:2]
else:
# get bgr and gray of foreground
if len(self.foreground.shape) < 3:
self.foreground = cv2.cvtColor(self.foreground, cv2.COLOR_GRAY2BGR)
fg_height, fg_width = self.foreground.shape[:2]
# get size
bg_height, bg_width = overlay_background.shape[:2]
for i in range(self.ntimes):
if isinstance(self.foreground, list):
foreground = random.choice(self.foreground)
else:
foreground = self.foreground
if self.edge == "random":
ystart = random.randint(0, bg_height - 10)
yend = ystart + fg_height
xstart = random.randint(0, bg_width - 10)
xend = xstart + fg_width
# prevent negative value
ystart = max(0, ystart)
xstart = max(0, xstart)
# prevent 0 size
if yend - ystart == 0:
yend += 1
if xend - xstart == 0:
xend += 1
# crop a section of background
base = overlay_background[ystart:yend, xstart:xend]
base_gray = overlay_background_gray[ystart:yend, xstart:xend]
base_y, base_x = base.shape[:2]
# center of overlay
if bg_width > fg_width:
center_x = xstart + int(fg_width / 2)
else:
center_x = xstart + int(bg_width / 2)
if bg_height > fg_height:
center_y = ystart + int(fg_height / 2)
else:
center_y = ystart + int(bg_height / 2)
center = (center_x, center_y)
# check for size mismatch issue
new_foreground, center = self.check_size(
foreground,
overlay_background,
center,
)
# new foreground height and width
fg_height, fg_width = new_foreground.shape[:2]
# check if new foreground size > width or height
half_width = int((xend - xstart) / 2)
half_height = int((yend - ystart) / 2)
foreground_half_width = int(fg_width / 2)
foreground_half_height = int(fg_height / 2)
if half_width != 0 and foreground_half_width > half_width:
half_difference = foreground_half_width - half_width
# remove right part
if self.edge == "left":
new_foreground = new_foreground[:, : -half_difference * 2]
# remove left part
elif self.edge == "right":
new_foreground = new_foreground[:, half_difference * 2 :]
# shift evenly
else:
new_foreground = new_foreground[:, half_difference:-half_difference]
if half_height != 0 and foreground_half_height > half_height:
half_difference = foreground_half_height - half_height
# remove top part
if self.edge == "bottom":
new_foreground = new_foreground[half_difference * 2 :, :]
# remove bottom part
elif self.edge == "top":
new_foreground = new_foreground[: -half_difference * 2, :]
# shift evenly
else:
new_foreground = new_foreground[half_difference:-half_difference, :]
# resize new_foreground to cropped background size
if self.overlay_types != "mix":
new_foreground = cv2.resize(
new_foreground,
(base_x, base_y),
interpolation=cv2.INTER_AREA,
)
# get new size of foreground again
fg_height, fg_width = new_foreground.shape[:2]
# convert foreground to gray again
if len(new_foreground.shape) > 2:
new_foreground_gray = cv2.cvtColor(new_foreground, cv2.COLOR_BGR2GRAY)
else:
new_foreground_gray = new_foreground
# ink to paper overlay type
if self.overlay_types == "ink_to_paper":
self.ink_to_paper_blend(
overlay_background,
base,
new_foreground,
xstart,
xend,
ystart,
yend,
)
# min or max overlay types
elif self.overlay_types == "min" or self.overlay_types == "max":
self.min_max_blend(
base,
base_gray,
new_foreground,
new_foreground_gray,
fg_height,
fg_width,
)
# mix overlay type
elif self.overlay_types == "mix":
overlay_background = self.mix_blend(
overlay_background,
new_foreground,
center,
fg_height,
fg_width,
)
# normal overlay type using alpha value
elif self.overlay_types == "normal":
self.normal_blend(
overlay_background,
base,
new_foreground,
xstart,
xend,
ystart,
yend,
self.alpha,
)
elif self.overlay_types == "FFT":
self.fft_blend(
overlay_background,
base,
new_foreground,
xstart,
xend,
ystart,
yend,
)
# overlay types:
# lighten, darken, addition, subtract, difference, screen, dodge
# multiply, divide, hard_light, grain_extract, grain_merge, overlay
else:
self.various_blend(
overlay_background,
base,
new_foreground,
xstart,
xend,
ystart,
yend,
)
# get original height and width from foreground
fg_height, fg_width = foreground.shape[:2]
if self.edge == "left" or self.edge == "right":
# for next loop ystart and yend
ystart += fg_height + offset_height
yend = ystart + fg_height
if self.overlay_types != "mix":
# break when next ystart is > image y size
if ystart >= bg_height - fg_height:
break
elif self.edge == "top" or self.edge == "bottom":
# for next loop xstart and xend
xstart += fg_width + offset_width
xend = xstart + fg_width
if self.overlay_types != "mix":
# break when next xstart is > image x size
if xstart >= bg_width - fg_width:
break
return overlay_background
[docs]
def build_overlay(self):
"""Construct the overlay image containing foreground copies"""
overlay_background = self.background
random_height_scale = np.random.uniform(self.nscales[0], self.nscales[1])
random_width_scale = np.random.uniform(self.nscales[0], self.nscales[1])
if isinstance(self.foreground, list):
new_fg_height = int((self.foreground[0].shape[0] * random_height_scale))
new_fg_width = int((self.foreground[0].shape[1] * random_width_scale))
for i, current_foreground in enumerate(self.foreground):
self.foreground[i] = cv2.resize(
current_foreground,
(int(new_fg_width), int(new_fg_height)),
interpolation=cv2.INTER_AREA,
)
foreground = self.foreground[0]
else:
foreground = self.foreground
new_fg_height = int((foreground.shape[0] * random_height_scale))
new_fg_width = int((foreground.shape[1] * random_width_scale))
# foreground size (height & width)
fg_height, fg_width = foreground.shape[:2]
# background size (height & width)
bg_height, bg_width = self.background.shape[:2]
# compute offsets between foreground and background
offset_width, offset_height = self.compute_offsets(foreground)
# get overlay location for each types of edge
if self.edge == "left":
ystart = offset_height
yend = ystart + fg_height
xstart = self.edge_offset
xend = self.edge_offset + fg_width
elif self.edge == "right":
ystart = offset_height
yend = ystart + fg_height
xstart = bg_width - self.edge_offset - fg_width
xend = bg_width - self.edge_offset
elif self.edge == "top":
ystart = self.edge_offset
yend = self.edge_offset + fg_height
xstart = offset_width
xend = offset_width + fg_width
elif self.edge == "bottom":
ystart = bg_height - self.edge_offset - fg_height
yend = bg_height - self.edge_offset
xstart = offset_width
xend = offset_width + fg_width
elif self.edge == "random":
ystart = random.randint(0, bg_height - 10)
yend = ystart + fg_height
xstart = random.randint(0, bg_width - 10)
xend = xstart + fg_width
elif self.edge == "center":
if bg_height > fg_height:
ystart = int(bg_height / 2) - int(fg_height / 2)
yend = ystart + fg_height
else:
ystart = 0
yend = bg_height
if bg_width > fg_width:
xstart = int(bg_width / 2) - int(fg_width / 2)
xend = xstart + fg_width
else:
xstart = 0
xend = bg_width
# apply overlay
overlay_background = self.apply_overlay(
overlay_background,
offset_width,
offset_height,
ystart,
yend,
xstart,
xend,
)
# convert image back to gray if background image is in grayscale
if len(self.background.shape) < 3 and len(overlay_background.shape) > 2:
overlay_background = cv2.cvtColor(overlay_background, cv2.COLOR_BGR2GRAY)
return overlay_background