26 Commits
Author SHA1 Message Date
Salvador E. Tropea 1078a8a9e0 Bumped version to 1.4.0 2025-10-22 13:23:05 -03:00
Salvador E. Tropea e939a863c2 [Added] "Image with text label" node 2025-10-20 10:50:03 -03:00
Salvador E. Tropea 1d1b99ac49 [Added] Arbitrary normalization and a node to input the parameters 2025-10-14 11:40:16 -03:00
Salvador E. Tropea c5060bafe6 [Added] Icon for the ComfyUI registry 2025-10-14 09:31:35 -03:00
Salvador E. Tropea 055236953d [Added] Icon image 2025-10-14 09:30:54 -03:00
Salvador E. Tropea 346fc8590b [DOCs] Updated the nodes description 2025-10-14 09:16:53 -03:00
Salvador E. Tropea 2c732ce7cb Bumped version to 1.3.0 2025-10-14 09:11:11 -03:00
Salvador E. Tropea e7d124d786 [ImagePad][Added] Copy input size when target_* is 0 2025-10-14 09:04:32 -03:00
Salvador E. Tropea c976f3d057 [DOCs] And another way to display the examples 2025-10-14 08:15:32 -03:00
Salvador E. Tropea c74e859aaa [DOCs] Trying a different way to show the examples 2025-10-14 08:14:28 -03:00
Salvador E. Tropea 684d3fcb6b [DOCs][Added] Image for the examples 2025-10-14 08:12:22 -03:00
Salvador E. Tropea 0f0157ed4f [DOCs] Moved node docs to a separated file
To make the README.md more clear
2025-10-14 08:07:31 -03:00
Salvador E. Tropea b306bd37a7 [DOCs] Separated nodes in categories (in the ToC) 2025-10-14 07:57:47 -03:00
Salvador E. Tropea ea94753216 [DOCs] Made the example links to point to raw files, easier to download 2025-10-14 07:52:54 -03:00
Salvador E. Tropea 3844dd20b5 [DOCs][Added] Documentation about the new nodes 2025-10-14 07:48:32 -03:00
Salvador E. Tropea 79f1003873 [Added] Resize example 2025-10-13 20:11:08 -03:00
Salvador E. Tropea e159265648 [ImagePad/Resize][Added] Control over padding transparency
By default is transparent
2025-10-13 20:00:58 -03:00
Salvador E. Tropea aa8dc7b2ce [Download][Added] Option to create RGBA images 2025-10-13 19:17:13 -03:00
Salvador E. Tropea 684afdf5e6 [ImageResize][Fixed] Mask processing for modes others than edge_pixel
Avoid replicating the edge for other modes.
2025-10-13 18:24:30 -03:00
Salvador E. Tropea 314c6bf609 [ResizeImage][Fixed] Comment about the difference when w/h==0 2025-10-13 17:50:04 -03:00
Salvador E. Tropea 529756d415 [ImagePad][Fixed] Error when padding an RGBA image 2025-10-13 17:47:54 -03:00
Salvador E. Tropea 79b14305d5 [ImageResize][Fixed] Size computation when w/h are 0 2025-10-13 13:24:30 -03:00
Salvador E. Tropea 968859fc5a [ImageResize][Fixed] Color must be [0,1] 2025-10-13 13:23:23 -03:00
Salvador E. Tropea f05c36f520 [Nodes][Added] ResizeMask also synced with Kijai 1.1.7 2025-10-06 13:36:24 -03:00
Salvador E. Tropea d99c165f3d Merge branch 'resize_pad'
Some operations are needed and Comfy-Core lacks them
2025-10-05 14:42:14 -03:00
Salvador E. Tropea 015f8be8c9 [Added] Resize and Pad image from KJNodes
Just to keep dependencies low
But current Comfy-Core can handle it
2025-09-16 08:36:55 -03:00
8 changed files with 1225 additions and 198 deletions
+39 -187
View File
@@ -17,16 +17,27 @@ Currently we just have a few nodes used by other nodes I maintain.
- 📦 [Dependencies](#-dependencies)
- 🖼️ [Examples](#️-examples)
- ✨ [Nodes](#-extra-nodes)
- [Image Download and Load](#1-image-download-and-load)
- [Face Composite](#2-face-composite)
- [Face Composite (frame by frame)](#3-face-composite-frame-by-frame)
- [Normalize Image to ImageNet](#4-normalize-image-to-imagenet)
- [Normalize Image to [-0.5, 0.5]](#5-normalize-image-to-05-05)
- [Normalize Image to [-1, 1]](#6-normalize-image-to-1-1)
- [Apply Mask using AFFCE](#7-apply-mask-using-affce)
- [Estimate foreground (AFFCE)](#8-estimate-foreground-affce)
- [Estimate foreground (FMLFE)](#9-estimate-foreground-fmlfe)
- [Create Empty Image](#10-create-empty-image)
- Foreground: (Foreground estimation)
- [Estimate foreground (AFFCE)](docs/nodes_img.md#8-estimate-foreground-affce)
- [Estimate foreground (FMLFE)](docs/nodes_img.md#9-estimate-foreground-fmlfe)
- Generation:
- [Create Empty Image](docs/nodes_img.md#10-create-empty-image)
- Input/Output:
- [Image Download and Load](docs/nodes_img.md#1-image-download-and-load)
- Manipulation:
- [Face Composite](docs/nodes_img.md#2-face-composite)
- [Face Composite (frame by frame)](docs/nodes_img.md#3-face-composite-frame-by-frame)
- [Apply Mask using AFFCE](docs/nodes_img.md#7-apply-mask-using-affce)
- [Pad Image](docs/nodes_img.md#11-pad-image-kjset)
- [Resize Image](docs/nodes_img.md#12-resize-image-kjset)
- [Resize Mask](docs/nodes_img.md#13-resize-mask-kjset)
- [Image with text label](docs/nodes_img.md#16-image-with-text-label)
- Normalization:
- [Normalize Image to ImageNet](docs/nodes_img.md#4-normalize-image-to-imagenet)
- [Normalize Image to [-0.5, 0.5]](docs/nodes_img.md#5-normalize-image-to-05-05)
- [Normalize Image to [-1, 1]](docs/nodes_img.md#6-normalize-image-to-1-1)
- [Arbitrary Normalize](docs/nodes_img.md#14-arbitrary-normalize)
- [Normalize Parameters](docs/nodes_img.md#15-normalize-parameters)
- 📝 [Usage Notes](#-usage-notes)
- 📜 [Project History](#-project-history)
- ⚖️ [License](#️-license)
@@ -34,180 +45,7 @@ Currently we just have a few nodes used by other nodes I maintain.
## ✨ Nodes
### 1. Image Download and Load
- **Display Name:** `Image Download and Load`
- **Internal Name:** `SET_ImageDownload`
- **Category:** `image/io`
- **Description:** Downloads an image file from a URL into the `ComfyUI/input/` directory if it's not already there, and then loads it as an image and mask. This is perfect for creating self-contained, shareable workflows with example image.
- **Inputs:**
- `image_bypass` (IMAGE, Optional): If an image is provided here it will be used for the output. You can connect a `Load Image` node here, if the connected node is muted (bypassed) we download the file, otherwise we use the image from the `Load Image` node.
- `mask_bypass` (MASK, Optional): This input complements `image_bypass`.
- `base_url` (STRING): The URL of the directory containing the image file.
- `filename` (STRING): The name of the image file to download (e.g., photo.jpg, art.png).
- `local_name` (STRING, optional): The name used locally. Leave blank to use `filename`.
- **Output:**
- `image` (IMAGE): The loaded image.
- `alpha_mask` (MASK): The alpha mask for the loaded image.
- **Behavior Details:**
- **Caching:** The node checks the `ComfyUI/input/` folder first. If the file with the specified `filename` already exists, the download is skipped.
- **Bypass:** If only one of `image_bypass` and `mask_bypass` is connected the other will be assumed to be empty. You should connect both or avoid using the output corresponding to the unconnected input.
### 2. Face Composite
- **Display Name:** `Face Composite`
- **Internal Name:** `SET_CompositeFace`
- **Category:** `image/manipulation`
- **Description:** This node is designed for complex batch operations where a single reference image generates multiple animated or processed frames. It handles an **M-to-N** relationship.
- **Purpose:** The primary use case is for animation workflows (e.g., using AnimateDiff or SVD) where you:
1. Extract a face from **one** source image.
2. Generate **N** animated frames of that face.
3. Paste each of the **N** animated frames back onto the original source image to create an animated sequence.
- **Inputs:**
- `animated` (`IMAGE`): A batch of `M*N` cropped/processed images (e.g., the animated faces).
- `reference` (`IMAGE`): A batch of `M` original context images that the faces were extracted from.
- `bboxes` (`BBOX`): A list of `M` bounding boxes. Each box in the list must correspond to a `reference` image, defining the location for the paste operation. Format: (X, Y, W, H)
- **Output:**
- `images` (`IMAGE`): The final output batch of `M*N` full-size images. Each image consists of a `reference` image with one of the `animated` frames pasted onto it.
- **How it Works:**
- For each of the `M` reference images and its corresponding bounding box, the node takes the next `N` frames from the `animated` batch. It then pastes each of these `N` frames onto a *copy* of the reference image, generating `N` final output frames. This process is repeated for all `M` reference images.
- The animated frame is automatically resized to fit the bounding box dimensions before pasting. The pasting logic safely handles cases where the bbox is partially outside the image boundaries.
### 3. Face Composite (frame by frame)
- **Display Name:** `"Face Composite (frame by frame)`
- **Internal Name:** `SET_CompositeFaceFrameByFrame`
- **Category:** `image/manipulation`
- **Description:** This node is a simplified variant for **1-to-1** compositing. It is perfect for video processing workflows where you need to paste a sequence of processed frames back into the original video sequence at a static location.
- **Purpose:** Ideal for tasks like video face restoration, stylization, or simple lip-sync, where:
1. A video is loaded as a sequence of `N` frames.
2. A corresponding sequence of `N` processed/animated frames is generated.
3. Each processed frame needs to be pasted back into its corresponding original frame at the same location.
- **Inputs:**
- `animated` (`IMAGE`): A batch of `N` processed frames.
- `reference` (`IMAGE`): A batch of `N` original frames. **Must have the same batch size as `animated`**.
- `bboxes` (`BBOX`): A list of bounding boxes. **Only the first bbox in the list is used** as the static paste location for all frames.
- **Output:**
- `images` (`IMAGE`): The final batch of `N` composited frames.
- **How it Works:** The node iterates from frame 0 to N-1. In each step, it takes the i-th `animated` frame and the i-th `reference` frame. It then pastes the animated frame onto the reference frame using the coordinates from the single, static bounding box.
### 4. Normalize Image to ImageNet
- **Display Name:** `"Normalize Image to ImageNet`
- **Internal Name:** `SET_NormalizeToImageNetDataset`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor using the mean and standard deviation of the ImageNet dataset.
- **Purpose:** Essential for pre-processing images before feeding them into models that were pre-trained on ImageNet (e.g., most ResNet, VGG, EfficientNet models).
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor. The value range will be altered significantly.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using the standard ImageNet values:
- **Mean:** `[0.485, 0.456, 0.406]`
- **Std Dev:** `[0.229, 0.224, 0.225]`
### 5. Normalize Image to [-0.5, 0.5]
- **Display Name:** `Normalize Image to [-0.5, 0.5]`
- **Internal Name:** `SET_NormalizeToMinus05_05`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor by centering its values around zero.
- **Purpose:** Useful for models trained from scratch or those that expect input data in the `[-0.5, 0.5]` range. This can help stabilize training.
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor, with values in the `[-0.5, 0.5]` range.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using:
- **Mean:** `[0.5, 0.5, 0.5]`
- **Std Dev:** `[1.0, 1.0, 1.0]`
### 6. Normalize Image to [-1, 1]
- **Display Name:** `Normalize Image to [-1, 1]`
- **Internal Name:** `SET_NormalizeToMinus1_1`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor to the `[-1, 1]` range.
- **Purpose:** A common requirement for certain model architectures, particularly Generative Adversarial Networks (GANs) and models using the `tanh` activation function in their output layer.
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor, with values in the `[-1, 1]` range.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using:
- **Mean:** `[0.5, 0.5, 0.5]`
- **Std Dev:** `[0.5, 0.5, 0.5]`
### 7. Apply Mask using AFFCE
- **Display Name:** `Apply Mask using AFFCE`
- **Internal Name:** `SET_ApplyMaskAFFCE`
- **Category:** `image/manipulation`
- **Description:** Applies a mask to an image using [Approximate Fast Foreground Colour Estimation](https://github.com/Photoroom/fast-foreground-estimation). This blends the image contour in a better way.
- **Purpose:** Used to apply the mask of a background removal model.
- **Inputs:**
- `images` (`IMAGE`): One ore more ComfyUI images
- `masks` (`MASK`): Masks to apply
- `blur_size` (`INT`): Diameter for the coarse gaussian blur
- `blur_size_two` (`INT`): Diameter for the fine gaussian blur
- `fill_color` (`BOOLEAN`): When enabled the removed image is replaced by a color, the output is an RGB image. Otherwise the removed part becomes transparent and the output is an RGBA image.
- `color` (`STRING`): A string representing a color to be used when `fill_color` is enabled. Can be an hexadecimal RGB (i.e. `#AABBCC`) or comma separated RGB components. The components can be in the [0-255] or [0-1.0] range.
- `batched` (`BOOLEAN`): Process all the images at once. Otherwise do it one at a time.
- **Output:**
- `image` (`IMAGE`): The image after applying the mask.
- `mask` (`MASK`): The input mask
### 8. Estimate foreground (AFFCE)
- **Display Name:** `Estimate foreground (AFFCE)`
- **Internal Name:** `SET_AFFCE`
- **Category:** `image/foreground`
- **Description:** Estimates the foreground of an image using [Approximate Fast Foreground Colour Estimation](https://github.com/Photoroom/fast-foreground-estimation). The result is suitable for background replacement.
- **Purpose:** Used to get a better foreground for background removal.
- **Inputs:**
- `images` (`IMAGE`): One ore more ComfyUI images
- `masks` (`MASK`): Masks to apply
- `blur_size` (`INT`): Diameter for the coarse gaussian blur
- `blur_size_two` (`INT`): Diameter for the fine gaussian blur
- `batched` (`BOOLEAN`): Process all the images at once. Otherwise do it one at a time.
- **Output:**
- `image` (`IMAGE`): The image after foreground estimation.
- `mask` (`MASK`): The input mask
### 9. Estimate foreground (FMLFE)
- **Display Name:** `Estimate foreground (FMLFE)`
- **Internal Name:** `SET_FMLFE`
- **Category:** `image/foreground`
- **Description:** Estimates the foreground of an image using [Fast Multi-Level Foreground Estimation](https://arxiv.org/abs/2006.14970). The result is suitable for background replacement.
- **Purpose:** Used to get a better foreground for background removal.
- **Inputs:**
- `images` (`IMAGE`): The source image(s) from which to estimate the foreground and background.
- `masks` (`MASK`): The alpha matte that guides the estimation. White areas are treated as known foreground, black as known background, and gray areas are the semi-transparent regions the algorithm will solve for.
- `implementation" (`auto`, `cupy`, `opencl`, `numba`, `torch`): Which implementation to use. The `auto` will use the fastest available. The `torch` implementation is slow and approximated, but doesn't need extra dependencies. The `numba` implementation is good and just needs [Numba](https://numba.pydata.org/). The `cupy` implementation is the fastest, but needs [CuPy](https://cupy.dev/) and a full CUDA environment.
- `regularization` (`FLOAT`): The regularization strength (epsilon). This acts as a smoothness prior. Higher values result in smoother, more blended foreground and background colors, but may lose very fine details. Lower values preserve more detail but can be noisier.
- `n_small_iterations` (`INT`): The number of solver iterations to perform on the lower-resolution levels of the image pyramid. More iterations can improve quality at the cost of speed.
- `n_big_iterations` (`INT`): The number of solver iterations to perform on the higher-resolution (larger) levels of the image pyramid. Fewer iterations are typically needed at high resolution as the details are propagated up from the smaller levels.
- `small_size` (`INT`): The pixel dimension threshold. Image pyramid levels smaller than this size will use the higher 'n_small_iterations' count, while larger levels will use 'n_big_iterations'.
- `gradient_weight` (`FLOAT`): Controls how strongly the edges in the alpha matte influence color blending. A higher value makes the algorithm respect the mask's edges more, leading to sharper color boundaries. A lower value allows more color bleeding, an effect similar to increasing regularization.
- **Output:**
- `image` (`IMAGE`): The estimated foreground image (F). This is a full-color image where the algorithm has estimated the true, un-blended color of the foreground object.
- `mask` (`MASK`): The estimated background image (B). The algorithm has effectively "inpainted" the area behind the foreground object, creating a clean background plate.
### 10. Create Empty Image
- **Display Name:** `Create Empty Image`
- **Internal Name:** `SET_CreateEmptyImage`
- **Category:** `image/generation`
- **Description:** Creates an image filled with a solid color. Similar to standard `EmptyImage`, but you can use an image as reference and you have more options to select the color.
- **Purpose:** Create an empty image
- **Inputs:**
- `width` (`INT`): The width of the new image in pixels. This value is ignored if a `reference` is provided.
- `height` (`INT`): The height of the new image in pixels. This value is ignored if a `reference` is provided.
- `batch_size` (`INT`): The number of images to create in the batch. This value is ignored if a `reference` is provided.
- `color` (`STRING`): The solid color to fill the image with. Can be a named color (e.g., "black", "red"), a hex string (e.g., "#FF0000", "00ff00"), or comma-separated components in the [0, 255] or [0.0, 1.0] range.
- `reference` (`IMAGE`, optional): If an image is connected here, its dimensions (batch size, height, and width) will be used for the new image, overriding the manual width, height, and batch_size inputs.
- **Output:**
- `image` (`IMAGE`): A new image tensor of the specified size and color.
The nodes are documented [here](docs/nodes_img.md). Use the above ToC to access them by category.
## 🚀 Installation
@@ -221,7 +59,7 @@ You can install the nodes from the ComfyUI nodes manager, the name is *Image Mis
2. Install dependencies: `pip install -r ComfyUI/custom_nodes/ComfyUI-ImageMisc/requirements.txt`
3. Restart ComfyUI.
The nodes should then appear under the "image/io" category in the "Add Node" menu.
The nodes should then appear under the "image/io", "image/manipulation", "image/normalization" and "image/foreground" categories in the "Add Node" menu.
## 📦 Dependencies
@@ -238,7 +76,18 @@ The nodes should then appear under the "image/io" category in the "Add Node" men
Once installed the examples are available in the ComfyUI workflow templates, in the *Image Misc* section (or ComfyUI-ImageMisc).
- [image_download.json](example_workflows/image_download.json): Shows how to use the image downloader node.
### [image_download.json](https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/refs/heads/main/example_workflows/image_download.json)
Shows how to use the image downloader node.
[![image_download.json](example_workflows/image_download.jpg)](https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/refs/heads/main/example_workflows/image_download.json)
### [resize.json](https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/refs/heads/main/example_workflows/resize.json)
Shows the most common uses of the resize and padding nodes. You can see how various modes and options affects the result and how the different interpolation methods works. It shows how masks and RGBA images are handled.
[![resize.json](example_workflows/resize.jpg)](https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/refs/heads/main/example_workflows/resize.json)
## 📝 Usage Notes
@@ -261,7 +110,9 @@ Once installed the examples are available in the ComfyUI workflow templates, in
- Apply mask nodes, including support for:
- "Fast Multi-Level Foreground Estimation" (from [PyMatting](https://github.com/pymatting/pymatting))
- "Approximate Fast Foreground Colour Estimation" (from [Photoroom](https://github.com/Photoroom/fast-foreground-estimation))
- Create empty image node, with reference image
- Create empty image node, from reference image
- 1.3.0 2025-10-14: Pad Image, Resize Image and Resize Mask nodes
- 1.4.0 2025-10-20: "Image with text label" and "Arbitrary Normalize"/"Normalize Parameters"
## ⚖️ License
@@ -271,3 +122,4 @@ Once installed the examples are available in the ComfyUI workflow templates, in
## 🙏 Attributions
- Good part of the initial code and this README was generated using Gemini 2.5 Pro.
- The *Pad Image*, *Resize Image* and *Resize Mask* are from [KJNodes](https://github.com/kijai/ComfyUI-KJNodes) by [Kijai](https://github.com/kijai) (Jukka Seppänen)
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

+307
View File
@@ -0,0 +1,307 @@
# 1. Image Download and Load
- **Display Name:** `Image Download and Load`
- **Internal Name:** `SET_ImageDownload`
- **Category:** `image/io`
- **Description:** Downloads an image file from a URL into the `ComfyUI/input/` directory if it's not already there, and then loads it as an image and mask. This is perfect for creating self-contained, shareable workflows with example image.
- **Inputs:**
- `image_bypass` (IMAGE, Optional): If an image is provided here it will be used for the output. You can connect a `Load Image` node here, if the connected node is muted (bypassed) we download the file, otherwise we use the image from the `Load Image` node.
- `mask_bypass` (MASK, Optional): This input complements `image_bypass`.
- `base_url` (STRING): The URL of the directory containing the image file.
- `filename` (STRING): The name of the image file to download (e.g., photo.jpg, art.png).
- `local_name` (STRING, optional): The name used locally. Leave blank to use `filename`.
- `embed_transparency` (BOOLEAN, optional): Create RGBA images when they have transparency. Not all nodes supports them.
- **Output:**
- `image` (IMAGE): The loaded image.
- `alpha_mask` (MASK): The alpha mask for the loaded image.
- **Behavior Details:**
- **Caching:** The node checks the `ComfyUI/input/` folder first. If the file with the specified `filename` already exists, the download is skipped.
- **Bypass:** If only one of `image_bypass` and `mask_bypass` is connected the other will be assumed to be empty. You should connect both or avoid using the output corresponding to the unconnected input.
# 2. Face Composite
- **Display Name:** `Face Composite`
- **Internal Name:** `SET_CompositeFace`
- **Category:** `image/manipulation`
- **Description:** This node is designed for complex batch operations where a single reference image generates multiple animated or processed frames. It handles an **M-to-N** relationship.
- **Purpose:** The primary use case is for animation workflows (e.g., using AnimateDiff or SVD) where you:
1. Extract a face from **one** source image.
2. Generate **N** animated frames of that face.
3. Paste each of the **N** animated frames back onto the original source image to create an animated sequence.
- **Inputs:**
- `animated` (`IMAGE`): A batch of `M*N` cropped/processed images (e.g., the animated faces).
- `reference` (`IMAGE`): A batch of `M` original context images that the faces were extracted from.
- `bboxes` (`BBOX`): A list of `M` bounding boxes. Each box in the list must correspond to a `reference` image, defining the location for the paste operation. Format: (X, Y, W, H)
- **Output:**
- `images` (`IMAGE`): The final output batch of `M*N` full-size images. Each image consists of a `reference` image with one of the `animated` frames pasted onto it.
- **How it Works:**
- For each of the `M` reference images and its corresponding bounding box, the node takes the next `N` frames from the `animated` batch. It then pastes each of these `N` frames onto a *copy* of the reference image, generating `N` final output frames. This process is repeated for all `M` reference images.
- The animated frame is automatically resized to fit the bounding box dimensions before pasting. The pasting logic safely handles cases where the bbox is partially outside the image boundaries.
# 3. Face Composite (frame by frame)
- **Display Name:** `Face Composite (frame by frame)`
- **Internal Name:** `SET_CompositeFaceFrameByFrame`
- **Category:** `image/manipulation`
- **Description:** This node is a simplified variant for **1-to-1** compositing. It is perfect for video processing workflows where you need to paste a sequence of processed frames back into the original video sequence at a static location.
- **Purpose:** Ideal for tasks like video face restoration, stylization, or simple lip-sync, where:
1. A video is loaded as a sequence of `N` frames.
2. A corresponding sequence of `N` processed/animated frames is generated.
3. Each processed frame needs to be pasted back into its corresponding original frame at the same location.
- **Inputs:**
- `animated` (`IMAGE`): A batch of `N` processed frames.
- `reference` (`IMAGE`): A batch of `N` original frames. **Must have the same batch size as `animated`**.
- `bboxes` (`BBOX`): A list of bounding boxes. **Only the first bbox in the list is used** as the static paste location for all frames.
- **Output:**
- `images` (`IMAGE`): The final batch of `N` composited frames.
- **How it Works:** The node iterates from frame 0 to N-1. In each step, it takes the i-th `animated` frame and the i-th `reference` frame. It then pastes the animated frame onto the reference frame using the coordinates from the single, static bounding box.
# 4. Normalize Image to ImageNet
- **Display Name:** `Normalize Image to ImageNet`
- **Internal Name:** `SET_NormalizeToImageNetDataset`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor using the mean and standard deviation of the ImageNet dataset.
- **Purpose:** Essential for pre-processing images before feeding them into models that were pre-trained on ImageNet (e.g., most ResNet, VGG, EfficientNet models).
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor. The value range will be altered significantly.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using the standard ImageNet values:
- **Mean:** `[0.485, 0.456, 0.406]`
- **Std Dev:** `[0.229, 0.224, 0.225]`
# 5. Normalize Image to [-0.5, 0.5]
- **Display Name:** `Normalize Image to [-0.5, 0.5]`
- **Internal Name:** `SET_NormalizeToMinus05_05`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor by centering its values around zero.
- **Purpose:** Useful for models trained from scratch or those that expect input data in the `[-0.5, 0.5]` range. This can help stabilize training.
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor, with values in the `[-0.5, 0.5]` range.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using:
- **Mean:** `[0.5, 0.5, 0.5]`
- **Std Dev:** `[1.0, 1.0, 1.0]`
# 6. Normalize Image to [-1, 1]
- **Display Name:** `Normalize Image to [-1, 1]`
- **Internal Name:** `SET_NormalizeToMinus1_1`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor to the `[-1, 1]` range.
- **Purpose:** A common requirement for certain model architectures, particularly Generative Adversarial Networks (GANs) and models using the `tanh` activation function in their output layer.
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor, with values in the `[-1, 1]` range.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using:
- **Mean:** `[0.5, 0.5, 0.5]`
- **Std Dev:** `[0.5, 0.5, 0.5]`
# 7. Apply Mask using AFFCE
- **Display Name:** `Apply Mask using AFFCE`
- **Internal Name:** `SET_ApplyMaskAFFCE`
- **Category:** `image/manipulation`
- **Description:** Applies a mask to an image using [Approximate Fast Foreground Colour Estimation](https://github.com/Photoroom/fast-foreground-estimation). This blends the image contour in a better way.
- **Purpose:** Used to apply the mask of a background removal model.
- **Inputs:**
- `images` (`IMAGE`): One ore more ComfyUI images
- `masks` (`MASK`): Masks to apply
- `blur_size` (`INT`): Diameter for the coarse gaussian blur
- `blur_size_two` (`INT`): Diameter for the fine gaussian blur
- `fill_color` (`BOOLEAN`): When enabled the removed image is replaced by a color, the output is an RGB image. Otherwise the removed part becomes transparent and the output is an RGBA image.
- `color` (`STRING`): A string representing a color to be used when `fill_color` is enabled. Can be an hexadecimal RGB (i.e. `#AABBCC`) or comma separated RGB components. The components can be in the [0-255] or [0-1.0] range.
- `batched` (`BOOLEAN`): Process all the images at once. Otherwise do it one at a time.
- **Output:**
- `image` (`IMAGE`): The image after applying the mask.
- `mask` (`MASK`): The input mask
# 8. Estimate foreground (AFFCE)
- **Display Name:** `Estimate foreground (AFFCE)`
- **Internal Name:** `SET_AFFCE`
- **Category:** `image/foreground`
- **Description:** Estimates the foreground of an image using [Approximate Fast Foreground Colour Estimation](https://github.com/Photoroom/fast-foreground-estimation). The result is suitable for background replacement.
- **Purpose:** Used to get a better foreground for background removal.
- **Inputs:**
- `images` (`IMAGE`): One ore more ComfyUI images
- `masks` (`MASK`): Masks to apply
- `blur_size` (`INT`): Diameter for the coarse gaussian blur
- `blur_size_two` (`INT`): Diameter for the fine gaussian blur
- `batched` (`BOOLEAN`): Process all the images at once. Otherwise do it one at a time.
- **Output:**
- `image` (`IMAGE`): The image after foreground estimation.
- `mask` (`MASK`): The input mask
# 9. Estimate foreground (FMLFE)
- **Display Name:** `Estimate foreground (FMLFE)`
- **Internal Name:** `SET_FMLFE`
- **Category:** `image/foreground`
- **Description:** Estimates the foreground of an image using [Fast Multi-Level Foreground Estimation](https://arxiv.org/abs/2006.14970). The result is suitable for background replacement.
- **Purpose:** Used to get a better foreground for background removal.
- **Inputs:**
- `images` (`IMAGE`): The source image(s) from which to estimate the foreground and background.
- `masks` (`MASK`): The alpha matte that guides the estimation. White areas are treated as known foreground, black as known background, and gray areas are the semi-transparent regions the algorithm will solve for.
- `implementation" (`auto`, `cupy`, `opencl`, `numba`, `torch`): Which implementation to use. The `auto` will use the fastest available. The `torch` implementation is slow and approximated, but doesn't need extra dependencies. The `numba` implementation is good and just needs [Numba](https://numba.pydata.org/). The `cupy` implementation is the fastest, but needs [CuPy](https://cupy.dev/) and a full CUDA environment.
- `regularization` (`FLOAT`): The regularization strength (epsilon). This acts as a smoothness prior. Higher values result in smoother, more blended foreground and background colors, but may lose very fine details. Lower values preserve more detail but can be noisier.
- `n_small_iterations` (`INT`): The number of solver iterations to perform on the lower-resolution levels of the image pyramid. More iterations can improve quality at the cost of speed.
- `n_big_iterations` (`INT`): The number of solver iterations to perform on the higher-resolution (larger) levels of the image pyramid. Fewer iterations are typically needed at high resolution as the details are propagated up from the smaller levels.
- `small_size` (`INT`): The pixel dimension threshold. Image pyramid levels smaller than this size will use the higher 'n_small_iterations' count, while larger levels will use 'n_big_iterations'.
- `gradient_weight` (`FLOAT`): Controls how strongly the edges in the alpha matte influence color blending. A higher value makes the algorithm respect the mask's edges more, leading to sharper color boundaries. A lower value allows more color bleeding, an effect similar to increasing regularization.
- **Output:**
- `image` (`IMAGE`): The estimated foreground image (F). This is a full-color image where the algorithm has estimated the true, un-blended color of the foreground object.
- `mask` (`MASK`): The estimated background image (B). The algorithm has effectively "inpainted" the area behind the foreground object, creating a clean background plate.
# 10. Create Empty Image
- **Display Name:** `Create Empty Image`
- **Internal Name:** `SET_CreateEmptyImage`
- **Category:** `image/generation`
- **Description:** Creates an image filled with a solid color. Similar to standard `EmptyImage`, but you can use an image as reference and you have more options to select the color.
- **Purpose:** Create an empty image
- **Inputs:**
- `width` (`INT`): The width of the new image in pixels. This value is ignored if a `reference` is provided.
- `height` (`INT`): The height of the new image in pixels. This value is ignored if a `reference` is provided.
- `batch_size` (`INT`): The number of images to create in the batch. This value is ignored if a `reference` is provided.
- `color` (`STRING`): The solid color to fill the image with. Can be a named color (e.g., "black", "red"), a hex string (e.g., "#FF0000", "00ff00"), or comma-separated components in the [0, 255] or [0.0, 1.0] range.
- `reference` (`IMAGE`, optional): If an image is connected here, its dimensions (batch size, height, and width) will be used for the new image, overriding the manual width, height, and batch_size inputs.
- **Output:**
- `image` (`IMAGE`): A new image tensor of the specified size and color.
# 11. Pad Image (KJ/SET)
- **Display Name:** `Pad Image (KJ/SET)`
- **Internal Name:** `SET_ImagePad`
- **Category:** `image/manipulation`
- **Description:** Pads the input image and an optional mask with specified padding on each side. It offers several modes to fill the padded area.
- **Purpose:** To add borders or expand the canvas of an image using various fill methods, such as solid color, edge pixel replication, or a blurred background effect.
- **Inputs:**
- `image` (`IMAGE`): The image to be padded.
- `left` (`INT`): The number of pixels of padding to add to the left side.
- `right` (`INT`): The number of pixels of padding to add to the right side.
- `top` (`INT`): The number of pixels of padding to add to the top side.
- `bottom` (`INT`): The number of pixels of padding to add to the bottom side.
- `extra_padding` (`INT`): Additional padding applied to all sides. Note: If `target_width` and `target_height` are used, this value will shrink the source image before padding.
- `pad_mode` (`STRING`): The method used to fill the padded area.
- `edge`: Fills with the average color of the nearest edge.
- `edge_pixel`: Repeats the outermost line of pixels.
- `color`: Fills with the specified solid `color`.
- `pillarbox_blur`: Fills with a blurred, desaturated, and dimmed version of the original image, scaled to fit the new dimensions.
- `color` (`STRING`): The solid color to use when `pad_mode` is "color". Accepts hex codes, color names, and RGB values. This is more flexible than in KJNodes.
- `mask` (`MASK`, optional): An optional mask to be padded along with the image.
- `target_width` (`INT`, optional): If provided with `target_height`, pads the image to this exact width, centering the original content. This overrides the `left` and `right` inputs. If you set it to 0 the width of the image is used. Note that you must also connect `target_height`.
- `target_height` (`INT`, optional): If provided with `target_width`, pads the image to this exact height, centering the original content. This overrides the `top` and `bottom` inputs. If you set it to 0 the heigth of the image is used. Note that you must also connect `target_width`.
- `pad_transparency` (`FLOAT`, optional): Controls the opacity of the padded area in the output mask. 1.0 is fully transparent (no mask), and 0.0 is fully opaque (masked). Not in KJNodes.
- **Outputs:**
- `images` (`IMAGE`): The padded image tensor.
- `masks` (`MASK`): The padded mask tensor.
# 12. Resize Image (KJ/SET)
- **Display Name:** `Resize Image (KJ/SET)`
- **Internal Name:** `SET_ImageResize`
- **Category:** `image/manipulation`
- **Description:** A comprehensive node to resize, crop, and pad images. It provides multiple methods for handling aspect ratio and can optionally process large batches in smaller chunks to conserve memory. This node uses [Pad Image](#11-pad-image-kjset) so please also read its documentation.
- **Purpose:** To change the dimensions of an image with fine-grained control over how the aspect ratio is preserved, including stretching, cropping, or padding to fit the target size.
- **Inputs:**
- `image` (`IMAGE`): The source image to resize.
- `width` (`INT`): The target width in pixels. If set to 0, it uses the original width or the width from `get_image_size`.
- `height` (`INT`): The target height in pixels. If set to 0, it uses the original height or the height from `get_image_size`.
- `upscale_method` (`STRING`): The interpolation algorithm to use for resizing (e.g., `bicubic`, `lanczos`, `nearest-exact`).
- `keep_proportion` (`STRING`): Defines how to handle the aspect ratio when resizing.
- `stretch`: Ignores aspect ratio and forces the image to the exact target dimensions.
- `resize`: Maintains aspect ratio, fitting the image within the target dimensions. The final output size may be smaller than the target on one axis.
- `pad`, `pad_edge`, `pad_edge_pixel`: Resizes the image to fit within the target dimensions while maintaining aspect ratio, then pads the remaining space to meet the exact target dimensions.
- `crop`: Resizes and crops the image to match the target aspect ratio and size.
- `pillarbox_blur`: Resizes the image to fit and fills the remaining space with a blurred background.
- `pad_color` (`STRING`): The solid color to use for padding when `keep_proportion` is set to `pad`. Accepts hex codes, color names, and RGB values. This is more flexible than in KJNodes.
- `crop_position` (`STRING`): The anchor point (`center`, `top`, `left`, etc.) for `crop` and `pad` operations.
- `divisible_by` (`INT`): Ensures the final width and height are multiples of this number, which is often required for models like VAEs.
- `mask` (`MASK`, optional): An optional mask to be transformed along with the image.
- `device` (`STRING`, optional): The computation device (`cpu` or `gpu`) to use for the operation. Note that `lanczos` is not supported on the GPU.
- `get_image_size` (`IMAGE`, optional): If an image is connected, its dimensions will be used as the target width and height, overriding the manual inputs. Not in KJNodes.
- `per_batch` (`INT`, optional): If greater than 0, processes the input batch in smaller sub-batches of this size to reduce memory usage.
- `pad_transparency` (`FLOAT`, optional): Controls the opacity of the padded area in the output mask. Not in KJNodes.
- **Outputs:**
- `IMAGE` (`IMAGE`): The transformed image.
- `width` (`INT`): The final width of the output image.
- `height` (`INT`): The final height of the output image.
- `mask` (`MASK`): The transformed mask.
# 13. Resize Mask (KJ/SET)
- **Display Name:** `Resize Mask (KJ/SET)`
- **Internal Name:** `SET_ResizeMask`
- **Category:** `image/manipulation`
- **Description:** Resizes a mask or a batch of masks to the specified width and height, with options to preserve proportions.
- **Purpose:** A dedicated and simplified node for resizing masks, which often require different interpolation methods (like `nearest-exact`) than images.
- **Inputs:**
- `mask` (`MASK`): The mask to be resized.
- `width` (`INT`): The target width for the new mask.
- `height` (`INT`): The target height for the new mask.
- `keep_proportions` (`BOOLEAN`): If `True`, maintains the original aspect ratio, fitting the mask within the target dimensions.
- `upscale_method` (`STRING`): The interpolation algorithm to use for resizing. Defaults to `nearest-exact`, which is ideal for masks to avoid creating grayscale values.
- `crop` (`STRING`): If and how to crop the mask (`disabled` or `center`).
- `get_image_size` (`IMAGE`, optional): If an image is connected here, its dimensions will be used as the target width and height.
- **Outputs:**
- `mask` (`MASK`): The resized mask tensor.
- `width` (`INT`): The final width of the output mask.
- `height` (`INT`): The final height of the output mask.
# 14. Arbitrary Normalize
- **Display Name:** `Arbitrary Normalize`
- **Internal Name:** `SET_NormalizeArbitrary`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor using the mean and standard deviation provided in the `parameters` input.
- **Purpose:** Essential for pre-processing images before feeding them into models
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- `parameters` (`NORM_PARAMS`): The parameters for the normalization. This is a dict with two keys, `mean` and `std`, which are lists with the R, G, B parameters.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor. The value range will be altered significantly.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using the values from NORM_PARAMS
# 15. Normalize Parameters
- **Display Name:** `Normalize Parameters`
- **Internal Name:** `SET_NormalizeParameters`
- **Category:** `image/normalization`
- **Description:** Provides the `NORM_PARAMS` for the [Arbitrary Normalize](#14-arbitrary-normalize) node
- **Purpose:** This is a user interface to the [Arbitrary Normalize](#14-arbitrary-normalize) node
- **Inputs:**
- `mean_red` (`FLOAT`): Mean value for the red channel
- `mean_green` (`FLOAT`): Mean value for the green channel
- `mean_blue` (`FLOAT`): Mean value for the blue channel
- `std_red` (`FLOAT`): Standard deviation for the red channel
- `std_green` (`FLOAT`): Standard deviation for the green channel
- `std_blue` (`FLOAT`): Standard deviation for the blue channel
- **Output:**
- `parameters` (`NORM_PARAMS`): The parameters for the normalization. This is a dict with two keys, `mean` and `std`, which are lists with the R, G, B parameters.
# 16. Image with text label
- **Display Name:** `Image with text label`
- **Internal Name:** `SET_ImageWithTextLabel`
- **Category:** `image/manipulation`
- **Description:** Adds a border and a label to an image or a mask. This can be used to create comparison grids. Use the Comfy-Core `Image Stitch` node to concatenate the images.
- **Purpose:** Intended to create labeled images
- **Inputs:**
- `image` (`IMAGE`): Image to use, leave unconnected when using a mask.
- `mask` ("MASK`): Mask to be used as image, leave unconnected when using an image.
- `text` (`STRING`): Text to use as label. Can contain multiple lines.
- `side` ("top", "bottom", "left", "right"): The side where the label will be placed.
- `label_size` (`STRING`): The width (left/right) or height (top/bottom) of the area for the label. Can be expressed as a percentage (i.e. 10%) or absolute number of pixels.
- `separation` (`STRING`): The size of the border around the image. Can be expressed as a percentage (i.e. 1%) or absolute number of pixels.
- `background_color` (`STRING`): Color to fill the border and the label area. Can be an hexadecimal RGB (i.e. `#AABBCC`) or comma separated RGB components. The components can be in the [0-255] or [0-1.0] range. Some color names are also supported.
- `foreground_color` (`STRING`): Color the label font. Can be an hexadecimal RGB (i.e. `#AABBCC`) or comma separated RGB components. The components can be in the [0-255] or [0-1.0] range. Some color names are also supported.
- `font_name` (`STRING`): The name of the font to use for the text.
- **Output:**
- `IMAGE` (`IMAGE`): The generated RGB image.
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long
+7 -2
View File
@@ -2,14 +2,18 @@
name = "image-misc"
description = """
Miscellaneous nodes for image manipulation.
Currently just download image with bypass, so you can create workflows including image examples.
Download image with bypass, so you can create workflows including image examples.
Foreground estimation and mask apply.
Face composition nodes (to reinsert a processed face).
Advanced image/mask pad/resize (from KJNodes with some additions).
Normalization nodes for low level image pre-processing.
No extra dependencies, just an internal module.
"""
# This is the modern Python mechanism, but not supported
# dynamic = ["version"]
# license = "GPL-3.0-or-later"
# Only this is supported :-(
version = "1.2.0"
version = "1.4.0"
license = { file = "LICENSE" }
# The following is just ignored
dependencies = ["seconohe"]
@@ -20,6 +24,7 @@ Repository = "https://github.com/set-soft/ComfyUI-ImageMisc"
[tool.comfy]
PublisherId = "set-soft"
DisplayName = "Image Misc"
Icon = "https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/main/assets/Icon_400x400.jpg"
[tool.setuptools.dynamic]
version = {attr = "nodes.__version__"}
+1 -1
View File
@@ -4,6 +4,6 @@
# Project: ComfyUI-ImageMisc
from seconohe.logger import initialize_logger
__version__ = "1.2.0"
__version__ = "1.4.0"
NODES_NAME = "ImageMisc"
main_logger = initialize_logger(NODES_NAME)
+870 -8
View File
@@ -2,28 +2,48 @@
# Copyright (c) 2025 Salvador E. Tropea
# Copyright (c) 2025 Instituto Nacional de Tecnologïa Industrial
# License: GPL-3.0
#
# Project: ComfyUI-ImageMisc
# From code generated by Gemini 2.5 Pro
# Credits:
# - ImagePad, ImageResize and ResizeMask are from Kijai (https://github.com/kijai/ComfyUI-KJNodes/) v1.1.7
# - Assisted by Gemini 2.5 Pro
from copy import deepcopy
import numpy as np
import os
from PIL import Image # Import the Python Imaging Library
from PIL import Image, ImageDraw, ImageFont # Import the Python Imaging Library
from seconohe.apply_mask import apply_mask
from seconohe.foreground_estimation.affce import affce
from seconohe.foreground_estimation.fmlfe import fmlfe, IMPL_PRIORITY
from seconohe.color import color_to_rgb_float
from seconohe.downloader import download_file
from seconohe.color import color_to_rgb_float, color_to_rgb_uint8
# We are the main source, so we use the main_logger
from . import main_logger
import torch
import torch.nn.functional as F
import torchvision.transforms.functional as TF
from typing import Optional
try:
from folder_paths import get_input_directory # To get the ComfyUI input directory
from comfy import model_management
from comfy.utils import common_upscale
except ModuleNotFoundError:
# No ComfyUI, this is a test environment
def get_input_directory():
return ""
try:
from nodes import ImageScale
except Exception:
class ImageScale(object):
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
try:
from nodes import MAX_RESOLUTION
except Exception:
MAX_RESOLUTION = 16384
try:
from server import PromptServer
except ModuleNotFoundError:
PromptServer = None
try:
# We need to import the built-in LoadImage class for ImageDownload
from nodes import LoadImage
@@ -44,12 +64,49 @@ COLOR_OPT = ("STRING", {
"tooltip": "Color for fill.\n"
"Can be an hexadecimal (#RRGGBB).\n"
"Can comma separated RGB values in [0-255] or [0-1.0] range."})
DEFAULT_UPSCALE = 'bicubic' # transforms.InterpolationMode.BICUBIC.value
MASK_UPSCALE = 'nearest-exact' # transforms.InterpolationMode.NEAREST_EXACT.value
BEST_UPSCALE = 'lanczos' # transforms.InterpolationMode.LANCZOS.value
UPSCALE_OPT = (ImageScale.upscale_methods, { # [mode.value for mode in transforms.InterpolationMode]
"default": DEFAULT_UPSCALE,
"tooltip": "Interpolation method for image resize"
})
UPSCALE_OPT_MASK = deepcopy(UPSCALE_OPT)
UPSCALE_OPT_MASK[1]["default"] = MASK_UPSCALE
PAD_SIZE_OPT = ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1, })
SIZE_OPT = ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1})
SIZE_OPT_FI = deepcopy(SIZE_OPT)
SIZE_OPT_FI[1]["forceInput"] = True
SIZE_OPT_FI[1]["tooltip"] = ("Connect both `target` inputs\n"
"If 0 the size of the image is used\n"
"Overrides left/right/top/bottom")
SIZE_OPT[1]["tooltip"] = "Used when no `get_image_size` is provided"
PAD_TRANS = ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"display": "number",
"tooltip": ("The transparency for the padded area for all modes except `edge_pixel`."
"1.0 is fully transparent, 0.0 is fully opaque.")})
NORM_PARAM = ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"display": "number"})
# A dictionary to cache loaded fonts
font_cache = {}
def tensor_to_pil(tensor: torch.Tensor) -> Image.Image:
"""Converts a single image tensor (H, W, C) [0, 1] to a Pillow Image."""
np_image = (tensor.cpu().numpy() * 255).astype(np.uint8)
return Image.fromarray(np_image)
if tensor.dim() == 2:
return Image.fromarray(np_image, 'L')
else:
return Image.fromarray(np_image, 'RGB')
def pil_to_tensor(pil_image: Image.Image) -> torch.Tensor:
@@ -58,6 +115,27 @@ def pil_to_tensor(pil_image: Image.Image) -> torch.Tensor:
return torch.from_numpy(np_image)
def upscale(image, width, height, upscale_method):
# return F.interpolate(image, size=(height, width), mode=upscale_method)
return common_upscale(image, width, height, upscale_method, crop="disabled")
def parse_size(size_str, reference_dim):
"""Parses a size string which can be pixels or a percentage."""
size_str = size_str.strip()
if size_str.endswith('%'):
try:
percentage = float(size_str[:-1])
return int(reference_dim * (percentage / 100.0))
except ValueError:
return 0
else:
try:
return int(size_str)
except ValueError:
return 0
if has_load_image:
class ImageDownload:
@classmethod
@@ -83,6 +161,10 @@ if has_load_image:
"default": "",
"tooltip": "The name used locally. Leave empty to use `filename`"
}),
"embed_transparency": ("BOOLEAN", {
"default": False,
"tooltip": "Create RGBA images when they have transparency."
}),
}
}
@@ -100,7 +182,8 @@ if has_load_image:
OUTPUT_NODE = True
def load_or_download_image(self, base_url: str, filename: str, image_bypass: Optional[torch.Tensor] = None,
mask_bypass: Optional[torch.Tensor] = None, local_name: str = None):
mask_bypass: Optional[torch.Tensor] = None, local_name: str = None,
embed_transparency: bool = False):
# If we have something at the bypass inputs use it
if image_bypass is not None or mask_bypass is not None:
if image_bypass is None:
@@ -146,6 +229,14 @@ if has_load_image:
# Call the method and return its result directly
result = loader_instance.load_image(dest_fname)
# Create an RGBA image if needed
if embed_transparency:
image, mask = result
# Expand the mask to (b, h, w, 1)
mask = mask[..., None]
# Concatenate image and mask into (b, h, w, 4)
image_with_alpha = torch.cat([image, 1.0 - mask], dim=-1)
result = (image_with_alpha, mask)
# This information is for the preview, as we are an output node and we return images
# they will be displayed in our node. Quite simple.
downloaded_file = {
@@ -355,7 +446,6 @@ class NormalizeToImageNetDataset():
DESCRIPTION = ("Normalize the image to the ImageNet dataset")
UNIQUE_NAME = "SET_NormalizeToImageNetDataset"
DISPLAY_NAME = "Normalize Image to ImageNet"
imagenet_normalize = None
def normalize(self, image: torch.Tensor):
return (TF.normalize(image.permute(0, 3, 1, 2), # BHWC -> BCHW
@@ -377,7 +467,6 @@ class NormalizeToRangeMinus05to05():
DESCRIPTION = ("Normalize the image to [-0.5, 0.5]")
UNIQUE_NAME = "SET_NormalizeToRangeMinus05to05"
DISPLAY_NAME = "Normalize Image to [-0.5, 0.5]"
imagenet_normalize = None
def normalize(self, image: torch.Tensor):
return (TF.normalize(image.permute(0, 3, 1, 2), # BHWC -> BCHW
@@ -399,7 +488,6 @@ class NormalizeToRangeMinus1to1():
DESCRIPTION = ("Normalize the image to [-1, 1]")
UNIQUE_NAME = "SET_NormalizeToRangeMinus1to1"
DISPLAY_NAME = "Normalize Image to [-1, 1] (i.e. GAN)"
imagenet_normalize = None
def normalize(self, image: torch.Tensor):
return (TF.normalize(image.permute(0, 3, 1, 2), # BHWC -> BCHW
@@ -407,6 +495,57 @@ class NormalizeToRangeMinus1to1():
std=[0.5, 0.5, 0.5]).permute(0, 2, 3, 1),) # BCHW -> BHWC
class NormalizeArbitrary():
"""
A ComfyUI node to normalize the values to arbitrary mean/std
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"parameters": ("NORM_PARAMS",),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "normalize"
CATEGORY = BASE_CATEGORY + "/" + NORMALIZATION
DESCRIPTION = ("Normalize the image to the provided parameters")
UNIQUE_NAME = "SET_NormalizeArbitrary"
DISPLAY_NAME = "Arbitrary Normalize"
def normalize(self, image: torch.Tensor, parameters):
return (TF.normalize(image.movedim(-1, 1), # BHWC -> BCHW
mean=parameters["mean"],
std=parameters["std"]).movedim(1, -1),) # BCHW -> BHWC
class NormalizeParameters():
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mean_red": NORM_PARAM,
"mean_green": NORM_PARAM,
"mean_blue": NORM_PARAM,
"std_red": NORM_PARAM,
"std_green": NORM_PARAM,
"std_blue": NORM_PARAM,
},
}
RETURN_TYPES = ("NORM_PARAMS",)
RETURN_NAMES = ("parameters",)
FUNCTION = "normalize"
CATEGORY = BASE_CATEGORY + "/" + NORMALIZATION
DESCRIPTION = ("Parameters for the arbitrary normalization")
UNIQUE_NAME = "SET_NormalizeParameters"
DISPLAY_NAME = "Normalize Parameters"
def normalize(self, mean_red, mean_green, mean_blue, std_red, std_green, std_blue):
return ({"mean": [mean_red, mean_green, mean_blue], "std": [std_red, std_green, std_blue]},)
class ApplyMaskAFFCE:
@classmethod
def INPUT_TYPES(cls):
@@ -663,3 +802,726 @@ class CreateEmptyImage:
final_image = color_tensor.expand(b, h, w, 3)
return (final_image,)
# Adapted from KJNodes, credits to Kijai
# - When target_width/target_height are 0 we use the image size
# - Added control over the transparency of the padded area (pad_transparency)
# - Handle RGBA images
class ImagePad:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE", ),
"left": PAD_SIZE_OPT,
"right": PAD_SIZE_OPT,
"top": PAD_SIZE_OPT,
"bottom": PAD_SIZE_OPT,
"extra_padding": PAD_SIZE_OPT,
"pad_mode": (["edge", "edge_pixel", "color", "pillarbox_blur"],),
"color": COLOR_OPT,
},
"optional": {
"mask": ("MASK", ),
"target_width": SIZE_OPT_FI,
"target_height": SIZE_OPT_FI,
"pad_transparency": PAD_TRANS,
}
}
RETURN_TYPES = ("IMAGE", "MASK", )
RETURN_NAMES = ("images", "masks",)
FUNCTION = "pad"
CATEGORY = BASE_CATEGORY + "/" + MANIPULATION_CATEGORY
DESCRIPTION = ("Pad the input image and optionally mask with the specified padding.\n"
"The `target_width`/`target_height` overrides left, right, top and bottom.")
UNIQUE_NAME = "SET_ImagePad"
DISPLAY_NAME = "Pad Image (KJ/SET)"
def pad(self, image, left, right, top, bottom, extra_padding, color, pad_mode, mask=None, target_width=None,
target_height=None, pad_transparency=1.0):
B, H, W, C = image.shape
# Resize masks to image dimensions if necessary
if mask is not None:
BM, HM, WM = mask.shape
if HM != H or WM != W:
mask = F.interpolate(mask.unsqueeze(1), size=(H, W), mode=MASK_UPSCALE).squeeze(1)
# Parse background color
color_tuple = color_to_rgb_float(logger, color)
if C == 4 and len(color_tuple) == 3:
color_tuple += (1.0 - pad_transparency,) # Use transparent color to pad RGBA images. 0 is transparent for RGBA
bg_color = torch.tensor(color_tuple, dtype=image.dtype, device=image.device)
# Calculate padding sizes with extra padding
if target_width is not None and target_height is not None:
# SET: If any of them is 0 use the current value
if target_width == 0:
target_width = W
if target_height == 0:
target_height = H
if extra_padding > 0:
image = upscale(image.movedim(-1, 1), W - extra_padding, H - extra_padding, BEST_UPSCALE).movedim(1, -1)
B, H, W, C = image.shape
padded_width = target_width
padded_height = target_height
pad_left = (padded_width - W) // 2
pad_right = padded_width - W - pad_left
pad_top = (padded_height - H) // 2
pad_bottom = padded_height - H - pad_top
else:
pad_left = left + extra_padding
pad_right = right + extra_padding
pad_top = top + extra_padding
pad_bottom = bottom + extra_padding
padded_width = W + pad_left + pad_right
padded_height = H + pad_top + pad_bottom
# Pillarbox blur mode
if pad_mode == "pillarbox_blur":
def _gaussian_blur_nchw(img_nchw, sigma_px):
if sigma_px <= 0:
return img_nchw
radius = max(1, int(3.0 * float(sigma_px)))
k = 2 * radius + 1
x = torch.arange(-radius, radius + 1, device=img_nchw.device, dtype=img_nchw.dtype)
k1 = torch.exp(-(x * x) / (2.0 * float(sigma_px) * float(sigma_px)))
k1 = k1 / k1.sum()
kx = k1.view(1, 1, 1, k)
ky = k1.view(1, 1, k, 1)
c = img_nchw.shape[1]
kx = kx.repeat(c, 1, 1, 1)
ky = ky.repeat(c, 1, 1, 1)
img_nchw = F.conv2d(img_nchw, kx, padding=(0, radius), groups=c)
img_nchw = F.conv2d(img_nchw, ky, padding=(radius, 0), groups=c)
return img_nchw
out_image = torch.zeros((B, padded_height, padded_width, C), dtype=image.dtype, device=image.device)
for b in range(B):
scale_fill = max(padded_width / float(W), padded_height / float(H)) if (W > 0 and H > 0) else 1.0
bg_w = max(1, int(round(W * scale_fill)))
bg_h = max(1, int(round(H * scale_fill)))
src_b = image[b].movedim(-1, 0).unsqueeze(0)
bg = upscale(src_b, bg_w, bg_h, "bilinear")
y0 = max(0, (bg_h - padded_height) // 2)
x0 = max(0, (bg_w - padded_width) // 2)
y1 = min(bg_h, y0 + padded_height)
x1 = min(bg_w, x0 + padded_width)
bg = bg[:, :, y0:y1, x0:x1]
if bg.shape[2] != padded_height or bg.shape[3] != padded_width:
pad_h = padded_height - bg.shape[2]
pad_w = padded_width - bg.shape[3]
pad_top_fix = max(0, pad_h // 2)
pad_bottom_fix = max(0, pad_h - pad_top_fix)
pad_left_fix = max(0, pad_w // 2)
pad_right_fix = max(0, pad_w - pad_left_fix)
bg = F.pad(bg, (pad_left_fix, pad_right_fix, pad_top_fix, pad_bottom_fix), mode="replicate")
sigma = max(1.0, 0.006 * float(min(padded_height, padded_width)))
bg = _gaussian_blur_nchw(bg, sigma_px=sigma)
if C >= 3:
r, g, bch = bg[:, 0:1], bg[:, 1:2], bg[:, 2:3]
luma = 0.2126 * r + 0.7152 * g + 0.0722 * bch
gray = torch.cat([luma, luma, luma], dim=1)
desat = 0.20
rgb = torch.cat([r, g, bch], dim=1)
rgb = rgb * (1.0 - desat) + gray * desat
bg[:, 0:3, :, :] = rgb
dim = 0.35
bg = torch.clamp(bg * dim, 0.0, 1.0)
out_image[b] = bg.squeeze(0).movedim(0, -1)
out_image[:, pad_top:pad_top+H, pad_left:pad_left+W, :] = image
# Mask handling for pillarbox_blur
if mask is not None:
fg_mask = mask
out_masks = torch.ones((B, padded_height, padded_width), dtype=image.dtype, device=image.device)
out_masks[:, pad_top:pad_top+H, pad_left:pad_left+W] = fg_mask
else:
out_masks = torch.ones((B, padded_height, padded_width), dtype=image.dtype, device=image.device)
out_masks[:, pad_top:pad_top+H, pad_left:pad_left+W] = 0.0
return (out_image, out_masks)
# Standard pad logic (edge/color)
out_image = torch.zeros((B, padded_height, padded_width, C), dtype=image.dtype, device=image.device)
for b in range(B):
if pad_mode == "edge":
# Pad with edge color (mean)
top_edge = image[b, 0, :, :]
bottom_edge = image[b, H-1, :, :]
left_edge = image[b, :, 0, :]
right_edge = image[b, :, W-1, :]
out_image[b, :pad_top, :, :] = top_edge.mean(dim=0)
out_image[b, pad_top+H:, :, :] = bottom_edge.mean(dim=0)
out_image[b, :, :pad_left, :] = left_edge.mean(dim=0)
out_image[b, :, pad_left+W:, :] = right_edge.mean(dim=0)
out_image[b, pad_top:pad_top+H, pad_left:pad_left+W, :] = image[b]
elif pad_mode == "edge_pixel":
# Pad with exact edge pixel values
for y in range(pad_top):
out_image[b, y, pad_left:pad_left+W, :] = image[b, 0, :, :]
for y in range(pad_top+H, padded_height):
out_image[b, y, pad_left:pad_left+W, :] = image[b, H-1, :, :]
for x in range(pad_left):
out_image[b, pad_top:pad_top+H, x, :] = image[b, :, 0, :]
for x in range(pad_left+W, padded_width):
out_image[b, pad_top:pad_top+H, x, :] = image[b, :, W-1, :]
out_image[b, :pad_top, :pad_left, :] = image[b, 0, 0, :]
out_image[b, :pad_top, pad_left+W:, :] = image[b, 0, W-1, :]
out_image[b, pad_top+H:, :pad_left, :] = image[b, H-1, 0, :]
out_image[b, pad_top+H:, pad_left+W:, :] = image[b, H-1, W-1, :]
out_image[b, pad_top:pad_top+H, pad_left:pad_left+W, :] = image[b]
else:
# Pad with specified background color
out_image[b, :, :, :] = bg_color.unsqueeze(0).unsqueeze(0)
out_image[b, pad_top:pad_top+H, pad_left:pad_left+W, :] = image[b]
# Note: in the mask 1 is transparent and 0 opaque (reverse of RGBA)
if mask is not None:
out_masks = torch.nn.functional.pad(
mask,
(pad_left, pad_right, pad_top, pad_bottom),
mode='replicate' if pad_mode == "edge_pixel" else 'constant',
value=None if pad_mode == "edge_pixel" else pad_transparency,
)
else:
out_masks = torch.full((B, padded_height, padded_width), pad_transparency, dtype=image.dtype,
device=image.device)
for m in range(B):
out_masks[m, pad_top:pad_top+H, pad_left:pad_left+W] = 0.0
return (out_image, out_masks)
# Adapted from KJNodes, credits to Kijai
# Differences:
# - The color is an string that support various formats
# - We can copy the size of a reference image (found in V1, not in V2)
# - Removed misleading code to compute padded size when width and/or height was missing
# - Added control over the transparency of the padded area
class ImageResize:
"""
A resize and crop node, from ImageResizeKJv2
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE", {"tooltip": "Image to resize"}),
"width": SIZE_OPT,
"height": SIZE_OPT,
"upscale_method": UPSCALE_OPT,
"keep_proportion": (["stretch", "resize", "pad", "pad_edge", "pad_edge_pixel", "crop", "pillarbox_blur"],
{"default": "stretch",
"tooltip": "`stretch` doesn't keep the aspect ratio\n"
"`pad` adds `pad_color` bars\n"
"`pad_edge` fills using the edge color\n"
"`resize` always keeps aspect, so W and H might change\n"
"`crop` takes a portion of the image"}),
"pad_color": COLOR_OPT,
"crop_position": (["center", "top", "bottom", "left", "right"],
{"default": "center", "tooltip": "Also used for `pad`"}),
"divisible_by": ("INT", {"default": 2, "min": 0, "max": 512, "step": 1,
"tooltip": "Force the final size to be divisible by"}),
},
"optional": {
"mask": ("MASK", {"tooltip": "Optional mask for the image\nwill be resized"}),
"device": (["cpu", "gpu"],),
"get_image_size": ("IMAGE", {"tooltip": "Image size to use as reference"}),
"per_batch": ("INT", {
"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1,
"tooltip": "Process images in sub-batches to reduce memory usage. 0 disables sub-batching."}),
"pad_transparency": PAD_TRANS,
},
"hidden": {
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "MASK",)
RETURN_NAMES = ("IMAGE", "width", "height", "mask",)
FUNCTION = "resize"
CATEGORY = BASE_CATEGORY + "/" + MANIPULATION_CATEGORY
DESCRIPTION = ("Resizes the image to the specified width and height.\n"
"Size can be retrieved from the input (when w=h=0) or a reference image.\n\n"
"Keep proportions keeps the aspect ratio of the image, by\n"
"highest dimension.")
UNIQUE_NAME = "SET_ImageResize"
DISPLAY_NAME = "Resize Image (KJ/SET)"
def resize(self, image, width, height, keep_proportion, upscale_method, divisible_by, pad_color, crop_position,
unique_id, device="cpu", mask=None, get_image_size=None, per_batch=0, pad_transparency=1.0):
B, H, W, C = image.shape
if device == "gpu":
if upscale_method == "lanczos":
raise Exception("Lanczos is not supported on the GPU")
device = model_management.get_torch_device()
else:
device = torch.device("cpu")
# Image size from a reference image
if get_image_size is not None:
width = get_image_size.shape[2]
height = get_image_size.shape[1]
# Copy the size that is 0
if width == 0:
width = W
if height == 0:
height = H
pillarbox_blur = keep_proportion == "pillarbox_blur"
# Initialize padding variables
pad_left = pad_right = pad_top = pad_bottom = 0
# Solve the size for the ones that keeps aspect: resize, pad and pad_edge
if keep_proportion == "resize" or keep_proportion.startswith("pad") or pillarbox_blur:
# If one of the dimensions is zero, calculate it to maintain the aspect ratio
ratio = min(width / W, height / H)
new_width = round(W * ratio)
new_height = round(H * ratio)
if keep_proportion.startswith("pad") or pillarbox_blur:
# Calculate padding based on position
if crop_position == "center":
pad_left = (width - new_width) // 2
pad_right = width - new_width - pad_left
pad_top = (height - new_height) // 2
pad_bottom = height - new_height - pad_top
elif crop_position == "top":
pad_left = (width - new_width) // 2
pad_right = width - new_width - pad_left
pad_top = 0
pad_bottom = height - new_height
elif crop_position == "bottom":
pad_left = (width - new_width) // 2
pad_right = width - new_width - pad_left
pad_top = height - new_height
pad_bottom = 0
elif crop_position == "left":
pad_left = 0
pad_right = width - new_width
pad_top = (height - new_height) // 2
pad_bottom = height - new_height - pad_top
elif crop_position == "right":
pad_left = width - new_width
pad_right = 0
pad_top = (height - new_height) // 2
pad_bottom = height - new_height - pad_top
width = new_width
height = new_height
if divisible_by > 1:
width = width - (width % divisible_by)
height = height - (height % divisible_by)
# Preflight estimate (log-only when batching is active)
if per_batch != 0 and B > per_batch:
try:
bytes_per_elem = image.element_size() # typically 4 for float32
est_total_bytes = B * height * width * C * bytes_per_elem
est_mb = est_total_bytes / (1024 * 1024)
msg = f"<tr><td>Resize Image</td><td>estimated output ~{est_mb:.2f} MB; batching {per_batch}/{B}</td></tr>"
if unique_id and PromptServer is not None:
try:
PromptServer.instance.send_progress_text(msg, unique_id)
except Exception:
pass
logger.info(f"estimated output ~{est_mb:.2f} MB; batching {per_batch}/{B}")
except Exception:
pass
def _process_subbatch(in_image, in_mask, pad_left, pad_right, pad_top, pad_bottom):
# Avoid unnecessary clones; only move if needed
out_image = in_image if in_image.device == device else in_image.to(device)
out_mask = None if in_mask is None else (in_mask if in_mask.device == device else in_mask.to(device))
# Crop logic
if keep_proportion == "crop":
old_height = out_image.shape[-3]
old_width = out_image.shape[-2]
old_aspect = old_width / old_height
new_aspect = width / height
# Calculate dimensions to keep
if old_aspect > new_aspect: # Image is wider than target
crop_w = round(old_height * new_aspect)
crop_h = old_height
else: # Image is taller than target
crop_w = old_width
crop_h = round(old_width / new_aspect)
# Calculate crop position
if crop_position == "center":
x = (old_width - crop_w) // 2
y = (old_height - crop_h) // 2
elif crop_position == "top":
x = (old_width - crop_w) // 2
y = 0
elif crop_position == "bottom":
x = (old_width - crop_w) // 2
y = old_height - crop_h
elif crop_position == "left":
x = 0
y = (old_height - crop_h) // 2
elif crop_position == "right":
x = old_width - crop_w
y = (old_height - crop_h) // 2
# Apply crop
out_image = out_image.narrow(-2, x, crop_w).narrow(-3, y, crop_h)
if out_mask is not None:
out_mask = out_mask.narrow(-1, x, crop_w).narrow(-2, y, crop_h)
# Resize the image
out_image = upscale(out_image.movedim(-1, 1), width, height, upscale_method).movedim(1, -1)
if out_mask is not None:
if upscale_method == "lanczos":
out_mask = upscale(out_mask.unsqueeze(1).repeat(1, 3, 1, 1), width, height,
upscale_method).movedim(1, -1)[:, :, :, 0]
else:
out_mask = upscale(out_mask.unsqueeze(1), width, height, upscale_method).squeeze(1)
# Pad logic
if (keep_proportion.startswith("pad") or pillarbox_blur) and (pad_left > 0 or pad_right > 0 or pad_top > 0
or pad_bottom > 0):
padded_width = width + pad_left + pad_right
padded_height = height + pad_top + pad_bottom
if divisible_by > 1:
width_remainder = padded_width % divisible_by
height_remainder = padded_height % divisible_by
if width_remainder > 0:
extra_width = divisible_by - width_remainder
pad_right += extra_width
if height_remainder > 0:
extra_height = divisible_by - height_remainder
pad_bottom += extra_height
pad_mode = (
"pillarbox_blur" if pillarbox_blur else
"edge" if keep_proportion == "pad_edge" else
"edge_pixel" if keep_proportion == "pad_edge_pixel" else
"color"
)
out_image, out_mask = ImagePad.pad(self, out_image, pad_left, pad_right, pad_top, pad_bottom, 0, pad_color,
pad_mode, mask=out_mask, pad_transparency=pad_transparency)
return out_image, out_mask
# If batching disabled (per_batch==0) or batch fits, process whole batch
if per_batch == 0 or B <= per_batch:
out_image, out_mask = _process_subbatch(image, mask, pad_left, pad_right, pad_top, pad_bottom)
else:
chunks = []
mask_chunks = [] if mask is not None else None
total_batches = (B + per_batch - 1) // per_batch
current_batch = 0
for start_idx in range(0, B, per_batch):
current_batch += 1
end_idx = min(start_idx + per_batch, B)
sub_img = image[start_idx:end_idx]
sub_mask = mask[start_idx:end_idx] if mask is not None else None
sub_out_img, sub_out_mask = _process_subbatch(sub_img, sub_mask, pad_left, pad_right, pad_top, pad_bottom)
chunks.append(sub_out_img.cpu())
if mask is not None:
mask_chunks.append(sub_out_mask.cpu() if sub_out_mask is not None else None)
# Per-batch progress update
if unique_id and PromptServer is not None:
try:
PromptServer.instance.send_progress_text(
f"<tr><td>Resize Image</td><td>batch {current_batch}/{total_batches} · images {end_idx}/{B}"
"</td></tr>",
unique_id
)
except Exception:
pass
else:
try:
logger.info(f"batch {current_batch}/{total_batches} · images {end_idx}/{B}")
except Exception:
pass
out_image = torch.cat(chunks, dim=0)
if mask is not None and any(m is not None for m in mask_chunks):
out_mask = torch.cat([m for m in mask_chunks if m is not None], dim=0)
else:
out_mask = None
# Progress UI
if unique_id and PromptServer is not None:
try:
num_elements = out_image.numel()
element_size = out_image.element_size()
memory_size_mb = (num_elements * element_size) / (1024 * 1024)
PromptServer.instance.send_progress_text(
f"<tr><td>Output: </td><td><b>{out_image.shape[0]}</b> x <b>{out_image.shape[2]}</b> x <b>"
f"{out_image.shape[1]} | {memory_size_mb:.2f}MB</b></td></tr>",
unique_id
)
except Exception:
pass
return (out_image.cpu(), out_image.shape[2], out_image.shape[1],
out_mask.cpu() if out_mask is not None else
torch.zeros(64, 64, device=torch.device("cpu"), dtype=torch.float32))
# Adapted from KJNodes, credits to Kijai
# Difference: reference image `get_image_size`
class ResizeMask:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mask": ("MASK",),
"width": SIZE_OPT,
"height": SIZE_OPT,
"keep_proportions": ("BOOLEAN", {"default": False}),
"upscale_method": UPSCALE_OPT_MASK,
"crop": (["disabled", "center"],),
},
"optional": {
"get_image_size": ("IMAGE", {"tooltip": "Image size to use as reference"}),
},
}
RETURN_TYPES = ("MASK", "INT", "INT",)
RETURN_NAMES = ("mask", "width", "height",)
FUNCTION = "resize"
CATEGORY = BASE_CATEGORY + "/" + MANIPULATION_CATEGORY
DESCRIPTION = "Resizes the mask or batch of masks to the specified width and height."
UNIQUE_NAME = "SET_ResizeMask"
DISPLAY_NAME = "Resize Mask (KJ/SET)"
def resize(self, mask, width, height, keep_proportions, upscale_method, crop, get_image_size=None):
# Image size from a reference image
if get_image_size is not None:
width = get_image_size.shape[2]
height = get_image_size.shape[1]
if keep_proportions:
_, oh, ow = mask.shape
width = ow if width == 0 else width
height = oh if height == 0 else height
ratio = min(width / ow, height / oh)
width = round(ow*ratio)
height = round(oh*ratio)
if upscale_method == "lanczos":
out_mask = common_upscale(mask.unsqueeze(1).repeat(1, 3, 1, 1), width, height, upscale_method,
crop=crop).movedim(1, -1)[:, :, :, 0]
else:
out_mask = common_upscale(mask.unsqueeze(1), width, height, upscale_method, crop=crop).squeeze(1)
return (out_mask, out_mask.shape[2], out_mask.shape[1],)
# #################################################################################################
# ImageWithTextLabel
# #################################################################################################
def load_font(font_name, font_size):
"""Loads a font from the cache or from file."""
if (font_name, font_size) in font_cache:
return font_cache[(font_name, font_size)]
try:
font = ImageFont.truetype(font_name, font_size)
except IOError:
try:
# Fallback for Linux
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
if not os.path.exists(font_path):
# Fallback to a common system font if the specified one isn't found
font_path = os.path.join("C:", os.sep, "Windows", "Fonts", f"{font_name}.ttf")
font = ImageFont.truetype(font_path, font_size)
except IOError:
# If all else fails, use the default Pillow font
logger.warning(f"Font '{font_name}' not found. Falling back to default font.")
font = ImageFont.load_default()
font_cache[(font_name, font_size)] = font
return font
class ImageWithTextLabel:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {
"multiline": True, "default": "Your text here",
"tooltip": "Label for this image"}),
"side": (["top", "bottom", "left", "right"],),
"label_size": ("STRING", {
"default": "10%",
"tooltip": "Expressed as a percentage (i.e. 10%) or absolute number of pixels"}),
"separation": ("STRING", {
"default": "1%",
"tooltip": "Expressed as a percentage (i.e. 1%) or absolute number of pixels"}),
"background_color": ("STRING", {"default": "white"}),
"foreground_color": ("STRING", {"default": "black"}),
"font_name": ("STRING", {"default": "Arial"}),
},
"optional": {
"image": ("IMAGE", {
"tooltip": "Image, leave unconnected when using a mask"}),
"mask": ("MASK", {
"tooltip": "Mask to be used as image, leave unconnected when using an image"}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "add_label"
CATEGORY = BASE_CATEGORY + "/" + MANIPULATION_CATEGORY
DESCRIPTION = ("Adds a text label to an image")
UNIQUE_NAME = "SET_ImageWithTextLabel"
DISPLAY_NAME = "Image with text label, good for comparison grids"
def add_label(self, text: str, side: str, label_size: str, separation: int, background_color: str,
foreground_color: str, font_name: str, image: Optional[torch.Tensor] = None,
mask: Optional[torch.Tensor] = None):
if image is None and mask is None:
raise ValueError("You must provide an image or a mask")
image = image if image is not None else mask
# Process multiple images in the batch
pil_images = [tensor_to_pil(img) for img in image]
output_images = []
for pil_img in pil_images:
original_width, original_height = pil_img.size
# Parse label size
if side in ["top", "bottom"]:
label_dim = parse_size(label_size, original_height)
separation_px = parse_size(separation, original_height)
else: # left, right
label_dim = parse_size(label_size, original_width)
separation_px = parse_size(separation, original_width)
# Calculate new canvas dimensions
new_width = original_width + 2 * separation_px
new_height = original_height + 2 * separation_px
if side in ["top", "bottom"]:
new_height += label_dim
else: # left, right
new_width += label_dim
# Create new image canvas
bg_color_rgb = color_to_rgb_uint8(logger, background_color)
canvas = Image.new("RGB", (new_width, new_height), bg_color_rgb)
# Paste the original image with separation
img_paste_position = (separation_px, separation_px)
if side == "top":
img_paste_position = (separation_px, separation_px + label_dim)
elif side == "left":
img_paste_position = (separation_px + label_dim, separation_px)
canvas.paste(pil_img, img_paste_position)
draw = ImageDraw.Draw(canvas)
# Define the text area
text_area = (0, 0, 0, 0)
if side == "top":
text_area = (separation_px, separation_px, new_width - separation_px, separation_px + label_dim)
elif side == "bottom":
text_area = (separation_px, original_height + separation_px, new_width - separation_px, new_height -
separation_px)
elif side == "left":
text_area = (separation_px, separation_px, separation_px + label_dim, new_height - separation_px)
elif side == "right":
text_area = (original_width + separation_px, separation_px, new_width - separation_px, new_height -
separation_px)
text_area_width = text_area[2] - text_area[0]
text_area_height = text_area[3] - text_area[1]
fg_color_rgb = color_to_rgb_uint8(logger, foreground_color)
text_x = text_area[0] + text_area_width / 2
text_y = text_area[1] + text_area_height / 2
font = None
# --- Find best font size and render text ---
if side in ["top", "bottom"]:
# --- HORIZONTAL TEXT LOGIC (with wrapping) ---
max_font_size = text_area_height
font_size = max_font_size
wrapped_text = text
while font_size > 5:
font = load_font(font_name, font_size)
# Simple word wrapping
lines = []
words = text.split()
current_line = ""
for word in words:
if font.getlength(current_line + word + " ") < text_area_width:
current_line += word + " "
else:
lines.append(current_line.strip())
current_line = word + " "
lines.append(current_line.strip())
wrapped_text = "\n".join(lines)
# Check if wrapped text fits vertically
bbox = draw.multiline_textbbox((0, 0), wrapped_text, font=font)
text_height = bbox[3] - bbox[1]
if text_height <= text_area_height:
break # Font size is good
font_size -= 1
else:
logger.warning("Text could not fit into the designated area even at the smallest font size.")
draw.multiline_text(
(text_x, text_y),
wrapped_text,
fill=fg_color_rgb,
font=font,
anchor="mm", # middle-middle anchor
align="center"
)
else: # --- VERTICAL TEXT LOGIC ---
max_font_size = text_area_width # For vertical, font size is constrained by width
font_size = max_font_size
while font_size > 5:
font = load_font(font_name, font_size)
# Use textbbox with direction to measure final size
bbox = draw.textbbox((0, 0), text, font=font, direction="ttb")
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Check if the vertically rendered text fits in the area
if text_width <= text_area_width and text_height <= text_area_height:
break # Font size is good
font_size -= 1
else:
logger.warning("Text could not fit into the designated area even at the smallest font size.")
draw.text(
(text_x, text_y),
text,
fill=fg_color_rgb,
font=font,
anchor="mm", # middle-middle anchor
direction="ttb" # Top-to-bottom direction
)
output_images.append(pil_to_tensor(canvas).unsqueeze(0))
# Stack the processed images back into a single tensor
return (torch.cat(output_images, dim=0),)