Compare commits
	
		
			2 Commits 
		
	
	
		
			f38ebb485a
			...
			b6d6cfa2ee
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | b6d6cfa2ee | |
|  | 91d9f8a536 | 
|  | @ -371,11 +371,11 @@ For the latest experimental builds and features, see the [experimental branch](h | |||
| 
 | ||||
| **TODO:** | ||||
| 
 | ||||
| - [x] Support multiple faces  | ||||
| - [ ] Develop a version for web app/service | ||||
| - [ ] UI/UX enhancements for desktop app | ||||
| - [ ] Speed up model loading | ||||
| - [ ] Speed up real-time face swapping | ||||
| - [x] Support multiple faces  | ||||
| - [x] UI/UX enhancements for desktop app | ||||
| 
 | ||||
| This is an open-source project developed in our free time. Updates may be delayed. | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,29 +8,41 @@ import modules.processors.frame.core | |||
| from modules.core import update_status | ||||
| from modules.face_analyser import get_one_face, get_many_faces, default_source_face | ||||
| from modules.typing import Face, Frame | ||||
| from modules.utilities import conditional_download, resolve_relative_path, is_image, is_video | ||||
| from modules.utilities import ( | ||||
|     conditional_download, | ||||
|     resolve_relative_path, | ||||
|     is_image, | ||||
|     is_video, | ||||
| ) | ||||
| from modules.cluster_analysis import find_closest_centroid | ||||
| 
 | ||||
| FACE_SWAPPER = None | ||||
| THREAD_LOCK = threading.Lock() | ||||
| NAME = 'DLC.FACE-SWAPPER' | ||||
| NAME = "DLC.FACE-SWAPPER" | ||||
| 
 | ||||
| 
 | ||||
| def pre_check() -> bool: | ||||
|     download_directory_path = resolve_relative_path('../models') | ||||
|     conditional_download(download_directory_path, ['https://huggingface.co/hacksider/deep-live-cam/blob/main/inswapper_128.onnx']) | ||||
|     download_directory_path = resolve_relative_path("../models") | ||||
|     conditional_download( | ||||
|         download_directory_path, | ||||
|         ["https://huggingface.co/hacksider/deep-live-cam/blob/main/inswapper_128.onnx"], | ||||
|     ) | ||||
|     return True | ||||
| 
 | ||||
| 
 | ||||
| def pre_start() -> bool: | ||||
|     if not modules.globals.map_faces and not is_image(modules.globals.source_path): | ||||
|         update_status('Select an image for source path.', NAME) | ||||
|         update_status("Select an image for source path.", NAME) | ||||
|         return False | ||||
|     elif not modules.globals.map_faces and not get_one_face(cv2.imread(modules.globals.source_path)): | ||||
|         update_status('No face in source path detected.', NAME) | ||||
|     elif not modules.globals.map_faces and not get_one_face( | ||||
|         cv2.imread(modules.globals.source_path) | ||||
|     ): | ||||
|         update_status("No face in source path detected.", NAME) | ||||
|         return False | ||||
|     if not is_image(modules.globals.target_path) and not is_video(modules.globals.target_path): | ||||
|         update_status('Select an image or video for target path.', NAME) | ||||
|     if not is_image(modules.globals.target_path) and not is_video( | ||||
|         modules.globals.target_path | ||||
|     ): | ||||
|         update_status("Select an image or video for target path.", NAME) | ||||
|         return False | ||||
|     return True | ||||
| 
 | ||||
|  | @ -40,20 +52,28 @@ def get_face_swapper() -> Any: | |||
| 
 | ||||
|     with THREAD_LOCK: | ||||
|         if FACE_SWAPPER is None: | ||||
|             model_path = resolve_relative_path('../models/inswapper_128.onnx') | ||||
|             FACE_SWAPPER = insightface.model_zoo.get_model(model_path, providers=modules.globals.execution_providers) | ||||
|             model_path = resolve_relative_path("../models/inswapper_128.onnx") | ||||
|             FACE_SWAPPER = insightface.model_zoo.get_model( | ||||
|                 model_path, providers=modules.globals.execution_providers | ||||
|             ) | ||||
|     return FACE_SWAPPER | ||||
| 
 | ||||
| 
 | ||||
| def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame: | ||||
|     return get_face_swapper().get(temp_frame, target_face, source_face, paste_back=True) | ||||
|     swapped_face = get_face_swapper().get( | ||||
|         temp_frame, target_face, source_face, paste_back=True | ||||
|     ) | ||||
| 
 | ||||
|     # Apply opacity after swapping | ||||
|     opacity = modules.globals.face_opacity / 100 | ||||
|     return cv2.addWeighted(swapped_face, opacity, temp_frame, 1 - opacity, 0) | ||||
| 
 | ||||
| 
 | ||||
| def process_frame(source_face: Face, temp_frame: Frame) -> Frame: | ||||
|     # Ensure the frame is in RGB format if color correction is enabled | ||||
|     if modules.globals.color_correction: | ||||
|         temp_frame = cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB) | ||||
|          | ||||
| 
 | ||||
