Nodes/ComfyUI-InstantIDUtils/Convert PIL to Tensor (NHWC)
ComfyUI Node

Convert PIL to Tensor (NHWC)

You Probably Don't Need This PIL-to-Tensor Converter (But Here's What It Does)

By BXYMartin·Created 2 years ago·Updated 2 years ago· 3
Convert PIL to Tensor (NHWC)
  • image
  • image

Let me save you some head-scratching up front: the thing this node converts to is the exact format ComfyUI already hands you for free. ComfyUI's IMAGE tensors are float 0–1 arrays in NHWC layout (batch, height, width, channels). A LoadImage node outputs precisely that. So why does a "Convert PIL to Tensor (NHWC)" node exist at all?

Because this pack - BXYMartin/ComfyUI-InstantIDUtils, a tiny grab-bag of "ControlNetUtils" helpers that shipped alongside the author's InstantID implementation back in March 2024 - was written for a world where a custom diffusers pipeline was receiving a PIL.Image object and needed a tensor before feeding it to a model. The author's own InstantID nodes live in that world. If you're not in that world, this node is mostly redundant. If you are, it does exactly one job.

How it works

Read the source and it's ten lines with a comment in Chinese that translates to "convert to CHW (if needed)":

img_array = np.array(image)
img_tensor = torch.from_numpy(img_array).float() / 255.
if img_tensor.ndim == 3 and img_tensor.shape[-1] == 3:
    img_tensor = img_tensor.permute(2, 0, 1)
img_tensor = img_tensor.unsqueeze(0).permute(0, 2, 3, 1)
return [img_tensor]

So: take a PIL image, dump it to a numpy array, divide by 255 to get floats in 0–1, flip it to channel-first (CHW), then add a batch dimension and flip it back to NHWC. That round-trip ends with the same layout you started with, which tells you the layout was never the real problem - the / 255. was.

The input/output trap

The schema says:

  • input image - type IMAGE
  • output image - type IMAGE

The problem is that an IMAGE socket in ComfyUI carries a tensor, not a PIL.Image. Feed this node a normal ComfyUI tensor and the code runs np.array() on a tensor of floats, skips the ndim == 3 branch (a stock tensor is already 4D), then unsqueeze(0) makes it 5D. You get garbage with an extra batch axis, or a shape error downstream. The node only behaves as advertised if whatever is feeding it genuinely hands it a PIL image - which a normal ComfyUI graph can't do through an IMAGE socket. The typing is a lie, and it only works because it's meant to sit inside the author's own pipeline nodes, not your average workflow.

When you'd actually reach for it

Honestly? Almost never, in a stock ComfyUI graph. If you need a PIL image converted, do it in a Python node or just skip it - LoadImage already gives you NHWC float 0–1. The real use case is debugging or building the kind of diffusers-based InstantID graph this pack was born for, where something downstream expects the author's exact tensor contract.

Install

The pack has no README - literally, the repo contains no README file, so this article is about the only documentation you'll get. Install is the usual two-step:

cd ComfyUI/custom_nodes
git clone https://github.com/BXYMartin/ComfyUI-InstantIDUtils

Then restart ComfyUI. Or use ComfyUI Manager and search for ComfyUI-InstantIDUtils (display title "ComfyUI-InstantIDUtils"). No model files to download - this pack ships no weights and needs nothing but the diffusers library that requirements.txt lists. PIL2NHWCTensor itself only touches numpy, torch and PIL, all of which ship with ComfyUI already.

Troubleshooting

  • Shape error / extra dimension: you fed it a stock tensor. It wants a PIL image, and you can't get one through an IMAGE socket without custom nodes. That's the mismatch working as designed.
  • The pack is abandonware: last commit May 2024, three stars, and the InstantID ecosystem it served has been dormant since then - on Flux, identity work moved to PuLID. Don't expect fixes.
  • Wrong values: if the output looks washed out, someone fed it a tensor already in 0–1 and the /255. double-scaled it. Same root cause as above.

If the node still confuses you, that's not your fault. It's a utility that made sense in one specific March-2024 pipeline and got typed badly everywhere else.

CategoryControlNetUtils

Inputs (1)

NameTypeDefaultDescription
imageIMAGE

Outputs (1)

NameTypeDescription
imageIMAGE