'''Image process functions for ComfyUI nodes by chflame https://github.com/chflame163 ''' import numpy as np import torch import scipy.ndimage import cv2 from typing import Union, List from PIL import Image, ImageFilter, ImageChops def log(message): name = 'LayerStyle' print(f"# 😺dzNodes: {name} -> {message}") 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 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 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 def mask_invert(mask:torch.Tensor) -> torch.Tensor: _image = mask2image(mask) return image2mask(ImageChops.invert(_image)) def shift_image(image:Image, distance_x:int, distance_y:int) -> Image: bkcolor = (0, 0, 0) width = image.width height = image.height ret_image = Image.new('RGB', size=(width, height), color=bkcolor) for x in range(width): for y in range(height): 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 = 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) # 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 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 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 subtract_mask(masks_a, masks_b): return torch.clamp(masks_a - masks_b, 0, 255) def RGB_to_Hex(RGB): # _rgb = tuple(map(int, RGB[1:-1].split(','))) _rgb = RGB color = '#' for i in _rgb: num = int(i) color += str(hex(num))[-2:].replace('x', '0').upper() return color def Hex_to_RGB(inhex): 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 step_value(start_value, end_value, total_step, step): factor = step / total_step return int((end_value - start_value) * factor) + start_value def step_color(start_color, end_color, total_step, step): if isinstance(start_color, str): start_color = tuple(Hex_to_RGB(start_color)) if isinstance(end_color, str): end_color = tuple(Hex_to_RGB(end_color)) 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 = (step_value(start_R, end_R, total_step, step), step_value(start_G, end_G, total_step, step), step_value(start_B, end_B, total_step, step), ) if isinstance(start_color, str): return RGB_to_Hex(ret_color) else: return ret_color