|     if modules.globals.many_faces: | ||||
|         many_faces = get_many_faces(temp_frame) | ||||
|         if many_faces: | ||||
|  | @ -71,34 +91,42 @@ def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame: | |||
|         if modules.globals.many_faces: | ||||
|             source_face = default_source_face() | ||||
|             for map in modules.globals.souce_target_map: | ||||
|                 target_face = map['target']['face'] | ||||
|                 target_face = map["target"]["face"] | ||||
|                 temp_frame = swap_face(source_face, target_face, temp_frame) | ||||
| 
 | ||||
|         elif not modules.globals.many_faces: | ||||
|             for map in modules.globals.souce_target_map: | ||||
|                 if "source" in map: | ||||
|                     source_face = map['source']['face'] | ||||
|                     target_face = map['target']['face']                | ||||
|                     source_face = map["source"]["face"] | ||||
|                     target_face = map["target"]["face"] | ||||
|                     temp_frame = swap_face(source_face, target_face, temp_frame) | ||||
| 
 | ||||
|     elif is_video(modules.globals.target_path): | ||||
|         if modules.globals.many_faces: | ||||
|             source_face = default_source_face() | ||||
|             for map in modules.globals.souce_target_map: | ||||
|                 target_frame = [f for f in map['target_faces_in_frame'] if f['location'] == temp_frame_path] | ||||
|                 target_frame = [ | ||||
|                     f | ||||
|                     for f in map["target_faces_in_frame"] | ||||
|                     if f["location"] == temp_frame_path | ||||
|                 ] | ||||
| 
 | ||||
|                 for frame in target_frame: | ||||
|                     for target_face in frame['faces']: | ||||
|                     for target_face in frame["faces"]: | ||||
|                         temp_frame = swap_face(source_face, target_face, temp_frame) | ||||
| 
 | ||||
|         elif not modules.globals.many_faces: | ||||
|             for map in modules.globals.souce_target_map: | ||||
|                 if "source" in map: | ||||
|                     target_frame = [f for f in map['target_faces_in_frame'] if f['location'] == temp_frame_path] | ||||
|                     source_face = map['source']['face'] | ||||
|                     target_frame = [ | ||||
|                         f | ||||
|                         for f in map["target_faces_in_frame"] | ||||
|                         if f["location"] == temp_frame_path | ||||
|                     ] | ||||
|                     source_face = map["source"]["face"] | ||||
| 
 | ||||
|                     for frame in target_frame: | ||||
|                         for target_face in frame['faces']: | ||||
|                         for target_face in frame["faces"]: | ||||
|                             temp_frame = swap_face(source_face, target_face, temp_frame) | ||||
|     else: | ||||
|         detected_faces = get_many_faces(temp_frame) | ||||
|  | @ -110,25 +138,46 @@ def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame: | |||
| 
 | ||||
