Union SMPL Part Masks
You Already Know Where the Hands Are — So Just Mask Them
- part_masks
- mask
Every hands-and-faces cleanup pass in ComfyUI starts with a detector. Render the image, run a face/hand detector, inpaint whatever it found. That works right up until the detector misses a hand, or finds one you didn't want, or finds one twice.
If your subject came out of Generate SMPL Avatar you can skip detection entirely. You built the body, so you know exactly which pixels are the hands. Union SMPL Part Masks turns those 24 per-part masks into the single MASK you actually wanted, with a couple of pixels of dilation on top.
What it does
Two ways to select, and they're additive:
preset- thirteen canned groups:none,head,head and neck,torso,arms,hands,arms and hands,legs,feet,legs and feet,upper body,lower body,full body.- The 24 toggles - one boolean per SMPL joint (
hips,spine,spine1,spine2,neck,head,leftShoulder,leftArm,leftForeArm,leftHand,leftHandIndex1, the right-side mirror, and legs). Each carries the author's plain-English gloss as its tooltip:spine2is "chest and upper back",leftShoulderis "left collarbone / trapezius area",leftHandIndex1is "fingers".
Then grow_px dilates the result with a positive number and erodes it with a negative one, up to 64 pixels. One output: mask.
Additive is the trap worth internalising: the selection is the union of preset and toggles, not an intersection and not "toggles override the preset". Leave preset on torso, switch on head, get torso plus head. Driving the toggles by hand? Set the preset back to none first.
How it works
part_masks arrives as one tensor of shape [24·N, H, W] - for each of N avatars, 24 masks in canonical SMPL joint order, hips at 0, rightHandIndex1 at 23. The node maps your preset to part names, adds any enabled toggles, converts the names to indices and does a max over those channels. Max rather than sum because the input masks are already disjoint: the pack multiplies each part label map by the silhouette, so no pixel belongs to two parts. Selecting nothing returns zeros rather than an error - deliberate, and occasionally confusing.
Batching is the quietly useful bit: one avatar gives one mask, two avatars give two. A workflow rendering a male and a female avatar in one node still lines up downstream, mask for image.
grow_px is a square-kernel max_pool2d, so corners grow diagonally - fine at 8 px, blocky at 40. The erosion path pads with zeros explicitly, because max_pool2d pads with negative infinity and negating it would count everything outside the image as inside the mask and glue it to the frame edge. Someone hit that.
Where it goes
Wire the output into anything that takes a mask: InpaintModelConditioning for a proper inpaint, VAEEncode with a mask, ImageCompositeMasked, Set Latent Noise Mask. Keep a Preview Mask on it while you tune - you can't judge a selection from the finished image.
What the parts unlock, in rough order of trouble saved:
handswithgrow_pxaround 6–8, then a hand-fix inpaint. No detector, no double-detection, and the mask is identical every run because the body is deterministic.headorhead and neckfor a higher-resolution face pass - same idea as a face-only second pass, except you never had to find the face.torsofor a clothing swap,legs and feetfor shoes,armsfor sleeves.full bodyas a person-shaped silhouette, handy for compositing and for outpainting everything that isn't the subject.
SMPL and SMPL-H share mesh topology, so one segmentation serves both - but pick model_type = SMPL-H on the generator if hands are the point, since that model has articulated hands.
Installing it
Same pack, same install, because this node doesn't exist on its own:
cd ComfyUI/custom_nodes
git clone https://github.com/corentineuvrard/ComfyUI-SMPL
cd ComfyUI-SMPL
pip install -r requirements.txt
Manager search "ComfyUI-SMPL" works too. Then the body models: download from smpl.is.tue.mpg.de and mano.is.tue.mpg.de (registration, non-commercial research licence) and run python scripts/prepare_models.py --source <downloads>. Both nodes appear under SMPL Avatar.
Troubleshooting
The mask is empty and the node warned about it. The node logs when every input part mask is empty. Two causes, both upstream on the generator: compute_part_masks was off (then part_masks is all zeros by design), or the body fell outside the view under framing = fixed. This node can only union what it's given.
"expected part masks of shape (24*N, H, W)". You wired something that isn't part_masks from Generate SMPL Avatar - a plain silhouette, say. The channel count is how the node knows which parts it's looking at.
Seams when you select one part. Parts are disjoint by construction: a vertex on the neck/head boundary is listed in both, but each face gets one label. So a single-part mask stops dead at the seam. Need a continuous region? Union the neighbours - that's what head and neck and arms and hands are for - or add a pixel or two of grow_px.
The licence. The pack's code is MIT; smplx and the SMPL model files are Max Planck non-commercial research licences, with commercial licensing through Meshcapade. Worth knowing before you build a paid pipeline on these masks.
Inputs (27)
| Name | Type | Default | Description |
|---|---|---|---|
| part_masks | MASK | The 'part_masks' output of 'Generate SMPL Avatar'. | |
| preset | COMBO | none | Group of parts to include. |
| hips | BOOLEAN | false | pelvis |
| leftUpLeg | BOOLEAN | false | left thigh |
| rightUpLeg | BOOLEAN | false | right thigh |
| spine | BOOLEAN | false | lower torso (belly / lower back) |
| leftLeg | BOOLEAN | false | left shin and calf |
| rightLeg | BOOLEAN | false | right shin and calf |
| spine1 | BOOLEAN | false | mid torso |
| leftFoot | BOOLEAN | false | left foot |
| rightFoot | BOOLEAN | false | right foot |
| spine2 | BOOLEAN | false | chest and upper back |
| leftToeBase | BOOLEAN | false | left toes |
| rightToeBase | BOOLEAN | false | right toes |
| neck | BOOLEAN | false | neck |
| leftShoulder | BOOLEAN | false | left collarbone / trapezius area |
| rightShoulder | BOOLEAN | false | right collarbone / trapezius area |
| head | BOOLEAN | false | head |
| leftArm | BOOLEAN | false | left upper arm |
| rightArm | BOOLEAN | false | right upper arm |
| leftForeArm | BOOLEAN | false | left forearm |
| rightForeArm | BOOLEAN | false | right forearm |
| leftHand | BOOLEAN | false | left hand (palm) |
| rightHand | BOOLEAN | false | right hand (palm) |
| leftHandIndex1 | BOOLEAN | false | left fingers |
| rightHandIndex1 | BOOLEAN | false | right fingers |
| grow_pxopt | INT | 0-64–64 | Dilate (positive) or erode (negative) the result by this many pixels. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| mask | MASK | Union of the selected parts (1 = selected). |