diff --git a/modules/core.py b/modules/core.py index c0c25f1..cb29643 100644 --- a/modules/core.py +++ b/modules/core.py @@ -20,6 +20,7 @@ import modules.metadata 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 0000000..f550109 Binary files /dev/null and b/modules/deeplivecam.ico differ diff --git a/modules/fake_face_handler.py b/modules/fake_face_handler.py new file mode 100644 index 0000000..f758c38 --- /dev/null +++ b/modules/fake_face_handler.py @@ -0,0 +1,120 @@ +import os +import requests +import tempfile +from pathlib import Path +import cv2 +import numpy as np +import modules.globals + +def add_padding_to_face(image, padding_ratio=0.3): + """Add padding around the face image + + Args: + image: The input face image + padding_ratio: Amount of padding to add as a ratio of image dimensions + + Returns: + Padded image with background padding added + """ + if image is None: + return None + + height, width = image.shape[:2] + pad_x = int(width * padding_ratio) + pad_y = int(height * padding_ratio) + + # Create larger image with padding + padded_height = height + 2 * pad_y + padded_width = width + 2 * pad_x + padded_image = np.zeros((padded_height, padded_width, 3), dtype=np.uint8) + + # Fill padded area with blurred and darkened edge pixels + edge_color = cv2.blur(image, (15, 15)) + edge_color = (edge_color * 0.6).astype(np.uint8) # Darken the padding + + # Fill the padded image with original face + padded_image[pad_y:pad_y+height, pad_x:pad_x+width] = image + + # Fill padding areas with edge color + # Top padding - repeat first row + top_edge = edge_color[0, :, :] + for i in range(pad_y): + padded_image[i, pad_x:pad_x+width] = top_edge + + # Bottom padding - repeat last row + bottom_edge = edge_color[-1, :, :] + for i in range(pad_y): + padded_image[pad_y+height+i, pad_x:pad_x+width] = bottom_edge + + # Left padding - repeat first column + left_edge = edge_color[:, 0, :] + for i in range(pad_x): + padded_image[pad_y:pad_y+height, i] = left_edge + + # Right padding - repeat last column + right_edge = edge_color[:, -1, :] + for i in range(pad_x): + padded_image[pad_y:pad_y+height, pad_x+width+i] = right_edge + + # Fill corners with nearest edge colors + # Top-left corner + padded_image[:pad_y, :pad_x] = edge_color[0, 0, :] + # Top-right corner + padded_image[:pad_y, pad_x+width:] = edge_color[0, -1, :] + # Bottom-left corner + padded_image[pad_y+height:, :pad_x] = edge_color[-1, 0, :] + # Bottom-right corner + padded_image[pad_y+height:, pad_x+width:] = edge_color[-1, -1, :] + + return padded_image + +def get_fake_face() -> 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 693084d..9bd9f4c 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -41,3 +41,5 @@ show_mouth_mask_box = False 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 c491dac..7a14f68 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -28,6 +28,7 @@ from modules.utilities import ( 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