From d8fc1ffa048b61b0e47561a1886da06c2ab15604 Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:58:15 +0530 Subject: [PATCH] Eyebrow Mask --- modules/globals.py | 2 + modules/processors/frame/face_swapper.py | 278 +++++++++++++++++++++++ modules/ui.py | 75 +++--- 3 files changed, 329 insertions(+), 26 deletions(-) diff --git a/modules/globals.py b/modules/globals.py index add0916..98eccf5 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -43,5 +43,7 @@ mask_down_size = 0.50 mask_size = 1 eyes_mask = False show_eyes_mask_box = False +eyebrows_mask = False +show_eyebrows_mask_box = False use_fake_face = False fake_face_path = None diff --git a/modules/processors/frame/face_swapper.py b/modules/processors/frame/face_swapper.py index cd6f926..cdfa7e6 100644 --- a/modules/processors/frame/face_swapper.py +++ b/modules/processors/frame/face_swapper.py @@ -111,6 +111,23 @@ def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame: swapped_frame, target_face, eyes_mask_data ) + if modules.globals.eyebrows_mask: + # Create the eyebrows mask + eyebrows_mask, eyebrows_cutout, eyebrows_box, eyebrows_polygon = ( + create_eyebrows_mask(target_face, temp_frame) + ) + + # Apply the eyebrows area + swapped_frame = apply_eyebrows_area( + swapped_frame, eyebrows_cutout, eyebrows_box, face_mask, eyebrows_polygon + ) + + if modules.globals.show_eyebrows_mask_box: + eyebrows_mask_data = (eyebrows_mask, eyebrows_cutout, eyebrows_box, eyebrows_polygon) + swapped_frame = draw_eyebrows_mask_visualization( + swapped_frame, target_face, eyebrows_mask_data + ) + return swapped_frame @@ -859,3 +876,264 @@ def draw_eyes_mask_visualization( return vis_frame return frame + + +def create_eyebrows_mask(face: Face, frame: Frame) -> (np.ndarray, np.ndarray, tuple, np.ndarray): + mask = np.zeros(frame.shape[:2], dtype=np.uint8) + eyebrows_cutout = None + landmarks = face.landmark_2d_106 + if landmarks is not None: + # Left eyebrow landmarks (97-105) and right eyebrow landmarks (43-51) + left_eyebrow = landmarks[97:105].astype(np.float32) + right_eyebrow = landmarks[43:51].astype(np.float32) + + # Calculate centers and dimensions for each eyebrow + left_center = np.mean(left_eyebrow, axis=0) + right_center = np.mean(right_eyebrow, axis=0) + + # Calculate bounding box with padding + all_points = np.vstack([left_eyebrow, right_eyebrow]) + min_x = np.min(all_points[:, 0]) - 25 + max_x = np.max(all_points[:, 0]) + 25 + min_y = np.min(all_points[:, 1]) - 20 + max_y = np.max(all_points[:, 1]) + 15 + + # Ensure coordinates are within frame bounds + min_x = max(0, int(min_x)) + min_y = max(0, int(min_y)) + max_x = min(frame.shape[1], int(max_x)) + max_y = min(frame.shape[0], int(max_y)) + + # Create mask for the eyebrows region + mask_roi = np.zeros((max_y - min_y, max_x - min_x), dtype=np.uint8) + + try: + # Convert points to local coordinates + left_local = left_eyebrow - [min_x, min_y] + right_local = right_eyebrow - [min_x, min_y] + + def create_curved_eyebrow(points): + if len(points) >= 5: + # Sort points by x-coordinate + sorted_idx = np.argsort(points[:, 0]) + sorted_points = points[sorted_idx] + + # Calculate dimensions + x_min, y_min = np.min(sorted_points, axis=0) + x_max, y_max = np.max(sorted_points, axis=0) + width = x_max - x_min + height = y_max - y_min + + # Create more points for smoother curve + num_points = 50 + x = np.linspace(x_min, x_max, num_points) + + # Fit cubic curve through points for more natural arch + coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 3) + y = np.polyval(coeffs, x) + + # Create points for top and bottom curves with varying offsets + top_offset = np.linspace(height * 0.4, height * 0.3, num_points) # Varying offset for more natural shape + bottom_offset = np.linspace(height * 0.2, height * 0.15, num_points) + + # Add some randomness to the offsets for more natural look + top_offset += np.random.normal(0, height * 0.02, num_points) + bottom_offset += np.random.normal(0, height * 0.01, num_points) + + # Smooth the offsets + top_offset = cv2.GaussianBlur(top_offset.reshape(-1, 1), (1, 3), 1).reshape(-1) + bottom_offset = cv2.GaussianBlur(bottom_offset.reshape(-1, 1), (1, 3), 1).reshape(-1) + + top_curve = y - top_offset + bottom_curve = y + bottom_offset + + # Create curved endpoints + end_points = 5 + start_curve = np.column_stack(( + np.linspace(x[0] - width * 0.05, x[0], end_points), + np.linspace(bottom_curve[0], top_curve[0], end_points) + )) + end_curve = np.column_stack(( + np.linspace(x[-1], x[-1] + width * 0.05, end_points), + np.linspace(bottom_curve[-1], top_curve[-1], end_points) + )) + + # Combine all points to form a smooth contour + contour_points = np.vstack([ + start_curve, + np.column_stack((x, top_curve)), + end_curve, + np.column_stack((x[::-1], bottom_curve[::-1])) + ]) + + # Add padding and smooth the shape + center = np.mean(contour_points, axis=0) + vectors = contour_points - center + padded_points = center + vectors * 1.2 # 20% padding + + # Convert to integer coordinates and draw + cv2.fillPoly(mask_roi, [padded_points.astype(np.int32)], 255) + + return padded_points + return points + + # Generate and draw eyebrow shapes + left_shape = create_curved_eyebrow(left_local) + right_shape = create_curved_eyebrow(right_local) + + # Apply multi-stage blurring for natural feathering + # First, strong Gaussian blur for initial softening + mask_roi = cv2.GaussianBlur(mask_roi, (21, 21), 7) + + # Second, medium blur for transition areas + mask_roi = cv2.GaussianBlur(mask_roi, (11, 11), 3) + + # Finally, light blur for fine details + mask_roi = cv2.GaussianBlur(mask_roi, (5, 5), 1) + + # Normalize mask values + mask_roi = cv2.normalize(mask_roi, None, 0, 255, cv2.NORM_MINMAX) + + # Place the mask ROI in the full-sized mask + mask[min_y:max_y, min_x:max_x] = mask_roi + + # Extract the masked area from the frame + eyebrows_cutout = frame[min_y:max_y, min_x:max_x].copy() + + # Combine points for visualization + eyebrows_polygon = np.vstack([ + left_shape + [min_x, min_y], + right_shape + [min_x, min_y] + ]).astype(np.int32) + + except Exception as e: + # Fallback to simple polygons if curve fitting fails + left_local = left_eyebrow - [min_x, min_y] + right_local = right_eyebrow - [min_x, min_y] + cv2.fillPoly(mask_roi, [left_local.astype(np.int32)], 255) + cv2.fillPoly(mask_roi, [right_local.astype(np.int32)], 255) + mask_roi = cv2.GaussianBlur(mask_roi, (21, 21), 7) + mask[min_y:max_y, min_x:max_x] = mask_roi + eyebrows_cutout = frame[min_y:max_y, min_x:max_x].copy() + eyebrows_polygon = np.vstack([left_eyebrow, right_eyebrow]).astype(np.int32) + + return mask, eyebrows_cutout, (min_x, min_y, max_x, max_y), eyebrows_polygon + + +def apply_eyebrows_area( + frame: np.ndarray, + eyebrows_cutout: np.ndarray, + eyebrows_box: tuple, + face_mask: np.ndarray, + eyebrows_polygon: np.ndarray, +) -> np.ndarray: + min_x, min_y, max_x, max_y = eyebrows_box + box_width = max_x - min_x + box_height = max_y - min_y + + if ( + eyebrows_cutout is None + or box_width is None + or box_height is None + or face_mask is None + or eyebrows_polygon is None + ): + return frame + + try: + resized_eyebrows_cutout = cv2.resize(eyebrows_cutout, (box_width, box_height)) + roi = frame[min_y:max_y, min_x:max_x] + + if roi.shape != resized_eyebrows_cutout.shape: + resized_eyebrows_cutout = cv2.resize( + resized_eyebrows_cutout, (roi.shape[1], roi.shape[0]) + ) + + color_corrected_eyebrows = apply_color_transfer(resized_eyebrows_cutout, roi) + + # Create mask for both eyebrows + polygon_mask = np.zeros(roi.shape[:2], dtype=np.uint8) + + # Split points for left and right eyebrows + mid_point = len(eyebrows_polygon) // 2 + left_points = eyebrows_polygon[:mid_point] - [min_x, min_y] + right_points = eyebrows_polygon[mid_point:] - [min_x, min_y] + + # Draw filled polygons + cv2.fillPoly(polygon_mask, [left_points], 255) + cv2.fillPoly(polygon_mask, [right_points], 255) + + # Apply strong initial feathering + polygon_mask = cv2.GaussianBlur(polygon_mask, (21, 21), 7) + + # Apply additional feathering + feather_amount = min( + 30, + box_width // modules.globals.mask_feather_ratio, + box_height // modules.globals.mask_feather_ratio, + ) + feathered_mask = cv2.GaussianBlur( + polygon_mask.astype(float), (0, 0), feather_amount + ) + feathered_mask = feathered_mask / feathered_mask.max() + + # Apply additional smoothing to the mask edges + feathered_mask = cv2.GaussianBlur(feathered_mask, (5, 5), 1) + + face_mask_roi = face_mask[min_y:max_y, min_x:max_x] + combined_mask = feathered_mask * (face_mask_roi / 255.0) + + combined_mask = combined_mask[:, :, np.newaxis] + blended = ( + color_corrected_eyebrows * combined_mask + roi * (1 - combined_mask) + ).astype(np.uint8) + + # Apply face mask to blended result + face_mask_3channel = ( + np.repeat(face_mask_roi[:, :, np.newaxis], 3, axis=2) / 255.0 + ) + final_blend = blended * face_mask_3channel + roi * (1 - face_mask_3channel) + + frame[min_y:max_y, min_x:max_x] = final_blend.astype(np.uint8) + except Exception as e: + pass + + return frame + + +def draw_eyebrows_mask_visualization( + frame: Frame, face: Face, eyebrows_mask_data: tuple +) -> Frame: + landmarks = face.landmark_2d_106 + if landmarks is not None and eyebrows_mask_data is not None: + mask, eyebrows_cutout, (min_x, min_y, max_x, max_y), eyebrows_polygon = eyebrows_mask_data + + vis_frame = frame.copy() + + # Ensure coordinates are within frame bounds + height, width = vis_frame.shape[:2] + min_x, min_y = max(0, min_x), max(0, min_y) + max_x, max_y = min(width, max_x), min(height, max_y) + + # Draw the eyebrows curves + mid_point = len(eyebrows_polygon) // 2 + left_points = eyebrows_polygon[:mid_point] + right_points = eyebrows_polygon[mid_point:] + + # Draw smooth curves with anti-aliasing + cv2.polylines(vis_frame, [left_points], True, (0, 255, 0), 2, cv2.LINE_AA) + cv2.polylines(vis_frame, [right_points], True, (0, 255, 0), 2, cv2.LINE_AA) + + # Add label + cv2.putText( + vis_frame, + "Eyebrows Mask", + (min_x, min_y - 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 255, 255), + 1, + ) + + return vis_frame + return frame diff --git a/modules/ui.py b/modules/ui.py index 30f1f53..c7c37cf 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -36,7 +36,7 @@ if platform.system() == "Windows": ROOT = None POPUP = None POPUP_LIVE = None -ROOT_HEIGHT = 700 +ROOT_HEIGHT = 730 ROOT_WIDTH = 600 PREVIEW = None @@ -167,20 +167,20 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C # Image Selection Area (Top) source_label = ctk.CTkLabel(root, text=None) - source_label.place(relx=0.1, rely=0.1, relwidth=0.3, relheight=0.25) + source_label.place(relx=0.1, rely=0.05, relwidth=0.3, relheight=0.25) target_label = ctk.CTkLabel(root, text=None) - target_label.place(relx=0.6, rely=0.1, relwidth=0.3, relheight=0.25) + target_label.place(relx=0.6, rely=0.05, relwidth=0.3, relheight=0.25) select_face_button = ctk.CTkButton( root, text=_("Select a face"), cursor="hand2", command=lambda: select_source_path() ) - select_face_button.place(relx=0.1, rely=0.4, relwidth=0.3, relheight=0.1) + select_face_button.place(relx=0.1, rely=0.35, relwidth=0.3, relheight=0.1) swap_faces_button = ctk.CTkButton( root, text="↔", cursor="hand2", command=lambda: swap_faces_paths() ) - swap_faces_button.place(relx=0.45, rely=0.4, relwidth=0.1, relheight=0.1) + swap_faces_button.place(relx=0.45, rely=0.35, relwidth=0.1, relheight=0.1) select_target_button = ctk.CTkButton( root, @@ -188,7 +188,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C cursor="hand2", command=lambda: select_target_path(), ) - select_target_button.place(relx=0.6, rely=0.4, relwidth=0.3, relheight=0.1) + select_target_button.place(relx=0.6, rely=0.35, relwidth=0.3, relheight=0.1) # AI Generated Face controls fake_face_value = ctk.BooleanVar(value=modules.globals.use_fake_face) @@ -199,7 +199,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C cursor="hand2", command=lambda: toggle_fake_face(fake_face_value) ) - fake_face_switch.place(relx=0.1, rely=0.55) + fake_face_switch.place(relx=0.1, rely=0.50) # Add refresh button next to the switch refresh_face_button = ctk.CTkButton( @@ -209,7 +209,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C cursor="hand2", command=lambda: refresh_fake_face_clicked() ) - refresh_face_button.place(relx=0.35, rely=0.55) + refresh_face_button.place(relx=0.35, rely=0.50) # Face Processing Options (Middle Left) many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces) @@ -223,7 +223,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - many_faces_switch.place(relx=0.1, rely=0.60) + many_faces_switch.place(relx=0.1, rely=0.55) map_faces = ctk.BooleanVar(value=modules.globals.map_faces) map_faces_switch = ctk.CTkSwitch( @@ -237,7 +237,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.65) + map_faces_switch.place(relx=0.1, rely=0.60) enhancer_value = ctk.BooleanVar(value=modules.globals.fp_ui["face_enhancer"]) enhancer_switch = ctk.CTkSwitch( @@ -250,7 +250,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - enhancer_switch.place(relx=0.1, rely=0.70) + enhancer_switch.place(relx=0.1, rely=0.65) keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio) keep_audio_switch = ctk.CTkSwitch( @@ -263,7 +263,21 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - keep_audio_switch.place(relx=0.1, rely=0.75) + keep_audio_switch.place(relx=0.1, rely=0.70) + + # Add show FPS switch right after keep_audio_switch + show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps) + show_fps_switch = ctk.CTkSwitch( + root, + text=_("Show FPS"), + variable=show_fps_value, + cursor="hand2", + command=lambda: ( + setattr(modules.globals, "show_fps", show_fps_value.get()), + save_switch_states(), + ), + ) + show_fps_switch.place(relx=0.1, rely=0.75) # Additional Options (Middle Right) mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask) @@ -274,7 +288,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C cursor="hand2", command=lambda: setattr(modules.globals, "mouth_mask", mouth_mask_var.get()), ) - mouth_mask_switch.place(relx=0.6, rely=0.55) + mouth_mask_switch.place(relx=0.6, rely=0.50) show_mouth_mask_box_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box) show_mouth_mask_box_switch = ctk.CTkSwitch( @@ -286,7 +300,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C modules.globals, "show_mouth_mask_box", show_mouth_mask_box_var.get() ), ) - show_mouth_mask_box_switch.place(relx=0.6, rely=0.60) + show_mouth_mask_box_switch.place(relx=0.6, rely=0.55) # Add eyes mask switch eyes_mask_var = ctk.BooleanVar(value=modules.globals.eyes_mask) @@ -297,7 +311,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C cursor="hand2", command=lambda: setattr(modules.globals, "eyes_mask", eyes_mask_var.get()), ) - eyes_mask_switch.place(relx=0.6, rely=0.65) + eyes_mask_switch.place(relx=0.6, rely=0.60) # Add show eyes mask box switch show_eyes_mask_box_var = ctk.BooleanVar(value=modules.globals.show_eyes_mask_box) @@ -310,21 +324,30 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C modules.globals, "show_eyes_mask_box", show_eyes_mask_box_var.get() ), ) - show_eyes_mask_box_switch.place(relx=0.6, rely=0.70) + show_eyes_mask_box_switch.place(relx=0.6, rely=0.65) - # Add show FPS switch - show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps) - show_fps_switch = ctk.CTkSwitch( + # Move the eyebrows mask switches up slightly + eyebrows_mask_var = ctk.BooleanVar(value=modules.globals.eyebrows_mask) + eyebrows_mask_switch = ctk.CTkSwitch( root, - text=_("Show FPS"), - variable=show_fps_value, + text=_("Eyebrows Mask"), + variable=eyebrows_mask_var, cursor="hand2", - command=lambda: ( - setattr(modules.globals, "show_fps", show_fps_value.get()), - save_switch_states(), + command=lambda: setattr(modules.globals, "eyebrows_mask", eyebrows_mask_var.get()), + ) + eyebrows_mask_switch.place(relx=0.6, rely=0.70) + + show_eyebrows_mask_box_var = ctk.BooleanVar(value=modules.globals.show_eyebrows_mask_box) + show_eyebrows_mask_box_switch = ctk.CTkSwitch( + root, + text=_("Show Eyebrows Mask Box"), + variable=show_eyebrows_mask_box_var, + cursor="hand2", + command=lambda: setattr( + modules.globals, "show_eyebrows_mask_box", show_eyebrows_mask_box_var.get() ), ) - show_fps_switch.place(relx=0.6, rely=0.75) + show_eyebrows_mask_box_switch.place(relx=0.6, rely=0.75) # Main Control Buttons (Bottom) start_button = ctk.CTkButton( @@ -392,7 +415,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C donate_label = ctk.CTkLabel( root, text="Deep Live Cam", justify="center", cursor="hand2" ) - donate_label.place(relx=0.1, rely=0.95, relwidth=0.8) + donate_label.place(relx=0.1, rely=0.94, relwidth=0.8) donate_label.configure( text_color=ctk.ThemeManager.theme.get("URL").get("text_color") )