Extensions/comfyui_imgtools
ComfyUI Extension

comfyui_imgtools

A ComfyUI custom node for automatically fitting images to standard model dimensions using center cropping, supporting SD, Flux, and Z-Turbo models.

By rholdorf·Created 8 months ago·Updated 15 days ago· 0
rholdorf/comfyui_imgtools
Nodes11
On cloudLocal install
Categoryrholdorf/image, rholdorf/lora
Stars0
Updated15 days ago
Readme

comfyui_rholdorf

A small ComfyUI custom-node pack: image processing (denoise, resize), resolution helpers, LoRA training-data loaders/savers, a LoRA merger, and a text utility.

All nodes are labelled ... (rholdorf) in the node menu and live under a rholdorf/... category.

Installation

Clone or copy this folder into your ComfyUI custom_nodes directory:

ComfyUI/
  custom_nodes/
    comfyui_rholdorf/

Restart ComfyUI. There is no build step and no extra dependency — everything runs on what ComfyUI already ships (torch, numpy, Pillow).

Changes to Python require a ComfyUI restart; changes to the JS under web/ only need a browser refresh.

Nodes

| Node | Category | Purpose | |---|---|---| | Image Denoise (Affinity style) | rholdorf/image | Wavelet denoise with Affinity Photo's five sliders | | Image Resize (Lanczos 3 non-separable) | rholdorf/image | Radial Lanczos downscale, no diagonal artifacts | | Image Dimension Fitter | rholdorf/image | Snap a size to the closest native resolution of a model | | Image Padding Calculator | rholdorf/image | Padding needed to centre an image in a target size | | Load Image with Caption | rholdorf/image | Load Image + its sidecar .txt caption | | Load Images with Captions from Dir | rholdorf/image | Batch-load a folder of image/caption pairs | | Save Image with Caption | rholdorf/image | Save PNG + matching .txt caption | | Resolution Selector from Dimensions | rholdorf/utils | Resolution Selector with auto-detected aspect ratio | | Path Splitter | rholdorf/utils | Split a path into directory / filename / stem | | Merge LoRAs to File | rholdorf/lora | Bake two LoRAs into one .safetensors | | Random Line Concatenator | rholdorf/text | Pick a random line from a toggleable pool |


Image Denoise (Affinity style)

Reproduces the control surface of Affinity Photo 2's Denoise filter. Not bit-identical (the original is proprietary), but the same five knobs with an equivalent response curve.

The image is converted to BT.709 YCbCr and decomposed with an à trous (starlet) wavelet transform; detail levels are shrunk with the non-negative garrote, which leaves strong coefficients — edges — untouched instead of dulling them the way soft thresholding would. Luma and chroma are filtered independently, which is what lets you crush colour noise hard without touching detail.

| Input | Type | Default | Description | |---|---|---|---| | image | IMAGE | — | Input image | | luminance | FLOAT 0–100 | 30 | Luma noise reduction strength | | luminance_detail | FLOAT 0–100 | 50 | Higher keeps more fine texture (and more grain) | | luminance_contribution | FLOAT 0–100 | 100 | Blend of the filtered luma over the original | | colours | FLOAT 0–100 | 50 | Chroma noise reduction strength | | colours_contribution | FLOAT 0–100 | 100 | Blend of the filtered chroma over the original | | levels | INT 1–7 (optional) | 5 | Wavelet depth; auto-capped to the image size |

Output: IMAGE.

Notes:

  • Luma and chroma use different noise models on purpose. Luma assumes white noise (sigma measured once at the finest scale), which keeps coarse structure safe. Chroma re-estimates sigma per level, because colour blotching is spatially correlated and carries almost no energy at the finest scale — with the luma model it would read as "no noise" and nothing would be removed.
  • Chroma depth is capped relative to the image size; past that point the filter starts flattening real colour gradients.
  • Runs on CPU or GPU (whatever device the tensor is on). ~1–3 s for 4K on CPU.
  • contribution = 0 or strength 0 is an exact pass-through.

Typical starting points: 30 / 50 / 100 / 50 / 100 for light grain, around 60 / 50 / 100 / 70 / 100 for heavy high-ISO noise. If detail goes mushy, raise luminance_detail before lowering luminance.

