Files

349 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ComfyUI Toggle Pass - Toggle nodes + Image IO + Multi-Ref Encode
Author: Clever
"""
from .multi_ref_encode import NODE_CLASS_MAPPINGS as MULTI_REF_NODES, NODE_DISPLAY_NAME_MAPPINGS as MULTI_REF_NAMES
from .unified_loader import NODE_CLASS_MAPPINGS as UNIFIED_NODES, NODE_DISPLAY_NAME_MAPPINGS as UNIFIED_NAMES
import os
import json
import random
import hashlib
import torch
import numpy as np
from PIL import Image, ImageOps, ImageSequence, ImageFile
from PIL.PngImagePlugin import PngInfo
import comfy.model_management
import comfy.utils
import folder_paths
import node_helpers
WEB_DIRECTORY = "./js"
_last_saved_path = {}
# ==================== Toggle Load Image ====================
class ToggleLoadImage:
"""Load image from input folder with privacy toggle support."""
@classmethod
def INPUT_TYPES(cls):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
files = folder_paths.filter_files_content_types(files, ["image"])
return {
"required": {
"image": (sorted(files), {"image_upload": True}),
},
}
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("IMAGE", "MASK")
FUNCTION = "load_image"
CATEGORY = "Toggle-Pass"
DESCRIPTION = "Load image with privacy toggle."
def load_image(self, image):
image_path = folder_paths.get_annotated_filepath(image)
# 尝试视频帧提取(如果安装了 comfy-extras 的 InputImpl)
try:
from comfy_extras.nodes_input import InputImpl
components = InputImpl.VideoFromFile(image_path).get_components()
if components.images.shape[0] > 0:
return (components.images, 1.0 - components.alpha[..., -1] if components.alpha is not None else torch.zeros((components.images.shape[0], 64, 64), dtype=torch.float32, device="cpu"))
except Exception:
pass
img = node_helpers.pillow(Image.open, image_path)
output_images = []
output_masks = []
w, h = None, None
dtype = comfy.model_management.intermediate_dtype()
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
elif i.mode == 'P' and 'transparency' in i.info:
mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
output_images.append(image.to(dtype=dtype))
output_masks.append(mask.unsqueeze(0).to(dtype=dtype))
if img.format == "MPO":
break
if len(output_images) > 1:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (output_image, output_mask)
@classmethod
def IS_CHANGED(cls, image):
image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
return m.digest().hex()
@classmethod
def VALIDATE_INPUTS(cls, image):
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True
# ==================== Toggle Preview Image ====================
class TogglePreviewImage:
"""Preview image with privacy toggle support. Saves to temp like built-in PreviewImage."""
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.compress_level = 1
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("IMAGE",)
OUTPUT_NODE = True
FUNCTION = "preview"
CATEGORY = "Toggle-Pass"
DESCRIPTION = "Preview image with privacy toggle. Saves to temp like PreviewImage."
def preview(self, images, prompt=None, extra_pnginfo=None):
results = []
for idx, img in enumerate(images):
img_np = 255. * img.cpu().numpy()
pil_img = Image.fromarray(np.clip(img_np, 0, 255).astype(np.uint8))
filename = f"toggle_preview_{idx:05}.png"
filepath = os.path.join(self.output_dir, filename)
pil_img.save(filepath, format="PNG", compress_level=self.compress_level)
results.append({
"filename": filename,
"subfolder": "",
"type": self.type,
})
return {"result": (images,), "ui": {"images": results}}
class SaveLoadImage:
"""Save image AND output it. Toggle OFF to auto-load last saved image instead."""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.prefix_append = ""
self.compress_level = 4
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"use_saved": (["Save", "Load"],),
},
"optional": {
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
},
"hidden": {
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
},
}
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("IMAGE", "MASK")
OUTPUT_NODE = True
FUNCTION = "execute"
CATEGORY = "Toggle-Pass"
DESCRIPTION = "Save image and output IMAGE. Toggle use_saved=ON to reuse last saved image."
def execute(self, images, use_saved="Save", filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
if use_saved == "Load":
return self._load_last()
else:
return self._save(images, filename_prefix, prompt, extra_pnginfo)
def _save(self, images, filename_prefix, prompt, extra_pnginfo):
if images is None:
images = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
)
results = list()
for (batch_number, image) in enumerate(images):
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = None
if not comfy.utils.args.disable_metadata:
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.png"
filepath = os.path.join(full_output_folder, file)
img.save(filepath, pnginfo=metadata, compress_level=self.compress_level)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
if id(self) not in _last_saved_path:
_last_saved_path[id(self)] = filepath
counter += 1
last_file = os.path.join(full_output_folder, results[0]["filename"])
_last_saved_path[id(self)] = last_file
mask = torch.zeros((images.shape[0], images.shape[1], images.shape[2]), dtype=torch.float32)
return {"result": (images, mask), "ui": {"images": results}}
def _load_last(self):
node_id = id(self)
if node_id in _last_saved_path and os.path.isfile(_last_saved_path[node_id]):
filepath = _last_saved_path[node_id]
img = node_helpers.pillow(Image.open, filepath)
output_images = []
output_masks = []
w, h = None, None
dtype = comfy.model_management.intermediate_dtype()
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda x: x * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
elif i.mode == 'P' and 'transparency' in i.info:
mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((h, w), dtype=torch.float32, device="cpu")
output_images.append(image.to(dtype=dtype))
output_masks.append(mask.unsqueeze(0).to(dtype=dtype))
if img.format == "MPO":
break
if len(output_images) > 1:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return {"result": (output_image, output_mask), "ui": {"images": []}}
dummy = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
dummy_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
return {"result": (dummy, dummy_mask), "ui": {"images": []}}
class ToggleEmptyLatent:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 1024, "min": 16, "max": 16384, "step": 8}),
"height": ("INT", {"default": 1024, "min": 16, "max": 16384, "step": 8}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
"enabled": (["ON", "OFF"],),
},
"optional": {
"fallback_width": ("INT", {"default": 1024, "min": 16, "max": 16384, "step": 8}),
"fallback_height": ("INT", {"default": 1024, "min": 16, "max": 16384, "step": 8}),
},
}
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("LATENT",)
FUNCTION = "generate"
CATEGORY = "Toggle-Pass"
def generate(self, width, height, batch_size=1, enabled="ON", fallback_width=None, fallback_height=None):
if fallback_width is None:
fallback_width = 1024
if fallback_height is None:
fallback_height = 1024
if enabled != "ON":
width = fallback_width
height = fallback_height
latent = torch.zeros([batch_size, 4, height // 8, width // 8],
device=comfy.model_management.intermediate_device(),
dtype=comfy.model_management.intermediate_dtype())
return ({"samples": latent, "downscale_ratio_spacial": 8},)
NODE_CLASS_MAPPINGS = {
**MULTI_REF_NODES,
**UNIFIED_NODES,
"ToggleEmptyLatent": ToggleEmptyLatent,
"SaveLoadImage": SaveLoadImage,
"ToggleLoadImage": ToggleLoadImage,
"TogglePreviewImage": TogglePreviewImage,
}
NODE_DISPLAY_NAME_MAPPINGS = {
**MULTI_REF_NAMES,
**UNIFIED_NAMES,
"ToggleEmptyLatent": "Toggle Empty Latent",
"SaveLoadImage": "Save/Load Image",
"ToggleLoadImage": "Toggle Load Image",
"TogglePreviewImage": "Toggle Preview Image",
}