Compare commits

...

10 Commits

Author SHA1 Message Date
Kenneth Estanislao d9a5500bdf Merge pull request #1713 from TeachDian/fix-1705-wsl-onnxruntime-gpu 2026-03-29 04:54:34 +08:00
TeachDian 86134b6e1d Fix #1705: Update onnxruntime-gpu requirement to 1.23.2 for WSL compatibility 2026-03-29 04:46:48 +08:00
Kenneth Estanislao 9e6f30c0a4 silenced deprecation 2026-03-27 21:35:27 +08:00
Kenneth Estanislao 97321a740d Update face_analyser.py
320 was over optimized, put back to 640
2026-03-27 21:24:19 +08:00
Kenneth Estanislao f5f7ac7764 Revise README for clarity and formatting
Updated README to remove emoji and clarify GPU support details.
2026-03-23 10:02:50 +08:00
Kenneth Estanislao 77d3492eef Add download link for models in README
Added a section for downloading models from Hugging Face.
2026-03-13 23:39:46 +08:00
Kenneth Estanislao 8e3d6e7c65 Add emoji to project title in README
Just want to add an emoji 😝
2026-03-13 22:17:32 +08:00
Kenneth Estanislao ee9699ee70 Happy 80k!
2.1 Released!

- Face randomizer added!
2026-03-13 22:09:18 +08:00
Kenneth Estanislao 3c8b259a3f Some edits on the UI
- Grouped the face enhancers
- Make the mouth mask just a slider
- Removed the redundant switches
2026-03-13 22:03:28 +08:00
Kenneth Estanislao 30b27c2b71 Update Quick Start section to v2.7 beta 2026-03-12 02:40:52 +08:00
9 changed files with 217 additions and 192 deletions
+6 -3
View File
@@ -1,4 +1,4 @@
<h1 align="center">Deep-Live-Cam 2.0.5c</h1> <h1 align="center">Deep-Live-Cam 2.1</h1>
<p align="center"> <p align="center">
Real-time face swap and video deepfake with a single click and only a single image. Real-time face swap and video deepfake with a single click and only a single image.
@@ -30,11 +30,11 @@ By using this software, you agree to these terms and commit to using it in a man
Users are expected to use this software responsibly and legally. If using a real person's face, obtain their consent and clearly label any output as a deepfake when sharing online. We are not responsible for end-user actions. Users are expected to use this software responsibly and legally. If using a real person's face, obtain their consent and clearly label any output as a deepfake when sharing online. We are not responsible for end-user actions.
## Exclusive v2.6d Quick Start - Pre-built (Windows/Mac Silicon) ## Exclusive v2.7 beta Quick Start - Pre-built (Windows/Mac Silicon/CPU)
<a href="https://deeplivecam.net/index.php/quickstart"> <img src="media/Download.png" width="285" height="77" /> <a href="https://deeplivecam.net/index.php/quickstart"> <img src="media/Download.png" width="285" height="77" />
##### This is the fastest build you can get if you have a discrete NVIDIA or AMD GPU or Mac Silicon, And you'll receive special priority support. ##### This is the fastest build you can get if you have a discrete NVIDIA or AMD GPU, CPU or Mac Silicon, And you'll receive special priority support. 2.7 beta is the best you can have with 30+ extra features than the open source version.
###### These Pre-builts are perfect for non-technical users or those who don't have time to, or can't manually install all the requirements. Just a heads-up: this is an open-source project, so you can also install it manually. ###### These Pre-builts are perfect for non-technical users or those who don't have time to, or can't manually install all the requirements. Just a heads-up: this is an open-source project, so you can also install it manually.
@@ -309,6 +309,9 @@ python run.py --execution-provider openvino
- Use a screen capture tool like OBS to stream. - Use a screen capture tool like OBS to stream.
- To change the face, select a new source image. - To change the face, select a new source image.
## Download all models in this huggingface link
- [**Download models here**](https://huggingface.co/hacksider/deep-live-cam/tree/main)
## Command Line Arguments (Unmaintained) ## Command Line Arguments (Unmaintained)
``` ```
+2 -2
View File
@@ -28,9 +28,9 @@ def get_face_analyser() -> Any:
FACE_ANALYSER = insightface.app.FaceAnalysis( FACE_ANALYSER = insightface.app.FaceAnalysis(
name='buffalo_l', name='buffalo_l',
providers=modules.globals.execution_providers, providers=modules.globals.execution_providers,
allowed_modules=['detection', 'recognition'] allowed_modules=['detection', 'recognition', 'landmark_2d_106']
) )
FACE_ANALYSER.prepare(ctx_id=0, det_size=(320, 320)) FACE_ANALYSER.prepare(ctx_id=0, det_size=(640, 640))
return FACE_ANALYSER return FACE_ANALYSER
+1
View File
@@ -63,6 +63,7 @@ show_mouth_mask_box: bool = False # Visualize the mouth mask area (for debuggin
mask_feather_ratio: int = 12 # Denominator for feathering calculation (higher = smaller feather) mask_feather_ratio: int = 12 # Denominator for feathering calculation (higher = smaller feather)
mask_down_size: float = 0.1 # Expansion factor for lower lip mask (relative) mask_down_size: float = 0.1 # Expansion factor for lower lip mask (relative)
mask_size: float = 1.0 # Expansion factor for upper lip mask (relative) mask_size: float = 1.0 # Expansion factor for upper lip mask (relative)
mouth_mask_size: float = 0.0 # Mouth mask size (0-100; 0=off, 100=mouth to chin)
# --- START: Added for Frame Interpolation --- # --- START: Added for Frame Interpolation ---
enable_interpolation: bool = True # Toggle temporal smoothing enable_interpolation: bool = True # Toggle temporal smoothing
+1 -1
View File
@@ -1,3 +1,3 @@
name = 'Deep-Live-Cam' name = 'Deep-Live-Cam'
version = '2.0.3c' version = '2.1'
edition = 'GitHub Edition' edition = 'GitHub Edition'
+11 -8
View File
@@ -82,8 +82,8 @@ def create_lower_mouth_mask(
landmarks = face.landmark_2d_106 landmarks = face.landmark_2d_106
if landmarks is not None: if landmarks is not None:
# Use outer mouth landmarks (52-63) to capture the lips only # Use outer mouth landmarks (52-71) to capture the full mouth area
lower_lip_order = list(range(52, 64)) lower_lip_order = list(range(52, 72))
if max(lower_lip_order) >= landmarks.shape[0]: if max(lower_lip_order) >= landmarks.shape[0]:
return mask, mouth_cutout, mouth_box, lower_lip_polygon return mask, mouth_cutout, mouth_box, lower_lip_polygon
@@ -94,13 +94,16 @@ def create_lower_mouth_mask(
center = np.mean(lower_lip_landmarks, axis=0) center = np.mean(lower_lip_landmarks, axis=0)
# Expand the landmarks outward using the mouth_mask_size # Expand the landmarks outward using the mouth_mask_size
# Use a more conservative expansion to avoid affecting face shape mouth_mask_size = getattr(modules.globals, "mouth_mask_size", 0.0) # 0-100 slider
expansion_factor = ( expansion_factor = 1 + (mouth_mask_size / 100.0) * 2.5
1 + modules.globals.mask_down_size * modules.globals.mouth_mask_size
)
expanded_landmarks = (lower_lip_landmarks - center) * expansion_factor + center
# Removed specific top/chin extensions to preserve face shape # Expand with extra downward bias toward chin
offsets = lower_lip_landmarks - center
chin_bias = 1 + (mouth_mask_size / 100.0) * 1.5
scale_y = np.where(offsets[:, 1] > 0, expansion_factor * chin_bias, expansion_factor)
expanded_landmarks = lower_lip_landmarks.copy()
expanded_landmarks[:, 0] = center[0] + offsets[:, 0] * expansion_factor
expanded_landmarks[:, 1] = center[1] + offsets[:, 1] * scale_y
# Convert back to integer coordinates # Convert back to integer coordinates
expanded_landmarks = expanded_landmarks.astype(np.int32) expanded_landmarks = expanded_landmarks.astype(np.int32)
+48 -88
View File
@@ -136,10 +136,12 @@ def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame:
if not hasattr(source_face, 'normed_embedding') or source_face.normed_embedding is None: if not hasattr(source_face, 'normed_embedding') or source_face.normed_embedding is None:
return temp_frame return temp_frame
# Store a copy of the original frame before swapping for opacity blending # Store a copy of the original frame before swapping for opacity blending and mouth mask
opacity = getattr(modules.globals, "opacity", 1.0) opacity = getattr(modules.globals, "opacity", 1.0)
opacity = max(0.0, min(1.0, opacity)) opacity = max(0.0, min(1.0, opacity))
original_frame = temp_frame if opacity >= 1.0 else temp_frame.copy() mouth_mask_enabled = getattr(modules.globals, "mouth_mask", False)
# Always copy if mouth mask is enabled (we need the unmodified original for mouth cutout)
original_frame = temp_frame.copy() if (opacity < 1.0 or mouth_mask_enabled) else temp_frame
# Pre-swap Input Check with optimization # Pre-swap Input Check with optimization
if temp_frame.dtype != np.uint8: if temp_frame.dtype != np.uint8:
@@ -190,28 +192,28 @@ def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame:
# --- Post-swap Processing (Masking, Opacity, etc.) --- # --- Post-swap Processing (Masking, Opacity, etc.) ---
# Now, work with the guaranteed uint8 'swapped_frame' # Now, work with the guaranteed uint8 'swapped_frame'
if getattr(modules.globals, "mouth_mask", False): # Check if mouth_mask is enabled if mouth_mask_enabled: # Check if mouth_mask is enabled
# Create a mask for the target face # Create a mask for the target face
face_mask = create_face_mask(target_face, temp_frame) # Use temp_frame (original shape) for mask creation geometry face_mask = create_face_mask(target_face, original_frame) # Use original_frame for mask creation geometry
# Create the mouth mask using original geometry # Create the mouth mask using the ORIGINAL frame (before swap) for cutout
mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon = ( mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon = (
create_lower_mouth_mask(target_face, temp_frame) # Use temp_frame (original) for cutout create_lower_mouth_mask(target_face, original_frame) # Use original_frame for real mouth cutout
) )
# Apply the mouth area only if mouth_cutout exists # Apply the mouth area only if mouth_cutout exists
if mouth_cutout is not None and mouth_box != (0,0,0,0): # Add check for valid box if mouth_cutout is not None and mouth_box != (0,0,0,0):
# Apply mouth area (from original) onto the 'swapped_frame' # Apply mouth area (from original) onto the 'swapped_frame'
swapped_frame = apply_mouth_area( swapped_frame = apply_mouth_area(
swapped_frame, mouth_cutout, mouth_box, face_mask, lower_lip_polygon swapped_frame, mouth_cutout, mouth_box, face_mask, lower_lip_polygon
) )
# Draw bounding box only while slider is being dragged
if getattr(modules.globals, "show_mouth_mask_box", False): if getattr(modules.globals, "show_mouth_mask_box", False):
mouth_mask_data = (mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon) mouth_mask_data = (mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon)
# Draw visualization on the swapped_frame *before* opacity blending swapped_frame = draw_mouth_mask_visualization(
swapped_frame = draw_mouth_mask_visualization( swapped_frame, target_face, mouth_mask_data
swapped_frame, target_face, mouth_mask_data )
)
# --- Poisson Blending --- # --- Poisson Blending ---
if getattr(modules.globals, "poisson_blend", False): if getattr(modules.globals, "poisson_blend", False):
@@ -750,9 +752,9 @@ def create_lower_mouth_mask(
return mask, mouth_cutout, mouth_box, lower_lip_polygon return mask, mouth_cutout, mouth_box, lower_lip_polygon
try: # Wrap main logic in try-except try: # Wrap main logic in try-except
# Use outer mouth landmarks (52-63) to capture the lips only # Use outer mouth landmarks (52-71) to capture the full mouth area
# This avoids including the chin/jawline, preserving the face shape from the swap # This covers both upper and lower lips for proper mouth preservation
lower_lip_order = list(range(52, 64)) lower_lip_order = list(range(52, 72))
# Check if all indices are valid for the loaded landmarks (already partially done by < 106 check) # Check if all indices are valid for the loaded landmarks (already partially done by < 106 check)
if max(lower_lip_order) >= landmarks.shape[0]: if max(lower_lip_order) >= landmarks.shape[0]:
@@ -772,9 +774,18 @@ def create_lower_mouth_mask(
return mask, mouth_cutout, mouth_box, lower_lip_polygon return mask, mouth_cutout, mouth_box, lower_lip_polygon
mask_down_size = getattr(modules.globals, "mask_down_size", 0.1) # Default 0.1 mouth_mask_size = getattr(modules.globals, "mouth_mask_size", 0.0) # 0-100 slider
expansion_factor = 1 + mask_down_size # 0=tight lip outline, 50=covers mouth area, 100=mouth to chin
expanded_landmarks = (lower_lip_landmarks - center) * expansion_factor + center expansion_factor = 1 + (mouth_mask_size / 100.0) * 2.5
# Expand landmarks from center, with extra downward bias toward chin
offsets = lower_lip_landmarks - center
# Add extra downward expansion for points below center (toward chin)
chin_bias = 1 + (mouth_mask_size / 100.0) * 1.5 # extra vertical stretch downward
scale_y = np.where(offsets[:, 1] > 0, expansion_factor * chin_bias, expansion_factor)
expanded_landmarks = lower_lip_landmarks.copy()
expanded_landmarks[:, 0] = center[0] + offsets[:, 0] * expansion_factor
expanded_landmarks[:, 1] = center[1] + offsets[:, 1] * scale_y
# Ensure landmarks are finite after adjustments # Ensure landmarks are finite after adjustments
if not np.all(np.isfinite(expanded_landmarks)): if not np.all(np.isfinite(expanded_landmarks)):
@@ -881,8 +892,8 @@ def draw_mouth_mask_visualization(
print(f"Error drawing polygon for visualization: {e}") # Optional debug print(f"Error drawing polygon for visualization: {e}") # Optional debug
pass pass
# Optional: Draw bounding box (red rectangle) # Draw bounding box (red rectangle)
# cv2.rectangle(vis_frame, (min_x, min_y), (max_x, max_y), (0, 0, 255), 1) cv2.rectangle(vis_frame, (min_x, min_y), (max_x, max_y), (0, 0, 255), 2)
# Optional: Add labels # Optional: Add labels
label_pos_y = min_y - 10 if min_y > 20 else max_y + 15 # Adjust position based on box location label_pos_y = min_y - 10 if min_y > 20 else max_y + 15 # Adjust position based on box location
@@ -962,85 +973,34 @@ def apply_mouth_area(
# print("Warning: Mouth cutout is invalid after resize attempt.") # print("Warning: Mouth cutout is invalid after resize attempt.")
return frame return frame
# --- Color Correction Step ---
# Apply color transfer from ROI (swapped face region) to the original mouth cutout
# This helps match lighting/color before blending
color_corrected_mouth = resized_mouth_cutout # Default to resized if correction fails
try:
# Ensure both images are 3 channels for color transfer
if len(resized_mouth_cutout.shape) == 3 and resized_mouth_cutout.shape[2] == 3 and \
len(roi.shape) == 3 and roi.shape[2] == 3:
color_corrected_mouth = apply_color_transfer(resized_mouth_cutout, roi)
else:
# print("Warning: Cannot apply color transfer, images not BGR.")
pass
except cv2.error as ct_e: # Handle potential errors in color transfer
# print(f"Warning: Color transfer failed: {ct_e}. Using uncorrected mouth cutout.") # Optional debug
pass
except Exception as ct_gen_e:
# print(f"Warning: Unexpected error during color transfer: {ct_gen_e}")
pass
# --- End Color Correction ---
# --- Mask Creation --- # --- Mask Creation ---
# Create a mask based *specifically* on the mouth_polygon, relative to the ROI # Create a mask based on the mouth_polygon, relative to the ROI
polygon_mask_roi = np.zeros(roi.shape[:2], dtype=np.uint8) polygon_mask_roi = np.zeros(roi.shape[:2], dtype=np.uint8)
# Adjust polygon coordinates relative to the ROI's top-left corner
adjusted_polygon = mouth_polygon - [min_x, min_y] adjusted_polygon = mouth_polygon - [min_x, min_y]
# Draw the filled polygon on the ROI mask
cv2.fillPoly(polygon_mask_roi, [adjusted_polygon.astype(np.int32)], 255) cv2.fillPoly(polygon_mask_roi, [adjusted_polygon.astype(np.int32)], 255)
# Feather the polygon mask (Gaussian blur) # Feather the edges with Gaussian blur for smooth blending
mask_feather_ratio = getattr(modules.globals, "mask_feather_ratio", 12) # Default 12 feather_amount = max(1, min(30, min(box_width, box_height) // 8))
# Calculate feather amount based on the smaller dimension of the box
feather_base_dim = min(box_width, box_height)
feather_amount = max(1, min(30, feather_base_dim // max(1, mask_feather_ratio))) # Avoid div by zero
# Ensure kernel size is odd and positive
kernel_size = 2 * feather_amount + 1 kernel_size = 2 * feather_amount + 1
feathered_polygon_mask = cv2.GaussianBlur(polygon_mask_roi.astype(np.float32), (kernel_size, kernel_size), 0) feathered_mask = cv2.GaussianBlur(polygon_mask_roi.astype(np.float32), (kernel_size, kernel_size), 0)
# Normalize feathered mask to [0.0, 1.0] range # Normalize to [0.0, 1.0]
max_val = feathered_polygon_mask.max() max_val = feathered_mask.max()
if max_val > 1e-6: # Avoid division by zero if max_val > 1e-6:
feathered_polygon_mask = feathered_polygon_mask / max_val feathered_mask = feathered_mask / max_val
else: else:
feathered_polygon_mask.fill(0.0) # Mask is all black if max is near zero feathered_mask.fill(0.0)
# --- End Mask Creation ---
# --- Blending: paste original mouth onto swapped face ---
# --- Refined Blending ---
# Get the corresponding ROI from the *full face mask* (already blurred)
# Ensure face_mask is float and normalized [0.0, 1.0]
if face_mask.dtype != np.float64 and face_mask.dtype != np.float32:
face_mask_float = face_mask.astype(np.float32) / 255.0
else: # Assume already float [0,1] if type is float
face_mask_float = face_mask.astype(np.float32) if face_mask.dtype == np.float64 else face_mask
face_mask_roi = face_mask_float[min_y:max_y, min_x:max_x]
# Combine the feathered mouth polygon mask with the face mask ROI
# Use minimum to ensure we only affect area inside both masks (mouth area within face)
# This helps blend the edges smoothly with the surrounding swapped face region
combined_mask = np.minimum(feathered_polygon_mask, face_mask_roi)
# Expand mask to 3 channels for blending (ensure it matches image channels)
if len(frame.shape) == 3 and frame.shape[2] == 3: if len(frame.shape) == 3 and frame.shape[2] == 3:
combined_mask_3channel = combined_mask[:, :, np.newaxis] mask_3ch = feathered_mask[:, :, np.newaxis].astype(np.float32)
inv_mask = 1.0 - mask_3ch
# Ensure data types are compatible for blending # Blend: (original_mouth * mask) + (swapped_face * (1 - mask))
# float32 provides sufficient precision for 8-bit image blending blended_roi = (resized_mouth_cutout.astype(np.float32) * mask_3ch +
combined_mask_f32 = combined_mask_3channel.astype(np.float32) roi.astype(np.float32) * inv_mask)
inv_mask = np.float32(1.0) - combined_mask_f32
# Blend: (original_mouth * combined_mask) + (swapped_face_roi * (1 - combined_mask)) frame[min_y:max_y, min_x:max_x] = np.clip(blended_roi, 0, 255).astype(np.uint8)
blended_roi = (color_corrected_mouth * combined_mask_f32 +
roi * inv_mask)
# Place the blended ROI back into the frame
frame[min_y:max_y, min_x:max_x] = blended_roi.astype(np.uint8)
else:
# print("Warning: Cannot apply mouth mask blending, frame is not 3-channel BGR.")
pass # Don't modify frame if it's not BGR
except Exception as e: except Exception as e:
print(f"Error applying mouth area: {e}") # Optional debug print(f"Error applying mouth area: {e}") # Optional debug
+144 -89
View File
@@ -10,6 +10,8 @@ import json
import queue import queue
import threading import threading
import numpy as np import numpy as np
import requests
import tempfile
import modules.globals import modules.globals
import modules.metadata import modules.metadata
from modules.face_analyser import ( from modules.face_analyser import (
@@ -135,6 +137,7 @@ def save_switch_states():
"show_fps": modules.globals.show_fps, "show_fps": modules.globals.show_fps,
"mouth_mask": modules.globals.mouth_mask, "mouth_mask": modules.globals.mouth_mask,
"show_mouth_mask_box": modules.globals.show_mouth_mask_box, "show_mouth_mask_box": modules.globals.show_mouth_mask_box,
"mouth_mask_size": modules.globals.mouth_mask_size,
} }
with open("switch_states.json", "w") as f: with open("switch_states.json", "w") as f:
json.dump(switch_states, f) json.dump(switch_states, f)
@@ -156,10 +159,10 @@ def load_switch_states():
modules.globals.live_resizable = switch_states.get("live_resizable", 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.fp_ui = switch_states.get("fp_ui", {"face_enhancer": False})
modules.globals.show_fps = switch_states.get("show_fps", False) modules.globals.show_fps = switch_states.get("show_fps", False)
modules.globals.mouth_mask = switch_states.get("mouth_mask", False) modules.globals.mouth_mask_size = switch_states.get("mouth_mask_size", 0.0)
modules.globals.show_mouth_mask_box = switch_states.get( # mouth_mask is driven by the slider: on if size > 0, off if 0
"show_mouth_mask_box", False modules.globals.mouth_mask = modules.globals.mouth_mask_size > 0
) modules.globals.show_mouth_mask_box = False # always start hidden
except FileNotFoundError: except FileNotFoundError:
# If the file doesn't exist, use default values # If the file doesn't exist, use default values
pass pass
@@ -191,9 +194,15 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
select_face_button = ctk.CTkButton( select_face_button = ctk.CTkButton(
root, text=_("Select a face"), cursor="hand2", command=lambda: select_source_path() root, text=_("Select a face"), cursor="hand2", command=lambda: select_source_path()
) )
select_face_button.place(relx=0.1, rely=0.30, relwidth=0.3, relheight=0.1) select_face_button.place(relx=0.1, rely=0.30, relwidth=0.24, relheight=0.1)
ToolTip(select_face_button, _("Choose the source face image to swap onto the target")) ToolTip(select_face_button, _("Choose the source face image to swap onto the target"))
random_face_button = ctk.CTkButton(
root, text="🔄", cursor="hand2", width=30, command=lambda: fetch_random_face()
)
random_face_button.place(relx=0.35, rely=0.30, relwidth=0.05, relheight=0.1)
ToolTip(random_face_button, _("Get a random face from thispersondoesnotexist.com"))
swap_faces_button = ctk.CTkButton( swap_faces_button = ctk.CTkButton(
root, text="", cursor="hand2", command=lambda: swap_faces_paths() root, text="", cursor="hand2", command=lambda: swap_faces_paths()
) )
@@ -220,7 +229,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
save_switch_states(), save_switch_states(),
), ),
) )
keep_fps_checkbox.place(relx=0.1, rely=0.5) keep_fps_checkbox.place(relx=0.1, rely=0.42)
ToolTip(keep_fps_checkbox, _("Output video keeps the original frame rate")) ToolTip(keep_fps_checkbox, _("Output video keeps the original frame rate"))
keep_frames_value = ctk.BooleanVar(value=modules.globals.keep_frames) keep_frames_value = ctk.BooleanVar(value=modules.globals.keep_frames)
@@ -234,51 +243,9 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
save_switch_states(), save_switch_states(),
), ),
) )
keep_frames_switch.place(relx=0.1, rely=0.55) keep_frames_switch.place(relx=0.1, rely=0.47)
ToolTip(keep_frames_switch, _("Keep extracted frames on disk after processing")) ToolTip(keep_frames_switch, _("Keep extracted frames on disk after processing"))
enhancer_value = ctk.BooleanVar(value=modules.globals.fp_ui["face_enhancer"])
enhancer_switch = ctk.CTkSwitch(
root,
text=_("Face Enhancer"),
variable=enhancer_value,
cursor="hand2",
command=lambda: (
update_tumbler("face_enhancer", enhancer_value.get()),
save_switch_states(),
),
)
enhancer_switch.place(relx=0.1, rely=0.6)
ToolTip(enhancer_switch, _("Improve face quality using the GFPGAN restoration model"))
gpen256_value = ctk.BooleanVar(value=modules.globals.fp_ui.get("face_enhancer_gpen256", False))
gpen256_switch = ctk.CTkSwitch(
root,
text=_("GPEN Enhancer 256"),
variable=gpen256_value,
cursor="hand2",
command=lambda: (
update_tumbler("face_enhancer_gpen256", gpen256_value.get()),
save_switch_states(),
),
)
gpen256_switch.place(relx=0.1, rely=0.65)
ToolTip(gpen256_switch, _("Use GPEN face enhancement model at 256px resolution (faster)"))
gpen512_value = ctk.BooleanVar(value=modules.globals.fp_ui.get("face_enhancer_gpen512", False))
gpen512_switch = ctk.CTkSwitch(
root,
text=_("GPEN Enhancer 512"),
variable=gpen512_value,
cursor="hand2",
command=lambda: (
update_tumbler("face_enhancer_gpen512", gpen512_value.get()),
save_switch_states(),
),
)
gpen512_switch.place(relx=0.1, rely=0.7)
ToolTip(gpen512_switch, _("Use GPEN face enhancement model at 512px resolution (higher quality)"))
keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio) keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio)
keep_audio_switch = ctk.CTkSwitch( keep_audio_switch = ctk.CTkSwitch(
root, root,
@@ -290,7 +257,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
save_switch_states(), save_switch_states(),
), ),
) )
keep_audio_switch.place(relx=0.6, rely=0.5) keep_audio_switch.place(relx=0.6, rely=0.42)
ToolTip(keep_audio_switch, _("Copy audio track from the source video to output")) ToolTip(keep_audio_switch, _("Copy audio track from the source video to output"))
many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces) many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces)
@@ -304,7 +271,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
save_switch_states(), save_switch_states(),
), ),
) )
many_faces_switch.place(relx=0.6, rely=0.55) many_faces_switch.place(relx=0.6, rely=0.47)
ToolTip(many_faces_switch, _("Swap every detected face, not just the primary one")) ToolTip(many_faces_switch, _("Swap every detected face, not just the primary one"))
color_correction_value = ctk.BooleanVar(value=modules.globals.color_correction) color_correction_value = ctk.BooleanVar(value=modules.globals.color_correction)
@@ -318,7 +285,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
save_switch_states(), save_switch_states(),
), ),
) )
color_correction_switch.place(relx=0.6, rely=0.6) color_correction_switch.place(relx=0.6, rely=0.57)
ToolTip(color_correction_switch, _("Fix blue/green color cast from some webcams")) ToolTip(color_correction_switch, _("Fix blue/green color cast from some webcams"))
# nsfw_value = ctk.BooleanVar(value=modules.globals.nsfw_filter) # nsfw_value = ctk.BooleanVar(value=modules.globals.nsfw_filter)
@@ -337,7 +304,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
close_mapper_window() if not map_faces.get() else None close_mapper_window() if not map_faces.get() else None
), ),
) )
map_faces_switch.place(relx=0.1, rely=0.75) map_faces_switch.place(relx=0.1, rely=0.52)
ToolTip(map_faces_switch, _("Manually assign which source face maps to which target face")) ToolTip(map_faces_switch, _("Manually assign which source face maps to which target face"))
poisson_blend_value = ctk.BooleanVar(value=modules.globals.poisson_blend) poisson_blend_value = ctk.BooleanVar(value=modules.globals.poisson_blend)
@@ -351,7 +318,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
save_switch_states(), save_switch_states(),
), ),
) )
poisson_blend_switch.place(relx=0.1, rely=0.8) poisson_blend_switch.place(relx=0.1, rely=0.57)
ToolTip(poisson_blend_switch, _("Blend face edges smoothly using Poisson blending")) ToolTip(poisson_blend_switch, _("Blend face edges smoothly using Poisson blending"))
show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps) show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps)
@@ -365,54 +332,34 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
save_switch_states(), save_switch_states(),
), ),
) )
show_fps_switch.place(relx=0.6, rely=0.65) show_fps_switch.place(relx=0.6, rely=0.52)
ToolTip(show_fps_switch, _("Display frames-per-second counter on the live preview")) ToolTip(show_fps_switch, _("Display frames-per-second counter on the live preview"))
# mouth_mask and show_mouth_mask_box are auto-controlled by the Mouth Mask slider
mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask) mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask)
mouth_mask_switch = ctk.CTkSwitch(
root,
text=_("Mouth Mask"),
variable=mouth_mask_var,
cursor="hand2",
command=lambda: setattr(modules.globals, "mouth_mask", mouth_mask_var.get()),
)
mouth_mask_switch.place(relx=0.1, rely=0.45)
ToolTip(mouth_mask_switch, _("Preserve original mouth movement in the swapped face"))
show_mouth_mask_box_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box) show_mouth_mask_box_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box)
show_mouth_mask_box_switch = ctk.CTkSwitch(
root,
text=_("Show Mouth Mask Box"),
variable=show_mouth_mask_box_var,
cursor="hand2",
command=lambda: setattr(
modules.globals, "show_mouth_mask_box", show_mouth_mask_box_var.get()
),
)
show_mouth_mask_box_switch.place(relx=0.6, rely=0.45)
ToolTip(show_mouth_mask_box_switch, _("Display the mouth mask boundary for debugging"))
start_button = ctk.CTkButton( start_button = ctk.CTkButton(
root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root) root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root)
) )
start_button.place(relx=0.15, rely=0.86, relwidth=0.2, relheight=0.05) start_button.place(relx=0.15, rely=0.78, relwidth=0.2, relheight=0.04)
ToolTip(start_button, _("Begin processing the target image/video with selected face")) ToolTip(start_button, _("Begin processing the target image/video with selected face"))
stop_button = ctk.CTkButton( stop_button = ctk.CTkButton(
root, text=_("Destroy"), cursor="hand2", command=lambda: destroy() root, text=_("Destroy"), cursor="hand2", command=lambda: destroy()
) )
stop_button.place(relx=0.4, rely=0.86, relwidth=0.2, relheight=0.05) stop_button.place(relx=0.4, rely=0.78, relwidth=0.2, relheight=0.04)
ToolTip(stop_button, _("Stop processing and close the application")) ToolTip(stop_button, _("Stop processing and close the application"))
preview_button = ctk.CTkButton( preview_button = ctk.CTkButton(
root, text=_("Preview"), cursor="hand2", command=lambda: toggle_preview() root, text=_("Preview"), cursor="hand2", command=lambda: toggle_preview()
) )
preview_button.place(relx=0.65, rely=0.86, relwidth=0.2, relheight=0.05) preview_button.place(relx=0.65, rely=0.78, relwidth=0.2, relheight=0.04)
ToolTip(preview_button, _("Show/hide a preview of the processed output")) ToolTip(preview_button, _("Show/hide a preview of the processed output"))
# --- Camera Selection --- # --- Camera Selection ---
camera_label = ctk.CTkLabel(root, text=_("Select Camera:")) camera_label = ctk.CTkLabel(root, text=_("Select Camera:"))
camera_label.place(relx=0.1, rely=0.92, relwidth=0.2, relheight=0.05) camera_label.place(relx=0.1, rely=0.83, relwidth=0.2, relheight=0.03)
available_cameras = get_available_cameras() available_cameras = get_available_cameras()
camera_indices, camera_names = available_cameras camera_indices, camera_names = available_cameras
@@ -431,7 +378,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
root, variable=camera_variable, values=camera_names root, variable=camera_variable, values=camera_names
) )
camera_optionmenu.place(relx=0.35, rely=0.92, relwidth=0.25, relheight=0.05) camera_optionmenu.place(relx=0.35, rely=0.83, relwidth=0.25, relheight=0.03)
ToolTip(camera_optionmenu, _("Select which camera to use for live mode")) ToolTip(camera_optionmenu, _("Select which camera to use for live mode"))
live_button = ctk.CTkButton( live_button = ctk.CTkButton(
@@ -452,10 +399,52 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
else "disabled" else "disabled"
), ),
) )
live_button.place(relx=0.65, rely=0.92, relwidth=0.2, relheight=0.05) live_button.place(relx=0.65, rely=0.83, relwidth=0.2, relheight=0.03)
ToolTip(live_button, _("Start real-time face swap using webcam")) ToolTip(live_button, _("Start real-time face swap using webcam"))
# --- End Camera Selection --- # --- End Camera Selection ---
# --- Face Enhancer Dropdown ---
enhancer_options = ["None", "GFPGAN", "GPEN-512", "GPEN-256"]
enhancer_key_map = {
"None": None,
"GFPGAN": "face_enhancer",
"GPEN-512": "face_enhancer_gpen512",
"GPEN-256": "face_enhancer_gpen256",
}
# Determine initial value from current fp_ui state
initial_enhancer = "None"
if modules.globals.fp_ui.get("face_enhancer", False):
initial_enhancer = "GFPGAN"
elif modules.globals.fp_ui.get("face_enhancer_gpen512", False):
initial_enhancer = "GPEN-512"
elif modules.globals.fp_ui.get("face_enhancer_gpen256", False):
initial_enhancer = "GPEN-256"
enhancer_variable = ctk.StringVar(value=initial_enhancer)
def on_enhancer_change(choice: str):
# Disable all enhancers first
for key in ["face_enhancer", "face_enhancer_gpen256", "face_enhancer_gpen512"]:
update_tumbler(key, False)
# Enable the selected one
selected_key = enhancer_key_map.get(choice)
if selected_key:
update_tumbler(selected_key, True)
save_switch_states()
enhancer_label = ctk.CTkLabel(root, text="Face Enhancer:")
enhancer_label.place(relx=0.1, rely=0.62, relwidth=0.2, relheight=0.03)
enhancer_dropdown = ctk.CTkOptionMenu(
root,
variable=enhancer_variable,
values=enhancer_options,
command=on_enhancer_change,
)
enhancer_dropdown.place(relx=0.35, rely=0.62, relwidth=0.3, relheight=0.03)
ToolTip(enhancer_dropdown, _("Select a face enhancement model (None = no enhancement)"))
# 1) Define a DoubleVar for transparency (0 = fully transparent, 1 = fully opaque) # 1) Define a DoubleVar for transparency (0 = fully transparent, 1 = fully opaque)
transparency_var = ctk.DoubleVar(value=1.0) transparency_var = ctk.DoubleVar(value=1.0)
@@ -475,9 +464,9 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
modules.globals.face_swapper_enabled = True modules.globals.face_swapper_enabled = True
update_status(f"Transparency set to {percentage}%") update_status(f"Transparency set to {percentage}%")
# 2) Transparency label and slider (placed ABOVE sharpness) # 2) Transparency label and slider
transparency_label = ctk.CTkLabel(root, text="Transparency:") transparency_label = ctk.CTkLabel(root, text="Transparency:")
transparency_label.place(relx=0.15, rely=0.75, relwidth=0.2, relheight=0.05) transparency_label.place(relx=0.15, rely=0.66, relwidth=0.2, relheight=0.03)
transparency_slider = ctk.CTkSlider( transparency_slider = ctk.CTkSlider(
root, root,
@@ -493,7 +482,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
border_width=1, border_width=1,
corner_radius=3, corner_radius=3,
) )
transparency_slider.place(relx=0.35, rely=0.77, relwidth=0.5, relheight=0.02) transparency_slider.place(relx=0.35, rely=0.67, relwidth=0.5, relheight=0.02)
ToolTip(transparency_slider, _("Blend between original and swapped face (0% = original, 100% = fully swapped)")) ToolTip(transparency_slider, _("Blend between original and swapped face (0% = original, 100% = fully swapped)"))
# 3) Sharpness label & slider # 3) Sharpness label & slider
@@ -503,7 +492,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
update_status(f"Sharpness set to {value:.1f}") update_status(f"Sharpness set to {value:.1f}")
sharpness_label = ctk.CTkLabel(root, text="Sharpness:") sharpness_label = ctk.CTkLabel(root, text="Sharpness:")
sharpness_label.place(relx=0.15, rely=0.80, relwidth=0.2, relheight=0.05) sharpness_label.place(relx=0.15, rely=0.69, relwidth=0.2, relheight=0.03)
sharpness_slider = ctk.CTkSlider( sharpness_slider = ctk.CTkSlider(
root, root,
@@ -519,18 +508,64 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
border_width=1, border_width=1,
corner_radius=3, corner_radius=3,
) )
sharpness_slider.place(relx=0.35, rely=0.82, relwidth=0.5, relheight=0.02) sharpness_slider.place(relx=0.35, rely=0.70, relwidth=0.5, relheight=0.02)
ToolTip(sharpness_slider, _("Sharpen the enhanced face output")) ToolTip(sharpness_slider, _("Sharpen the enhanced face output"))
# 4) Mouth Mask Size slider
mouth_mask_size_var = ctk.DoubleVar(value=modules.globals.mouth_mask_size)
def on_mouth_mask_size_change(value: float):
val = float(value)
modules.globals.mouth_mask_size = val
# Auto-enable/disable mouth mask based on slider position
if val > 0:
modules.globals.mouth_mask = True
mouth_mask_var.set(True)
else:
modules.globals.mouth_mask = False
mouth_mask_var.set(False)
modules.globals.show_mouth_mask_box = False
def on_mouth_mask_slider_release(event):
# Hide bounding box when user releases the slider
modules.globals.show_mouth_mask_box = False
def on_mouth_mask_slider_press(event):
# Show bounding box while dragging
if modules.globals.mouth_mask_size > 0:
modules.globals.show_mouth_mask_box = True
mouth_mask_size_label = ctk.CTkLabel(root, text="Mouth Mask:")
mouth_mask_size_label.place(relx=0.15, rely=0.72, relwidth=0.2, relheight=0.03)
mouth_mask_size_slider = ctk.CTkSlider(
root,
from_=0.0,
to=100.0,
variable=mouth_mask_size_var,
command=on_mouth_mask_size_change,
fg_color="#E0E0E0",
progress_color="#007BFF",
button_color="#FFFFFF",
button_hover_color="#CCCCCC",
height=5,
border_width=1,
corner_radius=3,
)
mouth_mask_size_slider.place(relx=0.35, rely=0.73, relwidth=0.5, relheight=0.02)
mouth_mask_size_slider.bind("<ButtonPress-1>", on_mouth_mask_slider_press)
mouth_mask_size_slider.bind("<ButtonRelease-1>", on_mouth_mask_slider_release)
ToolTip(mouth_mask_size_slider, _("0 = use swapped mouth, 100 = expose original mouth to chin area"))
# Status and link at the bottom # Status and link at the bottom
global status_label global status_label
status_label = ctk.CTkLabel(root, text=None, justify="center") status_label = ctk.CTkLabel(root, text=None, justify="center")
status_label.place(relx=0.1, rely=0.96, relwidth=0.8) status_label.place(relx=0.1, rely=0.75, relwidth=0.8)
donate_label = ctk.CTkLabel( donate_label = ctk.CTkLabel(
root, text="Deep Live Cam", justify="center", cursor="hand2" root, text="Deep Live Cam", justify="center", cursor="hand2"
) )
donate_label.place(relx=0.1, rely=0.98, relwidth=0.8) donate_label.place(relx=0.1, rely=0.87, relwidth=0.8)
donate_label.configure( donate_label.configure(
text_color=ctk.ThemeManager.theme.get("URL").get("text_color") text_color=ctk.ThemeManager.theme.get("URL").get("text_color")
) )
@@ -739,6 +774,26 @@ def update_tumbler(var: str, value: bool) -> None:
) )
def fetch_random_face() -> None:
PREVIEW.withdraw()
try:
response = requests.get(
"https://thispersondoesnotexist.com/",
headers={"User-Agent": "Mozilla/5.0"},
timeout=10,
)
response.raise_for_status()
temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, "deep_live_cam_random_face.jpg")
with open(temp_path, "wb") as f:
f.write(response.content)
modules.globals.source_path = temp_path
image = render_image_preview(temp_path, (200, 200))
source_label.configure(image=image)
except Exception as e:
print(f"Failed to fetch random face: {e}")
def select_source_path() -> None: def select_source_path() -> None:
global RECENT_DIRECTORY_SOURCE, img_ft, vid_ft global RECENT_DIRECTORY_SOURCE, img_ft, vid_ft
+1 -1
View File
@@ -9,7 +9,7 @@ tk==0.1.0
customtkinter==5.2.2 customtkinter==5.2.2
pillow==12.1.1 pillow==12.1.1
onnxruntime-silicon==1.16.3; sys_platform == 'darwin' and platform_machine == 'arm64' onnxruntime-silicon==1.16.3; sys_platform == 'darwin' and platform_machine == 'arm64'
onnxruntime-gpu==1.24.2; sys_platform != 'darwin' onnxruntime-gpu==1.23.2; sys_platform != 'darwin'
tensorflow; sys_platform != 'darwin' tensorflow; sys_platform != 'darwin'
opennsfw2==0.10.2 opennsfw2==0.10.2
protobuf==4.25.1 protobuf==4.25.1
+3
View File
@@ -1,3 +1,6 @@
import os
os.environ.setdefault('TK_SILENCE_DEPRECATION', '1')
import tkinter import tkinter
# Only needs to be imported once at the beginning of the application # Only needs to be imported once at the beginning of the application