Compare commits

..

12 Commits

Author SHA1 Message Date
KRSHH 48c83151a4 size 2025-02-03 20:59:06 +05:30
KRSHH bb3502d9bd update branch
update branch
2025-01-31 22:32:47 +05:30
KRSHH a101a1f3f1 Reorganized switches 2025-01-30 20:20:04 +05:30
KRSHH 01ef955372 Improve eyebrow mask 2025-01-30 20:14:16 +05:30
KRSHH ab3b73631b Shift masking features to face_masking.py 2025-01-30 20:04:31 +05:30
KRSHH d8fc1ffa04 Eyebrow Mask 2025-01-30 19:58:15 +05:30
KRSHH 5dfd1c0ced Eye Mask 2025-01-30 19:20:58 +05:30
KRSHH 59cd3be0f9 Simplify Privacy mode switch state 2025-01-27 00:47:34 +05:30
KRSHH ccb676ac17 FPS Switch re-enable 2025-01-26 23:41:44 +05:30
KRSHH f0c66732e7 Use Logo 2025-01-26 23:20:08 +05:30
KRSHH 8055d79daf Privacy Mode 2025-01-26 23:10:45 +05:30
KRSHH 3c7dd1a574 Hardcoded Keep FPS and Keep Frames 2025-01-23 18:45:43 +05:30
12 changed files with 1282 additions and 410 deletions
+1
View File
@@ -0,0 +1 @@
3.10.0
+23 -79
View File
@@ -31,13 +31,25 @@ 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.
## Quick Start - Pre-built (Windows / Nvidia)
<a href="https://hacksider.gumroad.com/l/vccdmm"> <img src="https://github.com/user-attachments/assets/7d993b32-e3e8-4cd3-bbfb-a549152ebdd5" width="285" height="77" />
##### This is the fastest build you can get if you have a discrete NVIDIA GPU.
## Quick Start - Pre-built (Mac / Silicon)
<a href="https://krshh.gumroad.com/l/Deep-Live-Cam-Mac"> <img src="https://github.com/user-attachments/assets/d5d913b5-a7de-4609-96b9-979a5749a703" width="285" height="77" />
###### These Pre-builts are perfect for non-technical users or those who dont 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.
## TLDR; Live Deepfake in just 3 Clicks ## TLDR; Live Deepfake in just 3 Clicks
![easysteps](https://github.com/user-attachments/assets/af825228-852c-411b-b787-ffd9aac72fc6) ![easysteps](https://github.com/user-attachments/assets/af825228-852c-411b-b787-ffd9aac72fc6)
1. Select a face 1. Select a face
2. Select which camera to use 2. Select which camera to use
3. Press live! 3. Press live!
## Features & Uses - Everything is in real-time ## Features & Uses - Everything is real-time
### Mouth Mask ### Mouth Mask
@@ -73,7 +85,7 @@ Users are expected to use this software responsibly and legally. If using a real
### Memes ### Memes
**Create Your Most Viral Meme Yet** **Create Your most viral meme yet**
<p align="center"> <p align="center">
<img src="media/meme.gif" alt="show" width="450"> <img src="media/meme.gif" alt="show" width="450">
@@ -81,13 +93,6 @@ Users are expected to use this software responsibly and legally. If using a real
<sub>Created using Many Faces feature in Deep-Live-Cam</sub> <sub>Created using Many Faces feature in Deep-Live-Cam</sub>
</p> </p>
### Omegle
**Surprise people on Omegle**
<p align="center">
<video src="https://github.com/user-attachments/assets/2e9b9b82-fa04-4b70-9f56-b1f68e7672d0" width="450" controls></video>
</p>
## Installation (Manual) ## Installation (Manual)
@@ -111,8 +116,7 @@ This is more likely to work on your computer but will be slower as it utilizes t
**2. Clone the Repository** **2. Clone the Repository**
```bash ```bash
git clone https://github.com/hacksider/Deep-Live-Cam.git https://github.com/hacksider/Deep-Live-Cam.git
cd Deep-Live-Cam
``` ```
**3. Download the Models** **3. Download the Models**
@@ -126,44 +130,14 @@ Place these files in the "**models**" folder.
We highly recommend using a `venv` to avoid issues. We highly recommend using a `venv` to avoid issues.
For Windows:
```bash ```bash
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt pip install -r requirements.txt
``` ```
**For macOS:** **For macOS:** Install or upgrade the `python-tk` package:
Apple Silicon (M1/M2/M3) requires specific setup:
```bash ```bash
# Install Python 3.10 (specific version is important)
brew install python@3.10
# Install tkinter package (required for the GUI)
brew install python-tk@3.10 brew install python-tk@3.10
# Create and activate virtual environment with Python 3.10
python3.10 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
```
** In case something goes wrong and you need to reinstall the virtual environment **
```bash
# Deactivate the virtual environment
rm -rf venv
# Reinstall the virtual environment
python -m venv venv
source venv/bin/activate
# install the dependencies again
pip install -r requirements.txt
``` ```
**Run:** If you don't have a GPU, you can run Deep-Live-Cam using `python run.py`. Note that initial execution will download models (~300MB). **Run:** If you don't have a GPU, you can run Deep-Live-Cam using `python run.py`. Note that initial execution will download models (~300MB).
@@ -172,7 +146,7 @@ pip install -r requirements.txt
**CUDA Execution Provider (Nvidia)** **CUDA Execution Provider (Nvidia)**
1. Install [CUDA Toolkit 11.8.0](https://developer.nvidia.com/cuda-11-8-0-download-archive) 1. Install [CUDA Toolkit 11.8](https://developer.nvidia.com/cuda-11-8-0-download-archive) or [CUDA Toolkit 12.1.1](https://developer.nvidia.com/cuda-12-1-1-download-archive)
2. Install dependencies: 2. Install dependencies:
```bash ```bash
@@ -188,39 +162,19 @@ python run.py --execution-provider cuda
**CoreML Execution Provider (Apple Silicon)** **CoreML Execution Provider (Apple Silicon)**
Apple Silicon (M1/M2/M3) specific installation: 1. Install dependencies:
1. Make sure you've completed the macOS setup above using Python 3.10.
2. Install dependencies:
```bash ```bash
pip uninstall onnxruntime onnxruntime-silicon pip uninstall onnxruntime onnxruntime-silicon
pip install onnxruntime-silicon==1.13.1 pip install onnxruntime-silicon==1.13.1
``` ```
3. Usage (important: specify Python 3.10): 2. Usage:
```bash ```bash
python3.10 run.py --execution-provider coreml python run.py --execution-provider coreml
``` ```
**Important Notes for macOS:**
- You **must** use Python 3.10, not newer versions like 3.11 or 3.13
- Always run with `python3.10` command not just `python` if you have multiple Python versions installed
- If you get error about `_tkinter` missing, reinstall the tkinter package: `brew reinstall python-tk@3.10`
- If you get model loading errors, check that your models are in the correct folder
- If you encounter conflicts with other Python versions, consider uninstalling them:
```bash
# List all installed Python versions
brew list | grep python
# Uninstall conflicting versions if needed
brew uninstall --ignore-dependencies python@3.11 python@3.13
# Keep only Python 3.10
brew cleanup
```
**CoreML Execution Provider (Apple Legacy)** **CoreML Execution Provider (Apple Legacy)**
1. Install dependencies: 1. Install dependencies:
@@ -265,6 +219,7 @@ pip install onnxruntime-openvino==1.15.0
```bash ```bash
python run.py --execution-provider openvino python run.py --execution-provider openvino
``` ```
</details> </details>
## Usage ## Usage
@@ -285,19 +240,6 @@ 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.
## Tips and Tricks
Check out these helpful guides to get the most out of Deep-Live-Cam:
- [Unlocking the Secrets to the Perfect Deepfake Image](https://deeplivecam.net/index.php/blog/tips-and-tricks/unlocking-the-secrets-to-the-perfect-deepfake-image) - Learn how to create the best deepfake with full head coverage
- [Video Call with DeepLiveCam](https://deeplivecam.net/index.php/blog/tips-and-tricks/video-call-with-deeplivecam) - Make your meetings livelier by using DeepLiveCam with OBS and meeting software
- [Have a Special Guest!](https://deeplivecam.net/index.php/blog/tips-and-tricks/have-a-special-guest) - Tutorial on how to use face mapping to add special guests to your stream
- [Watch Deepfake Movies in Realtime](https://deeplivecam.net/index.php/blog/tips-and-tricks/watch-deepfake-movies-in-realtime) - See yourself star in any video without processing the video
- [Better Quality without Sacrificing Speed](https://deeplivecam.net/index.php/blog/tips-and-tricks/better-quality-without-sacrificing-speed) - Tips for achieving better results without impacting performance
- [Instant Vtuber!](https://deeplivecam.net/index.php/blog/tips-and-tricks/instant-vtuber) - Create a new persona/vtuber easily using Metahuman Creator
Visit our [official blog](https://deeplivecam.net/index.php/blog/tips-and-tricks) for more tips and tutorials.
## Command Line Arguments (Unmaintained) ## Command Line Arguments (Unmaintained)
``` ```
@@ -371,3 +313,5 @@ Looking for a CLI mode? Using the -s/--source argument will make the run program
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=hacksider/deep-live-cam&type=Date" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=hacksider/deep-live-cam&type=Date" />
</picture> </picture>
</a> </a>
+4 -4
View File
@@ -20,6 +20,7 @@ import modules.metadata
import modules.ui as ui import modules.ui as ui
from modules.processors.frame.core import get_frame_processors_modules 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.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: if 'ROCMExecutionProvider' in modules.globals.execution_providers:
del torch del torch
@@ -35,9 +36,7 @@ def parse_args() -> None:
program.add_argument('-t', '--target', help='select an target image or video', dest='target_path') 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('-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('--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-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('--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('--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) program.add_argument('--map-faces', help='map source target faces', dest='map_faces', action='store_true', default=False)
@@ -65,9 +64,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.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.frame_processors = args.frame_processor
modules.globals.headless = args.source_path or args.target_path or args.output_path 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_audio = args.keep_audio
modules.globals.keep_frames = args.keep_frames
modules.globals.many_faces = args.many_faces modules.globals.many_faces = args.many_faces
modules.globals.mouth_mask = args.mouth_mask modules.globals.mouth_mask = args.mouth_mask
modules.globals.nsfw_filter = args.nsfw_filter modules.globals.nsfw_filter = args.nsfw_filter
@@ -241,6 +240,7 @@ def start() -> None:
def destroy(to_quit=True) -> None: def destroy(to_quit=True) -> None:
if modules.globals.target_path: if modules.globals.target_path:
clean_temp(modules.globals.target_path) clean_temp(modules.globals.target_path)
cleanup_fake_face()
if to_quit: quit() if to_quit: quit()
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

+12 -12
View File
@@ -39,13 +39,13 @@ def get_many_faces(frame: Frame) -> Any:
return None return None
def has_valid_map() -> bool: def has_valid_map() -> bool:
for map in modules.globals.source_target_map: for map in modules.globals.souce_target_map:
if "source" in map and "target" in map: if "source" in map and "target" in map:
return True return True
return False return False
def default_source_face() -> Any: def default_source_face() -> Any:
for map in modules.globals.source_target_map: for map in modules.globals.souce_target_map:
if "source" in map: if "source" in map:
return map['source']['face'] return map['source']['face']
return None return None
@@ -53,7 +53,7 @@ def default_source_face() -> Any:
def simplify_maps() -> Any: def simplify_maps() -> Any:
centroids = [] centroids = []
faces = [] faces = []
for map in modules.globals.source_target_map: for map in modules.globals.souce_target_map:
if "source" in map and "target" in map: if "source" in map and "target" in map:
centroids.append(map['target']['face'].normed_embedding) centroids.append(map['target']['face'].normed_embedding)
faces.append(map['source']['face']) faces.append(map['source']['face'])
@@ -64,10 +64,10 @@ def simplify_maps() -> Any:
def add_blank_map() -> Any: def add_blank_map() -> Any:
try: try:
max_id = -1 max_id = -1
if len(modules.globals.source_target_map) > 0: if len(modules.globals.souce_target_map) > 0:
max_id = max(modules.globals.source_target_map, key=lambda x: x['id'])['id'] max_id = max(modules.globals.souce_target_map, key=lambda x: x['id'])['id']
modules.globals.source_target_map.append({ modules.globals.souce_target_map.append({
'id' : max_id + 1 'id' : max_id + 1
}) })
except ValueError: except ValueError:
@@ -75,14 +75,14 @@ def add_blank_map() -> Any:
def get_unique_faces_from_target_image() -> Any: def get_unique_faces_from_target_image() -> Any:
try: try:
modules.globals.source_target_map = [] modules.globals.souce_target_map = []
target_frame = cv2.imread(modules.globals.target_path) target_frame = cv2.imread(modules.globals.target_path)
many_faces = get_many_faces(target_frame) many_faces = get_many_faces(target_frame)
i = 0 i = 0
for face in many_faces: for face in many_faces:
x_min, y_min, x_max, y_max = face['bbox'] x_min, y_min, x_max, y_max = face['bbox']
modules.globals.source_target_map.append({ modules.globals.souce_target_map.append({
'id' : i, 'id' : i,
'target' : { 'target' : {
'cv2' : target_frame[int(y_min):int(y_max), int(x_min):int(x_max)], 'cv2' : target_frame[int(y_min):int(y_max), int(x_min):int(x_max)],
@@ -96,7 +96,7 @@ def get_unique_faces_from_target_image() -> Any:
def get_unique_faces_from_target_video() -> Any: def get_unique_faces_from_target_video() -> Any:
try: try:
modules.globals.source_target_map = [] modules.globals.souce_target_map = []
frame_face_embeddings = [] frame_face_embeddings = []
face_embeddings = [] face_embeddings = []
@@ -127,7 +127,7 @@ def get_unique_faces_from_target_video() -> Any:
face['target_centroid'] = closest_centroid_index face['target_centroid'] = closest_centroid_index
for i in range(len(centroids)): for i in range(len(centroids)):
modules.globals.source_target_map.append({ modules.globals.souce_target_map.append({
'id' : i 'id' : i
}) })
@@ -135,7 +135,7 @@ def get_unique_faces_from_target_video() -> Any:
for frame in tqdm(frame_face_embeddings, desc=f"Mapping frame embeddings to centroids-{i}"): for frame in tqdm(frame_face_embeddings, desc=f"Mapping frame embeddings to centroids-{i}"):
temp.append({'frame': frame['frame'], 'faces': [face for face in frame['faces'] if face['target_centroid'] == i], 'location': frame['location']}) temp.append({'frame': frame['frame'], 'faces': [face for face in frame['faces'] if face['target_centroid'] == i], 'location': frame['location']})
modules.globals.source_target_map[i]['target_faces_in_frame'] = temp modules.globals.souce_target_map[i]['target_faces_in_frame'] = temp
# dump_faces(centroids, frame_face_embeddings) # dump_faces(centroids, frame_face_embeddings)
default_target_face() default_target_face()
@@ -144,7 +144,7 @@ def get_unique_faces_from_target_video() -> Any:
def default_target_face(): def default_target_face():
for map in modules.globals.source_target_map: for map in modules.globals.souce_target_map:
best_face = None best_face = None
best_frame = None best_frame = None
for frame in map['target_faces_in_frame']: for frame in map['target_faces_in_frame']:
+120
View File
@@ -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
+11 -2
View File
@@ -9,7 +9,7 @@ file_types = [
("Video", ("*.mp4", "*.mkv")), ("Video", ("*.mp4", "*.mkv")),
] ]
source_target_map = [] souce_target_map = []
simple_map = {} simple_map = {}
source_path = None source_path = None
@@ -21,7 +21,7 @@ keep_audio = True
keep_frames = False keep_frames = False
many_faces = False many_faces = False
map_faces = False map_faces = False
color_correction = False # New global variable for color correction toggle color_correction = False
nsfw_filter = False nsfw_filter = False
video_encoder = None video_encoder = None
video_quality = None video_quality = None
@@ -41,3 +41,12 @@ show_mouth_mask_box = False
mask_feather_ratio = 8 mask_feather_ratio = 8
mask_down_size = 0.50 mask_down_size = 0.50
mask_size = 1 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
+1 -1
View File
@@ -1,3 +1,3 @@
name = 'Deep-Live-Cam' name = 'Deep-Live-Cam'
version = '1.9' version = '1.8'
edition = 'GitHub Edition' edition = 'GitHub Edition'
+634
View File
@@ -0,0 +1,634 @@
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 using the mouth_mask_size
expansion_factor = (
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
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 * 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)
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 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 * 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)
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_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)
y = np.polyval(coeffs, x)
# 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 more pronounced taper
end_points = 5
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((
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.2 # Increased padding slightly
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
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 adjusted by size
all_points = np.vstack([left_eyebrow, right_eyebrow])
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))
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 quadratic curve through points for more natural arch
coeffs = np.polyfit(sorted_points[:, 0], sorted_points[:, 1], 2)
y = np.polyval(coeffs, x)
# 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 more pronounced taper
end_points = 5
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((
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.2 # Increased padding slightly
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
+202 -157
View File
@@ -1,44 +1,63 @@
import os # <-- Added for os.path.exists
from typing import Any, List from typing import Any, List
import cv2 import cv2
import insightface import insightface
import threading import threading
import numpy as np
import modules.globals import modules.globals
import modules.processors.frame.core import modules.processors.frame.core
# Ensure update_status is imported if not already globally accessible
# If it's part of modules.core, it might already be accessible via modules.core.update_status
from modules.core import update_status from modules.core import update_status
from modules.face_analyser import get_one_face, get_many_faces, default_source_face from modules.face_analyser import get_one_face, get_many_faces, default_source_face
from modules.typing import Face, Frame 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,
is_image,
is_video,
)
from modules.cluster_analysis import find_closest_centroid 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 FACE_SWAPPER = None
THREAD_LOCK = threading.Lock() THREAD_LOCK = threading.Lock()
NAME = 'DLC.FACE-SWAPPER' NAME = "DLC.FACE-SWAPPER"
abs_dir = os.path.dirname(os.path.abspath(__file__))
models_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(abs_dir))), "models"
)
def pre_check() -> bool: def pre_check() -> bool:
download_directory_path = resolve_relative_path('../models') download_directory_path = abs_dir
# Ensure both models are mentioned or downloaded if necessary conditional_download(
# Conditional download might need adjustment if you want it to fetch FP32 too download_directory_path,
conditional_download(download_directory_path, ['https://huggingface.co/hacksider/deep-live-cam/blob/main/inswapper_128_fp16.onnx']) [
# Add a check or download for the FP32 model if you have a URL "https://huggingface.co/hacksider/deep-live-cam/blob/main/inswapper_128_fp16.onnx"
# conditional_download(download_directory_path, ['URL_TO_FP32_MODEL_HERE']) ],
)
return True return True
def pre_start() -> bool: def pre_start() -> bool:
# --- No changes needed in pre_start ---
if not modules.globals.map_faces and not is_image(modules.globals.source_path): 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 return False
elif not modules.globals.map_faces and not get_one_face(cv2.imread(modules.globals.source_path)): elif not modules.globals.map_faces and not get_one_face(
update_status('No face in source path detected.', NAME) cv2.imread(modules.globals.source_path)
):
update_status("No face in source path detected.", NAME)
return False return False
if not is_image(modules.globals.target_path) and not is_video(modules.globals.target_path): if not is_image(modules.globals.target_path) and not is_video(
update_status('Select an image or video for target path.', NAME) modules.globals.target_path
):
update_status("Select an image or video for target path.", NAME)
return False return False
return True return True
@@ -48,57 +67,85 @@ def get_face_swapper() -> Any:
with THREAD_LOCK: with THREAD_LOCK:
if FACE_SWAPPER is None: if FACE_SWAPPER is None:
# --- MODIFICATION START --- model_path = os.path.join(models_dir, "inswapper_128_fp16.onnx")
# Define paths for both FP32 and FP16 models FACE_SWAPPER = insightface.model_zoo.get_model(
model_dir = resolve_relative_path('../models') model_path, providers=modules.globals.execution_providers
model_path_fp32 = os.path.join(model_dir, 'inswapper_128.onnx') )
model_path_fp16 = os.path.join(model_dir, 'inswapper_128_fp16.onnx')
chosen_model_path = None
# Prioritize FP32 model
if os.path.exists(model_path_fp32):
chosen_model_path = model_path_fp32
update_status(f"Loading FP32 model: {os.path.basename(chosen_model_path)}", NAME)
# Fallback to FP16 model
elif os.path.exists(model_path_fp16):
chosen_model_path = model_path_fp16
update_status(f"FP32 model not found. Loading FP16 model: {os.path.basename(chosen_model_path)}", NAME)
# Error if neither model is found
else:
error_message = f"Face Swapper model not found. Please ensure 'inswapper_128.onnx' (recommended) or 'inswapper_128_fp16.onnx' exists in the '{model_dir}' directory."
update_status(error_message, NAME)
raise FileNotFoundError(error_message)
# Load the chosen model
try:
FACE_SWAPPER = insightface.model_zoo.get_model(chosen_model_path, providers=modules.globals.execution_providers)
except Exception as e:
update_status(f"Error loading Face Swapper model {os.path.basename(chosen_model_path)}: {e}", NAME)
# Optionally, re-raise the exception or handle it more gracefully
raise e
# --- MODIFICATION END ---
return FACE_SWAPPER return FACE_SWAPPER
def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame: def swap_face(source_face: Face, target_face: Face, temp_frame: Frame) -> Frame:
# --- No changes needed in swap_face --- face_swapper = get_face_swapper()
swapper = get_face_swapper()
if swapper is None: # Apply the face swap
# Handle case where model failed to load swapped_frame = face_swapper.get(
update_status("Face swapper model not loaded, skipping swap.", NAME) temp_frame, target_face, source_face, paste_back=True
return temp_frame )
return swapper.get(temp_frame, target_face, source_face, paste_back=True)
# Create face mask for both mouth and eyes masking
face_mask = create_face_mask(target_face, temp_frame)
if modules.globals.mouth_mask:
# 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:
swapped_frame = draw_mask_visualization(
swapped_frame,
mouth_mask_data,
"Lower Mouth Mask"
)
if modules.globals.eyes_mask:
# 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:
swapped_frame = draw_mask_visualization(
swapped_frame,
eyes_mask_data,
"Eyes Mask",
draw_method="ellipse"
)
if modules.globals.eyebrows_mask:
# 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:
swapped_frame = draw_mask_visualization(
swapped_frame,
eyebrows_mask_data,
"Eyebrows Mask"
)
return swapped_frame
def process_frame(source_face: Face, temp_frame: Frame) -> Frame: def process_frame(source_face: Face, temp_frame: Frame) -> Frame:
# --- No changes needed in process_frame --- if modules.globals.color_correction:
# Ensure the frame is in RGB format if color correction is enabled temp_frame = cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB)
# Note: InsightFace swapper often expects BGR by default. Double-check if color issues appear.
# If color correction is needed *before* swapping and insightface needs BGR:
# original_was_bgr = True # Assume input is BGR
# if modules.globals.color_correction:
# temp_frame = cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB)
# original_was_bgr = False # Now it's RGB
if modules.globals.many_faces: if modules.globals.many_faces:
many_faces = get_many_faces(temp_frame) many_faces = get_many_faces(temp_frame)
@@ -109,51 +156,53 @@ def process_frame(source_face: Face, temp_frame: Frame) -> Frame:
target_face = get_one_face(temp_frame) target_face = get_one_face(temp_frame)
if target_face: if target_face:
temp_frame = swap_face(source_face, target_face, temp_frame) temp_frame = swap_face(source_face, target_face, temp_frame)
# Convert back if necessary (example, might not be needed depending on workflow)
# if modules.globals.color_correction and not original_was_bgr:
# temp_frame = cv2.cvtColor(temp_frame, cv2.COLOR_RGB2BGR)
return temp_frame return temp_frame
def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame: def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame:
# --- No changes needed in process_frame_v2 ---
# (Assuming swap_face handles the potential None return from get_face_swapper)
if is_image(modules.globals.target_path): if is_image(modules.globals.target_path):
if modules.globals.many_faces: if modules.globals.many_faces:
source_face = default_source_face() source_face = default_source_face()
for map_entry in modules.globals.souce_target_map: # Renamed 'map' to 'map_entry' for map in modules.globals.souce_target_map:
target_face = map_entry['target']['face'] target_face = map["target"]["face"]
temp_frame = swap_face(source_face, target_face, temp_frame) temp_frame = swap_face(source_face, target_face, temp_frame)
elif not modules.globals.many_faces: elif not modules.globals.many_faces:
for map_entry in modules.globals.souce_target_map: # Renamed 'map' to 'map_entry' for map in modules.globals.souce_target_map:
if "source" in map_entry: if "source" in map:
source_face = map_entry['source']['face'] source_face = map["source"]["face"]
target_face = map_entry['target']['face'] target_face = map["target"]["face"]
temp_frame = swap_face(source_face, target_face, temp_frame) temp_frame = swap_face(source_face, target_face, temp_frame)
elif is_video(modules.globals.target_path): elif is_video(modules.globals.target_path):
if modules.globals.many_faces: if modules.globals.many_faces:
source_face = default_source_face() source_face = default_source_face()
for map_entry in modules.globals.souce_target_map: # Renamed 'map' to 'map_entry' for map in modules.globals.souce_target_map:
target_frame = [f for f in map_entry['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 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) temp_frame = swap_face(source_face, target_face, temp_frame)
elif not modules.globals.many_faces: elif not modules.globals.many_faces:
for map_entry in modules.globals.souce_target_map: # Renamed 'map' to 'map_entry' for map in modules.globals.souce_target_map:
if "source" in map_entry: if "source" in map:
target_frame = [f for f in map_entry['target_faces_in_frame'] if f['location'] == temp_frame_path] target_frame = [
source_face = map_entry['source']['face'] 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 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) temp_frame = swap_face(source_face, target_face, temp_frame)
else: # Fallback for neither image nor video (e.g., live feed?)
else:
detected_faces = get_many_faces(temp_frame) detected_faces = get_many_faces(temp_frame)
if modules.globals.many_faces: if modules.globals.many_faces:
if detected_faces: if detected_faces:
@@ -162,97 +211,93 @@ def process_frame_v2(temp_frame: Frame, temp_frame_path: str = "") -> Frame:
temp_frame = swap_face(source_face, target_face, temp_frame) temp_frame = swap_face(source_face, target_face, temp_frame)
elif not modules.globals.many_faces: elif not modules.globals.many_faces:
if detected_faces and hasattr(modules.globals, 'simple_map') and modules.globals.simple_map: # Check simple_map exists 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: 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(
temp_frame = swap_face(modules.globals.simple_map['source_faces'][closest_centroid_index], detected_face, temp_frame) 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,
)
else: else:
detected_faces_centroids = [face.normed_embedding for face in detected_faces] detected_faces_centroids = []
for face in detected_faces:
detected_faces_centroids.append(face.normed_embedding)
i = 0 i = 0
for target_embedding in modules.globals.simple_map['target_embeddings']: for target_embedding in modules.globals.simple_map[
closest_centroid_index, _ = find_closest_centroid(detected_faces_centroids, target_embedding) "target_embeddings"
# Ensure index is valid before accessing detected_faces ]:
if closest_centroid_index < len(detected_faces): closest_centroid_index, _ = find_closest_centroid(
temp_frame = swap_face(modules.globals.simple_map['source_faces'][i], detected_faces[closest_centroid_index], temp_frame) detected_faces_centroids, target_embedding
)
temp_frame = swap_face(
modules.globals.simple_map["source_faces"][i],
detected_faces[closest_centroid_index],
temp_frame,
)
i += 1 i += 1
return temp_frame return temp_frame
def process_frames(source_path: str, temp_frame_paths: List[str], progress: Any = None) -> None: def process_frames(
# --- No changes needed in process_frames --- source_path: str, temp_frame_paths: List[str], progress: Any = None
# Note: Ensure get_one_face is called only once if possible for efficiency if !map_faces ) -> None:
source_face = None
if not modules.globals.map_faces: if not modules.globals.map_faces:
source_img = cv2.imread(source_path) source_face = get_one_face(cv2.imread(source_path))
if source_img is not None: for temp_frame_path in temp_frame_paths:
source_face = get_one_face(source_img) temp_frame = cv2.imread(temp_frame_path)
if source_face is None: try:
update_status(f"Could not find face in source image: {source_path}, skipping swap.", NAME) result = process_frame(source_face, temp_frame)
# If no source face, maybe skip processing? Or handle differently. cv2.imwrite(temp_frame_path, result)
# For now, it will proceed but swap_face might fail later. except Exception as exception:
print(exception)
for temp_frame_path in temp_frame_paths: pass
temp_frame = cv2.imread(temp_frame_path) if progress:
if temp_frame is None: progress.update(1)
update_status(f"Warning: Could not read frame {temp_frame_path}", NAME) else:
if progress: progress.update(1) # Still update progress even if frame fails for temp_frame_path in temp_frame_paths:
continue # Skip to next frame temp_frame = cv2.imread(temp_frame_path)
try:
try: result = process_frame_v2(temp_frame, temp_frame_path)
if not modules.globals.map_faces: cv2.imwrite(temp_frame_path, result)
if source_face: # Only process if source face was found except Exception as exception:
result = process_frame(source_face, temp_frame) print(exception)
else: pass
result = temp_frame # No source face, return original frame
else:
result = process_frame_v2(temp_frame, temp_frame_path)
cv2.imwrite(temp_frame_path, result)
except Exception as exception:
update_status(f"Error processing frame {os.path.basename(temp_frame_path)}: {exception}", NAME)
# Decide whether to 'pass' (continue processing other frames) or raise
pass # Continue processing other frames
finally:
if progress: if progress:
progress.update(1) progress.update(1)
def process_image(source_path: str, target_path: str, output_path: str) -> None: def process_image(source_path: str, target_path: str, output_path: str) -> None:
# --- No changes needed in process_image ---
# Note: Added checks for successful image reads and face detection
target_frame = cv2.imread(target_path) # Read original target for processing
if target_frame is None:
update_status(f"Error: Could not read target image: {target_path}", NAME)
return
if not modules.globals.map_faces: if not modules.globals.map_faces:
source_img = cv2.imread(source_path) source_face = get_one_face(cv2.imread(source_path))
if source_img is None: target_frame = cv2.imread(target_path)
update_status(f"Error: Could not read source image: {source_path}", NAME)
return
source_face = get_one_face(source_img)
if source_face is None:
update_status(f"Error: No face found in source image: {source_path}", NAME)
return
result = process_frame(source_face, target_frame) result = process_frame(source_face, target_frame)
cv2.imwrite(output_path, result)
else: else:
if modules.globals.many_faces: if modules.globals.many_faces:
update_status('Many faces enabled. Using first source image (if applicable in v2). Processing...', NAME) update_status(
# For process_frame_v2 on single image, it reads the 'output_path' which should be a copy "Many faces enabled. Using first source image. Progressing...", NAME
# Let's process the 'target_frame' we read instead. )
result = process_frame_v2(target_frame) # Process the frame directly target_frame = cv2.imread(output_path)
result = process_frame_v2(target_frame)
# Write the final result to the output path cv2.imwrite(output_path, result)
success = cv2.imwrite(output_path, result)
if not success:
update_status(f"Error: Failed to write output image to: {output_path}", NAME)
def process_video(source_path: str, temp_frame_paths: List[str]) -> None: def process_video(source_path: str, temp_frame_paths: List[str]) -> None:
# --- No changes needed in process_video ---
if modules.globals.map_faces and modules.globals.many_faces: if modules.globals.map_faces and modules.globals.many_faces:
update_status('Many faces enabled. Using first source image (if applicable in v2). Processing...', NAME) update_status(
# The core processing logic is delegated, which is good. "Many faces enabled. Using first source image. Progressing...", NAME
modules.processors.frame.core.process_video(source_path, temp_frame_paths, process_frames) )
modules.processors.frame.core.process_video(
source_path, temp_frame_paths, process_frames
)
+263 -145
View File
@@ -3,7 +3,7 @@ import webbrowser
import customtkinter as ctk import customtkinter as ctk
from typing import Callable, Tuple from typing import Callable, Tuple
import cv2 import cv2
from cv2_enumerate_cameras import enumerate_cameras # Add this import from cv2_enumerate_cameras import enumerate_cameras
from PIL import Image, ImageOps from PIL import Image, ImageOps
import time import time
import json import json
@@ -28,6 +28,7 @@ from modules.utilities import (
from modules.video_capture import VideoCapturer from modules.video_capture import VideoCapturer
from modules.gettext import LanguageManager from modules.gettext import LanguageManager
import platform import platform
from modules.fake_face_handler import cleanup_fake_face, refresh_fake_face
if platform.system() == "Windows": if platform.system() == "Windows":
from pygrabber.dshow_graph import FilterGraph from pygrabber.dshow_graph import FilterGraph
@@ -35,7 +36,7 @@ if platform.system() == "Windows":
ROOT = None ROOT = None
POPUP = None POPUP = None
POPUP_LIVE = None POPUP_LIVE = None
ROOT_HEIGHT = 700 ROOT_HEIGHT = 730
ROOT_WIDTH = 600 ROOT_WIDTH = 600
PREVIEW = None PREVIEW = None
@@ -78,9 +79,12 @@ target_label_dict_live = {}
img_ft, vid_ft = modules.globals.file_types 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: 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 = LanguageManager(lang)
_ = lang_manager._ _ = lang_manager._
@@ -91,51 +95,56 @@ def init(start: Callable[[], None], destroy: Callable[[], None], lang: str) -> c
def save_switch_states(): def save_switch_states():
switch_states = { try:
"keep_fps": modules.globals.keep_fps, states = {
"keep_audio": modules.globals.keep_audio, "keep_fps": modules.globals.keep_fps,
"keep_frames": modules.globals.keep_frames, "keep_audio": modules.globals.keep_audio,
"many_faces": modules.globals.many_faces, "keep_frames": modules.globals.keep_frames,
"map_faces": modules.globals.map_faces, "many_faces": modules.globals.many_faces,
"color_correction": modules.globals.color_correction, "map_faces": modules.globals.map_faces,
"nsfw_filter": modules.globals.nsfw_filter, "color_correction": modules.globals.color_correction,
"live_mirror": modules.globals.live_mirror, "nsfw_filter": modules.globals.nsfw_filter,
"live_resizable": modules.globals.live_resizable, "live_mirror": modules.globals.live_mirror,
"fp_ui": modules.globals.fp_ui, "live_resizable": modules.globals.live_resizable,
"show_fps": modules.globals.show_fps, "fp_ui": modules.globals.fp_ui,
"mouth_mask": modules.globals.mouth_mask, "show_fps": modules.globals.show_fps,
"show_mouth_mask_box": modules.globals.show_mouth_mask_box, "mouth_mask": modules.globals.mouth_mask,
} "show_mouth_mask_box": modules.globals.show_mouth_mask_box,
with open("switch_states.json", "w") as f: "use_fake_face": modules.globals.use_fake_face
json.dump(switch_states, f) }
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(): def load_switch_states():
try: try:
with open("switch_states.json", "r") as f: if os.path.exists(get_config_path()):
switch_states = json.load(f) with open(get_config_path(), 'r') as f:
modules.globals.keep_fps = switch_states.get("keep_fps", True) states = json.load(f)
modules.globals.keep_audio = switch_states.get("keep_audio", True) modules.globals.keep_fps = states.get("keep_fps", True)
modules.globals.keep_frames = switch_states.get("keep_frames", False) modules.globals.keep_audio = states.get("keep_audio", True)
modules.globals.many_faces = switch_states.get("many_faces", False) modules.globals.keep_frames = states.get("keep_frames", False)
modules.globals.map_faces = switch_states.get("map_faces", False) modules.globals.many_faces = states.get("many_faces", False)
modules.globals.color_correction = switch_states.get("color_correction", False) modules.globals.map_faces = states.get("map_faces", False)
modules.globals.nsfw_filter = switch_states.get("nsfw_filter", False) modules.globals.color_correction = states.get("color_correction", False)
modules.globals.live_mirror = switch_states.get("live_mirror", False) modules.globals.nsfw_filter = states.get("nsfw_filter", False)
modules.globals.live_resizable = switch_states.get("live_resizable", False) modules.globals.live_mirror = states.get("live_mirror", False)
modules.globals.fp_ui = switch_states.get("fp_ui", {"face_enhancer": False}) modules.globals.live_resizable = states.get("live_resizable", False)
modules.globals.show_fps = switch_states.get("show_fps", False) modules.globals.fp_ui = states.get("fp_ui", {"face_enhancer": False})
modules.globals.mouth_mask = switch_states.get("mouth_mask", False) modules.globals.show_fps = states.get("show_fps", False)
modules.globals.show_mouth_mask_box = switch_states.get( modules.globals.mouth_mask = states.get("mouth_mask", False)
"show_mouth_mask_box", False modules.globals.show_mouth_mask_box = states.get(
) "show_mouth_mask_box", False
except FileNotFoundError: )
# If the file doesn't exist, use default values modules.globals.use_fake_face = False
pass except Exception as e:
print(f"Error loading switch states: {str(e)}")
def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.CTk: 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() load_switch_states()
@@ -150,22 +159,28 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
) )
root.configure() root.configure()
root.protocol("WM_DELETE_WINDOW", lambda: destroy()) 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) 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 = 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( 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.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( swap_faces_button = ctk.CTkButton(
root, text="", cursor="hand2", command=lambda: swap_faces_paths() 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( select_target_button = ctk.CTkButton(
root, root,
@@ -173,60 +188,30 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
cursor="hand2", cursor="hand2",
command=lambda: select_target_path(), 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)
keep_fps_value = ctk.BooleanVar(value=modules.globals.keep_fps) # AI Generated Face controls
keep_fps_checkbox = ctk.CTkSwitch( fake_face_value = ctk.BooleanVar(value=modules.globals.use_fake_face)
fake_face_switch = ctk.CTkSwitch(
root, root,
text=_("Keep fps"), text=_("Privacy Mode"),
variable=keep_fps_value, variable=fake_face_value,
cursor="hand2", cursor="hand2",
command=lambda: ( command=lambda: toggle_fake_face(fake_face_value)
setattr(modules.globals, "keep_fps", keep_fps_value.get()),
save_switch_states(),
),
) )
keep_fps_checkbox.place(relx=0.1, rely=0.6) fake_face_switch.place(relx=0.1, rely=0.50)
keep_frames_value = ctk.BooleanVar(value=modules.globals.keep_frames) # Add refresh button next to the switch
keep_frames_switch = ctk.CTkSwitch( refresh_face_button = ctk.CTkButton(
root, root,
text=_("Keep frames"), text="",
variable=keep_frames_value, width=30,
cursor="hand2", cursor="hand2",
command=lambda: ( command=lambda: refresh_fake_face_clicked()
setattr(modules.globals, "keep_frames", keep_frames_value.get()),
save_switch_states(),
),
) )
keep_frames_switch.place(relx=0.1, rely=0.65) refresh_face_button.place(relx=0.35, rely=0.50)
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_value = ctk.BooleanVar(value=modules.globals.many_faces)
many_faces_switch = ctk.CTkSwitch( many_faces_switch = ctk.CTkSwitch(
root, root,
@@ -238,24 +223,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.65) many_faces_switch.place(relx=0.1, rely=0.55)
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)
map_faces = ctk.BooleanVar(value=modules.globals.map_faces) map_faces = ctk.BooleanVar(value=modules.globals.map_faces)
map_faces_switch = ctk.CTkSwitch( map_faces_switch = ctk.CTkSwitch(
@@ -269,8 +237,35 @@ 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.60)
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.65)
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.70)
# Add show FPS switch right after keep_audio_switch
show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps) show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps)
show_fps_switch = ctk.CTkSwitch( show_fps_switch = ctk.CTkSwitch(
root, root,
@@ -282,8 +277,9 @@ 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.75) show_fps_switch.place(relx=0.1, rely=0.75)
# Mask Switches (Middle Right - Top Section)
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( mouth_mask_switch = ctk.CTkSwitch(
root, root,
@@ -292,38 +288,117 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
cursor="hand2", cursor="hand2",
command=lambda: setattr(modules.globals, "mouth_mask", mouth_mask_var.get()), 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.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,
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)
# 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,
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)
# 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_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box)
show_mouth_mask_box_switch = ctk.CTkSwitch( show_mouth_mask_box_switch = ctk.CTkSwitch(
root, root,
text=_("Show Mouth Mask Box"), text=_("Show Mouth Box"),
variable=show_mouth_mask_box_var, variable=show_mouth_mask_box_var,
cursor="hand2", cursor="hand2",
command=lambda: setattr( command=lambda: setattr(
modules.globals, "show_mouth_mask_box", show_mouth_mask_box_var.get() 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)
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 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.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 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_eyebrows_mask_box_switch.place(relx=0.6, rely=0.75)
# Main Control Buttons (Bottom)
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.80, relwidth=0.2, relheight=0.05) start_button.place(relx=0.15, 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.4, rely=0.80, relwidth=0.2, relheight=0.05)
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.80, relwidth=0.2, relheight=0.05) preview_button.place(relx=0.4, rely=0.80, relwidth=0.2, relheight=0.05)
# --- Camera Selection --- stop_button = ctk.CTkButton(
root, text=_("Destroy"), cursor="hand2", command=lambda: destroy()
)
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 = 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.87, relwidth=0.2, relheight=0.05)
available_cameras = get_available_cameras() available_cameras = get_available_cameras()
camera_indices, camera_names = available_cameras camera_indices, camera_names = available_cameras
@@ -342,7 +417,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.86, 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( live_button = ctk.CTkButton(
root, root,
@@ -362,16 +437,16 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
else "disabled" else "disabled"
), ),
) )
live_button.place(relx=0.65, rely=0.86, relwidth=0.2, relheight=0.05) live_button.place(relx=0.65, rely=0.87, relwidth=0.2, relheight=0.05)
# --- End Camera Selection ---
# Status and Links (Bottom)
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.9, relwidth=0.8) status_label.place(relx=0.1, rely=0.92, 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.95, relwidth=0.8) donate_label.place(relx=0.1, rely=0.94, 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")
) )
@@ -397,7 +472,7 @@ def analyze_target(start: Callable[[], None], root: ctk.CTk):
return return
if modules.globals.map_faces: if modules.globals.map_faces:
modules.globals.source_target_map = [] modules.globals.souce_target_map = []
if is_image(modules.globals.target_path): if is_image(modules.globals.target_path):
update_status("Getting unique faces") update_status("Getting unique faces")
@@ -406,8 +481,8 @@ def analyze_target(start: Callable[[], None], root: ctk.CTk):
update_status("Getting unique faces") update_status("Getting unique faces")
get_unique_faces_from_target_video() get_unique_faces_from_target_video()
if len(modules.globals.source_target_map) > 0: if len(modules.globals.souce_target_map) > 0:
create_source_target_popup(start, root, modules.globals.source_target_map) create_source_target_popup(start, root, modules.globals.souce_target_map)
else: else:
update_status("No faces found in target") update_status("No faces found in target")
else: else:
@@ -544,6 +619,11 @@ def create_preview(parent: ctk.CTkToplevel) -> ctk.CTkToplevel:
preview.configure() preview.configure()
preview.protocol("WM_DELETE_WINDOW", lambda: toggle_preview()) preview.protocol("WM_DELETE_WINDOW", lambda: toggle_preview())
preview.resizable(width=True, height=True) 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 = ctk.CTkLabel(preview, text=None)
preview_label.pack(fill="both", expand=True) preview_label.pack(fill="both", expand=True)
@@ -580,7 +660,7 @@ def update_tumbler(var: str, value: bool) -> None:
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, fake_face_switch, fake_face_value
PREVIEW.withdraw() PREVIEW.withdraw()
source_path = ctk.filedialog.askopenfilename( source_path = ctk.filedialog.askopenfilename(
@@ -589,6 +669,10 @@ def select_source_path() -> None:
filetypes=[img_ft], filetypes=[img_ft],
) )
if is_image(source_path): 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 modules.globals.source_path = source_path
RECENT_DIRECTORY_SOURCE = os.path.dirname(modules.globals.source_path) RECENT_DIRECTORY_SOURCE = os.path.dirname(modules.globals.source_path)
image = render_image_preview(modules.globals.source_path, (200, 200)) image = render_image_preview(modules.globals.source_path, (200, 200))
@@ -696,21 +780,17 @@ def check_and_ignore_nsfw(target, destroy: Callable = None) -> bool:
def fit_image_to_size(image, width: int, height: int): def fit_image_to_size(image, width: int, height: int):
if width is None or height is None or width <= 0 or height <= 0: if width is None and height is None:
return image return image
h, w, _ = image.shape h, w, _ = image.shape
ratio_h = 0.0 ratio_h = 0.0
ratio_w = 0.0 ratio_w = 0.0
ratio_w = width / w if width > height:
ratio_h = height / h ratio_h = height / h
# Use the smaller ratio to ensure the image fits within the given dimensions else:
ratio = min(ratio_w, ratio_h) ratio_w = width / w
ratio = max(ratio_w, ratio_h)
# Compute new dimensions, ensuring they're at least 1 pixel new_size = (int(ratio * w), int(ratio * h))
new_width = max(1, int(ratio * w))
new_height = max(1, int(ratio * h))
new_size = (new_width, new_height)
return cv2.resize(image, dsize=new_size) return cv2.resize(image, dsize=new_size)
@@ -765,8 +845,7 @@ def update_preview(frame_number: int = 0) -> None:
modules.globals.frame_processors modules.globals.frame_processors
): ):
temp_frame = frame_processor.process_frame( 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 = Image.fromarray(cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB))
image = ImageOps.contain( image = ImageOps.contain(
image, (PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.LANCZOS image, (PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.LANCZOS
@@ -791,9 +870,9 @@ def webcam_preview(root: ctk.CTk, camera_index: int):
return return
create_webcam_preview(camera_index) create_webcam_preview(camera_index)
else: else:
modules.globals.source_target_map = [] modules.globals.souce_target_map = []
create_source_target_popup_for_webcam( create_source_target_popup_for_webcam(
root, modules.globals.source_target_map, camera_index root, modules.globals.souce_target_map, camera_index
) )
@@ -1204,3 +1283,42 @@ def update_webcam_target(
else: else:
update_pop_live_status("Face could not be detected in last upload!") update_pop_live_status("Face could not be detected in last upload!")
return map 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:
"""Handle refresh button click to update fake face during live preview"""
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"""
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")
+11 -10
View File
@@ -1,20 +1,21 @@
--extra-index-url https://download.pytorch.org/whl/cu118
numpy>=1.23.5,<2 numpy>=1.23.5,<2
typing-extensions>=4.8.0 opencv-python==4.10.0.84
opencv-python==4.11.0.86 cv2_enumerate_cameras==1.1.15
onnx==1.17.0 onnx==1.16.0
cv2_enumerate_cameras==1.1.18.3
insightface==0.7.3 insightface==0.7.3
psutil==5.9.8 psutil==5.9.8
tk==0.1.0 tk==0.1.0
customtkinter==5.2.2 customtkinter==5.2.2
pillow==11.1.0 pillow==11.1.0
torch; sys_platform != 'darwin' --index-url https://download.pytorch.org/whl/cu126 torch==2.0.1+cu118; sys_platform != 'darwin'
torch; sys_platform == 'darwin' --index-url https://download.pytorch.org/whl/cu126 torch==2.0.1; sys_platform == 'darwin'
torchvision; sys_platform != 'darwin' --index-url https://download.pytorch.org/whl/cu126 torchvision==0.15.2+cu118; sys_platform != 'darwin'
torchvision; sys_platform == 'darwin' --index-url https://download.pytorch.org/whl/cu126 torchvision==0.15.2; sys_platform == 'darwin'
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.21; sys_platform != 'darwin' onnxruntime-gpu==1.16.3; sys_platform != 'darwin'
tensorflow; sys_platform != 'darwin' tensorflow==2.12.1; sys_platform != 'darwin'
opennsfw2==0.10.2 opennsfw2==0.10.2
protobuf==4.23.2 protobuf==4.23.2
tqdm==4.66.4 tqdm==4.66.4