Added ComparisonGate and added screenshots to the main README.
This commit is contained in:
@@ -7,32 +7,53 @@ PortraitUtils is a focused collection of ComfyUI nodes designed for pipelines us
|
||||
Each entry links to a short, task-oriented reference page (coming soon) for deeper usage notes and parameter breakdowns.
|
||||
|
||||
### Image Adjustments & Preparation
|
||||
- `AutoAdjustNode` – One-click levels, tone, and color balancing. [Docs](docs/AutoAdjustNode.md)
|
||||
- `AutoWBColorMatch` – White-balances and colour matches against a reference image using several algorithms. [Docs](docs/AutoWBColorMatch.md)
|
||||
- `AutoColorConfigNode` – Lightweight toggle bundle for sharing Auto Adjust presets. [Docs](docs/AutoColorConfigNode.md)
|
||||
- `FluxResolutionPrepare` – Crops and resizes to the nearest Flux-friendly resolution, with optional pre-upscale. [Docs](docs/FluxResolutionPrepare.md)
|
||||
- `ImageMegapixelSelector` – Chooses image sizes by target megapixels to match downstream model limits. [Docs](docs/ImageMegapixelSelector.md)
|
||||
- `AutoAdjustNode` - One-click levels, tone, and color balancing. [Docs](docs/AutoAdjustNode.md)
|
||||
<div align="center"><img src="docs/screenshots/auto_adjust_node.png" alt="AutoAdjustNode screenshot" width="500" /></div>
|
||||
- `AutoWBColorMatch` - White-balances and colour matches against a reference image using several algorithms. [Docs](docs/AutoWBColorMatch.md)
|
||||
<div align="center"><img src="docs/screenshots/auto_wb_color_match.png" alt="AutoWBColorMatch screenshot" width="300" /></div>
|
||||
- `AutoColorConfigNode` - Lightweight toggle bundle for sharing Auto Adjust presets. [Docs](docs/AutoColorConfigNode.md)
|
||||
<div align="center"><img src="docs/screenshots/auto_color_config_node.png" alt="AutoColorConfigNode screenshot" width="500" /></div>
|
||||
- `FluxResolutionPrepare` - Crops and resizes to the nearest Flux-friendly resolution, with optional pre-upscale. [Docs](docs/FluxResolutionPrepare.md)
|
||||
<div align="center"><img src="docs/screenshots/flux_resolution_prepare.png" alt="FluxResolutionPrepare screenshot" width="300" /></div>
|
||||
- `ImageMegapixelSelector` - Chooses image sizes by target megapixels to match downstream model limits. [Docs](docs/ImageMegapixelSelector.md)
|
||||
<div align="center"><img src="docs/screenshots/image_megapixel_selector.png" alt="ImageMegapixelSelector screenshot" width="300" /></div>
|
||||
|
||||
### Cropping, Framing, and Stitching
|
||||
- `AutoCropBorders` – Removes uniform borders using adaptive fuzzy detection. GPU acceleration is currently broken, so leave that off for now. [Docs](docs/AutoCropBorders.md)
|
||||
- `CropImageByMargins` – Manual margin-based cropper for RGB images. [Docs](docs/CropImageByMargins.md)
|
||||
- `CropMaskByMargins` – Margin cropper designed for MASK tensors. [Docs](docs/CropMaskByMargins.md)
|
||||
- `FitAspectHeadSafe` – Rescales and pads while preserving face-safe regions for portrait framing. [Docs](docs/FitAspectHeadSafe.md)
|
||||
- `MQBBoxMin` – Utility for merging bounding boxes while enforcing minimum sizes. [Docs](docs/MQBBoxMin.md)
|
||||
- `StitchByMask` – Composites foreground and background images using mask-driven blending. [Docs](docs/StitchByMask.md)
|
||||
- `AutoCropBorders` - Removes uniform borders using adaptive fuzzy detection. GPU acceleration is currently broken, so leave that off for now. [Docs](docs/AutoCropBorders.md)
|
||||
<div align="center"><img src="docs/screenshots/auto_crop_borders.png" alt="AutoCropBorders screenshot" width="300" /></div>
|
||||
- `CropImageByMargins` - Manual margin-based cropper for RGB images. [Docs](docs/CropImageByMargins.md)
|
||||
<div align="center"><img src="docs/screenshots/crop_image_by_margins.png" alt="CropImageByMargins screenshot" width="300" /></div>
|
||||
- `CropMaskByMargins` - Margin cropper designed for MASK tensors. [Docs](docs/CropMaskByMargins.md)
|
||||
<div align="center"><img src="docs/screenshots/crop_mask_by_margins.png" alt="CropMaskByMargins screenshot" width="300" /></div>
|
||||
- `FitAspectHeadSafe` - Rescales and pads while preserving face-safe regions for portrait framing. [Docs](docs/FitAspectHeadSafe.md)
|
||||
<div align="center"><img src="docs/screenshots/fit_aspect_head_safe.png" alt="FitAspectHeadSafe screenshot" width="500" /></div>
|
||||
- `MQBBoxMin` - Utility for merging bounding boxes while enforcing minimum sizes. [Docs](docs/MQBBoxMin.md)
|
||||
<div align="center"><img src="docs/screenshots/mq_bbox_min.png" alt="MQBBoxMin screenshot" width="500" /></div>
|
||||
- `StitchByMask` - Composites foreground and background images using mask-driven blending. [Docs](docs/StitchByMask.md)
|
||||
<div align="center"><img src="docs/screenshots/stitch_by_mask.png" alt="StitchByMask screenshot" width="300" /></div>
|
||||
|
||||
### Prompting & Workflow Configuration
|
||||
- `MultiPromptNode` – Cycles through prompt variants with per-slot weights. [Docs](docs/MultiPromptNode.md)
|
||||
- `WorkflowConfig` – Centralized key/value store for workflow-wide settings. [Docs](docs/WorkflowConfig.md)
|
||||
- `OutpaintConfigNode` – Packs outpainting preferences (steps, padding, sampler choices). [Docs](docs/OutpaintConfigNode.md)
|
||||
- `OutpaintPaddingComputeNode` – Calculates padding offsets to feed outpaint chains as either percentage or absolute pixels, with gravity. [Docs](docs/OutpaintPaddingComputeNode.md)
|
||||
- `MultiPromptNode` - Cycles through prompt variants with per-slot weights. [Docs](docs/MultiPromptNode.md)
|
||||
<div align="center"><img src="docs/screenshots/multi_prompt_node.png" alt="MultiPromptNode screenshot" width="300" /></div>
|
||||
- `WorkflowConfig` - Centralized key/value store for workflow-wide settings. [Docs](docs/WorkflowConfig.md)
|
||||
<div align="center"><img src="docs/screenshots/workflow_config.png" alt="WorkflowConfig screenshot" width="300" /></div>
|
||||
- `OutpaintConfigNode` - Packs outpainting preferences (steps, padding, sampler choices). [Docs](docs/OutpaintConfigNode.md)
|
||||
<div align="center"><img src="docs/screenshots/outpaint_config_node.png" alt="OutpaintConfigNode screenshot" width="500" /></div>
|
||||
- `OutpaintPaddingComputeNode` - Calculates padding offsets to feed outpaint chains as either percentage or absolute pixels, with gravity. [Docs](docs/OutpaintPaddingComputeNode.md)
|
||||
<div align="center"><img src="docs/screenshots/outpaint_padding_compute_node.png" alt="OutpaintPaddingComputeNode screenshot" width="500" /></div>
|
||||
|
||||
### I/O & Utility Helpers
|
||||
- `LoadImageCombined` – Hybrid single/batch image loader with auto-advance support. [Docs](docs/LoadImageCombined.md)
|
||||
- `PortraitUtils_PairedImageLoader` – Loads synchronized source/output pairs for A/B comparison workflows. [Docs](docs/PortraitUtils_PairedImageLoader.md)
|
||||
- `FilenameAppendSuffix` – Adds suffixes to filenames while respecting multi-extension cases. [Docs](docs/FilenameAppendSuffix.md)
|
||||
- `GpuSyncCacheClear` – Forces a CUDA sync and empties selected caches to avoid VRAM buildup. [Docs](docs/GpuSyncCacheClear.md)
|
||||
- `SeedVR2Prep` – Convenience helper for SeedVR2/VRAM-heavy pipelines (cache priming & cleanup). [Docs](docs/SeedVR2Prep.md)
|
||||
- `LoadImageCombined` - Hybrid single/batch image loader with auto-advance support. [Docs](docs/LoadImageCombined.md)
|
||||
<div align="center"><img src="docs/screenshots/load_image_combined.png" alt="LoadImageCombined screenshot" width="200" /></div>
|
||||
- `PairedImageLoader` - Loads synchronized source/output pairs for A/B comparison workflows. [Docs](docs/PairedImageLoader.md)
|
||||
<div align="center"><img src="docs/screenshots/portraitutils_paired_image_loader.png" alt="PairedImageLoader screenshot" width="300" /></div>
|
||||
- `ComparisonGate` - Gates comparison outputs until two populated images are available. [Docs](docs/ComparisonGate.md)
|
||||
- `FilenameAppendSuffix` - Adds suffixes to filenames while respecting multi-extension cases. [Docs](docs/FilenameAppendSuffix.md)
|
||||
<div align="center"><img src="docs/screenshots/filename_append_suffix.png" alt="FilenameAppendSuffix screenshot" width="300" /></div>
|
||||
- `GpuSyncCacheClear` - Forces a CUDA sync and empties selected caches to avoid VRAM buildup. [Docs](docs/GpuSyncCacheClear.md)
|
||||
<div align="center"><img src="docs/screenshots/gpu_sync_cache_clear.png" alt="GpuSyncCacheClear screenshot" width="300" /></div>
|
||||
- `SeedVR2Prep` - Convenience helper for SeedVR2/VRAM-heavy pipelines (cache priming & cleanup). [Docs](docs/SeedVR2Prep.md)
|
||||
<div align="center"><img src="docs/screenshots/seedvr2_prep.png" alt="SeedVR2Prep screenshot" width="300" /></div>
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -58,6 +58,10 @@ from .paired_image_loader import (
|
||||
NODE_DISPLAY_NAME_MAPPINGS as PAIRED_LOADER_DISPLAY_MAP,
|
||||
)
|
||||
from .load_image_combined import LoadImageCombined
|
||||
from .comparison_gate import (
|
||||
NODE_CLASS_MAPPINGS as COMPARISON_GATE_CLASS_MAP,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as COMPARISON_GATE_DISPLAY_MAP,
|
||||
)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
@@ -77,6 +81,7 @@ NODE_CLASS_MAPPINGS.update(FLUX_PREP_CLASS_MAP)
|
||||
NODE_CLASS_MAPPINGS.update(WORKFLOW_CONFIG_CLASS_MAP)
|
||||
NODE_CLASS_MAPPINGS.update(SMART_CROP_CLASS_MAP)
|
||||
NODE_CLASS_MAPPINGS.update(PAIRED_LOADER_CLASS_MAP)
|
||||
NODE_CLASS_MAPPINGS.update(COMPARISON_GATE_CLASS_MAP)
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(AUTO_ADJUST_DISPLAY_MAP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(OUTPAINT_DISPLAY_MAP)
|
||||
@@ -91,6 +96,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update(FLUX_PREP_DISPLAY_MAP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(WORKFLOW_CONFIG_DISPLAY_MAP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(SMART_CROP_DISPLAY_MAP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(PAIRED_LOADER_DISPLAY_MAP)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(COMPARISON_GATE_DISPLAY_MAP)
|
||||
|
||||
# Register the load-image node which does not expose pre-built mappings.
|
||||
NODE_CLASS_MAPPINGS["LoadImageCombined"] = LoadImageCombined
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Gate a source image and up to three comparison inputs before forwarding them.
|
||||
|
||||
ComfyUI passes ``None`` through empty sockets. This node inspects every input,
|
||||
selects an appropriate pair of populated images, and only emits them when both
|
||||
slots are ready. Otherwise, it mirrors the behaviour of an unplugged viewer
|
||||
socket by returning ``None`` for both outputs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, List, Tuple, Any
|
||||
|
||||
|
||||
def _collect_present(values: Iterable[Any]) -> List[Any]:
|
||||
"""
|
||||
Return the sequence of values that look like real images.
|
||||
|
||||
ComfyUI represents disconnected sockets as ``None`` and occasionally wraps
|
||||
outputs in small containers. We filter out anything that is clearly empty so
|
||||
the gate never forwards placeholders into downstream nodes.
|
||||
"""
|
||||
present: List[Any] = []
|
||||
for value in values:
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, (list, tuple)):
|
||||
if not value:
|
||||
continue
|
||||
if all(item is None for item in value):
|
||||
continue
|
||||
present.append(value)
|
||||
return present
|
||||
|
||||
|
||||
class ComparisonGate:
|
||||
"""
|
||||
Emit two images only when at least two inputs are populated.
|
||||
|
||||
Designed for comparison viewers: the first populated comparison becomes the
|
||||
"final image", while the designated source socket feeds the secondary output
|
||||
(with a fallback to the next comparison if the source is missing). If fewer
|
||||
than two images are available the node returns ``None`` for both sockets, so
|
||||
downstream widgets remain blank.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# Mark every socket as optional so that workflows can omit connections
|
||||
# without triggering validation errors. ComfyUI fills unspecified inputs
|
||||
# with ``None`` which this node explicitly understands.
|
||||
return {
|
||||
"optional": {
|
||||
"source_image": ("IMAGE", {"default": None}),
|
||||
"image_a": ("IMAGE", {"default": None}),
|
||||
"image_b": ("IMAGE", {"default": None}),
|
||||
"image_c": ("IMAGE", {"default": None}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE")
|
||||
RETURN_NAMES = ("final_image", "source_image")
|
||||
FUNCTION = "forward_images"
|
||||
CATEGORY = "PortraitUtils"
|
||||
|
||||
def forward_images(
|
||||
self,
|
||||
source_image=None,
|
||||
image_a=None,
|
||||
image_b=None,
|
||||
image_c=None,
|
||||
) -> Tuple[Any, Any]:
|
||||
# Gather the inputs in priority order to ensure predictable selection.
|
||||
total_inputs = (source_image, image_a, image_b, image_c)
|
||||
populated = _collect_present(total_inputs)
|
||||
if len(populated) < 2:
|
||||
return None, None
|
||||
|
||||
# The first comparison with data becomes the final output. The source
|
||||
# socket remains reserved for the secondary output to keep viewer labels
|
||||
# aligned with the user's expectations.
|
||||
candidate_finals = _collect_present((image_a, image_b, image_c))
|
||||
final_image = candidate_finals[0] if candidate_finals else None
|
||||
source_output = source_image
|
||||
|
||||
# If the source socket is empty but multiple comparisons are available,
|
||||
# fall back to the next comparison so we still deliver two images.
|
||||
if source_output is None and len(candidate_finals) > 1:
|
||||
source_output = candidate_finals[1]
|
||||
|
||||
# Avoid emitting half-populated pairs; downstream nodes expect either a
|
||||
# full tuple of tensors or pure ``None`` values.
|
||||
if final_image is None or source_output is None:
|
||||
return None, None
|
||||
|
||||
return final_image, source_output
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ComparisonGate": ComparisonGate,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ComparisonGate": "Comparison Gate",
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
# AutoAdjustNode
|
||||
<div align="center"><img src="screenshots/auto_adjust_node.png" alt="Screenshot" width="500" /></div>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# AutoColorConfigNode
|
||||
<div align="center"><img src="screenshots/auto_color_config_node.png" alt="Screenshot" width="500" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# AutoCropBorders
|
||||
<div align="center"><img src="screenshots/auto_crop_borders.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# AutoWBColorMatch
|
||||
<div align="center"><img src="screenshots/auto_wb_color_match.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# ComparisonGate
|
||||
|
||||
## Overview
|
||||
`ComparisonGate` keeps comparison viewers tidy by only emitting images when a complete pair is available. It listens to a designated `source_image` input together with up to three comparison sockets and mirrors ComfyUI's unplugged behaviour (`None`, `None`) until two populated tensors are ready to forward.
|
||||
|
||||
## Inputs
|
||||
- `source_image` (`IMAGE`, optional): Preferred reference image for the secondary output. Leave disconnected when you want the first populated comparison to feed both sockets.
|
||||
- `image_a` (`IMAGE`, optional): Primary comparison feed. Becomes the `final_image` output whenever populated.
|
||||
- `image_b` (`IMAGE`, optional): Secondary comparison feed used either as a fallback `final_image` or to backfill the `source_image` output when the dedicated source socket is empty.
|
||||
- `image_c` (`IMAGE`, optional): Tertiary comparison feed that participates in the fallback cascade.
|
||||
|
||||
All inputs accept `None`; the node filters out empty lists/tuples as well as bare `None` values so placeholder widgets will not trigger accidental forwards.
|
||||
|
||||
## Outputs
|
||||
- `final_image` (`IMAGE`): The first populated comparison input (`image_a` › `image_b` › `image_c`). Returns `None` until at least one comparison produces data.
|
||||
- `source_image` (`IMAGE`): Mirrors the dedicated `source_image` socket, or falls back to the next populated comparison input so the viewer always receives a pair. Returns `None` alongside `final_image` when fewer than two valid images are present.
|
||||
|
||||
## Forwarding Rules
|
||||
- The node inspects every input on each execution and counts how many look like real image tensors. If fewer than two are found it emits `(None, None)` to keep downstream nodes blank.
|
||||
- When only comparison sockets are populated, the first comparison feeds `final_image` and the second feeds `source_image`, ensuring A/B layouts remain synchronised.
|
||||
- When `source_image` is connected, it always feeds the `source_image` output unless it is empty—in that case the node automatically falls back to the second non-empty comparison input.
|
||||
- Containers such as one-item tuples or lists are unwrapped only when they contain real data; sequences of `None` values are ignored.
|
||||
|
||||
## Tips
|
||||
- Place the gate directly before two-input comparison viewers to prevent half-populated renders during workflow warm-up.
|
||||
- Combine with `PairedImageLoader` for workflow QA: let the loader drive comparisons and feed its outputs through the gate so the viewer only refreshes when both sockets advance.
|
||||
- Because the node simply relays its inputs, it adds no meaningful processing cost; you can safely sprinkle it anywhere you need to guarantee paired images.*** End Patch
|
||||
@@ -1,5 +1,4 @@
|
||||
# CropImageByMargins
|
||||
<div align="center"><img src="screenshots/crop_image_by_margins.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# CropMaskByMargins
|
||||
<div align="center"><img src="screenshots/crop_mask_by_margins.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# FilenameAppendSuffix
|
||||
<div align="center"><img src="screenshots/filename_append_suffix.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
@@ -22,5 +21,5 @@
|
||||
- Directory handling uses `os.path.join`, so platform-specific separators are respected.
|
||||
|
||||
## Tips
|
||||
- Chain this node after `LoadImageCombined` or `PortraitUtils_PairedImageLoader` to build save paths that mirror the input file names.
|
||||
- Chain this node after `LoadImageCombined` or `PairedImageLoader` to build save paths that mirror the input file names.
|
||||
- Disable `preserve_directory` when you want to relocate files into a new output folder without carrying over their original path.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# FitAspectHeadSafe
|
||||
<div align="center"><img src="screenshots/fit_aspect_head_safe.png" alt="Screenshot" width="500" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# FluxResolutionPrepare
|
||||
<div align="center"><img src="screenshots/flux_resolution_prepare.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# GpuSyncCacheClear
|
||||
<div align="center"><img src="screenshots/gpu_sync_cache_clear.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# ImageMegapixelSelector
|
||||
<div align="center"><img src="screenshots/image_megapixel_selector.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# LoadImageCombined
|
||||
<div align="center"><img src="screenshots/load_image_combined.png" alt="Screenshot" width="200" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
@@ -26,6 +25,6 @@
|
||||
- All images are converted to RGB using Pillow, with Exif orientation applied.
|
||||
|
||||
## Tips
|
||||
- Chain this node with `PortraitUtils_PairedImageLoader` or comparison nodes to quickly inspect before/after renders.
|
||||
- Chain this node with `PairedImageLoader` or comparison nodes to quickly inspect before/after renders.
|
||||
- Use `repeat_last` when you need to re-run the same batch frame after adjusting downstream parameters.
|
||||
- Combine with `FilenameAppendSuffix` to build descriptive output paths based on `filename_no_ext`.*** End Patch
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# MQBBoxMin
|
||||
<div align="center"><img src="screenshots/mq_bbox_min.png" alt="Screenshot" width="500" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# MultiPromptNode
|
||||
<div align="center"><img src="screenshots/multi_prompt_node.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# OutpaintConfigNode
|
||||
<div align="center"><img src="screenshots/outpaint_config_node.png" alt="Screenshot" width="500" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# OutpaintPaddingComputeNode
|
||||
<div align="center"><img src="screenshots/outpaint_padding_compute_node.png" alt="Screenshot" width="500" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# PortraitUtils_PairedImageLoader
|
||||
<div align="center"><img src="screenshots/portraitutils_paired_image_loader.png" alt="Screenshot" width="300" /></div>
|
||||
# PairedImageLoader
|
||||
|
||||
|
||||
## Overview
|
||||
`PortraitUtils_PairedImageLoader` pairs images from two directories (typically “source” and “output”) and emits the next matching duo on every run. It keeps its position per node instance, supports forward/backward stepping, and reports the active filename for UI display or logging.
|
||||
`PairedImageLoader` pairs images from two directories (typically "source" and "output") and emits the next matching duo on every run. It keeps its position per node instance, supports forward/backward stepping, and reports the active filename for UI display or logging.
|
||||
|
||||
## Inputs
|
||||
- `source_dir` (`STRING`): Directory containing the reference/source images. Relative paths resolve against the current working directory.
|
||||
@@ -1,5 +1,4 @@
|
||||
# SeedVR2Prep
|
||||
<div align="center"><img src="screenshots/seedvr2_prep.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# StitchByMask
|
||||
<div align="center"><img src="screenshots/stitch_by_mask.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# WorkflowConfig
|
||||
<div align="center"><img src="screenshots/workflow_config.png" alt="Screenshot" width="300" /></div>
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
import math
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
def _to_numpy_mask(m):
|
||||
if torch is not None and isinstance(m, torch.Tensor):
|
||||
arr = m.detach().cpu().numpy()
|
||||
elif isinstance(m, dict) and "mask" in m:
|
||||
v = m["mask"]
|
||||
arr = v.detach().cpu().numpy() if torch is not None and isinstance(v, torch.Tensor) else np.asarray(v)
|
||||
else:
|
||||
arr = np.asarray(m)
|
||||
arr = np.squeeze(arr)
|
||||
if arr.ndim == 3:
|
||||
arr = arr.max(axis=0) if arr.shape[0] in (1, 3, 4) else arr.max(axis=-1)
|
||||
if arr.ndim != 2:
|
||||
h, w = arr.shape[-2], arr.shape[-1]
|
||||
arr = arr.reshape(h, w)
|
||||
return arr.astype(np.float32, copy=False)
|
||||
|
||||
|
||||
def _normalize01(m):
|
||||
mmax = float(m.max()) if m.size else 0.0
|
||||
if mmax <= 0:
|
||||
return m
|
||||
if mmax > 1.5:
|
||||
return (m / 255.0).astype(np.float32, copy=False)
|
||||
return m
|
||||
|
||||
|
||||
def _border_mass_ratio(m, border_px=8):
|
||||
H, W = m.shape
|
||||
border_px = int(max(1, min(border_px, min(H, W) // 4)))
|
||||
total = float(m.sum()) + 1e-12
|
||||
top = m[:border_px, :].sum()
|
||||
bottom = m[-border_px:, :].sum()
|
||||
left = m[:, :border_px].sum()
|
||||
right = m[:, -border_px:].sum()
|
||||
corners = (
|
||||
m[:border_px, :border_px].sum()
|
||||
+ m[:border_px, -border_px:].sum()
|
||||
+ m[-border_px:, :border_px].sum()
|
||||
+ m[-border_px:, -border_px:].sum()
|
||||
)
|
||||
border = top + bottom + left + right - corners
|
||||
return float(border) / total
|
||||
|
||||
|
||||
def _auto_pick_foreground(m, mode="auto"):
|
||||
m = _normalize01(m)
|
||||
if mode == "false":
|
||||
fg, pick = m, "fg"
|
||||
elif mode == "true":
|
||||
fg, pick = 1.0 - m, "bg"
|
||||
else:
|
||||
r_fg = _border_mass_ratio(m)
|
||||
r_bg = _border_mass_ratio(1.0 - m)
|
||||
if r_bg < r_fg:
|
||||
fg, pick = 1.0 - m, "auto->bg"
|
||||
else:
|
||||
fg, pick = m, "auto->fg"
|
||||
return fg, pick
|
||||
|
||||
|
||||
def _quantile_bounds(mass, qL, qR, qT, qB, min_span):
|
||||
H, W = mass.shape
|
||||
eps = 1e-8
|
||||
c = mass.sum(axis=0) + eps
|
||||
r = mass.sum(axis=1) + eps
|
||||
cc = np.cumsum(c)
|
||||
rc = np.cumsum(r)
|
||||
ct = float(cc[-1])
|
||||
rt = float(rc[-1])
|
||||
|
||||
def qidx(cum, total, q):
|
||||
return int(np.clip(np.searchsorted(cum, total * float(q), "left"), 0, len(cum) - 1))
|
||||
|
||||
x0 = qidx(cc, ct, qL)
|
||||
x1 = qidx(cc, ct, qR)
|
||||
y0 = qidx(rc, rt, qT)
|
||||
y1 = qidx(rc, rt, qB)
|
||||
if x1 < x0:
|
||||
x0, x1 = x1, x0
|
||||
if y1 < y0:
|
||||
y0, y1 = y1, y0
|
||||
if (x1 - x0 + 1) < min_span:
|
||||
cx = (x0 + x1) // 2
|
||||
half = max(1, min_span // 2)
|
||||
x0 = max(0, cx - half)
|
||||
x1 = min(W - 1, cx + half)
|
||||
if (y1 - y0 + 1) < min_span:
|
||||
cy = (y0 + y1) // 2
|
||||
half = max(1, min_span // 2)
|
||||
y0 = max(0, cy - half)
|
||||
y1 = min(H - 1, cy + half)
|
||||
return int(x0), int(y0), int(x1 - x0 + 1), int(y1 - y0 + 1)
|
||||
|
||||
|
||||
def _pad_rect(x, y, w, h, pad, W, H):
|
||||
x2 = max(0, x - pad)
|
||||
y2 = max(0, y - pad)
|
||||
w2 = min(W - x2, w + 2 * pad)
|
||||
h2 = min(H - y2, h + 2 * pad)
|
||||
return int(x2), int(y2), int(w2), int(h2)
|
||||
|
||||
|
||||
def _container_for_ar(x, y, w, h, R, W, H):
|
||||
"""
|
||||
Smallest rectangle of aspect R (=w/h) that contains (x,y,w,h), centered,
|
||||
then clamped to image. If it can't fit (too large), return None.
|
||||
"""
|
||||
wA = max(w, int(np.ceil(h * R)))
|
||||
hA = int(np.ceil(wA / R))
|
||||
hB = max(h, int(np.ceil(w / R)))
|
||||
wB = int(np.ceil(hB * R))
|
||||
cand = []
|
||||
if wA <= W and hA <= H:
|
||||
cand.append((wA, hA))
|
||||
if wB <= W and hB <= H:
|
||||
cand.append((wB, hB))
|
||||
if not cand:
|
||||
return None
|
||||
cw, ch = min(cand, key=lambda t: t[0] * t[1])
|
||||
|
||||
cx = x + w / 2.0
|
||||
cy = y + h / 2.0
|
||||
X = int(np.round(cx - cw / 2.0))
|
||||
Y = int(np.round(cy - ch / 2.0))
|
||||
X = max(0, min(W - cw, X))
|
||||
Y = max(0, min(H - ch, Y))
|
||||
return X, Y, int(cw), int(ch)
|
||||
|
||||
|
||||
class MQBBoxMin:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mask": ("MASK",),
|
||||
"invert_mask": (["auto", "false", "true"], {"default": "auto"}),
|
||||
"q_left": ("FLOAT", {"default": 0.005, "min": 0.0, "max": 0.2, "step": 0.001}),
|
||||
"q_right": ("FLOAT", {"default": 0.995, "min": 0.8, "max": 1.0, "step": 0.001}),
|
||||
"q_top": ("FLOAT", {"default": 0.005, "min": 0.0, "max": 0.2, "step": 0.001}),
|
||||
"q_bottom": ("FLOAT", {"default": 0.995, "min": 0.8, "max": 1.0, "step": 0.001}),
|
||||
"min_span_px": ("INT", {"default": 8, "min": 1, "max": 2048}),
|
||||
"tight_crop": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": False,
|
||||
"tooltip": "ON: tight subject bbox with smart AR; OFF: full frame (minimal AR crop downstream).",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT", "INT", "INT", "STRING")
|
||||
RETURN_NAMES = ("x", "y", "w", "h", "debug")
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "utils/debug"
|
||||
|
||||
def run(self, mask, invert_mask, q_left, q_right, q_top, q_bottom, min_span_px, tight_crop=True):
|
||||
m = _to_numpy_mask(mask)
|
||||
H, W = m.shape
|
||||
|
||||
if not tight_crop:
|
||||
dbg = f"tight=False full=({0},{0},{W},{H})"
|
||||
return (0, 0, int(W), int(H), dbg)
|
||||
|
||||
# --- original tight bbox ---
|
||||
fg, picked = _auto_pick_foreground(m, invert_mask)
|
||||
x, y, w, h = _quantile_bounds(fg, q_left, q_right, q_top, q_bottom, int(min_span_px))
|
||||
|
||||
# --- smart AR (minimal-area container), internal only ---
|
||||
pad_px = max(8, int(round(0.02 * min(H, W))))
|
||||
sx, sy, sw, sh = _pad_rect(x, y, w, h, pad_px, W, H)
|
||||
|
||||
# Baseline: container using current image AR
|
||||
R0 = float(W) / max(1.0, float(H))
|
||||
base = _container_for_ar(sx, sy, sw, sh, R0, W, H)
|
||||
if base is None:
|
||||
dbg = f"tight=True pick={picked} mq=({x},{y},{w},{h})"
|
||||
return (x, y, w, h, dbg)
|
||||
bx, by, bw, bh = base
|
||||
base_area = bw * bh
|
||||
|
||||
# Try common ARs in BOTH orientations (includes 9:16)
|
||||
ratios = [
|
||||
(1, 1),
|
||||
(2, 3),
|
||||
(3, 2),
|
||||
(3, 4),
|
||||
(4, 3),
|
||||
(9, 16),
|
||||
(16, 9),
|
||||
]
|
||||
|
||||
best_rect = base
|
||||
best_area = base_area
|
||||
best_info = f"AR={W}:{H}"
|
||||
|
||||
for aw, ah in ratios:
|
||||
R = float(aw) / float(ah)
|
||||
cand = _container_for_ar(sx, sy, sw, sh, R, W, H)
|
||||
if cand is None:
|
||||
continue
|
||||
cx, cy, cw, ch = cand
|
||||
area = cw * ch
|
||||
if area < best_area:
|
||||
best_area = area
|
||||
best_rect = cand
|
||||
best_info = f"AR={aw}:{ah}"
|
||||
|
||||
rx, ry, rw, rh = best_rect
|
||||
dbg = f"tight=True pick={picked} mq=({x},{y},{w},{h}) -> smart({rx},{ry},{rw},{rh}) {best_info}, pad={pad_px}"
|
||||
return (int(rx), int(ry), int(rw), int(rh), dbg)
|
||||
|
||||
|
||||
def _get_hw(image):
|
||||
t = image["image"] if isinstance(image, dict) and "image" in image else image
|
||||
s = tuple(getattr(t, "shape", np.asarray(t).shape))
|
||||
if len(s) == 4:
|
||||
H, W = (s[1], s[2]) if s[-1] in (1, 3, 4) else (s[2], s[3])
|
||||
elif len(s) == 3:
|
||||
H, W = (s[0], s[1]) if s[0] not in (1, 3, 4) else (s[1], s[2])
|
||||
else:
|
||||
H, W = s[-2], s[-1]
|
||||
return int(H), int(W)
|
||||
|
||||
|
||||
def _parse_aspects(s):
|
||||
out = []
|
||||
for p in s.split(","):
|
||||
p = p.strip()
|
||||
if not p:
|
||||
continue
|
||||
if ":" in p:
|
||||
a, b = p.split(":", 1)
|
||||
else:
|
||||
a, b = p.replace("X", "x").split("x", 1)
|
||||
try:
|
||||
a = float(a)
|
||||
b = float(b)
|
||||
if a > 0 and b > 0:
|
||||
out.append((a / b, f"{int(a)}:{int(b)}"))
|
||||
except Exception:
|
||||
pass
|
||||
return out or [(2 / 3.0, "2:3")]
|
||||
|
||||
|
||||
def _closest_ratio(target_ratio, candidates):
|
||||
tr = float(target_ratio)
|
||||
best = None
|
||||
for r, tag in candidates:
|
||||
d = abs(math.log(r) - math.log(tr))
|
||||
if best is None or d < best[0]:
|
||||
best = (d, r, tag)
|
||||
return best[1], best[2]
|
||||
|
||||
|
||||
def _cover_min_rect(x0, y0, w0, h0, ratio):
|
||||
"""
|
||||
Smallest rectangle of aspect 'ratio' that fully covers the rect (x0,y0,w0,h0).
|
||||
Returns floats (xa,ya,wa,ha) without clamping to image.
|
||||
"""
|
||||
if w0 <= 0 or h0 <= 0:
|
||||
return x0, y0, w0, h0
|
||||
r0 = w0 / max(1.0, h0)
|
||||
if r0 >= ratio:
|
||||
wa = w0
|
||||
ha = w0 / ratio
|
||||
else:
|
||||
ha = h0
|
||||
wa = h0 * ratio
|
||||
cx = x0 + w0 / 2.0
|
||||
cy = y0 + h0 / 2.0
|
||||
return cx - wa / 2.0, cy - ha / 2.0, wa, ha
|
||||
|
||||
|
||||
class FitAspectHeadSafe:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"x": ("INT",),
|
||||
"y": ("INT",),
|
||||
"w": ("INT",),
|
||||
"h": ("INT",),
|
||||
"aspects_csv": (
|
||||
"STRING",
|
||||
{"default": "2:3,3:4,1:1,9:16,16:9,5:8,8:5"},
|
||||
),
|
||||
"match_to": (["image", "mq_box"], {"default": "mq_box"}),
|
||||
"headroom_ratio": ("FLOAT", {"default": 0.12, "min": 0.0, "max": 0.5, "step": 0.01}),
|
||||
"footroom_ratio": ("FLOAT", {"default": 0.06, "min": 0.0, "max": 0.5, "step": 0.01}),
|
||||
"side_margin_ratio": ("FLOAT", {"default": 0.08, "min": 0.0, "max": 0.5, "step": 0.01}),
|
||||
"bottom_priority": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
"horiz_gravity": (["center", "left", "right"], {"default": "center"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT", "INT", "INT", "STRING", "STRING")
|
||||
RETURN_NAMES = ("w", "h", "x", "y", "aspect_used", "debug")
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "utils/geometry"
|
||||
|
||||
def run(
|
||||
self,
|
||||
image,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
aspects_csv,
|
||||
match_to,
|
||||
headroom_ratio,
|
||||
footroom_ratio,
|
||||
side_margin_ratio,
|
||||
bottom_priority,
|
||||
horiz_gravity,
|
||||
):
|
||||
|
||||
H, W = _get_hw(image)
|
||||
img_ratio = W / max(1.0, H)
|
||||
mq_ratio = w / max(1.0, h)
|
||||
target_ratio = img_ratio if match_to == "image" else mq_ratio
|
||||
|
||||
aspects = _parse_aspects(aspects_csv)
|
||||
ratio, tag = _closest_ratio(target_ratio, aspects)
|
||||
|
||||
req_x0 = x
|
||||
req_y0 = y
|
||||
req_x1 = x + w
|
||||
req_y1 = y + h
|
||||
|
||||
guess_h = H
|
||||
head_px = headroom_ratio * guess_h
|
||||
foot_px = footroom_ratio * guess_h
|
||||
side_px = side_margin_ratio * (guess_h * ratio)
|
||||
|
||||
req_x0p = req_x0 - side_px
|
||||
req_x1p = req_x1 + side_px
|
||||
req_y0p = req_y0 - head_px
|
||||
req_y1p = req_y1 + foot_px
|
||||
|
||||
xa, ya, wa, ha = _cover_min_rect(
|
||||
req_x0p,
|
||||
req_y0p,
|
||||
max(1.0, req_x1p - req_x0p),
|
||||
max(1.0, req_y1p - req_y0p),
|
||||
ratio,
|
||||
)
|
||||
|
||||
head_px = headroom_ratio * ha
|
||||
foot_px = footroom_ratio * ha
|
||||
side_px = side_margin_ratio * wa
|
||||
req_x0p = req_x0 - side_px
|
||||
req_x1p = req_x1 + side_px
|
||||
req_y0p = req_y0 - head_px
|
||||
req_y1p = req_y1 + foot_px
|
||||
xa, ya, wa, ha = _cover_min_rect(
|
||||
req_x0p,
|
||||
req_y0p,
|
||||
max(1.0, req_x1p - req_x0p),
|
||||
max(1.0, req_y1p - req_y0p),
|
||||
ratio,
|
||||
)
|
||||
|
||||
if horiz_gravity == "left":
|
||||
xa = 0.0
|
||||
elif horiz_gravity == "right":
|
||||
xa = W - wa
|
||||
else:
|
||||
cx = x + w / 2.0
|
||||
xa = cx - wa / 2.0
|
||||
xa = max(0.0, min(xa, W - wa))
|
||||
|
||||
y_head = y
|
||||
y_feet = y + h
|
||||
y_top_des = y_head - head_px
|
||||
y_bot_des = y_feet + foot_px
|
||||
ya = y_top_des
|
||||
ya = max(0.0, min(ya, H - ha))
|
||||
|
||||
def violations(Y):
|
||||
top_gap = (Y) - (y_head - head_px)
|
||||
bot_gap = (Y + ha) - (y_feet + foot_px)
|
||||
loss_top = max(0.0, -top_gap)
|
||||
loss_bot = max(0.0, -bot_gap)
|
||||
return loss_top, loss_bot, (1.0 - bottom_priority) * loss_top + bottom_priority * loss_bot
|
||||
|
||||
candidates = [
|
||||
max(0.0, min(y_top_des, H - ha)),
|
||||
max(0.0, min(y_bot_des - ha, H - ha)),
|
||||
0.0,
|
||||
float(H - ha),
|
||||
]
|
||||
best = None
|
||||
for Yc in candidates:
|
||||
lt, lb, cost = violations(Yc)
|
||||
if best is None or cost < best[0]:
|
||||
best = (cost, Yc, lt, lb)
|
||||
_, ya, lt, lb = best
|
||||
ya = max(0.0, min(ya, H - ha))
|
||||
|
||||
X = int(round(xa))
|
||||
Y = int(round(ya))
|
||||
Wc = int(round(wa))
|
||||
Hc = int(round(ha))
|
||||
X = max(0, min(X, W - 1))
|
||||
Y = max(0, min(Y, H - 1))
|
||||
Wc = max(1, min(Wc, W - X))
|
||||
Hc = max(1, min(Hc, H - Y))
|
||||
|
||||
dbg = (
|
||||
f"match_to={match_to}, chosen={tag}({ratio:.4f}), "
|
||||
f"img={W}x{H}, mq=({x},{y},{w},{h}), "
|
||||
f"margins(px): head={head_px:.1f}, foot={foot_px:.1f}, side≈{side_px:.1f}, "
|
||||
f"viol: top={lt:.1f}, bot={lb:.1f}; final=({X},{Y},{Wc},{Hc})"
|
||||
)
|
||||
return (Wc, Hc, X, Y, tag, dbg)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"MQBBoxMin": MQBBoxMin,
|
||||
"FitAspectHeadSafe": FitAspectHeadSafe,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"MQBBoxMin": "MQ BBox (MIN)",
|
||||
"FitAspectHeadSafe": "Fit Aspect (Head-Safe) - Closest AR + Tight Cover",
|
||||
}
|
||||
+13
-13
@@ -93,7 +93,7 @@ def _coerce_str(value) -> str:
|
||||
return str(value)
|
||||
|
||||
|
||||
class PortraitUtils_PairedImageLoader:
|
||||
class PairedImageLoader:
|
||||
CATEGORY = "PortraitUtils"
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE", "STRING")
|
||||
FUNCTION = "load_next_pair"
|
||||
@@ -161,20 +161,20 @@ class PortraitUtils_PairedImageLoader:
|
||||
self._state_key = state_key
|
||||
|
||||
if not source_dir:
|
||||
raise ValueError("PortraitUtils_PairedImageLoader: source_dir is required.")
|
||||
raise ValueError("PairedImageLoader: source_dir is required.")
|
||||
if not output_dir:
|
||||
raise ValueError("PortraitUtils_PairedImageLoader: output_dir is required.")
|
||||
raise ValueError("PairedImageLoader: output_dir is required.")
|
||||
|
||||
src_path = _resolve_directory(source_dir)
|
||||
out_path = _resolve_directory(output_dir)
|
||||
|
||||
if not src_path.exists() or not src_path.is_dir():
|
||||
raise ValueError(
|
||||
f"PortraitUtils_PairedImageLoader: source_dir not found: {src_path}"
|
||||
f"PairedImageLoader: source_dir not found: {src_path}"
|
||||
)
|
||||
if not out_path.exists() or not out_path.is_dir():
|
||||
raise ValueError(
|
||||
f"PortraitUtils_PairedImageLoader: output_dir not found: {out_path}"
|
||||
f"PairedImageLoader: output_dir not found: {out_path}"
|
||||
)
|
||||
|
||||
state = _STATE.setdefault(state_key, migrated_state or _NodeState())
|
||||
@@ -196,7 +196,7 @@ class PortraitUtils_PairedImageLoader:
|
||||
|
||||
if not pairs:
|
||||
raise RuntimeError(
|
||||
"PortraitUtils_PairedImageLoader: no matching images found "
|
||||
"PairedImageLoader: no matching images found "
|
||||
f"between {src_path} and {out_path}."
|
||||
)
|
||||
|
||||
@@ -216,7 +216,7 @@ class PortraitUtils_PairedImageLoader:
|
||||
_SIGNATURE_INDEX[signature] = state.index
|
||||
|
||||
print(
|
||||
"[PortraitUtils_PairedImageLoader] "
|
||||
"[PairedImageLoader] "
|
||||
f"{pair.display_name} ({next_index + 1}/{len(pairs)})"
|
||||
)
|
||||
|
||||
@@ -383,23 +383,23 @@ class PortraitUtils_PairedImageLoader:
|
||||
|
||||
if source_only:
|
||||
print(
|
||||
"[PortraitUtils_PairedImageLoader] Unmatched source files "
|
||||
"[PairedImageLoader] Unmatched source files "
|
||||
f"({len(source_only)}) in {source_dir}: {_summarize(source_only)}"
|
||||
)
|
||||
if output_only:
|
||||
print(
|
||||
"[PortraitUtils_PairedImageLoader] Unmatched output files "
|
||||
"[PairedImageLoader] Unmatched output files "
|
||||
f"({len(output_only)}) in {output_dir}: {_summarize(output_only)}"
|
||||
)
|
||||
if collisions:
|
||||
for key, files in collisions.items():
|
||||
print(
|
||||
"[PortraitUtils_PairedImageLoader] Normalised name collision "
|
||||
"[PairedImageLoader] Normalised name collision "
|
||||
f"for '{key}': {_summarize(sorted(set(files), key=_natural_sort_key))}"
|
||||
)
|
||||
if ext_mismatch:
|
||||
print(
|
||||
"[PortraitUtils_PairedImageLoader] Chosen fallback matches with differing "
|
||||
"[PairedImageLoader] Chosen fallback matches with differing "
|
||||
f"extensions ({len(ext_mismatch)}): {_summarize(ext_mismatch)}"
|
||||
)
|
||||
|
||||
@@ -473,9 +473,9 @@ class PortraitUtils_PairedImageLoader:
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PortraitUtils_PairedImageLoader": PortraitUtils_PairedImageLoader,
|
||||
"PairedImageLoader": PairedImageLoader,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PortraitUtils_PairedImageLoader": "Paired Image Loader",
|
||||
"PairedImageLoader": "Paired Image Loader",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user