|         elif not modules.globals.many_faces: | ||||
|             if detected_faces: | ||||
|                 if len(detected_faces) <= len(modules.globals.simple_map['target_embeddings']): | ||||
|                 if len(detected_faces) <= len( | ||||
|                     modules.globals.simple_map["target_embeddings"] | ||||
|                 ): | ||||
|                     for detected_face in detected_faces: | ||||
|                         closest_centroid_index, _ = find_closest_centroid(modules.globals.simple_map['target_embeddings'], detected_face.normed_embedding) | ||||
|                         closest_centroid_index, _ = find_closest_centroid( | ||||
|                             modules.globals.simple_map["target_embeddings"], | ||||
|                             detected_face.normed_embedding, | ||||
|                         ) | ||||
| 
 | ||||
|                         temp_frame = swap_face(modules.globals.simple_map['source_faces'][closest_centroid_index], detected_face, temp_frame) | ||||
|                         temp_frame = swap_face( | ||||
|                             modules.globals.simple_map["source_faces"][ | ||||
|                                 closest_centroid_index | ||||
|                             ], | ||||
|                             detected_face, | ||||
|                             temp_frame, | ||||
|                         ) | ||||
|                 else: | ||||
|                     detected_faces_centroids = [] | ||||
|                     for face in detected_faces: | ||||
|                             detected_faces_centroids.append(face.normed_embedding) | ||||
|                         detected_faces_centroids.append(face.normed_embedding) | ||||
|                     i = 0 | ||||
|                     for target_embedding in modules.globals.simple_map['target_embeddings']: | ||||
|                         closest_centroid_index, _ = find_closest_centroid(detected_faces_centroids, target_embedding) | ||||
|                     for target_embedding in modules.globals.simple_map[ | ||||
|                         "target_embeddings" | ||||
|                     ]: | ||||
|                         closest_centroid_index, _ = find_closest_centroid( | ||||
|                             detected_faces_centroids, target_embedding | ||||
|                         ) | ||||
| 
 | ||||
|                         temp_frame = swap_face(modules.globals.simple_map['source_faces'][i], detected_faces[closest_centroid_index], temp_frame) | ||||
|                         temp_frame = swap_face( | ||||
|                             modules.globals.simple_map["source_faces"][i], | ||||
|                             detected_faces[closest_centroid_index], | ||||
|                             temp_frame, | ||||
|                         ) | ||||
|                         i += 1 | ||||
|     return temp_frame | ||||
| 
 | ||||
| 
 | ||||
| def process_frames(source_path: str, temp_frame_paths: List[str], progress: Any = None) -> None: | ||||
| def process_frames( | ||||
|     source_path: str, temp_frame_paths: List[str], progress: Any = None | ||||
| ) -> None: | ||||
|     if not modules.globals.map_faces: | ||||
|         source_face = get_one_face(cv2.imread(source_path)) | ||||
|         for temp_frame_path in temp_frame_paths: | ||||
|  | @ -162,7 +211,9 @@ def process_image(source_path: str, target_path: str, output_path: str) -> None: | |||
|         cv2.imwrite(output_path, result) | ||||
|     else: | ||||
|         if modules.globals.many_faces: | ||||
|             update_status('Many faces enabled. Using first source image. Progressing...', NAME) | ||||
|             update_status( | ||||
|                 "Many faces enabled. Using first source image. Progressing...", NAME | ||||
|             ) | ||||
|         target_frame = cv2.imread(output_path) | ||||
|         result = process_frame_v2(target_frame) | ||||
|         cv2.imwrite(output_path, result) | ||||
|  | @ -170,5 +221,9 @@ def process_image(source_path: str, target_path: str, output_path: str) -> None: | |||
| 
 | ||||
