895 lines
39 KiB
Python
895 lines
39 KiB
Python
from decimal import ROUND_CEILING
|
|
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageDraw, ImageChops, ImageOps, ImageMath
|
|
import requests
|
|
import base64
|
|
import numpy as np
|
|
import math
|
|
from io import BytesIO
|
|
from modules.processing import apply_overlay, slerp
|
|
from timeit import default_timer as timer
|
|
|
|
|
|
def shrink_and_paste_on_blank(current_image, mask_width, mask_height):
|
|
"""
|
|
Decreases size of current_image by mask_width pixels from each side,
|
|
then adds a mask_width width transparent frame,
|
|
so that the image the function returns is the same size as the input.
|
|
:param current_image: input image to transform
|
|
:param mask_width: width in pixels to shrink from each side
|
|
:param mask_height: height in pixels to shrink from each side
|
|
"""
|
|
|
|
# calculate new dimensions
|
|
width, height = current_image.size
|
|
new_width = width - 2 * mask_width
|
|
new_height = height - 2 * mask_height
|
|
|
|
# resize and paste onto blank image
|
|
prev_image = current_image.resize((new_width, new_height))
|
|
blank_image = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
|
blank_image.paste(prev_image, (mask_width, mask_height))
|
|
|
|
return blank_image
|
|
|
|
|
|
def open_image(image_path):
|
|
"""
|
|
Opens an image from a file path or URL, or decodes a DataURL string into an image.
|
|
|
|
Parameters:
|
|
image_path (str): The file path, URL, or DataURL string of the image to open.
|
|
|
|
Returns:
|
|
Image: A PIL Image object of the opened image.
|
|
"""
|
|
if image_path.startswith('http'):
|
|
# If the image path is a URL, download the image using requests
|
|
response = requests.get(image_path)
|
|
img = Image.open(BytesIO(response.content))
|
|
elif image_path.startswith('data'):
|
|
# If the image path is a DataURL, decode the base64 string
|
|
encoded_data = image_path.split(',')[1]
|
|
decoded_data = base64.b64decode(encoded_data)
|
|
img = Image.open(BytesIO(decoded_data))
|
|
else:
|
|
# Assume that the image path is a file path
|
|
img = Image.open(image_path)
|
|
return img
|
|
|
|
def apply_alpha_mask(image, mask_image, invert = False):
|
|
"""
|
|
Applies a mask image as the alpha channel of the input image.
|
|
|
|
Parameters:
|
|
image (Image): A PIL Image object of the image to apply the mask to.
|
|
mask_image (Image): A PIL Image object of the alpha mask to apply.
|
|
|
|
Returns:
|
|
Image: A PIL Image object of the input image with the applied alpha mask.
|
|
"""
|
|
# Resize the mask to match the current image size
|
|
mask_image = resize_and_crop_image(mask_image, image.width, image.height).convert('L') # convert to grayscale
|
|
if invert:
|
|
mask_image = ImageOps.invert(mask_image)
|
|
# Apply the mask as the alpha layer of the current image
|
|
result_image = image.copy()
|
|
result_image.putalpha(mask_image)
|
|
return result_image
|
|
|
|
def convert_to_rgba(images):
|
|
start = timer()
|
|
rgba_images = []
|
|
for img in images:
|
|
if img.mode == 'RGB':
|
|
rgba_img = img.convert('RGBA')
|
|
rgba_images.append(rgba_img)
|
|
else:
|
|
rgba_images.append(img)
|
|
end = timer()
|
|
print(f"rgb convert:{end - start}")
|
|
return rgba_images
|
|
|
|
def lerp(value1, value2, factor):
|
|
"""
|
|
Linearly interpolate between value1 and value2 by factor.
|
|
"""
|
|
result = np.interp(factor, [0, 1], [value1, value2])
|
|
return result
|
|
|
|
def lerp(a, b, t):
|
|
#start = timer()
|
|
t = np.clip(t, 0, 1) # clip t to the range [0, 1]
|
|
result = ((1 - t) * np.array(a) + t * np.array(b))
|
|
#end = timer()
|
|
#print(end - start)
|
|
return result
|
|
|
|
def lerpy(img1, img2, alpha):
|
|
start = timer()
|
|
vector = np.vectorize(np.int_)
|
|
if type(img1) is PIL.Image.Image:
|
|
img1 = np.array(img1)[:, :, 3]
|
|
if type(img2) is PIL.Image.Image:
|
|
img2 = np.array(img2)[:, :, 3]
|
|
beta = 1.0 - alpha
|
|
gamma = beta / img1.shape[1]
|
|
delta = gamma / img1.shape[0]
|
|
for j in range(img1.shape[0]):
|
|
for i in range(img1.shape[1]):
|
|
img1[j][i]=vector(((img1[j][i]*alpha)+(img2[j][i]*beta))+gamma)
|
|
#cv2.imshow('linear interpolation',img1[:, :, :, None])
|
|
#cv2.waitKey(0) & 0xFF
|
|
#result = Image.fromarray(img1[:, :], mode='L')
|
|
return Image.fromarray(img1[:, :], 'RGBA')
|
|
#return img1[:, :]
|
|
end = timer()
|
|
print(end - start)
|
|
return result
|
|
|
|
def lerp_color(color1, color2, t):
|
|
"""
|
|
Performs a linear interpolation (lerp) between two colors at a given progress value.
|
|
|
|
Args:
|
|
color1 (tuple): A tuple of 4 floats representing the first color in RGBA format.
|
|
color2 (tuple): A tuple of 4 floats representing the second color in RGBA format.
|
|
t (float): A value between 0.0 and 1.0 representing the progress of the lerp operation.
|
|
|
|
Returns:
|
|
A tuple of 4 floats representing the resulting color in RGBA format.
|
|
"""
|
|
r = (1 - t) * color1[0] + t * color2[0]
|
|
g = (1 - t) * color1[1] + t * color2[1]
|
|
b = (1 - t) * color1[2] + t * color2[2]
|
|
a = (1 - t) * color1[3] + t * color2[3]
|
|
return (r, g, b, a)
|
|
|
|
def lerp_imagemath(img1, img2, alpha:int = 50):
|
|
start = timer()
|
|
# must use ImageMath.eval to avoid overflow and alpha must be an int from 0 to 100
|
|
result = ImageMath.eval("((im * (100 - a)) / 100) + (im2 * a) / 100", im=img1.convert('L'), im2= img2.convert('L'), a=alpha)
|
|
end = timer()
|
|
print(f"lerp_imagemath: {end - start}")
|
|
return result
|
|
|
|
def lerp_imagemath_RGBA(img1, img2, alphaimg, factor:int = 50):
|
|
"""
|
|
Performs a linear interpolation (lerp) between two images at a given factor value.
|
|
|
|
Args:
|
|
img1 (PIL.Image): The first image to be interpolated.
|
|
img2 (PIL.Image): The second image to be interpolated.
|
|
factor (float): A value between 0.0 and 1.0 representing the progress of the interpolation.
|
|
|
|
Returns:
|
|
A PIL.Image object representing the resulting interpolated image.
|
|
"""
|
|
#start = timer()
|
|
# create alpha and alpha inverst from luma wipe image
|
|
# multiply the time factor
|
|
if img1.mode != "RGBA":
|
|
img1 = img1.convert("RGBA")
|
|
if img2.mode != "RGBA":
|
|
img2 = img2.convert("RGBA")
|
|
# Split the input images into color bands
|
|
r1, g1, b1, a1 = img1.split()
|
|
r2, g2, b2, a2 = img2.split()
|
|
rl = ImageMath.eval("((im * (100 - a)) / 100) + (im2 * a) / 100", im=r1.convert('L'), im2= r2.convert('L'), a=factor).convert('L')
|
|
gl = ImageMath.eval("((im * (100 - a)) / 100) + (im2 * a) / 100", im=g1.convert('L'), im2= g2.convert('L'), a=factor).convert('L')
|
|
bl = ImageMath.eval("((im * (100 - a)) / 100) + (im2 * a) / 100", im=b1.convert('L'), im2= b2.convert('L'), a=factor).convert('L')
|
|
if alphaimg is None:
|
|
alphaimg = ImageMath.eval("((im * (100 - a)) / 100) + (im2 * a) / 100", im=a1.convert('L'), im2= a2.convert('L'), a=factor).convert('L')
|
|
#ai = ImageMath.eval("(255-a) * t / 100",a = aa,t=5).convert('L')
|
|
#yellow_end_image = ImageChops.multiply(r2.convert("RGB"),aa.convert("RGB"))
|
|
#rebuilt_image = Image.merge("RGBA", (r1,g1,b1,aa))
|
|
# Multiply the red channel of the first image by the factor
|
|
#r1 = ImageMath.eval("convert(int(a*b), 'L')", a=r1, b=factor)
|
|
# Merge the color bands back into an RGBA image
|
|
rebuilt_image = Image.merge("RGBA", (rl, gl, bl, alphaimg.convert('L')))
|
|
#end = timer()
|
|
#print(f"lerp_imagemath_rgba: {end - start}")
|
|
return rebuilt_image
|
|
|
|
|
|
|
|
def CMYKInvert(img) :
|
|
return Image.merge(img.mode, [ImageOps.invert(b.convert('L')) for b in img.split()])
|
|
|
|
def clip_gradient_image(gradient_image, min_value:int = 50, max_value:int =75, invert= False, mask = False):
|
|
"""
|
|
Return only the values of a gradient grayscale image between a minimum and maximum value.
|
|
|
|
Args:
|
|
gradient_image (PIL.Image): The gradient grayscale image to adjust.
|
|
min_value (int): The minimum brightness value (0-255) to map to in the output image.
|
|
max_value (int): The maximum brightness value (0-255) to map to in the output image.
|
|
Returns:
|
|
A PIL.Image object representing the adjusted gradient image.
|
|
"""
|
|
#start = timer()
|
|
# Convert the image to grayscale if needed
|
|
if gradient_image.mode != "L":
|
|
gradient_image = gradient_image.convert("L")
|
|
# Normalize the input range to 0-1 NOTUSED
|
|
#normalized_image = ImageMath.eval("im/255", im=gradient_image)
|
|
normalized_image = gradient_image
|
|
# Adjust the brightness using ImageEnhance
|
|
#enhancer = ImageEnhance.Brightness(normalized_image)
|
|
#adjusted_image = enhancer.enhance(1.0 + max_value / 255)
|
|
adjusted_image = normalized_image
|
|
# Clip the values outside the desired range
|
|
if mask:
|
|
adjusted_image_mask = ImageMath.eval("im <= 0 ", im=adjusted_image)
|
|
adjusted_image = ImageMath.eval("mask * (255 - im)", im=adjusted_image, mask=adjusted_image_mask).convert("L")
|
|
# Map the brightness to the desired range
|
|
#mapped_image = ImageMath.eval("im * (max_value - min_value) + min_value", im=adjusted_image, min_value=min_value, max_value=max_value)
|
|
#mapped_image = ImageMath.eval("float(im) * ((max_value + min_value)/(max_value - min_value)) - min_value", im=adjusted_image, min_value=min_value, max_value=max_value)
|
|
if min_value <= 0 and max_value >= 0:
|
|
#mapped_image_mask = ImageMath.eval("im < max_value ", im=adjusted_image, min_value=min_value, max_value=max_value)
|
|
#mapped_image = ImageMath.eval("mask * im", im=adjusted_image, mask=mapped_image_mask, max_value=max_value).convert("L")
|
|
mapped_image = ImageMath.eval("im*(im<max_value)%256", im=adjusted_image, min_value=min_value, max_value=max_value)
|
|
elif min_value > 0 and max_value >= 255:
|
|
mapped_image = ImageMath.eval("im*(im>min_value)%256", im=adjusted_image, min_value=min_value, max_value=max_value)
|
|
else:
|
|
#mapped_image = ImageMath.eval("min(min(255 - float(im),255 - max_value) , (max(float(im), min_value) - min_value))", im=adjusted_image, min_value=min_value, max_value=max_value)
|
|
mapped_image = ImageMath.eval("min(im*(im<max_value)%256 , im*(im>min_value)%256)", im=adjusted_image, min_value=min_value, max_value=max_value)
|
|
# Convert the image back to 8-bit grayscale
|
|
final_image = ImageOps.grayscale(mapped_image)
|
|
if invert:
|
|
final_image = ImageOps.invert(final_image)
|
|
#end = timer()
|
|
#print(end - start)
|
|
return final_image
|
|
|
|
def resize_image_with_aspect_ratio(image: Image, basewidth: int = 512, baseheight: int = 512) -> Image:
|
|
"""
|
|
Resizes an image while maintaining its aspect ratio. This may not fill the entire image height.
|
|
|
|
Args:
|
|
- image (PIL.Image): The input image.
|
|
- basewidth (int): The desired width of the output image. Defaults to 512.
|
|
- baseheight (int): The desired height of the output image. Defaults to 512.
|
|
|
|
Returns:
|
|
- PIL.Image: The resized image.
|
|
|
|
Raises:
|
|
- ValueError: If `basewidth` or `baseheight` is less than or equal to 0.
|
|
|
|
"""
|
|
if basewidth <= 0 or baseheight <= 0:
|
|
raise ValueError("resize_image_with_aspect_ratio error: basewidth and baseheight must be greater than 0")
|
|
|
|
# Get the original size of the image
|
|
orig_width, orig_height = image.size
|
|
|
|
# Calculate the height that corresponds to the given width while maintaining aspect ratio
|
|
wpercent = (basewidth / float(orig_width))
|
|
hsize = int((float(orig_height) * float(wpercent)))
|
|
|
|
# Resize the image with Lanczos resampling filter
|
|
resized_image = image.resize((basewidth, hsize), resample=Image.Resampling.LANCZOS)
|
|
|
|
# If the height of the resized image is still larger than the given baseheight,
|
|
# then crop the image from the top and bottom to match the baseheight
|
|
if hsize > baseheight:
|
|
# Calculate the number of pixels to crop from the top and bottom
|
|
crop_height = (hsize - baseheight) // 2
|
|
# Crop the image
|
|
resized_image = resized_image.crop((0, crop_height, basewidth, hsize - crop_height))
|
|
else:
|
|
if hsize < baseheight:
|
|
# If the height of the resized image is smaller than the given baseheight,
|
|
# then paste the resized image in the middle of a blank image with the given baseheight
|
|
blank_image = Image.new("RGBA", (basewidth, baseheight), (255, 255, 255, 0))
|
|
blank_image.paste(resized_image, (0, (baseheight - hsize) // 2))
|
|
resized_image = blank_image
|
|
|
|
return resized_image
|
|
|
|
def resize_and_crop_image(image: Image, new_width: int = 512, new_height: int = 512) -> Image:
|
|
"""
|
|
Resizes and crops an image to a specified width and height. This ensures that the entire new_width and new_height
|
|
dimensions are filled by the image, and the aspect ratio is maintained.
|
|
|
|
Parameters:
|
|
- image (PIL.Image): The image to be resized and cropped.
|
|
- new_width (int): The desired width of the new image. Default is 512.
|
|
- new_height (int): The desired height of the new image. Default is 512.
|
|
|
|
Returns:
|
|
- cropped_image (PIL.Image): The resized and cropped image.
|
|
"""
|
|
# Get the dimensions of the original image
|
|
orig_width, orig_height = image.size
|
|
# Calculate the aspect ratios of the original and new images
|
|
orig_aspect_ratio = orig_width / float(orig_height)
|
|
new_aspect_ratio = new_width / float(new_height)
|
|
# Calculate the new size of the image while maintaining aspect ratio
|
|
if orig_aspect_ratio > new_aspect_ratio:
|
|
# The original image is wider than the new image, so we need to crop the sides
|
|
resized_width = int(new_height * orig_aspect_ratio)
|
|
resized_height = new_height
|
|
left_offset = (resized_width - new_width) // 2
|
|
top_offset = 0
|
|
else:
|
|
# The original image is taller than the new image, so we need to crop the top and bottom
|
|
resized_width = new_width
|
|
resized_height = int(new_width / orig_aspect_ratio)
|
|
left_offset = 0
|
|
top_offset = (resized_height - new_height) // 2
|
|
# Resize the image with Lanczos resampling filter
|
|
resized_image = image.resize((resized_width, resized_height), resample=Image.Resampling.LANCZOS)
|
|
# Crop the image to fill the entire height and width of the new image
|
|
cropped_image = resized_image.crop((left_offset, top_offset, left_offset + new_width, top_offset + new_height))
|
|
return cropped_image
|
|
|
|
def grayscale_to_gradient(image, gradient_colors = list([(255,255,255),(255,0,0)])):
|
|
"""
|
|
Converts a grayscale PIL Image into a two color image using the specified gradient colors.
|
|
|
|
Args:
|
|
image (PIL.Image.Image): The input grayscale image.
|
|
gradient_colors (list): A list of two tuples representing the gradient colors.
|
|
|
|
Returns:
|
|
PIL.Image.Image: A two color image with the same dimensions as the input grayscale image.
|
|
"""
|
|
# Create a new image with a palette
|
|
result = Image.new("P", image.size)
|
|
result.putpalette([c for color in gradient_colors for c in color])
|
|
# Convert the input image to a list of pixel values
|
|
pixel_values = list(image.getdata())
|
|
# Convert the pixel values to indices in the palette and assign them to the output image
|
|
#result.putdata([np.dot(p, gradient_colors[(len(gradient_colors) - 1)]) for p in pixel_values])
|
|
result.putdata([int(p * gradient_colors[(len(gradient_colors) - 1)]) for p in pixel_values])
|
|
return result
|
|
|
|
def rgb2gray(rgb):
|
|
return np.dot(rgb[...,:3], [0.2989, 0.5870, 0.1140])
|
|
|
|
|
|
# _, (h, k) = ellipse_bbox(0,0,768,512,math.radians(0.0)) # Ellipse center
|
|
def ellipse_bbox(h, k, a, b, theta):
|
|
"""
|
|
Computes the bounding box of an ellipse centered at (h,k) with semi-major axis 'a',
|
|
semi-minor axis 'b', and rotation angle 'theta' (in radians).
|
|
|
|
Args:
|
|
h (float): x-coordinate of the ellipse center.
|
|
k (float): y-coordinate of the ellipse center.
|
|
a (float): Length of the semi-major axis.
|
|
b (float): Length of the semi-minor axis.
|
|
theta (float): Angle of rotation (in radians) of the ellipse.
|
|
|
|
Returns:
|
|
tuple: A tuple of two tuples representing the top left and bottom right corners of
|
|
the bounding box of the ellipse, respectively.
|
|
"""
|
|
ux = a * math.cos(theta)
|
|
uy = a * math.sin(theta)
|
|
vx = b * math.cos(theta + math.pi / 2)
|
|
vy = b * math.sin(theta + math.pi / 2)
|
|
box_halfwidth = np.ceil(math.sqrt(ux**2 + vx**2))
|
|
box_halfheight = np.ceil(math.sqrt(uy**2 + vy**2))
|
|
return ((int(h - box_halfwidth), int(k - box_halfheight))
|
|
, (int(h + box_halfwidth), int(k + box_halfheight)))
|
|
|
|
|
|
# intel = make_gradient_v2(768,512,h/2,k/2,h*2/3,k*2/3,math.radians(0.0))
|
|
def make_gradient_v1(width, height, h, k, a, b, theta):
|
|
"""
|
|
Generates a gradient image with an elliptical shape.
|
|
|
|
Args:
|
|
width (int): Width of the output image.
|
|
height (int): Height of the output image.
|
|
h (float): x-coordinate of the center of the ellipse.
|
|
k (float): y-coordinate of the center of the ellipse.
|
|
a (float): Length of the semi-major axis.
|
|
b (float): Length of the semi-minor axis.
|
|
theta (float): Angle of rotation (in radians) of the ellipse.
|
|
|
|
Returns:
|
|
PIL.Image.Image: A PIL Image object representing the gradient with an elliptical shape.
|
|
"""
|
|
# Precalculate constants
|
|
st, ct = math.sin(theta), math.cos(theta)
|
|
aa, bb = a**2, b**2
|
|
|
|
# Initialize an empty array to hold the weights
|
|
weights = np.zeros((height, width), np.float64)
|
|
|
|
# Calculate the weight for each pixel
|
|
for y in range(height):
|
|
for x in range(width):
|
|
weights[y, x] = ((((x-h) * ct + (y-k) * st) ** 2) / aa
|
|
+ (((x-h) * st - (y-k) * ct) ** 2) / bb)
|
|
|
|
# Convert the weights to pixel values and create a PIL Image
|
|
pixel_values = np.uint8(np.clip(1.0 - weights, 0, 1) * 255)
|
|
return Image.fromarray(pixel_values, mode='L')
|
|
|
|
|
|
# make_gradient_v2(768,512,h/2,k/2,h-192,k-192,math.radians(30.0))
|
|
def make_gradient_v2(width, height, h, k, a, b, theta):
|
|
"""
|
|
Generates a gradient image with an elliptical shape.
|
|
|
|
Args:
|
|
width (int): Width of the output image.
|
|
height (int): Height of the output image.
|
|
h (float): x-coordinate of the center of the ellipse.
|
|
k (float): y-coordinate of the center of the ellipse.
|
|
a (float): Length of the semi-major axis.
|
|
b (float): Length of the semi-minor axis.
|
|
theta (float): Angle of rotation (in radians) of the ellipse.
|
|
|
|
Returns:
|
|
PIL.Image.Image: A PIL Image object representing the gradient with an elliptical shape.
|
|
"""
|
|
# Precalculate constants
|
|
st, ct = math.sin(theta), math.cos(theta)
|
|
aa, bb = a**2, b**2
|
|
# Generate (x,y) coordinate arrays
|
|
y,x = np.mgrid[-k:height-k,-h:width-h]
|
|
# Calculate the weight for each pixel
|
|
weights = (((x * ct + y * st) ** 2) / aa) + (((x * st - y * ct) ** 2) / bb)
|
|
# Convert the weights to pixel values and create a PIL Image
|
|
pixel_values = np.uint8(np.clip(1.0 - weights, 0, 1) * 255)
|
|
return Image.fromarray(pixel_values, mode='L')
|
|
|
|
def make_gradient_v3(width, height, h, k, a, b, theta, gradient_colors=[(255, 255, 255, 1), (0, 0, 0, 1)]):
|
|
"""
|
|
Generates a gradient image with an elliptical shape and the specified gradient colors.
|
|
|
|
Args:
|
|
width (int): Width of the output image.
|
|
height (int): Height of the output image.
|
|
h (float): x-coordinate of the center of the ellipse.
|
|
k (float): y-coordinate of the center of the ellipse.
|
|
a (float): Length of the semi-major axis.
|
|
b (float): Length of the semi-minor axis.
|
|
theta (float): Angle of rotation (in radians) of the ellipse.
|
|
gradient_colors (list): A list of two tuples representing the gradient colors.
|
|
|
|
Returns:
|
|
PIL.Image.Image: A two color gradient image with an elliptical shape.
|
|
"""
|
|
# Precalculate constants
|
|
st, ct = math.sin(theta), math.cos(theta)
|
|
aa, bb = a**2, b**2
|
|
# Generate (x,y) coordinate arrays
|
|
y, x = np.mgrid[-k:height-k, -h:width-h]
|
|
# Calculate the weight for each pixel
|
|
weights = (((x * ct + y * st) ** 2) / aa) + (((x * st - y * ct) ** 2) / bb)
|
|
# Normalize the weights to the range [0, 1]
|
|
weights = 1.0 - np.clip(weights / np.max(weights), 0.0, 1.0)
|
|
# Create a grayscale image from the weights array
|
|
grayscale_image = Image.fromarray(np.uint8(weights * 255))
|
|
# Convert the grayscale image into a two color gradient image using the specified gradient colors
|
|
gradient_image = grayscale_to_gradient(grayscale_image, gradient_colors)
|
|
|
|
return gradient_image
|
|
|
|
def draw_gradient_ellipse(width=512, height=512, white_amount=1.0, rotation = 0.0, contrast = 1.0):
|
|
"""
|
|
Draw an ellipse with a radial gradient fill, and a variable amount of white in the center.
|
|
|
|
:param height: The height of the output image. Default is 512.
|
|
:param width: The width of the output image. Default is 512.
|
|
:param white_amount: The amount of white in the center of the ellipse, as a float between 0.0 and 1.0. Default is 1.0.
|
|
:return: An RGBA image with the gradient ellipse.
|
|
"""
|
|
# Create a new image for outer ellipse
|
|
size = (width, height)
|
|
image = Image.new('RGBA', size, (255, 255, 255, 0))
|
|
theta = rotation * (math.pi / 180)
|
|
# Define the ellipse parameters
|
|
center = (int(width // 2), int(height // 2))
|
|
# Draw the ellipse and fill it with the radial gradient
|
|
image = make_gradient_v2(width, height, center[0], center[1], width * white_amount, height * white_amount, theta)
|
|
# Apply brightness method of ImageEnhance class
|
|
image = ImageEnhance.Contrast(image).enhance(contrast).convert('RGBA')
|
|
# Apply the alpha mask to the image
|
|
image = apply_alpha_mask(image, image)
|
|
# Return the result image
|
|
return image
|
|
|
|
def crop_fethear_ellipse(image: Image.Image, feather_margin: int = 30, width_offset: int = 0, height_offset: int = 0) -> Image.Image:
|
|
"""
|
|
Crop an elliptical region from the input image with a feathered edge.
|
|
|
|
Args:
|
|
image (PIL.Image.Image): The input image.
|
|
feather_margin (int): The size of the feathered edge, in pixels. Default is 30.
|
|
width_offset (int): The offset from the left and right edges of the image to the elliptical region. Default is 0.
|
|
height_offset (int): The offset from the top and bottom edges of the image to the elliptical region. Default is 0.
|
|
|
|
Returns:
|
|
A new PIL Image containing the cropped elliptical region with a feathered edge.
|
|
"""
|
|
|
|
# Create a blank mask image with the same size as the original image
|
|
mask = Image.new("L", image.size, 0)
|
|
draw = ImageDraw.Draw(mask)
|
|
|
|
# Calculate the ellipse's bounding box
|
|
ellipse_box = (
|
|
width_offset,
|
|
height_offset,
|
|
image.width - width_offset,
|
|
image.height - height_offset,
|
|
)
|
|
|
|
# Draw the ellipse on the mask
|
|
draw.ellipse(ellipse_box, fill=255)
|
|
|
|
# Apply the mask to the original image
|
|
result = Image.new("RGBA", image.size)
|
|
result.paste(image, mask=mask)
|
|
|
|
# Crop the resulting image to the ellipse's bounding box
|
|
cropped_image = result.crop(ellipse_box)
|
|
|
|
# Create a new mask image with a black background (0)
|
|
mask = Image.new("L", cropped_image.size, 0)
|
|
draw = ImageDraw.Draw(mask)
|
|
|
|
# Draw an ellipse on the mask image with a feathered edge
|
|
draw.ellipse(
|
|
(
|
|
0 + feather_margin,
|
|
0 + feather_margin,
|
|
cropped_image.width - feather_margin,
|
|
cropped_image.height - feather_margin,
|
|
),
|
|
fill=255,
|
|
outline=0,
|
|
)
|
|
|
|
# Apply a Gaussian blur to the mask image
|
|
mask = mask.filter(ImageFilter.GaussianBlur(radius=feather_margin / 2))
|
|
cropped_image.putalpha(mask)
|
|
|
|
# Paste the cropped image onto a new image with the same size as the input image
|
|
res = Image.new(cropped_image.mode, (image.width, image.height))
|
|
paste_pos = (
|
|
int((res.width - cropped_image.width) / 2),
|
|
int((res.height - cropped_image.height) / 2),
|
|
)
|
|
res.paste(cropped_image, paste_pos)
|
|
|
|
return res
|
|
|
|
def crop_inner_image(image: Image, width_offset: int, height_offset: int) -> Image:
|
|
"""
|
|
Crops an input image to the center, with the specified width and height offsets.
|
|
|
|
Args:
|
|
image (PIL.Image): The input image to be cropped.
|
|
width_offset (int): The width offset used for cropping.
|
|
height_offset (int): The height offset used for cropping.
|
|
|
|
Returns:
|
|
PIL.Image: The cropped image, resized to the original image size using Lanczos resampling.
|
|
"""
|
|
# Get the size of the input image
|
|
width, height = image.size
|
|
|
|
# Calculate the center coordinates of the image
|
|
center_x, center_y = int(width / 2), int(height / 2)
|
|
|
|
# Crop the image to the center using the specified offsets
|
|
cropped_image = image.crop(
|
|
(
|
|
center_x - width_offset,
|
|
center_y - height_offset,
|
|
center_x + width_offset,
|
|
center_y + height_offset,
|
|
)
|
|
)
|
|
|
|
# Resize the cropped image to the original image size using Lanczos resampling
|
|
resized_image = cropped_image.resize((width, height), resample=Image.Resampling.LANCZOS)
|
|
|
|
return resized_image
|
|
|
|
def multiply_alpha_ImageMath(image, factor):
|
|
"""
|
|
Multiply the alpha layer of a PIL RGBA image by a given factor and clip it between 0 and 255.
|
|
Returns a modified image.
|
|
"""
|
|
|
|
start = timer()
|
|
# Split the image into separate bands
|
|
r, g, b, a = image.split()
|
|
# Multiply the alpha band by the factor using ImageMath
|
|
a = ImageMath.eval("convert(float(a) * factor, 'L')", a=a, factor=factor)
|
|
# Clip the alpha band between 0 and 255
|
|
a = ImageMath.eval("convert(min(max(a, 0), 255), 'L')", a=a)
|
|
# Merge the bands back into an RGBA image
|
|
result_image = Image.merge("RGBA", (r, g, b, a))
|
|
end = timer()
|
|
print(f"multiply_alpha_ImageMath:{end - start}")
|
|
return result_image
|
|
|
|
def multiply_alpha(image, factor):
|
|
"""
|
|
Multiplies the alpha layer of an RGBA image by the given factor.
|
|
|
|
Args:
|
|
image (PIL.Image.Image): The input image.
|
|
factor (float): The multiplication factor for the alpha layer.
|
|
|
|
Returns:
|
|
PIL.Image.Image: The modified image.
|
|
"""
|
|
start = timer()
|
|
# Convert the image to a numpy array
|
|
np_image = np.array(image)
|
|
# Extract the alpha channel from the image
|
|
alpha = np_image[:, :, 3].astype(float)
|
|
# Multiply the alpha channel by the given factor
|
|
alpha *= factor
|
|
# Clip the alpha channel between 0 and 255
|
|
alpha = np.clip(alpha, 0, 255)
|
|
# Replace the original alpha channel with the modified one
|
|
np_image[:, :, 3] = alpha.astype(np.uint8)
|
|
# Convert the numpy array back to a PIL image
|
|
result_image = Image.fromarray(np_image)
|
|
end = timer()
|
|
print(f"multiply_alpha:{end - start}")
|
|
return result_image
|
|
|
|
def blend_images(start_image: Image, stop_image: Image, num_frames: int, invert:bool = False) -> list:
|
|
"""
|
|
Blend two images together via the alpha amount of each frame.
|
|
This function takes in three parameters:
|
|
- start_image: the starting PIL image in RGBA mode
|
|
- stop_image: the target PIL image in RGBA mode
|
|
- num_frames: the number of frames to generate in the blending animation
|
|
|
|
The function returns a list of PIL images representing the blending animation.
|
|
"""
|
|
# Initialize the list of blended frames
|
|
blended_frames = []
|
|
if (invert):
|
|
start_image, stop_image = stop_image, start_image
|
|
# Generate each frame of the blending animation
|
|
for i in range(num_frames):
|
|
start = timer()
|
|
# Calculate the alpha amount for this frame
|
|
alpha = i / float(num_frames - 1)
|
|
|
|
# Blend the two images using the alpha amount
|
|
blended_image = Image.blend(start_image, stop_image, alpha)
|
|
|
|
# Append the blended frame to the list
|
|
blended_frames.append(blended_image)
|
|
|
|
end = timer()
|
|
print(f"blend:{end - start}")
|
|
blended_frames.append(stop_image)
|
|
# Return the list of blended frames
|
|
return blended_frames
|
|
|
|
def alpha_composite_images(start_image: Image, stop_image: Image, gray_image: Image, num_frames: int, invert:bool = False) -> list:
|
|
"""
|
|
Blend two images together by using the gray image as the alpha amount of each frame.
|
|
This function takes in three parameters:
|
|
- start_image: the starting PIL image in RGBA mode
|
|
- stop_image: the target PIL image in RGBA mode
|
|
- gray_image: a gray scale PIL image of the same size as start_image and stop_image
|
|
- num_frames: the number of frames to generate in the blending animation
|
|
|
|
The function returns a list of PIL images representing the blending animation.
|
|
"""
|
|
# Initialize the list of blended frames
|
|
ac_frames = []
|
|
if (invert):
|
|
gray_image = ImageOps.invert(gray_image)
|
|
#set alpha layers of images to be blended
|
|
start_image_c = apply_alpha_mask(start_image.copy(), gray_image)
|
|
stop_image_c = apply_alpha_mask(stop_image.copy(), gray_image, invert = False)
|
|
|
|
# Generate each frame of the blending animation
|
|
for i in range(num_frames):
|
|
start = timer()
|
|
# Calculate the alpha amount for this frame
|
|
alpha = i / float(num_frames - 1)
|
|
start_adj_image = multiply_alpha(start_image_c, 1 - alpha)
|
|
stop_adj_image = multiply_alpha(stop_image_c, alpha)
|
|
|
|
# Blend the two images using the alpha amount
|
|
ac_image = Image.alpha_composite(start_adj_image, stop_adj_image)
|
|
|
|
# Append the blended frame to the list
|
|
ac_frames.append(ac_image)
|
|
end = timer()
|
|
print(f"alpha_composited:{end - start}")
|
|
ac_frames.append(stop_image)
|
|
# Return the list of blended frames
|
|
return ac_frames
|
|
|
|
###def luma_wipe_images(start_image: Image, stop_image: Image, alpha: Image, num_frames: int) -> list:
|
|
### #progress(0, status='Generating luma wipe...')
|
|
### lw_frames = []
|
|
### for i in range(num_frames):
|
|
### start = timer()
|
|
### # Compute the luma value for this frame
|
|
### luma_progress = i / (num_frames - 1)
|
|
### # Create a new image for the transition
|
|
### transition = Image.new("RGBA", start_image.size)
|
|
### # Loop over each pixel in the alpha layer
|
|
### for x in range(alpha.width):
|
|
### for y in range(alpha.height):
|
|
### # Compute the luma value for this pixel
|
|
### luma = alpha.getpixel((x, y))[0] / 255.0
|
|
### if luma_progress >= luma:
|
|
### # Interpolate between the two images based on the luma value
|
|
### pixel = (
|
|
### int(start_image.getpixel((x, y))[0] * (1 - luma) + stop_image.getpixel((x, y))[0] * luma),
|
|
### int(start_image.getpixel((x, y))[1] * (1 - luma) + stop_image.getpixel((x, y))[1] * luma),
|
|
### int(start_image.getpixel((x, y))[2] * (1 - luma) + stop_image.getpixel((x, y))[2] * luma),
|
|
### int(255 * luma_progress) # Set the alpha value based on the luma value
|
|
### )
|
|
### # Set the new pixel in the transition image
|
|
### transition.putpixel((x, y), pixel)
|
|
### else:
|
|
### # Set the start pixel in the transition image
|
|
### transition.putpixel((x, y), start_image.getpixel((x, y)))
|
|
### # Append the transition image to the list
|
|
### lw_frames.append(transition)
|
|
### #progress((x + 1) / num_frames)
|
|
### end = timer()
|
|
### print(f"luma_wipe:{end - start}")
|
|
### return lw_frames
|
|
|
|
###def srgb_nonlinear_to_linear_channel(u):
|
|
### return (u / 12.92) if (u <= 0.04045) else pow((u + 0.055) / 1.055, 2.4)
|
|
|
|
###def srgb_nonlinear_to_linear(v):
|
|
### return [srgb_nonlinear_to_linear_channel(x) for x in v]
|
|
|
|
####result_img = eval("convert('RGBA')", lambda x, y: PSLumaWipe(img_a.getpixel((x,y)), img_b.getpixel((x,y)), test_g_image.getpixel((x,y))[0]/255,(1,0,0,.5), 0.25, False, 0.1, 0.01, 0.01))
|
|
####list(np.divide((255,255,245,225),255))
|
|
###def PSLumaWipe(a_color, b_color, luma, l_color=(255, 255, 255, 255), progress=0.0, invert=False, softness=0.01, start_adjust = 0.01, stop_adjust = 0.0):
|
|
### # - adjust for min and max. Do not process if luma value is outside min or max
|
|
### if ((luma >= (start_adjust)) and (luma <= (1 - stop_adjust))):
|
|
### if (invert):
|
|
### luma = 1.0 - luma
|
|
### # user color with luma
|
|
### out_color = np.array([l_color[0], l_color[1], l_color[2], luma * 255])
|
|
### time = lerp(0.0, 1.0 + softness, progress)
|
|
### #print(f"softness: {str(softness)} out_color: {str(out_color)} a_color: {str(a_color)} b_color: {str(b_color)} time: {str(time)} luma: {str(luma)} progress: {str(progress)}")
|
|
### # if luma less than time, do not blend color
|
|
### if (luma <= time - softness):
|
|
### alpha_behind = np.clip(1.0 - (time - softness - luma) / softness, 0.0, 1.0)
|
|
### return tuple(np.round(lerp(b_color, out_color, alpha_behind)).astype(int))
|
|
### # if luma greater than time, show original color
|
|
### if (luma >= time):
|
|
### return a_color
|
|
### alpha = (time - luma) / softness
|
|
### out_color = lerp(a_color, b_color + out_color, alpha)
|
|
### #print(f"alpha: {str(alpha)} out_color: {str(out_color)} time: {str(time)} luma: {str(luma)}")
|
|
### out_color = srgb_nonlinear_to_linear(out_color)
|
|
### return tuple(np.round(out_color).astype(int))
|
|
### else:
|
|
### # return original pixel color
|
|
### return a_color
|
|
|
|
###def PSLumaWipe_images(start_image: Image, stop_image: Image, luma_wipe_image: Image, num_frames: int, transition_color: tuple[int, int, int, int] = (255,255,255,255)) -> list:
|
|
### #progress(0, status='Generating luma wipe...')
|
|
### # fix transition_color to relative 0.0 - 1.0
|
|
### #luma_color = list(np.divide(transition_color,255))
|
|
|
|
### softness = 0.03
|
|
### lw_frames = []
|
|
### lw_frames.append(start_image)
|
|
### width, height = start_image.size
|
|
### #compensate for different image sizes for LumaWipe
|
|
### if (start_image.size != luma_wipe_image.size):
|
|
### luma_wipe_image = resize_and_crop_image(luma_wipe_image,width,height)
|
|
### # call PSLumaWipe for each pixel
|
|
### for i in range(num_frames):
|
|
### start = timer()
|
|
### # Compute the luma value for this frame
|
|
### luma_progress = i / (num_frames - 1)
|
|
### transition = Image.new(start_image.mode, (width, height))
|
|
### # apply to each pixel in the image
|
|
### for x in range(width):
|
|
### for y in range(height):
|
|
### # call PSLumaWipe for each pixel
|
|
### pixel = PSLumaWipe(start_image.getpixel((x, y)), stop_image.getpixel((x, y)), luma_wipe_image.getpixel((x, y))[0]/255, transition_color, luma_progress, False, softness, 0.01, 0.00)
|
|
### transition.putpixel((x, y), pixel)
|
|
### lw_frames.append(transition)
|
|
### print(f"Luma Wipe frame:{len(lw_frames)}")
|
|
### #lw_frames[-1].show()
|
|
### end = timer()
|
|
### print(f"PSLumaWipe:{end - start}")
|
|
### lw_frames.append(stop_image)
|
|
### return lw_frames
|
|
|
|
#result_img = , 0.25, False, 0.1, 0.01, 0.01))
|
|
#list(np.divide((255,255,245,225),255))
|
|
def PSLumaWipe2(a_color, b_color, luma, l_color=(255, 255, 0, 255), progress=0.0, invert=False, softness=1.0, start_adjust = 0.1, stop_adjust = 0.1):
|
|
# luma is now an image file
|
|
# color is now a color image file that is merged with approaching b_color image
|
|
# the entire frame is built based upon progress
|
|
# image alpha layers are the luma image grayscale image
|
|
#0. Handle edge cases
|
|
#1. invert luma if invert is true
|
|
#2. build the colorized out_color image, b_color is the rgb value, alpha channel from clip_gradient_image
|
|
#3. build the colorized luma image
|
|
#4. Use a_color image as the base image
|
|
#5. merge or composite images together
|
|
#6. return the merged image
|
|
# - adjust for min and max. Do not process if luma value is outside min or max
|
|
#start = timer()
|
|
if (progress <= start_adjust):
|
|
final_image = a_color
|
|
elif (progress >= (1 - stop_adjust)):
|
|
final_image = b_color
|
|
else:
|
|
if luma.mode != "L":
|
|
luma = luma.convert("L")
|
|
# invert luma if invert is true
|
|
if (invert):
|
|
luma = ImageOps.invert(luma)
|
|
# build time values to create the image at the correct progress point
|
|
max_time = int(np.ceil(np.clip(lerp(0.0, 1.0 + softness, progress) * 255, 0, 255)))
|
|
time = int(np.ceil(np.clip(lerp(0.0, 1.0, progress) * 255, 0, 255)))
|
|
# build the colorized out_color image
|
|
# b_color is the rgb value
|
|
out_color = Image.new("RGBA", a_color.size, (l_color[0], l_color[1], l_color[2], l_color[3]))
|
|
out_color_alpha = clip_gradient_image(luma, time, max_time, False)
|
|
#build colorized luma image
|
|
out_color.putalpha(out_color_alpha)
|
|
# add out_color to a_color within alpha limits
|
|
|
|
# lerp_imagemath_RGBA works reasonably fast, but minimal visual difference, softness increases visibility
|
|
if softness >= 0.1:
|
|
a_out_color = lerp_imagemath_RGBA(a_color, b_color, out_color_alpha, int(np.ceil((max_time * 100)/255)))
|
|
else:
|
|
a_out_color = a_color.copy()
|
|
a_out_color.putalpha(out_color_alpha)
|
|
a_out_color = Image.alpha_composite(a_out_color, out_color)
|
|
# build the colorized out_color image, b_color is the rgb value
|
|
# out_color_alpha should provide transparency to see b_color
|
|
# we need the alpha channel to be reversed so that the color is transparent
|
|
b_color_alpha = clip_gradient_image(ImageOps.invert(luma), 255 - time, 255, False)
|
|
b_out_color = b_color.copy()
|
|
b_out_color.putalpha(b_color_alpha)
|
|
out_color_comp = Image.alpha_composite(a_out_color, b_out_color)
|
|
# ensure that the composited images have transparency
|
|
a_color.putalpha(ImageOps.invert(b_color_alpha))
|
|
#a_color.show("a_color b_color_alpha")
|
|
final_image = Image.alpha_composite(a_color, out_color_comp)
|
|
|
|
#end = timer()
|
|
#print(f"PSLumaWipe2:{end - start} ")
|
|
return final_image.convert("RGBA")
|
|
|
|
|
|
def PSLumaWipe_images2(start_image: Image, stop_image: Image, luma_wipe_image: Image, num_frames: int, invert:bool = False, transition_color: tuple[int, int, int, int] = (255,255,255,255)) -> list:
|
|
#progress(0, status='Generating luma wipe...')
|
|
#luma_color = list(np.divide(transition_color,255))
|
|
softness = 0.095
|
|
lw_frames = []
|
|
lw_frames.append(start_image.convert("RGBA"))
|
|
width, height = start_image.size
|
|
#compensate for different image sizes for LumaWipe
|
|
if (start_image.size != luma_wipe_image.size):
|
|
luma_wipe_image = resize_and_crop_image(luma_wipe_image,width,height)
|
|
# call PSLumaWipe for each frame
|
|
for i in range(num_frames):
|
|
# Compute the luma value for this frame
|
|
luma_progress = i / (num_frames - 1)
|
|
|
|
# call PSLumaWipe for frame
|
|
transition = PSLumaWipe2(start_image.copy(), stop_image.copy(), luma_wipe_image.copy(), transition_color, luma_progress, invert, softness, 0.02, 0.01)
|
|
lw_frames.append(transition)
|
|
print(f"Luma Wipe frame:{len(lw_frames)} {transition.size} {luma_progress * 100}%")
|
|
lw_frames.append(stop_image.convert("RGBA"))
|
|
return lw_frames |