diff --git a/Loader.py b/Loader.py index 79fd475..a12c30d 100644 --- a/Loader.py +++ b/Loader.py @@ -38,18 +38,20 @@ class Loader: def setup_override(self): override_nodes_len = 0 - if self.config()["override"]["postprocessing"]: + def override(function): start_len = nodes.NODE_CLASS_MAPPINGS.__len__() nodes.NODE_CLASS_MAPPINGS = dict( - filter( - lambda item: not item[1].CATEGORY.startswith("image/postprocessing"), - nodes.NODE_CLASS_MAPPINGS.items() - ) + filter(function, nodes.NODE_CLASS_MAPPINGS.items()) ) - end_len = nodes.NODE_CLASS_MAPPINGS.__len__() - override_nodes_len += start_len - end_len + return start_len - nodes.NODE_CLASS_MAPPINGS.__len__() + + if self.config()["override"]["postprocessing"]: + override_nodes_len += override(lambda item: not item[1].CATEGORY.startswith("image/postprocessing")) + + if self.config()["override"]["transform"]: + override_nodes_len += override(lambda item: not item[0] == "ImageScale" and not item[0] == "ImageInvert") self.__log(str(override_nodes_len) + " standard nodes was overridden.") @@ -88,6 +90,10 @@ class Loader: from .modules import ImageText modules.update(ImageText.NODE_CLASS_MAPPINGS) + if self.config()["modules"]["ImageTransform"]: + from .modules import ImageTransform + modules.update(ImageTransform.NODE_CLASS_MAPPINGS) + modules_len = dict( filter( lambda item: item[1], diff --git a/README.MD b/README.MD index d87f4ec..62a2631 100644 --- a/README.MD +++ b/README.MD @@ -639,6 +639,79 @@ You also can change the fonts folder in config. +--- +### Image Transform +![image_transform](images/image_transform.png) +> Transform your images. + +
+ Nodes: + +### Resize +![image_transform_resize](images/image_transform_resize.png) +> Change size of images. + +
+ Params: + +#### Absolute +* [width, height] `[1 - *]` - New size of images. + +#### Relative +* scale_[width, height] `[0.0 - 1.0]` - New size of images. + +
+ +### Crop +![image_transform_crop](images/image_transform_crop.png) +> Returns area from images. + +
+ Params: + +#### Absolute +* start_[x, y] `[1 - *]` - Start of rectangle point. +* end_[x, y] `[1 - *]` - End of rectangle point. + +#### Relative +* start_[x, y] `[0.0 - 1.0]` - Start of rectangle point. +* end_[x, y] `[0.0 - 1.0]` - End of rectangle point. + +
+ +### Crop Corners +![image_transform_crop_corners](images/image_transform_crop_corners.png) +> Round corners of your images. + +
+ Params: + +* radius `[0 -360]` - Radius of the corners. +* [top_left, top_right, bottom_right, bottom_left]_corner `[boolean]` - The ability to choose for which angle to apply the radius. +* SSAO [1 - 16] - [Super Sampling Anti-Aliasing](https://en.wikipedia.org/wiki/Supersampling). The figure is drawn initially at a higher resolution, and then compressed to the specified resolution. + +
+ + +### Rotate +![image_transform_rotate](images/image_transform_rotate.png) +> Rotate your images. + +
+ Params: + +* angle `[0 -360]` - Angle in degrees. Angles are measured from 3 o’clock, increasing clockwise. +* expand `[boolean]` - If "true" when rotating, change the size of the image to fit into it. +* SSAO [1 - 16] - [Super Sampling Anti-Aliasing](https://en.wikipedia.org/wiki/Supersampling). The figure is drawn initially at a higher resolution, and then compressed to the specified resolution. + +
+ +### Transpose +![image_transform_transpose](images/image_transform_transpose.png) +> Transpose your images. + +
+ --- ### Clamp ![clamp](images/clamp.png) @@ -670,6 +743,8 @@ You can configurate Allor with `config.json`. * fonts_folder_path - Array with a relative path to fonts folder, by default is `["comfy_extras", "fonts"]`. (Converted to `ComfyUI/comfy_extras/fonts` for Unix and `ComfyUI\comfy_extras\fonts` for Windows). * modules - You can disable (or enable) modules at will. Nodes from disabled modules will not be loaded during the start of ComfyUI. * override - If the nested value is set to `true`, similar functionality from the original nodes is disabled. +* * postprocessing - Disable `image/postprocessing` nodes. (See `ImageFilter` module) +* * transform - Disable `ImageScale` and `ImageInvert` nodes. (See `ImageTransform` module) ## Examples: ![example](images/example.png) diff --git a/config.json b/config.json index 6f319d7..67533f5 100644 --- a/config.json +++ b/config.json @@ -8,9 +8,11 @@ "ImageDraw": true, "ImageFilter": true, "ImageSegmentation": true, - "ImageText": true + "ImageText": true, + "ImageTransform": true }, "override" : { - "postprocessing": true + "postprocessing": true, + "transform": true } } diff --git a/modules/ImageContainer.py b/modules/ImageContainer.py index 9513c31..1a25fa6 100644 --- a/modules/ImageContainer.py +++ b/modules/ImageContainer.py @@ -161,7 +161,7 @@ class ImageContainerInheritanceScale: CATEGORY = "image/container" def image_container_inheritance_scale(self, images, scale_width, scale_height, red, green, blue, alpha, method): - width, height = images[0, :, :, 0].shape + height, width = images[0, :, :, 0].shape width = int((width * scale_width) - width) height = int((height * scale_height) - height) diff --git a/modules/ImageDraw.py b/modules/ImageDraw.py index 7e3cb39..e078e08 100644 --- a/modules/ImageDraw.py +++ b/modules/ImageDraw.py @@ -1520,7 +1520,7 @@ class ImageDrawRectangleRounded: (width * start_x * SSAA, height * start_y * SSAA), (width * end_x * SSAA, height * end_y * SSAA) ), - radius, + radius * SSAA, (fill_red, fill_green, fill_blue, int(fill_alpha * 255)), (outline_red, outline_green, outline_blue, int(outline_alpha * 255)), outline_size * SSAA, diff --git a/modules/ImageTransform.py b/modules/ImageTransform.py new file mode 100644 index 0000000..7e27691 --- /dev/null +++ b/modules/ImageTransform.py @@ -0,0 +1,393 @@ +import torch +from PIL import Image, ImageDraw + + +class ImageTransformResizeAbsolute: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "width": ("INT", { + "default": 256, + "min": 1, + "step": 1 + }), + "height": ("INT", { + "default": 256, + "min": 1, + "step": 1 + }), + "method": (["lanczos", "bicubic", "hamming", "bilinear", "box", "nearest"],), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "image_transform_resize_absolute" + CATEGORY = "image/transform" + + def image_transform_resize_absolute(self, images, width, height, method): + def resize_tensor(tensor): + if method == "lanczos": + sampler = Image.LANCZOS + elif method == "bicubic": + sampler = Image.BICUBIC + elif method == "hamming": + sampler = Image.HAMMING + elif method == "bilinear": + sampler = Image.BILINEAR + elif method == "box": + sampler = Image.BOX + elif method == "nearest": + sampler = Image.NEAREST + else: + raise ValueError() + + return tensor.tensor_to_image().resize((width, height), sampler).image_to_tensor() + + return (torch.stack([ + resize_tensor(images[i]) for i in range(len(images)) + ]),) + + +class ImageTransformResizeRelative: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "scale_width": ("FLOAT", { + "default": 1.0, + "step": 0.1 + }), + "scale_height": ("FLOAT", { + "default": 1.0, + "step": 0.1 + }), + "method": (["lanczos", "bicubic", "hamming", "bilinear", "box", "nearest"],), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "image_transform_resize_relative" + CATEGORY = "image/transform" + + def image_transform_resize_relative(self, images, scale_width, scale_height, method): + height, width = images[0, :, :, 0].shape + + width = int(width * scale_width) + height = int(height * scale_height) + + return ImageTransformResizeAbsolute().image_transform_resize_absolute(images, width, height, method) + + +class ImageTransformCropAbsolute: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "start_x": ("INT", { + "default": 0, + "step": 1 + }), + "start_y": ("INT", { + "default": 0, + "step": 1 + }), + "end_x": ("INT", { + "default": 128, + "step": 1 + }), + "end_y": ("INT", { + "default": 128, + "step": 1 + }), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "image_transform_crop_absolute" + CATEGORY = "image/transform" + + def image_transform_crop_absolute(self, images, start_x, start_y, end_x, end_y): + def resize_tensor(tensor): + return tensor.tensor_to_image().crop([start_x, start_y, end_x, end_y]).image_to_tensor() + + return (torch.stack([ + resize_tensor(images[i]) for i in range(len(images)) + ]),) + + +class ImageTransformCropRelative: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "start_x": ("FLOAT", { + "default": 0.25, + "max": 1.0, + "step": 0.01 + }), + "start_y": ("FLOAT", { + "default": 0.25, + "max": 1.0, + "step": 0.01 + }), + "end_x": ("FLOAT", { + "default": 0.75, + "max": 1.0, + "step": 0.01 + }), + "end_y": ("FLOAT", { + "default": 0.75, + "max": 1.0, + "step": 0.01 + }), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "image_transform_crop_relative" + CATEGORY = "image/transform" + + def image_transform_crop_relative(self, images, start_x, start_y, end_x, end_y): + height, width = images[0, :, :, 0].shape + + return ImageTransformCropAbsolute().image_transform_crop_absolute( + images, + width * start_x, + height * start_y, + width * end_x, + height * end_y + ) + + +class ImageTransformCropCorners: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "radius": ("INT", { + "default": 180, + "max": 360, + "step": 1 + }), + "top_left_corner": (["true", "false"],), + "top_right_corner": (["true", "false"],), + "bottom_right_corner": (["true", "false"],), + "bottom_left_corner": (["true", "false"],), + "SSAA": ("INT", { + "default": 4, + "min": 1, + "max": 16, + "step": 1 + }), + "method": (["lanczos", "bicubic", "hamming", "bilinear", "box", "nearest"],), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "image_transform_crop_corners" + CATEGORY = "image/transform" + + # noinspection PyUnresolvedReferences, PyArgumentList + def image_transform_crop_corners( + self, + images, + radius, + top_left_corner, + top_right_corner, + bottom_right_corner, + bottom_left_corner, + SSAA, + method + ): + if method == "lanczos": + sampler = Image.LANCZOS + elif method == "bicubic": + sampler = Image.BICUBIC + elif method == "hamming": + sampler = Image.HAMMING + elif method == "bilinear": + sampler = Image.BILINEAR + elif method == "box": + sampler = Image.BOX + elif method == "nearest": + sampler = Image.NEAREST + else: + raise ValueError() + + height, width = images[0, :, :, 0].shape + + canvas = Image.new("RGBA", (width * SSAA, height * SSAA), (0, 0, 0, 0)) + draw = ImageDraw.Draw(canvas) + + draw.rounded_rectangle( + ((0, 0), (width * SSAA, height * SSAA)), + radius * SSAA, (255, 255, 255, 255), + corners=( + True if top_left_corner == "true" else False, + True if top_right_corner == "true" else False, + True if bottom_right_corner == "true" else False, + True if bottom_left_corner == "true" else False + ) + ) + + canvas = canvas.resize((width, height), sampler) + mask = 1.0 - canvas.image_to_tensor()[:, :, 3] + + def crop_tensor(tensor): + return torch.stack([ + tensor[:, :, i] - mask for i in range(tensor.shape[2]) + ], dim=2) + + return (torch.stack([ + crop_tensor(images[i]) for i in range(len(images)) + ]),) + + +class ImageTransformRotate: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "angle": ("FLOAT", { + "default": 35.0, + "max": 360.0, + "step": 0.1 + }), + "expand": (["true", "false"],), + "SSAA": ("INT", { + "default": 4, + "min": 1, + "max": 16, + "step": 1 + }), + "method": (["lanczos", "bicubic", "hamming", "bilinear", "box", "nearest"],), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "image_transform_rotate" + CATEGORY = "image/transform" + + def image_transform_rotate(self, images, angle, expand, SSAA, method): + height, width = images[0, :, :, 0].shape + + def rotate_tensor(tensor): + if method == "lanczos": + resize_sampler = Image.LANCZOS + rotate_sampler = Image.BICUBIC + elif 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 + else: + raise ValueError() + + if SSAA > 1: + img = tensor.tensor_to_image() + 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 = img_down_scaled.image_to_tensor() + else: + img = tensor.tensor_to_image() + img_rotated = img.rotate(angle, rotate_sampler, expand == "true", fillcolor=(0, 0, 0, 0)) + result = img_rotated.image_to_tensor() + + return result + + if angle == 0.0 or angle == 360.0: + return (images,) + else: + return (torch.stack([ + rotate_tensor(images[i]) for i in range(len(images)) + ]),) + + +class ImageTransformTranspose: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "method": (["flip_horizontally", "flip_vertically", "rotate_90", "rotate_180", "rotate_270", "transpose", "transverse"],), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "image_transform_transpose" + CATEGORY = "image/transform" + + def image_transform_transpose(self, images, method): + def transpose_tensor(tensor): + if method == "flip_horizontally": + transpose = Image.FLIP_LEFT_RIGHT + elif method == "flip_vertically": + transpose = Image.FLIP_TOP_BOTTOM + elif method == "rotate_90": + transpose = Image.ROTATE_90 + elif method == "rotate_180": + transpose = Image.ROTATE_180 + elif method == "rotate_270": + transpose = Image.ROTATE_270 + elif method == "transpose": + transpose = Image.TRANSPOSE + elif method == "transverse": + transpose = Image.TRANSVERSE + else: + raise ValueError() + + return tensor.tensor_to_image().transpose(transpose).image_to_tensor() + + return (torch.stack([ + transpose_tensor(images[i]) for i in range(len(images)) + ]),) + + +NODE_CLASS_MAPPINGS = { + "ImageTransformResizeAbsolute": ImageTransformResizeAbsolute, + "ImageTransformResizeRelative": ImageTransformResizeRelative, + "ImageTransformCropAbsolute": ImageTransformCropAbsolute, + "ImageTransformCropRelative": ImageTransformCropRelative, + "ImageTransformCropCorners": ImageTransformCropCorners, + "ImageTransformRotate": ImageTransformRotate, + "ImageTransformTranspose": ImageTransformTranspose +}