Add ImageTransform module

This commit is contained in:
Nourepide
2023-06-08 05:03:39 +03:00
parent d60ce91933
commit 094a0c0c65
6 changed files with 487 additions and 11 deletions
+13 -7
View File
@@ -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],
+75
View File
@@ -639,6 +639,79 @@ You also can change the fonts folder in config.
</details>
---
### Image Transform
![image_transform](images/image_transform.png)
> Transform your images.
<details>
<summary>Nodes:</summary>
### Resize
![image_transform_resize](images/image_transform_resize.png)
> Change size of images.
<details>
<summary>Params:</summary>
#### Absolute
* [width, height] `[1 - *]` - New size of images.
#### Relative
* scale_[width, height] `[0.0 - 1.0]` - New size of images.
</details>
### Crop
![image_transform_crop](images/image_transform_crop.png)
> Returns area from images.
<details>
<summary>Params:</summary>
#### 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.
</details>
### Crop Corners
![image_transform_crop_corners](images/image_transform_crop_corners.png)
> Round corners of your images.
<details>
<summary>Params:</summary>
* 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.
</details>
### Rotate
![image_transform_rotate](images/image_transform_rotate.png)
> Rotate your images.
<details>
<summary>Params:</summary>
* 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.
</details>
### Transpose
![image_transform_transpose](images/image_transform_transpose.png)
> Transpose your images.
</details>
---
### 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)
+4 -2
View File
@@ -8,9 +8,11 @@
"ImageDraw": true,
"ImageFilter": true,
"ImageSegmentation": true,
"ImageText": true
"ImageText": true,
"ImageTransform": true
},
"override" : {
"postprocessing": true
"postprocessing": true,
"transform": true
}
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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,
+393
View File
@@ -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
}