Files
mirabarukaso 068e2f4f2e 0.6.0.0
Update to V3 node
2026-10-01 02:20:01 +09:00

1531 lines
47 KiB
Python

import logging
logger = logging.getLogger("comfy")
from comfy_api.latest import IO
from .v3schema import node
import math
import random
import torch
import numpy as np
from PIL import Image
import cv2
from .color_flat import flat_color_multi_scale
from .color_transfer import ColorTransfer
from comfy import model_management
import torchvision.transforms as T
import torchvision.transforms.functional as con
import folder_paths
import comfy.utils
import base64
import gzip
from io import BytesIO
class AlwaysEqualProxy(str):
#ComfyUI-Logic
#refer: https://github.com/theUpsider/ComfyUI-Logic
def __eq__(self, _):
return True
def __ne__(self, _):
return False
cat = "Mira/Util"
cat_image = "Mira/Util/Image"
def DecodeImage(src_image):
i = 255. * src_image[0].cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return img
def EncodeImage(src_image):
img = np.array(src_image).astype(np.float32) / 255.0
img = torch.from_numpy(img)[None,]
return img
def ConvertToNP(src_image):
i = 255. * src_image[0].cpu().numpy()
array_image = np.clip(i, 0, 255).astype(np.uint8)
return array_image.astype(np.float32)
def SafeCheck(Width = 16, Height = 16, Batch = 1, HiResMultiplier = 1.0):
if 16 > Width:
Width = 16
if 16 > Height:
Height = 16
if 1 > Batch:
Batch = 1
if 1.0 > HiResMultiplier:
HiResMultiplier = 1.0
return Width, Height, Batch, HiResMultiplier
def Fixeight(num):
new_num = int(num)
if 0 != math.floor(num)%8:
residue = math.floor(num)%8
if 3 >= math.floor(num)%8:
new_num = math.floor(num) - residue
else:
new_num = math.floor(num) + 8 - residue
return new_num
def decompress_base64_gzip(base64_string):
try:
gzipped_data = base64.b64decode(base64_string)
decompressed_data = gzip.decompress(gzipped_data)
return bytearray(decompressed_data)
except Exception as error:
logger.error(f'[decompress_base64_gzip]: Error on decompressing: {error}')
return None
class CanvasCreatorBasic(IO.ComfyNode):
'''
Create Canvas information Width and Height for Latent.
Inputs:
Width - Image Width
Height - Image Height
Outputs:
Width - Image Width
Height - Image Height
'''
@classmethod
def define_schema(cls):
return node(
"CanvasCreatorBasic",
cat,
cls._v1_inputs(),
("INT","INT",),
("Width","Height",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"Width": ("INT", {
"default": 576,
"min": 16,
"max": 4096,
"step": 8,
"display": "number"
}),
"Height": ("INT", {
"default": 1024,
"min": 16,
"max": 4096,
"step": 8,
"display": "number"
}),
},
}
@classmethod
def execute(cls, Width, Height):
Width, Height, Batch, HiResMultiplier = SafeCheck(Width, Height)
return(Width, Height,)
class CanvasCreatorSimple(IO.ComfyNode):
'''
Create Canvas information Width and Height for Latent with Landscape switch.
Inputs:
Width - Image Width
Height - Image Height
Landscape - When ENABLED, will swap Width and Height for output
Outputs:
Width - Image Width
Height - Image Height
'''
@classmethod
def define_schema(cls):
return node(
"CanvasCreatorSimple",
cat,
cls._v1_inputs(),
("INT","INT",),
("Width","Height",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"Width": ("INT", {
"default": 576,
"min": 16,
"max": 4096,
"step": 8,
"display": "number"
}),
"Height": ("INT", {
"default": 1024,
"min": 16,
"max": 4096,
"step": 8,
"display": "number"
}),
"Landscape": ("BOOLEAN", {
"default": False
}),
},
}
@classmethod
def execute(cls, Width, Height, Landscape):
Width, Height, Batch, HiResMultiplier = SafeCheck(Width, Height)
if(False == Landscape):
return(Width, Height,)
else:
return(Height, Width,)
class CanvasCreatorAdvanced(IO.ComfyNode):
'''
Create Canvas information Width and Height for Latent with Landscape switch, Batch and HiResMultiplier.
Inputs:
Width - Image Width
Height - Image Height
Landscape - When ENABLED, will swap Width and Height for output
Batch - Batch size for Latent
HiResMultiplier - Multiplier setting for high-resolution output
Outputs:
Width - Image Width for Latent
Height - Image Height for Latent
Batch - Batch size for Latent
HiRes Width - Width x HiResMultiplier. The result is not the product of the original data, but the nearest multiple of 8.
HiRes Height - Height x HiResMultiplier.
HiResMultiplier - Same as Input
'''
@classmethod
def define_schema(cls):
return node(
"CanvasCreatorAdvanced",
cat,
cls._v1_inputs(),
("INT","INT","INT","INT","INT","FLOAT"),
("Width","Height","Batch","HiRes Width","HiRes Height","HiResMultiplier",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"Width": ("INT", {
"default": 576,
"min": 16,
"max": 4096,
"step": 8,
"display": "number"
}),
"Height": ("INT", {
"default": 1024,
"min": 16,
"max": 4096,
"step": 8,
"display": "number"
}),
"Batch": ("INT", {
"default": 1,
"min": 1,
"max": 16,
"step": 1,
"display": "number"
}),
"Landscape": ("BOOLEAN", {
"default": False
}),
"HiResMultiplier": ("FLOAT", {
"default": 1.5,
"min": 0.1,
"max": 8,
"step": 0.1,
"display": "number"
}),
},
}
@classmethod
def execute(cls, Width, Height, Batch, Landscape, HiResMultiplier):
Width, Height, Batch, HiResMultiplier = SafeCheck(Width, Height, Batch, HiResMultiplier)
HiResWidth = Fixeight(Width * HiResMultiplier)
HiResHeight = Fixeight(Height * HiResMultiplier)
if(False == Landscape):
intHiResHeight = math.floor(HiResHeight)
intHiResWidth = math.floor(HiResWidth)
return(Width, Height, Batch, intHiResWidth, intHiResHeight, HiResMultiplier, )
else:
intHiResHeight = math.floor(HiResHeight)
intHiResWidth = math.floor(HiResWidth)
return(Height, Width, Batch, intHiResHeight, intHiResWidth, HiResMultiplier, )
class RandomTillingLayouts(IO.ComfyNode):
'''
[#1](https://github.com/mirabarukaso/ComfyUI_Mira/issues/1)
Random Tilling Mask Layout Generator
Highly recommend connect the output `layout` or `Create Tilling PNG Mask -> Debug` to `ShowText` node.
**Known Issue** about `Seed Generator`
Switching `randomize` to `fixed` now works immediately.
But, switching `fixed` to `randomize`, it need 2 times `Queue Prompt` to take affect. (Because of the ComfyUI logic)
Solution: Try `Global Seed (Inspire)` from [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack)
**Reminder **
The `rnd_seed` have nothing to do with the actual random numbers,
you can't get the same `layout` with the same `rnd_seed`,
it is recommended to use `ShowText` and `Notes` to save your favourite `layout`.
**Hint**
Set rows or colums to `0` for only one direction cuts.
Whichever is set to `0` will automatically cut according to the other non-zero setting.
Just in case all fours are `0`, it will return `1,1`.
Inputs:
min_rows, max_rows - Range of how many `N cuts` you may want, set both to 0 to disable it.
min_colums, max_colums - Range of how many `G cuts` you may want, set both to 0 to disable it.
max_weights_gcuts - The maxium weight of `G cuts` range from 1 to `max_weights_gcuts`
max_weights_ncuts - The maxium weight of `N cuts` range from 1 to `max_weights_ncuts`
rnd_seed - Connect to the `Seed Generator` node, then use `Global Seed (Inspire)` node to control it properly.
Outputs:
Layout - Layouts string, you need connect it to `Create Tilling PNG Mask -> layout`
Example:
[2,2,2,1]@3.8,4.2,2.1,3.3;3.6,3.5,3.3,3.7;2.7,3.2,4.9
^ * * *
^2 colums
* the 1st block has 2 rows (3 blocks)
* the 2nd block has 2 rows (3 blocks)
* the 3rd block has 1 row (2 blocks)
3+3+2= 8 blocks in total
Colum_first == False
1|4|7
2|5|
3|6|8
'''
@classmethod
def define_schema(cls):
return node(
"RandomTillingLayouts",
cat,
cls._v1_inputs(),
("STRING",),
("layout",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"min_rows": ("INT", {
"default": 1,
"min": 0,
"max": 16,
"step": 1,
"display": "number"
}),
"max_rows": ("INT", {
"default": 1,
"min": 0,
"max": 16,
"step": 1,
"display": "number"
}),
"min_colums": ("INT", {
"default": 1,
"min": 0,
"max": 16,
"step": 1,
"display": "number"
}),
"max_colums": ("INT", {
"default": 1,
"min": 0,
"max": 16,
"step": 1,
"display": "number"
}),
"max_weights_gcuts": ("FLOAT", {
"default": 2.0,
"min": 1.0,
"max": 10.0,
"step": 0.1,
"display": "number"
}),
"max_weights_ncuts": ("FLOAT", {
"default": 2.0,
"min": 1.0,
"max": 10.0,
"step": 0.1,
"display": "number"
}),
"rnd_seed": (AlwaysEqualProxy('*'), {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff,
"display": "input"
}),
},
}
@classmethod
def execute(cls, min_rows, max_rows, min_colums, max_colums, max_weights_gcuts, max_weights_ncuts, rnd_seed):
if min_colums > max_colums:
min_colums = max_colums
if min_rows > max_rows:
min_rows = max_rows
if 0 == max_colums and max_colums == max_rows:
layouts = "1,1"
return (layouts,)
if 0 == max_colums or 0 == max_rows:
max_colums = max(max_colums, max_rows)
colums = random.randrange(min_colums, max_colums + 1)
#print("Mira: colums: " + str(colums))
random.seed(rnd_seed)
row_and_colum_info = '[' + str(rnd_seed) + '][' + str(colums) + ","
layouts = ""
if 0 == colums:
rows = random.randrange(min_rows, max_rows + 1)
#print("Mira: colums = 0 rows: " + str(rows))
if 0 == rows:
row_and_colum_info += "0,"
layouts = "1,1"
else:
row_and_colum_info += str(rows) + ","
for _ in range(0, rows + 1):
layouts += str(round(random.uniform(1, max_weights_ncuts),1))
layouts += ","
layouts = layouts[:-1]
else:
for _ in range(0, colums + 1):
layouts += str(round(random.uniform(1, max_weights_gcuts),1))
rows = random.randrange(min_rows, max_rows + 1)
#print("Mira: rows: " + str(rows))
if 0 == rows:
row_and_colum_info += "0,"
layouts += ",1"
else:
row_and_colum_info += str(rows) + ","
layouts += ","
for _ in range(0, rows + 1):
layouts += str(round(random.uniform(1, max_weights_ncuts),1))
layouts += ","
layouts = layouts[:-1]
layouts += ";"
layouts = layouts[:-1]
row_and_colum_info = row_and_colum_info[:-1]
row_and_colum_info += ']@'
return (row_and_colum_info + layouts,)
class RandomNestedLayouts(IO.ComfyNode):
'''
Random Nested Mask Layout Generator
All known issues same as upper one.
Inputs:
min_nested, max_nested - Range of nest you want.
min_weights, max_weights - The weight of every nest.
rnd_seed - Connect to the `Seed Generator` node, then use `Global Seed (Inspire)` node to control it properly.
Outputs:
Layout - Layouts string, you need connect it to `Create Nested PNG Mask -> layout`
top, bottom, left, right - Random Boolean
'''
@classmethod
def define_schema(cls):
return node(
"RandomNestedLayouts",
cat,
cls._v1_inputs(),
("STRING", "BOOLEAN", "BOOLEAN", "BOOLEAN", "BOOLEAN"),
("layout", "top", "bottom", "left", "right"),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"min_nested": ("INT", {
"default": 1,
"min": 1,
"max": 32,
"step": 1,
"display": "number"
}),
"max_nested": ("INT", {
"default": 2,
"min": 1,
"max": 32,
"step": 1,
"display": "number"
}),
"min_weights": ("FLOAT", {
"default": 1.0,
"min": 0.1,
"max": 10.0,
"step": 0.1,
"display": "number"
}),
"max_weights": ("FLOAT", {
"default": 2.0,
"min": 1.0,
"max": 10.0,
"step": 0.1,
"display": "number"
}),
"rnd_seed": (AlwaysEqualProxy('*'), {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff,
"display": "input"
}),
},
}
@classmethod
def execute(cls, min_nested, max_nested, min_weights, max_weights, rnd_seed):
if min_nested > max_nested:
min_nested = max_nested
if min_weights > max_weights:
min_weights = max_weights
nested = random.randrange(min_nested, max_nested + 1)
bool1 = bool(random.getrandbits(1))
bool2 = bool(random.getrandbits(1))
bool3 = bool(random.getrandbits(1))
bool4 = bool(random.getrandbits(1))
random.seed(rnd_seed)
generator_info = '[' + str(rnd_seed) + '][' + str(nested) + '][' + str(bool1) + ',' + str(bool2) + ',' + str(bool3) + ',' + str(bool4) + ']@'
layouts = ""
for _ in range(0, nested):
layouts += str(round(random.uniform(min_weights, max_weights),1))
layouts += ','
layouts = layouts[:-1]
return (generator_info + layouts, bool1, bool2, bool3, bool4,)
class SeedGenerator(IO.ComfyNode):
'''
SeedGenerator
'''
@classmethod
def define_schema(cls):
return node(
"SeedGeneratorMira",
cat,
cls._v1_inputs(),
("INT",),
("seed",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff
}),
},
}
@classmethod
def execute(cls, seed):
return(seed,)
class ImageGrayscale(IO.ComfyNode):
'''
Convert Image to Grayscale
Inputs:
src_image - Source Image
Outputs:
image - Torched Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageGrayscale",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
},
}
@classmethod
def execute(cls, src_image):
img = DecodeImage(src_image)
img_adj = con.to_grayscale(img)
result = EncodeImage(img_adj)
return(result,)
class ImageContrast(IO.ComfyNode):
'''
Adjust Image Contrast
Inputs:
src_image - Source Image
level - Contrast Level, default is 1.0
Outputs:
image - Torched Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageContrast",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
"level": ("FLOAT", {
"default": 1.0,
"step": 0.001,
"min": 0,
"max": 10
}),
},
}
@classmethod
def execute(cls, src_image, level):
img = DecodeImage(src_image)
img_adj = con.adjust_contrast(img, level)
result = EncodeImage(img_adj)
return(result,)
class ImageSharpness(IO.ComfyNode):
'''
Adjust Image Sharpness
Inputs:
src_image - Source Image
level - Sharpness Level, default is 1.0
Outputs:
image - Torched Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageSharpness",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
"level": ("FLOAT", {
"default": 1.0,
"step": 0.1,
"min": 0,
"max": 100
}),
},
}
@classmethod
def execute(cls, src_image, level):
img = DecodeImage(src_image)
img_adj = con.adjust_sharpness(img, level)
result = EncodeImage(img_adj)
return(result,)
class ImageBrightness(IO.ComfyNode):
'''
Adjust Image Brightness
Inputs:
src_image - Source Image
level - Brightness Level, default is 1.0
Outputs:
image - Torched Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageBrightness",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
"level": ("FLOAT", {
"default": 1.0,
"step": 0.001,
"min": 0,
"max": 10
}),
},
}
@classmethod
def execute(cls, src_image, level):
img = DecodeImage(src_image)
img_adj = con.adjust_brightness(img, level)
result = EncodeImage(img_adj)
return(result,)
class ImageSaturation(IO.ComfyNode):
'''
Adjust Image Saturation
Inputs:
src_image - Source Image
level - Saturation Level, default is 0.0
Outputs:
image - Torched Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageSaturation",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
"level": ("FLOAT", {
"default": 0.0,
"step": 0.001,
"min": 0,
"max": 10
}),
},
}
@classmethod
def execute(cls, src_image, level):
img = DecodeImage(src_image)
img_adj = con.adjust_saturation(img, level)
result = EncodeImage(img_adj)
return(result,)
class ImageHUE(IO.ComfyNode):
'''
Adjust Image HUE
Inputs:
src_image - Source Image
level - HUE Level, default is 0.0
Outputs:
image - Torched Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageHUE",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
"level": ("FLOAT", {
"default": 0.0,
"step": 0.001,
"min": -0.5,
"max": 0.5
}),
},
}
@classmethod
def execute(cls, src_image, level):
img = DecodeImage(src_image)
img_adj = con.adjust_hue(img, level)
result = EncodeImage(img_adj)
return(result,)
class ImageGamma(IO.ComfyNode):
'''
Adjust Image Gamma
Inputs:
src_image - Source Image
level - Gamma Level, default is 0.0
Outputs:
image - Torched Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageGamma",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
"level": ("FLOAT", {
"default": 0.0,
"step": 0.001,
"min": 0,
"max": 10
}),
},
}
@classmethod
def execute(cls, src_image, level):
img = DecodeImage(src_image)
img_adj = con.adjust_gamma(img, level)
result = EncodeImage(img_adj)
return(result,)
class ImageColorTransfer(IO.ComfyNode):
'''
Refer to: https://en.wikipedia.org/wiki/Image_color_transfer
Image Color Transfer
Inputs:
src_image - Source Image
ref_image - Reference image. The colors of this Image will applied to the Source Image
Outputs:
image - Output Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageColorTransferMira",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
"ref_image": ("IMAGE", {
"default": None,
}),
"method" : (['Mean', 'Lab', 'Pdf', 'Pdf+Regrain'], ),
},
}
@classmethod
def execute(cls, src_image, ref_image, method):
if not isinstance(src_image, torch.Tensor):
raise ValueError("src_image must be a torch.Tensor")
if not isinstance(ref_image, torch.Tensor):
raise ValueError("ref_image must be a torch.Tensor")
if src_image.ndim == 3:
src_image = src_image.unsqueeze(0)
if ref_image.ndim == 3:
ref_image = ref_image.unsqueeze(0)
batch_size = src_image.shape[0]
ref_batch_size = ref_image.shape[0]
# Ensure ref_image batch size is either 1 or equal to src_image batch size
if ref_batch_size == 1:
ref_images = [ref_image[0]] * batch_size
elif ref_batch_size == batch_size:
ref_images = [ref_image[i] for i in range(batch_size)]
else:
raise ValueError("ref_image batch size must be 1 or equal to src_image batch size")
PT = ColorTransfer()
out_imgs = []
for i in range(batch_size):
s = src_image[i].unsqueeze(0)
r = ref_images[i].unsqueeze(0)
if method == "Mean":
s_np = ConvertToNP(s)
r_np = ConvertToNP(r)
new_img = PT.mean_std_transfer(img_arr_in=s_np, img_arr_ref=r_np)
elif method == "Lab":
s_pil = np.array(DecodeImage(s), dtype=np.uint8)
r_pil = np.array(DecodeImage(r), dtype=np.uint8)
new_img = PT.lab_transfer(img_arr_in=s_pil, img_arr_ref=r_pil)
elif method == "Pdf":
s_np = ConvertToNP(s)
r_np = ConvertToNP(r)
new_img = PT.pdf_transfer(img_arr_in=s_np, img_arr_ref=r_np, regrain=False)
else:
s_np = ConvertToNP(s)
r_np = ConvertToNP(r)
new_img = PT.pdf_transfer(img_arr_in=s_np, img_arr_ref=r_np, regrain=True)
result = EncodeImage(new_img)
out_imgs.append(result)
out_imgs_tensor = torch.cat(out_imgs, dim=0)
if out_imgs_tensor.ndim == 3:
out_imgs_tensor = out_imgs_tensor.unsqueeze(0)
return (out_imgs_tensor,)
class ImageToneCurve(IO.ComfyNode):
'''
Image Tone Curve
Adjust the overall brightness using the `RGB Channels` or `Brightness` node.
Inputs:
src_image - Source Image
low - Increase shadow range
high - Increase highlight range
Outputs:
image - Output Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageToneCurve",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
"low": ("FLOAT", {
"default": -1.0,
"step": 0.01,
"min": -10,
"max": 10
}),
"high": ("FLOAT", {
"default": 1.0,
"step": 0.01,
"min": -10,
"max": 10
}),
},
}
@classmethod
def execute(cls, src_image, low, high):
y = np.arctan(np.linspace(low, high, 256))
y = 255 / (y.max() - y.min()) * (y - y.max()) + 255
s = np.array(DecodeImage(src_image), dtype=np.uint8)
new_img = cv2.LUT(s, y).astype(np.uint8)
result = EncodeImage(new_img)
return(result,)
class ImageRGBChannel(IO.ComfyNode):
'''
Image RGB Channel
Inputs:
src_image - Source Image
R/G/B - Colour magnification value, less than 1.0 means attenuation
Outputs:
image - Output Image
'''
@classmethod
def define_schema(cls):
return node(
"ImageRGBChannel",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"src_image": ("IMAGE", {
"default": None,
}),
"Red": ("FLOAT", {
"default": 1.0,
"step": 0.01,
"min": 0,
"max": 5
}),
"Green": ("FLOAT", {
"default": 1.0,
"step": 0.01,
"min": 0,
"max": 5
}),
"Blue": ("FLOAT", {
"default": 1.0,
"step": 0.01,
"min": 0,
"max": 5
}),
},
}
@classmethod
def execute(cls, src_image, Red, Green, Blue):
s = DecodeImage(src_image)
r, g, b = s.split()
r = r.point(lambda i: i * Red)
g = g.point(lambda i: i * Green)
b = b.point(lambda i: i * Blue)
new_img = Image.merge('RGB', (r, g, b))
result = EncodeImage(new_img)
return(result,)
class UpscaleImageByModelThenResize(IO.ComfyNode):
'''
Upscale Image By Model Then Resize
This is an experimental feature for zooming in an image on a model and then zooming out to a specified size (a multiple of 8 in length and width).
For example, if the input model zooms the image 4x by default and the node is set to zoom 2x, then the image will first be zoomed 4x using the model and then resized to 2x.
Inputs:
upscale_model - Model for upscaling
image - Source Image
resize_scale - Real resize ratio, the result will be the nearest multiple of 8.
resize_method - Resize method, nearest, nearest-exact, bilinear, bicubic
Outputs:
image - Output Image
'''
@classmethod
def define_schema(cls):
return node(
"UpscaleImageByModelThenResize",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"upscale_model": ("UPSCALE_MODEL",),
"image": ("IMAGE", {
"default": None,
}),
"resize_scale": ("FLOAT", {
"default": 1.5,
"step": 0.1,
"min": 0.1,
"max": 8
}),
"resize_method" : (['lanczos', 'nearest', 'nearest-exact', 'bilinear', 'bicubic', 'box', 'hamming'], ),
},
}
@classmethod
def execute(cls, upscale_model, image, resize_scale, resize_method):
def resize_torch_image(img, size, interpolation_mode):
transform = T.Resize(size, interpolation=interpolation_mode, antialias=True)
resized_img = transform(DecodeImage(img))
result = EncodeImage(resized_img) # [1, C, H, W]
return result
# refer: https://github.com/comfyanonymous/ComfyUI/blob/4ffea0e864275301329ddb5ecc3fbc7211d7a802/comfy_extras/nodes_upscale_model.py#L50
def upscale(upscale_model, image):
device = model_management.get_torch_device()
memory_required = model_management.module_size(upscale_model.model)
memory_required += (512 * 512 * 3) * image.element_size() * max(upscale_model.scale, 1.0) * 384.0 #The 384.0 is an estimate of how much some of these models take, TODO: make it more accurate
memory_required += image.nelement() * image.element_size()
model_management.free_memory(memory_required, device)
upscale_model.to(device)
in_img = image.movedim(-1,-3).to(device)
tile = 512
overlap = 32
oom = True
while oom:
try:
steps = in_img.shape[0] * comfy.utils.get_tiled_scale_steps(in_img.shape[3], in_img.shape[2], tile_x=tile, tile_y=tile, overlap=overlap)
pbar = comfy.utils.ProgressBar(steps)
s = comfy.utils.tiled_scale(in_img, lambda a: upscale_model(a), tile_x=tile, tile_y=tile, overlap=overlap, upscale_amount=upscale_model.scale, pbar=pbar)
oom = False
except model_management.OOM_EXCEPTION as e:
tile //= 2
if tile < 128:
raise e
upscale_model.to("cpu")
s = torch.clamp(s.movedim(-3,-1), min=0, max=1.0)
return (s,)
interpolation_mode = T.InterpolationMode.NEAREST
if resize_method == 'nearest-exact':
interpolation_mode = T.InterpolationMode.NEAREST_EXACT
elif resize_method == 'bilinear':
interpolation_mode = T.InterpolationMode.BILINEAR
elif resize_method == 'bicubic':
interpolation_mode = T.InterpolationMode.BICUBIC
elif resize_method == 'lanczos':
interpolation_mode = T.InterpolationMode.LANCZOS
elif resize_method == 'box':
interpolation_mode = T.InterpolationMode.BOX
elif resize_method == 'hamming':
interpolation_mode = T.InterpolationMode.HAMMING
if image.shape[3] == 3: # [B, H, W, C]
width = image.shape[2]
height = image.shape[1]
else: # [B, C, H, W]
width = image.shape[3]
height = image.shape[2]
new_width = Fixeight(width*resize_scale)
new_height = Fixeight(height*resize_scale)
size = (new_height, new_width)
new_img = (upscale(upscale_model, image))[0]
batch_size = new_img.shape[0]
out_imgs = []
if batch_size == 1:
result = resize_torch_image(new_img, size, interpolation_mode) # [1, C, H, W]
out_imgs.append(result)
else:
for i in range(batch_size):
img_i = new_img[i] # [H, W, C]
result = resize_torch_image(img_i.unsqueeze(0), size, interpolation_mode) # [1, C, H, W]
out_imgs.append(result)
out_imgs_tensor = torch.cat(out_imgs, dim=0) # [B, C, H, W]
if out_imgs_tensor.ndim == 3:
out_imgs_tensor = out_imgs_tensor.unsqueeze(0)
return (out_imgs_tensor,)
class CheckpointLoaderSimple(IO.ComfyNode):
@classmethod
def define_schema(cls):
return node(
"CheckpointLoaderSimpleMira",
cat,
cls._v1_inputs(),
("MODEL", "CLIP", "VAE", "STRING"),
("MODEL", "CLIP", "VAE", "model_name"),
description="Loads a diffusion model checkpoint, diffusion models are used to denoise latents.",
output_tooltips=("The model used for denoising latents.",
"The CLIP model used for encoding text prompts.",
"The VAE model used for encoding and decoding images to and from latent space.",
"The model_name used for Image Save node to save model name."),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"ckpt_name": (folder_paths.get_filename_list("checkpoints"), {"tooltip": "The name of the checkpoint (model) to load."}),
}
}
@classmethod
def execute(cls, ckpt_name):
ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
final_out = (*out[:3], ckpt_name)
return final_out
class GzippedBase64ToImage(IO.ComfyNode):
@classmethod
def define_schema(cls):
return node(
"GzippedBase64ToImage",
cat_image,
cls._v1_inputs(),
("IMAGE",),
("image",),
description="Load Gzipped Base64 Image string from SAA. Convert back to Image for other nodes.",
output_tooltips=("Load Gzipped Base64 Image string from SAA.",
"Convert back to Image for other nodes."),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"base64text": ("STRING", {"display": "input", "multiline": True}),
}
}
@classmethod
def execute(cls, base64text):
result = decompress_base64_gzip(base64text)
img = Image.open(BytesIO(result))
img.load()
if img.mode == "RGBA":
new_img = Image.new("RGB", img.size, (255,255,255))
new_img.paste(img, mask=img.split()[3])
image = EncodeImage(new_img)
else:
image = EncodeImage(img)
return (image,)
class ImageToGzippedBase64(IO.ComfyNode):
@classmethod
def define_schema(cls):
return node(
"ImageToGzippedBase64",
cat_image,
cls._v1_inputs(),
("STRING",),
("base64text",),
description="Load Image and Gzipped then convert to Base64 string for SAA.",
output_tooltips=("Load Image and Gzipped then convert to Base64 string for SAA.",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"image": ("IMAGE", {
"default": None,
}),
}
}
@classmethod
def execute(cls, image):
image_pil = DecodeImage(image)
temp = BytesIO()
image_pil.save(temp, format="png")
compressed_data = gzip.compress(temp.getvalue())
img_str_base64 = base64.b64encode(compressed_data).decode("ascii")
return (img_str_base64,)
import torch
class ReverseImageAndAllImages(IO.ComfyNode):
'''
Reverse the order of images in a batch and concatenate the reversed images with the original batch.
Inputs:
images - Source Images
Outputs:
rev_image - Reversed Images
all_image - Source + Reversed Images
'''
@classmethod
def define_schema(cls):
return node(
"ReverseImageAndAllImages",
cat_image,
cls._v1_inputs(),
("IMAGE", "IMAGE", ),
("rev_image", "all_image", ),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"images": ("IMAGE", {
"default": None,
}),
},
}
@classmethod
def execute(cls, images):
# Ensure images is a torch tensor
if not isinstance(images, torch.Tensor):
raise ValueError("Input 'images' must be a torch.Tensor")
# The input 'images' is a batched tensor of shape (B, H, W, C), where B is the batch size (number of images).
effective_count = images.shape[0]
# Extract the first 'effective_count' images to reverse
to_reverse = images[:effective_count]
# Reverse the order along the batch dimension
rev_image = to_reverse.flip(0) # Using flip(0) for batch dimension reversal
# Append the reversed images to the original batch by concatenating along the batch dimension
all_image = torch.cat((images, rev_image), dim=0)
return (rev_image, all_image,)
class StackImages(IO.ComfyNode):
'''
Stack Images and Extract Last Image
Stacks the input images with optional previous images and extracts the last image from the input images.
Inputs:
images - Source Images
last_images_in - Optional input images to prepend
Outputs:
all_images - Concatenated images (last_images_in + images, or images if last_images_in is None)
last_image - Last image from the input images
'''
@classmethod
def define_schema(cls):
return node(
"StackImages",
cat_image,
cls._v1_inputs(),
("IMAGE", "IMAGE", ),
("all_images", "last_image", ),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"images": ("IMAGE", {
"default": None,
}),
},
"optional": {
"last_images_in": ("IMAGE", {
"default": None,
}),
},
}
@classmethod
def execute(cls, images, last_images_in=None):
# Ensure images is a torch tensor
if not isinstance(images, torch.Tensor):
raise ValueError("Input 'images' must be a torch.Tensor")
if images.ndim == 3:
images = images.unsqueeze(0)
if last_images_in is not None and last_images_in.ndim == 3:
last_images_in = last_images_in.unsqueeze(0)
last_image = images[-1:] # (1, H, W, C)
if last_images_in is not None:
all_images = torch.cat((last_images_in, images), dim=0)
else:
all_images = images
if all_images.ndim == 3:
all_images = all_images.unsqueeze(0)
if last_image.ndim == 3:
last_image = last_image.unsqueeze(0)
return (all_images, last_image,)
class FlatColorQuantization(IO.ComfyNode):
'''
Flat Color Quantization
https://github.com/mirabarukaso/flat_color_quantization
Inputs:
image - Source Image
Outputs:
out_image - Quantizated image
'''
@classmethod
def define_schema(cls):
return node(
"FlatColorQuantization",
cat_image,
cls._v1_inputs(),
("IMAGE", ),
("out_image",),
)
@classmethod
def _v1_inputs(s):
return {
"required": {
"image": ("IMAGE", {
"default": None,
}),
"n_colors": ("INT", {
"default": 2048,
"min": 2,
"max": 16384,
"step": 1
}),
"block_size": ("INT", {
"default": 512,
"min": 64,
"max": 2048,
"step": 64
}),
"temperature": ("FLOAT", {
"default": 2,
"min": 1,
"max": 10,
"step": 0.05
}),
"spatial_scale": ("INT", {
"default": 80,
"min": 1,
"max": 300,
"step": 1
}),
"sharpen": ("FLOAT", {
"default": 0,
"min": 0,
"max": 3,
"step": 0.1
}),
},
}
@classmethod
def execute(cls, image, n_colors, block_size, temperature, spatial_scale, sharpen):
img = ConvertToNP(image)
result = flat_color_multi_scale(image_input=img, n_colors=n_colors, block_size=block_size,
spatial_scale=spatial_scale, temperature=temperature, sharpen_strength=sharpen)
return(EncodeImage(result),)