Image Resize (Lanczos 3 non-separable)

Downscale with a radial Lanczos-3 kernel: the kernel is evaluated on the 2D distance sqrt(dx² + dy²) rather than applied as separable passes along X then Y. The result is circularly symmetric and free of the diagonal artifacts the separable form introduces — this matches Affinity Photo's "Lanczos 3 (non-separable)" resampling.

| Input | Type | Default | Description | |---|---|---|---| | image | IMAGE | — | Input image | | max_width | INT | 1024 | Bounding width | | max_height | INT | 1024 | Bounding height |

Outputs: image, width, height.

The result fits inside the bounds preserving aspect ratio and never upscales — an image that already fits is passed through untouched.

Image Dimension Fitter

Given a reference width/height, returns the closest native resolution for the selected model, matched by aspect-ratio distance. Ties resolve to the smaller total-pixel candidate, so it never picks a larger resolution than needed.

| Input | Type | Default | Description | |---|---|---|---| | width | INT | 1024 | Reference width | | height | INT | 1024 | Reference height | | model | SD / Flux / Z-Turbo / Krea 2 | SD | Target model |

Outputs: target_width, target_height — feed them into Empty Latent Image or any node that takes dimensions.

Resolution tables:

  • SD (~262k px, /8): 512×512, 640×512, 512×640, 704×512, 512×704, 768×512, 512×768
  • Flux (~1MP, /32): 1024×1024, 1152×896, 1216×832, 1344×768, 1536×640, 1920×1080 (+ portrait counterparts)
  • Z-Turbo: same as Flux minus the 1920×1080 pair
  • Krea 2 (~1MP, /32): 1024×1024, 1152×864, 1248×832, 1344×768, 1120×896 (+ portrait counterparts)

Krea does not publish exact pixel dimensions per ratio, so the Krea 2 table is derived from its documented ratios targeting ~1MP, each side floored to a multiple of 32 — approximating downward on purpose.

Image Padding Calculator

Padding needed to centre an image inside a target size.

| Input | Type | Default | |---|---|---| | image | IMAGE | — | | target_width | INT | 1024 | | target_height | INT | 1024 |

Outputs: left, top, right, bottom. Odd leftovers go to the right/bottom side. An image already at or above the target gets zero padding on that axis.

Load Image with Caption

Mirrors the built-in Load Image (dropdown plus upload button) and additionally returns the sidecar .txt caption. See image + caption pairs.

| Input | Type | Description | |---|---|---| | image | combo | File from the input/ folder |

Outputs: image, mask, caption.

The node adds an "upload pair (.png + .txt)" button: multi-select the image and its .txt in the file dialog (Cmd/Ctrl+click) and both are uploaded at once. If ComfyUI renames the image because input/ already had that name, the .txt is renamed to match — otherwise the pair would silently desync.

Load Images with Captions from Dir

Batch equivalent of the above: replicates the Inspire pack's Load Image List From Dir (same inputs, same per-item list semantics, images at their original size) and adds the caption output.

| Input | Type | Default | Description | |---|---|---|---| | directory | STRING | — | Folder to scan (.jpg, .jpeg, .png, .webp) | | image_load_cap | INT (optional) | 0 | Stop after N images (0 = no limit) | | start_index | INT (optional) | 0 | Skip N files from the start of the sorted list | | load_always | BOOLEAN (optional) | false | Re-read the folder every run, defeating ComfyUI's cache | | sort_method | combo (optional) | None | Alphabetical / Numerical / Datetime, ASC or DESC |

Outputs: image, mask, caption, file_path — all lists (OUTPUT_IS_LIST), so downstream nodes execute once per item instead of receiving a batched tensor.

Raises if the directory is missing or contains no images; a missing .txt is not an error.

Save Image with Caption

Mirrors the built-in SaveImage and writes a matching UTF-8 .txt beside each PNG.

| Input | Type | Default | Description | |---|---|---|---| | images | IMAGE | — | Images to save | | caption | STRING (multiline) | "" | Caption text | | filename_prefix | STRING | ComfyUI | Supports %date:yyyy-MM-dd%, %Empty Latent Image.width%, etc. | | caption_in | STRING (optional, input-only) | — | Connected upstream string; overrides the widget when present |

