feat: ⚡️ initial commit

This commit is contained in:
melMass
2023-06-03 20:05:19 +02:00
commit 1ae3bbc89a
7 changed files with 551 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
__pycache__
*.py[cod]
*.onnx
+27
View File
@@ -0,0 +1,27 @@
from .nodes.image_processing import (
ImageCompare,
Denoise,
Blur,
HSVtoRGB,
RGBtoHSV,
ColorCorrect,
)
from .nodes.crop import Crop, Uncrop, BoundingBox
from .nodes.graph_utils import IntToNumber, Modulo
# NODE MAPPING
NODE_CLASS_MAPPINGS = {
"Int to Number (mtb)": IntToNumber,
"Bounding Box (mtb)": BoundingBox,
"Crop (mtb)": Crop,
"Uncrop (mtb)": Uncrop,
"ImageBlur (mtb)": Blur,
"Denoise (mtb)": Denoise,
"ImageCompare (mtb)": ImageCompare,
"RGB to HSV (mtb)": RGBtoHSV,
"HSV to RGB (mtb)": HSVtoRGB,
"Color Correct (mtb)": ColorCorrect,
"Modulo (mtb)": Modulo,
}
+142
View File
@@ -0,0 +1,142 @@
import torch
from ..utils import tensor2pil, pil2tensor
from PIL import Image, ImageFilter, ImageDraw
class BoundingBox:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"x": ("INT", {"default": 0, "max": 10000000, "min": 0, "step": 1}),
"y": ("INT", {"default": 0, "max": 10000000, "min": 0, "step": 1}),
"width": (
"INT",
{"default": 256, "max": 10000000, "min": 0, "step": 1},
),
"height": (
"INT",
{"default": 256, "max": 10000000, "min": 0, "step": 1},
),
}
}
RETURN_TYPES = ("BBOX",)
FUNCTION = "do_crop"
CATEGORY = "image/crop"
def do_crop(self, x, y, width, height):
return (x, y, width, height)
class Crop:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"x": ("INT", {"default": 0, "max": 10000000, "min": 0, "step": 1}),
"y": ("INT", {"default": 0, "max": 10000000, "min": 0, "step": 1}),
"width": (
"INT",
{"default": 256, "max": 10000000, "min": 0, "step": 1},
),
"height": (
"INT",
{"default": 256, "max": 10000000, "min": 0, "step": 1},
),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BBOX")
FUNCTION = "do_crop"
CATEGORY = "image/crop"
def do_crop(self, image: torch.Tensor, mask, x, y, width, height):
image = image.numpy()
mask = mask.numpy()
cropped_image = image[:, y : y + height, x : x + width, :]
cropped_mask = mask[y : y + height, x : x + width]
crop_data = (x, y, width, height)
return (
torch.from_numpy(cropped_image),
torch.from_numpy(cropped_mask),
crop_data,
)
class Uncrop:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"crop_image": ("IMAGE",),
"bbox": ("BBOX",),
"border_blending": (
"FLOAT",
{"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01},
),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "do_crop"
CATEGORY = "image/crop"
def do_crop(self, image, crop_image, bbox, border_blending):
def inset_border(image, border_width=20, border_color=(0)):
width, height = image.size
bordered_image = Image.new(image.mode, (width, height), border_color)
bordered_image.paste(image, (0, 0))
draw = ImageDraw.Draw(bordered_image)
draw.rectangle(
(0, 0, width - 1, height - 1), outline=border_color, width=border_width
)
return bordered_image
image = tensor2pil(image)
crop_img = tensor2pil(crop_image)
crop_img = crop_img.convert("RGB")
# uncrop the image based on the bounding box
bb_x, bb_y, bb_width, bb_height = bbox
if border_blending > 1.0:
border_blending = 1.0
elif border_blending < 0.0:
border_blending = 0.0
blend_ratio = (max(crop_img.size) / 2) * float(border_blending)
blend = image.convert("RGBA")
mask = Image.new("L", image.size, 0)
mask_block = Image.new("L", (bb_width, bb_height), 255)
mask_block = inset_border(mask_block, int(blend_ratio / 2), (0))
print(bbox)
mask.paste(mask_block, (bb_x, bb_y, bb_x + bb_width, bb_y + bb_height))
blend.paste(crop_img, (bb_x, bb_y, bb_x + bb_width, bb_y + bb_height))
mask = mask.filter(ImageFilter.BoxBlur(radius=blend_ratio / 4))
mask = mask.filter(ImageFilter.GaussianBlur(radius=blend_ratio / 4))
blend.putalpha(mask)
image = Image.alpha_composite(image.convert("RGBA"), blend)
return (pil2tensor(image.convert("RGB")),)
+41
View File
@@ -0,0 +1,41 @@
class Modulo:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"int": ("INT", {"default": 0, "min": 0, "max": 1e9, "step": 1}),
"mod": ("INT", {"default": 0, "min": 0, "max": 1e9, "step": 1}),
}
}
RETURN_TYPES = ("INT",)
FUNCTION = "modulo"
CATEGORY = "number"
def modulo(self, int, mod):
return ((int + 1) % (mod + 1),)
class IntToNumber:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"int": ("INT", {"default": 0, "min": 0, "max": 1e9, "step": 1}),
}
}
RETURN_TYPES = ("NUMBER",)
FUNCTION = "int_to_number"
CATEGORY = "number"
def int_to_number(self, int):
return (int,)
+322
View File
@@ -0,0 +1,322 @@
import torch
from skimage.filters import gaussian
from skimage.restoration import denoise_tv_chambolle
from skimage.util import compare_images
from skimage.color import rgb2hsv, hsv2rgb
import numpy as np
import torchvision.transforms.functional as F
from PIL import Image
from ..utils import tensor2pil, pil2tensor
class ColorCorrect:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"clamp": ([True, False], {"default": True}),
"gamma": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.01},
),
"contrast": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.01},
),
"exposure": (
"FLOAT",
{"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01},
),
"offset": (
"FLOAT",
{"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01},
),
"hue": (
"FLOAT",
{"default": 0.0, "min": -0.5, "max": 0.5, "step": 0.01},
),
"saturation": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.01},
),
"value": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.01},
),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "correct"
CATEGORY = "image/postprocessing"
@staticmethod
def gamma_correction_tensor(image, gamma):
gamma_inv = 1.0 / gamma
return image.pow(gamma_inv)
@staticmethod
def contrast_adjustment_tensor(image, contrast):
contrasted = (image - 0.5) * contrast + 0.5
return torch.clamp(contrasted, 0.0, 1.0)
@staticmethod
def exposure_adjustment_tensor(image, exposure):
return image * (2.0**exposure)
@staticmethod
def offset_adjustment_tensor(image, offset):
return image + offset
@staticmethod
def hsv_adjustment(image: torch.Tensor, hue, saturation, value):
image = tensor2pil(image)
hsv_image = image.convert("HSV")
h, s, v = hsv_image.split()
h = h.point(lambda x: (x + hue * 255) % 256)
s = s.point(lambda x: int(x * saturation))
v = v.point(lambda x: int(x * value))
hsv_image = Image.merge("HSV", (h, s, v))
rgb_image = hsv_image.convert("RGB")
return pil2tensor(rgb_image)
@staticmethod
def hsv_adjustment_tensor_not_working(image: torch.Tensor, hue, saturation, value):
"""Abandonning for now"""
image = image.squeeze(0).permute(2, 0, 1)
max_val, _ = image.max(dim=0, keepdim=True)
min_val, _ = image.min(dim=0, keepdim=True)
delta = max_val - min_val
hue_image = torch.zeros_like(max_val)
mask = delta != 0.0
r, g, b = image[0], image[1], image[2]
hue_image[mask & (max_val == r)] = ((g - b) / delta)[
mask & (max_val == r)
] % 6.0
hue_image[mask & (max_val == g)] = ((b - r) / delta)[
mask & (max_val == g)
] + 2.0
hue_image[mask & (max_val == b)] = ((r - g) / delta)[
mask & (max_val == b)
] + 4.0
saturation_image = delta / (max_val + 1e-7)
value_image = max_val
hue_image = (hue_image + hue) % 1.0
saturation_image = torch.where(
mask, saturation * saturation_image, saturation_image
)
value_image = value * value_image
c = value_image * saturation_image
x = c * (1 - torch.abs((hue_image % 2) - 1))
m = value_image - c
prime_image = torch.zeros_like(image)
prime_image[0] = torch.where(
max_val == r, c, torch.where(max_val == g, x, prime_image[0])
)
prime_image[1] = torch.where(
max_val == r, x, torch.where(max_val == g, c, prime_image[1])
)
prime_image[2] = torch.where(
max_val == g, x, torch.where(max_val == b, c, prime_image[2])
)
rgb_image = prime_image + m
rgb_image = rgb_image.permute(1, 2, 0).unsqueeze(0)
return rgb_image
def correct(
self,
image: torch.Tensor,
clamp: bool,
gamma: float = 1.0,
contrast: float = 1.0,
exposure: float = 0.0,
offset: float = 0.0,
hue: float = 0.0,
saturation: float = 1.0,
value: float = 1.0,
):
# Apply color correction operations
image = self.gamma_correction_tensor(image, gamma)
image = self.contrast_adjustment_tensor(image, contrast)
image = self.exposure_adjustment_tensor(image, exposure)
image = self.offset_adjustment_tensor(image, offset)
image = self.hsv_adjustment(image, hue, saturation, value)
if clamp:
image = torch.clamp(image, 0.0, 1.0)
return (image,)
class HSVtoRGB:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "convert"
CATEGORY = "image/postprocessing"
def convert(self, image):
image = image.numpy()
image = image.squeeze()
# image = image.transpose(1,2,3,0)
image = hsv2rgb(image)
image = np.expand_dims(image, axis=0)
# image = image.transpose(3,0,1,2)
return (torch.from_numpy(image),)
class RGBtoHSV:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "convert"
CATEGORY = "image/postprocessing"
def convert(self, image):
image = image.numpy()
# image = image.transpose(1,2,3,0)
image = np.squeeze(image)
image = rgb2hsv(image)
image = np.expand_dims(image, axis=0)
# image = image.transpose(3,0,1,2)
return (torch.from_numpy(image),)
class ImageCompare:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"imageA": ("IMAGE",),
"imageB": ("IMAGE",),
"mode": (
["checkerboard", "diff", "blend"],
{"default": "checkerboard"},
),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "compare"
CATEGORY = "image"
def compare(self, imageA: torch.Tensor, imageB: torch.Tensor, mode):
imageA = imageA.numpy()
imageB = imageB.numpy()
imageA = imageA.squeeze()
imageB = imageB.squeeze()
image = compare_images(imageA, imageB, method=mode)
image = np.expand_dims(image, axis=0)
return (torch.from_numpy(image),)
class Denoise:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"weight": (
"FLOAT",
{"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01},
),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "denoise"
CATEGORY = "image/postprocessing"
def denoise(self, image: torch.Tensor, weight):
image = image.numpy()
# image = image.transpose(1,2,3,0)
image = image.squeeze()
image = denoise_tv_chambolle(image, weight=weight)
# image = image.transpose(3,0,1,2)
image = np.expand_dims(image, axis=0)
return (torch.from_numpy(image),)
class Blur:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"sigmaX": (
"FLOAT",
{"default": 3.0, "min": 0.0, "max": 10.0, "step": 0.01},
),
"sigmaY": (
"FLOAT",
{"default": 3.0, "min": 0.0, "max": 10.0, "step": 0.01},
),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "blur"
CATEGORY = "image/postprocessing"
def blur(self, image: torch.Tensor, sigmaX, sigmaY):
image = image.numpy()
image = image.transpose(1, 2, 3, 0)
# image = ndimage.gaussian_filter(image, sigma)
image = gaussian(image, sigma=(sigmaX, sigmaY, 0, 0))
# (image, sigma=sigma, multichannel=True)
image = image.transpose(3, 0, 1, 2)
return (torch.from_numpy(image),)
+2
View File
@@ -0,0 +1,2 @@
onnxruntime
imageio
+14
View File
@@ -0,0 +1,14 @@
from PIL import Image
import numpy as np
import torch
# Tensor to PIL (grabbed from WAS Suite)
def tensor2pil(image: torch.Tensor) -> Image.Image:
return Image.fromarray(
np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
)
# Convert PIL to Tensor (grabbed from WAS Suite)
def pil2tensor(image: Image.Image) -> torch.Tensor:
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)