Convert Tensor (NHWC) to PIL
This Node Returns a PIL Image From an IMAGE Socket. That's the Problem.
- image
- image
NHWCTensor2PIL is the return trip for the pack's PIL2NHWCTensor: it takes a tensor and hands you back a PIL.Image. The description in the source says exactly that - "convert NHWC Tensor to PIL Image." And that's precisely where the trouble starts, because a PIL image is not something a ComfyUI IMAGE socket is allowed to carry, and this node's output is typed IMAGE.
How it works
The whole execute method is five lines:
img_array = image.squeeze(0).cpu().numpy() * 255.0
img_pil = Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
return [img_pil]
It drops the batch dimension with squeeze(0), moves to CPU, multiplies by 255 to get back to 0–255 integers, clips, and builds a PIL.Image. If the input is a normal ComfyUI image tensor - float 0–1, NHWC - this produces a perfectly reasonable PIL image of the first frame. The math is right.
The contract violation
In ComfyUI, an IMAGE output is expected to be a torch tensor. Downstream nodes like PreviewImage, VAEEncode and SaveImage assume tensor and will choke on a PIL object - the next node will try to do tensor things to it and blow up, or silently misbehave. You can't preview it, you can't encode it, and you can't feed it back into a sampler. The node "works" only in the narrow sense that it returns without crashing, and only if your graph is built to consume a raw PIL object (which, again, standard nodes are not).
Two more sharp edges hiding in those five lines:
- It only handles one image.
squeeze(0)silently drops everything past the first frame of a batch. Feed it a 4-frame batch and you get one PIL image back and no warning. - It assumes
0–1float input. A tensor in some other range gives you clipped, washed-out or blown-out pixels, and the clip at0–255hides it instead of reporting it.
Should you use it?
In a normal ComfyUI workflow, no. The pair it forms with PIL2NHWCTensor (tensor → PIL → tensor) is a round-trip that a stock graph never needs - ComfyUI already passes images around as 0–1 NHWC tensors everywhere. The only place it earns its keep is inside BXYMartin's own diffusers-style InstantID pipeline, where a PIL.Image is a first-class citizen and something downstream genuinely wants one. If you find yourself wanting to convert a tensor to PIL in a standard graph, use a Python node or a proper image-utility pack instead - those follow the socket contract.
Install
No README in this repo, so the short version: ComfyUI Manager → search ComfyUI-InstantIDUtils → install, or manually:
cd ComfyUI/custom_nodes
git clone https://github.com/BXYMartin/ComfyUI-InstantIDUtils
Restart after. No models to download. The only declared dependency in requirements.txt is diffusers, and this node doesn't even import it - it's numpy, torch and PIL, all already present in ComfyUI.
Troubleshooting
- "PreviewImage" errors / nothing renders: the output is a PIL object and can't be previewed. This isn't a bug you can configure away - it's the node's contract.
- Missing frames: batch size > 1 and
squeeze(0)dropped them. Feed one image at a time. - White or flat output: input wasn't
0–1floats, so the×255landed wrong.
The honest verdict: a neat Python snippet wearing a node costume. If it's in a workflow you downloaded, the workflow author probably was too - this is a March-2024 InstantID-era helper from a pack that's been untouched since May 2024.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |