'''Image process functions for ComfyUI nodes by chflame https://github.com/chflame163 ''' import os import sys sys.path.append(os.path.dirname(os.path.abspath(__file__))) import pickle import copy import re import json import math import glob import numpy as np import torch import scipy.ndimage import cv2 import random import time from tqdm import tqdm from functools import lru_cache from typing import Union, List from PIL import Image, ImageFilter, ImageChops, ImageDraw, ImageOps, ImageEnhance, ImageFont from skimage import img_as_float, img_as_ubyte import torchvision.transforms.functional as TF import torch.nn.functional as F import colorsys from typing import Union import folder_paths from .briarmbg import BriaRMBG from .filmgrainer import processing as processing_utils from .filmgrainer import filmgrainer as filmgrainer import wget from .blendmodes import * def log(message:str, message_type:str='info'): name = 'LayerStyle' if message_type == 'error': message = '\033[1;41m' + message + '\033[m' elif message_type == 'warning': message = '\033[1;31m' + message + '\033[m' elif message_type == 'finish': message = '\033[1;32m' + message + '\033[m' else: message = '\033[1;33m' + message + '\033[m' print(f"# 😺dzNodes: {name} -> {message}") try: from cv2.ximgproc import guidedFilter except ImportError as e: # print(e) log(f"Cannot import name 'guidedFilter' from 'cv2.ximgproc'" f"\nA few nodes cannot works properly, while most nodes are not affected. Please REINSTALL package 'opencv-contrib-python'." f"\nFor detail refer to \033[4mhttps://github.com/chflame163/ComfyUI_LayerStyle/issues/5\033[0m") '''warpper''' # create a wrapper function that can apply a function to multiple images in a batch while passing all other arguments to the function def apply_to_batch(func): def wrapper(self, image, *args, **kwargs): images = [] for img in image: images.append(func(self, img, *args, **kwargs)) batch_tensor = torch.cat(images, dim=0) return (batch_tensor,) return wrapper '''pickle''' def read_image(filename:str) -> Image: return Image.open(filename) def pickle_to_file(obj:object, file_path:str): with open(file_path, 'wb') as f: pickle.dump(obj, f) def load_pickle(file_name:str) -> object: with open(file_name, 'rb') as f: obj = pickle.load(f) return obj def load_light_leak_images() -> list: file = os.path.join(folder_paths.models_dir, "layerstyle", "light_leak.pkl") return load_pickle(file) '''Converter''' def cv22ski(cv2_image:np.ndarray) -> np.array: return img_as_float(cv2_image) def ski2cv2(ski:np.array) -> np.ndarray: return img_as_ubyte(ski) def cv22pil(cv2_img:np.ndarray) -> Image: cv2_img = cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB) return Image.fromarray(cv2_img) def pil2cv2(pil_img:Image) -> np.array: np_img_array = np.asarray(pil_img) return cv2.cvtColor(np_img_array, cv2.COLOR_RGB2BGR) def pil2tensor(image:Image) -> torch.Tensor: return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) def np2pil(np_image:np.ndarray) -> Image: return Image.fromarray(np_image) def pil2np(pil_image:Image) -> np.array: return np.ndarray(pil_image) def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor: if isinstance(img_np, list): return torch.cat([np2tensor(img) for img in img_np], dim=0) return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0) def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]: if len(tensor.shape) == 3: # Single image return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8) else: # Batch of images return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor] def tensor2pil(t_image: torch.Tensor) -> Image: return Image.fromarray(np.clip(255.0 * t_image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) def tensor2cv2(image:torch.Tensor) -> np.array: if image.dim() == 4: image = image.squeeze() npimage = image.numpy() cv2image = np.uint8(npimage * 255 / npimage.max()) return cv2.cvtColor(cv2image, cv2.COLOR_RGB2BGR) def image2mask(image:Image) -> torch.Tensor: _image = image.convert('RGBA') alpha = _image.split() [0] bg = Image.new("L", _image.size) _image = Image.merge('RGBA', (bg, bg, bg, alpha)) ret_mask = torch.tensor([pil2tensor(_image)[0, :, :, 3].tolist()]) return ret_mask def mask2image(mask:torch.Tensor) -> Image: masks = tensor2np(mask) for m in masks: _mask = Image.fromarray(m).convert("L") _image = Image.new("RGBA", _mask.size, color='white') _image = Image.composite( _image, Image.new("RGBA", _mask.size, color='black'), _mask) return _image '''Image Functions''' # 颜色加深 def blend_color_burn(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) img = 1 - (1 - img_2) / (img_1 + 0.001) mask_1 = img < 0 mask_2 = img > 1 img = img * (1 - mask_1) img = img * (1 - mask_2) + mask_2 return cv22pil(ski2cv2(img)) # 颜色减淡 def blend_color_dodge(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) img = img_2 / (1.0 - img_1 + 0.001) mask_2 = img > 1 img = img * (1 - mask_2) + mask_2 return cv22pil(ski2cv2(img)) # 线性加深 def blend_linear_burn(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) img = img_1 + img_2 - 1 mask_1 = img < 0 img = img * (1 - mask_1) return cv22pil(ski2cv2(img)) # 线性减淡 def blend_linear_dodge(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) img = img_1 + img_2 mask_2 = img > 1 img = img * (1 - mask_2) + mask_2 return cv22pil(ski2cv2(img)) # 变亮 def blend_lighten(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) img = img_1 - img_2 mask = img > 0 img = img_1 * mask + img_2 * (1 - mask) return cv22pil(ski2cv2(img)) # 变暗 def blend_dark(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) img = img_1 - img_2 mask = img < 0 img = img_1 * mask + img_2 * (1 - mask) return cv22pil(ski2cv2(img)) # 滤色 def blend_screen(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) img = 1 - (1 - img_1) * (1 - img_2) return cv22pil(ski2cv2(img)) # 叠加 def blend_overlay(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) mask = img_2 < 0.5 img = 2 * img_1 * img_2 * mask + (1 - mask) * (1 - 2 * (1 - img_1) * (1 - img_2)) return cv22pil(ski2cv2(img)) # 柔光 def blend_soft_light(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) mask = img_1 < 0.5 T1 = (2 * img_1 - 1) * (img_2 - img_2 * img_2) + img_2 T2 = (2 * img_1 - 1) * (np.sqrt(img_2) - img_2) + img_2 img = T1 * mask + T2 * (1 - mask) return cv22pil(ski2cv2(img)) # 强光 def blend_hard_light(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) mask = img_1 < 0.5 T1 = 2 * img_1 * img_2 T2 = 1 - 2 * (1 - img_1) * (1 - img_2) img = T1 * mask + T2 * (1 - mask) return cv22pil(ski2cv2(img)) # 亮光 def blend_vivid_light(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) mask = img_1 < 0.5 T1 = 1 - (1 - img_2) / (2 * img_1 + 0.001) T2 = img_2 / (2 * (1 - img_1) + 0.001) mask_1 = T1 < 0 mask_2 = T2 > 1 T1 = T1 * (1 - mask_1) T2 = T2 * (1 - mask_2) + mask_2 img = T1 * mask + T2 * (1 - mask) return cv22pil(ski2cv2(img)) # 点光 def blend_pin_light(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) mask_1 = img_2 < (img_1 * 2 - 1) mask_2 = img_2 > 2 * img_1 T1 = 2 * img_1 - 1 T2 = img_2 T3 = 2 * img_1 img = T1 * mask_1 + T2 * (1 - mask_1) * (1 - mask_2) + T3 * mask_2 return cv22pil(ski2cv2(img)) # 线性光 def blend_linear_light(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) img = img_2 + img_1 * 2 - 1 mask_1 = img < 0 mask_2 = img > 1 img = img * (1 - mask_1) img = img * (1 - mask_2) + mask_2 return cv22pil(ski2cv2(img)) def blend_hard_mix(background_image:Image, layer_image:Image) -> Image: img_1 = cv22ski(pil2cv2(background_image)) img_2 = cv22ski(pil2cv2(layer_image)) img = img_1 + img_2 mask = img_1 + img_2 > 1 img = img * (1 - mask) + mask img = img * mask return cv22pil(ski2cv2(img)) def shift_image(image:Image, distance_x:int, distance_y:int, background_color:str='#000000', cyclic:bool=False) -> Image: width = image.width height = image.height ret_image = Image.new('RGB', size=(width, height), color=background_color) for x in range(width): for y in range(height): if cyclic: orig_x = x + distance_x if orig_x > width-1 or orig_x < 0: orig_x = abs(orig_x % width) orig_y = y + distance_y if orig_y > height-1 or orig_y < 0: orig_y = abs(orig_y % height) pixel = image.getpixel((orig_x, orig_y)) ret_image.putpixel((x, y), pixel) else: if x > -distance_x and y > -distance_y: # 防止回转 if x + distance_x < width and y + distance_y < height: # 防止越界 pixel = image.getpixel((x + distance_x, y + distance_y)) ret_image.putpixel((x, y), pixel) return ret_image def chop_image(background_image:Image, layer_image:Image, blend_mode:str, opacity:int) -> Image: ret_image = background_image if blend_mode == 'normal': ret_image = copy.deepcopy(layer_image) if blend_mode == 'multply': ret_image = ImageChops.multiply(background_image,layer_image) if blend_mode == 'screen': ret_image = ImageChops.screen(background_image, layer_image) if blend_mode == 'add': ret_image = ImageChops.add(background_image, layer_image, 1, 0) if blend_mode == 'subtract': ret_image = ImageChops.subtract(background_image, layer_image, 1, 0) if blend_mode == 'difference': ret_image = ImageChops.difference(background_image, layer_image) if blend_mode == 'darker': ret_image = ImageChops.darker(background_image, layer_image) if blend_mode == 'lighter': ret_image = ImageChops.lighter(background_image, layer_image) if blend_mode == 'color_burn': ret_image = blend_color_burn(background_image, layer_image) if blend_mode == 'color_dodge': ret_image = blend_color_dodge(background_image, layer_image) if blend_mode == 'linear_burn': ret_image = blend_linear_burn(background_image, layer_image) if blend_mode == 'linear_dodge': ret_image = blend_linear_dodge(background_image, layer_image) if blend_mode == 'overlay': ret_image = blend_overlay(background_image, layer_image) if blend_mode == 'soft_light': ret_image = blend_soft_light(background_image, layer_image) if blend_mode == 'hard_light': ret_image = blend_hard_light(background_image, layer_image) if blend_mode == 'vivid_light': ret_image = blend_vivid_light(background_image, layer_image) if blend_mode == 'pin_light': ret_image = blend_pin_light(background_image, layer_image) if blend_mode == 'linear_light': ret_image = blend_linear_light(background_image, layer_image) if blend_mode == 'hard_mix': ret_image = blend_hard_mix(background_image, layer_image) # opacity if opacity == 0: ret_image = background_image elif opacity < 100: alpha = 1.0 - float(opacity) / 100 ret_image = Image.blend(ret_image, background_image, alpha) return ret_image def chop_image_v2(background_image:Image, layer_image:Image, blend_mode:str, opacity:int) -> Image: backdrop_prepped = np.asfarray(background_image.convert('RGBA')) source_prepped = np.asfarray(layer_image.convert('RGBA')) blended_np = BLEND_MODES[blend_mode](backdrop_prepped, source_prepped, opacity / 100) # final_tensor = (torch.from_numpy(blended_np / 255)).unsqueeze(0) # return tensor2pil(_tensor) return Image.fromarray(np.uint8(blended_np)) def remove_background(image:Image, mask:Image, color:str) -> Image: width = image.width height = image.height ret_image = Image.new('RGB', size=(width, height), color=color) ret_image.paste(image, mask=mask) return ret_image def sharpen(image:Image) -> Image: img = pil2cv2(image) Laplace_kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]], dtype=np.float32) ret_image = cv2.filter2D(img, -1, Laplace_kernel) return cv22pil(ret_image) def gaussian_blur(image:Image, radius:int) -> Image: # image = image.convert("RGBA") ret_image = image.filter(ImageFilter.GaussianBlur(radius=radius)) return ret_image def motion_blur(image:Image, angle:int, blur:int) -> Image: angle += 45 blur *= 5 image = np.array(pil2cv2(image)) M = cv2.getRotationMatrix2D((blur / 2, blur / 2), angle, 1) motion_blur_kernel = np.diag(np.ones(blur)) motion_blur_kernel = cv2.warpAffine(motion_blur_kernel, M, (blur, blur)) motion_blur_kernel = motion_blur_kernel / blur blurred = cv2.filter2D(image, -1, motion_blur_kernel) # convert to uint8 cv2.normalize(blurred, blurred, 0, 255, cv2.NORM_MINMAX) blurred = np.array(blurred, dtype=np.uint8) ret_image = cv22pil(blurred) return ret_image def __apply_vignette(image, vignette): # If image needs to be normalized (0-1 range) needs_normalization = image.max() > 1 if needs_normalization: image = image.astype(np.float32) / 255 final_image = np.clip(image * vignette[..., np.newaxis], 0, 1) if needs_normalization: final_image = (final_image * 255).astype(np.uint8) return final_image def vignette_image(image:Image, intensity: float, center_x: float, center_y: float) -> Image: image = pil2tensor(image) _, height, width, _ = image.shape # Generate the vignette for each image in the batch # Create linear space but centered around the provided center point ratios x = np.linspace(-1, 1, width) y = np.linspace(-1, 1, height) X, Y = np.meshgrid(x - (2 * center_x - 1), y - (2 * center_y - 1)) # Calculate distances to the furthest corner distances_to_corners = [ np.sqrt((0 - center_x) ** 2 + (0 - center_y) ** 2), np.sqrt((1 - center_x) ** 2 + (0 - center_y) ** 2), np.sqrt((0 - center_x) ** 2 + (1 - center_y) ** 2), np.sqrt((1 - center_x) ** 2 + (1 - center_y) ** 2) ] max_distance_to_corner = np.max(distances_to_corners) radius = np.sqrt(X ** 2 + Y ** 2) radius = radius / (max_distance_to_corner * np.sqrt(2)) # Normalize radius opacity = np.clip(intensity, 0, 1) vignette = 1 - radius * opacity tensor_image = image.numpy() # Apply vignette vignette_image = __apply_vignette(tensor_image, vignette) return tensor2pil(torch.from_numpy(vignette_image).unsqueeze(0)) def filmgrain_image(image:Image, scale:float, grain_power:float, shadows:float, highs:float, grain_sat:float, sharpen:int=1, grain_type:int=4, src_gamma:float=1.0, gray_scale:bool=False, seed:int=0) -> Image: # image = pil2tensor(image) # grain_type, 1=fine, 2=fine simple, 3=coarse, 4=coarser grain_type_index = 3 # Apply grain grain_image = filmgrainer.process(image, scale=scale, src_gamma=src_gamma, grain_power=grain_power, shadows=shadows, highs=highs, grain_type=grain_type_index, grain_sat=grain_sat, gray_scale=gray_scale, sharpen=sharpen, seed=seed) return tensor2pil(torch.from_numpy(grain_image).unsqueeze(0)) def __apply_radialblur(image, blur_strength, radial_mask, focus_spread, steps): needs_normalization = image.max() > 1 if needs_normalization: image = image.astype(np.float32) / 255 blurred_images = processing_utils.generate_blurred_images(image, blur_strength, steps, focus_spread) final_image = processing_utils.apply_blurred_images(image, blurred_images, radial_mask) if needs_normalization: final_image = np.clip(final_image * 255, 0, 255).astype(np.uint8) return final_image def radialblur_image(image:Image, blur_strength:float, center_x:float, center_y:float, focus_spread:float, steps:int=5) -> Image: width, height = image.size image = pil2tensor(image) if image.dim() == 4: image = image[0] # _, height, width, = image.shape # Generate the vignette for each image in the batch c_x, c_y = int(width * center_x), int(height * center_y) # Calculate distances to all corners from the center distances_to_corners = [ np.sqrt((c_x - 0)**2 + (c_y - 0)**2), np.sqrt((c_x - width)**2 + (c_y - 0)**2), np.sqrt((c_x - 0)**2 + (c_y - height)**2), np.sqrt((c_x - width)**2 + (c_y - height)**2) ] max_distance_to_corner = max(distances_to_corners) # Create and adjust radial mask X, Y = np.meshgrid(np.arange(width) - c_x, np.arange(height) - c_y) radial_mask = np.sqrt(X**2 + Y**2) / max_distance_to_corner tensor_image = image.numpy() # Apply blur blur_image = __apply_radialblur(tensor_image, blur_strength, radial_mask, focus_spread, steps) return tensor2pil(torch.from_numpy(blur_image).unsqueeze(0)) def __apply_depthblur(image, depth_map, blur_strength, focal_depth, focus_spread, steps): # Normalize the input image if needed needs_normalization = image.max() > 1 if needs_normalization: image = image.astype(np.float32) / 255 # Normalize the depth map if needed depth_map = depth_map.astype(np.float32) / 255 if depth_map.max() > 1 else depth_map # Resize depth map to match the image dimensions depth_map_resized = cv2.resize(depth_map, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_LINEAR) if len(depth_map_resized.shape) > 2: depth_map_resized = cv2.cvtColor(depth_map_resized, cv2.COLOR_BGR2GRAY) # Adjust the depth map based on the focal plane depth_mask = np.abs(depth_map_resized - focal_depth) depth_mask = np.clip(depth_mask / np.max(depth_mask), 0, 1) # Generate blurred versions of the image blurred_images = processing_utils.generate_blurred_images(image, blur_strength, steps, focus_spread) # Use the adjusted depth map as a mask for applying blurred images final_image = processing_utils.apply_blurred_images(image, blurred_images, depth_mask) # Convert back to original range if the image was normalized if needs_normalization: final_image = np.clip(final_image * 255, 0, 255).astype(np.uint8) return final_image def depthblur_image(image:Image, depth_map:Image, blur_strength:float, focal_depth:float, focus_spread:float, steps:int=5) -> Image: width, height = image.size image = pil2tensor(image) depth_map = pil2tensor(depth_map) if image.dim() == 4: image = image[0] if depth_map.dim() == 4: depth_map = depth_map[0] tensor_image = image.numpy() tensor_image_depth = depth_map.numpy() # Apply blur blur_image = __apply_depthblur(tensor_image, tensor_image_depth, blur_strength, focal_depth, focus_spread, steps) return tensor2pil(torch.from_numpy(blur_image).unsqueeze(0)) def fit_resize_image(image:Image, target_width:int, target_height:int, fit:str, resize_sampler:str, background_color:str = '#000000') -> Image: image = image.convert('RGB') orig_width, orig_height = image.size if image is not None: if fit == 'letterbox': if orig_width / orig_height > target_width / target_height: # 更宽,上下留黑 fit_width = target_width fit_height = int(target_width / orig_width * orig_height) else: # 更瘦,左右留黑 fit_height = target_height fit_width = int(target_height / orig_height * orig_width) fit_image = image.resize((fit_width, fit_height), resize_sampler) ret_image = Image.new('RGB', size=(target_width, target_height), color=background_color) ret_image.paste(fit_image, box=((target_width - fit_width)//2, (target_height - fit_height)//2)) elif fit == 'crop': if orig_width / orig_height > target_width / target_height: # 更宽,裁左右 fit_width = int(orig_height * target_width / target_height) fit_image = image.crop( ((orig_width - fit_width)//2, 0, (orig_width - fit_width)//2 + fit_width, orig_height)) else: # 更瘦,裁上下 fit_height = int(orig_width * target_height / target_width) fit_image = image.crop( (0, (orig_height-fit_height)//2, orig_width, (orig_height-fit_height)//2 + fit_height)) ret_image = fit_image.resize((target_width, target_height), resize_sampler) else: ret_image = image.resize((target_width, target_height), resize_sampler) return ret_image def __rotate_expand(image:Image, angle:float, SSAA:int=0, method:str="lanczos") -> Image: images = pil2tensor(image) expand = "true" height, width = images[0, :, :, 0].shape def rotate_tensor(tensor): resize_sampler = Image.LANCZOS rotate_sampler = Image.BICUBIC if method == "bicubic": resize_sampler = Image.BICUBIC rotate_sampler = Image.BICUBIC elif method == "hamming": resize_sampler = Image.HAMMING rotate_sampler = Image.BILINEAR elif method == "bilinear": resize_sampler = Image.BILINEAR rotate_sampler = Image.BILINEAR elif method == "box": resize_sampler = Image.BOX rotate_sampler = Image.NEAREST elif method == "nearest": resize_sampler = Image.NEAREST rotate_sampler = Image.NEAREST img = tensor2pil(tensor) if SSAA > 1: img_us_scaled = img.resize((width * SSAA, height * SSAA), resize_sampler) img_rotated = img_us_scaled.rotate(angle, rotate_sampler, expand == "true", fillcolor=(0, 0, 0, 0)) img_down_scaled = img_rotated.resize((img_rotated.width // SSAA, img_rotated.height // SSAA), resize_sampler) result = pil2tensor(img_down_scaled) else: img_rotated = img.rotate(angle, rotate_sampler, expand == "true", fillcolor=(0, 0, 0, 0)) result = pil2tensor(img_rotated) return result if angle == 0.0 or angle == 360.0: return tensor2pil(images) else: rotated_tensor = torch.stack([rotate_tensor(images[i]) for i in range(len(images))]) return tensor2pil(rotated_tensor).convert('RGB') def image_rotate_extend_with_alpha(image:Image, angle:float, alpha:Image=None, method:str="lanczos", SSAA:int=0) -> tuple: _image = __rotate_expand(image.convert('RGB'), angle, SSAA, method) if angle is not None: _alpha = __rotate_expand(alpha.convert('RGB'), angle, SSAA, method) ret_image = RGB2RGBA(_image, _alpha) else: ret_image = _image return (_image, _alpha, ret_image) def create_box_gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int, scale:int=50) -> Image: # scale is percent of border to center for the rectangle if scale > 100: scale = 100 elif scale < 1: scale = 1 start_color = Hex_to_RGB(start_color_inhex) end_color = Hex_to_RGB(end_color_inhex) ret_image = Image.new("RGB", (width, height), start_color) draw = ImageDraw.Draw(ret_image) step = int(min(width, height) * scale / 100 / 2) if step > 0: for i in range(step): R = int(start_color[0] * (step - i) / step + end_color[0] * i / step) G = int(start_color[1] * (step - i) / step + end_color[1] * i / step) B = int(start_color[2] * (step - i) / step + end_color[2] * i / step) color = (R, G, B) log(f"step={step},i={i}, color={color}") draw.rectangle((i, i, width - i, height - i), fill=color) draw.rectangle((step, step, width - step, height - step), fill=end_color) return ret_image def create_gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int, direction:str='bottom') -> Image: # direction = one of top, bottom, left, right start_color = Hex_to_RGB(start_color_inhex) end_color = Hex_to_RGB(end_color_inhex) ret_image = Image.new("RGB", (width, height), start_color) draw = ImageDraw.Draw(ret_image) if direction == 'bottom': for i in range(height): R = int(start_color[0] * (height - i) / height + end_color[0] * i / height) G = int(start_color[1] * (height - i) / height + end_color[1] * i / height) B = int(start_color[2] * (height - i) / height + end_color[2] * i / height) color = (R, G, B) draw.line((0, i, width, i), fill=color) elif direction == 'top': for i in range(height): R = int(end_color[0] * (height - i) / height + start_color[0] * i / height) G = int(end_color[1] * (height - i) / height + start_color[1] * i / height) B = int(end_color[2] * (height - i) / height + start_color[2] * i / height) color = (R, G, B) draw.line((0, i, width, i), fill=color) elif direction == 'right': for i in range(width): R = int(start_color[0] * (width - i) / width + end_color[0] * i / width) G = int(start_color[1] * (width - i) / width + end_color[1] * i / width) B = int(start_color[2] * (width - i) / width + end_color[2] * i / width) color = (R, G, B) draw.line((i, 0, i, height), fill=color) elif direction == 'left': for i in range(width): R = int(end_color[0] * (width - i) / width + start_color[0] * i / width) G = int(end_color[1] * (width - i) / width + start_color[1] * i / width) B = int(end_color[2] * (width - i) / width + start_color[2] * i / width) color = (R, G, B) draw.line((i, 0, i, height), fill=color) else: log(f'A argument error of imagefunc.create_gradient(), ' f'"direction=" must one of "top, bottom, left, right".', message_type='error') return ret_image def gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int, angle:float, ) -> Image: radius = int((width + height) / 4) g = create_gradient(start_color_inhex, end_color_inhex, radius, radius) _canvas = Image.new('RGB', size=(radius, radius*3), color=start_color_inhex) top = Image.new('RGB', size=(radius, radius), color=start_color_inhex) bottom = Image.new('RGB', size=(radius, radius),color=end_color_inhex) _canvas.paste(top, box=(0, 0, radius, radius)) _canvas.paste(g, box=(0, radius, radius, radius * 2)) _canvas.paste(bottom,box=(0, radius * 2, radius, radius * 3)) _canvas = _canvas.resize((radius * 3, radius * 3)) _canvas = __rotate_expand(_canvas,angle) center = int(_canvas.width / 2) _x = int(width / 3) _y = int(height / 3) ret_image = _canvas.crop((center - _x, center - _y, center + _x, center + _y)) ret_image = ret_image.resize((width, height)) return ret_image def draw_rect(image:Image, x:int, y:int, width:int, height:int, line_color:str, line_width:int, box_color:str=None) -> Image: draw = ImageDraw.Draw(image) draw.rectangle((x, y, x + width, y + height), fill=box_color, outline=line_color, width=line_width, ) return image def draw_border(image:Image, border_width:int, color:str='#FFFFFF') -> Image: return ImageOps.expand(image, border=border_width, fill=color) def histogram_equalization(image:Image, mask:Image=None, gamma_strength=0.5) -> Image: def remap_pixel(pixel, min_brightness, max_brightness): return int((pixel - min_brightness) / (max_brightness - min_brightness) * 255) if image.mode != 'L': image = image.convert('L') if mask is not None: if mask.mode != 'L': mask = mask.convert('L') else: mask = Image.new('L', size=image.size, color = 'white') # calculate Min/Max brightness pixel min_brightness = 255 max_brightness = 0 average_brightness = 0 total_pixel = 0 for y in range(image.height): for x in range(image.width): if mask.getpixel((x, y)) == 0: continue else: pixel = image.getpixel((x, y)) if pixel < min_brightness: min_brightness = pixel if pixel > max_brightness: max_brightness = pixel average_brightness += pixel total_pixel += 1 average_brightness = int(average_brightness / total_pixel) for y in range(image.height): for x in range(image.width): pixel = image.getpixel((x, y)) image.putpixel((x, y), remap_pixel(pixel, min_brightness, max_brightness)) image = gamma_trans(image, (average_brightness - 127) / 127 * gamma_strength * 0.66 + 1) return image.convert('L') def get_image_color_tone(image:Image) -> str: image = image.convert('RGB') max_score = 0.0001 dominant_color = (255, 255, 255) for count, (r, g, b) in image.getcolors(image.width * image.height): saturation = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)[1] y = min(abs(r * 2104 + g * 4130 + b * 802 + 4096 + 131072) >> 13,235) y = (y - 16.0) / (235 - 16) score = (saturation+0.1)*count if score > max_score: max_score = score dominant_color = (r, g, b) ret_color = RGB_to_Hex(dominant_color) return ret_color def get_image_color_average(image:Image) -> str: image = image.convert('RGB') width, height = image.size total_red = 0 total_green = 0 total_blue = 0 for y in range(height): for x in range(width): rgb = image.getpixel((x, y)) total_red += rgb[0] total_green += rgb[1] total_blue += rgb[2] average_red = total_red // (width * height) average_green = total_green // (width * height) average_blue = total_blue // (width * height) color = (average_red, average_green, average_blue) ret_color = RGB_to_Hex(color) return ret_color def get_gray_average(image:Image, mask:Image=None) -> int: # image.mode = 'HSV', mask.mode = 'L' image = image.convert('HSV') _, _, _v = image.convert('HSV').split() if mask is not None: if mask.mode != 'L': mask = mask.convert('L') width, height = image.size total_gray = 0 valid_pixels = 0 for y in range(height): for x in range(width): if mask is not None: if mask.getpixel((x, y)) > 16: #mask亮度低于16的忽略不计 gray = _v.getpixel((x, y)) total_gray += gray valid_pixels += 1 else: gray = _v.getpixel((x, y)) total_gray += gray valid_pixels += 1 average_gray = total_gray // valid_pixels return average_gray def calculate_shadow_highlight_level(gray:int) -> float: range = 255 shadow_exponent = 3 highlight_exponent = 2 shadow_ratio = gray ** shadow_exponent / range ** shadow_exponent highlight_ratio = gray ** highlight_exponent / range ** highlight_exponent shadow_level = shadow_ratio * 100 + (1 - shadow_ratio) * 32 highlight_level = highlight_ratio * 100 + (1 - highlight_ratio) * 32 return shadow_level, highlight_level def luminance_keyer(image:Image, low:float=0, high:float=1, gamma:float=1) -> Image: image = pil2tensor(image) t = image[:, :, :, :3].detach().clone() alpha = 0.2126 * t[:, :, :, 0] + 0.7152 * t[:, :, :, 1] + 0.0722 * t[:, :, :, 2] if low == high: alpha = (alpha > high).to(t.dtype) else: alpha = (alpha - low) / (high - low) if gamma != 1.0: alpha = torch.pow(alpha, 1 / gamma) alpha = torch.clamp(alpha, min=0, max=1).unsqueeze(3).repeat(1, 1, 1, 3) return tensor2pil(alpha).convert('L') def get_image_bright_average(image:Image) -> int: image = image.convert('L') width, height = image.size total_bright = 0 pixels = 0 for y in range(height): for x in range(width): b = image.getpixel((x, y)) if b > 1: # 排除死黑 pixels += 1 total_bright += b return int(total_bright / pixels) def image_channel_split(image:Image, mode = 'RGBA') -> tuple: _image = image.convert('RGBA') channel1 = Image.new('L', size=_image.size, color='black') channel2 = Image.new('L', size=_image.size, color='black') channel3 = Image.new('L', size=_image.size, color='black') channel4 = Image.new('L', size=_image.size, color='black') if mode == 'RGBA': channel1, channel2, channel3, channel4 = _image.split() if mode == 'RGB': channel1, channel2, channel3 = _image.convert('RGB').split() if mode == 'YCbCr': channel1, channel2, channel3 = _image.convert('YCbCr').split() if mode == 'LAB': channel1, channel2, channel3 = _image.convert('LAB').split() if mode == 'HSV': channel1, channel2, channel3 = _image.convert('HSV').split() return channel1, channel2, channel3, channel4 def image_channel_merge(channels:tuple, mode = 'RGB' ) -> Image: channel1 = channels[0].convert('L') channel2 = channels[1].convert('L') channel3 = channels[2].convert('L') channel4 = Image.new('L', size=channel1.size, color='white') if mode == 'RGBA': if len(channels) > 3: channel4 = channels[3].convert('L') ret_image = Image.merge('RGBA',[channel1, channel2, channel3, channel4]) elif mode == 'RGB': ret_image = Image.merge('RGB', [channel1, channel2, channel3]) elif mode == 'YCbCr': ret_image = Image.merge('YCbCr', [channel1, channel2, channel3]).convert('RGB') elif mode == 'LAB': ret_image = Image.merge('LAB', [channel1, channel2, channel3]).convert('RGB') elif mode == 'HSV': ret_image = Image.merge('HSV', [channel1, channel2, channel3]).convert('RGB') return ret_image def image_gray_offset(image:Image, offset:int) -> Image: image = image.convert('L') width = image.width height = image.height ret_image = Image.new('L', size=(width, height), color='black') for x in range(width): for y in range(height): pixel = image.getpixel((x, y)) _pixel = pixel + offset if _pixel > 255: _pixel = 255 if _pixel < 0: _pixel = 0 ret_image.putpixel((x, y), _pixel) return ret_image def image_gray_ratio(image:Image, ratio:float) -> Image: image = image.convert('L') width = image.width height = image.height ret_image = Image.new('L', size=(width, height), color='black') for x in range(width): for y in range(height): pixel = image.getpixel((x, y)) _pixel = int(pixel * ratio) ret_image.putpixel((x, y), _pixel) return ret_image def image_hue_offset(image:Image, offset:int) -> Image: image = image.convert('L') width = image.width height = image.height ret_image = Image.new('L', size=(width, height), color='black') for x in range(width): for y in range(height): pixel = image.getpixel((x, y)) _pixel = pixel + offset if _pixel > 255: _pixel -= 256 if _pixel < 0: _pixel += 256 ret_image.putpixel((x, y), _pixel) return ret_image def gamma_trans(image:Image, gamma:float) -> Image: cv2_image = pil2cv2(image) gamma_table = [np.power(x/255.0,gamma)*255.0 for x in range(256)] gamma_table = np.round(np.array(gamma_table)).astype(np.uint8) _corrected = cv2.LUT(cv2_image,gamma_table) return cv22pil(_corrected) def apply_lut(image:Image, lut_file:str, log:bool=False) -> Image: from colour.io.luts.iridas_cube import read_LUT_IridasCube, LUT3D, LUT3x1D lut: Union[LUT3x1D, LUT3D] = read_LUT_IridasCube(lut_file) lut.name = os.path.splitext(os.path.basename(lut_file))[0] # use base filename instead of internal LUT name im_array = np.asarray(image.convert('RGB'), dtype=np.float32) / 255 is_non_default_domain = not np.array_equal(lut.domain, np.array([[0., 0., 0.], [1., 1., 1.]])) dom_scale = None if is_non_default_domain: dom_scale = lut.domain[1] - lut.domain[0] im_array = im_array * dom_scale + lut.domain[0] if log: im_array = im_array ** (1 / 2.2) im_array = lut.apply(im_array) if log: im_array = im_array ** (2.2) if is_non_default_domain: im_array = (im_array - lut.domain[0]) / dom_scale im_array = im_array * 255 ret_image = Image.fromarray(np.uint8(im_array)) return ret_image def color_adapter(image:Image, ref_image:Image) -> Image: image = pil2cv2(image) ref_image = pil2cv2(ref_image) image = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) image_mean, image_std = calculate_mean_std(image) ref_image = cv2.cvtColor(ref_image, cv2.COLOR_BGR2LAB) ref_image_mean, ref_image_std = calculate_mean_std(ref_image) _image = ((image - image_mean) * (ref_image_std / image_std)) + ref_image_mean np.putmask(_image, _image > 255, values=255) np.putmask(_image, _image < 0, values=0) ret_image = cv2.cvtColor(cv2.convertScaleAbs(_image), cv2.COLOR_LAB2BGR) return cv22pil(ret_image) def calculate_mean_std(image:Image): mean, std = cv2.meanStdDev(image) mean = np.hstack(np.around(mean, decimals=2)) std = np.hstack(np.around(std, decimals=2)) return mean, std def image_watercolor(image:Image, level:int=50) -> Image: img = pil2cv2(image) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) factor = (level / 128.0) ** 2 sigmaS= int((image.width + image.height) / 5.0 * factor) + 1 sigmaR = sigmaS / 32.0 * factor + 0.002 img_color = cv2.stylization(img, sigma_s=sigmaS, sigma_r=sigmaR) ret_image = cv2.cvtColor(img_color, cv2.COLOR_BGR2RGB) return cv22pil(ret_image) def image_beauty(image:Image, level:int=50) -> Image: img = pil2cv2(image) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) factor = (level / 50.0)**2 d = int((image.width + image.height) / 256 * factor) sigmaColor = int((image.width + image.height) / 256 * factor) sigmaSpace = int((image.width + image.height) / 160 * factor) img_bit = cv2.bilateralFilter(src=img, d=d, sigmaColor=sigmaColor, sigmaSpace=sigmaSpace) ret_image = cv2.cvtColor(img_bit, cv2.COLOR_BGR2RGB) return cv22pil(ret_image) def pixel_spread(image:Image, mask:Image) -> Image: from pymatting import estimate_foreground_ml i1 = pil2tensor(image) if mask.mode != 'RGB': mask = mask.convert('RGB') i_dup = copy.deepcopy(i1.cpu().numpy().astype(np.float64)) a_dup = copy.deepcopy(pil2tensor(mask).cpu().numpy().astype(np.float64)) fg = copy.deepcopy(i1.cpu().numpy().astype(np.float64)) for index, img in enumerate(i_dup): alpha = a_dup[index][:, :, 0] fg[index], _ = estimate_foreground_ml(img, np.array(alpha), return_background=True) return tensor2pil(torch.from_numpy(fg.astype(np.float32))) def generate_text_image(text:str, font_path:str, font_size:int, text_color:str="#FFFFFF", vertical:bool=True, stroke_width:int=1, stroke_color:str="#000000", spacing:int=0, leading:int=0) -> tuple: lines = text.split("\n") if vertical: layout = "vertical" else: layout = "horizontal" char_coordinates = [] if layout == "vertical": x = 0 y = 0 for i in range(len(lines)): line = lines[i] for char in line: char_coordinates.append((x, y)) y += font_size + spacing x += font_size + leading y = 0 else: x = 0 y = 0 for line in lines: for char in line: char_coordinates.append((x, y)) x += font_size + spacing y += font_size + leading x = 0 if layout == "vertical": width = (len(lines) * (font_size + spacing)) - spacing height = ((len(max(lines, key=len)) + 1) * (font_size + spacing)) + spacing else: width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing height = ((len(lines) - 1) * (font_size + spacing)) + font_size image = Image.new('RGBA', size=(width, height), color=stroke_color) draw = ImageDraw.Draw(image) font = ImageFont.truetype(font_path, font_size) index = 0 for i, line in enumerate(lines): for j, char in enumerate(line): x, y = char_coordinates[index] if stroke_width > 0: draw.text((x - stroke_width, y), char, font=font, fill=stroke_color) draw.text((x + stroke_width, y), char, font=font, fill=stroke_color) draw.text((x, y - stroke_width), char, font=font, fill=stroke_color) draw.text((x, y + stroke_width), char, font=font, fill=stroke_color) draw.text((x, y), char, font=font, fill=text_color) index += 1 return (image.convert('RGB'), image.split()[3]) '''Mask Functions''' @lru_cache(maxsize=1, typed=False) def load_RMBG_model(): current_directory = os.path.dirname(os.path.abspath(__file__)) device = "cuda" if torch.cuda.is_available() else "cpu" net = BriaRMBG() model_path = "" try: model_path = os.path.join(os.path.normpath(folder_paths.folder_names_and_paths['rmbg'][0][0]), "model.pth") except: pass if not os.path.exists(model_path): model_path = os.path.join(folder_paths.models_dir, "rmbg", "RMBG-1.4", "model.pth") if not os.path.exists(model_path): model_path = os.path.join(os.path.dirname(current_directory), "RMBG-1.4", "model.pth") net.load_state_dict(torch.load(model_path, map_location=device)) net.to(device) net.eval() return net def RMBG(image:Image) -> Image: rmbgmodel = load_RMBG_model() w, h = image.size im_np = np.array(image.resize((1024, 1024), Image.BILINEAR)) im_tensor = torch.tensor(im_np, dtype=torch.float32).permute(2, 0, 1) im_tensor = torch.divide(torch.unsqueeze(im_tensor, 0), 255.0) im_tensor = TF.normalize(im_tensor, [0.5, 0.5, 0.5], [1.0, 1.0, 1.0]) if torch.cuda.is_available(): im_tensor = im_tensor.cuda() result = rmbgmodel(im_tensor) result = torch.squeeze(F.interpolate(result[0][0], size=(h, w), mode='bilinear'), 0) ma = torch.max(result) mi = torch.min(result) result = (result - mi) / (ma - mi) im_array = (result * 255).cpu().data.numpy().astype(np.uint8) _mask = torch.from_numpy(np.squeeze(im_array).astype(np.float32)) return tensor2pil(_mask) class VITMatteModel: def __init__(self,model,processor): self.model = model self.processor = processor def load_VITMatte_model(model_name:str, local_files_only:bool=False) -> object: from transformers import VitMatteImageProcessor, VitMatteForImageMatting model = VitMatteForImageMatting.from_pretrained(model_name, local_files_only=local_files_only) processor = VitMatteImageProcessor.from_pretrained(model_name, local_files_only=local_files_only) vitmatte = VITMatteModel(model, processor) return vitmatte def generate_VITMatte(image:Image, trimap:Image, local_files_only:bool=False) -> Image: if image.mode != 'RGB': image = image.convert('RGB') if trimap.mode != 'L': trimap = trimap.convert('L') model_name = "hustvl/vitmatte-small-composition-1k" vit_matte_model = load_VITMatte_model(model_name=model_name, local_files_only=local_files_only) inputs = vit_matte_model.processor(images=image, trimaps=trimap, return_tensors="pt") with torch.no_grad(): predictions = vit_matte_model.model(**inputs).alphas mask = tensor2pil(predictions).convert('L') mask = mask.crop( (0, 0, image.width, image.height)) # remove padding that the prediction appends (works in 32px tiles) return mask def generate_VITMatte_trimap(mask:torch.Tensor, erode_kernel_size:int, dilate_kernel_size:int) -> Image: mask = mask.squeeze(0).cpu().detach().numpy().astype(np.uint8) * 255 trimap = __generate_trimap(mask, erode_kernel_size, dilate_kernel_size).astype(np.float32) trimap[trimap == 128] = 0.5 trimap[trimap == 255] = 1 trimap = torch.from_numpy(trimap).unsqueeze(0) return tensor2pil(trimap).convert('L') def __generate_trimap(mask, erode_kernel_size=10, dilate_kernel_size=10): erode_kernel = np.ones((erode_kernel_size, erode_kernel_size), np.uint8) dilate_kernel = np.ones((dilate_kernel_size, dilate_kernel_size), np.uint8) eroded = cv2.erode(mask, erode_kernel, iterations=5) dilated = cv2.dilate(mask, dilate_kernel, iterations=5) trimap = np.zeros_like(mask) trimap[dilated == 255] = 128 trimap[eroded == 255] = 255 return trimap def get_a_person_mask_generator_model_path() -> str: model_folder_name = 'mediapipe' model_name = 'selfie_multiclass_256x256.tflite' model_file_path = "" try: model_file_path = os.path.join(os.path.normpath(folder_paths.folder_names_and_paths[model_folder_name][0][0]), model_name) except: pass if not os.path.exists(model_file_path): model_file_path = os.path.join(folder_paths.models_dir, model_folder_name, model_name) if not os.path.exists(model_file_path): model_url = f'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/latest/{model_name}' print(f"Downloading '{model_name}' model") os.makedirs(model_file_path, exist_ok=True) wget.download(model_url, model_file_path) return model_file_path def mask_edge_detail(image:torch.Tensor, mask:torch.Tensor, detail_range:int=8, black_point:float=0.01, white_point:float=0.99) -> torch.Tensor: from pymatting import fix_trimap, estimate_alpha_cf d = detail_range * 5 + 1 mask = pil2tensor(tensor2pil(mask).convert('RGB')) if not bool(d % 2): d += 1 i_dup = copy.deepcopy(image.cpu().numpy().astype(np.float64)) a_dup = copy.deepcopy(mask.cpu().numpy().astype(np.float64)) for index, img in enumerate(i_dup): trimap = a_dup[index][:, :, 0] # convert to single channel if detail_range > 0: trimap = cv2.GaussianBlur(trimap, (d, d), 0) trimap = fix_trimap(trimap, black_point, white_point) alpha = estimate_alpha_cf(img, trimap, laplacian_kwargs={"epsilon": 1e-6}, cg_kwargs={"maxiter": 500}) a_dup[index] = np.stack([alpha, alpha, alpha], axis=-1) # convert back to rgb return torch.from_numpy(a_dup.astype(np.float32)) def guided_filter_alpha(image:torch.Tensor, mask:torch.Tensor, filter_radius:int) -> torch.Tensor: sigma = 0.15 d = filter_radius + 1 mask = pil2tensor(tensor2pil(mask).convert('RGB')) if not bool(d % 2): d += 1 s = sigma / 10 i_dup = copy.deepcopy(image.cpu().numpy()) a_dup = copy.deepcopy(mask.cpu().numpy()) for index, image in enumerate(i_dup): alpha_work = a_dup[index] i_dup[index] = guidedFilter(image, alpha_work, d, s) return torch.from_numpy(i_dup) def mask_fix(images:torch.Tensor, radius:int, fill_holes:int, white_threshold:float, extra_clip:float) -> torch.Tensor: d = radius * 2 + 1 i_dup = copy.deepcopy(images.cpu().numpy()) for index, image in enumerate(i_dup): cleaned = cv2.bilateralFilter(image, 9, 0.05, 8) alpha = np.clip((image - white_threshold) / (1 - white_threshold), 0, 1) rgb = image * alpha alpha = cv2.GaussianBlur(alpha, (d, d), 0) * 0.99 + np.average(alpha) * 0.01 rgb = cv2.GaussianBlur(rgb, (d, d), 0) * 0.99 + np.average(rgb) * 0.01 rgb = rgb / np.clip(alpha, 0.00001, 1) rgb = rgb * extra_clip cleaned = np.clip(cleaned / rgb, 0, 1) if fill_holes > 0: fD = fill_holes * 2 + 1 gamma = cleaned * cleaned kD = np.ones((fD, fD), np.uint8) kE = np.ones((fD + 2, fD + 2), np.uint8) gamma = cv2.dilate(gamma, kD, iterations=1) gamma = cv2.erode(gamma, kE, iterations=1) gamma = cv2.GaussianBlur(gamma, (fD, fD), 0) cleaned = np.maximum(cleaned, gamma) i_dup[index] = cleaned return torch.from_numpy(i_dup) def histogram_remap(image:torch.Tensor, blackpoint:float, whitepoint:float) -> torch.Tensor: bp = min(blackpoint, whitepoint - 0.001) scale = 1 / (whitepoint - bp) i_dup = copy.deepcopy(image.cpu().numpy()) i_dup = np.clip((i_dup - bp) * scale, 0.0, 1.0) return torch.from_numpy(i_dup) def expand_mask(mask:torch.Tensor, grow:int, blur:int) -> torch.Tensor: # grow c = 0 kernel = np.array([[c, 1, c], [1, 1, 1], [c, 1, c]]) growmask = mask.reshape((-1, mask.shape[-2], mask.shape[-1])) out = [] for m in growmask: output = m.numpy() for _ in range(abs(grow)): if grow < 0: output = scipy.ndimage.grey_erosion(output, footprint=kernel) else: output = scipy.ndimage.grey_dilation(output, footprint=kernel) output = torch.from_numpy(output) out.append(output) # blur for idx, tensor in enumerate(out): pil_image = tensor2pil(tensor.cpu().detach()) pil_image = pil_image.filter(ImageFilter.GaussianBlur(blur)) out[idx] = pil2tensor(pil_image) ret_mask = torch.cat(out, dim=0) return ret_mask def mask_invert(mask:torch.Tensor) -> torch.Tensor: return 1 - mask def subtract_mask(masks_a:torch.Tensor, masks_b:torch.Tensor) -> torch.Tensor: return torch.clamp(masks_a - masks_b, 0, 255) def add_mask(masks_a:torch.Tensor, masks_b:torch.Tensor) -> torch.Tensor: mask = chop_image(tensor2pil(masks_a), tensor2pil(masks_b), blend_mode='add', opacity=100) return image2mask(mask) def RGB2RGBA(image:Image, mask:Image) -> Image: (R, G, B) = image.convert('RGB').split() return Image.merge('RGBA', (R, G, B, mask.convert('L'))) def mask_area(image:Image) -> tuple: cv2_image = pil2cv2(image.convert('RGBA')) gray = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(gray, 127, 255, 0) locs = np.where(thresh == 255) x1 = np.min(locs[1]) if len(locs[1]) > 0 else 0 x2 = np.max(locs[1]) if len(locs[1]) > 0 else image.width y1 = np.min(locs[0]) if len(locs[0]) > 0 else 0 y2 = np.max(locs[0]) if len(locs[0]) > 0 else image.height x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2) return (x1, y1, x2 - x1, y2 - y1) def min_bounding_rect(image:Image) -> tuple: cv2_image = pil2cv2(image) gray = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(gray, 127, 255, 0) contours, _ = cv2.findContours(thresh, 1, 2) x, y, width, height = 0, 0, 0, 0 area = 0 for contour in contours: _x, _y, _w, _h = cv2.boundingRect(contour) _area = _w * _h if _area > area: area = _area x, y, width, height = _x, _y, _w, _h return (x, y, width, height) def max_inscribed_rect(image:Image) -> tuple: img = pil2cv2(image) img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, img_bin = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(img_bin, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) contour = contours[0].reshape(len(contours[0]), 2) rect = [] for i in range(len(contour)): x1, y1 = contour[i] for j in range(len(contour)): x2, y2 = contour[j] area = abs(y2 - y1) * abs(x2 - x1) rect.append(((x1, y1), (x2, y2), area)) all_rect = sorted(rect, key=lambda x: x[2], reverse=True) if all_rect: best_rect_found = False index_rect = 0 nb_rect = len(all_rect) while not best_rect_found and index_rect < nb_rect: rect = all_rect[index_rect] (x1, y1) = rect[0] (x2, y2) = rect[1] valid_rect = True x = min(x1, x2) while x < max(x1, x2) + 1 and valid_rect: if any(img[y1, x]) == 0 or any(img[y2, x]) == 0: valid_rect = False x += 1 y = min(y1, y2) while y < max(y1, y2) + 1 and valid_rect: if any(img[y, x1]) == 0 or any(img[y, x2]) == 0: valid_rect = False y += 1 if valid_rect: best_rect_found = True index_rect += 1 #较小的数值排前面 x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2) return (x1, y1, x2 - x1, y2 - y1) def gray_threshold(image:Image, thresh:int=127, otsu:bool=False) -> Image: cv2_image = pil2cv2(image) gray = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2GRAY) if otsu: _, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) else: _, thresh = cv2.threshold(gray, thresh, 255, cv2.THRESH_TOZERO) return cv22pil(thresh).convert('L') def image_to_colormap(image:Image, index:int) -> Image: return cv22pil(cv2.applyColorMap(pil2cv2(image), index)) '''Color Functions''' def color_balance(image:Image, shadows:list, midtones:list, highlights:list, shadow_center:float=0.15, midtone_center:float=0.5, highlight_center:float=0.8, shadow_max:float=0.1, midtone_max:float=0.3, highlight_max:float=0.2, preserve_luminosity:bool=False) -> Image: img = pil2tensor(image) # Create a copy of the img tensor img_copy = img.clone() # Calculate the original luminance if preserve_luminosity is True if preserve_luminosity: original_luminance = 0.2126 * img_copy[..., 0] + 0.7152 * img_copy[..., 1] + 0.0722 * img_copy[..., 2] # Define the adjustment curves def adjust(x, center, value, max_adjustment): # Scale the adjustment value value = value * max_adjustment # Define control points points = torch.tensor([[0, 0], [center, center + value], [1, 1]]) # Create cubic spline from scipy.interpolate import CubicSpline cs = CubicSpline(points[:, 0], points[:, 1]) # Apply the cubic spline to the color channel return torch.clamp(torch.from_numpy(cs(x)), 0, 1) # Apply the adjustments to each color channel # shadows, midtones, highlights are lists of length 3 (for R, G, B channels) with values between -1 and 1 for i, (s, m, h) in enumerate(zip(shadows, midtones, highlights)): img_copy[..., i] = adjust(img_copy[..., i], shadow_center, s, shadow_max) img_copy[..., i] = adjust(img_copy[..., i], midtone_center, m, midtone_max) img_copy[..., i] = adjust(img_copy[..., i], highlight_center, h, highlight_max) # If preserve_luminosity is True, adjust the RGB values to match the original luminance if preserve_luminosity: current_luminance = 0.2126 * img_copy[..., 0] + 0.7152 * img_copy[..., 1] + 0.0722 * img_copy[..., 2] img_copy *= (original_luminance / current_luminance).unsqueeze(-1) return tensor2pil(img_copy) def RGB_to_Hex(RGB:tuple) -> str: color = '#' for i in RGB: num = int(i) color += str(hex(num))[-2:].replace('x', '0').upper() return color def Hex_to_RGB(inhex:str) -> tuple: if not inhex.startswith('#'): raise ValueError(f'Invalid Hex Code in {inhex}') else: rval = inhex[1:3] gval = inhex[3:5] bval = inhex[5:] rgb = (int(rval, 16), int(gval, 16), int(bval, 16)) return tuple(rgb) def RGB_to_HSV(RGB:tuple) -> list: HSV = colorsys.rgb_to_hsv(RGB[0] / 255.0, RGB[1] / 255.0, RGB[2] / 255.0) return [int(x * 360) for x in HSV] def Hex_to_HSV_255level(inhex:str) -> list: if not inhex.startswith('#'): raise ValueError(f'Invalid Hex Code in {inhex}') else: rval = inhex[1:3] gval = inhex[3:5] bval = inhex[5:] RGB = (int(rval, 16), int(gval, 16), int(bval, 16)) HSV = colorsys.rgb_to_hsv(RGB[0] / 255.0, RGB[1] / 255.0, RGB[2] / 255.0) return [int(x * 255) for x in HSV] '''Value Functions''' def step_value(start_value, end_value, total_step, step) -> float: # 按当前步数在总步数中的位置返回比例值 factor = step / total_step return (end_value - start_value) * factor + start_value def step_color(start_color_inhex:str, end_color_inhex:str, total_step:int, step:int) -> str: # 按当前步数在总步数中的位置返回比例颜色 start_color = tuple(Hex_to_RGB(start_color_inhex)) end_color = tuple(Hex_to_RGB(end_color_inhex)) start_R, start_G, start_B = start_color[0], start_color[1], start_color[2] end_R, end_G, end_B = end_color[0], end_color[1], end_color[2] ret_color = (int(step_value(start_R, end_R, total_step, step)), int(step_value(start_G, end_G, total_step, step)), int(step_value(start_B, end_B, total_step, step)), ) return RGB_to_Hex(ret_color) def has_letters(string:str) -> bool: pattern = r'[a-zA-Z]' match = re.search(pattern, string) if match: return True else: return False def replace_case(old:str, new:str, text:str) -> str: index = text.lower().find(old.lower()) if index == -1: return text return replace_case(old, new, text[:index] + new + text[index + len(old):]) def random_numbers(total:int, random_range:int, seed:int=0, sum_of_numbers:int=0) -> list: random.seed(seed) numbers = [random.randint(-random_range//2, random_range//2) for _ in range(total - 1)] avg = sum(numbers) // total ret_list = [] for i in numbers: ret_list.append(i - avg) ret_list.append((sum_of_numbers - sum(ret_list)) // 2) return ret_list def num_round_to_multiple(number:int, multiple:int) -> int: remainder = number % multiple if remainder == 0 : return number else: factor = int(number / multiple) if number - factor * multiple > multiple / 2: factor += 1 return factor * multiple def calculate_side_by_ratio(orig_width:int, orig_height:int, ratio:float, longest_side:int=0) -> int: if orig_width > orig_height: if longest_side: target_width = longest_side else: target_width = orig_width target_height = int(target_width / ratio) else: if longest_side: target_height = longest_side else: target_height = orig_height target_width = int(target_height * ratio) if ratio < 1: if longest_side: _r = longest_side / target_height target_height = longest_side else: _r = orig_height / target_height target_height = orig_height target_width = int(target_width * _r) return target_width, target_height def generate_random_name(prefix:str, suffix:str, length:int) -> str: name = ''.join(random.choice("abcdefghijklmnopqrstupvxyz1234567890") for x in range(length)) return prefix + name + suffix def check_image_file(file_name:str, interval:int) -> object: while True: if os.path.isfile(file_name): try: image = Image.open(file_name) ret_image = copy.deepcopy(image) image.close() return ret_image except Exception as e: print(e) return None break time.sleep(interval / 1000) # 判断字符串是否包含中文 def is_contain_chinese(check_str:str) -> bool: for ch in check_str: if u'\u4e00' <= ch <= u'\u9fff': return True return False def tensor_info(tensor:object) -> str: value = '' if isinstance(tensor, torch.Tensor): value += f"\n Input dim = {tensor.dim()}, shape[0] = {tensor.shape[0]} \n" for i in range(tensor.shape[0]): t = tensor[i] image = tensor2pil(t) value += f'\n index {i}: Image.size = {image.size}, Image.mode = {image.mode}, dim = {t.dim()}, ' for j in range(t.dim()): value += f'shape[{j}] = {t.shape[j]}, ' else: value = f"tensor_info: Not tensor, type is {type(tensor)}" return value '''CLASS''' class AnyType(str): """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" def __ne__(self, __value: object) -> bool: return False '''Constant''' chop_mode = [ 'normal', 'multply', 'screen', 'add', 'subtract', 'difference', 'darker', 'lighter', 'color_burn', 'color_dodge', 'linear_burn', 'linear_dodge', 'overlay', 'soft_light', 'hard_light', 'vivid_light', 'pin_light', 'linear_light', 'hard_mix' ] # Blend Mode from Virtuoso Pack https://github.com/chrisfreilich/virtuoso-nodes chop_mode_v2 = list(BLEND_MODES.keys()) '''Load INI File''' default_lut_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), 'lut') default_font_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), 'font') resource_dir_ini_file = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), "resource_dir.ini") api_key_ini_file = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), "api_key.ini") custom_size_file = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), "custom_size.ini") # def load_inference_prompt() -> str: # inference_prompt_file = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), "resource", # "inference.prompt") # ret_value = '' # try: # with open(inference_prompt_file, 'r') as f: # ret_value = f.readlines() # except Exception as e: # log(f'Warning: {inference_prompt_file} ' + repr(e) + f", check it to be correct. ", message_type='warning') # return ''.join(ret_value) def load_custom_size() -> list: ret_value = ['1024 x 1024', '768 x 512', '512 x 768', '1280 x 720', '720 x 1280', '1344 x 768', '768 x 1344', '1536 x 640', '640 x 1536' ] try: with open(custom_size_file, 'r') as f: ini = f.readlines() for line in ini: if not line.startswith(f'#'): ret_value.append(line.strip()) except Exception as e: # log(f'Warning: {custom_size_file} ' + repr(e) + f", use default size. ") log(f'Warning: {custom_size_file} not found' + f", use default size. ") return ret_value def get_api_key(api_name:str) -> str: ret_value = '' try: with open(api_key_ini_file, 'r') as f: ini = f.readlines() for line in ini: if line.startswith(f'{api_name}='): ret_value = line[line.find('=') + 1:].rstrip().lstrip() break except Exception as e: log(f'Warning: {api_key_ini_file} ' + repr(e) + f", check it to be correct. ", message_type='warning') remove_char = ['"', "'", '“', '”', '‘', '’'] for i in remove_char: if i in ret_value: ret_value = ret_value.replace(i, '') if len(ret_value) < 4: log(f'Warning: Invalid API-key, Check the key in {api_key_ini_file}.', message_type='warning') return ret_value try: with open(resource_dir_ini_file, 'r') as f: ini = f.readlines() for line in ini: if line.startswith('LUT_dir='): _ldir = line[line.find('=') + 1:].rstrip().lstrip() if os.path.exists(_ldir): default_lut_dir = _ldir else: log(f'Invalid LUT directory, default to be used. check {resource_dir_ini_file}') elif line.startswith('FONT_dir='): _fdir = line[line.find('=') + 1:].rstrip().lstrip() if os.path.exists(_fdir): default_font_dir = _fdir else: log(f'Invalid FONT directory, default to be used. check {resource_dir_ini_file}') except Exception as e: # log(f'Warning: {resource_dir_ini_file} ' + repr(e) + f", default directory to be used. ") log(f'Warning: {resource_dir_ini_file} not found' + f", default directory to be used. ") __lut_file_list = glob.glob(default_lut_dir + '/*.cube') LUT_DICT = {} for i in range(len(__lut_file_list)): _, __filename = os.path.split(__lut_file_list[i]) LUT_DICT[__filename] = __lut_file_list[i] LUT_LIST = list(LUT_DICT.keys()) log(f'Find {len(LUT_LIST)} LUTs in {default_lut_dir}') __font_file_list = glob.glob(default_font_dir + '/*.ttf') __font_file_list.extend(glob.glob(default_font_dir + '/*.otf')) FONT_DICT = {} for i in range(len(__font_file_list)): _, __filename = os.path.split(__font_file_list[i]) FONT_DICT[__filename] = __font_file_list[i] FONT_LIST = list(FONT_DICT.keys()) log(f'Find {len(FONT_LIST)} Fonts in {default_font_dir}') gemini_generate_config = { "temperature": 0, "top_p": 1, "top_k": 1, "max_output_tokens": 400 } gemini_safety_settings = [ { "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE" }, { "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE" }, { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE" }, { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE" } ]