Spaces:
Running
on
Zero
Running
on
Zero
# utils/image_utils.py | |
import os | |
from io import BytesIO | |
import base64 | |
import numpy as np | |
#from decimal import ROUND_CEILING | |
from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter, ImageDraw, ImageOps, ImageMath | |
from typing import List, Union | |
#import numpy as np | |
#import math | |
from utils.constants import default_lut_example_img | |
from utils.color_utils import ( | |
detect_color_format, | |
update_color_opacity | |
) | |
from utils.misc import (pause) | |
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. | |
Raises: | |
Exception: If there is an error opening the image. | |
""" | |
import requests | |
try: | |
# Strip leading and trailing double quotation marks, if present | |
image_path = image_path.strip('"') | |
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) | |
except Exception as e: | |
raise Exception(f'Error opening image: {e}') | |
return img | |
def build_prerendered_images(images_list): | |
""" | |
Opens a list of images from file paths, URLs, or DataURL strings. | |
Parameters: | |
images_list (list): A list of file paths, URLs, or DataURL strings of the images to open. | |
Returns: | |
list: A list of PIL Image objects of the opened images. | |
""" | |
return [open_image(image) for image in images_list] | |
def build_encoded_images(images_list): | |
""" | |
Encodes a list of images to base64 strings. | |
Parameters: | |
images_list (list): A list of file paths, URLs, DataURL strings, or PIL Image objects of the images to encode. | |
Returns: | |
list: A list of base64-encoded strings of the images. | |
""" | |
return [image_to_base64(image) for image in images_list] | |
def image_to_base64(image): | |
""" | |
Encodes an image to a base64 string. | |
Parameters: | |
image (str or PIL.Image.Image): The file path, URL, DataURL string, or PIL Image object of the image to encode. | |
Returns: | |
str: A base64-encoded string of the image. | |
""" | |
buffered = BytesIO() | |
if type(image) is str: | |
image = open_image(image) | |
image.save(buffered, format="PNG") | |
return "data:image/png;base64," + base64.b64encode(buffered.getvalue()).decode() | |
def change_color(image, color, opacity=0.75): | |
""" | |
Changes the color of an image by overlaying it with a specified color and opacity. | |
Parameters: | |
image (str or PIL.Image.Image): The file path, URL, DataURL string, or PIL Image object of the image to change. | |
color (str or tuple): The color to overlay on the image. | |
opacity (float): The opacity of the overlay color (0.0 to 1.0). | |
Returns: | |
PIL.Image.Image: The image with the color changed. | |
""" | |
if type(image) is str: | |
image = open_image(image) | |
try: | |
# Convert the color to RGBA format | |
rgba_color = detect_color_format(color) | |
rgba_color = update_color_opacity(rgba_color, opacity) | |
# Convert the image to RGBA mode | |
image = image.convert("RGBA") | |
# Create a new image with the same size and mode | |
new_image = Image.new("RGBA", image.size, rgba_color) | |
# Composite the new image with the original image | |
result = Image.alpha_composite(image, new_image) | |
except Exception as e: | |
print(f"Error changing color: {e}") | |
return image | |
return result | |
def convert_str_to_int_or_zero(value): | |
""" | |
Converts a string to an integer, or returns zero if the conversion fails. | |
Parameters: | |
value (str): The string to convert. | |
Returns: | |
int: The converted integer, or zero if the conversion fails. | |
""" | |
try: | |
return int(value) | |
except ValueError: | |
return 0 | |
def upscale_image(image, scale_factor): | |
""" | |
Upscales an image by a given scale factor using the LANCZOS filter. | |
Parameters: | |
image (PIL.Image.Image): The input image to be upscaled. | |
scale_factor (float): The factor by which to upscale the image. | |
Returns: | |
PIL.Image.Image: The upscaled image. | |
""" | |
# Calculate the new size | |
new_width = int(image.width * scale_factor) | |
new_height = int(image.height * scale_factor) | |
# Upscale the image using the LANCZOS filter | |
upscaled_image = image.resize((new_width, new_height), Image.LANCZOS) | |
return upscaled_image | |
def crop_and_resize_image(image, width, height): | |
""" | |
Crops the image to a centered square and resizes it to the specified width and height. | |
Parameters: | |
image (PIL.Image.Image): The input image to be cropped and resized. | |
width (int): The desired width of the output image. | |
height (int): The desired height of the output image. | |
Returns: | |
PIL.Image.Image: The cropped and resized image. | |
""" | |
# Get original dimensions | |
original_width, original_height = image.size | |
# Determine the smaller dimension to make a square crop | |
min_dim = min(original_width, original_height) | |
# Calculate coordinates for cropping to a centered square | |
left = (original_width - min_dim) // 2 | |
top = (original_height - min_dim) // 2 | |
right = left + min_dim | |
bottom = top + min_dim | |
# Crop the image | |
cropped_image = image.crop((left, top, right, bottom)) | |
# Resize the image to the desired dimensions | |
resized_image = cropped_image.resize((width, height), Image.LANCZOS) | |
return resized_image | |
def resize_image_with_aspect_ratio(image, target_width, target_height): | |
""" | |
Resizes the image to fit within the target dimensions while maintaining aspect ratio. | |
If the aspect ratio does not match, the image will be padded with black pixels. | |
Parameters: | |
image (PIL.Image.Image): The input image to be resized. | |
target_width (int): The target width. | |
target_height (int): The target height. | |
Returns: | |
PIL.Image.Image: The resized image. | |
""" | |
# Calculate aspect ratios | |
original_width, original_height = image.size | |
target_aspect = target_width / target_height | |
original_aspect = original_width / original_height | |
# Decide whether to fit width or height | |
if original_aspect > target_aspect: | |
# Image is wider than target aspect ratio | |
new_width = target_width | |
new_height = int(target_width / original_aspect) | |
else: | |
# Image is taller than target aspect ratio | |
new_height = target_height | |
new_width = int(target_height * original_aspect) | |
# Resize the image | |
resized_image = image.resize((new_width, new_height), Image.LANCZOS) | |
# Create a new image with target dimensions and black background | |
new_image = Image.new("RGB", (target_width, target_height), (0, 0, 0)) | |
# Paste the resized image onto the center of the new image | |
paste_x = (target_width - new_width) // 2 | |
paste_y = (target_height - new_height) // 2 | |
new_image.paste(resized_image, (paste_x, paste_y)) | |
return new_image | |
def lerp_imagemath(img1, img2, alpha_percent: int = 50): | |
""" | |
Performs linear interpolation (LERP) between two images based on the given alpha value. | |
Parameters: | |
img1 (str or PIL.Image.Image): The first image or its file path. | |
img2 (str or PIL.Image.Image): The second image or its file path. | |
alpha (int): The interpolation factor (0 to 100). | |
Returns: | |
PIL.Image.Image: The interpolated image. | |
""" | |
if isinstance(img1, str): | |
img1 = open_image(img1) | |
if isinstance(img2, str): | |
img2 = open_image(img2) | |
# Ensure both images are in the same mode (e.g., RGBA) | |
img1 = img1.convert('RGBA') | |
img2 = img2.convert('RGBA') | |
# Convert images to NumPy arrays | |
arr1 = np.array(img1, dtype=np.float32) | |
arr2 = np.array(img2, dtype=np.float32) | |
# Perform linear interpolation | |
alpha = alpha_percent / 100.0 | |
result_arr = (arr1 * (1 - alpha)) + (arr2 * alpha) | |
# Convert the result back to a PIL image | |
result_img = Image.fromarray(np.uint8(result_arr)) | |
#result_img.show() | |
return result_img | |
def shrink_and_paste_on_blank(current_image, mask_width, mask_height, blank_color:tuple[int, int, int, int] = (0,0,0,0)): | |
""" | |
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. | |
Parameters: | |
current_image (PIL.Image.Image): The input image to transform. | |
mask_width (int): Width in pixels to shrink from each side. | |
mask_height (int): Height in pixels to shrink from each side. | |
blank_color (tuple): The color of the blank frame (default is transparent). | |
Returns: | |
PIL.Image.Image: The transformed image. | |
""" | |
# 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), blank_color) | |
blank_image.paste(prev_image, (mask_width, mask_height)) | |
return blank_image | |
def multiply_and_blend_images(base_image, image2, alpha_percent=50): | |
""" | |
Multiplies two images and blends the result with the original image. | |
Parameters: | |
image1 (PIL.Image.Image): The first input image. | |
image2 (PIL.Image.Image): The second input image. | |
alpha (float): The blend factor (0.0 to 100.0) for blending the multiplied result with the original image. | |
Returns: | |
PIL.Image.Image: The blended image. | |
""" | |
alpha = alpha_percent / 100.0 | |
if isinstance(base_image, str): | |
base_image = open_image(base_image) | |
if isinstance(image2, str): | |
image2 = open_image(image2) | |
# Ensure both images are in the same mode and size | |
base_image = base_image.convert('RGBA') | |
image2 = image2.convert('RGBA') | |
image2 = image2.resize(base_image.size) | |
# Multiply the images | |
multiplied_image = ImageChops.multiply(base_image, image2) | |
# Blend the multiplied result with the original | |
blended_image = Image.blend(base_image, multiplied_image, alpha) | |
return blended_image | |
def alpha_composite_with_control(base_image, image_with_alpha, alpha_percent=100): | |
""" | |
Overlays image_with_alpha onto base_image with controlled alpha transparency. | |
Parameters: | |
base_image (PIL.Image.Image): The base image. | |
image_with_alpha (PIL.Image.Image): The image to overlay with an alpha channel. | |
alpha_percent (float): The multiplier for the alpha channel (0.0 to 100.0). | |
Returns: | |
PIL.Image.Image: The resulting image after alpha compositing. | |
""" | |
alpha_multiplier = alpha_percent / 100.0 | |
if isinstance(base_image, str): | |
base_image = open_image(base_image) | |
if isinstance(image_with_alpha, str): | |
image_with_alpha = open_image(image_with_alpha) | |
# Ensure both images are in RGBA mode | |
base_image = base_image.convert('RGBA') | |
image_with_alpha = image_with_alpha.convert('RGBA') | |
# Extract the alpha channel and multiply by alpha_multiplier | |
alpha_channel = image_with_alpha.split()[3] | |
alpha_channel = alpha_channel.point(lambda p: p * alpha_multiplier) | |
# Apply the modified alpha channel back to the image | |
image_with_alpha.putalpha(alpha_channel) | |
# Composite the images | |
result = Image.alpha_composite(base_image, image_with_alpha) | |
return result | |
def apply_alpha_mask(image, mask_image, invert = False): | |
""" | |
Applies a mask image as the alpha channel of the input image. | |
Parameters: | |
image (PIL.Image.Image): The image to apply the mask to. | |
mask_image (PIL.Image.Image): The alpha mask to apply. | |
invert (bool): Whether to invert the mask (default is False). | |
Returns: | |
PIL.Image.Image: The 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 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.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: | |
PIL.Image.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 | |
##################################################### LUTs ############################################################ | |
def is_3dlut_row(row: List[str]) -> bool: | |
""" | |
Check if one line in the file has exactly 3 numeric values. | |
Parameters: | |
row (list): A list of strings representing the values in a row. | |
Returns: | |
bool: True if the row has exactly 3 numeric values, False otherwise. | |
""" | |
try: | |
row_values = [float(val) for val in row] | |
return len(row_values) == 3 | |
except ValueError: | |
return False | |
def read_lut(path_lut: Union[str, os.PathLike], num_channels: int = 3) -> ImageFilter.Color3DLUT: | |
""" | |
Read LUT from a raw file. | |
Each line in the file is considered part of the LUT table. The function | |
reads the file, parses the rows, and constructs a Color3DLUT object. | |
Args: | |
path_lut: A string or os.PathLike object representing the path to the LUT file. | |
num_channels: An integer specifying the number of color channels in the LUT (default is 3). | |
Returns: | |
An instance of ImageFilter.Color3DLUT representing the LUT. | |
Raises: | |
FileNotFoundError: If the LUT file specified by path_lut does not exist. | |
""" | |
with open(path_lut) as f: | |
lut_raw = f.read().splitlines() | |
size = round(len(lut_raw) ** (1 / 3)) | |
row2val = lambda row: tuple([float(val) for val in row.split(" ")]) | |
lut_table = [row2val(row) for row in lut_raw if is_3dlut_row(row.split(" "))] | |
return ImageFilter.Color3DLUT(size, lut_table, num_channels) | |
def apply_lut(img: Image, lut_path: str = "", lut: ImageFilter.Color3DLUT = None) -> Image: | |
""" | |
Apply a LUT to an image and return a PIL Image with the LUT applied. | |
The function applies the LUT to the input image using the filter() method of the PIL Image class. | |
Args: | |
img: A PIL Image object to which the LUT should be applied. | |
lut_path: A string representing the path to the LUT file (optional if lut argument is provided). | |
lut: An instance of ImageFilter.Color3DLUT representing the LUT (optional if lut_path is provided). | |
Returns: | |
A PIL Image object with the LUT applied. | |
Raises: | |
ValueError: If both lut_path and lut arguments are not provided. | |
""" | |
if lut is None: | |
if lut_path == "": | |
raise ValueError("Either lut_path or lut argument must be provided.") | |
lut = read_lut(lut_path) | |
return img.filter(lut) | |
def show_lut(lut_filename: str, lut_example_image: Image = default_lut_example_img) -> Image: | |
if lut_filename is not None: | |
try: | |
lut_example_image = apply_lut(lut_example_image, lut_filename) | |
except Exception as e: | |
print(f"BAD LUT: Error applying LUT {str(e)}.") | |
else: | |
lut_example_image = open_image(default_lut_example_img) | |
return lut_example_image | |
def convert_rgb_to_rgba_safe(image: Image) -> Image: | |
""" | |
Converts an RGB image to RGBA by adding an alpha channel. | |
Ensures that the original image remains unaltered. | |
Parameters: | |
image (PIL.Image.Image): The RGB image to convert. | |
Returns: | |
PIL.Image.Image: The converted RGBA image. | |
""" | |
if image.mode != 'RGB': | |
if image.mode == 'RGBA': | |
return image | |
elif image.mode == 'P': | |
# Convert palette image to RGBA | |
image = image.convert('RGB') | |
else: | |
raise ValueError("Unsupported image mode for conversion to RGBA.") | |
# Create a copy of the image to avoid modifying the original | |
rgba_image = image.copy() | |
# Optionally, set a default alpha value (e.g., fully opaque) | |
alpha = Image.new('L', rgba_image.size, 255) # 255 for full opacity | |
rgba_image.putalpha(alpha) | |
return rgba_image | |
def apply_lut_to_image_path(lut_filename: str, image_path: str) -> Image: | |
""" | |
Apply a LUT to an image and return the result. | |
Args: | |
lut_filename: A string representing the path to the LUT file. | |
image_path: A string representing the path to the input image. | |
Returns: | |
A PIL Image object with the LUT applied. | |
""" | |
img = open_image(image_path) | |
# Handle specific file formats by converting to appropriate modes | |
if image_path.lower().endswith(('.gif', '.webp')): | |
# Convert to RGBA to preserve transparency | |
img = img.convert('RGBA') | |
elif image_path.lower().endswith(('.jpg', '.jpeg')): | |
# Convert to RGB since JPEG doesn't support transparency | |
img = convert_rgb_to_rgba_safe(img) | |
# For other formats like PNG, retain the existing mode | |
# Apply the LUT if provided | |
if lut_filename is not None: | |
try: | |
img = apply_lut(img, lut_filename) | |
except Exception as e: | |
print(f"BAD LUT: Error applying LUT {str(e)}.") | |
return img | |
def convert_to_rgba_png(file_path: str) -> None: | |
""" | |
Converts an image to RGBA PNG format and saves it with the same base name and a .png extension. | |
Args: | |
file_path (str): The path to the input image file. | |
Raises: | |
ValueError: If the input file extension is not a supported image format. | |
Exception: If there is an error during the conversion or saving process. | |
""" | |
try: | |
# Open the original image | |
img = open_image(file_path) | |
# Convert the image to RGBA | |
rgba_img = convert_rgb_to_rgba_safe(img) | |
# Generate the new file name with .png extension | |
base_name = os.path.splitext(file_path)[0] | |
new_file_path = f"{base_name}.png" | |
# Save the RGBA image as PNG | |
rgba_img.save(new_file_path, format='PNG') | |
print(f"Image saved as {new_file_path}") | |
except ValueError as ve: | |
print(f"ValueError: {ve}") | |
except Exception as e: | |
print(f"Error converting image: {e}") |