Mouth Masks (A+B) from Sapiens2 Seg
Sapiens2 pose can't see teeth. Its segmentation head can — this node reads it
- class_id_mask
- mask_a
- mask_b
- selected_a
- selected_b
Pose keypoints can trace your lips, but they can't see inside your mouth. Teeth whitening, smile replacement, tongue edits - all of those need a per-pixel mask of the teeth or tongue, and no keypoint cloud is going to hand you one. Sapiens2's body-part segmentation head does, because it classifies every pixel of the frame into one of 29 body-part classes - and that list includes Upper_Teeth, Lower_Teeth, and Tongue alongside Upper_Lip and Lower_Lip. MouthMaskFromSapiens2Seg is the node that reads that segmentation output and splits the mouth into the two masks you actually want.
The defaults tell you exactly how the author intends it: mask A = Tongue, mask B = Upper_Teeth + Lower_Teeth. That's a "preserve the tongue, mask the teeth" setup - you use B to inpaint over just the teeth while the tongue stays put. Swap the roles, and you're preserving teeth while editing the tongue. It's a two-mask splitter with the annoying mask math already done.
How it works
The input class_id_mask comes from the Sapiens2 Body-Part Segmentation node's output, and the format is the slightly unusual part: it's a float MASK whose pixel value is class_id / (num_classes - 1), so the node has to decode class IDs back out of it before it can do anything. num_classes defaults to 29, the Sapiens2 standard - only touch it if your source uses a different class count.
For each of the two outputs it then builds a mask by unioning up to four selected classes (mask_a_class_1..4, mask_b_class_1..4). Each mask gets its own independent post-processing block: mask_a_blur_radius/mask_b_blur_radius (feather), mask_a_grow_pixels/mask_b_grow_pixels (dilate), mask_a_fill_holes/mask_b_fill_holes (close interior holes), and mask_a_invert/mask_b_invert. The class dropdowns have the mouth classes floated to the top for convenience, but the full 29 are there - you can grab Face_Neck, Hair, whatever, since this is really a general "pick up to 4 classes per mask" extractor that happens to be great at mouths.
Outputs: mask_a and mask_b (the two MASKs - wire them into inpainting, or combine them with ComfyUI's built-in mask math), plus selected_a and selected_b strings that echo back the comma-joined class names actually used. That last pair is the debugging gift: if a mask comes out empty, the string tells you exactly which classes went into it.
Installing it
Manager (search "lip_mask_from_pose") or git clone https://github.com/Jalen-Brunson/lip_mask_from_pose into custom_nodes/, restart. Zero extra dependencies - this node is numpy/cv2/torch only. The real dependency is upstream: you need a Sapiens2 pipeline producing the 29-class body-part segmentation class_id_mask, which lives in a separate Sapiens2 pack. No Sapiens2, no input, no point.
Troubleshooting
- Teeth mask is empty: most likely the source segmentation was run with a different class count and
num_classesdoesn't match - verify against your Sapiens2 pack's class list (the node's class order mirrors it). - Mask has holes in the middle of a tooth: turn on
mask_b_fill_holesfor that mask. - Mask bleeds onto the lip: shrink
mask_b_grow_pixelsor feather less - the seg head's teeth boundary is usually tight, but a couple of pixels of grow can cross it.
Inputs (18)
| Name | Type | Default | Description |
|---|---|---|---|
| class_id_mask | MASK | — | |
| mask_a_class_1 | COMBO | Tongue | 30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24 |
| mask_a_class_2 | COMBO | none | 30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24 |
| mask_a_class_3 | COMBO | none | 30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24 |
| mask_a_class_4 | COMBO | none | 30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24 |
| mask_a_blur_radius | INT | 00–100 | Mask A: Gaussian blur radius for feathered edges. |
| mask_a_grow_pixels | INT | 00–200 | Mask A: dilate outward by this many pixels. |
| mask_a_fill_holes | BOOLEAN | false | Mask A: close small interior holes. |
| mask_a_invert | BOOLEAN | false | Mask A: invert the binary mask. |
| mask_b_class_1 | COMBO | Upper_Teeth | 30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24 |
| mask_b_class_2 | COMBO | Lower_Teeth | 30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24 |
| mask_b_class_3 | COMBO | none | 30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24 |
| mask_b_class_4 | COMBO | none | 30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24 |
| mask_b_blur_radius | INT | 00–100 | Mask B: Gaussian blur radius for feathered edges. |
| mask_b_grow_pixels | INT | 00–200 | Mask B: dilate outward by this many pixels. |
| mask_b_fill_holes | BOOLEAN | false | Mask B: close small interior holes. |
| mask_b_invert | BOOLEAN | false | Mask B: invert the binary mask. |
| num_classes | INT | 292–256 | Total classes in the source segmentation. Sapiens2 standard = 29. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| mask_a | MASK | — |
| mask_b | MASK | — |
| selected_a | STRING | — |
| selected_b | STRING | — |