Comfy-Org/ComfyUI · error · ValueError

No {file_format}/{bit_depth} encoder for {num_channels}-chan

Error message

No {file_format}/{bit_depth} encoder for {num_channels}-channel images: supported channel counts are 1 (grayscale), 3 (RGB) and 4 (RGBA).

What it means

Raised by the image-format save path when _FORMAT_SPECS has no entry for the (file_format, bit_depth, num_channels) tuple. The node supports PNG 8/16-bit and EXR 32-bit float, each only for 1, 3, or 4 channels (a 2-D HxW tensor is first unsqueezed to 1 channel). A tensor with 2, 5+ channels, or a format/depth/channel combination not in the spec table, is rejected.

Source

Thrown at comfy_extras/nodes_images.py:1115

    For EXR the input is interpreted according to `colorspace` and converted
    to scene-linear (EXR's convention) before writing:

      "sRGB"   → input is sRGB-encoded Rec. 709; apply inverse sRGB EOTF.
      "HDR"    → input is HLG-encoded Rec. 2020 (BT.2100); apply inverse HLG
                 OETF to get scene-linear, per BT.2100 Note 5a.
      "linear" → input is already scene-linear (Rec. 709 primaries); write
                 through unchanged. Use this for renderer/compositor output.

    For PNG, colorspace selection does not modify pixels — PNG is delivered
    sRGB-encoded and there is no PNG path for wide-gamut HDR in this node.
    """
    if img_tensor.ndim == 2:
        img_tensor = img_tensor.unsqueeze(-1)  # Some nodes emit grayscale as (H, W) with no channel dim, mask-style.
    height, width, num_channels = img_tensor.shape

    spec = _FORMAT_SPECS.get((file_format, bit_depth, num_channels))
    if spec is None:
        raise ValueError(
            f"No {file_format}/{bit_depth} encoder for {num_channels}-channel images: "
            "supported channel counts are 1 (grayscale), 3 (RGB) and 4 (RGBA)."
        )

    if spec["dtype"] == np.float32:
        # EXR path: preserve full range, no clamp.
        if colorspace == "sRGB":
            img_tensor = srgb_to_linear(img_tensor)
        elif colorspace == "HDR":
            img_tensor = hlg_to_linear(img_tensor)
        img_np = img_tensor.cpu().numpy().astype(np.float32)
    else:
        # PNG path: quantize to integer range.
        scaled = (img_tensor * spec["scale"]).clamp(0, spec["scale"])
        img_np = scaled.to(torch.int32).cpu().numpy().astype(spec["dtype"])

    # Encode directly via CodecContext. PyAV's `image2` muxer does NOT write to
    # BytesIO (it expects a real file path), so we bypass the container entirely.

View on GitHub (pinned to 1c6d8d45b3)

Solutions

  1. Reduce channels to 1, 3, or 4 before saving: slice or pad, e.g. t[..., :3] for RGB, torch.cat([t, t[..., :1]], -1) to promote grayscale to RGBA.
  2. If you need the extra channels, save them as a separate 1/3/4-channel image or a .pt/.npz file instead.
  3. Double-check the file_format/bit_depth pair — EXR only pairs with '32-bit float'.

Example fix

# before
img = flow_field  # (B, H, W, 2) -> ValueError
save(img)

# after
img = torch.cat([flow_field, torch.zeros_like(flow_field[..., :1])], dim=-1)  # (B,H,W,3)
save(img)
Defensive patterns

Strategy: validation

Validate before calling

SUPPORTED = {(1), (3), (4)}
assert img_tensor.shape[-1] in SUPPORTED, (
    f"{img_tensor.shape[-1]} channels not saveable; convert to 1, 3 or 4 channels first")

Type guard

def is_saveableable_image(t) -> bool:
    # after the node's own (H, W) -> (H, W, 1) unsqueeze convention
    return t.ndim >= 3 and t.shape[-1] in (1, 3, 4)

Try / catch

try:
    save_image(img, "png", "8-bit")
except ValueError as e:
    if "supported channel counts" in str(e):
        img = img[..., :3] if img.shape[-1] > 3 else img
        save_image(img, "png", "8-bit")
    else:
        raise

Prevention

When it happens

Trigger: Saving a 2-channel tensor (e.g. flow fields, xy maps, or a mask concatenated to a 1-channel image making 2 channels); saving an image whose channel count changed after custom compositing; selecting EXR with a bit_depth other than '32-bit float'.

Common situations: Workflows that repurpose image tensors to carry non-image data (normals as 3ch is fine, flow as 2ch is not); nodes that append extra channels (depth+alpha) producing 5 channels.

Related errors


AI-assisted analysis of Comfy-Org/ComfyUI@1c6d8d45b3 (2026-08-14). Data as JSON: /api/errors/664c83935d6ad9f3. Report an issue: GitHub.