[Partner Nodes] Stop adding an opaque alpha channel to API node images (#15369)

* Stop adding an opaque alpha channel to API node images

bytesio_to_image_tensor converted every downloaded image to RGBA, so nodes
whose API returns no transparency still emitted a 4 channel IMAGE. Keep the
alpha when the decoded image has one, stay RGB when it does not.

---------

Signed-off-by: bigcat88 <bigcat88@icloud.com>
Co-authored-by: bigcat88 <bigcat88@icloud.com>
This commit is contained in:
Christian Byrne
2026-08-15 10:27:24 -07:00
committed by GitHub
parent 0f1fa67ad8
commit a9ab2b62da
7 changed files with 170 additions and 10 deletions

View File

@@ -56,6 +56,8 @@ from comfy_api_nodes.util import (
ApiEndpoint,
audio_bytes_to_audio_input,
audio_input_to_mp3,
bytesio_to_image_tensor,
download_url_as_bytesio,
download_url_to_image_tensor,
download_url_to_video_output,
downscale_image_tensor_by_max_side,
@@ -1315,7 +1317,9 @@ class ByteDanceSeedreamLayerSeparationNode(IO.ComfyNode):
left, top, rect_w, rect_h = spec["left"], spec["top"], spec["rect_w"], spec["rect_h"]
async with semaphore:
try:
rgba = (await download_url_to_image_tensor(str(item["url"])))[0]
# the layer math below needs the alpha channel, and ByteDance encodes
# alpha-less images as plain RGB (the base plate is one), so force RGBA
rgba = bytesio_to_image_tensor(await download_url_as_bytesio(str(item["url"])), mode="RGBA")[0]
except ProcessingInterrupted:
raise
except Exception as exc:

View File

@@ -43,6 +43,7 @@ from comfy_api_nodes.util import (
download_url_to_image_tensor,
download_url_to_video_output,
get_number_of_images,
pad_images_to_common_channels,
sync_op,
tensor_to_base64_string,
upload_audio_to_comfyapi,
@@ -233,8 +234,8 @@ async def get_image_from_response(response: GeminiGenerateContentResponse, thoug
"Try rephrasing your prompt or changing the response modality to 'IMAGE+TEXT' "
"to see the model's reasoning."
)
return torch.zeros((1, 1024, 1024, 4))
return torch.cat(image_tensors, dim=0)
return torch.zeros((1, 1024, 1024, 3))
return torch.cat(pad_images_to_common_channels(image_tensors), dim=0)
def get_text_from_interaction(interaction: GeminiInteraction) -> str:

View File

@@ -27,6 +27,7 @@ from comfy_api_nodes.util import (
ApiEndpoint,
bytesio_to_image_tensor,
download_url_as_bytesio,
pad_images_to_common_channels,
resize_mask_to_image,
sync_op,
tensor_to_bytesio,
@@ -621,7 +622,7 @@ class RecraftImageToImageNode(IO.ComfyNode):
images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0))
pbar.update(1)
return IO.NodeOutput(torch.cat(images, dim=0))
return IO.NodeOutput(torch.cat(pad_images_to_common_channels(images), dim=0))
class RecraftImageInpaintingNode(IO.ComfyNode):
@@ -723,7 +724,7 @@ class RecraftImageInpaintingNode(IO.ComfyNode):
images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0))
pbar.update(1)
return IO.NodeOutput(torch.cat(images, dim=0))
return IO.NodeOutput(torch.cat(pad_images_to_common_channels(images), dim=0))
class RecraftTextToVectorNode(IO.ComfyNode):
@@ -954,7 +955,7 @@ class RecraftReplaceBackgroundNode(IO.ComfyNode):
images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0))
pbar.update(1)
return IO.NodeOutput(torch.cat(images, dim=0))
return IO.NodeOutput(torch.cat(pad_images_to_common_channels(images), dim=0))
class RecraftRemoveBackgroundNode(IO.ComfyNode):
@@ -995,7 +996,7 @@ class RecraftRemoveBackgroundNode(IO.ComfyNode):
image=image[i],
path="/proxy/recraft/images/removeBackground",
)
images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0))
images.append(torch.cat([bytesio_to_image_tensor(x, mode="RGBA") for x in sub_bytes], dim=0))
pbar.update(1)
images_tensor = torch.cat(images, dim=0)
@@ -1047,7 +1048,7 @@ class RecraftCrispUpscaleNode(IO.ComfyNode):
images.append(torch.cat([bytesio_to_image_tensor(x) for x in sub_bytes], dim=0))
pbar.update(1)
return IO.NodeOutput(torch.cat(images, dim=0))
return IO.NodeOutput(torch.cat(pad_images_to_common_channels(images), dim=0))
class RecraftCreativeUpscaleNode(RecraftCrispUpscaleNode):

View File

@@ -18,6 +18,7 @@ from .conversions import (
downscale_image_tensor_by_max_side,
downscale_video_to_max_pixels,
image_tensor_pair_to_batch,
pad_images_to_common_channels,
pil_to_bytesio,
resize_mask_to_image,
tensor_to_base64_string,
@@ -92,6 +93,7 @@ __all__ = [
"downscale_image_tensor_by_max_side",
"downscale_video_to_max_pixels",
"image_tensor_pair_to_batch",
"pad_images_to_common_channels",
"pil_to_bytesio",
"resize_mask_to_image",
"tensor_to_base64_string",

View File

@@ -16,12 +16,14 @@ from comfy_api.latest import Input, InputImpl, Types
from ._helpers import mimetype_to_extension
def bytesio_to_image_tensor(image_bytesio: BytesIO, mode: str = "RGBA") -> torch.Tensor:
def bytesio_to_image_tensor(image_bytesio: BytesIO, mode: str | None = None) -> torch.Tensor:
"""Converts image data from BytesIO to a torch.Tensor.
Args:
image_bytesio: BytesIO object containing the image data.
mode: The PIL mode to convert the image to (e.g., "RGB", "RGBA").
mode: The PIL mode to convert the image to (e.g., "RGB", "RGBA"). Defaults
to RGBA when the decoded image carries transparency and RGB when it
does not, so an API that returns no alpha does not get an opaque one.
Returns:
A torch.Tensor representing the image (1, H, W, C).
@@ -31,6 +33,8 @@ def bytesio_to_image_tensor(image_bytesio: BytesIO, mode: str = "RGBA") -> torch
ValueError: If the specified mode is invalid.
"""
image = Image.open(image_bytesio)
if mode is None:
mode = "RGBA" if "A" in image.getbands() or "transparency" in image.info else "RGB"
image = image.convert(mode)
image_array = np.array(image).astype(np.float32) / 255.0
return torch.from_numpy(image_array).unsqueeze(0)
@@ -53,6 +57,17 @@ def image_tensor_pair_to_batch(image1: torch.Tensor, image2: torch.Tensor) -> to
return torch.cat((image1, image2), dim=0)
def pad_images_to_common_channels(images: list[torch.Tensor]) -> list[torch.Tensor]:
"""Pads [B, H, W, C] image tensors with opaque alpha so they all share the largest channel count."""
channels = max(image.shape[-1] for image in images)
return [
torch.nn.functional.pad(image, (0, channels - image.shape[-1]), value=1.0)
if image.shape[-1] < channels
else image
for image in images
]
def tensor_to_bytesio(
image: torch.Tensor,
*,