LuckyOda's picture
Add files using upload-large-folder tool
ba088fc verified
Raw
History Blame Contribute Delete
4.95 kB
# SPDX-License-Identifier: MIT
# Copyright (C) 2025 ComfyUI-Multiband Contributors
"""Compose Multiband node - stack multiple inputs into one multiband."""
import torch
from ..multiband_types import MULTIBAND_IMAGE, create_multiband
class ComposeMultiband:
"""
Compose multiple masks/images/multiband inputs into a single MULTIBAND_IMAGE.
Each input can be:
- MASK (B, H, W) -> becomes 1 channel
- IMAGE (B, H, W, 3) -> becomes 3 channels
- MULTIBAND_IMAGE -> all channels are added
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"input_1": ("*", {"tooltip": "First input (MASK, IMAGE, or MULTIBAND_IMAGE)"}),
"input_2": ("*", {"tooltip": "Second input (optional)"}),
"input_3": ("*", {"tooltip": "Third input (optional)"}),
"input_4": ("*", {"tooltip": "Fourth input (optional)"}),
"input_5": ("*", {"tooltip": "Fifth input (optional)"}),
"input_6": ("*", {"tooltip": "Sixth input (optional)"}),
"input_7": ("*", {"tooltip": "Seventh input (optional)"}),
"input_8": ("*", {"tooltip": "Eighth input (optional)"}),
"channel_names": ("STRING", {
"default": "",
"tooltip": "Comma-separated channel names (optional, auto-generated if empty)"
}),
}
}
RETURN_TYPES = (MULTIBAND_IMAGE,)
RETURN_NAMES = ("multiband",)
FUNCTION = "compose"
CATEGORY = "multiband/compose"
def _to_channels(self, inp, name_prefix: str) -> tuple:
"""Convert input to (B, C, H, W) tensor and channel names."""
if inp is None:
return None, []
if isinstance(inp, dict) and 'samples' in inp:
# MULTIBAND_IMAGE
samples = inp['samples']
names = inp.get('channel_names', [f"{name_prefix}_{i}" for i in range(samples.shape[1])])
return samples, names
if isinstance(inp, torch.Tensor):
if inp.ndim == 3:
# MASK: (B, H, W) -> (B, 1, H, W)
return inp.unsqueeze(1), [name_prefix]
elif inp.ndim == 4:
if inp.shape[-1] in (1, 3, 4):
# IMAGE: (B, H, W, C) -> (B, C, H, W)
samples = inp.permute(0, 3, 1, 2)
C = samples.shape[1]
if C == 3:
names = [f"{name_prefix}_R", f"{name_prefix}_G", f"{name_prefix}_B"]
elif C == 4:
names = [f"{name_prefix}_R", f"{name_prefix}_G", f"{name_prefix}_B", f"{name_prefix}_A"]
else:
names = [f"{name_prefix}_{i}" for i in range(C)]
return samples, names
else:
# Assume already (B, C, H, W)
C = inp.shape[1]
return inp, [f"{name_prefix}_{i}" for i in range(C)]
raise ValueError(f"Unsupported input type: {type(inp)}")
def compose(
self,
input_1=None, input_2=None, input_3=None, input_4=None,
input_5=None, input_6=None, input_7=None, input_8=None,
channel_names: str = ""
):
inputs = [input_1, input_2, input_3, input_4, input_5, input_6, input_7, input_8]
all_channels = []
all_names = []
for i, inp in enumerate(inputs):
if inp is None:
continue
channels, names = self._to_channels(inp, f"input_{i+1}")
if channels is not None:
all_channels.append(channels)
all_names.extend(names)
if not all_channels:
raise ValueError("At least one input is required")
# Verify all inputs have same batch size and spatial dimensions
B, _, H, W = all_channels[0].shape
for i, ch in enumerate(all_channels[1:], 2):
if ch.shape[0] != B:
raise ValueError(f"Batch size mismatch: input_1 has B={B}, input_{i} has B={ch.shape[0]}")
if ch.shape[2] != H or ch.shape[3] != W:
raise ValueError(f"Spatial size mismatch: input_1 has {H}x{W}, input_{i} has {ch.shape[2]}x{ch.shape[3]}")
# Concatenate all channels
samples = torch.cat(all_channels, dim=1)
# Use custom channel names if provided
if channel_names.strip():
custom_names = [n.strip() for n in channel_names.split(',')]
# Pad if needed
while len(custom_names) < samples.shape[1]:
custom_names.append(f"channel_{len(custom_names)}")
all_names = custom_names[:samples.shape[1]]
print(f"ComposeMultiband: Created {samples.shape[1]} channels from {len([i for i in inputs if i is not None])} inputs")
return (create_multiband(samples, all_names),)