From 3c7dd1a57467fe837b303d4e1e6a94b955162d27 Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:45:43 +0530 Subject: [PATCH 01/11] Hardcoded Keep FPS and Keep Frames --- .python-version | 1 + modules/core.py | 6 +-- modules/ui.py | 132 +++++++++++++++--------------------------------- 3 files changed, 43 insertions(+), 96 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..30291cba --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10.0 diff --git a/modules/core.py b/modules/core.py index b6ef9b8b..c0c25f1e 100644 --- a/modules/core.py +++ b/modules/core.py @@ -35,9 +35,7 @@ def parse_args() -> None: program.add_argument('-t', '--target', help='select an target image or video', dest='target_path') program.add_argument('-o', '--output', help='select output file or directory', dest='output_path') program.add_argument('--frame-processor', help='pipeline of frame processors', dest='frame_processor', default=['face_swapper'], choices=['face_swapper', 'face_enhancer'], nargs='+') - program.add_argument('--keep-fps', help='keep original fps', dest='keep_fps', action='store_true', default=False) program.add_argument('--keep-audio', help='keep original audio', dest='keep_audio', action='store_true', default=True) - program.add_argument('--keep-frames', help='keep temporary frames', dest='keep_frames', action='store_true', default=False) program.add_argument('--many-faces', help='process every face', dest='many_faces', action='store_true', default=False) program.add_argument('--nsfw-filter', help='filter the NSFW image or video', dest='nsfw_filter', action='store_true', default=False) program.add_argument('--map-faces', help='map source target faces', dest='map_faces', action='store_true', default=False) @@ -65,9 +63,9 @@ def parse_args() -> None: modules.globals.output_path = normalize_output_path(modules.globals.source_path, modules.globals.target_path, args.output_path) modules.globals.frame_processors = args.frame_processor modules.globals.headless = args.source_path or args.target_path or args.output_path - modules.globals.keep_fps = args.keep_fps + modules.globals.keep_fps = True + modules.globals.keep_frames = True modules.globals.keep_audio = args.keep_audio - modules.globals.keep_frames = args.keep_frames modules.globals.many_faces = args.many_faces modules.globals.mouth_mask = args.mouth_mask modules.globals.nsfw_filter = args.nsfw_filter diff --git a/modules/ui.py b/modules/ui.py index 8eb92896..c491dac9 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -151,6 +151,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C root.configure() root.protocol("WM_DELETE_WINDOW", lambda: destroy()) + # 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) @@ -175,58 +176,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) select_target_button.place(relx=0.6, rely=0.4, relwidth=0.3, relheight=0.1) - keep_fps_value = ctk.BooleanVar(value=modules.globals.keep_fps) - keep_fps_checkbox = ctk.CTkSwitch( - root, - text=_("Keep fps"), - variable=keep_fps_value, - cursor="hand2", - command=lambda: ( - setattr(modules.globals, "keep_fps", keep_fps_value.get()), - save_switch_states(), - ), - ) - keep_fps_checkbox.place(relx=0.1, rely=0.6) - - keep_frames_value = ctk.BooleanVar(value=modules.globals.keep_frames) - keep_frames_switch = ctk.CTkSwitch( - root, - text=_("Keep frames"), - variable=keep_frames_value, - cursor="hand2", - command=lambda: ( - setattr(modules.globals, "keep_frames", keep_frames_value.get()), - save_switch_states(), - ), - ) - keep_frames_switch.place(relx=0.1, rely=0.65) - - 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.7) - - keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio) - keep_audio_switch = ctk.CTkSwitch( - root, - text=_("Keep audio"), - variable=keep_audio_value, - cursor="hand2", - command=lambda: ( - setattr(modules.globals, "keep_audio", keep_audio_value.get()), - save_switch_states(), - ), - ) - keep_audio_switch.place(relx=0.6, rely=0.6) - + # Face Processing Options (Middle Left) many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces) many_faces_switch = ctk.CTkSwitch( root, @@ -238,24 +188,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - many_faces_switch.place(relx=0.6, rely=0.65) - - color_correction_value = ctk.BooleanVar(value=modules.globals.color_correction) - color_correction_switch = ctk.CTkSwitch( - root, - text=_("Fix Blueish Cam"), - variable=color_correction_value, - cursor="hand2", - command=lambda: ( - setattr(modules.globals, "color_correction", color_correction_value.get()), - save_switch_states(), - ), - ) - color_correction_switch.place(relx=0.6, rely=0.70) - - # nsfw_value = ctk.BooleanVar(value=modules.globals.nsfw_filter) - # nsfw_switch = ctk.CTkSwitch(root, text='NSFW filter', variable=nsfw_value, cursor='hand2', command=lambda: setattr(modules.globals, 'nsfw_filter', nsfw_value.get())) - # nsfw_switch.place(relx=0.6, rely=0.7) + many_faces_switch.place(relx=0.1, rely=0.55) map_faces = ctk.BooleanVar(value=modules.globals.map_faces) map_faces_switch = ctk.CTkSwitch( @@ -269,21 +202,22 @@ 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.6) - show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps) - show_fps_switch = ctk.CTkSwitch( + enhancer_value = ctk.BooleanVar(value=modules.globals.fp_ui["face_enhancer"]) + enhancer_switch = ctk.CTkSwitch( root, - text=_("Show FPS"), - variable=show_fps_value, + text=_("Face Enhancer"), + variable=enhancer_value, cursor="hand2", command=lambda: ( - setattr(modules.globals, "show_fps", show_fps_value.get()), + update_tumbler("face_enhancer", enhancer_value.get()), save_switch_states(), ), ) - show_fps_switch.place(relx=0.6, rely=0.75) + enhancer_switch.place(relx=0.1, rely=0.65) + # Additional Options (Middle Right) mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask) mouth_mask_switch = ctk.CTkSwitch( root, @@ -292,7 +226,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.1, rely=0.55) + mouth_mask_switch.place(relx=0.6, rely=0.55) show_mouth_mask_box_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box) show_mouth_mask_box_switch = ctk.CTkSwitch( @@ -304,26 +238,40 @@ 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.55) + show_mouth_mask_box_switch.place(relx=0.6, rely=0.6) - start_button = ctk.CTkButton( - root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root) + keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio) + keep_audio_switch = ctk.CTkSwitch( + root, + text=_("Keep audio"), + variable=keep_audio_value, + cursor="hand2", + command=lambda: ( + setattr(modules.globals, "keep_audio", keep_audio_value.get()), + save_switch_states(), + ), ) - start_button.place(relx=0.15, rely=0.80, relwidth=0.2, relheight=0.05) + keep_audio_switch.place(relx=0.6, rely=0.65) - stop_button = ctk.CTkButton( - root, text=_("Destroy"), cursor="hand2", command=lambda: destroy() + # Main Control Buttons (Bottom) + start_button = ctk.CTkButton( + root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root) ) - stop_button.place(relx=0.4, rely=0.80, relwidth=0.2, relheight=0.05) + start_button.place(relx=0.15, rely=0.75, relwidth=0.2, relheight=0.05) preview_button = ctk.CTkButton( root, text=_("Preview"), cursor="hand2", command=lambda: toggle_preview() ) - preview_button.place(relx=0.65, rely=0.80, relwidth=0.2, relheight=0.05) + preview_button.place(relx=0.4, rely=0.75, relwidth=0.2, relheight=0.05) + + stop_button = ctk.CTkButton( + root, text=_("Destroy"), cursor="hand2", command=lambda: destroy() + ) + stop_button.place(relx=0.65, rely=0.75, relwidth=0.2, relheight=0.05) - # --- Camera Selection --- + # Camera Section (Bottom) camera_label = ctk.CTkLabel(root, text=_("Select Camera:")) - camera_label.place(relx=0.1, rely=0.86, relwidth=0.2, relheight=0.05) + camera_label.place(relx=0.1, rely=0.85, relwidth=0.2, relheight=0.05) available_cameras = get_available_cameras() camera_indices, camera_names = available_cameras @@ -342,7 +290,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.86, relwidth=0.25, relheight=0.05) + camera_optionmenu.place(relx=0.35, rely=0.85, relwidth=0.25, relheight=0.05) live_button = ctk.CTkButton( root, @@ -362,11 +310,11 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C else "disabled" ), ) - live_button.place(relx=0.65, rely=0.86, relwidth=0.2, relheight=0.05) - # --- End Camera Selection --- + live_button.place(relx=0.65, rely=0.85, relwidth=0.2, relheight=0.05) + # Status and Links (Bottom) status_label = ctk.CTkLabel(root, text=None, justify="center") - status_label.place(relx=0.1, rely=0.9, relwidth=0.8) + status_label.place(relx=0.1, rely=0.92, relwidth=0.8) donate_label = ctk.CTkLabel( root, text="Deep Live Cam", justify="center", cursor="hand2" From 8055d79daf5a6477f4f7c7daab8dee2981600df9 Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:10:45 +0530 Subject: [PATCH 02/11] Privacy Mode --- modules/core.py | 2 + modules/deeplivecam.ico | Bin 0 -> 270622 bytes modules/fake_face_handler.py | 120 ++++++++++++++++++++++++++++ modules/globals.py | 2 + modules/ui.py | 150 ++++++++++++++++++++++++----------- 5 files changed, 227 insertions(+), 47 deletions(-) create mode 100644 modules/deeplivecam.ico create mode 100644 modules/fake_face_handler.py diff --git a/modules/core.py b/modules/core.py index c0c25f1e..cb296436 100644 --- a/modules/core.py +++ b/modules/core.py @@ -20,6 +20,7 @@ import modules.ui as ui from modules.processors.frame.core import get_frame_processors_modules from modules.utilities import has_image_extension, is_image, is_video, detect_fps, create_video, extract_frames, get_temp_frame_paths, restore_audio, create_temp, move_temp, clean_temp, normalize_output_path +from modules.fake_face_handler import cleanup_fake_face if 'ROCMExecutionProvider' in modules.globals.execution_providers: del torch @@ -239,6 +240,7 @@ def start() -> None: def destroy(to_quit=True) -> None: if modules.globals.target_path: clean_temp(modules.globals.target_path) + cleanup_fake_face() if to_quit: quit() diff --git a/modules/deeplivecam.ico b/modules/deeplivecam.ico new file mode 100644 index 0000000000000000000000000000000000000000..f550109a1719d538c869c0f8b1d941f75cf36df9 GIT binary patch literal 270622 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 str: + """Fetch a face from thispersondoesnotexist.com and save it temporarily""" + try: + # Create temp directory if it doesn't exist + temp_dir = Path(tempfile.gettempdir()) / "deep-live-cam" + temp_dir.mkdir(parents=True, exist_ok=True) + + # Generate temp file path + temp_file = temp_dir / "fake_face.jpg" + + # Basic headers to mimic a browser request + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + + # Fetch the image + response = requests.get('https://thispersondoesnotexist.com', headers=headers) + + if response.status_code == 200: + # Read image from response + image_array = np.asarray(bytearray(response.content), dtype=np.uint8) + image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + + # Add padding around the face + padded_image = add_padding_to_face(image) + + # Save the padded image + cv2.imwrite(str(temp_file), padded_image) + return str(temp_file) + else: + print(f"Failed to fetch fake face: {response.status_code}") + return None + except Exception as e: + print(f"Error fetching fake face: {str(e)}") + return None + +def cleanup_fake_face(): + """Clean up the temporary fake face image""" + try: + if modules.globals.fake_face_path and os.path.exists(modules.globals.fake_face_path): + os.remove(modules.globals.fake_face_path) + modules.globals.fake_face_path = None + except Exception as e: + print(f"Error cleaning up fake face: {str(e)}") + +def refresh_fake_face(): + """Refresh the fake face image""" + cleanup_fake_face() + modules.globals.fake_face_path = get_fake_face() + return modules.globals.fake_face_path is not None \ No newline at end of file diff --git a/modules/globals.py b/modules/globals.py index 693084d5..9bd9f4cd 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -41,3 +41,5 @@ mask_feather_ratio = 8 mask_down_size = 0.50 mask_size = 1 +use_fake_face = False +fake_face_path = None diff --git a/modules/ui.py b/modules/ui.py index c491dac9..7a14f687 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -28,6 +28,7 @@ from modules.video_capture import VideoCapturer from modules.gettext import LanguageManager import platform +from modules.fake_face_handler import cleanup_fake_face, refresh_fake_face if platform.system() == "Windows": from pygrabber.dshow_graph import FilterGraph @@ -91,47 +92,52 @@ def init(start: Callable[[], None], destroy: Callable[[], None], lang: str) -> c def save_switch_states(): - switch_states = { - "keep_fps": modules.globals.keep_fps, - "keep_audio": modules.globals.keep_audio, - "keep_frames": modules.globals.keep_frames, - "many_faces": modules.globals.many_faces, - "map_faces": modules.globals.map_faces, - "color_correction": modules.globals.color_correction, - "nsfw_filter": modules.globals.nsfw_filter, - "live_mirror": modules.globals.live_mirror, - "live_resizable": modules.globals.live_resizable, - "fp_ui": modules.globals.fp_ui, - "show_fps": modules.globals.show_fps, - "mouth_mask": modules.globals.mouth_mask, - "show_mouth_mask_box": modules.globals.show_mouth_mask_box, - } - with open("switch_states.json", "w") as f: - json.dump(switch_states, f) + try: + states = { + "keep_fps": modules.globals.keep_fps, + "keep_audio": modules.globals.keep_audio, + "keep_frames": modules.globals.keep_frames, + "many_faces": modules.globals.many_faces, + "map_faces": modules.globals.map_faces, + "color_correction": modules.globals.color_correction, + "nsfw_filter": modules.globals.nsfw_filter, + "live_mirror": modules.globals.live_mirror, + "live_resizable": modules.globals.live_resizable, + "fp_ui": modules.globals.fp_ui, + "show_fps": modules.globals.show_fps, + "mouth_mask": modules.globals.mouth_mask, + "show_mouth_mask_box": modules.globals.show_mouth_mask_box, + "use_fake_face": modules.globals.use_fake_face + } + with open(get_config_path(), 'w') as f: + json.dump(states, f) + except Exception as e: + print(f"Error saving switch states: {str(e)}") def load_switch_states(): try: - with open("switch_states.json", "r") as f: - switch_states = json.load(f) - modules.globals.keep_fps = switch_states.get("keep_fps", True) - modules.globals.keep_audio = switch_states.get("keep_audio", True) - modules.globals.keep_frames = switch_states.get("keep_frames", False) - modules.globals.many_faces = switch_states.get("many_faces", False) - modules.globals.map_faces = switch_states.get("map_faces", False) - modules.globals.color_correction = switch_states.get("color_correction", False) - modules.globals.nsfw_filter = switch_states.get("nsfw_filter", False) - modules.globals.live_mirror = switch_states.get("live_mirror", False) - 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 - ) - except FileNotFoundError: - # If the file doesn't exist, use default values - pass + if os.path.exists(get_config_path()): + with open(get_config_path(), 'r') as f: + states = json.load(f) + modules.globals.keep_fps = states.get("keep_fps", True) + modules.globals.keep_audio = states.get("keep_audio", True) + modules.globals.keep_frames = states.get("keep_frames", False) + modules.globals.many_faces = states.get("many_faces", False) + modules.globals.map_faces = states.get("map_faces", False) + modules.globals.color_correction = states.get("color_correction", False) + modules.globals.nsfw_filter = states.get("nsfw_filter", False) + modules.globals.live_mirror = states.get("live_mirror", False) + modules.globals.live_resizable = states.get("live_resizable", False) + modules.globals.fp_ui = states.get("fp_ui", {"face_enhancer": False}) + modules.globals.show_fps = states.get("show_fps", False) + modules.globals.mouth_mask = states.get("mouth_mask", False) + modules.globals.show_mouth_mask_box = states.get( + "show_mouth_mask_box", False + ) + modules.globals.use_fake_face = states.get('use_fake_face', False) + except Exception as e: + print(f"Error loading switch states: {str(e)}") def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.CTk: @@ -176,6 +182,27 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) select_target_button.place(relx=0.6, rely=0.4, relwidth=0.3, relheight=0.1) + # AI Generated Face controls + fake_face_value = ctk.BooleanVar(value=modules.globals.use_fake_face) + fake_face_switch = ctk.CTkSwitch( + root, + text=_("Privacy Mode"), + variable=fake_face_value, + cursor="hand2", + command=lambda: toggle_fake_face(fake_face_value) + ) + fake_face_switch.place(relx=0.1, rely=0.55) + + # Add refresh button next to the switch + refresh_face_button = ctk.CTkButton( + root, + text="↻", + width=30, + cursor="hand2", + command=lambda: refresh_fake_face_clicked() + ) + refresh_face_button.place(relx=0.35, rely=0.55) + # Face Processing Options (Middle Left) many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces) many_faces_switch = ctk.CTkSwitch( @@ -188,7 +215,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - many_faces_switch.place(relx=0.1, rely=0.55) + many_faces_switch.place(relx=0.1, rely=0.60) map_faces = ctk.BooleanVar(value=modules.globals.map_faces) map_faces_switch = ctk.CTkSwitch( @@ -202,7 +229,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.6) + map_faces_switch.place(relx=0.1, rely=0.65) enhancer_value = ctk.BooleanVar(value=modules.globals.fp_ui["face_enhancer"]) enhancer_switch = ctk.CTkSwitch( @@ -215,7 +242,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - enhancer_switch.place(relx=0.1, rely=0.65) + enhancer_switch.place(relx=0.1, rely=0.70) # Additional Options (Middle Right) mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask) @@ -257,21 +284,21 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C start_button = ctk.CTkButton( root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root) ) - start_button.place(relx=0.15, rely=0.75, relwidth=0.2, relheight=0.05) + start_button.place(relx=0.15, rely=0.80, relwidth=0.2, relheight=0.05) preview_button = ctk.CTkButton( root, text=_("Preview"), cursor="hand2", command=lambda: toggle_preview() ) - preview_button.place(relx=0.4, rely=0.75, relwidth=0.2, relheight=0.05) + preview_button.place(relx=0.4, rely=0.80, relwidth=0.2, relheight=0.05) stop_button = ctk.CTkButton( root, text=_("Destroy"), cursor="hand2", command=lambda: destroy() ) - stop_button.place(relx=0.65, rely=0.75, relwidth=0.2, relheight=0.05) + stop_button.place(relx=0.65, rely=0.80, relwidth=0.2, relheight=0.05) # Camera Section (Bottom) camera_label = ctk.CTkLabel(root, text=_("Select Camera:")) - camera_label.place(relx=0.1, rely=0.85, relwidth=0.2, relheight=0.05) + camera_label.place(relx=0.1, rely=0.87, relwidth=0.2, relheight=0.05) available_cameras = get_available_cameras() camera_indices, camera_names = available_cameras @@ -290,7 +317,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.85, relwidth=0.25, relheight=0.05) + camera_optionmenu.place(relx=0.35, rely=0.87, relwidth=0.25, relheight=0.05) live_button = ctk.CTkButton( root, @@ -310,7 +337,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C else "disabled" ), ) - live_button.place(relx=0.65, rely=0.85, relwidth=0.2, relheight=0.05) + live_button.place(relx=0.65, rely=0.87, relwidth=0.2, relheight=0.05) # Status and Links (Bottom) status_label = ctk.CTkLabel(root, text=None, justify="center") @@ -1147,4 +1174,33 @@ def update_webcam_target( target_label_dict_live[button_num] = target_image else: update_pop_live_status("Face could not be detected in last upload!") - return map \ No newline at end of file + return map + +def toggle_fake_face(switch_var: ctk.BooleanVar) -> None: + modules.globals.use_fake_face = switch_var.get() + if modules.globals.use_fake_face: + if not modules.globals.fake_face_path: + if refresh_fake_face(): + modules.globals.source_path = modules.globals.fake_face_path + # Update the source image preview + image = render_image_preview(modules.globals.source_path, (200, 200)) + source_label.configure(image=image) + else: + cleanup_fake_face() + # Clear the source image preview + source_label.configure(image=None) + modules.globals.source_path = None + +def refresh_fake_face_clicked() -> None: + if modules.globals.use_fake_face: + if refresh_fake_face(): + modules.globals.source_path = modules.globals.fake_face_path + # Update the source image preview + image = render_image_preview(modules.globals.source_path, (200, 200)) + source_label.configure(image=image) + +def get_config_path() -> str: + """Get the path to the config file""" + config_dir = os.path.join(os.path.expanduser("~"), ".deep-live-cam") + os.makedirs(config_dir, exist_ok=True) + return os.path.join(config_dir, "switch_states.json") \ No newline at end of file From f0c66732e77a31127911a9fea9042a31d0031311 Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:20:08 +0530 Subject: [PATCH 03/11] Use Logo --- modules/ui.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/ui.py b/modules/ui.py index 7a14f687..cd8f03e1 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -156,6 +156,11 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) root.configure() root.protocol("WM_DELETE_WINDOW", lambda: destroy()) + + # Add icon to the main window + icon_path = resolve_relative_path("deeplivecam.ico") + if os.path.exists(icon_path): + root.iconbitmap(icon_path) # Image Selection Area (Top) source_label = ctk.CTkLabel(root, text=None) @@ -519,6 +524,11 @@ def create_preview(parent: ctk.CTkToplevel) -> ctk.CTkToplevel: preview.configure() preview.protocol("WM_DELETE_WINDOW", lambda: toggle_preview()) preview.resizable(width=True, height=True) + + # Add icon to the preview window + icon_path = resolve_relative_path("deeplivecam.ico") + if os.path.exists(icon_path): + preview.iconbitmap(icon_path) preview_label = ctk.CTkLabel(preview, text=None) preview_label.pack(fill="both", expand=True) From ccb676ac175992d238c88dae66b3b2ac91398452 Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:41:44 +0530 Subject: [PATCH 04/11] FPS Switch re-enable --- modules/ui.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/modules/ui.py b/modules/ui.py index cd8f03e1..14e5fa54 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -285,6 +285,20 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) keep_audio_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( + 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.6, rely=0.70) + # Main Control Buttons (Bottom) start_button = ctk.CTkButton( root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root) @@ -1202,12 +1216,18 @@ def toggle_fake_face(switch_var: ctk.BooleanVar) -> None: modules.globals.source_path = None def refresh_fake_face_clicked() -> None: + """Handle refresh button click to update fake face during live preview""" if modules.globals.use_fake_face: if refresh_fake_face(): modules.globals.source_path = modules.globals.fake_face_path # Update the source image preview image = render_image_preview(modules.globals.source_path, (200, 200)) source_label.configure(image=image) + + # Force reload of frame processors to use new source face + global FRAME_PROCESSORS_MODULES + FRAME_PROCESSORS_MODULES = [] + frame_processors = get_frame_processors_modules(modules.globals.frame_processors) def get_config_path() -> str: """Get the path to the config file""" From 59cd3be0f9d0c4ee248bb160370543e844169376 Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Mon, 27 Jan 2025 00:47:34 +0530 Subject: [PATCH 05/11] Simplify Privacy mode switch state --- modules/ui.py | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/modules/ui.py b/modules/ui.py index 14e5fa54..8242d144 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -79,9 +79,12 @@ img_ft, vid_ft = modules.globals.file_types +fake_face_switch = None +fake_face_value = None + def init(start: Callable[[], None], destroy: Callable[[], None], lang: str) -> ctk.CTk: - global ROOT, PREVIEW, _ + global ROOT, PREVIEW, _, fake_face_switch, fake_face_value lang_manager = LanguageManager(lang) _ = lang_manager._ @@ -135,13 +138,13 @@ def load_switch_states(): modules.globals.show_mouth_mask_box = states.get( "show_mouth_mask_box", False ) - modules.globals.use_fake_face = states.get('use_fake_face', False) + modules.globals.use_fake_face = False except Exception as e: print(f"Error loading switch states: {str(e)}") def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.CTk: - global source_label, target_label, status_label, show_fps_switch + global source_label, target_label, status_label, show_fps_switch, fake_face_switch, fake_face_value load_switch_states() @@ -579,7 +582,7 @@ def update_tumbler(var: str, value: bool) -> None: def select_source_path() -> None: - global RECENT_DIRECTORY_SOURCE, img_ft, vid_ft + global RECENT_DIRECTORY_SOURCE, img_ft, vid_ft, fake_face_switch, fake_face_value PREVIEW.withdraw() source_path = ctk.filedialog.askopenfilename( @@ -588,6 +591,10 @@ def select_source_path() -> None: filetypes=[img_ft], ) if is_image(source_path): + modules.globals.use_fake_face = False + fake_face_value.set(False) + cleanup_fake_face() + modules.globals.source_path = source_path RECENT_DIRECTORY_SOURCE = os.path.dirname(modules.globals.source_path) image = render_image_preview(modules.globals.source_path, (200, 200)) @@ -1217,17 +1224,21 @@ def toggle_fake_face(switch_var: ctk.BooleanVar) -> None: def refresh_fake_face_clicked() -> None: """Handle refresh button click to update fake face during live preview""" - if modules.globals.use_fake_face: - if refresh_fake_face(): - modules.globals.source_path = modules.globals.fake_face_path - # Update the source image preview - image = render_image_preview(modules.globals.source_path, (200, 200)) - source_label.configure(image=image) - - # Force reload of frame processors to use new source face - global FRAME_PROCESSORS_MODULES - FRAME_PROCESSORS_MODULES = [] - frame_processors = get_frame_processors_modules(modules.globals.frame_processors) + if not modules.globals.use_fake_face: + # If privacy mode is off, turn it on first + modules.globals.use_fake_face = True + fake_face_value.set(True) + + if refresh_fake_face(): + modules.globals.source_path = modules.globals.fake_face_path + # Update the source image preview + image = render_image_preview(modules.globals.source_path, (200, 200)) + source_label.configure(image=image) + + # Force reload of frame processors to use new source face + global FRAME_PROCESSORS_MODULES + FRAME_PROCESSORS_MODULES = [] + frame_processors = get_frame_processors_modules(modules.globals.frame_processors) def get_config_path() -> str: """Get the path to the config file""" From 5dfd1c0ced39ad67ea7323df0459791e448b824e Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:20:58 +0530 Subject: [PATCH 06/11] Eye Mask --- modules/globals.py | 4 +- modules/processors/frame/face_swapper.py | 252 ++++++++++++++++++++++- modules/ui.py | 49 +++-- 3 files changed, 288 insertions(+), 17 deletions(-) diff --git a/modules/globals.py b/modules/globals.py index 9bd9f4cd..add09165 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -21,7 +21,7 @@ keep_frames = False many_faces = False map_faces = False -color_correction = False # New global variable for color correction toggle +color_correction = False nsfw_filter = False video_encoder = None video_quality = None @@ -41,5 +41,7 @@ mask_feather_ratio = 8 mask_down_size = 0.50 mask_size = 1 +eyes_mask = False +show_eyes_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 c1883939..cd6f9260 100644 --- a/modules/processors/frame/face_swapper.py +++ b/modules/processors/frame/face_swapper.py @@ -74,10 +74,10 @@ def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame: temp_frame, target_face, source_face, paste_back=True ) - if modules.globals.mouth_mask: - # Create a mask for the target face - face_mask = create_face_mask(target_face, temp_frame) + # Create face mask for both mouth and eyes masking + face_mask = create_face_mask(target_face, temp_frame) + if modules.globals.mouth_mask: # Create the mouth mask mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon = ( create_lower_mouth_mask(target_face, temp_frame) @@ -94,6 +94,23 @@ def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame: swapped_frame, target_face, mouth_mask_data ) + if modules.globals.eyes_mask: + # Create the eyes mask + eyes_mask, eyes_cutout, eyes_box, eyes_polygon = ( + create_eyes_mask(target_face, temp_frame) + ) + + # Apply the eyes area + swapped_frame = apply_eyes_area( + swapped_frame, eyes_cutout, eyes_box, face_mask, eyes_polygon + ) + + if modules.globals.show_eyes_mask_box: + eyes_mask_data = (eyes_mask, eyes_cutout, eyes_box, eyes_polygon) + swapped_frame = draw_eyes_mask_visualization( + swapped_frame, target_face, eyes_mask_data + ) + return swapped_frame @@ -613,3 +630,232 @@ def apply_color_transfer(source, target): source = (source - source_mean) * (target_std / source_std) + target_mean return cv2.cvtColor(np.clip(source, 0, 255).astype("uint8"), cv2.COLOR_LAB2BGR) + + +def create_eyes_mask(face: Face, frame: Frame) -> (np.ndarray, np.ndarray, tuple, np.ndarray): + mask = np.zeros(frame.shape[:2], dtype=np.uint8) + eyes_cutout = None + landmarks = face.landmark_2d_106 + if landmarks is not None: + # Left eye landmarks (87-96) and right eye landmarks (33-42) + left_eye = landmarks[87:96] + right_eye = landmarks[33:42] + + # Calculate centers and dimensions for each eye + left_eye_center = np.mean(left_eye, axis=0).astype(np.int32) + right_eye_center = np.mean(right_eye, axis=0).astype(np.int32) + + # Calculate eye dimensions + def get_eye_dimensions(eye_points): + x_coords = eye_points[:, 0] + y_coords = eye_points[:, 1] + width = int((np.max(x_coords) - np.min(x_coords)) * (1 + modules.globals.mask_down_size)) + height = int((np.max(y_coords) - np.min(y_coords)) * (1 + modules.globals.mask_down_size)) + return width, height + + left_width, left_height = get_eye_dimensions(left_eye) + right_width, right_height = get_eye_dimensions(right_eye) + + # Add extra padding + padding = int(max(left_width, right_width) * 0.2) + + # Calculate bounding box for both eyes + min_x = min(left_eye_center[0] - left_width//2, right_eye_center[0] - right_width//2) - padding + max_x = max(left_eye_center[0] + left_width//2, right_eye_center[0] + right_width//2) + padding + min_y = min(left_eye_center[1] - left_height//2, right_eye_center[1] - right_height//2) - padding + max_y = max(left_eye_center[1] + left_height//2, right_eye_center[1] + right_height//2) + padding + + # Ensure coordinates are within frame bounds + min_x = max(0, min_x) + min_y = max(0, min_y) + max_x = min(frame.shape[1], max_x) + max_y = min(frame.shape[0], max_y) + + # Create mask for the eyes region + mask_roi = np.zeros((max_y - min_y, max_x - min_x), dtype=np.uint8) + + # Draw ellipses for both eyes + left_center = (left_eye_center[0] - min_x, left_eye_center[1] - min_y) + right_center = (right_eye_center[0] - min_x, right_eye_center[1] - min_y) + + # Calculate axes lengths (half of width and height) + left_axes = (left_width//2, left_height//2) + right_axes = (right_width//2, right_height//2) + + # Draw filled ellipses + cv2.ellipse(mask_roi, left_center, left_axes, 0, 0, 360, 255, -1) + cv2.ellipse(mask_roi, right_center, right_axes, 0, 0, 360, 255, -1) + + # Apply Gaussian blur to soften mask edges + mask_roi = cv2.GaussianBlur(mask_roi, (15, 15), 5) + + # 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 + eyes_cutout = frame[min_y:max_y, min_x:max_x].copy() + + # Create polygon points for visualization + def create_ellipse_points(center, axes): + t = np.linspace(0, 2*np.pi, 32) + x = center[0] + axes[0] * np.cos(t) + y = center[1] + axes[1] * np.sin(t) + return np.column_stack((x, y)).astype(np.int32) + + # Generate points for both ellipses + left_points = create_ellipse_points((left_eye_center[0], left_eye_center[1]), (left_width//2, left_height//2)) + right_points = create_ellipse_points((right_eye_center[0], right_eye_center[1]), (right_width//2, right_height//2)) + + # Combine points for both eyes + eyes_polygon = np.vstack([left_points, right_points]) + + return mask, eyes_cutout, (min_x, min_y, max_x, max_y), eyes_polygon + + +def apply_eyes_area( + frame: np.ndarray, + eyes_cutout: np.ndarray, + eyes_box: tuple, + face_mask: np.ndarray, + eyes_polygon: np.ndarray, +) -> np.ndarray: + min_x, min_y, max_x, max_y = eyes_box + box_width = max_x - min_x + box_height = max_y - min_y + + if ( + eyes_cutout is None + or box_width is None + or box_height is None + or face_mask is None + or eyes_polygon is None + ): + return frame + + try: + resized_eyes_cutout = cv2.resize(eyes_cutout, (box_width, box_height)) + roi = frame[min_y:max_y, min_x:max_x] + + if roi.shape != resized_eyes_cutout.shape: + resized_eyes_cutout = cv2.resize( + resized_eyes_cutout, (roi.shape[1], roi.shape[0]) + ) + + color_corrected_eyes = apply_color_transfer(resized_eyes_cutout, roi) + + # Create mask for both eyes + polygon_mask = np.zeros(roi.shape[:2], dtype=np.uint8) + + # Split points for left and right eyes + mid_point = len(eyes_polygon) // 2 + left_eye_points = eyes_polygon[:mid_point] - [min_x, min_y] + right_eye_points = eyes_polygon[mid_point:] - [min_x, min_y] + + # Draw filled ellipses using points + left_rect = cv2.minAreaRect(left_eye_points) + right_rect = cv2.minAreaRect(right_eye_points) + + # Convert rect to ellipse parameters + def rect_to_ellipse_params(rect): + center = rect[0] + size = rect[1] + angle = rect[2] + return (int(center[0]), int(center[1])), (int(size[0]/2), int(size[1]/2)), angle + + # Draw filled ellipses + left_params = rect_to_ellipse_params(left_rect) + right_params = rect_to_ellipse_params(right_rect) + cv2.ellipse(polygon_mask, left_params[0], left_params[1], left_params[2], 0, 360, 255, -1) + cv2.ellipse(polygon_mask, right_params[0], right_params[1], right_params[2], 0, 360, 255, -1) + + # Apply 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() + + 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_eyes * 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_eyes_mask_visualization( + frame: Frame, face: Face, eyes_mask_data: tuple +) -> Frame: + landmarks = face.landmark_2d_106 + if landmarks is not None and eyes_mask_data is not None: + mask, eyes_cutout, (min_x, min_y, max_x, max_y), eyes_polygon = eyes_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 eyes ellipses + mid_point = len(eyes_polygon) // 2 + left_points = eyes_polygon[:mid_point] + right_points = eyes_polygon[mid_point:] + + try: + # Fit ellipses to points - need at least 5 points + if len(left_points) >= 5 and len(right_points) >= 5: + # Convert points to the correct format for ellipse fitting + left_points = left_points.astype(np.float32) + right_points = right_points.astype(np.float32) + + # Fit ellipses + left_ellipse = cv2.fitEllipse(left_points) + right_ellipse = cv2.fitEllipse(right_points) + + # Draw the ellipses + cv2.ellipse(vis_frame, left_ellipse, (0, 255, 0), 2) + cv2.ellipse(vis_frame, right_ellipse, (0, 255, 0), 2) + except Exception as e: + # If ellipse fitting fails, draw simple rectangles as fallback + left_rect = cv2.boundingRect(left_points) + right_rect = cv2.boundingRect(right_points) + cv2.rectangle(vis_frame, + (left_rect[0], left_rect[1]), + (left_rect[0] + left_rect[2], left_rect[1] + left_rect[3]), + (0, 255, 0), 2) + cv2.rectangle(vis_frame, + (right_rect[0], right_rect[1]), + (right_rect[0] + right_rect[2], right_rect[1] + right_rect[3]), + (0, 255, 0), 2) + + # Add label + cv2.putText( + vis_frame, + "Eyes 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 8242d144..30f1f53a 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -3,7 +3,7 @@ import customtkinter as ctk from typing import Callable, Tuple import cv2 -from cv2_enumerate_cameras import enumerate_cameras # Add this import +from cv2_enumerate_cameras import enumerate_cameras from PIL import Image, ImageOps import time import json @@ -252,6 +252,19 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) enhancer_switch.place(relx=0.1, rely=0.70) + keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio) + keep_audio_switch = ctk.CTkSwitch( + root, + text=_("Keep audio"), + variable=keep_audio_value, + cursor="hand2", + command=lambda: ( + setattr(modules.globals, "keep_audio", keep_audio_value.get()), + save_switch_states(), + ), + ) + keep_audio_switch.place(relx=0.1, rely=0.75) + # Additional Options (Middle Right) mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask) mouth_mask_switch = ctk.CTkSwitch( @@ -273,20 +286,31 @@ 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.6) + show_mouth_mask_box_switch.place(relx=0.6, rely=0.60) - keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio) - keep_audio_switch = ctk.CTkSwitch( + # Add eyes mask switch + eyes_mask_var = ctk.BooleanVar(value=modules.globals.eyes_mask) + eyes_mask_switch = ctk.CTkSwitch( root, - text=_("Keep audio"), - variable=keep_audio_value, + text=_("Eyes Mask"), + variable=eyes_mask_var, cursor="hand2", - command=lambda: ( - setattr(modules.globals, "keep_audio", keep_audio_value.get()), - save_switch_states(), + command=lambda: setattr(modules.globals, "eyes_mask", eyes_mask_var.get()), + ) + eyes_mask_switch.place(relx=0.6, rely=0.65) + + # Add show eyes mask box switch + show_eyes_mask_box_var = ctk.BooleanVar(value=modules.globals.show_eyes_mask_box) + show_eyes_mask_box_switch = ctk.CTkSwitch( + root, + text=_("Show Eyes Mask Box"), + variable=show_eyes_mask_box_var, + cursor="hand2", + command=lambda: setattr( + modules.globals, "show_eyes_mask_box", show_eyes_mask_box_var.get() ), ) - keep_audio_switch.place(relx=0.6, rely=0.65) + show_eyes_mask_box_switch.place(relx=0.6, rely=0.70) # Add show FPS switch show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps) @@ -300,7 +324,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C save_switch_states(), ), ) - show_fps_switch.place(relx=0.6, rely=0.70) + show_fps_switch.place(relx=0.6, rely=0.75) # Main Control Buttons (Bottom) start_button = ctk.CTkButton( @@ -767,8 +791,7 @@ def update_preview(frame_number: int = 0) -> None: modules.globals.frame_processors ): temp_frame = frame_processor.process_frame( - get_one_face(cv2.imread(modules.globals.source_path)), temp_frame - ) + get_one_face(cv2.imread(modules.globals.source_path)), temp_frame) image = Image.fromarray(cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB)) image = ImageOps.contain( image, (PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.LANCZOS 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 07/11] 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 add09165..98eccf51 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -43,5 +43,7 @@ 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 cd6f9260..cdfa7e64 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 30f1f53a..c7c37cf6 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -36,7 +36,7 @@ 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") ) From ab3b73631b51f23448acbd357d069b05972e54b3 Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:04:31 +0530 Subject: [PATCH 08/11] Shift masking features to face_masking.py --- modules/processors/frame/face_masking.py | 580 ++++++++++++++ modules/processors/frame/face_swapper.py | 926 ++--------------------- 2 files changed, 625 insertions(+), 881 deletions(-) create mode 100644 modules/processors/frame/face_masking.py diff --git a/modules/processors/frame/face_masking.py b/modules/processors/frame/face_masking.py new file mode 100644 index 00000000..3ae963e8 --- /dev/null +++ b/modules/processors/frame/face_masking.py @@ -0,0 +1,580 @@ +import cv2 +import numpy as np +from modules.typing import Face, Frame +import modules.globals + +def apply_color_transfer(source, target): + """ + Apply color transfer from target to source image + """ + source = cv2.cvtColor(source, cv2.COLOR_BGR2LAB).astype("float32") + target = cv2.cvtColor(target, cv2.COLOR_BGR2LAB).astype("float32") + + source_mean, source_std = cv2.meanStdDev(source) + target_mean, target_std = cv2.meanStdDev(target) + + # Reshape mean and std to be broadcastable + source_mean = source_mean.reshape(1, 1, 3) + source_std = source_std.reshape(1, 1, 3) + target_mean = target_mean.reshape(1, 1, 3) + target_std = target_std.reshape(1, 1, 3) + + # Perform the color transfer + source = (source - source_mean) * (target_std / source_std) + target_mean + + return cv2.cvtColor(np.clip(source, 0, 255).astype("uint8"), cv2.COLOR_LAB2BGR) + +def create_face_mask(face: Face, frame: Frame) -> np.ndarray: + mask = np.zeros(frame.shape[:2], dtype=np.uint8) + landmarks = face.landmark_2d_106 + if landmarks is not None: + # Convert landmarks to int32 + landmarks = landmarks.astype(np.int32) + + # Extract facial features + right_side_face = landmarks[0:16] + left_side_face = landmarks[17:32] + right_eye = landmarks[33:42] + right_eye_brow = landmarks[43:51] + left_eye = landmarks[87:96] + left_eye_brow = landmarks[97:105] + + # Calculate forehead extension + right_eyebrow_top = np.min(right_eye_brow[:, 1]) + left_eyebrow_top = np.min(left_eye_brow[:, 1]) + eyebrow_top = min(right_eyebrow_top, left_eyebrow_top) + + face_top = np.min([right_side_face[0, 1], left_side_face[-1, 1]]) + forehead_height = face_top - eyebrow_top + extended_forehead_height = int(forehead_height * 5.0) # Extend by 50% + + # Create forehead points + forehead_left = right_side_face[0].copy() + forehead_right = left_side_face[-1].copy() + forehead_left[1] -= extended_forehead_height + forehead_right[1] -= extended_forehead_height + + # Combine all points to create the face outline + face_outline = np.vstack( + [ + [forehead_left], + right_side_face, + left_side_face[::-1], # Reverse left side to create a continuous outline + [forehead_right], + ] + ) + + # Calculate padding + padding = int( + np.linalg.norm(right_side_face[0] - left_side_face[-1]) * 0.05 + ) # 5% of face width + + # Create a slightly larger convex hull for padding + hull = cv2.convexHull(face_outline) + hull_padded = [] + for point in hull: + x, y = point[0] + center = np.mean(face_outline, axis=0) + direction = np.array([x, y]) - center + direction = direction / np.linalg.norm(direction) + padded_point = np.array([x, y]) + direction * padding + hull_padded.append(padded_point) + + hull_padded = np.array(hull_padded, dtype=np.int32) + + # Fill the padded convex hull + cv2.fillConvexPoly(mask, hull_padded, 255) + + # Smooth the mask edges + mask = cv2.GaussianBlur(mask, (5, 5), 3) + + return mask + +def create_lower_mouth_mask( + face: Face, frame: Frame +) -> (np.ndarray, np.ndarray, tuple, np.ndarray): + mask = np.zeros(frame.shape[:2], dtype=np.uint8) + mouth_cutout = None + landmarks = face.landmark_2d_106 + if landmarks is not None: + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + lower_lip_order = [ + 65, + 66, + 62, + 70, + 69, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 0, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 65, + ] + lower_lip_landmarks = landmarks[lower_lip_order].astype( + np.float32 + ) # Use float for precise calculations + + # Calculate the center of the landmarks + center = np.mean(lower_lip_landmarks, axis=0) + + # Expand the landmarks outward + expansion_factor = ( + 1 + modules.globals.mask_down_size + ) # Adjust this for more or less expansion + expanded_landmarks = (lower_lip_landmarks - center) * expansion_factor + center + + # Extend the top lip part + toplip_indices = [ + 20, + 0, + 1, + 2, + 3, + 4, + 5, + ] # Indices for landmarks 2, 65, 66, 62, 70, 69, 18 + toplip_extension = ( + modules.globals.mask_size * 0.5 + ) # Adjust this factor to control the extension + for idx in toplip_indices: + direction = expanded_landmarks[idx] - center + direction = direction / np.linalg.norm(direction) + expanded_landmarks[idx] += direction * toplip_extension + + # Extend the bottom part (chin area) + chin_indices = [ + 11, + 12, + 13, + 14, + 15, + 16, + ] # Indices for landmarks 21, 22, 23, 24, 0, 8 + chin_extension = 2 * 0.2 # Adjust this factor to control the extension + for idx in chin_indices: + expanded_landmarks[idx][1] += ( + expanded_landmarks[idx][1] - center[1] + ) * chin_extension + + # Convert back to integer coordinates + expanded_landmarks = expanded_landmarks.astype(np.int32) + + # Calculate bounding box for the expanded lower mouth + min_x, min_y = np.min(expanded_landmarks, axis=0) + max_x, max_y = np.max(expanded_landmarks, axis=0) + + # Add some padding to the bounding box + padding = int((max_x - min_x) * 0.1) # 10% padding + min_x = max(0, min_x - padding) + min_y = max(0, min_y - padding) + max_x = min(frame.shape[1], max_x + padding) + max_y = min(frame.shape[0], max_y + padding) + + # Ensure the bounding box dimensions are valid + if max_x <= min_x or max_y <= min_y: + if (max_x - min_x) <= 1: + max_x = min_x + 1 + if (max_y - min_y) <= 1: + max_y = min_y + 1 + + # Create the mask + mask_roi = np.zeros((max_y - min_y, max_x - min_x), dtype=np.uint8) + cv2.fillPoly(mask_roi, [expanded_landmarks - [min_x, min_y]], 255) + + # Apply Gaussian blur to soften the mask edges + mask_roi = cv2.GaussianBlur(mask_roi, (15, 15), 5) + + # 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 + mouth_cutout = frame[min_y:max_y, min_x:max_x].copy() + + # Return the expanded lower lip polygon in original frame coordinates + lower_lip_polygon = expanded_landmarks + + return mask, mouth_cutout, (min_x, min_y, max_x, max_y), lower_lip_polygon + +def create_eyes_mask(face: Face, frame: Frame) -> (np.ndarray, np.ndarray, tuple, np.ndarray): + mask = np.zeros(frame.shape[:2], dtype=np.uint8) + eyes_cutout = None + landmarks = face.landmark_2d_106 + if landmarks is not None: + # Left eye landmarks (87-96) and right eye landmarks (33-42) + left_eye = landmarks[87:96] + right_eye = landmarks[33:42] + + # Calculate centers and dimensions for each eye + left_eye_center = np.mean(left_eye, axis=0).astype(np.int32) + right_eye_center = np.mean(right_eye, axis=0).astype(np.int32) + + # Calculate eye dimensions + def get_eye_dimensions(eye_points): + x_coords = eye_points[:, 0] + y_coords = eye_points[:, 1] + width = int((np.max(x_coords) - np.min(x_coords)) * (1 + modules.globals.mask_down_size)) + height = int((np.max(y_coords) - np.min(y_coords)) * (1 + modules.globals.mask_down_size)) + return width, height + + left_width, left_height = get_eye_dimensions(left_eye) + right_width, right_height = get_eye_dimensions(right_eye) + + # Add extra padding + padding = int(max(left_width, right_width) * 0.2) + + # Calculate bounding box for both eyes + min_x = min(left_eye_center[0] - left_width//2, right_eye_center[0] - right_width//2) - padding + max_x = max(left_eye_center[0] + left_width//2, right_eye_center[0] + right_width//2) + padding + min_y = min(left_eye_center[1] - left_height//2, right_eye_center[1] - right_height//2) - padding + max_y = max(left_eye_center[1] + left_height//2, right_eye_center[1] + right_height//2) + padding + + # Ensure coordinates are within frame bounds + min_x = max(0, min_x) + min_y = max(0, min_y) + max_x = min(frame.shape[1], max_x) + max_y = min(frame.shape[0], max_y) + + # Create mask for the eyes region + mask_roi = np.zeros((max_y - min_y, max_x - min_x), dtype=np.uint8) + + # Draw ellipses for both eyes + left_center = (left_eye_center[0] - min_x, left_eye_center[1] - min_y) + right_center = (right_eye_center[0] - min_x, right_eye_center[1] - min_y) + + # Calculate axes lengths (half of width and height) + left_axes = (left_width//2, left_height//2) + right_axes = (right_width//2, right_height//2) + + # Draw filled ellipses + cv2.ellipse(mask_roi, left_center, left_axes, 0, 0, 360, 255, -1) + cv2.ellipse(mask_roi, right_center, right_axes, 0, 0, 360, 255, -1) + + # Apply Gaussian blur to soften mask edges + mask_roi = cv2.GaussianBlur(mask_roi, (15, 15), 5) + + # 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 + eyes_cutout = frame[min_y:max_y, min_x:max_x].copy() + + # Create polygon points for visualization + def create_ellipse_points(center, axes): + t = np.linspace(0, 2*np.pi, 32) + x = center[0] + axes[0] * np.cos(t) + y = center[1] + axes[1] * np.sin(t) + return np.column_stack((x, y)).astype(np.int32) + + # Generate points for both ellipses + left_points = create_ellipse_points((left_eye_center[0], left_eye_center[1]), (left_width//2, left_height//2)) + right_points = create_ellipse_points((right_eye_center[0], right_eye_center[1]), (right_width//2, right_height//2)) + + # Combine points for both eyes + eyes_polygon = np.vstack([left_points, right_points]) + + return mask, eyes_cutout, (min_x, min_y, max_x, max_y), eyes_polygon + +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_mask_area( + frame: np.ndarray, + cutout: np.ndarray, + box: tuple, + face_mask: np.ndarray, + polygon: np.ndarray, +) -> np.ndarray: + min_x, min_y, max_x, max_y = box + box_width = max_x - min_x + box_height = max_y - min_y + + if ( + cutout is None + or box_width is None + or box_height is None + or face_mask is None + or polygon is None + ): + return frame + + try: + resized_cutout = cv2.resize(cutout, (box_width, box_height)) + roi = frame[min_y:max_y, min_x:max_x] + + if roi.shape != resized_cutout.shape: + resized_cutout = cv2.resize( + resized_cutout, (roi.shape[1], roi.shape[0]) + ) + + color_corrected_area = apply_color_transfer(resized_cutout, roi) + + # Create mask for the area + polygon_mask = np.zeros(roi.shape[:2], dtype=np.uint8) + + # Split points for left and right parts if needed + if len(polygon) > 50: # Arbitrary threshold to detect if we have multiple parts + mid_point = len(polygon) // 2 + left_points = polygon[:mid_point] - [min_x, min_y] + right_points = polygon[mid_point:] - [min_x, min_y] + cv2.fillPoly(polygon_mask, [left_points], 255) + cv2.fillPoly(polygon_mask, [right_points], 255) + else: + adjusted_polygon = polygon - [min_x, min_y] + cv2.fillPoly(polygon_mask, [adjusted_polygon], 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_area * 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_mask_visualization( + frame: Frame, + mask_data: tuple, + label: str, + draw_method: str = "polygon" +) -> Frame: + mask, cutout, (min_x, min_y, max_x, max_y), polygon = 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) + + if draw_method == "ellipse" and len(polygon) > 50: # For eyes + # Split points for left and right parts + mid_point = len(polygon) // 2 + left_points = polygon[:mid_point] + right_points = polygon[mid_point:] + + try: + # Fit ellipses to points - need at least 5 points + if len(left_points) >= 5 and len(right_points) >= 5: + # Convert points to the correct format for ellipse fitting + left_points = left_points.astype(np.float32) + right_points = right_points.astype(np.float32) + + # Fit ellipses + left_ellipse = cv2.fitEllipse(left_points) + right_ellipse = cv2.fitEllipse(right_points) + + # Draw the ellipses + cv2.ellipse(vis_frame, left_ellipse, (0, 255, 0), 2) + cv2.ellipse(vis_frame, right_ellipse, (0, 255, 0), 2) + except Exception as e: + # If ellipse fitting fails, draw simple rectangles as fallback + left_rect = cv2.boundingRect(left_points) + right_rect = cv2.boundingRect(right_points) + cv2.rectangle(vis_frame, + (left_rect[0], left_rect[1]), + (left_rect[0] + left_rect[2], left_rect[1] + left_rect[3]), + (0, 255, 0), 2) + cv2.rectangle(vis_frame, + (right_rect[0], right_rect[1]), + (right_rect[0] + right_rect[2], right_rect[1] + right_rect[3]), + (0, 255, 0), 2) + else: # For mouth and eyebrows + # Draw the polygon + if len(polygon) > 50: # If we have multiple parts + mid_point = len(polygon) // 2 + left_points = polygon[:mid_point] + right_points = polygon[mid_point:] + 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) + else: + cv2.polylines(vis_frame, [polygon], True, (0, 255, 0), 2, cv2.LINE_AA) + + # Add label + cv2.putText( + vis_frame, + label, + (min_x, min_y - 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 255, 255), + 1, + ) + + return vis_frame \ No newline at end of file diff --git a/modules/processors/frame/face_swapper.py b/modules/processors/frame/face_swapper.py index cdfa7e64..26330d3b 100644 --- a/modules/processors/frame/face_swapper.py +++ b/modules/processors/frame/face_swapper.py @@ -14,6 +14,14 @@ is_video, ) from modules.cluster_analysis import find_closest_centroid +from modules.processors.frame.face_masking import ( + create_face_mask, + create_lower_mouth_mask, + create_eyes_mask, + create_eyebrows_mask, + apply_mask_area, + draw_mask_visualization +) import os FACE_SWAPPER = None @@ -78,54 +86,58 @@ def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame: face_mask = create_face_mask(target_face, temp_frame) if modules.globals.mouth_mask: - # Create the mouth mask - mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon = ( - create_lower_mouth_mask(target_face, temp_frame) - ) - - # Apply the mouth area - swapped_frame = apply_mouth_area( - swapped_frame, mouth_cutout, mouth_box, face_mask, lower_lip_polygon + # Create and apply mouth mask + mouth_mask_data = create_lower_mouth_mask(target_face, temp_frame) + swapped_frame = apply_mask_area( + swapped_frame, + mouth_mask_data[1], # mouth_cutout + mouth_mask_data[2], # mouth_box + face_mask, + mouth_mask_data[3] # mouth_polygon ) if modules.globals.show_mouth_mask_box: - 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 + swapped_frame = draw_mask_visualization( + swapped_frame, + mouth_mask_data, + "Lower Mouth Mask" ) if modules.globals.eyes_mask: - # Create the eyes mask - eyes_mask, eyes_cutout, eyes_box, eyes_polygon = ( - create_eyes_mask(target_face, temp_frame) - ) - - # Apply the eyes area - swapped_frame = apply_eyes_area( - swapped_frame, eyes_cutout, eyes_box, face_mask, eyes_polygon + # Create and apply eyes mask + eyes_mask_data = create_eyes_mask(target_face, temp_frame) + swapped_frame = apply_mask_area( + swapped_frame, + eyes_mask_data[1], # eyes_cutout + eyes_mask_data[2], # eyes_box + face_mask, + eyes_mask_data[3] # eyes_polygon ) if modules.globals.show_eyes_mask_box: - eyes_mask_data = (eyes_mask, eyes_cutout, eyes_box, eyes_polygon) - swapped_frame = draw_eyes_mask_visualization( - swapped_frame, target_face, eyes_mask_data + swapped_frame = draw_mask_visualization( + swapped_frame, + eyes_mask_data, + "Eyes Mask", + draw_method="ellipse" ) 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 + # Create and apply eyebrows mask + eyebrows_mask_data = create_eyebrows_mask(target_face, temp_frame) + swapped_frame = apply_mask_area( + swapped_frame, + eyebrows_mask_data[1], # eyebrows_cutout + eyebrows_mask_data[2], # eyebrows_box + face_mask, + eyebrows_mask_data[3] # 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 + swapped_frame = draw_mask_visualization( + swapped_frame, + eyebrows_mask_data, + "Eyebrows Mask" ) return swapped_frame @@ -289,851 +301,3 @@ def process_video(source_path: str, temp_frame_paths: List[str]) -> None: modules.processors.frame.core.process_video( source_path, temp_frame_paths, process_frames ) - - -def create_lower_mouth_mask( - face: Face, frame: Frame -) -> (np.ndarray, np.ndarray, tuple, np.ndarray): - mask = np.zeros(frame.shape[:2], dtype=np.uint8) - mouth_cutout = None - landmarks = face.landmark_2d_106 - if landmarks is not None: - # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - lower_lip_order = [ - 65, - 66, - 62, - 70, - 69, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 0, - 8, - 7, - 6, - 5, - 4, - 3, - 2, - 65, - ] - lower_lip_landmarks = landmarks[lower_lip_order].astype( - np.float32 - ) # Use float for precise calculations - - # Calculate the center of the landmarks - center = np.mean(lower_lip_landmarks, axis=0) - - # Expand the landmarks outward - expansion_factor = ( - 1 + modules.globals.mask_down_size - ) # Adjust this for more or less expansion - expanded_landmarks = (lower_lip_landmarks - center) * expansion_factor + center - - # Extend the top lip part - toplip_indices = [ - 20, - 0, - 1, - 2, - 3, - 4, - 5, - ] # Indices for landmarks 2, 65, 66, 62, 70, 69, 18 - toplip_extension = ( - modules.globals.mask_size * 0.5 - ) # Adjust this factor to control the extension - for idx in toplip_indices: - direction = expanded_landmarks[idx] - center - direction = direction / np.linalg.norm(direction) - expanded_landmarks[idx] += direction * toplip_extension - - # Extend the bottom part (chin area) - chin_indices = [ - 11, - 12, - 13, - 14, - 15, - 16, - ] # Indices for landmarks 21, 22, 23, 24, 0, 8 - chin_extension = 2 * 0.2 # Adjust this factor to control the extension - for idx in chin_indices: - expanded_landmarks[idx][1] += ( - expanded_landmarks[idx][1] - center[1] - ) * chin_extension - - # Convert back to integer coordinates - expanded_landmarks = expanded_landmarks.astype(np.int32) - - # Calculate bounding box for the expanded lower mouth - min_x, min_y = np.min(expanded_landmarks, axis=0) - max_x, max_y = np.max(expanded_landmarks, axis=0) - - # Add some padding to the bounding box - padding = int((max_x - min_x) * 0.1) # 10% padding - min_x = max(0, min_x - padding) - min_y = max(0, min_y - padding) - max_x = min(frame.shape[1], max_x + padding) - max_y = min(frame.shape[0], max_y + padding) - - # Ensure the bounding box dimensions are valid - if max_x <= min_x or max_y <= min_y: - if (max_x - min_x) <= 1: - max_x = min_x + 1 - if (max_y - min_y) <= 1: - max_y = min_y + 1 - - # Create the mask - mask_roi = np.zeros((max_y - min_y, max_x - min_x), dtype=np.uint8) - cv2.fillPoly(mask_roi, [expanded_landmarks - [min_x, min_y]], 255) - - # Apply Gaussian blur to soften the mask edges - mask_roi = cv2.GaussianBlur(mask_roi, (15, 15), 5) - - # 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 - mouth_cutout = frame[min_y:max_y, min_x:max_x].copy() - - # Return the expanded lower lip polygon in original frame coordinates - lower_lip_polygon = expanded_landmarks - - return mask, mouth_cutout, (min_x, min_y, max_x, max_y), lower_lip_polygon - - -def draw_mouth_mask_visualization( - frame: Frame, face: Face, mouth_mask_data: tuple -) -> Frame: - landmarks = face.landmark_2d_106 - if landmarks is not None and mouth_mask_data is not None: - mask, mouth_cutout, (min_x, min_y, max_x, max_y), lower_lip_polygon = ( - mouth_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) - - # Adjust mask to match the region size - mask_region = mask[0 : max_y - min_y, 0 : max_x - min_x] - - # Remove the color mask overlay - # color_mask = cv2.applyColorMap((mask_region * 255).astype(np.uint8), cv2.COLORMAP_JET) - - # Ensure shapes match before blending - vis_region = vis_frame[min_y:max_y, min_x:max_x] - # Remove blending with color_mask - # if vis_region.shape[:2] == color_mask.shape[:2]: - # blended = cv2.addWeighted(vis_region, 0.7, color_mask, 0.3, 0) - # vis_frame[min_y:max_y, min_x:max_x] = blended - - # Draw the lower lip polygon - cv2.polylines(vis_frame, [lower_lip_polygon], True, (0, 255, 0), 2) - - # Remove the red box - # cv2.rectangle(vis_frame, (min_x, min_y), (max_x, max_y), (0, 0, 255), 2) - - # Visualize the feathered mask - feather_amount = max( - 1, - min( - 30, - (max_x - min_x) // modules.globals.mask_feather_ratio, - (max_y - min_y) // modules.globals.mask_feather_ratio, - ), - ) - # Ensure kernel size is odd - kernel_size = 2 * feather_amount + 1 - feathered_mask = cv2.GaussianBlur( - mask_region.astype(float), (kernel_size, kernel_size), 0 - ) - feathered_mask = (feathered_mask / feathered_mask.max() * 255).astype(np.uint8) - # Remove the feathered mask color overlay - # color_feathered_mask = cv2.applyColorMap(feathered_mask, cv2.COLORMAP_VIRIDIS) - - # Ensure shapes match before blending feathered mask - # if vis_region.shape == color_feathered_mask.shape: - # blended_feathered = cv2.addWeighted(vis_region, 0.7, color_feathered_mask, 0.3, 0) - # vis_frame[min_y:max_y, min_x:max_x] = blended_feathered - - # Add labels - cv2.putText( - vis_frame, - "Lower Mouth Mask", - (min_x, min_y - 10), - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - (255, 255, 255), - 1, - ) - cv2.putText( - vis_frame, - "Feathered Mask", - (min_x, max_y + 20), - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - (255, 255, 255), - 1, - ) - - return vis_frame - return frame - - -def apply_mouth_area( - frame: np.ndarray, - mouth_cutout: np.ndarray, - mouth_box: tuple, - face_mask: np.ndarray, - mouth_polygon: np.ndarray, -) -> np.ndarray: - min_x, min_y, max_x, max_y = mouth_box - box_width = max_x - min_x - box_height = max_y - min_y - - if ( - mouth_cutout is None - or box_width is None - or box_height is None - or face_mask is None - or mouth_polygon is None - ): - return frame - - try: - resized_mouth_cutout = cv2.resize(mouth_cutout, (box_width, box_height)) - roi = frame[min_y:max_y, min_x:max_x] - - if roi.shape != resized_mouth_cutout.shape: - resized_mouth_cutout = cv2.resize( - resized_mouth_cutout, (roi.shape[1], roi.shape[0]) - ) - - color_corrected_mouth = apply_color_transfer(resized_mouth_cutout, roi) - - # Use the provided mouth polygon to create the mask - polygon_mask = np.zeros(roi.shape[:2], dtype=np.uint8) - adjusted_polygon = mouth_polygon - [min_x, min_y] - cv2.fillPoly(polygon_mask, [adjusted_polygon], 255) - - # Apply feathering to the polygon mask - 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() - - 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_mouth * 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 create_face_mask(face: Face, frame: Frame) -> np.ndarray: - mask = np.zeros(frame.shape[:2], dtype=np.uint8) - landmarks = face.landmark_2d_106 - if landmarks is not None: - # Convert landmarks to int32 - landmarks = landmarks.astype(np.int32) - - # Extract facial features - right_side_face = landmarks[0:16] - left_side_face = landmarks[17:32] - right_eye = landmarks[33:42] - right_eye_brow = landmarks[43:51] - left_eye = landmarks[87:96] - left_eye_brow = landmarks[97:105] - - # Calculate forehead extension - right_eyebrow_top = np.min(right_eye_brow[:, 1]) - left_eyebrow_top = np.min(left_eye_brow[:, 1]) - eyebrow_top = min(right_eyebrow_top, left_eyebrow_top) - - face_top = np.min([right_side_face[0, 1], left_side_face[-1, 1]]) - forehead_height = face_top - eyebrow_top - extended_forehead_height = int(forehead_height * 5.0) # Extend by 50% - - # Create forehead points - forehead_left = right_side_face[0].copy() - forehead_right = left_side_face[-1].copy() - forehead_left[1] -= extended_forehead_height - forehead_right[1] -= extended_forehead_height - - # Combine all points to create the face outline - face_outline = np.vstack( - [ - [forehead_left], - right_side_face, - left_side_face[ - ::-1 - ], # Reverse left side to create a continuous outline - [forehead_right], - ] - ) - - # Calculate padding - padding = int( - np.linalg.norm(right_side_face[0] - left_side_face[-1]) * 0.05 - ) # 5% of face width - - # Create a slightly larger convex hull for padding - hull = cv2.convexHull(face_outline) - hull_padded = [] - for point in hull: - x, y = point[0] - center = np.mean(face_outline, axis=0) - direction = np.array([x, y]) - center - direction = direction / np.linalg.norm(direction) - padded_point = np.array([x, y]) + direction * padding - hull_padded.append(padded_point) - - hull_padded = np.array(hull_padded, dtype=np.int32) - - # Fill the padded convex hull - cv2.fillConvexPoly(mask, hull_padded, 255) - - # Smooth the mask edges - mask = cv2.GaussianBlur(mask, (5, 5), 3) - - return mask - - -def apply_color_transfer(source, target): - """ - Apply color transfer from target to source image - """ - source = cv2.cvtColor(source, cv2.COLOR_BGR2LAB).astype("float32") - target = cv2.cvtColor(target, cv2.COLOR_BGR2LAB).astype("float32") - - source_mean, source_std = cv2.meanStdDev(source) - target_mean, target_std = cv2.meanStdDev(target) - - # Reshape mean and std to be broadcastable - source_mean = source_mean.reshape(1, 1, 3) - source_std = source_std.reshape(1, 1, 3) - target_mean = target_mean.reshape(1, 1, 3) - target_std = target_std.reshape(1, 1, 3) - - # Perform the color transfer - source = (source - source_mean) * (target_std / source_std) + target_mean - - return cv2.cvtColor(np.clip(source, 0, 255).astype("uint8"), cv2.COLOR_LAB2BGR) - - -def create_eyes_mask(face: Face, frame: Frame) -> (np.ndarray, np.ndarray, tuple, np.ndarray): - mask = np.zeros(frame.shape[:2], dtype=np.uint8) - eyes_cutout = None - landmarks = face.landmark_2d_106 - if landmarks is not None: - # Left eye landmarks (87-96) and right eye landmarks (33-42) - left_eye = landmarks[87:96] - right_eye = landmarks[33:42] - - # Calculate centers and dimensions for each eye - left_eye_center = np.mean(left_eye, axis=0).astype(np.int32) - right_eye_center = np.mean(right_eye, axis=0).astype(np.int32) - - # Calculate eye dimensions - def get_eye_dimensions(eye_points): - x_coords = eye_points[:, 0] - y_coords = eye_points[:, 1] - width = int((np.max(x_coords) - np.min(x_coords)) * (1 + modules.globals.mask_down_size)) - height = int((np.max(y_coords) - np.min(y_coords)) * (1 + modules.globals.mask_down_size)) - return width, height - - left_width, left_height = get_eye_dimensions(left_eye) - right_width, right_height = get_eye_dimensions(right_eye) - - # Add extra padding - padding = int(max(left_width, right_width) * 0.2) - - # Calculate bounding box for both eyes - min_x = min(left_eye_center[0] - left_width//2, right_eye_center[0] - right_width//2) - padding - max_x = max(left_eye_center[0] + left_width//2, right_eye_center[0] + right_width//2) + padding - min_y = min(left_eye_center[1] - left_height//2, right_eye_center[1] - right_height//2) - padding - max_y = max(left_eye_center[1] + left_height//2, right_eye_center[1] + right_height//2) + padding - - # Ensure coordinates are within frame bounds - min_x = max(0, min_x) - min_y = max(0, min_y) - max_x = min(frame.shape[1], max_x) - max_y = min(frame.shape[0], max_y) - - # Create mask for the eyes region - mask_roi = np.zeros((max_y - min_y, max_x - min_x), dtype=np.uint8) - - # Draw ellipses for both eyes - left_center = (left_eye_center[0] - min_x, left_eye_center[1] - min_y) - right_center = (right_eye_center[0] - min_x, right_eye_center[1] - min_y) - - # Calculate axes lengths (half of width and height) - left_axes = (left_width//2, left_height//2) - right_axes = (right_width//2, right_height//2) - - # Draw filled ellipses - cv2.ellipse(mask_roi, left_center, left_axes, 0, 0, 360, 255, -1) - cv2.ellipse(mask_roi, right_center, right_axes, 0, 0, 360, 255, -1) - - # Apply Gaussian blur to soften mask edges - mask_roi = cv2.GaussianBlur(mask_roi, (15, 15), 5) - - # 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 - eyes_cutout = frame[min_y:max_y, min_x:max_x].copy() - - # Create polygon points for visualization - def create_ellipse_points(center, axes): - t = np.linspace(0, 2*np.pi, 32) - x = center[0] + axes[0] * np.cos(t) - y = center[1] + axes[1] * np.sin(t) - return np.column_stack((x, y)).astype(np.int32) - - # Generate points for both ellipses - left_points = create_ellipse_points((left_eye_center[0], left_eye_center[1]), (left_width//2, left_height//2)) - right_points = create_ellipse_points((right_eye_center[0], right_eye_center[1]), (right_width//2, right_height//2)) - - # Combine points for both eyes - eyes_polygon = np.vstack([left_points, right_points]) - - return mask, eyes_cutout, (min_x, min_y, max_x, max_y), eyes_polygon - - -def apply_eyes_area( - frame: np.ndarray, - eyes_cutout: np.ndarray, - eyes_box: tuple, - face_mask: np.ndarray, - eyes_polygon: np.ndarray, -) -> np.ndarray: - min_x, min_y, max_x, max_y = eyes_box - box_width = max_x - min_x - box_height = max_y - min_y - - if ( - eyes_cutout is None - or box_width is None - or box_height is None - or face_mask is None - or eyes_polygon is None - ): - return frame - - try: - resized_eyes_cutout = cv2.resize(eyes_cutout, (box_width, box_height)) - roi = frame[min_y:max_y, min_x:max_x] - - if roi.shape != resized_eyes_cutout.shape: - resized_eyes_cutout = cv2.resize( - resized_eyes_cutout, (roi.shape[1], roi.shape[0]) - ) - - color_corrected_eyes = apply_color_transfer(resized_eyes_cutout, roi) - - # Create mask for both eyes - polygon_mask = np.zeros(roi.shape[:2], dtype=np.uint8) - - # Split points for left and right eyes - mid_point = len(eyes_polygon) // 2 - left_eye_points = eyes_polygon[:mid_point] - [min_x, min_y] - right_eye_points = eyes_polygon[mid_point:] - [min_x, min_y] - - # Draw filled ellipses using points - left_rect = cv2.minAreaRect(left_eye_points) - right_rect = cv2.minAreaRect(right_eye_points) - - # Convert rect to ellipse parameters - def rect_to_ellipse_params(rect): - center = rect[0] - size = rect[1] - angle = rect[2] - return (int(center[0]), int(center[1])), (int(size[0]/2), int(size[1]/2)), angle - - # Draw filled ellipses - left_params = rect_to_ellipse_params(left_rect) - right_params = rect_to_ellipse_params(right_rect) - cv2.ellipse(polygon_mask, left_params[0], left_params[1], left_params[2], 0, 360, 255, -1) - cv2.ellipse(polygon_mask, right_params[0], right_params[1], right_params[2], 0, 360, 255, -1) - - # Apply 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() - - 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_eyes * 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_eyes_mask_visualization( - frame: Frame, face: Face, eyes_mask_data: tuple -) -> Frame: - landmarks = face.landmark_2d_106 - if landmarks is not None and eyes_mask_data is not None: - mask, eyes_cutout, (min_x, min_y, max_x, max_y), eyes_polygon = eyes_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 eyes ellipses - mid_point = len(eyes_polygon) // 2 - left_points = eyes_polygon[:mid_point] - right_points = eyes_polygon[mid_point:] - - try: - # Fit ellipses to points - need at least 5 points - if len(left_points) >= 5 and len(right_points) >= 5: - # Convert points to the correct format for ellipse fitting - left_points = left_points.astype(np.float32) - right_points = right_points.astype(np.float32) - - # Fit ellipses - left_ellipse = cv2.fitEllipse(left_points) - right_ellipse = cv2.fitEllipse(right_points) - - # Draw the ellipses - cv2.ellipse(vis_frame, left_ellipse, (0, 255, 0), 2) - cv2.ellipse(vis_frame, right_ellipse, (0, 255, 0), 2) - except Exception as e: - # If ellipse fitting fails, draw simple rectangles as fallback - left_rect = cv2.boundingRect(left_points) - right_rect = cv2.boundingRect(right_points) - cv2.rectangle(vis_frame, - (left_rect[0], left_rect[1]), - (left_rect[0] + left_rect[2], left_rect[1] + left_rect[3]), - (0, 255, 0), 2) - cv2.rectangle(vis_frame, - (right_rect[0], right_rect[1]), - (right_rect[0] + right_rect[2], right_rect[1] + right_rect[3]), - (0, 255, 0), 2) - - # Add label - cv2.putText( - vis_frame, - "Eyes Mask", - (min_x, min_y - 10), - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - (255, 255, 255), - 1, - ) - - 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 From 01ef955372057b07b2e7def640ca0d7a0219069a Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:14:16 +0530 Subject: [PATCH 09/11] Improve eyebrow mask --- modules/processors/frame/face_masking.py | 95 ++++++++++++++++++------ 1 file changed, 74 insertions(+), 21 deletions(-) diff --git a/modules/processors/frame/face_masking.py b/modules/processors/frame/face_masking.py index 3ae963e8..d0dfb64c 100644 --- a/modules/processors/frame/face_masking.py +++ b/modules/processors/frame/face_masking.py @@ -285,6 +285,65 @@ def create_ellipse_points(center, axes): return mask, eyes_cutout, (min_x, min_y, max_x, max_y), eyes_polygon +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 quadratic curve through points for more natural arch + coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 2) # Changed to quadratic + y = np.polyval(coeffs, x) + + # Create points for top and bottom curves with consistent offsets + top_offset = height * 0.3 # Simplified offset for cleaner curve + bottom_offset = height * 0.1 # Thinner bottom curve + + # Create smooth curves + top_curve = y - top_offset + bottom_curve = y + bottom_offset + + # Create curved endpoints with slight taper + end_points = 5 + start_x = np.linspace(x[0] - width * 0.1, x[0], end_points) + end_x = np.linspace(x[-1], x[-1] + width * 0.1, end_points) + + # Create tapered ends + start_curve = np.column_stack(( + start_x, + np.linspace(bottom_curve[0], top_curve[0], end_points) + )) + end_curve = np.column_stack(( + end_x, + 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 slight padding for better coverage + center = np.mean(contour_points, axis=0) + vectors = contour_points - center + padded_points = center + vectors * 1.15 # 15% padding + + return padded_points + return points + 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 @@ -335,33 +394,30 @@ def create_curved_eyebrow(points): 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) + # Fit quadratic curve through points for more natural arch + coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 2) # Changed to quadratic 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) + # Create points for top and bottom curves with consistent offsets + top_offset = height * 0.3 # Simplified offset for cleaner curve + bottom_offset = height * 0.1 # Thinner bottom curve + # Create smooth curves top_curve = y - top_offset bottom_curve = y + bottom_offset - # Create curved endpoints + # Create curved endpoints with slight taper end_points = 5 + start_x = np.linspace(x[0] - width * 0.1, x[0], end_points) + end_x = np.linspace(x[-1], x[-1] + width * 0.1, end_points) + + # Create tapered ends start_curve = np.column_stack(( - np.linspace(x[0] - width * 0.05, x[0], end_points), + start_x, 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), + end_x, np.linspace(bottom_curve[-1], top_curve[-1], end_points) )) @@ -373,13 +429,10 @@ def create_curved_eyebrow(points): np.column_stack((x[::-1], bottom_curve[::-1])) ]) - # Add padding and smooth the shape + # Add slight padding for better coverage 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) + padded_points = center + vectors * 1.15 # 15% padding return padded_points return points From a101a1f3f13eb30fb51693396352643aa337b20e Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:20:04 +0530 Subject: [PATCH 10/11] Reorganized switches --- modules/processors/frame/face_masking.py | 32 +++++++------- modules/ui.py | 56 ++++++++++++------------ 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/modules/processors/frame/face_masking.py b/modules/processors/frame/face_masking.py index d0dfb64c..4ee0885b 100644 --- a/modules/processors/frame/face_masking.py +++ b/modules/processors/frame/face_masking.py @@ -302,21 +302,21 @@ def create_curved_eyebrow(points): x = np.linspace(x_min, x_max, num_points) # Fit quadratic curve through points for more natural arch - coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 2) # Changed to quadratic + coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 2) y = np.polyval(coeffs, x) - # Create points for top and bottom curves with consistent offsets - top_offset = height * 0.3 # Simplified offset for cleaner curve - bottom_offset = height * 0.1 # Thinner bottom curve + # Increased offsets to create more separation + top_offset = height * 0.5 # Increased from 0.3 to shift up more + bottom_offset = height * 0.2 # Increased from 0.1 to shift down more # Create smooth curves top_curve = y - top_offset bottom_curve = y + bottom_offset - # Create curved endpoints with slight taper + # Create curved endpoints with more pronounced taper end_points = 5 - start_x = np.linspace(x[0] - width * 0.1, x[0], end_points) - end_x = np.linspace(x[-1], x[-1] + width * 0.1, end_points) + start_x = np.linspace(x[0] - width * 0.15, x[0], end_points) # Increased taper + end_x = np.linspace(x[-1], x[-1] + width * 0.15, end_points) # Increased taper # Create tapered ends start_curve = np.column_stack(( @@ -339,7 +339,7 @@ def create_curved_eyebrow(points): # Add slight padding for better coverage center = np.mean(contour_points, axis=0) vectors = contour_points - center - padded_points = center + vectors * 1.15 # 15% padding + padded_points = center + vectors * 1.2 # Increased padding slightly return padded_points return points @@ -395,21 +395,21 @@ def create_curved_eyebrow(points): x = np.linspace(x_min, x_max, num_points) # Fit quadratic curve through points for more natural arch - coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 2) # Changed to quadratic + coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 2) y = np.polyval(coeffs, x) - # Create points for top and bottom curves with consistent offsets - top_offset = height * 0.3 # Simplified offset for cleaner curve - bottom_offset = height * 0.1 # Thinner bottom curve + # Increased offsets to create more separation + top_offset = height * 0.5 # Increased from 0.3 to shift up more + bottom_offset = height * 0.2 # Increased from 0.1 to shift down more # Create smooth curves top_curve = y - top_offset bottom_curve = y + bottom_offset - # Create curved endpoints with slight taper + # Create curved endpoints with more pronounced taper end_points = 5 - start_x = np.linspace(x[0] - width * 0.1, x[0], end_points) - end_x = np.linspace(x[-1], x[-1] + width * 0.1, end_points) + start_x = np.linspace(x[0] - width * 0.15, x[0], end_points) # Increased taper + end_x = np.linspace(x[-1], x[-1] + width * 0.15, end_points) # Increased taper # Create tapered ends start_curve = np.column_stack(( @@ -432,7 +432,7 @@ def create_curved_eyebrow(points): # Add slight padding for better coverage center = np.mean(contour_points, axis=0) vectors = contour_points - center - padded_points = center + vectors * 1.15 # 15% padding + padded_points = center + vectors * 1.2 # Increased padding slightly return padded_points return points diff --git a/modules/ui.py b/modules/ui.py index c7c37cf6..a390f694 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -279,7 +279,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) show_fps_switch.place(relx=0.1, rely=0.75) - # Additional Options (Middle Right) + # Mask Switches (Middle Right - Top Section) mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask) mouth_mask_switch = ctk.CTkSwitch( root, @@ -290,57 +290,55 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) mouth_mask_switch.place(relx=0.6, rely=0.50) + eyes_mask_var = ctk.BooleanVar(value=modules.globals.eyes_mask) + eyes_mask_switch = ctk.CTkSwitch( + root, + text=_("Eyes Mask"), + variable=eyes_mask_var, + cursor="hand2", + command=lambda: setattr(modules.globals, "eyes_mask", eyes_mask_var.get()), + ) + eyes_mask_switch.place(relx=0.6, rely=0.55) + + eyebrows_mask_var = ctk.BooleanVar(value=modules.globals.eyebrows_mask) + eyebrows_mask_switch = ctk.CTkSwitch( + root, + text=_("Eyebrows Mask"), + variable=eyebrows_mask_var, + cursor="hand2", + command=lambda: setattr(modules.globals, "eyebrows_mask", eyebrows_mask_var.get()), + ) + eyebrows_mask_switch.place(relx=0.6, rely=0.60) + + # Box Visualization Switches (Middle Right - Bottom Section) 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"), + text=_("Show Mouth 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.55) + show_mouth_mask_box_switch.place(relx=0.6, rely=0.65) - # Add eyes mask switch - eyes_mask_var = ctk.BooleanVar(value=modules.globals.eyes_mask) - eyes_mask_switch = ctk.CTkSwitch( - root, - text=_("Eyes Mask"), - variable=eyes_mask_var, - cursor="hand2", - command=lambda: setattr(modules.globals, "eyes_mask", eyes_mask_var.get()), - ) - 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) show_eyes_mask_box_switch = ctk.CTkSwitch( root, - text=_("Show Eyes Mask Box"), + text=_("Show Eyes Box"), variable=show_eyes_mask_box_var, cursor="hand2", command=lambda: setattr( modules.globals, "show_eyes_mask_box", show_eyes_mask_box_var.get() ), ) - show_eyes_mask_box_switch.place(relx=0.6, rely=0.65) - - # Move the eyebrows mask switches up slightly - eyebrows_mask_var = ctk.BooleanVar(value=modules.globals.eyebrows_mask) - eyebrows_mask_switch = ctk.CTkSwitch( - root, - text=_("Eyebrows Mask"), - variable=eyebrows_mask_var, - cursor="hand2", - command=lambda: setattr(modules.globals, "eyebrows_mask", eyebrows_mask_var.get()), - ) - eyebrows_mask_switch.place(relx=0.6, rely=0.70) + show_eyes_mask_box_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"), + text=_("Show Eyebrows Box"), variable=show_eyebrows_mask_box_var, cursor="hand2", command=lambda: setattr( From 48c83151a463ad222b61a63fc34500f775037440 Mon Sep 17 00:00:00 2001 From: KRSHH <136873090+KRSHH@users.noreply.github.com> Date: Mon, 3 Feb 2025 20:59:06 +0530 Subject: [PATCH 11/11] size --- modules/globals.py | 3 +++ modules/processors/frame/face_masking.py | 27 +++++++++---------- modules/ui.py | 33 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/modules/globals.py b/modules/globals.py index 98eccf51..c4f2bbe4 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -41,9 +41,12 @@ mask_feather_ratio = 8 mask_down_size = 0.50 mask_size = 1 +mouth_mask_size = 1.0 eyes_mask = False show_eyes_mask_box = False eyebrows_mask = False show_eyebrows_mask_box = False +eyes_mask_size = 1.0 +eyebrows_mask_size = 1.0 use_fake_face = False fake_face_path = None diff --git a/modules/processors/frame/face_masking.py b/modules/processors/frame/face_masking.py index 4ee0885b..2a5f24a1 100644 --- a/modules/processors/frame/face_masking.py +++ b/modules/processors/frame/face_masking.py @@ -128,10 +128,10 @@ def create_lower_mouth_mask( # Calculate the center of the landmarks center = np.mean(lower_lip_landmarks, axis=0) - # Expand the landmarks outward + # Expand the landmarks outward using the mouth_mask_size expansion_factor = ( - 1 + modules.globals.mask_down_size - ) # Adjust this for more or less expansion + 1 + modules.globals.mask_down_size * modules.globals.mouth_mask_size + ) # Adjust expansion based on slider expanded_landmarks = (lower_lip_landmarks - center) * expansion_factor + center # Extend the top lip part @@ -145,8 +145,8 @@ def create_lower_mouth_mask( 5, ] # Indices for landmarks 2, 65, 66, 62, 70, 69, 18 toplip_extension = ( - modules.globals.mask_size * 0.5 - ) # Adjust this factor to control the extension + modules.globals.mask_size * modules.globals.mouth_mask_size * 0.5 + ) # Adjust extension based on slider for idx in toplip_indices: direction = expanded_landmarks[idx] - center direction = direction / np.linalg.norm(direction) @@ -219,12 +219,12 @@ def create_eyes_mask(face: Face, frame: Frame) -> (np.ndarray, np.ndarray, tuple left_eye_center = np.mean(left_eye, axis=0).astype(np.int32) right_eye_center = np.mean(right_eye, axis=0).astype(np.int32) - # Calculate eye dimensions + # Calculate eye dimensions with size adjustment def get_eye_dimensions(eye_points): x_coords = eye_points[:, 0] y_coords = eye_points[:, 1] - width = int((np.max(x_coords) - np.min(x_coords)) * (1 + modules.globals.mask_down_size)) - height = int((np.max(y_coords) - np.min(y_coords)) * (1 + modules.globals.mask_down_size)) + width = int((np.max(x_coords) - np.min(x_coords)) * (1 + modules.globals.mask_down_size * modules.globals.eyes_mask_size)) + height = int((np.max(y_coords) - np.min(y_coords)) * (1 + modules.globals.mask_down_size * modules.globals.eyes_mask_size)) return width, height left_width, left_height = get_eye_dimensions(left_eye) @@ -357,12 +357,13 @@ def create_eyebrows_mask(face: Face, frame: Frame) -> (np.ndarray, np.ndarray, t left_center = np.mean(left_eyebrow, axis=0) right_center = np.mean(right_eyebrow, axis=0) - # Calculate bounding box with padding + # Calculate bounding box with padding adjusted by size 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 + padding_factor = modules.globals.eyebrows_mask_size + min_x = np.min(all_points[:, 0]) - 25 * padding_factor + max_x = np.max(all_points[:, 0]) + 25 * padding_factor + min_y = np.min(all_points[:, 1]) - 20 * padding_factor + max_y = np.max(all_points[:, 1]) + 15 * padding_factor # Ensure coordinates are within frame bounds min_x = max(0, int(min_x)) diff --git a/modules/ui.py b/modules/ui.py index a390f694..ea77aabc 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -290,6 +290,17 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) mouth_mask_switch.place(relx=0.6, rely=0.50) + # Add mouth mask size slider + mouth_mask_size_slider = ctk.CTkSlider( + root, + from_=0.5, + to=2.0, + number_of_steps=30, + command=lambda value: setattr(modules.globals, "mouth_mask_size", value) + ) + mouth_mask_size_slider.set(modules.globals.mouth_mask_size) + mouth_mask_size_slider.place(relx=0.8, rely=0.50, relwidth=0.1) + eyes_mask_var = ctk.BooleanVar(value=modules.globals.eyes_mask) eyes_mask_switch = ctk.CTkSwitch( root, @@ -300,6 +311,17 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) eyes_mask_switch.place(relx=0.6, rely=0.55) + # Add eyes mask size slider + eyes_mask_size_slider = ctk.CTkSlider( + root, + from_=0.5, + to=2.0, + number_of_steps=30, + command=lambda value: setattr(modules.globals, "eyes_mask_size", value) + ) + eyes_mask_size_slider.set(modules.globals.eyes_mask_size) + eyes_mask_size_slider.place(relx=0.8, rely=0.55, relwidth=0.1) + eyebrows_mask_var = ctk.BooleanVar(value=modules.globals.eyebrows_mask) eyebrows_mask_switch = ctk.CTkSwitch( root, @@ -310,6 +332,17 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C ) eyebrows_mask_switch.place(relx=0.6, rely=0.60) + # Add eyebrows mask size slider + eyebrows_mask_size_slider = ctk.CTkSlider( + root, + from_=0.5, + to=2.0, + number_of_steps=30, + command=lambda value: setattr(modules.globals, "eyebrows_mask_size", value) + ) + eyebrows_mask_size_slider.set(modules.globals.eyebrows_mask_size) + eyebrows_mask_size_slider.place(relx=0.8, rely=0.60, relwidth=0.1) + # Box Visualization Switches (Middle Right - Bottom Section) show_mouth_mask_box_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box) show_mouth_mask_box_switch = ctk.CTkSwitch(