| def process_video(source_path: str, temp_frame_paths: List[str]) -> None: | ||||
|     if modules.globals.map_faces and modules.globals.many_faces: | ||||
|         update_status('Many faces enabled. Using first source image. Progressing...', NAME) | ||||
|     modules.processors.frame.core.process_video(source_path, temp_frame_paths, process_frames) | ||||
|         update_status( | ||||
|             "Many faces enabled. Using first source image. Progressing...", NAME | ||||
|         ) | ||||
|     modules.processors.frame.core.process_video( | ||||
|         source_path, temp_frame_paths, process_frames | ||||
|     ) | ||||
|  |  | |||
								
									
									
										
											122
										
									
									modules/ui.py
									
									
									
									
								
								
							
							
										
											122
										
									
									modules/ui.py
									
									
									
									
								|  | @ -3,7 +3,6 @@ import webbrowser | |||
| import customtkinter as ctk | ||||
| from typing import Callable, Tuple | ||||
| import cv2 | ||||
| from cv2_enumerate_cameras import enumerate_cameras | ||||
| from PIL import Image, ImageOps | ||||
| import tkinterdnd2 as tkdnd | ||||
| import time | ||||
|  | @ -404,33 +403,11 @@ def create_root( | |||
|     ) | ||||
|     preview_button.pack(side="left", padx=10, expand=True) | ||||
| 
 | ||||
|     # --- Camera Selection --- | ||||
|     camera_label = ctk.CTkLabel(root, text="Select Camera:") | ||||
|     camera_label.place(relx=0.4, rely=0.86, relwidth=0.2, relheight=0.05) | ||||
|     available_cameras = get_available_cameras() | ||||
|     # Convert camera indices to strings for CTkOptionMenu | ||||
|     available_camera_indices, available_camera_strings = available_cameras | ||||
|     camera_variable = ctk.StringVar( | ||||
|         value=available_camera_strings[0] | ||||
|         if available_camera_strings | ||||
|         else "No cameras found" | ||||
|     ) | ||||
|     camera_optionmenu = ctk.CTkOptionMenu( | ||||
|         root, variable=camera_variable, values=available_camera_strings | ||||
|     ) | ||||
|     camera_optionmenu.place(relx=0.65, rely=0.86, relwidth=0.2, relheight=0.05) | ||||
|     # --- End Camera Selection --- | ||||
| 
 | ||||
|     live_button = ModernButton( | ||||
|         button_frame, | ||||
|         text="Live", | ||||
|         cursor="hand2", | ||||
|         command=lambda: webcam_preview( | ||||
|             root, | ||||
|             available_camera_indices[ | ||||
|                 available_camera_strings.index(camera_variable.get()) | ||||
|             ], | ||||
|         ), | ||||
|         command=lambda: webcam_preview(root), | ||||
|     ) | ||||
|     live_button.pack(side="left", padx=10, expand=True) | ||||
| 
 | ||||
|  | @ -478,6 +455,29 @@ def create_root( | |||
|     ) | ||||
|     remove_donate_button.pack(side="right", padx=(10, 0)) | ||||
| 
 | ||||
|     # Add opacity slider | ||||
|     opacity_frame = ctk.CTkFrame(options_column, fg_color="#2a2d2e") | ||||
|     opacity_frame.pack(pady=5, anchor="w", fill="x") | ||||
| 
 | ||||
|     opacity_label = ctk.CTkLabel( | ||||
|         opacity_frame, text="Face Opacity:", font=("Roboto", 14, "bold") | ||||
|     ) | ||||
|     opacity_label.pack(side="left", padx=(0, 10)) | ||||
| 
 | ||||
|     opacity_slider = ctk.CTkSlider( | ||||
|         opacity_frame, | ||||
|         from_=0, | ||||
|         to=100, | ||||
|         number_of_steps=100, | ||||
|         command=update_opacity, | ||||
|         fg_color=("gray75", "gray25"), | ||||
|         progress_color="#3a7ebf", | ||||
|         button_color="#3a7ebf", | ||||
|         button_hover_color="#2b5d8b", | ||||
|     ) | ||||
|     opacity_slider.pack(side="left", fill="x", expand=True) | ||||
|     opacity_slider.set(modules.globals.face_opacity) | ||||
| 
 | ||||
