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