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
- 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.
- If you need the extra channels, save them as a separate 1/3/4-channel image or a .pt/.npz file instead.
- 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
- Normalize tensors to 1/3/4 channels right after any custom channel math.
- Remember EXR only pairs with '32-bit float'.
- Store non-image channel layouts (e.g. flow as 2ch) as .pt/.npz, not images.
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
- Input audio must have {expected_channels} channels, got {wav
- SeedVR2 expected {name} channels to be {channels}, got shape
- SeedVR2Conditioning expects SeedVR2 VAE latents with {SEEDVR
- SeedVR2TemporalChunk: expected {SEEDVR2_LATENT_CHANNELS} lat
- INVALID_TAG_FILTER
AI-assisted analysis of Comfy-Org/ComfyUI@1c6d8d45b3 (2026-08-14).
Data as JSON: /api/errors/664c83935d6ad9f3.
Report an issue: GitHub.