diff --git a/iz_helpers/InfZoomConfig.py b/iz_helpers/InfZoomConfig.py index ab0e0bd..549cf6c 100644 --- a/iz_helpers/InfZoomConfig.py +++ b/iz_helpers/InfZoomConfig.py @@ -34,6 +34,7 @@ class InfZoomConfig(): blend_gradient_size:int blend_invert_do:bool blend_color:str + lut_filename:str=None audio_filename:str=None audio_volume:float = 1 inpainting_denoising_strength:float=1 diff --git a/iz_helpers/image.py b/iz_helpers/image.py index f67a7ca..cc77ece 100644 --- a/iz_helpers/image.py +++ b/iz_helpers/image.py @@ -5,8 +5,10 @@ import base64 import numpy as np import math from io import BytesIO +import os from modules.processing import apply_overlay, slerp from timeit import default_timer as timer +from typing import List, Union def shrink_and_paste_on_blank(current_image, mask_width, mask_height, blank_color:tuple[int, int, int, int] = (0,0,0,0)): @@ -91,6 +93,8 @@ def convert_to_rgba(images): print(f"rgb convert:{end - start}") return rgba_images +######################################################## lerp ######################################################## + def lerp(value1, value2, factor): """ Linearly interpolate between value1 and value2 by factor. @@ -195,6 +199,77 @@ def lerp_imagemath_RGBA(img1, img2, alphaimg, factor:int = 50): def CMYKInvert(img) : return Image.merge(img.mode, [ImageOps.invert(b.convert('L')) for b in img.split()]) +##################################################### LUTs ############################################################ + +def is_3dlut_row(row: List[str]) -> bool: + """ + Check if one line in the file has exactly 3 numeric values. + + Args: + row: A list of strings representing the values in a row. + + Returns: + 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) + +##################################################### Masks ############################################################ + def combine_masks(mask:Image, altmask:Image, width:int, height:int): """ Combine two masks using lighter color @@ -208,6 +283,8 @@ def combine_masks(mask:Image, altmask:Image, width:int, height:int): result = ImageChops.lighter(mask, altmask) return result +##################################################### Gradients ######################################################### + 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. @@ -668,6 +745,8 @@ def multiply_alpha(image, factor): print(f"multiply_alpha:{end - start}") return result_image +#################################################################### Blends and Wipes #################################################################### + 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. diff --git a/iz_helpers/run.py b/iz_helpers/run.py index 2cbff0d..acf828e 100644 --- a/iz_helpers/run.py +++ b/iz_helpers/run.py @@ -15,7 +15,17 @@ from .helpers import ( do_upscaleImg,value_to_bool, find_ffmpeg_binary ) from .sd_helpers import renderImg2Img, renderTxt2Img -from .image import multiply_alpha, shrink_and_paste_on_blank, open_image, apply_alpha_mask, draw_gradient_ellipse, resize_and_crop_image, crop_fethear_ellipse, crop_inner_image, combine_masks +from .image import ( + multiply_alpha, + shrink_and_paste_on_blank, + open_image, + apply_alpha_mask, + draw_gradient_ellipse, + resize_and_crop_image, + crop_fethear_ellipse, + crop_inner_image, + apply_lut, + read_lut) from .video import write_video, add_audio_to_video, ContinuousVideoWriter from .InfZoomConfig import InfZoomConfig @@ -117,6 +127,13 @@ class InfZoomer: processed = self.fnOutpaintMainFrames() + if self.C.lut_filename is not None: + try: + #processed = apply_lut(processed, self.C.lut_filename) + self.main_frames = [apply_lut(frame, self.C.lut_filename) for frame in self.main_frames] + except Exception as e: + input(f"Skip LUT: Error applying LUT {str(e)}. Enter to continue...") + #trim frames that are blended or luma wiped self.start_frames = self.main_frames[:2] self.end_frames = self.main_frames[(len(self.main_frames) - 2):] @@ -231,6 +248,7 @@ class InfZoomer: def outpaint_steps_cornerStrategy(self): current_image = self.main_frames[-1] + exit_img = None # just 30 radius to get inpaint connected between outer and innter motive masked_image = create_mask_with_circles( diff --git a/iz_helpers/run_interface.py b/iz_helpers/run_interface.py index 91ecec0..66b8460 100644 --- a/iz_helpers/run_interface.py +++ b/iz_helpers/run_interface.py @@ -37,6 +37,7 @@ def createZoom( blend_gradient_size:int, blend_invert_do:bool, blend_color:str, + lut_filename:str=None, audio_filename:str = None, audio_volume:float = 1, inpainting_denoising_strength:float=1, @@ -77,6 +78,7 @@ def createZoom( blend_gradient_size, blend_invert_do, blend_color, + lut_filename, audio_filename, audio_volume, inpainting_denoising_strength, diff --git a/iz_helpers/ui.py b/iz_helpers/ui.py index c489067..35a6d3a 100644 --- a/iz_helpers/ui.py +++ b/iz_helpers/ui.py @@ -241,6 +241,18 @@ Free to use grayscale blend images can be found here: https://github.com/Oncorpo Ideas for custom blend images: https://www.pexels.com/search/gradient/ """ ) + with gr.Row(): + lut_filename = gr.Textbox( + value=None, + label="Look Up Table (LUT) File Name", + elem_id="infzoom_lutFileName") + lut_file = gr.File( + value=None, + file_count="single", + file_types=[".cube"], + type="file", + label="LUT cube File") + lut_file.change(get_filename, inputs=[lut_file], outputs=[lut_filename]) with gr.Tab("Audio"): with gr.Row(): @@ -447,6 +459,7 @@ Our best experience and trade-off is the R-ERSGAn4x upscaler. blend_gradient_size, blend_invert_do, blend_color, + lut_filename, audio_filename, audio_volume, ], @@ -469,7 +482,10 @@ def checkPrompts(p): ) def get_filename(file): - return file.name + filename = None + if file is not None: + filename = file.name + return filename def get_min_outpaint_amount(width, outpaint_amount, strategy): #automatically sets the minimum outpaint amount based on the width for Center strategy