182 lines
4.9 KiB
Python
182 lines
4.9 KiB
Python
from typing import NamedTuple
|
|
|
|
import torch
|
|
import torchvision.transforms.functional as F
|
|
from torchvision.transforms import InterpolationMode
|
|
|
|
NODE_CLASS_MAPPINGS = {}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {}
|
|
|
|
|
|
def register_node(identifier: str, display_name: str):
|
|
def decorator(cls):
|
|
NODE_CLASS_MAPPINGS[identifier] = cls
|
|
NODE_DISPLAY_NAME_MAPPINGS[identifier] = display_name
|
|
|
|
return cls
|
|
|
|
return decorator
|
|
|
|
|
|
MAX_RESOLUTION = 8192
|
|
|
|
|
|
def validate_bounds(img: torch.Tensor, x: int, y: int, w: int, h: int):
|
|
_, img_h, img_w, _ = img.shape
|
|
|
|
assert x >= 0
|
|
assert y >= 0
|
|
|
|
assert (
|
|
x + w <= img_w
|
|
), f"crop region out of bounds: crop {(x, y, w, h)} from image {(img_w, img_h)}"
|
|
assert (
|
|
y + h <= img_h
|
|
), f"crop region out of bounds: crop {(x, y, w, h)} from image {(img_w, img_h)}"
|
|
|
|
|
|
def crop_image(img: torch.Tensor, x: int, y: int, w: int, h: int):
|
|
validate_bounds(img, x, y, w, h)
|
|
|
|
to_x = x + w
|
|
to_y = y + h
|
|
return img[:, y:to_y, x:to_x, :]
|
|
|
|
|
|
def resize_image(img: torch.Tensor, w: int, h: int):
|
|
img = img.permute(0, 3, 1, 2)
|
|
img = F.resize(
|
|
img,
|
|
(h, w), # type: ignore
|
|
interpolation=InterpolationMode.BILINEAR,
|
|
antialias=True,
|
|
)
|
|
img = img.permute(0, 2, 3, 1)
|
|
return img
|
|
|
|
|
|
class CropRect(NamedTuple):
|
|
x: int
|
|
y: int
|
|
width: int
|
|
height: int
|
|
|
|
|
|
@register_node("JWUncropNewRect", "Uncrop: New rect")
|
|
class _:
|
|
CATEGORY = "jamesWalker55"
|
|
INPUT_TYPES = lambda: {
|
|
"required": {
|
|
"x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
|
|
"y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
|
|
"width": (
|
|
"INT",
|
|
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1},
|
|
),
|
|
"height": (
|
|
"INT",
|
|
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1},
|
|
),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("CROP_RECT",)
|
|
FUNCTION = "execute"
|
|
|
|
def execute(
|
|
self,
|
|
x: int,
|
|
y: int,
|
|
width: int,
|
|
height: int,
|
|
) -> tuple[CropRect]:
|
|
return (CropRect(x, y, width, height),)
|
|
|
|
|
|
@register_node("JWUncropCrop", "Uncrop: Crop")
|
|
class _:
|
|
CATEGORY = "jamesWalker55"
|
|
INPUT_TYPES = lambda: {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"resize_length": ("INT", {"default": 512, "min": 8, "step": 8}),
|
|
"crop_rect": ("CROP_RECT",),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "execute"
|
|
|
|
def execute(
|
|
self,
|
|
image: torch.Tensor,
|
|
resize_length: int,
|
|
crop_rect: CropRect,
|
|
) -> tuple[torch.Tensor]:
|
|
x, y, width, height = crop_rect
|
|
|
|
# crop the image
|
|
image = crop_image(image, x, y, width, height)
|
|
|
|
shortest_side = min(width, height)
|
|
scale_ratio = resize_length / shortest_side
|
|
new_width = round(round(width * scale_ratio / 8) * 8)
|
|
new_height = round(round(height * scale_ratio / 8) * 8)
|
|
|
|
image = resize_image(image, new_width, new_height)
|
|
|
|
return (image,)
|
|
|
|
|
|
@register_node("JWUncropUncrop", "Uncrop: Uncrop")
|
|
class _:
|
|
CATEGORY = "jamesWalker55"
|
|
INPUT_TYPES = lambda: {
|
|
"required": {
|
|
"original_image": ("IMAGE",),
|
|
"cropped_image": ("IMAGE",),
|
|
"cropped_mask": ("MASK",),
|
|
"crop_rect": ("CROP_RECT",),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "execute"
|
|
|
|
def execute(
|
|
self,
|
|
original_image: torch.Tensor,
|
|
cropped_image: torch.Tensor,
|
|
cropped_mask: torch.Tensor,
|
|
crop_rect: CropRect,
|
|
) -> tuple[torch.Tensor]:
|
|
x, y, width, height = crop_rect
|
|
|
|
validate_bounds(original_image, x, y, width, height)
|
|
|
|
# resize cropped image if needed
|
|
_, _h, _w, _ = cropped_image.shape
|
|
if _w != width or _h != height:
|
|
cropped_image = resize_image(cropped_image, width, height)
|
|
|
|
# resize cropped mask if needed
|
|
_h, _w = cropped_mask.shape[-2:]
|
|
if _w != width or _h != height:
|
|
cropped_mask = torch.reshape(cropped_mask, (1, _h, _w, 1))
|
|
cropped_mask = resize_image(cropped_mask, width, height)
|
|
cropped_mask = torch.reshape(cropped_mask, (height, width))
|
|
|
|
to_x = x + width
|
|
to_y = y + height
|
|
|
|
# https://easings.net/#easeOutQuint
|
|
weighted_mask = 1 - (1 - cropped_mask) ** 5
|
|
|
|
# blend original image with cropped image using mask
|
|
cropped_image = original_image[:, y:to_y, x:to_x, :] * (
|
|
1 - weighted_mask.view(1, *weighted_mask.shape, 1)
|
|
) + cropped_image * weighted_mask.view(1, *weighted_mask.shape, 1)
|
|
|
|
# paste cropped image into original image
|
|
original_image = original_image.clone()
|
|
original_image[:, y:to_y, x:to_x, :] = cropped_image
|
|
|
|
return (original_image,)
|