Deep-Live-Cam/modules/ui.py

1321 lines
46 KiB
Python
Raw Normal View History

2023-09-24 21:36:57 +08:00
import os
import webbrowser
import customtkinter as ctk
from typing import Callable, Tuple
2023-09-24 21:36:57 +08:00
import cv2
2024-10-04 22:16:45 +08:00
from cv2_enumerate_cameras import enumerate_cameras # Add this import
2023-09-24 21:36:57 +08:00
from PIL import Image, ImageOps
2024-10-05 20:09:41 +08:00
import time
2024-10-09 22:21:04 +08:00
import json
2023-09-24 21:36:57 +08:00
import modules.globals
import modules.metadata
2024-09-25 20:06:50 +08:00
from modules.face_analyser import (
get_one_face,
get_unique_faces_from_target_image,
get_unique_faces_from_target_video,
add_blank_map,
has_valid_map,
simplify_maps,
)
2023-09-24 21:36:57 +08:00
from modules.capturer import get_video_frame, get_video_frame_total
from modules.processors.frame.core import get_frame_processors_modules
2024-09-25 20:06:50 +08:00
from modules.utilities import (
is_image,
is_video,
resolve_relative_path,
has_image_extension,
)
from modules.video_capture import VideoCapturer
from modules.gettext import LanguageManager
import platform
if platform.system() == "Windows":
from pygrabber.dshow_graph import FilterGraph
2023-09-24 21:36:57 +08:00
ROOT = None
POPUP = None
POPUP_LIVE = None
2024-10-04 22:16:45 +08:00
ROOT_HEIGHT = 700
ROOT_WIDTH = 600
2023-09-24 21:36:57 +08:00
PREVIEW = None
2024-10-04 22:16:45 +08:00
PREVIEW_MAX_HEIGHT = 700
PREVIEW_MAX_WIDTH = 1200
PREVIEW_DEFAULT_WIDTH = 960
PREVIEW_DEFAULT_HEIGHT = 540
2023-09-24 21:36:57 +08:00
2024-10-04 22:16:45 +08:00
POPUP_WIDTH = 750
POPUP_HEIGHT = 810
POPUP_SCROLL_WIDTH = (740,)
POPUP_SCROLL_HEIGHT = 700
2024-10-04 22:16:45 +08:00
POPUP_LIVE_WIDTH = 900
POPUP_LIVE_HEIGHT = 820
POPUP_LIVE_SCROLL_WIDTH = (890,)
POPUP_LIVE_SCROLL_HEIGHT = 700
2024-10-04 22:16:45 +08:00
MAPPER_PREVIEW_MAX_HEIGHT = 100
MAPPER_PREVIEW_MAX_WIDTH = 100
DEFAULT_BUTTON_WIDTH = 200
DEFAULT_BUTTON_HEIGHT = 40
2023-09-24 21:36:57 +08:00
RECENT_DIRECTORY_SOURCE = None
RECENT_DIRECTORY_TARGET = None
RECENT_DIRECTORY_OUTPUT = None
_ = None
2023-09-24 21:36:57 +08:00
preview_label = None
preview_slider = None
source_label = None
target_label = None
status_label = None
popup_status_label = None
popup_status_label_live = None
source_label_dict = {}
source_label_dict_live = {}
target_label_dict_live = {}
2023-09-24 21:36:57 +08:00
img_ft, vid_ft = modules.globals.file_types
def init(start: Callable[[], None], destroy: Callable[[], None], lang: str) -> ctk.CTk:
global ROOT, PREVIEW, _
2023-09-24 21:36:57 +08:00
lang_manager = LanguageManager(lang)
_ = lang_manager._
2023-09-24 21:36:57 +08:00
ROOT = create_root(start, destroy)
PREVIEW = create_preview(ROOT)
return ROOT
2024-10-09 22:21:04 +08:00
def save_switch_states():
switch_states = {
"keep_fps": modules.globals.keep_fps,
"keep_audio": modules.globals.keep_audio,
"keep_frames": modules.globals.keep_frames,
"many_faces": modules.globals.many_faces,
"map_faces": modules.globals.map_faces,
"color_correction": modules.globals.color_correction,
"nsfw_filter": modules.globals.nsfw_filter,
"live_mirror": modules.globals.live_mirror,
"live_resizable": modules.globals.live_resizable,
"fp_ui": modules.globals.fp_ui,
"show_fps": modules.globals.show_fps,
"mouth_mask": modules.globals.mouth_mask,
"show_mouth_mask_box": modules.globals.show_mouth_mask_box,
"enable_hair_swapping": modules.globals.enable_hair_swapping,
2024-10-09 22:21:04 +08:00
}
with open("switch_states.json", "w") as f:
json.dump(switch_states, f)
def load_switch_states():
try:
with open("switch_states.json", "r") as f:
switch_states = json.load(f)
modules.globals.keep_fps = switch_states.get("keep_fps", True)
modules.globals.keep_audio = switch_states.get("keep_audio", True)
modules.globals.keep_frames = switch_states.get("keep_frames", False)
modules.globals.many_faces = switch_states.get("many_faces", False)
modules.globals.map_faces = switch_states.get("map_faces", False)
modules.globals.color_correction = switch_states.get("color_correction", False)
modules.globals.nsfw_filter = switch_states.get("nsfw_filter", False)
modules.globals.live_mirror = switch_states.get("live_mirror", False)
modules.globals.live_resizable = switch_states.get("live_resizable", False)
modules.globals.fp_ui = switch_states.get("fp_ui", {"face_enhancer": False})
modules.globals.show_fps = switch_states.get("show_fps", False)
2024-10-26 20:24:45 +08:00
modules.globals.mouth_mask = switch_states.get("mouth_mask", False)
modules.globals.show_mouth_mask_box = switch_states.get(
"show_mouth_mask_box", False
)
modules.globals.enable_hair_swapping = switch_states.get(
"enable_hair_swapping", True # Default to True if not found
)
2024-10-09 22:21:04 +08:00
except FileNotFoundError:
# If the file doesn't exist, use default values
pass
2024-10-04 22:16:45 +08:00
def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.CTk:
2024-10-05 20:09:41 +08:00
global source_label, target_label, status_label, show_fps_switch
2023-09-24 21:36:57 +08:00
2024-10-09 22:21:04 +08:00
load_switch_states()
2024-10-04 22:16:45 +08:00
ctk.deactivate_automatic_dpi_awareness()
ctk.set_appearance_mode("system")
ctk.set_default_color_theme(resolve_relative_path("ui.json"))
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
root = ctk.CTk()
root.minsize(ROOT_WIDTH, ROOT_HEIGHT)
2024-09-25 20:06:50 +08:00
root.title(
f"{modules.metadata.name} {modules.metadata.version} {modules.metadata.edition}"
)
2024-10-04 22:16:45 +08:00
root.configure()
2024-09-25 20:06:50 +08:00
root.protocol("WM_DELETE_WINDOW", lambda: destroy())
2024-10-04 22:16:45 +08:00
source_label = ctk.CTkLabel(root, text=None)
source_label.place(relx=0.1, rely=0.1, relwidth=0.3, relheight=0.25)
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
target_label = ctk.CTkLabel(root, text=None)
target_label.place(relx=0.6, rely=0.1, relwidth=0.3, relheight=0.25)
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
select_face_button = ctk.CTkButton(
root, text=_("Select a face"), cursor="hand2", command=lambda: select_source_path()
2024-09-25 20:06:50 +08:00
)
2024-10-04 22:16:45 +08:00
select_face_button.place(relx=0.1, rely=0.4, relwidth=0.3, relheight=0.1)
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
swap_faces_button = ctk.CTkButton(
root, text="", cursor="hand2", command=lambda: swap_faces_paths()
2024-09-25 20:06:50 +08:00
)
2024-10-04 22:16:45 +08:00
swap_faces_button.place(relx=0.45, rely=0.4, relwidth=0.1, relheight=0.1)
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
select_target_button = ctk.CTkButton(
root,
text=_("Select a target"),
2024-09-25 20:06:50 +08:00
cursor="hand2",
command=lambda: select_target_path(),
)
2024-10-04 22:16:45 +08:00
select_target_button.place(relx=0.6, rely=0.4, relwidth=0.3, relheight=0.1)
2024-09-25 20:06:50 +08:00
2023-09-24 21:36:57 +08:00
keep_fps_value = ctk.BooleanVar(value=modules.globals.keep_fps)
2024-09-25 20:06:50 +08:00
keep_fps_checkbox = ctk.CTkSwitch(
2024-10-04 22:16:45 +08:00
root,
text=_("Keep fps"),
2024-09-25 20:06:50 +08:00
variable=keep_fps_value,
cursor="hand2",
2024-10-09 22:21:04 +08:00
command=lambda: (
setattr(modules.globals, "keep_fps", keep_fps_value.get()),
save_switch_states(),
2024-09-25 20:06:50 +08:00
),
)
2024-10-04 22:16:45 +08:00
keep_fps_checkbox.place(relx=0.1, rely=0.6)
2023-09-24 21:36:57 +08:00
keep_frames_value = ctk.BooleanVar(value=modules.globals.keep_frames)
2024-09-25 20:06:50 +08:00
keep_frames_switch = ctk.CTkSwitch(
2024-10-04 22:16:45 +08:00
root,
text=_("Keep frames"),
2024-09-25 20:06:50 +08:00
variable=keep_frames_value,
cursor="hand2",
2024-10-09 22:21:04 +08:00
command=lambda: (
setattr(modules.globals, "keep_frames", keep_frames_value.get()),
save_switch_states(),
2024-09-25 20:06:50 +08:00
),
)
2024-10-04 22:16:45 +08:00
keep_frames_switch.place(relx=0.1, rely=0.65)
2024-09-25 20:06:50 +08:00
enhancer_value = ctk.BooleanVar(value=modules.globals.fp_ui["face_enhancer"])
enhancer_switch = ctk.CTkSwitch(
2024-10-04 22:16:45 +08:00
root,
text=_("Face Enhancer"),
2024-09-25 20:06:50 +08:00
variable=enhancer_value,
cursor="hand2",
2024-10-09 22:21:04 +08:00
command=lambda: (
update_tumbler("face_enhancer", enhancer_value.get()),
save_switch_states(),
),
2024-09-25 20:06:50 +08:00
)
2024-10-04 22:16:45 +08:00
enhancer_switch.place(relx=0.1, rely=0.7)
2023-09-24 21:36:57 +08:00
keep_audio_value = ctk.BooleanVar(value=modules.globals.keep_audio)
2024-09-25 20:06:50 +08:00
keep_audio_switch = ctk.CTkSwitch(
2024-10-04 22:16:45 +08:00
root,
text=_("Keep audio"),
2024-09-25 20:06:50 +08:00
variable=keep_audio_value,
cursor="hand2",
2024-10-09 22:21:04 +08:00
command=lambda: (
setattr(modules.globals, "keep_audio", keep_audio_value.get()),
save_switch_states(),
),
2024-09-25 20:06:50 +08:00
)
2024-10-04 22:16:45 +08:00
keep_audio_switch.place(relx=0.6, rely=0.6)
2023-09-24 21:36:57 +08:00
many_faces_value = ctk.BooleanVar(value=modules.globals.many_faces)
2024-09-25 20:06:50 +08:00
many_faces_switch = ctk.CTkSwitch(
2024-10-04 22:16:45 +08:00
root,
text=_("Many faces"),
2024-09-25 20:06:50 +08:00
variable=many_faces_value,
cursor="hand2",
2024-10-09 22:21:04 +08:00
command=lambda: (
setattr(modules.globals, "many_faces", many_faces_value.get()),
save_switch_states(),
),
2024-09-25 20:06:50 +08:00
)
2024-10-04 22:16:45 +08:00
many_faces_switch.place(relx=0.6, rely=0.65)
2023-09-24 21:36:57 +08:00
color_correction_value = ctk.BooleanVar(value=modules.globals.color_correction)
2024-09-25 20:06:50 +08:00
color_correction_switch = ctk.CTkSwitch(
2024-10-04 22:16:45 +08:00
root,
text=_("Fix Blueish Cam"),
2024-09-25 20:06:50 +08:00
variable=color_correction_value,
cursor="hand2",
2024-10-09 22:21:04 +08:00
command=lambda: (
setattr(modules.globals, "color_correction", color_correction_value.get()),
save_switch_states(),
2024-09-25 20:06:50 +08:00
),
)
2024-10-04 22:16:45 +08:00
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)
2024-09-25 20:06:50 +08:00
map_faces_switch = ctk.CTkSwitch(
2024-10-04 22:16:45 +08:00
root,
text=_("Map faces"),
2024-09-25 20:06:50 +08:00
variable=map_faces,
cursor="hand2",
2024-10-09 22:21:04 +08:00
command=lambda: (
setattr(modules.globals, "map_faces", map_faces.get()),
save_switch_states(),
2025-01-06 23:31:33 +08:00
close_mapper_window() if not map_faces.get() else None
2024-10-09 22:21:04 +08:00
),
2024-09-25 20:06:50 +08:00
)
2024-10-04 22:16:45 +08:00
map_faces_switch.place(relx=0.1, rely=0.75)
2024-09-25 20:06:50 +08:00
2024-10-09 22:21:04 +08:00
show_fps_value = ctk.BooleanVar(value=modules.globals.show_fps)
2024-10-05 20:09:41 +08:00
show_fps_switch = ctk.CTkSwitch(
root,
text=_("Show FPS"),
2024-10-05 20:09:41 +08:00
variable=show_fps_value,
cursor="hand2",
2024-10-09 22:21:04 +08:00
command=lambda: (
setattr(modules.globals, "show_fps", show_fps_value.get()),
save_switch_states(),
),
2024-10-05 20:09:41 +08:00
)
show_fps_switch.place(relx=0.6, rely=0.75)
# Hair Swapping Switch (placed below "Show FPS" on the right column)
hair_swapping_value = ctk.BooleanVar(value=modules.globals.enable_hair_swapping)
hair_swapping_switch = ctk.CTkSwitch(
root,
text=_("Swap Hair"),
variable=hair_swapping_value,
cursor="hand2",
command=lambda: (
setattr(modules.globals, "enable_hair_swapping", hair_swapping_value.get()),
save_switch_states(),
)
)
hair_swapping_switch.place(relx=0.6, rely=0.80) # Adjusted rely from 0.75 to 0.80
2024-10-25 23:29:30 +08:00
mouth_mask_var = ctk.BooleanVar(value=modules.globals.mouth_mask)
mouth_mask_switch = ctk.CTkSwitch(
root,
text=_("Mouth Mask"),
2024-10-25 23:29:30 +08:00
variable=mouth_mask_var,
cursor="hand2",
command=lambda: setattr(modules.globals, "mouth_mask", mouth_mask_var.get()),
)
mouth_mask_switch.place(relx=0.1, rely=0.55)
show_mouth_mask_box_var = ctk.BooleanVar(value=modules.globals.show_mouth_mask_box)
show_mouth_mask_box_switch = ctk.CTkSwitch(
root,
text=_("Show Mouth Mask Box"),
2024-10-25 23:29:30 +08:00
variable=show_mouth_mask_box_var,
cursor="hand2",
command=lambda: setattr(
modules.globals, "show_mouth_mask_box", show_mouth_mask_box_var.get()
),
)
show_mouth_mask_box_switch.place(relx=0.6, rely=0.55)
# Adjusting placement of Start, Stop, Preview buttons due to new switch
2024-10-04 22:16:45 +08:00
start_button = ctk.CTkButton(
root, text=_("Start"), cursor="hand2", command=lambda: analyze_target(start, root)
2024-10-04 22:16:45 +08:00
)
start_button.place(relx=0.15, rely=0.85, relwidth=0.2, relheight=0.05) # rely from 0.80 to 0.85
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
stop_button = ctk.CTkButton(
root, text=_("Destroy"), cursor="hand2", command=lambda: destroy()
2024-09-25 20:06:50 +08:00
)
stop_button.place(relx=0.4, rely=0.85, relwidth=0.2, relheight=0.05) # rely from 0.80 to 0.85
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
preview_button = ctk.CTkButton(
root, text=_("Preview"), cursor="hand2", command=lambda: toggle_preview()
2024-09-25 20:06:50 +08:00
)
preview_button.place(relx=0.65, rely=0.85, relwidth=0.2, relheight=0.05) # rely from 0.80 to 0.85
2024-09-25 20:06:50 +08:00
# --- Camera Selection ---
# Adjusting placement of Camera selection due to new switch
camera_label = ctk.CTkLabel(root, text=_("Select Camera:"))
camera_label.place(relx=0.1, rely=0.91, relwidth=0.2, relheight=0.05) # rely from 0.86 to 0.91
2024-10-04 22:16:45 +08:00
available_cameras = get_available_cameras()
camera_indices, camera_names = available_cameras
if not camera_names or camera_names[0] == "No cameras found":
camera_variable = ctk.StringVar(value="No cameras found")
camera_optionmenu = ctk.CTkOptionMenu(
root,
variable=camera_variable,
values=["No cameras found"],
state="disabled",
2024-12-13 22:19:11 +08:00
)
else:
camera_variable = ctk.StringVar(value=camera_names[0])
camera_optionmenu = ctk.CTkOptionMenu(
root, variable=camera_variable, values=camera_names
)
camera_optionmenu.place(relx=0.35, rely=0.91, relwidth=0.25, relheight=0.05) # rely from 0.86 to 0.91
2024-10-04 22:16:45 +08:00
live_button = ctk.CTkButton(
root,
text=_("Live"),
2024-09-25 20:06:50 +08:00
cursor="hand2",
command=lambda: webcam_preview(
root,
(
camera_indices[camera_names.index(camera_variable.get())]
if camera_names and camera_names[0] != "No cameras found"
else None
),
),
state=(
"normal"
if camera_names and camera_names[0] != "No cameras found"
else "disabled"
),
2024-09-25 20:06:50 +08:00
)
live_button.place(relx=0.65, rely=0.91, relwidth=0.2, relheight=0.05) # rely from 0.86 to 0.91
2024-10-04 22:16:45 +08:00
# --- End Camera Selection ---
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
status_label = ctk.CTkLabel(root, text=None, justify="center")
status_label.place(relx=0.1, rely=0.96, relwidth=0.8) # rely from 0.9 to 0.96
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
donate_label = ctk.CTkLabel(
root, text="Deep Live Cam", justify="center", cursor="hand2"
2024-09-25 20:06:50 +08:00
)
donate_label.place(relx=0.1, rely=0.99, relwidth=0.8) # rely from 0.95 to 0.99
2024-10-04 22:16:45 +08:00
donate_label.configure(
text_color=ctk.ThemeManager.theme.get("URL").get("text_color")
2024-09-25 20:06:50 +08:00
)
donate_label.bind(
"<Button>", lambda event: webbrowser.open("https://deeplivecam.net")
2024-09-25 20:06:50 +08:00
)
2023-09-24 21:36:57 +08:00
return root
2025-01-06 23:31:33 +08:00
def close_mapper_window():
global POPUP, POPUP_LIVE
if POPUP and POPUP.winfo_exists():
POPUP.destroy()
POPUP = None
if POPUP_LIVE and POPUP_LIVE.winfo_exists():
POPUP_LIVE.destroy()
POPUP_LIVE = None
2024-09-25 20:06:50 +08:00
def analyze_target(start: Callable[[], None], root: ctk.CTk):
2024-10-04 22:16:45 +08:00
if POPUP != None and POPUP.winfo_exists():
update_status("Please complete pop-up or close it.")
return
if modules.globals.map_faces:
modules.globals.source_target_map = []
if is_image(modules.globals.target_path):
2024-09-25 20:06:50 +08:00
update_status("Getting unique faces")
get_unique_faces_from_target_image()
elif is_video(modules.globals.target_path):
2024-09-25 20:06:50 +08:00
update_status("Getting unique faces")
get_unique_faces_from_target_video()
if len(modules.globals.source_target_map) > 0:
create_source_target_popup(start, root, modules.globals.source_target_map)
else:
update_status("No faces found in target")
else:
select_output_path(start)
2024-09-25 20:06:50 +08:00
def create_source_target_popup(
2025-01-06 23:31:33 +08:00
start: Callable[[], None], root: ctk.CTk, map: list
2024-09-25 20:06:50 +08:00
) -> None:
global POPUP, popup_status_label
POPUP = ctk.CTkToplevel(root)
POPUP.title(_("Source x Target Mapper"))
POPUP.geometry(f"{POPUP_WIDTH}x{POPUP_HEIGHT}")
POPUP.focus()
def on_submit_click(start):
if has_valid_map():
POPUP.destroy()
select_output_path(start)
else:
2025-05-07 19:23:31 +08:00
update_pop_status("At least 1 source with target is required!")
2024-09-25 20:06:50 +08:00
scrollable_frame = ctk.CTkScrollableFrame(
POPUP, width=POPUP_SCROLL_WIDTH, height=POPUP_SCROLL_HEIGHT
)
2024-10-04 22:16:45 +08:00
scrollable_frame.grid(row=0, column=0, padx=0, pady=0, sticky="nsew")
def on_button_click(map, button_num):
2024-10-04 22:16:45 +08:00
map = update_popup_source(scrollable_frame, map, button_num)
2024-09-17 20:16:14 +08:00
2024-09-25 20:06:50 +08:00
for item in map:
id = item["id"]
2024-10-04 22:16:45 +08:00
button = ctk.CTkButton(
scrollable_frame,
text=_("Select source image"),
2024-09-25 20:06:50 +08:00
command=lambda id=id: on_button_click(map, id),
width=DEFAULT_BUTTON_WIDTH,
height=DEFAULT_BUTTON_HEIGHT,
)
2024-10-04 22:16:45 +08:00
button.grid(row=id, column=0, padx=50, pady=10)
2024-09-25 20:06:50 +08:00
x_label = ctk.CTkLabel(
2024-10-04 22:16:45 +08:00
scrollable_frame,
2024-09-25 20:06:50 +08:00
text=f"X",
2024-10-04 22:16:45 +08:00
width=MAPPER_PREVIEW_MAX_WIDTH,
height=MAPPER_PREVIEW_MAX_HEIGHT,
2024-09-25 20:06:50 +08:00
)
2024-10-04 22:16:45 +08:00
x_label.grid(row=id, column=2, padx=10, pady=10)
2024-09-25 20:06:50 +08:00
image = Image.fromarray(cv2.cvtColor(item["target"]["cv2"], cv2.COLOR_BGR2RGB))
image = image.resize(
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
)
tk_image = ctk.CTkImage(image, size=image.size)
2024-09-25 20:06:50 +08:00
target_image = ctk.CTkLabel(
2024-10-04 22:16:45 +08:00
scrollable_frame,
2024-09-25 20:06:50 +08:00
text=f"T-{id}",
width=MAPPER_PREVIEW_MAX_WIDTH,
height=MAPPER_PREVIEW_MAX_HEIGHT,
)
2024-10-04 22:16:45 +08:00
target_image.grid(row=id, column=3, padx=10, pady=10)
target_image.configure(image=tk_image)
2024-10-04 22:16:45 +08:00
popup_status_label = ctk.CTkLabel(POPUP, text=None, justify="center")
popup_status_label.grid(row=1, column=0, pady=15)
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
close_button = ctk.CTkButton(
POPUP, text=_("Submit"), command=lambda: on_submit_click(start)
2024-10-04 22:16:45 +08:00
)
close_button.grid(row=2, column=0, pady=10)
2024-09-25 20:06:50 +08:00
def update_popup_source(
2025-01-06 23:31:33 +08:00
scrollable_frame: ctk.CTkScrollableFrame, map: list, button_num: int
2024-09-25 20:06:50 +08:00
) -> list:
2024-10-04 22:16:45 +08:00
global source_label_dict
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
source_path = ctk.filedialog.askopenfilename(
2025-05-07 19:23:31 +08:00
title=_("select a source image"),
2024-10-04 22:16:45 +08:00
initialdir=RECENT_DIRECTORY_SOURCE,
filetypes=[img_ft],
)
if "source" in map[button_num]:
map[button_num].pop("source")
2024-10-04 22:16:45 +08:00
source_label_dict[button_num].destroy()
del source_label_dict[button_num]
2024-09-25 20:06:50 +08:00
if source_path == "":
return map
else:
cv2_img = cv2.imread(source_path)
face = get_one_face(cv2_img)
if face:
2024-09-25 20:06:50 +08:00
x_min, y_min, x_max, y_max = face["bbox"]
map[button_num]["source"] = {
2025-01-06 23:31:33 +08:00
"cv2": cv2_img[int(y_min): int(y_max), int(x_min): int(x_max)],
2024-09-25 20:06:50 +08:00
"face": face,
}
image = Image.fromarray(
cv2.cvtColor(map[button_num]["source"]["cv2"], cv2.COLOR_BGR2RGB)
)
image = image.resize(
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
)
tk_image = ctk.CTkImage(image, size=image.size)
2024-09-25 20:06:50 +08:00
source_image = ctk.CTkLabel(
2024-10-04 22:16:45 +08:00
scrollable_frame,
2024-09-25 20:06:50 +08:00
text=f"S-{button_num}",
width=MAPPER_PREVIEW_MAX_WIDTH,
height=MAPPER_PREVIEW_MAX_HEIGHT,
)
2024-10-04 22:16:45 +08:00
source_image.grid(row=button_num, column=1, padx=10, pady=10)
source_image.configure(image=tk_image)
source_label_dict[button_num] = source_image
else:
update_pop_status("Face could not be detected in last upload!")
return map
2024-09-10 07:07:58 +08:00
2023-09-24 21:36:57 +08:00
def create_preview(parent: ctk.CTkToplevel) -> ctk.CTkToplevel:
2023-09-24 21:36:57 +08:00
global preview_label, preview_slider
preview = ctk.CTkToplevel(parent)
preview.withdraw()
preview.title(_("Preview"))
preview.configure()
2024-09-25 20:06:50 +08:00
preview.protocol("WM_DELETE_WINDOW", lambda: toggle_preview())
preview.resizable(width=True, height=True)
2023-09-24 21:36:57 +08:00
2024-10-04 22:16:45 +08:00
preview_label = ctk.CTkLabel(preview, text=None)
2024-09-25 20:06:50 +08:00
preview_label.pack(fill="both", expand=True)
preview_slider = ctk.CTkSlider(
2024-10-04 22:16:45 +08:00
preview, from_=0, to=0, command=lambda frame_value: update_preview(frame_value)
2024-09-25 20:06:50 +08:00
)
2023-09-24 21:36:57 +08:00
return preview
def update_status(text: str) -> None:
status_label.configure(text=_(text))
2023-09-24 21:36:57 +08:00
ROOT.update()
2024-09-25 20:06:50 +08:00
def update_pop_status(text: str) -> None:
popup_status_label.configure(text=_(text))
2024-09-25 20:06:50 +08:00
def update_pop_live_status(text: str) -> None:
popup_status_label_live.configure(text=_(text))
2023-09-24 21:36:57 +08:00
2024-09-25 20:06:50 +08:00
2023-09-24 21:36:57 +08:00
def update_tumbler(var: str, value: bool) -> None:
modules.globals.fp_ui[var] = value
2024-10-09 22:21:04 +08:00
save_switch_states()
# If we're currently in a live preview, update the frame processors
if PREVIEW.state() == "normal":
global frame_processors
frame_processors = get_frame_processors_modules(
modules.globals.frame_processors
)
2023-09-24 21:36:57 +08:00
def select_source_path() -> None:
global RECENT_DIRECTORY_SOURCE, img_ft, vid_ft
2023-09-24 21:36:57 +08:00
PREVIEW.withdraw()
2024-09-25 20:06:50 +08:00
source_path = ctk.filedialog.askopenfilename(
2025-05-07 19:23:31 +08:00
title=_("select a source image"),
2024-09-25 20:06:50 +08:00
initialdir=RECENT_DIRECTORY_SOURCE,
filetypes=[img_ft],
)
2023-09-24 21:36:57 +08:00
if is_image(source_path):
modules.globals.source_path = source_path
RECENT_DIRECTORY_SOURCE = os.path.dirname(modules.globals.source_path)
image = render_image_preview(modules.globals.source_path, (200, 200))
source_label.configure(image=image)
else:
modules.globals.source_path = None
source_label.configure(image=None)
def swap_faces_paths() -> None:
global RECENT_DIRECTORY_SOURCE, RECENT_DIRECTORY_TARGET
source_path = modules.globals.source_path
target_path = modules.globals.target_path
if not is_image(source_path) or not is_image(target_path):
return
modules.globals.source_path = target_path
modules.globals.target_path = source_path
RECENT_DIRECTORY_SOURCE = os.path.dirname(modules.globals.source_path)
RECENT_DIRECTORY_TARGET = os.path.dirname(modules.globals.target_path)
PREVIEW.withdraw()
source_image = render_image_preview(modules.globals.source_path, (200, 200))
source_label.configure(image=source_image)
target_image = render_image_preview(modules.globals.target_path, (200, 200))
target_label.configure(image=target_image)
2023-09-24 21:36:57 +08:00
def select_target_path() -> None:
global RECENT_DIRECTORY_TARGET, img_ft, vid_ft
2023-09-24 21:36:57 +08:00
PREVIEW.withdraw()
2024-09-25 20:06:50 +08:00
target_path = ctk.filedialog.askopenfilename(
2025-05-07 19:23:31 +08:00
title=_("select a target image or video"),
2024-09-25 20:06:50 +08:00
initialdir=RECENT_DIRECTORY_TARGET,
filetypes=[img_ft, vid_ft],
)
2023-09-24 21:36:57 +08:00
if is_image(target_path):
modules.globals.target_path = target_path
RECENT_DIRECTORY_TARGET = os.path.dirname(modules.globals.target_path)
image = render_image_preview(modules.globals.target_path, (200, 200))
target_label.configure(image=image)
elif is_video(target_path):
modules.globals.target_path = target_path
RECENT_DIRECTORY_TARGET = os.path.dirname(modules.globals.target_path)
video_frame = render_video_preview(target_path, (200, 200))
target_label.configure(image=video_frame)
else:
modules.globals.target_path = None
target_label.configure(image=None)
def select_output_path(start: Callable[[], None]) -> None:
global RECENT_DIRECTORY_OUTPUT, img_ft, vid_ft
2023-09-24 21:36:57 +08:00
if is_image(modules.globals.target_path):
2024-09-25 20:06:50 +08:00
output_path = ctk.filedialog.asksaveasfilename(
title=_("save image output file"),
2024-09-25 20:06:50 +08:00
filetypes=[img_ft],
defaultextension=".png",
initialfile="output.png",
initialdir=RECENT_DIRECTORY_OUTPUT,
)
2023-09-24 21:36:57 +08:00
elif is_video(modules.globals.target_path):
2024-09-25 20:06:50 +08:00
output_path = ctk.filedialog.asksaveasfilename(
title=_("save video output file"),
2024-09-25 20:06:50 +08:00
filetypes=[vid_ft],
defaultextension=".mp4",
initialfile="output.mp4",
initialdir=RECENT_DIRECTORY_OUTPUT,
)
2023-09-24 21:36:57 +08:00
else:
output_path = None
if output_path:
modules.globals.output_path = output_path
RECENT_DIRECTORY_OUTPUT = os.path.dirname(modules.globals.output_path)
start()
def check_and_ignore_nsfw(target, destroy: Callable = None) -> bool:
2024-09-25 20:06:50 +08:00
"""Check if the target is NSFW.
TODO: Consider to make blur the target.
2024-09-25 20:06:50 +08:00
"""
from numpy import ndarray
from modules.predicter import predict_image, predict_video, predict_frame
2024-09-25 20:06:50 +08:00
if type(target) is str: # image/video file path
check_nsfw = predict_image if has_image_extension(target) else predict_video
2024-09-25 20:06:50 +08:00
elif type(target) is ndarray: # frame object
check_nsfw = predict_frame
if check_nsfw and check_nsfw(target):
2024-09-25 20:06:50 +08:00
if destroy:
destroy(
to_quit=False
) # Do not need to destroy the window frame if the target is NSFW
update_status("Processing ignored!")
return True
2024-09-25 20:06:50 +08:00
else:
return False
def fit_image_to_size(image, width: int, height: int):
if width is None or height is None or width <= 0 or height <= 0:
2024-09-25 20:06:50 +08:00
return image
h, w, _ = image.shape
ratio_h = 0.0
ratio_w = 0.0
ratio_w = width / w
ratio_h = height / h
# Use the smaller ratio to ensure the image fits within the given dimensions
ratio = min(ratio_w, ratio_h)
# Compute new dimensions, ensuring they're at least 1 pixel
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)
2023-09-24 21:36:57 +08:00
def render_image_preview(image_path: str, size: Tuple[int, int]) -> ctk.CTkImage:
image = Image.open(image_path)
if size:
image = ImageOps.fit(image, size, Image.LANCZOS)
return ctk.CTkImage(image, size=image.size)
2024-09-25 20:06:50 +08:00
def render_video_preview(
2025-01-06 23:31:33 +08:00
video_path: str, size: Tuple[int, int], frame_number: int = 0
2024-09-25 20:06:50 +08:00
) -> ctk.CTkImage:
2023-09-24 21:36:57 +08:00
capture = cv2.VideoCapture(video_path)
if frame_number:
capture.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
has_frame, frame = capture.read()
if has_frame:
image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
if size:
image = ImageOps.fit(image, size, Image.LANCZOS)
return ctk.CTkImage(image, size=image.size)
capture.release()
cv2.destroyAllWindows()
2023-09-24 21:36:57 +08:00
def toggle_preview() -> None:
2024-09-25 20:06:50 +08:00
if PREVIEW.state() == "normal":
2023-09-24 21:36:57 +08:00
PREVIEW.withdraw()
elif modules.globals.source_path and modules.globals.target_path:
init_preview()
update_preview()
def init_preview() -> None:
if is_image(modules.globals.target_path):
preview_slider.pack_forget()
if is_video(modules.globals.target_path):
2023-09-24 21:36:57 +08:00
video_frame_total = get_video_frame_total(modules.globals.target_path)
preview_slider.configure(to=video_frame_total)
2024-09-25 20:06:50 +08:00
preview_slider.pack(fill="x")
2023-09-24 21:36:57 +08:00
preview_slider.set(0)
def update_preview(frame_number: int = 0) -> None:
if modules.globals.source_path and modules.globals.target_path:
2024-09-25 20:06:50 +08:00
update_status("Processing...")
2024-10-04 22:16:45 +08:00
temp_frame = get_video_frame(modules.globals.target_path, frame_number)
if modules.globals.nsfw_filter and check_and_ignore_nsfw(temp_frame):
return
2024-09-25 20:06:50 +08:00
for frame_processor in get_frame_processors_modules(
2025-01-06 23:31:33 +08:00
modules.globals.frame_processors
2024-09-25 20:06:50 +08:00
):
2023-09-24 21:36:57 +08:00
temp_frame = frame_processor.process_frame(
2024-09-25 20:06:50 +08:00
get_one_face(cv2.imread(modules.globals.source_path)), temp_frame
2023-09-24 21:36:57 +08:00
)
image = Image.fromarray(cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB))
2024-09-25 20:06:50 +08:00
image = ImageOps.contain(
image, (PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.LANCZOS
)
2023-09-24 21:36:57 +08:00
image = ctk.CTkImage(image, size=image.size)
preview_label.configure(image=image)
2024-09-25 20:06:50 +08:00
update_status("Processing succeed!")
PREVIEW.deiconify()
2023-09-24 21:36:57 +08:00
2024-09-25 20:06:50 +08:00
2024-10-05 17:30:00 +08:00
def webcam_preview(root: ctk.CTk, camera_index: int):
2025-01-06 23:31:33 +08:00
global POPUP_LIVE
if POPUP_LIVE and POPUP_LIVE.winfo_exists():
update_status("Source x Target Mapper is already open.")
POPUP_LIVE.focus()
return
if not modules.globals.map_faces:
if modules.globals.source_path is None:
update_status("Please select a source image first")
return
2024-10-05 17:30:00 +08:00
create_webcam_preview(camera_index)
else:
modules.globals.source_target_map = []
2024-10-05 17:30:00 +08:00
create_source_target_popup_for_webcam(
root, modules.globals.source_target_map, camera_index
2024-10-05 17:30:00 +08:00
)
2024-09-25 20:06:50 +08:00
2025-01-06 23:31:33 +08:00
def get_available_cameras():
"""Returns a list of available camera names and indices."""
if platform.system() == "Windows":
try:
graph = FilterGraph()
devices = graph.get_input_devices()
# Create list of indices and names
camera_indices = list(range(len(devices)))
camera_names = devices
# If no cameras found through DirectShow, try OpenCV fallback
if not camera_names:
# Try to open camera with index -1 and 0
test_indices = [-1, 0]
working_cameras = []
for idx in test_indices:
cap = cv2.VideoCapture(idx)
if cap.isOpened():
working_cameras.append(f"Camera {idx}")
cap.release()
if working_cameras:
return test_indices[: len(working_cameras)], working_cameras
# If still no cameras found, return empty lists
if not camera_names:
return [], ["No cameras found"]
return camera_indices, camera_names
except Exception as e:
print(f"Error detecting cameras: {str(e)}")
return [], ["No cameras found"]
else:
# Unix-like systems (Linux/Mac) camera detection
camera_indices = []
camera_names = []
if platform.system() == "Darwin": # macOS specific handling
# Try to open the default FaceTime camera first
cap = cv2.VideoCapture(0)
if cap.isOpened():
camera_indices.append(0)
camera_names.append("FaceTime Camera")
cap.release()
2025-01-06 23:31:33 +08:00
# On macOS, additional cameras typically use indices 1 and 2
for i in [1, 2]:
cap = cv2.VideoCapture(i)
if cap.isOpened():
camera_indices.append(i)
camera_names.append(f"Camera {i}")
cap.release()
else:
# Linux camera detection - test first 10 indices
for i in range(10):
cap = cv2.VideoCapture(i)
if cap.isOpened():
camera_indices.append(i)
camera_names.append(f"Camera {i}")
cap.release()
if not camera_names:
return [], ["No cameras found"]
2024-12-13 22:19:11 +08:00
return camera_indices, camera_names
2024-12-13 22:19:11 +08:00
2024-10-05 17:30:00 +08:00
def create_webcam_preview(camera_index: int):
2024-10-02 16:50:56 +08:00
global preview_label, PREVIEW
2023-09-24 21:36:57 +08:00
cap = VideoCapturer(camera_index)
if not cap.start(PREVIEW_DEFAULT_WIDTH, PREVIEW_DEFAULT_HEIGHT, 60):
update_status("Failed to start camera")
return
2024-10-04 22:16:45 +08:00
preview_label.configure(width=PREVIEW_DEFAULT_WIDTH, height=PREVIEW_DEFAULT_HEIGHT)
PREVIEW.deiconify()
2024-10-02 16:50:56 +08:00
frame_processors = get_frame_processors_modules(modules.globals.frame_processors)
# --- Source Image Loading and Validation (Moved before the loop) ---
source_face_obj_for_cam = None
source_frame_full_for_cam = None
source_frame_full_for_cam_map_faces = None
if not modules.globals.map_faces:
if not modules.globals.source_path:
update_status("Error: No source image selected for webcam mode.")
cap.release()
PREVIEW.withdraw()
while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
ROOT.update_idletasks()
ROOT.update()
time.sleep(0.05)
return
if not os.path.exists(modules.globals.source_path):
update_status(f"Error: Source image not found at {modules.globals.source_path}")
cap.release()
PREVIEW.withdraw()
while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
ROOT.update_idletasks()
ROOT.update()
time.sleep(0.05)
return
source_frame_full_for_cam = cv2.imread(modules.globals.source_path)
if source_frame_full_for_cam is None:
update_status(f"Error: Could not read source image at {modules.globals.source_path}")
cap.release()
PREVIEW.withdraw()
while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
ROOT.update_idletasks()
ROOT.update()
time.sleep(0.05)
return
source_face_obj_for_cam = get_one_face(source_frame_full_for_cam)
if source_face_obj_for_cam is None:
update_status(f"Error: No face detected in source image {modules.globals.source_path}")
# This error is less critical for stopping immediately, but we'll make it persistent too.
# The loop below will run, but processing for frames will effectively be skipped.
# For consistency in error handling, make it persistent.
cap.release()
PREVIEW.withdraw()
while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
ROOT.update_idletasks()
ROOT.update()
time.sleep(0.05)
return
else: # modules.globals.map_faces is True
if not modules.globals.source_path:
update_status("Error: No global source image selected (for hair/background in map_faces mode).")
cap.release()
PREVIEW.withdraw()
while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
ROOT.update_idletasks()
ROOT.update()
time.sleep(0.05)
return
if not os.path.exists(modules.globals.source_path):
update_status(f"Error: Source image (for hair/background) not found at {modules.globals.source_path}")
cap.release()
PREVIEW.withdraw()
while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
ROOT.update_idletasks()
ROOT.update()
time.sleep(0.05)
return
source_frame_full_for_cam_map_faces = cv2.imread(modules.globals.source_path)
if source_frame_full_for_cam_map_faces is None:
update_status(f"Error: Could not read source image (for hair/background) at {modules.globals.source_path}")
cap.release()
PREVIEW.withdraw()
while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
ROOT.update_idletasks()
ROOT.update()
time.sleep(0.05)
return
if not modules.globals.source_target_map and not modules.globals.simple_map:
update_status("Warning: No face map defined for map_faces mode. Swapper may not work as expected.")
# This is a warning, not a fatal error for the preview window itself. Processing will continue.
# No persistent loop here, as it's a warning about functionality, not a critical load error.
# --- End Source Image Loading ---
2024-10-05 20:09:41 +08:00
prev_time = time.time()
fps_update_interval = 0.5
frame_count = 0
2024-10-05 20:09:41 +08:00
fps = 0
while True:
ret, frame = cap.read()
if not ret:
break
2024-10-05 20:09:41 +08:00
temp_frame = frame.copy()
if modules.globals.live_mirror:
temp_frame = cv2.flip(temp_frame, 1)
if modules.globals.live_resizable:
temp_frame = fit_image_to_size(
temp_frame, PREVIEW.winfo_width(), PREVIEW.winfo_height()
)
else:
temp_frame = fit_image_to_size(
temp_frame, PREVIEW.winfo_width(), PREVIEW.winfo_height()
)
if not modules.globals.map_faces:
# Case 1: map_faces is False - source_face_obj_for_cam and source_frame_full_for_cam are pre-loaded
if source_face_obj_for_cam is not None and source_frame_full_for_cam is not None: # Check if valid after pre-loading
for frame_processor in frame_processors:
if frame_processor.NAME == "DLC.FACE-ENHANCER":
if modules.globals.fp_ui["face_enhancer"]:
temp_frame = frame_processor.process_frame(None, temp_frame)
else:
feat: Implement hair swapping and enhance realism This commit introduces the capability to swap hair along with the face from a source image to a target image/video or live webcam feed. Key changes include: 1. **Hair Segmentation:** - Integrated the `isjackwild/segformer-b0-finetuned-segments-skin-hair-clothing` model from Hugging Face using the `transformers` library. - Added `modules/hair_segmenter.py` with a `segment_hair` function to produce a binary hair mask from an image. - Updated `requirements.txt` with `transformers`. 2. **Combined Face-Hair Mask:** - Implemented `create_face_and_hair_mask` in `modules/processors/frame/face_swapper.py` to generate a unified mask for both face (from landmarks) and segmented hair from the source image. 3. **Enhanced Swapping Logic:** - Modified `swap_face` and related processing functions (`process_frame`, `process_frame_v2`, `process_frames`, `process_image`) to utilize the full source image (`source_frame_full`). - The `swap_face` function now performs the standard face swap and then: - Segments hair from the `source_frame_full`. - Warps the hair and its mask to the target face's position using an affine transformation estimated from facial landmarks. - Applies color correction (`apply_color_transfer`) to the warped hair. - Blends the hair onto the target frame, preferably using `cv2.seamlessClone` for improved realism. - Existing mouth mask logic is preserved and applied to the final composited frame. 4. **Webcam Integration:** - Updated the webcam processing loop in `modules/ui.py` (`create_webcam_preview`) to correctly load and pass the `source_frame_full` to the frame processors. - This enables hair swapping in live webcam mode. - Added error handling for source image loading in webcam mode. This set of changes addresses your request for more realistic face swaps that include hair. Further testing and refinement of blending parameters may be beneficial for optimal results across all scenarios.
2025-05-22 02:47:31 +08:00
temp_frame = frame_processor.process_frame(source_face_obj_for_cam, source_frame_full_for_cam, temp_frame)
# If source image was invalid (e.g. no face), source_face_obj_for_cam might be None.
# In this case, the frame processors that need it will be skipped, effectively just showing the raw webcam frame.
# The error message is already persistent due to the pre-loop check.
else:
# Case 2: map_faces is True - source_frame_full_for_cam_map_faces is pre-loaded
if source_frame_full_for_cam_map_faces is not None: # Check if valid after pre-loading
modules.globals.target_path = None # Standard for live mode
for frame_processor in frame_processors:
if frame_processor.NAME == "DLC.FACE-ENHANCER":
if modules.globals.fp_ui["face_enhancer"]:
# Corrected: face_enhancer.process_frame_v2 is expected to take only temp_frame
temp_frame = frame_processor.process_frame_v2(temp_frame)
else:
# This is for other processors when map_faces is True
feat: Implement hair swapping and enhance realism This commit introduces the capability to swap hair along with the face from a source image to a target image/video or live webcam feed. Key changes include: 1. **Hair Segmentation:** - Integrated the `isjackwild/segformer-b0-finetuned-segments-skin-hair-clothing` model from Hugging Face using the `transformers` library. - Added `modules/hair_segmenter.py` with a `segment_hair` function to produce a binary hair mask from an image. - Updated `requirements.txt` with `transformers`. 2. **Combined Face-Hair Mask:** - Implemented `create_face_and_hair_mask` in `modules/processors/frame/face_swapper.py` to generate a unified mask for both face (from landmarks) and segmented hair from the source image. 3. **Enhanced Swapping Logic:** - Modified `swap_face` and related processing functions (`process_frame`, `process_frame_v2`, `process_frames`, `process_image`) to utilize the full source image (`source_frame_full`). - The `swap_face` function now performs the standard face swap and then: - Segments hair from the `source_frame_full`. - Warps the hair and its mask to the target face's position using an affine transformation estimated from facial landmarks. - Applies color correction (`apply_color_transfer`) to the warped hair. - Blends the hair onto the target frame, preferably using `cv2.seamlessClone` for improved realism. - Existing mouth mask logic is preserved and applied to the final composited frame. 4. **Webcam Integration:** - Updated the webcam processing loop in `modules/ui.py` (`create_webcam_preview`) to correctly load and pass the `source_frame_full` to the frame processors. - This enables hair swapping in live webcam mode. - Added error handling for source image loading in webcam mode. This set of changes addresses your request for more realistic face swaps that include hair. Further testing and refinement of blending parameters may be beneficial for optimal results across all scenarios.
2025-05-22 02:47:31 +08:00
temp_frame = frame_processor.process_frame_v2(source_frame_full_for_cam_map_faces, temp_frame)
# If source_frame_full_for_cam_map_faces was invalid, error is persistent from pre-loop check.
# Calculate and display FPS
current_time = time.time()
frame_count += 1
if current_time - prev_time >= fps_update_interval:
fps = frame_count / (current_time - prev_time)
frame_count = 0
prev_time = current_time
if modules.globals.show_fps:
cv2.putText(
temp_frame,
f"FPS: {fps:.1f}",
(10, 30),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(0, 255, 0),
2,
)
image = cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB)
image = Image.fromarray(image)
image = ImageOps.contain(
image, (temp_frame.shape[1], temp_frame.shape[0]), Image.LANCZOS
)
image = ctk.CTkImage(image, size=image.size)
preview_label.configure(image=image)
ROOT.update()
if PREVIEW.state() == "withdrawn":
break
cap.release()
PREVIEW.withdraw()
2024-10-02 15:53:39 +08:00
2024-10-05 17:30:00 +08:00
def create_source_target_popup_for_webcam(
2025-01-06 23:31:33 +08:00
root: ctk.CTk, map: list, camera_index: int
2024-10-05 17:30:00 +08:00
) -> None:
global POPUP_LIVE, popup_status_label_live
POPUP_LIVE = ctk.CTkToplevel(root)
POPUP_LIVE.title(_("Source x Target Mapper"))
POPUP_LIVE.geometry(f"{POPUP_LIVE_WIDTH}x{POPUP_LIVE_HEIGHT}")
POPUP_LIVE.focus()
def on_submit_click():
if has_valid_map():
simplify_maps()
2025-01-06 23:31:33 +08:00
update_pop_live_status("Mappings successfully submitted!")
create_webcam_preview(camera_index) # Open the preview window
else:
2024-10-05 17:30:00 +08:00
update_pop_live_status("At least 1 source with target is required!")
def on_add_click():
add_blank_map()
refresh_data(map)
update_pop_live_status("Please provide mapping!")
2025-01-06 23:31:33 +08:00
def on_clear_click():
clear_source_target_images(map)
refresh_data(map)
update_pop_live_status("All mappings cleared!")
2024-09-25 20:06:50 +08:00
popup_status_label_live = ctk.CTkLabel(POPUP_LIVE, text=None, justify="center")
popup_status_label_live.grid(row=1, column=0, pady=15)
add_button = ctk.CTkButton(POPUP_LIVE, text=_("Add"), command=lambda: on_add_click())
2025-01-06 23:31:33 +08:00
add_button.place(relx=0.1, rely=0.92, relwidth=0.2, relheight=0.05)
clear_button = ctk.CTkButton(POPUP_LIVE, text=_("Clear"), command=lambda: on_clear_click())
2025-01-06 23:31:33 +08:00
clear_button.place(relx=0.4, rely=0.92, relwidth=0.2, relheight=0.05)
2024-09-25 20:06:50 +08:00
close_button = ctk.CTkButton(
POPUP_LIVE, text=_("Submit"), command=lambda: on_submit_click()
2024-09-25 20:06:50 +08:00
)
2025-01-06 23:31:33 +08:00
close_button.place(relx=0.7, rely=0.92, relwidth=0.2, relheight=0.05)
def clear_source_target_images(map: list):
global source_label_dict_live, target_label_dict_live
for item in map:
if "source" in item:
del item["source"]
if "target" in item:
del item["target"]
for button_num in list(source_label_dict_live.keys()):
source_label_dict_live[button_num].destroy()
del source_label_dict_live[button_num]
for button_num in list(target_label_dict_live.keys()):
target_label_dict_live[button_num].destroy()
del target_label_dict_live[button_num]
def refresh_data(map: list):
global POPUP_LIVE
2024-09-25 20:06:50 +08:00
scrollable_frame = ctk.CTkScrollableFrame(
POPUP_LIVE, width=POPUP_LIVE_SCROLL_WIDTH, height=POPUP_LIVE_SCROLL_HEIGHT
)
scrollable_frame.grid(row=0, column=0, padx=0, pady=0, sticky="nsew")
def on_sbutton_click(map, button_num):
map = update_webcam_source(scrollable_frame, map, button_num)
def on_tbutton_click(map, button_num):
map = update_webcam_target(scrollable_frame, map, button_num)
for item in map:
2024-09-25 20:06:50 +08:00
id = item["id"]
2024-10-04 22:16:45 +08:00
button = ctk.CTkButton(
scrollable_frame,
text=_("Select source image"),
2024-09-25 20:06:50 +08:00
command=lambda id=id: on_sbutton_click(map, id),
width=DEFAULT_BUTTON_WIDTH,
height=DEFAULT_BUTTON_HEIGHT,
)
2024-10-04 22:16:45 +08:00
button.grid(row=id, column=0, padx=30, pady=10)
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
x_label = ctk.CTkLabel(
scrollable_frame,
text=f"X",
2024-09-25 20:06:50 +08:00
width=MAPPER_PREVIEW_MAX_WIDTH,
height=MAPPER_PREVIEW_MAX_HEIGHT,
)
2024-10-04 22:16:45 +08:00
x_label.grid(row=id, column=2, padx=10, pady=10)
button = ctk.CTkButton(
scrollable_frame,
text=_("Select target image"),
2024-10-04 22:16:45 +08:00
command=lambda id=id: on_tbutton_click(map, id),
width=DEFAULT_BUTTON_WIDTH,
height=DEFAULT_BUTTON_HEIGHT,
)
button.grid(row=id, column=3, padx=20, pady=10)
2024-09-19 15:54:57 +08:00
if "source" in item:
image = Image.fromarray(
cv2.cvtColor(item["source"]["cv2"], cv2.COLOR_BGR2RGB)
2024-10-04 22:16:45 +08:00
)
image = image.resize(
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
)
tk_image = ctk.CTkImage(image, size=image.size)
source_image = ctk.CTkLabel(
scrollable_frame,
text=f"S-{id}",
width=MAPPER_PREVIEW_MAX_WIDTH,
height=MAPPER_PREVIEW_MAX_HEIGHT,
)
source_image.grid(row=id, column=1, padx=10, pady=10)
source_image.configure(image=tk_image)
if "target" in item:
image = Image.fromarray(
cv2.cvtColor(item["target"]["cv2"], cv2.COLOR_BGR2RGB)
2024-10-04 22:16:45 +08:00
)
image = image.resize(
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
)
tk_image = ctk.CTkImage(image, size=image.size)
target_image = ctk.CTkLabel(
scrollable_frame,
text=f"T-{id}",
width=MAPPER_PREVIEW_MAX_WIDTH,
height=MAPPER_PREVIEW_MAX_HEIGHT,
)
target_image.grid(row=id, column=4, padx=20, pady=10)
target_image.configure(image=tk_image)
2024-09-25 20:06:50 +08:00
def update_webcam_source(
2025-01-06 23:31:33 +08:00
scrollable_frame: ctk.CTkScrollableFrame, map: list, button_num: int
2024-09-25 20:06:50 +08:00
) -> list:
global source_label_dict_live
2024-09-25 20:06:50 +08:00
source_path = ctk.filedialog.askopenfilename(
2025-05-07 19:23:31 +08:00
title=_("select a source image"),
2024-09-25 20:06:50 +08:00
initialdir=RECENT_DIRECTORY_SOURCE,
filetypes=[img_ft],
)
if "source" in map[button_num]:
map[button_num].pop("source")
2024-10-04 22:16:45 +08:00
source_label_dict_live[button_num].destroy()
del source_label_dict_live[button_num]
2024-09-25 20:06:50 +08:00
if source_path == "":
return map
else:
cv2_img = cv2.imread(source_path)
face = get_one_face(cv2_img)
if face:
2024-09-25 20:06:50 +08:00
x_min, y_min, x_max, y_max = face["bbox"]
map[button_num]["source"] = {
2025-01-06 23:31:33 +08:00
"cv2": cv2_img[int(y_min): int(y_max), int(x_min): int(x_max)],
2024-09-25 20:06:50 +08:00
"face": face,
}
image = Image.fromarray(
cv2.cvtColor(map[button_num]["source"]["cv2"], cv2.COLOR_BGR2RGB)
)
image = image.resize(
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
)
tk_image = ctk.CTkImage(image, size=image.size)
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
source_image = ctk.CTkLabel(
scrollable_frame,
text=f"S-{button_num}",
width=MAPPER_PREVIEW_MAX_WIDTH,
height=MAPPER_PREVIEW_MAX_HEIGHT,
)
source_image.grid(row=button_num, column=1, padx=10, pady=10)
source_image.configure(image=tk_image)
source_label_dict_live[button_num] = source_image
else:
update_pop_live_status("Face could not be detected in last upload!")
return map
2024-09-25 20:06:50 +08:00
def update_webcam_target(
2025-01-06 23:31:33 +08:00
scrollable_frame: ctk.CTkScrollableFrame, map: list, button_num: int
2024-09-25 20:06:50 +08:00
) -> list:
global target_label_dict_live
2024-10-04 22:16:45 +08:00
target_path = ctk.filedialog.askopenfilename(
2025-05-07 19:23:31 +08:00
title=_("select a target image"),
2024-10-04 22:16:45 +08:00
initialdir=RECENT_DIRECTORY_SOURCE,
filetypes=[img_ft],
)
if "target" in map[button_num]:
map[button_num].pop("target")
2024-10-04 22:16:45 +08:00
target_label_dict_live[button_num].destroy()
del target_label_dict_live[button_num]
2024-09-25 20:06:50 +08:00
if target_path == "":
return map
else:
cv2_img = cv2.imread(target_path)
face = get_one_face(cv2_img)
if face:
2024-09-25 20:06:50 +08:00
x_min, y_min, x_max, y_max = face["bbox"]
map[button_num]["target"] = {
2025-01-06 23:31:33 +08:00
"cv2": cv2_img[int(y_min): int(y_max), int(x_min): int(x_max)],
2024-09-25 20:06:50 +08:00
"face": face,
}
image = Image.fromarray(
cv2.cvtColor(map[button_num]["target"]["cv2"], cv2.COLOR_BGR2RGB)
)
image = image.resize(
(MAPPER_PREVIEW_MAX_WIDTH, MAPPER_PREVIEW_MAX_HEIGHT), Image.LANCZOS
)
tk_image = ctk.CTkImage(image, size=image.size)
2024-09-25 20:06:50 +08:00
2024-10-04 22:16:45 +08:00
target_image = ctk.CTkLabel(
scrollable_frame,
text=f"T-{button_num}",
width=MAPPER_PREVIEW_MAX_WIDTH,
height=MAPPER_PREVIEW_MAX_HEIGHT,
)
target_image.grid(row=button_num, column=4, padx=20, pady=10)
target_image.configure(image=tk_image)
target_label_dict_live[button_num] = target_image
else:
update_pop_live_status("Face could not be detected in last upload!")
return map