mirror of
https://github.com/hacksider/Deep-Live-Cam.git
synced 2026-07-11 12:46:33 +02:00
Fix mouth mask
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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"]:
|
||||
|
||||
Reference in New Issue
Block a user