Fix mouth mask

This commit is contained in:
Kenneth Estanislao
2026-05-18 02:11:04 +08:00
parent 0e97e474e4
commit ca8e39e3bb
3 changed files with 68 additions and 16 deletions
+30
View File
@@ -187,6 +187,36 @@ def detect_many_faces_fast(frame: Frame) -> Any:
for i in range(bboxes.shape[0])]
def ensure_landmarks(frame: Frame, faces: Any) -> None:
"""Run the 2d106 landmark model in-place on faces that lack it.
The fast webcam path (detect_one_face_fast / detect_many_faces_fast)
produces detection-only Face objects with no ``landmark_2d_106``.
Mouth masking needs those landmarks, so add them on demand only when
the feature is active — keeping the fast path fast otherwise.
"""
if faces is None:
return
if not isinstance(faces, (list, tuple)):
faces = [faces]
fa = get_face_analyser()
lmk_model = fa.models.get("landmark_2d_106")
if lmk_model is None:
return
for face in faces:
if face is None:
continue
# insightface Face is a dict; missing keys raise AttributeError,
# so getattr(..., None) is the safe presence check.
if getattr(face, "landmark_2d_106", None) is None:
try:
lmk_model.get(frame, face)
except Exception as e: # pragma: no cover - never break the swap
print(f"Error computing 2d106 landmarks: {e}")
def has_valid_map() -> bool:
for map in modules.globals.source_target_map:
if "source" in map and "target" in map:
+25 -12
View File
@@ -1111,13 +1111,20 @@ def create_lower_mouth_mask(
return mask, mouth_cutout, mouth_box, lower_lip_polygon
try: # Wrap main logic in try-except
# Use outer mouth landmarks (52-71) to capture the full mouth area
# This covers both upper and lower lips for proper mouth preservation
lower_lip_order = list(range(52, 72))
# Outer mouth/lip landmarks (52-63) — the lip outline only. In this
# repo's insightface 2d106 convention these 12 points, taken in index
# order, form a SIMPLE (non-self-intersecting) closed polygon that
# cv2.fillPoly fills as one solid region directly over the mouth.
# This is the last shipped, known-good landmark set; range(52,72)
# (the regression) added the inner-lip points and made the path
# self-intersect, and the ancient [65,66,62,...,0,8,7...] indices
# belong to a different/older landmark convention (they land on the
# inner lip + random jaw points, so the mask never covers the mouth).
lower_lip_order = list(range(52, 64))
# Check if all indices are valid for the loaded landmarks (already partially done by < 106 check)
# All indices must be valid for the loaded landmark set
if max(lower_lip_order) >= landmarks.shape[0]:
# print(f"Warning: Landmark index {max(lower_lip_order)} out of bounds for shape {landmarks.shape[0]}.")
# print(f"Warning: Landmark index out of bounds for shape {landmarks.shape[0]}.")
return mask, mouth_cutout, mouth_box, lower_lip_polygon
lower_lip_landmarks = landmarks[lower_lip_order].astype(np.float32)
@@ -1132,16 +1139,22 @@ def create_lower_mouth_mask(
# print("Warning: Could not calculate valid center for mouth mask.")
return mask, mouth_cutout, mouth_box, lower_lip_polygon
# Drive expansion from the Mouth Mask slider so it actually responds.
# The known-good version expanded by the now-unused mask_down_size
# constant, which is why the slider had no effect.
# s: 0.0 (slider ~0, tight lip outline) -> 1.0 (slider 100, mouth->chin).
mouth_mask_size = getattr(modules.globals, "mouth_mask_size", 0.0) # 0-100 slider
# 0=tight lip outline, 50=covers mouth area, 100=mouth to chin
expansion_factor = 1 + (mouth_mask_size / 100.0) * 2.5
s = max(0.0, min(1.0, mouth_mask_size / 100.0))
# Expand landmarks from center, with extra downward bias toward chin
# Uniformly scaling a simple polygon about its centroid keeps it simple
# (no self-intersection). x grows with expansion_factor; points below
# centre (toward the chin) also get an extra downward stretch so high
# slider values reach from the mouth down to the chin.
expansion_factor = 1.0 + s * 2.0 # 1.0x -> 3.0x
chin_bias = 1.0 + s * 2.0 # extra downward stretch
offsets = lower_lip_landmarks - center
# Add extra downward expansion for points below center (toward chin)
chin_bias = 1 + (mouth_mask_size / 100.0) * 1.5 # extra vertical stretch downward
scale_y = np.where(offsets[:, 1] > 0, expansion_factor * chin_bias, expansion_factor)
scale_y = np.where(offsets[:, 1] > 0,
expansion_factor * chin_bias, expansion_factor)
expanded_landmarks = lower_lip_landmarks.copy()
expanded_landmarks[:, 0] = center[0] + offsets[:, 0] * expansion_factor
expanded_landmarks[:, 1] = center[1] + offsets[:, 1] * scale_y
+13 -4
View File
@@ -58,6 +58,7 @@ from modules.face_analyser import (
add_blank_map,
detect_many_faces_fast,
detect_one_face_fast,
ensure_landmarks,
get_one_face,
get_unique_faces_from_target_image,
get_unique_faces_from_target_video,
@@ -336,8 +337,10 @@ def load_switch_states():
modules.globals.live_resizable = state.get("live_resizable", False)
modules.globals.fp_ui = state.get("fp_ui", {"face_enhancer": False})
modules.globals.show_fps = state.get("show_fps", False)
modules.globals.mouth_mask_size = state.get("mouth_mask_size", 0.0)
modules.globals.mouth_mask = modules.globals.mouth_mask_size > 0
# Mouth mask always starts disabled (slider at 0) on launch,
# regardless of the persisted value — enable it explicitly each session.
modules.globals.mouth_mask_size = 0.0
modules.globals.mouth_mask = False
modules.globals.show_mouth_mask_box = False
except FileNotFoundError:
pass
@@ -655,9 +658,9 @@ class MainWindow(QMainWindow):
self.s_sharpness.setToolTip(_("Sharpen the enhanced face output"))
grid.addWidget(self.s_sharpness, 1, 1)
# Mouth mask
# Mouth mask — always starts at 0 (disabled) on launch
grid.addWidget(QLabel(_("Mouth Mask")), 2, 0)
self.s_mouth = slider(0.0, 100.0, modules.globals.mouth_mask_size, 1,
self.s_mouth = slider(0.0, 100.0, 0.0, 1,
self._on_mouth_mask_change)
self.s_mouth.sliderPressed.connect(self._on_mouth_mask_pressed)
self.s_mouth.sliderReleased.connect(self._on_mouth_mask_released)
@@ -1085,6 +1088,12 @@ class _ProcessingWorker(QThread):
elif cached_target_face is not None:
cached_faces = [cached_target_face]
# Fast detection skips the 2d106 landmark model, but the mouth
# mask needs it. Attach landmarks on demand (computed once per
# detection cycle — the helper no-ops if already present).
if modules.globals.mouth_mask and cached_faces:
ensure_landmarks(temp_frame, cached_faces)
for fp in frame_processors:
if fp.NAME == "DLC.FACE-ENHANCER":
if modules.globals.fp_ui["face_enhancer"]: