diff --git a/iz_helpers/image.py b/iz_helpers/image.py index a0545b1..4b1b7c2 100644 --- a/iz_helpers/image.py +++ b/iz_helpers/image.py @@ -1,10 +1,12 @@ -from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageDraw, ImageFont, ImageOps, ImageMath +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): @@ -51,8 +53,7 @@ def open_image(image_path): img = Image.open(BytesIO(decoded_data)) else: # Assume that the image path is a file path - img = Image.open(image_path) - + img = Image.open(image_path) return img def apply_alpha_mask(image, mask_image, invert = False): @@ -69,33 +70,42 @@ def apply_alpha_mask(image, mask_image, invert = False): # 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: - ImageEnhance.Contrast(mask_image).enhance(-1.0) + 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) + 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. """ - return np.interp(factor, [0, 1], [value1, value2]) + 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] - return ((1 - t) * np.array(a) + t * np.array(b)) + 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] @@ -109,13 +119,12 @@ def lerpy(img1, img2, alpha): img1[j][i]=vector(((img1[j][i]*alpha)+(img2[j][i]*beta))+gamma) #cv2.imshow('linear interpolation',img1[:, :, :, None]) #cv2.waitKey(0) & 0xFF - return Image.fromarray(img1[:, :], mode='L') - #return Image.fromarray(img1[:, :], 'RGBA') + #result = Image.fromarray(img1[:, :], mode='L') + return Image.fromarray(img1[:, :], 'RGBA') #return img1[:, :] - -def lerp_imagemath(img1, img2, alpha:int = 50): - # must use ImageMath.eval to avoid overflow and alpha must be an int from 0 to 100 - return ImageMath.eval("((im * a) / 100) + (im2 * (100 - a)) / 100", im=img1.convert('L'), im2= img2.convert('L'), a=alpha) + end = timer() + print(end - start) + return result def lerp_color(color1, color2, t): """ @@ -135,6 +144,97 @@ def lerp_color(color1, color2, t): 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(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. + """ + # create alpha and alpha inverst from luma wipe image + # multiply the time factor + + # Split the input images into color bands + r1, g1, b1 = img1.split() + r2, g2, b2 = img2.split() + aa, ba, aa = alphaimg.split() + 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 + result = Image.merge("RGBA", (r1, g2, b1, aa)) + + return result + #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] + +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): + """ + 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 + 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 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*(immin_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. @@ -196,11 +296,9 @@ def resize_and_crop_image(image: Image, new_width: int = 512, new_height: int = """ # 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 @@ -214,16 +312,13 @@ def resize_and_crop_image(image: Image, new_width: int = 512, new_height: int = 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): +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. @@ -236,19 +331,18 @@ def grayscale_to_gradient(image, gradient_colors): """ # 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]) - + 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()) - + pixel_values = list(image.getdata()) # Convert the pixel values to indices in the palette and assign them to the output image - result.putdata([gradient_colors[int(p * (len(gradient_colors) - 1))] for p in pixel_values]) - + #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): """ @@ -509,6 +603,8 @@ 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 @@ -516,7 +612,10 @@ def multiply_alpha_ImageMath(image, 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 - return Image.merge("RGBA", (r, g, b, a)) + 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): """ @@ -529,6 +628,7 @@ def multiply_alpha(image, factor): 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 @@ -541,6 +641,8 @@ def multiply_alpha(image, factor): 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, gray_image: Image, num_frames: int) -> list: @@ -563,6 +665,7 @@ def blend_images(start_image: Image, stop_image: Image, gray_image: Image, num_f # 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) @@ -571,6 +674,8 @@ def blend_images(start_image: Image, stop_image: Image, gray_image: Image, num_f # Append the blended frame to the list blended_frames.append(blended_image) + end = timer() + print(f"blend:{end - start}") # Return the list of blended frames return blended_frames @@ -585,7 +690,7 @@ def alpha_composite_images(start_image: Image, stop_image: Image, gray_image: Im - 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 = [] @@ -595,6 +700,7 @@ def alpha_composite_images(start_image: Image, stop_image: Image, gray_image: Im # 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.copy(), 1 - alpha) @@ -605,14 +711,17 @@ def alpha_composite_images(start_image: Image, stop_image: Image, gray_image: Im # Append the blended frame to the list ac_frames.append(ac_image) + end = timer() + print(f"alpha_composited:{end - start}") # 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...') + #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 @@ -638,6 +747,8 @@ def luma_wipe_images(start_image: Image, stop_image: Image, alpha: Image, num_fr # 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): @@ -648,7 +759,7 @@ def srgb_nonlinear_to_linear(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): +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): @@ -667,7 +778,7 @@ def PSLumaWipe(a_color, b_color, luma, l_color=(255, 255, 255, 255), progress=0. 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) + out_color = srgb_nonlinear_to_linear(out_color) return tuple(np.round(out_color).astype(int)) else: # return original pixel color @@ -677,6 +788,7 @@ def PSLumaWipe_images(start_image: Image, stop_image: Image, luma_wipe_image: Im #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) @@ -686,6 +798,7 @@ def PSLumaWipe_images(start_image: Image, stop_image: Image, luma_wipe_image: Im 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)) @@ -698,5 +811,84 @@ def PSLumaWipe_images(start_image: Image, stop_image: Image, luma_wipe_image: Im 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): + return a_color + if (progress >= (1 - stop_adjust)): + return b_color + # 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 + 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.convert("L")), 255 - max_time, 255, False) + b_out_color = b_color.copy() + b_out_color.putalpha(b_color_alpha) + b_out_color = Image.alpha_composite(a_out_color, b_out_color) + final_image = Image.alpha_composite(a_color.convert("RGBA"), b_out_color) + end = timer() + print(f"PSLumaWipe2:{end - start}") + return final_image + + +def PSLumaWipe_images2(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)) + # call PSLumaWipe for frame + transition = PSLumaWipe2(start_image, stop_image, luma_wipe_image, transition_color, luma_progress, False, softness, 0.01, 0.00) + + lw_frames.append(transition) + + print(f"Luma Wipe frame:{len(lw_frames)}") + #lw_frames[-1].show() lw_frames.append(stop_image) return lw_frames \ No newline at end of file