import os, json, io, base64 import numpy as np from PIL import Image from PIL.PngImagePlugin import PngInfo import folder_paths from comfy.cli_args import args from .helpers.imgutil import scale_tensor_image # ============================================================================== # adpated from ComfyUI SaveImage node # ============================================================================== class PseudoSaveImageWithEmbeddedMasks: 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(s): return { "required": { "imgs": ("IMAGE", {"tooltip": "The images to save."}), "filename_prefix": ("STRING", {"default": "pseudocomfy", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."}) }, "optional": { "msks": ("MASK", {"tooltip": "The masks to embed in the image."}), # List of mask tensors ([1, H, W]) }, "hidden": { "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" }, } INPUT_IS_LIST = True RETURN_TYPES = () FUNCTION = "save_images" OUTPUT_NODE = True CATEGORY = "Pseudocomfy/IO" DESCRIPTION = "Saves the input images with embedded masks as extra channels." def save_images(self, imgs, filename_prefix="pseudocomfy", msks=[], prompt=None, extra_pnginfo=None): # given that INPUT_IS_LIST, msks is a list of masks that should be embedded in each image in each batch # given that INPUT_IS_LIST, imgs is a list of image batches image_batches = imgs typical_image_shape = image_batches[0][0].shape # if inputs that are meant to be singletons are a list, use the first element if isinstance(filename_prefix, list) and len(filename_prefix)>0: filename_prefix = filename_prefix[0] if isinstance(prompt, list) and len(prompt)>0: prompt = prompt[0] if isinstance(extra_pnginfo, list) and len(extra_pnginfo)>0: extra_pnginfo = extra_pnginfo[0] print(f"[pseudocomfy] SaveImageWithEmbeddedMasks") print(f"\tfilename_prefix = {filename_prefix}") print(f"\ttypical_image_shape = {tuple(typical_image_shape)}") print(f"\timage_batches count = {len(image_batches)}") for i, img_batch in enumerate(image_batches): print(f"\timg_batch[{i}] shape = {tuple(img_batch.shape)}") print(f"\tmsks count = {len(msks)}") for i, msk in enumerate(msks): print(f"\tmsks[{i}] shape = {tuple(msk.shape)}") # Ensure all masks have shape [1, width, height] for i in range(len(msks)): mask = msks[i] # Check mask has three dimensions and the first dimension is 1 if not (isinstance(mask.shape, tuple) and len(mask.shape) == 3 and mask.shape[0] == 1): raise ValueError(f"Mask at index {i} must have shape [1, width, height], got {mask.shape}") # scale the mask to the desired width and height if necessary if mask.shape[-2] != typical_image_shape[0] or mask.shape[-1] != typical_image_shape[1]: msks[i] = scale_tensor_image(mask, typical_image_shape[1], typical_image_shape[0]) # note that for scale_tensor_image, the order of width and height is reversed from order of tensor shape print(f"\tmsks count = {len(msks)}") for i, msk in enumerate(msks): print(f"\tmsks[{i}] shape = {tuple(msk.shape)}") filename_prefix += self.prefix_append full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, typical_image_shape[1], typical_image_shape[0]) results = list() for img_batch in image_batches: for (batch_number, image) in enumerate(img_batch): i = 255. * image.cpu().numpy() image = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) metadata = None if not 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])) # Embed ALL masks as base64 PNGs in metadata for mask_idx, mask in enumerate(msks): mask_np = mask.squeeze().cpu().numpy() mask_img = Image.fromarray(np.clip(255 * mask_np, 0, 255).astype(np.uint8), mode="L") buf = io.BytesIO() mask_img.save(buf, format="PNG") b64 = base64.b64encode(buf.getvalue()).decode("utf-8") mask_json = { "format": "PNG", "encoding": "base64", "size": list(mask_img.size), "data": b64 } metadata.add_text(f"pseudocomfy_mask_{mask_idx:02d}", json.dumps(mask_json)) filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) file = f"{filename_with_batch_num}_{counter:05}_.png" image.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level) results.append({ "filename": file, "subfolder": subfolder, "type": self.type }) counter += 1 return { "ui": { "images": results } }