Output node — no outputs. Uses ComfyUI's standard get_save_image_path prefix/counter logic and respects --disable-metadata.

Resolution Selector from Dimensions

Same math as ComfyUI's built-in Resolution Selector, but the aspect ratio is auto-detected from a reference (width, height) pair instead of picked from a combo.

| Input | Type | Default | Description | |---|---|---|---| | width | INT | 1024 | Reference width (only used to detect the ratio) | | height | INT | 1024 | Reference height | | megapixels | FLOAT 0.1–16 | 1.0 | Target total megapixels | | multiple | INT 8–128 | 8 | Snap the result to this multiple |

Outputs: width, height, aspect_ratio (label, also drawn on the node face).

Ratios recognised: 1:1, 2:3, 3:2, 3:4, 4:3, 9:16, 16:9, 21:9. Detection uses log-space distance so 2:3 and 3:2 are treated symmetrically — linear distance in w/h biases toward landscape.

Path Splitter

| Input | Type | |---|---| | path | STRING |

Outputs: directory, filename, stem.

Merge LoRAs to File

Bakes two LoRAs into a single .safetensors, reproducing exactly what applying both in sequence does (à la rgthree's Power Lora Loader).

| Input | Type | Default | Description | |---|---|---|---| | lora_name1 / lora_name2 | combo | — | LoRAs to combine | | strength1 / strength2 | FLOAT | 0.5 | Strength baked in for each | | filename_prefix | STRING | merged_lora | Output name, relative to the loras folder | | target_rank | INT (optional) | 0 | 0 = exact; a positive value recompresses via truncated SVD |

Outputs: path, retained_energy. Also an output node — the retained-energy percentage is displayed on the node body after execution.

Use the merged file at strength 1.0. The output alpha is set equal to the merged rank so the load-time scale is exactly 1.0.

How it works: because [Up_A | Up_B] @ [Down_A ; Down_B] == Up_A@Down_A + Up_B@Down_B, concatenating the up matrices along the rank dimension (with each LoRA's strength and alpha/rank scale folded into the up side) and the down matrices along rank yields the exact weighted sum of both deltas. No SVD, no approximation. Ranks may differ — 32 + 64 gives a merged rank of 96.

target_rank trades exactness for file size: a positive value recompresses the merged delta to that rank via truncated SVD (Eckart–Young optimal). The recompression works on the small factors, so it never materialises the full weight matrix and stays cheap on big layers. retained_energy reports the average retained Frobenius energy as a quality signal.

Supported: standard lora_up/lora_down (+ alpha) and the other up/down naming schemes ComfyUI recognises, plus direct .diff/.diff_b deltas. Refused: LoHa, LoKr, OFT, DoRA and LoCon-mid files — those aren't a plain up @ down product, so the node errors rather than emit a wrong result.

Random Line Concatenator

Picks one line at random from a toggleable pool and concatenates it between optional prefix/suffix strings.

| Input | Type | Default | Description | |---|---|---|---| | seed | INT | 0 | With the standard control-after-generate dropdown | | adjust_whitespace | BOOLEAN | true | Collapse whitespace runs and join parts with a single space | | start | STRING | "" | Prepended before the chosen line | | random_choice | STRING | [] | The options pool — edit through the rows in the node UI | | end | STRING | "" | Appended after the chosen line | | text_in | STRING (optional, input-only) | — | Upstream string, placed before everything else |

Output: text.

The pool is edited through a custom widget (checkbox + text + delete per row). Disabled rows and blank rows are skipped when picking. An empty pool yields an empty choice rather than an error.


Image + caption pairs

Several nodes here share one convention used for LoRA training data: an image (.png, .jpg, .jpeg, .webp) and a UTF-8 .txt caption with the same base name in the same directory:

dataset/
  imagem001.png
  imagem001.txt
  imagem002.png     <- no caption; loads as ""

Matching is by stem only, and the caption side is optional — a missing .txt is read as an empty string, never an error, because real training folders routinely contain unlabelled images.

Save Image with Caption writes these pairs; Load Image with Caption and Load Images with Captions from Dir read them back.

License

MIT