|     main_frame.grid_columnconfigure((0, 2), weight=1) | ||||
|     main_frame.grid_rowconfigure((0, 1, 2), weight=1) | ||||
| 
 | ||||
|  | @ -706,6 +706,29 @@ def create_preview(parent: ctk.CTkToplevel) -> ctk.CTkToplevel: | |||
|     ) | ||||
|     preview_slider.pack(fill="x", padx=20, pady=10) | ||||
| 
 | ||||
|     last_update_time = 0 | ||||
|     debounce_delay = 0.1  # Adjust this delay as needed (in seconds) | ||||
| 
 | ||||
|     def on_key_press(event): | ||||
|         nonlocal last_update_time | ||||
| 
 | ||||
|         current_time = time.time() | ||||
|         if current_time - last_update_time > debounce_delay: | ||||
|             current_frame = int(preview_slider.get()) | ||||
|             if event.keysym == "Left": | ||||
|                 new_frame = max(0, current_frame - 1) | ||||
|             elif event.keysym == "Right": | ||||
|                 new_frame = min(int(preview_slider.cget("to")), current_frame + 1) | ||||
|             else: | ||||
|                 return  # Ignore other key presses | ||||
| 
 | ||||
|             preview_slider.set(new_frame) | ||||
|             update_preview(new_frame) | ||||
|             last_update_time = current_time | ||||
| 
 | ||||
|     preview.bind("<Left>", on_key_press) | ||||
|     preview.bind("<Right>", on_key_press) | ||||
| 
 | ||||
|     return preview | ||||
| 
 | ||||
| 
 | ||||
|  | @ -903,6 +926,11 @@ def init_preview() -> None: | |||
|         preview_slider.configure(to=video_frame_total) | ||||
|         preview_slider.pack(fill="x") | ||||
|         preview_slider.set(0) | ||||
|     # Disable slider if it's an image | ||||
|     if is_image(modules.globals.target_path): | ||||
|         preview_slider.configure(state="disabled") | ||||
|     else: | ||||
|         preview_slider.configure(state="normal") | ||||
| 
 | ||||
| 
 | ||||
| def update_preview(frame_number: int = 0) -> None: | ||||
|  | @ -955,44 +983,26 @@ def update_preview(frame_number: int = 0) -> None: | |||
|         PREVIEW.deiconify() | ||||
| 
 | ||||
| 
 | ||||
| def webcam_preview(root: ctk.CTk, camera_index: int): | ||||
| def webcam_preview(root: ctk.CTk): | ||||
|     if not modules.globals.map_faces: | ||||
|         if modules.globals.source_path is None: | ||||
|             # No image selected | ||||
|             return | ||||
|         create_webcam_preview(camera_index) | ||||
|         create_webcam_preview() | ||||
|     else: | ||||
|         modules.globals.souce_target_map = [] | ||||
|         create_source_target_popup_for_webcam(root, modules.globals.souce_target_map) | ||||
| 
 | ||||
| 
 | ||||
| def get_available_cameras(): | ||||
|     """Returns a list of available camera names and indices.""" | ||||
|     camera_indices = [] | ||||
|     camera_names = [] | ||||
| 
 | ||||
|     for camera in enumerate_cameras(): | ||||
|         cap = cv2.VideoCapture(camera.index) | ||||
|         if cap.isOpened(): | ||||
|             camera_indices.append(camera.index) | ||||
|             camera_names.append(camera.name) | ||||
|             cap.release() | ||||
|     return (camera_indices, camera_names) | ||||
| 
 | ||||
| 
 | ||||
