added more nodes
This commit is contained in:
@@ -24,6 +24,21 @@ Chromatic Abarration adds a color shift to the image. Useful for making the imag
|
||||
### Add Text To Image
|
||||
Supply the path to a .ttf file and add text to an image input. Has options for achor placement, rotation, color and more.
|
||||
|
||||
### Blending
|
||||
Blending node that supports various blending modes. Uses [blend-modes](https://github.com/flrs/blend_modes) under the hood. Will install the module upon installation.
|
||||
|
||||
### Displacement
|
||||
Displacement node that can distort an image using a supplied mask.
|
||||
|
||||
### Generate Noise
|
||||
Generate various noises to use in masks or blending.
|
||||
|
||||
### Flatten Colors
|
||||
Flatten the colors of an image to a variable amount of colors.
|
||||
|
||||
|
||||
|
||||
|
||||
## Citations
|
||||
Pixel sort by: [satyarth](https://github.com/satyarth)
|
||||
Pixel Sort by: [satyarth](https://github.com/satyarth)
|
||||
Blend Modes by: [flrs](https://github.com/flrs/)
|
||||
@@ -0,0 +1,106 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import subprocess
|
||||
import sys
|
||||
try:
|
||||
import blend_modes
|
||||
except ModuleNotFoundError:
|
||||
# install pixelsort in current venv
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "blend-modes"])
|
||||
import blend_modes
|
||||
|
||||
class Blend():
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""
|
||||
Input Types
|
||||
"""
|
||||
return {
|
||||
"required": {
|
||||
"images_1": ("IMAGE",),
|
||||
"images_2": ("IMAGE",),},
|
||||
"optional": {
|
||||
"blend_mode": ([
|
||||
"soft_light",
|
||||
"lighten_only",
|
||||
"dodge",
|
||||
"addition",
|
||||
"darken_only",
|
||||
"multiply",
|
||||
"hard_light",
|
||||
"difference",
|
||||
"subtract",
|
||||
"grain_extract",
|
||||
"grain_merge",
|
||||
"divide",
|
||||
"overlay",
|
||||
"normal",
|
||||
],),
|
||||
"blend_opacity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply_blend"
|
||||
|
||||
CATEGORY = "VextraNodes"
|
||||
|
||||
def tensor_to_pil(self, img):
|
||||
if img is not None:
|
||||
i = 255. * img.cpu().numpy().squeeze()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
return img
|
||||
|
||||
def hack_alpha_channel(self, pil_image):
|
||||
# Create a new image with the same size and mode as the original image and fill it with opaque white
|
||||
new_image = Image.new('RGBA', pil_image.size, (255, 255, 255, 255))
|
||||
|
||||
# Paste the original image onto the new image
|
||||
new_image.paste(pil_image, (0, 0))
|
||||
return new_image
|
||||
|
||||
def apply_blend(self, images_1, images_2, blend_mode, blend_opacity):
|
||||
#create empty tensor with the same shape as images
|
||||
total_images = []
|
||||
blend_fn = getattr(blend_modes, blend_mode)
|
||||
if len(images_1) > len(images_2):
|
||||
raise Exception("BLEND: Second set of images cannot be less than the first set of images!")
|
||||
for i, image_1 in enumerate(images_1):
|
||||
image = self.tensor_to_pil(image_1)
|
||||
image = self.hack_alpha_channel(image)
|
||||
|
||||
image_2 = self.tensor_to_pil(images_2[i])
|
||||
image_2 = self.hack_alpha_channel(image_2)
|
||||
|
||||
if image.size != image_2.size:
|
||||
raise Exception("BLEND: Images must be the same size!")
|
||||
|
||||
|
||||
image = np.array(image)
|
||||
image = image.astype(float)
|
||||
|
||||
image_2 = np.array(image_2)
|
||||
image_2 = image_2.astype(float)
|
||||
|
||||
out_image = blend_fn(image, image_2, blend_opacity)
|
||||
out_image = Image.fromarray(out_image.astype(np.uint8))
|
||||
|
||||
# convert to tensor
|
||||
out_image = np.array(out_image.convert("RGB")).astype(np.float32) / 255.0
|
||||
out_image = torch.from_numpy(out_image).unsqueeze(0)
|
||||
total_images.append(out_image)
|
||||
|
||||
|
||||
total_images = torch.cat(total_images, 0)
|
||||
return (total_images,)
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Blend": Blend,
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import math
|
||||
|
||||
class Chromatic_Aberration():
|
||||
"""
|
||||
This node provides a simple interface to apply PixelSort blur to the output image.
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""
|
||||
Input Types
|
||||
"""
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),},
|
||||
"optional": {
|
||||
"chromatic_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "do_chromatic"
|
||||
|
||||
CATEGORY = "VextraNodes"
|
||||
|
||||
def tensor_to_pil(self, img):
|
||||
if img is not None:
|
||||
i = 255. * img.cpu().numpy().squeeze()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
return img
|
||||
|
||||
def do_chromatic(self, images, chromatic_strength=0):
|
||||
#create empty tensor with the same shape as images
|
||||
total_images = []
|
||||
for image in images:
|
||||
image = self.tensor_to_pil(image)
|
||||
image = add_chromatic(image, chromatic_strength)
|
||||
# convert to tensor
|
||||
out_image = np.array(image.convert("RGB")).astype(np.float32) / 255.0
|
||||
out_image = torch.from_numpy(out_image).unsqueeze(0)
|
||||
total_images.append(out_image)
|
||||
|
||||
|
||||
total_images = torch.cat(total_images, 0)
|
||||
return (total_images,)
|
||||
|
||||
def add_chromatic(im, strength: float = 0):
|
||||
if strength == 0:
|
||||
return im
|
||||
r, g, b = im.split()
|
||||
rdata = np.asarray(r)
|
||||
rfinal = r
|
||||
gfinal = g
|
||||
bfinal = b
|
||||
|
||||
# enlarge the green and blue channels slightly, blue being the most enlarged
|
||||
gfinal = gfinal.resize((round((1 + 0.018 * strength) * rdata.shape[1]),
|
||||
round((1 + 0.018 * strength) * rdata.shape[0])), Image.ANTIALIAS)
|
||||
bfinal = bfinal.resize((round((1 + 0.044 * strength) * rdata.shape[1]),
|
||||
round((1 + 0.044 * strength) * rdata.shape[0])), Image.ANTIALIAS)
|
||||
|
||||
rwidth, rheight = rfinal.size
|
||||
gwidth, gheight = gfinal.size
|
||||
bwidth, bheight = bfinal.size
|
||||
rhdiff = (bheight - rheight) // 2
|
||||
rwdiff = (bwidth - rwidth) // 2
|
||||
ghdiff = (bheight - gheight) // 2
|
||||
gwdiff = (bwidth - gwidth) // 2
|
||||
|
||||
# Centre the channels
|
||||
im = Image.merge("RGB", (
|
||||
rfinal.crop((-rwdiff, -rhdiff, bwidth - rwdiff, bheight - rhdiff)),
|
||||
gfinal.crop((-gwdiff, -ghdiff, bwidth - gwdiff, bheight - ghdiff)),
|
||||
bfinal))
|
||||
|
||||
# Crop the image to the original image dimensions
|
||||
return im.crop((rwdiff, rhdiff, rwidth + rwdiff, rheight + rhdiff))
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Chromatic Aberration": Chromatic_Aberration
|
||||
}
|
||||
|
||||
|
||||
def cartesian_to_polar(data: np.ndarray) -> np.ndarray:
|
||||
"""Returns the polar form of <data>
|
||||
"""
|
||||
width = data.shape[1]
|
||||
height = data.shape[0]
|
||||
assert (width > 2)
|
||||
assert (height > 2)
|
||||
assert (width % 2 == 1)
|
||||
assert (height % 2 == 1)
|
||||
perimeter = 2 * (width + height - 2)
|
||||
halfdiag = math.ceil(((width ** 2 + height ** 2) ** 0.5) / 2)
|
||||
halfw = width // 2
|
||||
halfh = height // 2
|
||||
ret = np.zeros((halfdiag, perimeter, 3))
|
||||
|
||||
# Don't want to deal with divide by zero errors...
|
||||
ret[0:(halfw + 1), halfh] = data[halfh, halfw::-1]
|
||||
ret[0:(halfw + 1), height + width - 2 +
|
||||
halfh] = data[halfh, halfw:(halfw * 2 + 1)]
|
||||
ret[0:(halfh + 1), height - 1 + halfw] = data[halfh:(halfh * 2 + 1), halfw]
|
||||
ret[0:(halfh + 1), perimeter - halfw] = data[halfh::-1, halfw]
|
||||
|
||||
# Divide the image into 8 triangles, and use the same calculation on
|
||||
# 4 triangles at a time. This is possible due to symmetry.
|
||||
# This section is also responsible for the corner pixels
|
||||
for i in range(0, halfh):
|
||||
slope = (halfh - i) / (halfw)
|
||||
diagx = ((halfdiag ** 2) / (slope ** 2 + 1)) ** 0.5
|
||||
unit_xstep = diagx / (halfdiag - 1)
|
||||
unit_ystep = diagx * slope / (halfdiag - 1)
|
||||
for row in range(halfdiag):
|
||||
ystep = round(row * unit_ystep)
|
||||
xstep = round(row * unit_xstep)
|
||||
if ((halfh >= ystep) and halfw >= xstep):
|
||||
ret[row, i] = data[halfh - ystep, halfw - xstep]
|
||||
ret[row, height - 1 - i] = data[halfh + ystep, halfw - xstep]
|
||||
ret[row, height + width - 2 +
|
||||
i] = data[halfh + ystep, halfw + xstep]
|
||||
ret[row, height + width + height - 3 -
|
||||
i] = data[halfh - ystep, halfw + xstep]
|
||||
else:
|
||||
break
|
||||
|
||||
# Remaining 4 triangles
|
||||
for j in range(1, halfw):
|
||||
slope = (halfh) / (halfw - j)
|
||||
diagx = ((halfdiag ** 2) / (slope ** 2 + 1)) ** 0.5
|
||||
unit_xstep = diagx / (halfdiag - 1)
|
||||
unit_ystep = diagx * slope / (halfdiag - 1)
|
||||
for row in range(halfdiag):
|
||||
ystep = round(row * unit_ystep)
|
||||
xstep = round(row * unit_xstep)
|
||||
if (halfw >= xstep and halfh >= ystep):
|
||||
ret[row, height - 1 + j] = data[halfh + ystep, halfw - xstep]
|
||||
ret[row, height + width - 2 -
|
||||
j] = data[halfh + ystep, halfw + xstep]
|
||||
ret[row, height + width + height - 3 +
|
||||
j] = data[halfh - ystep, halfw + xstep]
|
||||
ret[row, perimeter - j] = data[halfh - ystep, halfw - xstep]
|
||||
else:
|
||||
break
|
||||
return ret
|
||||
|
||||
|
||||
def polar_to_cartesian(data: np.ndarray, width: int, height: int) -> np.ndarray:
|
||||
"""Returns the cartesian form of <data>.
|
||||
|
||||
<width> is the original width of the cartesian image
|
||||
<height> is the original height of the cartesian image
|
||||
"""
|
||||
assert (width > 2)
|
||||
assert (height > 2)
|
||||
assert (width % 2 == 1)
|
||||
assert (height % 2 == 1)
|
||||
perimeter = 2 * (width + height - 2)
|
||||
halfdiag = math.ceil(((width ** 2 + height ** 2) ** 0.5) / 2)
|
||||
halfw = width // 2
|
||||
halfh = height // 2
|
||||
ret = np.zeros((height, width, 3))
|
||||
|
||||
def div0():
|
||||
# Don't want to deal with divide by zero errors...
|
||||
ret[halfh, halfw::-1] = data[0:(halfw + 1), halfh]
|
||||
ret[halfh, halfw:(halfw * 2 + 1)] = data[0:(halfw + 1),
|
||||
height + width - 2 + halfh]
|
||||
ret[halfh:(halfh * 2 + 1), halfw] = data[0:(halfh + 1), height - 1 + halfw]
|
||||
ret[halfh::-1, halfw] = data[0:(halfh + 1), perimeter - halfw]
|
||||
|
||||
div0()
|
||||
|
||||
# Same code as above, except the order of the assignments are switched
|
||||
# Code blocks are split up for easier profiling
|
||||
def part1():
|
||||
for i in range(0, halfh):
|
||||
slope = (halfh - i) / (halfw)
|
||||
diagx = ((halfdiag ** 2) / (slope ** 2 + 1)) ** 0.5
|
||||
unit_xstep = diagx / (halfdiag - 1)
|
||||
unit_ystep = diagx * slope / (halfdiag - 1)
|
||||
for row in range(halfdiag):
|
||||
ystep = round(row * unit_ystep)
|
||||
xstep = round(row * unit_xstep)
|
||||
if ((halfh >= ystep) and halfw >= xstep):
|
||||
ret[halfh - ystep, halfw - xstep] = \
|
||||
data[row, i]
|
||||
ret[halfh + ystep, halfw - xstep] = \
|
||||
data[row, height - 1 - i]
|
||||
ret[halfh + ystep, halfw + xstep] = \
|
||||
data[row, height + width - 2 + i]
|
||||
ret[halfh - ystep, halfw + xstep] = \
|
||||
data[row, height + width + height - 3 - i]
|
||||
else:
|
||||
break
|
||||
|
||||
part1()
|
||||
|
||||
def part2():
|
||||
for j in range(1, halfw):
|
||||
slope = (halfh) / (halfw - j)
|
||||
diagx = ((halfdiag ** 2) / (slope ** 2 + 1)) ** 0.5
|
||||
unit_xstep = diagx / (halfdiag - 1)
|
||||
unit_ystep = diagx * slope / (halfdiag - 1)
|
||||
for row in range(halfdiag):
|
||||
ystep = round(row * unit_ystep)
|
||||
xstep = round(row * unit_xstep)
|
||||
if (halfw >= xstep and halfh >= ystep):
|
||||
ret[halfh + ystep, halfw - xstep] = \
|
||||
data[row, height - 1 + j]
|
||||
ret[halfh + ystep, halfw + xstep] = \
|
||||
data[row, height + width - 2 - j]
|
||||
ret[halfh - ystep, halfw + xstep] = \
|
||||
data[row, height + width + height - 3 + j]
|
||||
ret[halfh - ystep, halfw - xstep] = \
|
||||
data[row, perimeter - j]
|
||||
else:
|
||||
break
|
||||
|
||||
part2()
|
||||
|
||||
# Repairs black/missing pixels in the transformed image
|
||||
def set_zeros():
|
||||
zero_mask = ret[1:-1, 1:-1] == 0
|
||||
ret[1:-1, 1:-1] = np.where(zero_mask, (ret[:-2, 1:-1] + ret[2:, 1:-1]) / 2, ret[1:-1, 1:-1])
|
||||
|
||||
set_zeros()
|
||||
|
||||
return ret
|
||||
@@ -0,0 +1,96 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
class Displacement_Map():
|
||||
"""
|
||||
This node provides a simple interface to apply PixelSort blur to the output image.
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""
|
||||
Input Types
|
||||
"""
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"displacement_maps": ("IMAGE",),},
|
||||
"optional": {
|
||||
"scale": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 500.0, "step": 0.1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "do_displace"
|
||||
|
||||
CATEGORY = "VextraNodes"
|
||||
|
||||
def tensor_to_pil(self, img):
|
||||
if img is not None:
|
||||
i = 255. * img.cpu().numpy().squeeze()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
return img
|
||||
|
||||
def do_displace(self, images, displacement_maps, scale):
|
||||
#create empty tensor with the same shape as images
|
||||
total_images = []
|
||||
|
||||
if len(images) > len(displacement_maps):
|
||||
raise Exception("Number of images must be equal or less than the number of displacement maps!")
|
||||
|
||||
for i, image in enumerate(images):
|
||||
displacement_map = displacement_maps[i]
|
||||
|
||||
displacement_map = self.tensor_to_pil(displacement_map)
|
||||
image = self.tensor_to_pil(image)
|
||||
|
||||
if displacement_map.size != image.size:
|
||||
raise Exception("Displacement map and image must be the same size!")
|
||||
|
||||
image = apply_displacement_map(image, displacement_map, scale)
|
||||
|
||||
|
||||
# convert to tensor
|
||||
out_image = np.array(image.convert("RGB")).astype(np.float32) / 255.0
|
||||
out_image = torch.from_numpy(out_image).unsqueeze(0)
|
||||
total_images.append(out_image)
|
||||
|
||||
|
||||
total_images = torch.cat(total_images, 0)
|
||||
return (total_images,)
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Displacement Map": Displacement_Map
|
||||
}
|
||||
|
||||
def apply_displacement_map(image, displacement_map, scale):
|
||||
# Convert PIL images to NumPy arrays
|
||||
image_array = np.array(image)
|
||||
displacement_map_array = np.array(displacement_map)
|
||||
|
||||
# Get the dimensions of the image
|
||||
height, width, _ = image_array.shape
|
||||
|
||||
# Calculate the displacement offsets based on the scale factor
|
||||
displacement_offsets = (displacement_map_array / 255 - 0.5) * scale
|
||||
|
||||
# Create arrays for the X and Y coordinates of the pixels
|
||||
x_coords, y_coords = np.meshgrid(np.arange(width), np.arange(height))
|
||||
|
||||
# Apply the displacement offsets to the X and Y coordinates
|
||||
x_displaced = (x_coords + displacement_offsets[..., 0]).clip(0, width - 1).astype(int)
|
||||
y_displaced = (y_coords + displacement_offsets[..., 1]).clip(0, height - 1).astype(int)
|
||||
|
||||
# Create a new array with the same shape as the original image and copy the displaced pixels
|
||||
displaced_image_array = np.zeros_like(image_array)
|
||||
displaced_image_array[y_coords, x_coords] = image_array[y_displaced, x_displaced]
|
||||
|
||||
# Convert the displaced image array back to a PIL image
|
||||
displaced_image = Image.fromarray(displaced_image_array)
|
||||
|
||||
return displaced_image
|
||||
@@ -0,0 +1,54 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
class Flatten_Colors():
|
||||
"""
|
||||
This node provides a simple interface to apply PixelSort blur to the output image.
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""
|
||||
Input Types
|
||||
"""
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),},
|
||||
"optional": {
|
||||
"number_of_colors": ("INT", {"default": 5, "min": 1, "max": 4000, "step": 1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "flatten"
|
||||
|
||||
CATEGORY = "VextraNodes"
|
||||
|
||||
def tensor_to_pil(self, img):
|
||||
if img is not None:
|
||||
i = 255. * img.cpu().numpy().squeeze()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
return img
|
||||
|
||||
def flatten(self, images, number_of_colors):
|
||||
#create empty tensor with the same shape as images
|
||||
total_images = []
|
||||
for image in images:
|
||||
image = self.tensor_to_pil(image)
|
||||
image = image.convert('P', palette=Image.ADAPTIVE, colors=number_of_colors)
|
||||
|
||||
# convert to tensor
|
||||
out_image = np.array(image.convert("RGB")).astype(np.float32) / 255.0
|
||||
out_image = torch.from_numpy(out_image).unsqueeze(0)
|
||||
total_images.append(out_image)
|
||||
|
||||
|
||||
total_images = torch.cat(total_images, 0)
|
||||
return (total_images,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Flatten Colors": Flatten_Colors
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
def create_noise(mode='gaussian', scale=0.1, width=512, height=512):
|
||||
# Create empty image
|
||||
noise = np.zeros((height, width, 3), dtype=np.float32)
|
||||
|
||||
if mode == 'gaussian':
|
||||
noise = np.random.normal(0, scale * 255, noise.shape).astype(np.float32)
|
||||
elif mode == 'uniform':
|
||||
noise = np.random.uniform(-scale * 255, scale * 255, noise.shape).astype(np.float32)
|
||||
elif mode == 'salt_and_pepper':
|
||||
salt = np.random.rand(*noise.shape[:2]) < scale / 2
|
||||
pepper = np.random.rand(*noise.shape[:2]) < scale / 2
|
||||
noise[..., 0] = np.where(salt, 255, 0)
|
||||
noise[..., 1] = np.where(pepper, 255, 0)
|
||||
noise[..., 2] = np.where(np.logical_not(salt | pepper), 255, 0)
|
||||
else:
|
||||
print(f'Unknown noise mode: {mode}')
|
||||
|
||||
return Image.fromarray(noise.astype(np.uint8), 'RGB')
|
||||
|
||||
|
||||
class NoiseImage():
|
||||
"""
|
||||
This node provides a simple interface to apply PixelSort blur to the output image.
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""
|
||||
Input Types
|
||||
"""
|
||||
return {
|
||||
"required": {
|
||||
"mode": (['gaussian', 'uniform', 'salt_and_pepper'],),
|
||||
"noise_scale": ("FLOAT", {"default": 1, "min": 0.0, "max": 100.0, "step": 0.01}),
|
||||
"width": ("INT", {"default": 512, "min": 1, "max": 10000, "step": 1}),
|
||||
"height": ("INT", {"default": 512, "min": 1, "max": 10000, "step": 1}),
|
||||
"batch_size": ("INT", {"default": 1, "min": 1, "max": 10000, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "do_noise"
|
||||
|
||||
CATEGORY = "VextraNodes"
|
||||
|
||||
def do_noise(self, mode, noise_scale, width, height, batch_size):
|
||||
#create empty tensor with the same shape as images
|
||||
total_images = []
|
||||
for i in range(batch_size):
|
||||
image = create_noise(mode, noise_scale, width, height)
|
||||
# convert to tensor
|
||||
out_image = np.array(image.convert("RGB")).astype(np.float32) / 255.0
|
||||
out_image = torch.from_numpy(out_image).unsqueeze(0)
|
||||
total_images.append(out_image)
|
||||
|
||||
|
||||
total_images = torch.cat(total_images, 0)
|
||||
return (total_images,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Generate Noise Image": NoiseImage
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import math
|
||||
|
||||
def or_convert(im, mode):
|
||||
return im if im.mode == mode else im.convert(mode)
|
||||
|
||||
def hue_rotate(im, deg=0):
|
||||
cos_hue = math.cos(math.radians(deg))
|
||||
sin_hue = math.sin(math.radians(deg))
|
||||
|
||||
matrix = [
|
||||
.213 + cos_hue * .787 - sin_hue * .213,
|
||||
.715 - cos_hue * .715 - sin_hue * .715,
|
||||
.072 - cos_hue * .072 + sin_hue * .928,
|
||||
0,
|
||||
.213 - cos_hue * .213 + sin_hue * .143,
|
||||
.715 + cos_hue * .285 + sin_hue * .140,
|
||||
.072 - cos_hue * .072 - sin_hue * .283,
|
||||
0,
|
||||
.213 - cos_hue * .213 - sin_hue * .787,
|
||||
.715 - cos_hue * .715 + sin_hue * .715,
|
||||
.072 + cos_hue * .928 + sin_hue * .072,
|
||||
0,
|
||||
]
|
||||
|
||||
rotated = or_convert(im, 'RGB').convert('RGB', matrix)
|
||||
return or_convert(rotated, im.mode)
|
||||
|
||||
|
||||
class HueRotation():
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""
|
||||
Input Types
|
||||
"""
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),},
|
||||
"optional": {
|
||||
"hue_rotation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 360.0, "step": 0.1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply_hr"
|
||||
|
||||
CATEGORY = "VextraNodes"
|
||||
|
||||
def tensor_to_pil(self, img):
|
||||
if img is not None:
|
||||
i = 255. * img.cpu().numpy().squeeze()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
return img
|
||||
|
||||
def apply_hr(self, images, hue_rotation):
|
||||
#create empty tensor with the same shape as images
|
||||
total_images = []
|
||||
for image in images:
|
||||
image = self.tensor_to_pil(image)
|
||||
image = hue_rotate(image, hue_rotation)
|
||||
# convert to tensor
|
||||
out_image = np.array(image.convert("RGB")).astype(np.float32) / 255.0
|
||||
out_image = torch.from_numpy(out_image).unsqueeze(0)
|
||||
total_images.append(out_image)
|
||||
|
||||
|
||||
total_images = torch.cat(total_images, 0)
|
||||
return (total_images,)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Hue Rotation": HueRotation,
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import subprocess
|
||||
import sys
|
||||
try:
|
||||
import pilgram
|
||||
except ModuleNotFoundError:
|
||||
# install pixelsort in current venv
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "pilgram"])
|
||||
import pilgram
|
||||
|
||||
class ApplyFilter():
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""
|
||||
Input Types
|
||||
"""
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),},
|
||||
"optional": {
|
||||
"instagram_filter": ([
|
||||
"_1977",
|
||||
"aden",
|
||||
"brannan",
|
||||
"brooklyn",
|
||||
"clarendon",
|
||||
"earlybird",
|
||||
"gingham",
|
||||
"hudson",
|
||||
"inkwell",
|
||||
"kelvin",
|
||||
"lark",
|
||||
"lofi",
|
||||
"maven",
|
||||
"mayfair",
|
||||
"moon",
|
||||
"nashville",
|
||||
"perpetua",
|
||||
"reyes",
|
||||
"rise",
|
||||
"slumber",
|
||||
"stinson",
|
||||
"toaster",
|
||||
"valencia",
|
||||
"walden",
|
||||
"willow",
|
||||
"xpro2",
|
||||
],),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply_filter"
|
||||
|
||||
CATEGORY = "VextraNodes"
|
||||
|
||||
def tensor_to_pil(self, img):
|
||||
if img is not None:
|
||||
i = 255. * img.cpu().numpy().squeeze()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
return img
|
||||
|
||||
def apply_filter(self, images, instagram_filter):
|
||||
#create empty tensor with the same shape as images
|
||||
total_images = []
|
||||
filter_fn = getattr(pilgram, instagram_filter)
|
||||
for image in images:
|
||||
image = self.tensor_to_pil(image)
|
||||
image = filter_fn(image)
|
||||
|
||||
# convert to tensor
|
||||
out_image = np.array(image.convert("RGB")).astype(np.float32) / 255.0
|
||||
out_image = torch.from_numpy(out_image).unsqueeze(0)
|
||||
total_images.append(out_image)
|
||||
|
||||
|
||||
total_images = torch.cat(total_images, 0)
|
||||
return (total_images,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Apply Instagram Filter": ApplyFilter,
|
||||
}
|
||||
@@ -46,7 +46,6 @@ class Swap_Color_Mode():
|
||||
return img
|
||||
|
||||
def do_swap(self, images, color_mode='default'):
|
||||
#create empty tensor with the same shape as images
|
||||
total_images = []
|
||||
for image in images:
|
||||
image = self.tensor_to_pil(image)
|
||||
|
||||
Reference in New Issue
Block a user