diff --git a/README.md b/README.md index 47be2e5..4707982 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,8 @@ options: --many-faces process every face --video-encoder {libx264,libx265,libvpx-vp9} adjust output video encoder --video-quality [0-51] adjust output video quality + --live-mirror the live camera display as you see it in the front-facing camera frame + --live-resizable the live camera frame is resizable --max-memory MAX_MEMORY maximum amount of RAM in GB --execution-provider {cpu} [{cpu} ...] available execution provider (choices: cpu, ...) --execution-threads EXECUTION_THREADS number of execution threads diff --git a/modules/core.py b/modules/core.py index 1cec474..b4e4a31 100644 --- a/modules/core.py +++ b/modules/core.py @@ -60,6 +60,10 @@ def parse_args() -> None: choices=['libx264', 'libx265', 'libvpx-vp9']) program.add_argument('--video-quality', help='Adjust output video quality', dest='video_quality', type=int, default=18, choices=range(52), metavar='[0-51]') + program.add_argument('--live-mirror', help='The live camera display as you see it in the front-facing camera frame', + dest='live_mirror', action='store_true', default=False) + program.add_argument('--live-resizable', help='The live camera frame is resizable', + dest='live_resizable', action='store_true', default=False) program.add_argument('--max-memory', help='Maximum amount of RAM in GB', dest='max_memory', type=int, default=suggest_max_memory()) program.add_argument('--execution-provider', help='Execution provider', dest='execution_provider', default=['cpu'], @@ -89,6 +93,8 @@ def parse_args() -> None: modules.globals.many_faces = args.many_faces modules.globals.video_encoder = args.video_encoder modules.globals.video_quality = args.video_quality + modules.globals.live_mirror = args.live_mirror + modules.globals.live_resizable = args.live_resizable modules.globals.max_memory = args.max_memory modules.globals.execution_providers = decode_execution_providers(args.execution_provider) modules.globals.execution_threads = args.execution_threads diff --git a/modules/globals.py b/modules/globals.py index c392a80..2bc15fd 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -19,6 +19,8 @@ keep_frames = None many_faces = None video_encoder = None video_quality = None +live_mirror = None +live_resizable = None max_memory = None execution_providers: List[str] = [] execution_threads = None diff --git a/modules/ui.py b/modules/ui.py index 5ad2a65..3c5fbc3 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -28,7 +28,9 @@ ROOT_WIDTH = 600 PREVIEW = None PREVIEW_MAX_HEIGHT = 700 -PREVIEW_MAX_WIDTH = 1200 +PREVIEW_MAX_WIDTH = 1200 +PREVIEW_DEFAULT_WIDTH = 960 +PREVIEW_DEFAULT_HEIGHT = 540 RECENT_DIRECTORY_SOURCE = None RECENT_DIRECTORY_TARGET = None @@ -42,6 +44,7 @@ status_label = None img_ft, vid_ft = modules.globals.file_types +camera = None def check_camera_permissions(): """Check and request camera access permission on macOS.""" @@ -184,7 +187,7 @@ def create_preview(parent: ctk.CTk) -> ctk.CTkToplevel: preview.withdraw() preview.title('Preview') preview.protocol('WM_DELETE_WINDOW', toggle_preview) - preview.resizable(width=False, height=False) + preview.resizable(width=True, height=True) preview_label = ctk.CTkLabel(preview, text=None) preview_label.pack(fill='both', expand=True) @@ -281,6 +284,11 @@ def toggle_preview() -> None: init_preview() update_preview() PREVIEW.deiconify() + global camera + if PREVIEW.state() == 'withdrawn': + if camera and camera.isOpened(): + camera.release() + camera = None def init_preview() -> None: @@ -320,11 +328,17 @@ def webcam_preview_loop(cap: cv2.VideoCapture, source_image: Any, frame_processo temp_frame = frame.copy() + if modules.globals.live_mirror: + temp_frame = cv2.flip(temp_frame, 1) # horizontal flipping + + if modules.globals.live_resizable: + temp_frame = fit_image_to_size(temp_frame, PREVIEW.winfo_width(), PREVIEW.winfo_height()) + for frame_processor in frame_processors: temp_frame = frame_processor.process_frame(source_image, temp_frame) image = Image.fromarray(cv2.cvtColor(temp_frame, cv2.COLOR_BGR2RGB)) - image = ImageOps.contain(image, (PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.LANCZOS) + 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) if virtual_cam: @@ -337,6 +351,20 @@ def webcam_preview_loop(cap: cv2.VideoCapture, source_image: Any, frame_processo return True +def fit_image_to_size(image, width: int, height: int): + if width is None and height is None: + return image + h, w, _ = image.shape + ratio_h = 0.0 + ratio_w = 0.0 + if width > height: + ratio_h = height / h + else: + ratio_w = width / w + ratio = max(ratio_w, ratio_h) + new_size = (int(ratio * w), int(ratio * h)) + return cv2.resize(image, dsize=new_size) + def webcam_preview(camera_name: str, virtual_cam_output: bool): if modules.globals.source_path is None: return @@ -356,20 +384,21 @@ def webcam_preview(camera_name: str, virtual_cam_output: bool): # Use OpenCV's camera index for cross-platform compatibility camera_index = get_camera_index_by_name(camera_name) - cap = cv2.VideoCapture(camera_index) + global camera + camera = cv2.VideoCapture(camera_index) - if not cap.isOpened(): + if not camera.isOpened(): update_status(f"Error: Could not open camera {camera_name}") return - cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH) - cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT) - cap.set(cv2.CAP_PROP_FPS, FPS) + camera.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH) + camera.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT) + camera.set(cv2.CAP_PROP_FPS, FPS) PREVIEW_MAX_WIDTH = WIDTH PREVIEW_MAX_HEIGHT = HEIGHT - preview_label.configure(image=None) + preview_label.configure(width=PREVIEW_DEFAULT_WIDTH, height=PREVIEW_DEFAULT_HEIGHT) PREVIEW.deiconify() frame_processors = get_frame_processors_modules(modules.globals.frame_processors) @@ -387,7 +416,7 @@ def webcam_preview(camera_name: str, virtual_cam_output: bool): - cap.release() + if camera: camera.release() PREVIEW.withdraw() @@ -420,8 +449,19 @@ def get_available_cameras(): elif device.deviceType() == "AVCaptureDeviceTypeContinuityCamera": print(f"Skipping Continuity Camera: {device.localizedName()}") elif platform.system() == 'Windows' or platform.system() == 'Linux': - # Use OpenCV to detect camera indexes - devices = FilterGraph().get_input_devices() + try: + devices = FilterGraph().get_input_devices() + except Exception as e: + # Use OpenCV to detect camera indexes + index = 0 + devices = [] + while True: + cap = cv2.VideoCapture(index) + if not cap.isOpened(): + break + devices.append(f"Camera {index}") + cap.release() + index += 1 available_cameras = devices return available_cameras