| # Add this function to update the opacity value | ||||
| def update_opacity(value): | ||||
|     modules.globals.face_opacity = int(value) | ||||
| 
 | ||||
| 
 | ||||
| # Modify the create_webcam_preview function to include the slider | ||||
| def create_webcam_preview(camera_index): | ||||
| def create_webcam_preview(): | ||||
|     global preview_label, PREVIEW | ||||
| 
 | ||||
|     camera = cv2.VideoCapture(camera_index) | ||||
|     if not camera.isOpened(): | ||||
|         update_status(f"Error: Could not open camera with index {camera_index}") | ||||
|         return | ||||
|     camera = cv2.VideoCapture(0) | ||||
|     camera.set(cv2.CAP_PROP_FRAME_WIDTH, PREVIEW_DEFAULT_WIDTH) | ||||
|     camera.set(cv2.CAP_PROP_FRAME_HEIGHT, PREVIEW_DEFAULT_HEIGHT) | ||||
|     camera.set(cv2.CAP_PROP_FPS, 60) | ||||
|  | @ -1030,15 +1040,10 @@ def create_webcam_preview(camera_index): | |||
|         if not ret: | ||||
|             break | ||||
| 
 | ||||
|         temp_frame = frame.copy()  # Create a copy of the frame | ||||
|         temp_frame = frame.copy() | ||||
| 
 | ||||
|         if modules.globals.live_mirror: | ||||
|             temp_frame = cv2.flip(temp_frame, 1)  # horizontal flipping | ||||
| 
 | ||||
|         if modules.globals.live_resizable: | ||||
|             temp_frame = fit_image_to_size( | ||||
|                 temp_frame, PREVIEW.winfo_width(), PREVIEW.winfo_height() | ||||
|             ) | ||||
|             temp_frame = cv2.flip(temp_frame, 1) | ||||
| 
 | ||||
|         if not modules.globals.map_faces: | ||||
|             if source_image is None and modules.globals.source_path: | ||||
|  | @ -1046,18 +1051,19 @@ def create_webcam_preview(camera_index): | |||
| 
 | ||||
|             for frame_processor in frame_processors: | ||||
|                 temp_frame = frame_processor.process_frame(source_image, temp_frame) | ||||
| 
 | ||||
|         else: | ||||
|             modules.globals.target_path = None | ||||
| 
 | ||||
|             for frame_processor in frame_processors: | ||||
|                 temp_frame = frame_processor.process_frame_v2(temp_frame) | ||||
| 
 | ||||
|         image = cv2.cvtColor( | ||||
|             temp_frame, cv2.COLOR_BGR2RGB | ||||
|         )  # Convert the image to RGB format to display it with Tkinter | ||||
|         image = cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB) | ||||
|         image = Image.fromarray(image) | ||||
|         image = ImageOps.contain( | ||||
|             image, (temp_frame.shape[1], temp_frame.shape[0]), Image.LANCZOS | ||||
|             image, | ||||
|             (preview_frame.winfo_width(), preview_frame.winfo_height()), | ||||
|             Image.LANCZOS, | ||||
|         ) | ||||
|         image = ctk.CTkImage(image, size=image.size) | ||||
|         preview_label.configure(image=image) | ||||
|  | @ -1067,7 +1073,7 @@ def create_webcam_preview(camera_index): | |||
|             break | ||||
| 
 | ||||
|     camera.release() | ||||
|     PREVIEW.withdraw()  # Close preview window when loop is finished | ||||
|     PREVIEW.withdraw() | ||||
| 
 | ||||
| 
 | ||||
| def create_source_target_popup_for_webcam(root: ctk.CTk, map: list) -> None: | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ | |||
| 
 | ||||
| numpy>=1.23.5,<2 | ||||
| opencv-python==4.8.1.78 | ||||
| cv2_enumerate_cameras==1.1.15 | ||||
| onnx==1.16.0 | ||||
| insightface==0.7.3 | ||||
| psutil==5.9.8 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue