Nodes/lip_mask_from_pose/Mouth Masks (A+B) from Sapiens2 Seg
ComfyUI Node

Mouth Masks (A+B) from Sapiens2 Seg

Sapiens2 pose can't see teeth. Its segmentation head can — this node reads it

By Jalen-Brunson·Created 5 months ago·Updated 21 days ago· 2
Mouth Masks (A+B) from Sapiens2 Seg
  • class_id_mask
  • mask_a
  • mask_b
  • selected_a
  • selected_b
mask_a_class_1Tongue
mask_a_class_2none
mask_a_class_3none
mask_a_class_4none
mask_a_blur_radius0
mask_a_grow_pixels0
mask_a_fill_holesfalse
mask_a_invertfalse
mask_b_class_1Upper_Teeth
mask_b_class_2Lower_Teeth
mask_b_class_3none
mask_b_class_4none
mask_b_blur_radius0
mask_b_grow_pixels0
mask_b_fill_holesfalse
mask_b_invertfalse
num_classes29

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_classes doesn'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_holes for that mask.
  • Mask bleeds onto the lip: shrink mask_b_grow_pixels or feather less - the seg head's teeth boundary is usually tight, but a couple of pixels of grow can cross it.
Categorymask/pose

Inputs (18)

NameTypeDefaultDescription
class_id_maskMASK
mask_a_class_1COMBOTongue30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24
mask_a_class_2COMBOnone30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24
mask_a_class_3COMBOnone30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24
mask_a_class_4COMBOnone30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24
mask_a_blur_radiusINT00–100Mask A: Gaussian blur radius for feathered edges.
mask_a_grow_pixelsINT00–200Mask A: dilate outward by this many pixels.
mask_a_fill_holesBOOLEANfalseMask A: close small interior holes.
mask_a_invertBOOLEANfalseMask A: invert the binary mask.
mask_b_class_1COMBOUpper_Teeth30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24
mask_b_class_2COMBOLower_Teeth30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24
mask_b_class_3COMBOnone30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24
mask_b_class_4COMBOnone30 options: none, Upper_Lip, Lower_Lip, Upper_Teeth, Lower_Teeth, Tongue, +24
mask_b_blur_radiusINT00–100Mask B: Gaussian blur radius for feathered edges.
mask_b_grow_pixelsINT00–200Mask B: dilate outward by this many pixels.
mask_b_fill_holesBOOLEANfalseMask B: close small interior holes.
mask_b_invertBOOLEANfalseMask B: invert the binary mask.
num_classesINT292–256Total classes in the source segmentation. Sapiens2 standard = 29.

Outputs (4)

NameTypeDescription
mask_aMASK
mask_bMASK
selected_aSTRING
selected_bSTRING