Rui Holdorf 51c027ded0 feat(resize_nodes): add allow_upscale toggle to Lanczos 3 resize
ImageResizeLanczos3NonSeparable passed the image through untouched
whenever it already fit inside max_width/max_height, so raising the
bounds never enlarged anything. The radial kernel already handles
enlargement correctly (sy/sx only widen to band-limit on downscale and
stay at 1.0 when growing), so this just gates the early return.

Default is false, keeping saved workflows on the previous behaviour.
2026-08-29 22:16:25 -03:00
2026-07-25 19:00:30 -03:00
2026-07-25 19:00:30 -03:00

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 resize, 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)

Resize 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
allow_upscale BOOLEAN false Also enlarge images smaller than the bounds

Outputs: image, width, height.

The result fits inside the bounds preserving aspect ratio. By default it never upscales — an image that already fits is passed through untouched. Enable allow_upscale to let it grow the image up to the bounds as well.

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

S
Description
No description provided
Readme
596 KiB
Languages
Python 77%
JavaScript 23%