infinite-zoom-automatic1111.../iz_helpers/image.py

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