diff --git a/modules/face_analyser.py b/modules/face_analyser.py index e56e2fb..b3b4ff1 100644 --- a/modules/face_analyser.py +++ b/modules/face_analyser.py @@ -28,7 +28,7 @@ def get_face_analyser() -> Any: FACE_ANALYSER = insightface.app.FaceAnalysis( name='buffalo_l', providers=modules.globals.execution_providers, - allowed_modules=['detection', 'recognition'] + allowed_modules=['detection', 'recognition', 'landmark_2d_106'] ) FACE_ANALYSER.prepare(ctx_id=0, det_size=(320, 320)) return FACE_ANALYSER diff --git a/modules/globals.py b/modules/globals.py index 65e3fdd..aabc19a 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -63,6 +63,7 @@ show_mouth_mask_box: bool = False # Visualize the mouth mask area (for debuggin mask_feather_ratio: int = 12 # Denominator for feathering calculation (higher = smaller feather) mask_down_size: float = 0.1 # Expansion factor for lower lip mask (relative) mask_size: float = 1.0 # Expansion factor for upper lip mask (relative) +mouth_mask_size: float = 0.0 # Mouth mask size (0-100; 0=off, 100=mouth to chin) # --- START: Added for Frame Interpolation --- enable_interpolation: bool = True # Toggle temporal smoothing diff --git a/modules/processors/frame/face_masking.py b/modules/processors/frame/face_masking.py index 3435b7e..5265921 100644 --- a/modules/processors/frame/face_masking.py +++ b/modules/processors/frame/face_masking.py @@ -82,8 +82,8 @@ def create_lower_mouth_mask( landmarks = face.landmark_2d_106 if landmarks is not None: - # Use outer mouth landmarks (52-63) to capture the lips only - lower_lip_order = list(range(52, 64)) + # Use outer mouth landmarks (52-71) to capture the full mouth area + lower_lip_order = list(range(52, 72)) if max(lower_lip_order) >= landmarks.shape[0]: return mask, mouth_cutout, mouth_box, lower_lip_polygon @@ -94,13 +94,16 @@ def create_lower_mouth_mask( center = np.mean(lower_lip_landmarks, axis=0) # Expand the landmarks outward using the mouth_mask_size - # Use a more conservative expansion to avoid affecting face shape - expansion_factor = ( - 1 + modules.globals.mask_down_size * modules.globals.mouth_mask_size - ) - expanded_landmarks = (lower_lip_landmarks - center) * expansion_factor + center + mouth_mask_size = getattr(modules.globals, "mouth_mask_size", 0.0) # 0-100 slider + expansion_factor = 1 + (mouth_mask_size / 100.0) * 2.5 - # Removed specific top/chin extensions to preserve face shape + # Expand with extra downward bias toward chin + offsets = lower_lip_landmarks - center + chin_bias = 1 + (mouth_mask_size / 100.0) * 1.5 + 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 # Convert back to integer coordinates expanded_landmarks = expanded_landmarks.astype(np.int32) diff --git a/modules/processors/frame/face_swapper.py b/modules/processors/frame/face_swapper.py index 991e294..04f846b 100644 --- a/modules/processors/frame/face_swapper.py +++ b/modules/processors/frame/face_swapper.py @@ -136,10 +136,12 @@ def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame: if not hasattr(source_face, 'normed_embedding') or source_face.normed_embedding is None: return temp_frame - # Store a copy of the original frame before swapping for opacity blending + # Store a copy of the original frame before swapping for opacity blending and mouth mask opacity = getattr(modules.globals, "opacity", 1.0) opacity = max(0.0, min(1.0, opacity)) - original_frame = temp_frame if opacity >= 1.0 else temp_frame.copy() + mouth_mask_enabled = getattr(modules.globals, "mouth_mask", False) + # Always copy if mouth mask is enabled (we need the unmodified original for mouth cutout) + original_frame = temp_frame.copy() if (opacity < 1.0 or mouth_mask_enabled) else temp_frame # Pre-swap Input Check with optimization if temp_frame.dtype != np.uint8: @@ -190,28 +192,28 @@ def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame: # --- Post-swap Processing (Masking, Opacity, etc.) --- # Now, work with the guaranteed uint8 'swapped_frame' - if getattr(modules.globals, "mouth_mask", False): # Check if mouth_mask is enabled + if mouth_mask_enabled: # Check if mouth_mask is enabled # Create a mask for the target face - face_mask = create_face_mask(target_face, temp_frame) # Use temp_frame (original shape) for mask creation geometry + face_mask = create_face_mask(target_face, original_frame) # Use original_frame for mask creation geometry - # Create the mouth mask using original geometry + # Create the mouth mask using the ORIGINAL frame (before swap) for cutout mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon = ( - create_lower_mouth_mask(target_face, temp_frame) # Use temp_frame (original) for cutout + create_lower_mouth_mask(target_face, original_frame) # Use original_frame for real mouth cutout ) # Apply the mouth area only if mouth_cutout exists - if mouth_cutout is not None and mouth_box != (0,0,0,0): # Add check for valid box - # Apply mouth area (from original) onto the 'swapped_frame' + if mouth_cutout is not None and mouth_box != (0,0,0,0): + # Apply mouth area (from original) onto the 'swapped_frame' swapped_frame = apply_mouth_area( swapped_frame, mouth_cutout, mouth_box, face_mask, lower_lip_polygon ) + # Draw bounding box only while slider is being dragged if getattr(modules.globals, "show_mouth_mask_box", False): - mouth_mask_data = (mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon) - # Draw visualization on the swapped_frame *before* opacity blending - swapped_frame = draw_mouth_mask_visualization( - swapped_frame, target_face, mouth_mask_data - ) + mouth_mask_data = (mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon) + swapped_frame = draw_mouth_mask_visualization( + swapped_frame, target_face, mouth_mask_data + ) # --- Poisson Blending --- if getattr(modules.globals, "poisson_blend", False): @@ -750,9 +752,9 @@ 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-63) to capture the lips only - # This avoids including the chin/jawline, preserving the face shape from the swap - lower_lip_order = list(range(52, 64)) + # 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)) # Check if all indices are valid for the loaded landmarks (already partially done by < 106 check) if max(lower_lip_order) >= landmarks.shape[0]: @@ -772,9 +774,18 @@ def create_lower_mouth_mask( return mask, mouth_cutout, mouth_box, lower_lip_polygon - mask_down_size = getattr(modules.globals, "mask_down_size", 0.1) # Default 0.1 - expansion_factor = 1 + mask_down_size - expanded_landmarks = (lower_lip_landmarks - center) * expansion_factor + center + 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 + + # Expand landmarks from center, with extra downward bias toward chin + 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) + expanded_landmarks = lower_lip_landmarks.copy() + expanded_landmarks[:, 0] = center[0] + offsets[:, 0] * expansion_factor + expanded_landmarks[:, 1] = center[1] + offsets[:, 1] * scale_y # Ensure landmarks are finite after adjustments if not np.all(np.isfinite(expanded_landmarks)): @@ -881,8 +892,8 @@ def draw_mouth_mask_visualization( print(f"Error drawing polygon for visualization: {e}") # Optional debug pass - # Optional: Draw bounding box (red rectangle) - # cv2.rectangle(vis_frame, (min_x, min_y), (max_x, max_y), (0, 0, 255), 1) + # Draw bounding box (red rectangle) + cv2.rectangle(vis_frame, (min_x, min_y), (max_x, max_y), (0, 0, 255), 2) # Optional: Add labels label_pos_y = min_y - 10 if min_y > 20 else max_y + 15 # Adjust position based on box location @@ -962,85 +973,34 @@ def apply_mouth_area( # print("Warning: Mouth cutout is invalid after resize attempt.") return frame - # --- Color Correction Step --- - # Apply color transfer from ROI (swapped face region) to the original mouth cutout - # This helps match lighting/color before blending - color_corrected_mouth = resized_mouth_cutout # Default to resized if correction fails - try: - # Ensure both images are 3 channels for color transfer - if len(resized_mouth_cutout.shape) == 3 and resized_mouth_cutout.shape[2] == 3 and \ - len(roi.shape) == 3 and roi.shape[2] == 3: - color_corrected_mouth = apply_color_transfer(resized_mouth_cutout, roi) - else: - # print("Warning: Cannot apply color transfer, images not BGR.") - pass - except cv2.error as ct_e: # Handle potential errors in color transfer - # print(f"Warning: Color transfer failed: {ct_e}. Using uncorrected mouth cutout.") # Optional debug - pass - except Exception as ct_gen_e: - # print(f"Warning: Unexpected error during color transfer: {ct_gen_e}") - pass - # --- End Color Correction --- - - # --- Mask Creation --- - # Create a mask based *specifically* on the mouth_polygon, relative to the ROI + # Create a mask based on the mouth_polygon, relative to the ROI polygon_mask_roi = np.zeros(roi.shape[:2], dtype=np.uint8) - # Adjust polygon coordinates relative to the ROI's top-left corner adjusted_polygon = mouth_polygon - [min_x, min_y] - # Draw the filled polygon on the ROI mask cv2.fillPoly(polygon_mask_roi, [adjusted_polygon.astype(np.int32)], 255) - # Feather the polygon mask (Gaussian blur) - mask_feather_ratio = getattr(modules.globals, "mask_feather_ratio", 12) # Default 12 - # Calculate feather amount based on the smaller dimension of the box - feather_base_dim = min(box_width, box_height) - feather_amount = max(1, min(30, feather_base_dim // max(1, mask_feather_ratio))) # Avoid div by zero - # Ensure kernel size is odd and positive + # Feather the edges with Gaussian blur for smooth blending + feather_amount = max(1, min(30, min(box_width, box_height) // 8)) kernel_size = 2 * feather_amount + 1 - feathered_polygon_mask = cv2.GaussianBlur(polygon_mask_roi.astype(np.float32), (kernel_size, kernel_size), 0) + feathered_mask = cv2.GaussianBlur(polygon_mask_roi.astype(np.float32), (kernel_size, kernel_size), 0) - # Normalize feathered mask to [0.0, 1.0] range - max_val = feathered_polygon_mask.max() - if max_val > 1e-6: # Avoid division by zero - feathered_polygon_mask = feathered_polygon_mask / max_val + # Normalize to [0.0, 1.0] + max_val = feathered_mask.max() + if max_val > 1e-6: + feathered_mask = feathered_mask / max_val else: - feathered_polygon_mask.fill(0.0) # Mask is all black if max is near zero - # --- End Mask Creation --- + feathered_mask.fill(0.0) - - # --- Refined Blending --- - # Get the corresponding ROI from the *full face mask* (already blurred) - # Ensure face_mask is float and normalized [0.0, 1.0] - if face_mask.dtype != np.float64 and face_mask.dtype != np.float32: - face_mask_float = face_mask.astype(np.float32) / 255.0 - else: # Assume already float [0,1] if type is float - face_mask_float = face_mask.astype(np.float32) if face_mask.dtype == np.float64 else face_mask - face_mask_roi = face_mask_float[min_y:max_y, min_x:max_x] - - # Combine the feathered mouth polygon mask with the face mask ROI - # Use minimum to ensure we only affect area inside both masks (mouth area within face) - # This helps blend the edges smoothly with the surrounding swapped face region - combined_mask = np.minimum(feathered_polygon_mask, face_mask_roi) - - # Expand mask to 3 channels for blending (ensure it matches image channels) + # --- Blending: paste original mouth onto swapped face --- if len(frame.shape) == 3 and frame.shape[2] == 3: - combined_mask_3channel = combined_mask[:, :, np.newaxis] + mask_3ch = feathered_mask[:, :, np.newaxis].astype(np.float32) + inv_mask = 1.0 - mask_3ch - # Ensure data types are compatible for blending - # float32 provides sufficient precision for 8-bit image blending - combined_mask_f32 = combined_mask_3channel.astype(np.float32) - inv_mask = np.float32(1.0) - combined_mask_f32 + # Blend: (original_mouth * mask) + (swapped_face * (1 - mask)) + blended_roi = (resized_mouth_cutout.astype(np.float32) * mask_3ch + + roi.astype(np.float32) * inv_mask) - # Blend: (original_mouth * combined_mask) + (swapped_face_roi * (1 - combined_mask)) - blended_roi = (color_corrected_mouth * combined_mask_f32 + - roi * inv_mask) - - # Place the blended ROI back into the frame - frame[min_y:max_y, min_x:max_x] = blended_roi.astype(np.uint8) - else: - # print("Warning: Cannot apply mouth mask blending, frame is not 3-channel BGR.") - pass # Don't modify frame if it's not BGR + frame[min_y:max_y, min_x:max_x] = np.clip(blended_roi, 0, 255).astype(np.uint8) except Exception as e: print(f"Error applying mouth area: {e}") # Optional debug diff --git a/modules/ui.py b/modules/ui.py index 4c6e23b..1a4c7ca 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -135,6 +135,7 @@ def save_switch_states(): "show_fps": modules.globals.show_fps, "mouth_mask": modules.globals.mouth_mask, "show_mouth_mask_box": modules.globals.show_mouth_mask_box, + "mouth_mask_size": modules.globals.mouth_mask_size, } with open("switch_states.json", "w") as f: json.dump(switch_states, f) @@ -156,10 +157,10 @@ def load_switch_states(): modules.globals.live_resizable = switch_states.get("live_resizable", False) modules.globals.fp_ui = switch_states.get("fp_ui", {"face_enhancer": False}) modules.globals.show_fps = switch_states.get("show_fps", False) - modules.globals.mouth_mask = switch_states.get("mouth_mask", False) - modules.globals.show_mouth_mask_box = switch_states.get( - "show_mouth_mask_box", False - ) + modules.globals.mouth_mask_size = switch_states.get("mouth_mask_size", 0.0) + # mouth_mask is driven by the slider: on if size > 0, off if 0 + modules.globals.mouth_mask = modules.globals.mouth_mask_size > 0 + modules.globals.show_mouth_mask_box = False # always start hidden except FileNotFoundError: # If the file doesn't exist, use default values pass @@ -220,7 +221,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - keep_fps_checkbox.place(relx=0.1, rely=0.5) + keep_fps_checkbox.place(relx=0.1, rely=0.42) ToolTip(keep_fps_checkbox, _("Output video keeps the original frame rate")) keep_frames_value = ctk.BooleanVar(value=modules.globals.keep_frames) @@ -234,51 +235,9 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - keep_frames_switch.place(relx=0.1, rely=0.55) + keep_frames_switch.place(relx=0.1, rely=0.47) ToolTip(keep_frames_switch, _("Keep extracted frames on disk after processing")) - enhancer_value = ctk.BooleanVar(value=modules.globals.fp_ui["face_enhancer"]) - enhancer_switch = ctk.CTkSwitch( - root, - text=_("Face Enhancer"), - variable=enhancer_value, - cursor="hand2", - command=lambda: ( - update_tumbler("face_enhancer", enhancer_value.get()), - save_switch_states(), - ), - ) - enhancer_switch.place(relx=0.1, rely=0.6) - ToolTip(enhancer_switch, _("Improve face quality using the GFPGAN restoration model")) - - gpen256_value = ctk.BooleanVar(value=modules.globals.fp_ui.get("face_enhancer_gpen256", False)) - gpen256_switch = ctk.CTkSwitch( - root, - text=_("GPEN Enhancer 256"), - variable=gpen256_value, - cursor="hand2", - command=lambda: ( - update_tumbler("face_enhancer_gpen256", gpen256_value.get()), - save_switch_states(), - ), - ) - gpen256_switch.place(relx=0.1, rely=0.65) - ToolTip(gpen256_switch, _("Use GPEN face enhancement model at 256px resolution (faster)")) - - gpen512_value = ctk.BooleanVar(value=modules.globals.fp_ui.get("face_enhancer_gpen512", False)) - gpen512_switch = ctk.CTkSwitch( - root, - text=_("GPEN Enhancer 512"), - variable=gpen512_value, - cursor="hand2", - command=lambda: ( - update_tumbler("face_enhancer_gpen512", gpen512_value.get()), - save_switch_states(), - ), - ) - gpen512_switch.place(relx=0.1, rely=0.7) - ToolTip(gpen512_switch, _("Use GPEN face enhancement model at 512px resolution (higher quality)")) - keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio) keep_audio_switch = ctk.CTkSwitch( root, @@ -290,7 +249,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - keep_audio_switch.place(relx=0.6, rely=0.5) + keep_audio_switch.place(relx=0.6, rely=0.42) ToolTip(keep_audio_switch, _("Copy audio track from the source video to output")) many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces) @@ -304,7 +263,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - many_faces_switch.place(relx=0.6, rely=0.55) + many_faces_switch.place(relx=0.6, rely=0.47) ToolTip(many_faces_switch, _("Swap every detected face, not just the primary one")) color_correction_value = ctk.BooleanVar(value=modules.globals.color_correction) @@ -318,7 +277,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - color_correction_switch.place(relx=0.6, rely=0.6) + color_correction_switch.place(relx=0.6, rely=0.57) ToolTip(color_correction_switch, _("Fix blue/green color cast from some webcams")) # nsfw_value = ctk.BooleanVar(value=modules.globals.nsfw_filter) @@ -337,7 +296,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C close_mapper_window() if not map_faces.get() else None ), ) - map_faces_switch.place(relx=0.1, rely=0.75) + map_faces_switch.place(relx=0.1, rely=0.52) ToolTip(map_faces_switch, _("Manually assign which source face maps to which target face")) poisson_blend_value = ctk.BooleanVar(value=modules.globals.poisson_blend) @@ -351,7 +310,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - poisson_blend_switch.place(relx=0.1, rely=0.8) + poisson_blend_switch.place(relx=0.1, rely=0.57) ToolTip(poisson_blend_switch, _("Blend face edges smoothly using Poisson blending")) show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps) @@ -365,54 +324,34 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - show_fps_switch.place(relx=0.6, rely=0.65) + show_fps_switch.place(relx=0.6, rely=0.52) ToolTip(show_fps_switch, _("Display frames-per-second counter on the live preview")) + # mouth_mask and show_mouth_mask_box are auto-controlled by the Mouth Mask slider mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask) - mouth_mask_switch = ctk.CTkSwitch( - root, - text=_("Mouth Mask"), - variable=mouth_mask_var, - cursor="hand2", - command=lambda: setattr(modules.globals, "mouth_mask", mouth_mask_var.get()), - ) - mouth_mask_switch.place(relx=0.1, rely=0.45) - ToolTip(mouth_mask_switch, _("Preserve original mouth movement in the swapped face")) - show_mouth_mask_box_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box) - show_mouth_mask_box_switch = ctk.CTkSwitch( - root, - text=_("Show Mouth Mask Box"), - variable=show_mouth_mask_box_var, - cursor="hand2", - command=lambda: setattr( - modules.globals, "show_mouth_mask_box", show_mouth_mask_box_var.get() - ), - ) - show_mouth_mask_box_switch.place(relx=0.6, rely=0.45) - ToolTip(show_mouth_mask_box_switch, _("Display the mouth mask boundary for debugging")) start_button = ctk.CTkButton( root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root) ) - start_button.place(relx=0.15, rely=0.86, relwidth=0.2, relheight=0.05) + start_button.place(relx=0.15, rely=0.78, relwidth=0.2, relheight=0.04) ToolTip(start_button, _("Begin processing the target image/video with selected face")) stop_button = ctk.CTkButton( root, text=_("Destroy"), cursor="hand2", command=lambda: destroy() ) - stop_button.place(relx=0.4, rely=0.86, relwidth=0.2, relheight=0.05) + stop_button.place(relx=0.4, rely=0.78, relwidth=0.2, relheight=0.04) ToolTip(stop_button, _("Stop processing and close the application")) preview_button = ctk.CTkButton( root, text=_("Preview"), cursor="hand2", command=lambda: toggle_preview() ) - preview_button.place(relx=0.65, rely=0.86, relwidth=0.2, relheight=0.05) + preview_button.place(relx=0.65, rely=0.78, relwidth=0.2, relheight=0.04) ToolTip(preview_button, _("Show/hide a preview of the processed output")) # --- Camera Selection --- camera_label = ctk.CTkLabel(root, text=_("Select Camera:")) - camera_label.place(relx=0.1, rely=0.92, relwidth=0.2, relheight=0.05) + camera_label.place(relx=0.1, rely=0.83, relwidth=0.2, relheight=0.03) available_cameras = get_available_cameras() camera_indices, camera_names = available_cameras @@ -431,7 +370,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C root, variable=camera_variable, values=camera_names ) - camera_optionmenu.place(relx=0.35, rely=0.92, relwidth=0.25, relheight=0.05) + camera_optionmenu.place(relx=0.35, rely=0.83, relwidth=0.25, relheight=0.03) ToolTip(camera_optionmenu, _("Select which camera to use for live mode")) live_button = ctk.CTkButton( @@ -452,10 +391,52 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C else "disabled" ), ) - live_button.place(relx=0.65, rely=0.92, relwidth=0.2, relheight=0.05) + live_button.place(relx=0.65, rely=0.83, relwidth=0.2, relheight=0.03) ToolTip(live_button, _("Start real-time face swap using webcam")) # --- End Camera Selection --- + # --- Face Enhancer Dropdown --- + enhancer_options = ["None", "GFPGAN", "GPEN-512", "GPEN-256"] + enhancer_key_map = { + "None": None, + "GFPGAN": "face_enhancer", + "GPEN-512": "face_enhancer_gpen512", + "GPEN-256": "face_enhancer_gpen256", + } + + # Determine initial value from current fp_ui state + initial_enhancer = "None" + if modules.globals.fp_ui.get("face_enhancer", False): + initial_enhancer = "GFPGAN" + elif modules.globals.fp_ui.get("face_enhancer_gpen512", False): + initial_enhancer = "GPEN-512" + elif modules.globals.fp_ui.get("face_enhancer_gpen256", False): + initial_enhancer = "GPEN-256" + + enhancer_variable = ctk.StringVar(value=initial_enhancer) + + def on_enhancer_change(choice: str): + # Disable all enhancers first + for key in ["face_enhancer", "face_enhancer_gpen256", "face_enhancer_gpen512"]: + update_tumbler(key, False) + # Enable the selected one + selected_key = enhancer_key_map.get(choice) + if selected_key: + update_tumbler(selected_key, True) + save_switch_states() + + enhancer_label = ctk.CTkLabel(root, text="Face Enhancer:") + enhancer_label.place(relx=0.1, rely=0.62, relwidth=0.2, relheight=0.03) + + enhancer_dropdown = ctk.CTkOptionMenu( + root, + variable=enhancer_variable, + values=enhancer_options, + command=on_enhancer_change, + ) + enhancer_dropdown.place(relx=0.35, rely=0.62, relwidth=0.3, relheight=0.03) + ToolTip(enhancer_dropdown, _("Select a face enhancement model (None = no enhancement)")) + # 1) Define a DoubleVar for transparency (0 = fully transparent, 1 = fully opaque) transparency_var = ctk.DoubleVar(value=1.0) @@ -475,9 +456,9 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C modules.globals.face_swapper_enabled = True update_status(f"Transparency set to {percentage}%") - # 2) Transparency label and slider (placed ABOVE sharpness) + # 2) Transparency label and slider transparency_label = ctk.CTkLabel(root, text="Transparency:") - transparency_label.place(relx=0.15, rely=0.75, relwidth=0.2, relheight=0.05) + transparency_label.place(relx=0.15, rely=0.66, relwidth=0.2, relheight=0.03) transparency_slider = ctk.CTkSlider( root, @@ -493,7 +474,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C border_width=1, corner_radius=3, ) - transparency_slider.place(relx=0.35, rely=0.77, relwidth=0.5, relheight=0.02) + transparency_slider.place(relx=0.35, rely=0.67, relwidth=0.5, relheight=0.02) ToolTip(transparency_slider, _("Blend between original and swapped face (0% = original, 100% = fully swapped)")) # 3) Sharpness label & slider @@ -503,7 +484,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C update_status(f"Sharpness set to {value:.1f}") sharpness_label = ctk.CTkLabel(root, text="Sharpness:") - sharpness_label.place(relx=0.15, rely=0.80, relwidth=0.2, relheight=0.05) + sharpness_label.place(relx=0.15, rely=0.69, relwidth=0.2, relheight=0.03) sharpness_slider = ctk.CTkSlider( root, @@ -519,18 +500,64 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C border_width=1, corner_radius=3, ) - sharpness_slider.place(relx=0.35, rely=0.82, relwidth=0.5, relheight=0.02) + sharpness_slider.place(relx=0.35, rely=0.70, relwidth=0.5, relheight=0.02) ToolTip(sharpness_slider, _("Sharpen the enhanced face output")) + # 4) Mouth Mask Size slider + mouth_mask_size_var = ctk.DoubleVar(value=modules.globals.mouth_mask_size) + + def on_mouth_mask_size_change(value: float): + val = float(value) + modules.globals.mouth_mask_size = val + # Auto-enable/disable mouth mask based on slider position + if val > 0: + modules.globals.mouth_mask = True + mouth_mask_var.set(True) + else: + modules.globals.mouth_mask = False + mouth_mask_var.set(False) + modules.globals.show_mouth_mask_box = False + + def on_mouth_mask_slider_release(event): + # Hide bounding box when user releases the slider + modules.globals.show_mouth_mask_box = False + + def on_mouth_mask_slider_press(event): + # Show bounding box while dragging + if modules.globals.mouth_mask_size > 0: + modules.globals.show_mouth_mask_box = True + + mouth_mask_size_label = ctk.CTkLabel(root, text="Mouth Mask:") + mouth_mask_size_label.place(relx=0.15, rely=0.72, relwidth=0.2, relheight=0.03) + + mouth_mask_size_slider = ctk.CTkSlider( + root, + from_=0.0, + to=100.0, + variable=mouth_mask_size_var, + command=on_mouth_mask_size_change, + fg_color="#E0E0E0", + progress_color="#007BFF", + button_color="#FFFFFF", + button_hover_color="#CCCCCC", + height=5, + border_width=1, + corner_radius=3, + ) + mouth_mask_size_slider.place(relx=0.35, rely=0.73, relwidth=0.5, relheight=0.02) + mouth_mask_size_slider.bind("", on_mouth_mask_slider_press) + mouth_mask_size_slider.bind("", on_mouth_mask_slider_release) + ToolTip(mouth_mask_size_slider, _("0 = use swapped mouth, 100 = expose original mouth to chin area")) + # Status and link at the bottom global status_label status_label = ctk.CTkLabel(root, text=None, justify="center") - status_label.place(relx=0.1, rely=0.96, relwidth=0.8) + status_label.place(relx=0.1, rely=0.75, relwidth=0.8) donate_label = ctk.CTkLabel( root, text="Deep Live Cam", justify="center", cursor="hand2" ) - donate_label.place(relx=0.1, rely=0.98, relwidth=0.8) + donate_label.place(relx=0.1, rely=0.87, relwidth=0.8) donate_label.configure( text_color=ctk.ThemeManager.theme.get("URL").get("text_color") )