from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageDraw, ImageFont import requests import base64 import numpy as np import math from io import BytesIO 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, 1)) 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): """ 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 = mask_image.resize(image.size) # Apply the mask as the alpha layer of the current image result_image = image.copy() result_image.putalpha(mask_image.convert('L')) # convert to grayscale return result_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.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.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): """ 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([gradient_colors[int(p * (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) # Define the radial gradient parameters #ellipse_width, ellipse_height = (int((width * white_amount) // 1.5), int((height * white_amount) // 1.5)) #ellipse_colors = [(255, 255, 255, 255), (0, 0, 0, 0)] # Create a new image for inner ellipse #inner_ellipse = Image.new("L", size, 0) #inner_ellipse = make_gradient_v2(width, height, center[0], center[1], ellipse_width, ellipse_height, theta) #inner_ellipse = apply_alpha_mask(inner_ellipse, inner_ellipse) #image.paste(inner_ellipse, center, mask=inner_ellipse) # Creating object of Brightness class # 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.LANCZOS) return resized_image