Improve FPS, blending, maintainability, and UI/UX for Deep-Live-Cam; address all review and Sourcery feedback; ready for deployment
							parent
							
								
									348e6c424e
								
							
						
					
					
						commit
						ba008e72eb
					
				| 
						 | 
					@ -9,12 +9,14 @@ HAIR_SEGMENTER_PROCESSOR = None
 | 
				
			||||||
HAIR_SEGMENTER_MODEL = None
 | 
					HAIR_SEGMENTER_MODEL = None
 | 
				
			||||||
MODEL_NAME = "isjackwild/segformer-b0-finetuned-segments-skin-hair-clothing"
 | 
					MODEL_NAME = "isjackwild/segformer-b0-finetuned-segments-skin-hair-clothing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def segment_hair(image_np: np.ndarray) -> np.ndarray:
 | 
					def segment_hair(image_np: np.ndarray, device: str = "cpu", hair_label_index: int = None) -> np.ndarray:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Segments hair from an image.
 | 
					    Segments hair from an image.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Args:
 | 
					    Args:
 | 
				
			||||||
        image_np: NumPy array representing the image (BGR format from OpenCV).
 | 
					        image_np: NumPy array representing the image (BGR format from OpenCV).
 | 
				
			||||||
 | 
					        device: Device to run the model on ("cpu" or "cuda").
 | 
				
			||||||
 | 
					        hair_label_index: Optional; index of the hair label in the segmentation map. If not provided, will use model config or default to 2.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Returns:
 | 
					    Returns:
 | 
				
			||||||
        NumPy array representing the binary hair mask.
 | 
					        NumPy array representing the binary hair mask.
 | 
				
			||||||
| 
						 | 
					@ -26,48 +28,38 @@ def segment_hair(image_np: np.ndarray) -> np.ndarray:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            HAIR_SEGMENTER_PROCESSOR = SegformerImageProcessor.from_pretrained(MODEL_NAME)
 | 
					            HAIR_SEGMENTER_PROCESSOR = SegformerImageProcessor.from_pretrained(MODEL_NAME)
 | 
				
			||||||
            HAIR_SEGMENTER_MODEL = SegformerForSemanticSegmentation.from_pretrained(MODEL_NAME)
 | 
					            HAIR_SEGMENTER_MODEL = SegformerForSemanticSegmentation.from_pretrained(MODEL_NAME)
 | 
				
			||||||
            # Optional: Move model to GPU if available and if other models use GPU
 | 
					            HAIR_SEGMENTER_MODEL = HAIR_SEGMENTER_MODEL.to(device)
 | 
				
			||||||
            # if torch.cuda.is_available():
 | 
					            print(f"Hair segmentation model and processor loaded successfully. Model moved to device: {device}")
 | 
				
			||||||
            #     HAIR_SEGMENTER_MODEL = HAIR_SEGMENTER_MODEL.to('cuda')
 | 
					 | 
				
			||||||
            #     print("Hair segmentation model moved to GPU.")
 | 
					 | 
				
			||||||
            print("Hair segmentation model and processor loaded successfully.")
 | 
					 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            print(f"Failed to load hair segmentation model/processor: {e}")
 | 
					            print(f"Failed to load hair segmentation model/processor: {e}")
 | 
				
			||||||
            # Return an empty mask compatible with expected output shape (H, W)
 | 
					 | 
				
			||||||
            return np.zeros((image_np.shape[0], image_np.shape[1]), dtype=np.uint8)
 | 
					            return np.zeros((image_np.shape[0], image_np.shape[1]), dtype=np.uint8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Ensure processor and model are loaded before proceeding
 | 
					 | 
				
			||||||
    if HAIR_SEGMENTER_PROCESSOR is None or HAIR_SEGMENTER_MODEL is None:
 | 
					    if HAIR_SEGMENTER_PROCESSOR is None or HAIR_SEGMENTER_MODEL is None:
 | 
				
			||||||
        print("Error: Hair segmentation models are not available.")
 | 
					        print("Error: Hair segmentation models are not available.")
 | 
				
			||||||
        return np.zeros((image_np.shape[0], image_np.shape[1]), dtype=np.uint8)
 | 
					        return np.zeros((image_np.shape[0], image_np.shape[1]), dtype=np.uint8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Convert BGR (OpenCV) to RGB (PIL)
 | 
					 | 
				
			||||||
    image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB)
 | 
					    image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB)
 | 
				
			||||||
    image_pil = Image.fromarray(image_rgb)
 | 
					    image_pil = Image.fromarray(image_rgb)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    inputs = HAIR_SEGMENTER_PROCESSOR(images=image_pil, return_tensors="pt")
 | 
					    inputs = HAIR_SEGMENTER_PROCESSOR(images=image_pil, return_tensors="pt")
 | 
				
			||||||
 | 
					    if device == "cuda" and hasattr(HAIR_SEGMENTER_MODEL, "device") and HAIR_SEGMENTER_MODEL.device.type == "cuda":
 | 
				
			||||||
 | 
					        inputs = {k: v.to("cuda") for k, v in inputs.items()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Optional: Move inputs to GPU if model is on GPU
 | 
					    with torch.no_grad():
 | 
				
			||||||
    # if HAIR_SEGMENTER_MODEL.device.type == 'cuda':
 | 
					 | 
				
			||||||
    #     inputs = inputs.to(HAIR_SEGMENTER_MODEL.device)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    with torch.no_grad(): # Important for inference
 | 
					 | 
				
			||||||
        outputs = HAIR_SEGMENTER_MODEL(**inputs)
 | 
					        outputs = HAIR_SEGMENTER_MODEL(**inputs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    logits = outputs.logits  # Shape: batch_size, num_labels, height, width
 | 
					    logits = outputs.logits
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Upsample logits to original image size
 | 
					 | 
				
			||||||
    upsampled_logits = torch.nn.functional.interpolate(
 | 
					    upsampled_logits = torch.nn.functional.interpolate(
 | 
				
			||||||
        logits,
 | 
					        logits,
 | 
				
			||||||
        size=(image_np.shape[0], image_np.shape[1]), # H, W
 | 
					        size=(image_np.shape[0], image_np.shape[1]),
 | 
				
			||||||
        mode='bilinear',
 | 
					        mode='bilinear',
 | 
				
			||||||
        align_corners=False
 | 
					        align_corners=False
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					 | 
				
			||||||
    segmentation_map = upsampled_logits.argmax(dim=1).squeeze().cpu().numpy().astype(np.uint8)
 | 
					    segmentation_map = upsampled_logits.argmax(dim=1).squeeze().cpu().numpy().astype(np.uint8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Label 2 is for hair in this model
 | 
					    if hair_label_index is None:
 | 
				
			||||||
    return np.where(segmentation_map == 2, 255, 0).astype(np.uint8)
 | 
					        hair_label_index = getattr(HAIR_SEGMENTER_MODEL, "hair_label_index", 2)
 | 
				
			||||||
 | 
					    return np.where(segmentation_map == hair_label_index, 255, 0).astype(np.uint8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					if __name__ == '__main__':
 | 
				
			||||||
    # This is a conceptual test.
 | 
					    # This is a conceptual test.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -162,67 +162,44 @@ def swap_face(source_face_obj: Face, target_face: Face, source_frame_full: Frame
 | 
				
			||||||
    swapped_frame = face_swapper.get(temp_frame, target_face, source_face_obj, paste_back=True)
 | 
					    swapped_frame = face_swapper.get(temp_frame, target_face, source_face_obj, paste_back=True)
 | 
				
			||||||
    final_swapped_frame = swapped_frame
 | 
					    final_swapped_frame = swapped_frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if modules.globals.enable_hair_swapping:
 | 
					    def do_hair_blending():
 | 
				
			||||||
        if not (source_face_obj.kps is not None and \
 | 
					        if not (source_face_obj.kps is not None and target_face.kps is not None and source_face_obj.kps.shape[0] >= 3 and target_face.kps.shape[0] >= 3):
 | 
				
			||||||
                target_face.kps is not None and \
 | 
					 | 
				
			||||||
                source_face_obj.kps.shape[0] >= 3 and \
 | 
					 | 
				
			||||||
                target_face.kps.shape[0] >= 3):
 | 
					 | 
				
			||||||
            logging.warning(
 | 
					            logging.warning(
 | 
				
			||||||
                f"Skipping hair blending due to insufficient keypoints. "
 | 
					                f"Skipping hair blending due to insufficient keypoints. "
 | 
				
			||||||
                f"Source kps: {source_face_obj.kps.shape if source_face_obj.kps is not None else 'None'}, "
 | 
					                f"Source kps: {source_face_obj.kps.shape if source_face_obj.kps is not None else 'None'}, "
 | 
				
			||||||
                f"Target kps: {target_face.kps.shape if target_face.kps is not None else 'None'}."
 | 
					                f"Target kps: {target_face.kps.shape if target_face.kps is not None else 'None'}."
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        else:
 | 
					            return swapped_frame
 | 
				
			||||||
        source_kps_float = source_face_obj.kps.astype(np.float32)
 | 
					        source_kps_float = source_face_obj.kps.astype(np.float32)
 | 
				
			||||||
        target_kps_float = target_face.kps.astype(np.float32)
 | 
					        target_kps_float = target_face.kps.astype(np.float32)
 | 
				
			||||||
        matrix, _ = cv2.estimateAffinePartial2D(source_kps_float, target_kps_float, method=cv2.LMEDS)
 | 
					        matrix, _ = cv2.estimateAffinePartial2D(source_kps_float, target_kps_float, method=cv2.LMEDS)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if matrix is None:
 | 
					        if matrix is None:
 | 
				
			||||||
            logging.warning("Failed to estimate affine transformation matrix for hair. Skipping hair blending.")
 | 
					            logging.warning("Failed to estimate affine transformation matrix for hair. Skipping hair blending.")
 | 
				
			||||||
            else:
 | 
					            return swapped_frame
 | 
				
			||||||
                dsize = (temp_frame.shape[1], temp_frame.shape[0]) # width, height
 | 
					        dsize = (temp_frame.shape[1], temp_frame.shape[0])
 | 
				
			||||||
                
 | 
					 | 
				
			||||||
        warped_material, warped_mask = _prepare_warped_source_material_and_mask(
 | 
					        warped_material, warped_mask = _prepare_warped_source_material_and_mask(
 | 
				
			||||||
            source_face_obj, source_frame_full, matrix, dsize
 | 
					            source_face_obj, source_frame_full, matrix, dsize
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if warped_material is not None and warped_mask is not None:
 | 
					        if warped_material is not None and warped_mask is not None:
 | 
				
			||||||
                    # Make a copy only now that we are sure we will modify it for hair.
 | 
					            out = swapped_frame.copy()
 | 
				
			||||||
                    final_swapped_frame = swapped_frame.copy() 
 | 
					            color_corrected_material = apply_color_transfer(warped_material, out)
 | 
				
			||||||
 | 
					            return _blend_material_onto_frame(out, color_corrected_material, warped_mask)
 | 
				
			||||||
 | 
					        return swapped_frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    color_corrected_material = apply_color_transfer(warped_material, final_swapped_frame) # Use final_swapped_frame for color context
 | 
					    def do_mouth_mask(frame):
 | 
				
			||||||
                    
 | 
					        out = frame.copy() if frame is swapped_frame else frame
 | 
				
			||||||
                    final_swapped_frame = _blend_material_onto_frame(
 | 
					 | 
				
			||||||
                        final_swapped_frame, 
 | 
					 | 
				
			||||||
                        color_corrected_material, 
 | 
					 | 
				
			||||||
                        warped_mask
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    # Mouth Mask Logic (operates on final_swapped_frame)
 | 
					 | 
				
			||||||
    if modules.globals.mouth_mask:
 | 
					 | 
				
			||||||
        # If final_swapped_frame wasn't copied for hair, it needs to be copied now before mouth mask modification.
 | 
					 | 
				
			||||||
        if final_swapped_frame is swapped_frame: # Check if it's still the same object
 | 
					 | 
				
			||||||
            final_swapped_frame = swapped_frame.copy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Create a mask for the target face
 | 
					 | 
				
			||||||
        face_mask = create_face_mask(target_face, temp_frame)
 | 
					        face_mask = create_face_mask(target_face, temp_frame)
 | 
				
			||||||
 | 
					        mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon = create_lower_mouth_mask(target_face, temp_frame)
 | 
				
			||||||
        # Create the mouth mask
 | 
					        out = apply_mouth_area(out, mouth_cutout, mouth_box, face_mask, lower_lip_polygon)
 | 
				
			||||||
        mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon = (
 | 
					 | 
				
			||||||
            create_lower_mouth_mask(target_face, temp_frame)
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Apply the mouth area
 | 
					 | 
				
			||||||
        # Apply to final_swapped_frame if hair blending happened, otherwise to swapped_frame
 | 
					 | 
				
			||||||
        final_swapped_frame = apply_mouth_area(
 | 
					 | 
				
			||||||
            final_swapped_frame, mouth_cutout, mouth_box, face_mask, lower_lip_polygon
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if modules.globals.show_mouth_mask_box:
 | 
					        if modules.globals.show_mouth_mask_box:
 | 
				
			||||||
            mouth_mask_data = (mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon)
 | 
					            mouth_mask_data = (mouth_mask, mouth_cutout, mouth_box, lower_lip_polygon)
 | 
				
			||||||
            final_swapped_frame = draw_mouth_mask_visualization(
 | 
					            out = draw_mouth_mask_visualization(out, target_face, mouth_mask_data)
 | 
				
			||||||
                final_swapped_frame, target_face, mouth_mask_data
 | 
					        return out
 | 
				
			||||||
            )
 | 
					
 | 
				
			||||||
 | 
					    if modules.globals.enable_hair_swapping:
 | 
				
			||||||
 | 
					        final_swapped_frame = do_hair_blending()
 | 
				
			||||||
 | 
					    if modules.globals.mouth_mask:
 | 
				
			||||||
 | 
					        final_swapped_frame = do_mouth_mask(final_swapped_frame)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if PROFILE_FACE_SWAP:
 | 
					    if PROFILE_FACE_SWAP:
 | 
				
			||||||
        elapsed = time.time() - start_time
 | 
					        elapsed = time.time() - start_time
 | 
				
			||||||
| 
						 | 
					@ -293,10 +270,14 @@ def _process_live_target_v2(source_frame_full: Frame, temp_frame: Frame) -> Fram
 | 
				
			||||||
        return temp_frame
 | 
					        return temp_frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if modules.globals.many_faces:
 | 
					    if modules.globals.many_faces:
 | 
				
			||||||
        source_face_obj = default_source_face()
 | 
					        if source_face_obj := default_source_face():
 | 
				
			||||||
        if source_face_obj:
 | 
					            swapped_faces = set()
 | 
				
			||||||
            for target_face in detected_faces:
 | 
					            for target_face in detected_faces:
 | 
				
			||||||
 | 
					                face_id = id(target_face)
 | 
				
			||||||
 | 
					                if face_id in swapped_faces:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
                temp_frame = swap_face(source_face_obj, target_face, source_frame_full, temp_frame)
 | 
					                temp_frame = swap_face(source_face_obj, target_face, source_frame_full, temp_frame)
 | 
				
			||||||
 | 
					                swapped_faces.add(face_id)
 | 
				
			||||||
    else: # not many_faces (apply simple_map logic)
 | 
					    else: # not many_faces (apply simple_map logic)
 | 
				
			||||||
        if not modules.globals.simple_map or \
 | 
					        if not modules.globals.simple_map or \
 | 
				
			||||||
           not modules.globals.simple_map.get("target_embeddings") or \
 | 
					           not modules.globals.simple_map.get("target_embeddings") or \
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -289,6 +289,7 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
 | 
				
			||||||
    show_fps_switch.place(relx=0.6, rely=0.75)
 | 
					    show_fps_switch.place(relx=0.6, rely=0.75)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Hair Swapping Switch (placed below "Show FPS" on the right column)
 | 
					    # Hair Swapping Switch (placed below "Show FPS" on the right column)
 | 
				
			||||||
 | 
					    segmentation_model_available = getattr(modules.globals, "segmentation_model_available", True)
 | 
				
			||||||
    hair_swapping_value = ctk.BooleanVar(value=modules.globals.enable_hair_swapping)
 | 
					    hair_swapping_value = ctk.BooleanVar(value=modules.globals.enable_hair_swapping)
 | 
				
			||||||
    hair_swapping_switch = ctk.CTkSwitch(
 | 
					    hair_swapping_switch = ctk.CTkSwitch(
 | 
				
			||||||
        root,
 | 
					        root,
 | 
				
			||||||
| 
						 | 
					@ -298,9 +299,10 @@ def create_root(start: Callable[[], None], destroy: Callable[[], None]) -> ctk.C
 | 
				
			||||||
        command=lambda: (
 | 
					        command=lambda: (
 | 
				
			||||||
            setattr(modules.globals, "enable_hair_swapping", hair_swapping_value.get()),
 | 
					            setattr(modules.globals, "enable_hair_swapping", hair_swapping_value.get()),
 | 
				
			||||||
            save_switch_states(),
 | 
					            save_switch_states(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        state="normal" if segmentation_model_available else "disabled"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    )
 | 
					    hair_swapping_switch.place(relx=0.6, rely=0.80)
 | 
				
			||||||
    hair_swapping_switch.place(relx=0.6, rely=0.80) # Adjusted rely from 0.75 to 0.80
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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(
 | 
				
			||||||
| 
						 | 
					@ -911,74 +913,82 @@ def create_webcam_preview(camera_index: int):
 | 
				
			||||||
            update_status("Error: No source image selected for webcam mode.")
 | 
					            update_status("Error: No source image selected for webcam mode.")
 | 
				
			||||||
            cap.release()
 | 
					            cap.release()
 | 
				
			||||||
            PREVIEW.withdraw()
 | 
					            PREVIEW.withdraw()
 | 
				
			||||||
            while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
					            def wait_for_withdraw():
 | 
				
			||||||
 | 
					                if PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
				
			||||||
                    ROOT.update_idletasks()
 | 
					                    ROOT.update_idletasks()
 | 
				
			||||||
                    ROOT.update()
 | 
					                    ROOT.update()
 | 
				
			||||||
                time.sleep(0.05)
 | 
					                    PREVIEW.after(50, wait_for_withdraw)
 | 
				
			||||||
 | 
					            wait_for_withdraw()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        if not os.path.exists(modules.globals.source_path):
 | 
					        if not os.path.exists(modules.globals.source_path):
 | 
				
			||||||
            update_status(f"Error: Source image not found at {modules.globals.source_path}")
 | 
					            update_status(f"Error: Source image not found at {modules.globals.source_path}")
 | 
				
			||||||
            cap.release()
 | 
					            cap.release()
 | 
				
			||||||
            PREVIEW.withdraw()
 | 
					            PREVIEW.withdraw()
 | 
				
			||||||
            while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
					            def wait_for_withdraw():
 | 
				
			||||||
 | 
					                if PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
				
			||||||
                    ROOT.update_idletasks()
 | 
					                    ROOT.update_idletasks()
 | 
				
			||||||
                    ROOT.update()
 | 
					                    ROOT.update()
 | 
				
			||||||
                time.sleep(0.05)
 | 
					                    PREVIEW.after(50, wait_for_withdraw)
 | 
				
			||||||
 | 
					            wait_for_withdraw()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        source_frame_full_for_cam = cv2.imread(modules.globals.source_path)
 | 
					        source_frame_full_for_cam = cv2.imread(modules.globals.source_path)
 | 
				
			||||||
        if source_frame_full_for_cam is None:
 | 
					        if source_frame_full_for_cam is None:
 | 
				
			||||||
            update_status(f"Error: Could not read source image at {modules.globals.source_path}")
 | 
					            update_status(f"Error: Could not read source image at {modules.globals.source_path}")
 | 
				
			||||||
            cap.release()
 | 
					            cap.release()
 | 
				
			||||||
            PREVIEW.withdraw()
 | 
					            PREVIEW.withdraw()
 | 
				
			||||||
            while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
					            def wait_for_withdraw():
 | 
				
			||||||
 | 
					                if PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
				
			||||||
                    ROOT.update_idletasks()
 | 
					                    ROOT.update_idletasks()
 | 
				
			||||||
                    ROOT.update()
 | 
					                    ROOT.update()
 | 
				
			||||||
                time.sleep(0.05)
 | 
					                    PREVIEW.after(50, wait_for_withdraw)
 | 
				
			||||||
 | 
					            wait_for_withdraw()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					 | 
				
			||||||
        source_face_obj_for_cam = get_one_face(source_frame_full_for_cam)
 | 
					        source_face_obj_for_cam = get_one_face(source_frame_full_for_cam)
 | 
				
			||||||
        if source_face_obj_for_cam is None:
 | 
					        if source_face_obj_for_cam is None:
 | 
				
			||||||
            update_status(f"Error: No face detected in source image {modules.globals.source_path}")
 | 
					            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()
 | 
					            cap.release()
 | 
				
			||||||
            PREVIEW.withdraw()
 | 
					            PREVIEW.withdraw()
 | 
				
			||||||
            while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
					            def wait_for_withdraw():
 | 
				
			||||||
 | 
					                if PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
				
			||||||
                    ROOT.update_idletasks()
 | 
					                    ROOT.update_idletasks()
 | 
				
			||||||
                    ROOT.update()
 | 
					                    ROOT.update()
 | 
				
			||||||
                time.sleep(0.05)
 | 
					                    PREVIEW.after(50, wait_for_withdraw)
 | 
				
			||||||
 | 
					            wait_for_withdraw()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
    else: # modules.globals.map_faces is True
 | 
					    else: # modules.globals.map_faces is True
 | 
				
			||||||
        if not modules.globals.source_path:
 | 
					        if not modules.globals.source_path:
 | 
				
			||||||
            update_status("Error: No global source image selected (for hair/background in map_faces mode).")
 | 
					            update_status("Error: No global source image selected (for hair/background in map_faces mode).")
 | 
				
			||||||
            cap.release()
 | 
					            cap.release()
 | 
				
			||||||
            PREVIEW.withdraw()
 | 
					            PREVIEW.withdraw()
 | 
				
			||||||
            while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
					            def wait_for_withdraw():
 | 
				
			||||||
 | 
					                if PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
				
			||||||
                    ROOT.update_idletasks()
 | 
					                    ROOT.update_idletasks()
 | 
				
			||||||
                    ROOT.update()
 | 
					                    ROOT.update()
 | 
				
			||||||
                time.sleep(0.05)
 | 
					                    PREVIEW.after(50, wait_for_withdraw)
 | 
				
			||||||
 | 
					            wait_for_withdraw()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        if not os.path.exists(modules.globals.source_path):
 | 
					        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}")
 | 
					            update_status(f"Error: Source image (for hair/background) not found at {modules.globals.source_path}")
 | 
				
			||||||
            cap.release()
 | 
					            cap.release()
 | 
				
			||||||
            PREVIEW.withdraw()
 | 
					            PREVIEW.withdraw()
 | 
				
			||||||
            while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
					            def wait_for_withdraw():
 | 
				
			||||||
 | 
					                if PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
				
			||||||
                    ROOT.update_idletasks()
 | 
					                    ROOT.update_idletasks()
 | 
				
			||||||
                    ROOT.update()
 | 
					                    ROOT.update()
 | 
				
			||||||
                time.sleep(0.05)
 | 
					                    PREVIEW.after(50, wait_for_withdraw)
 | 
				
			||||||
 | 
					            wait_for_withdraw()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					 | 
				
			||||||
        source_frame_full_for_cam_map_faces = cv2.imread(modules.globals.source_path)
 | 
					        source_frame_full_for_cam_map_faces = cv2.imread(modules.globals.source_path)
 | 
				
			||||||
        if source_frame_full_for_cam_map_faces is None:
 | 
					        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}")
 | 
					            update_status(f"Error: Could not read source image (for hair/background) at {modules.globals.source_path}")
 | 
				
			||||||
            cap.release()
 | 
					            cap.release()
 | 
				
			||||||
            PREVIEW.withdraw()
 | 
					            PREVIEW.withdraw()
 | 
				
			||||||
            while PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
					            def wait_for_withdraw():
 | 
				
			||||||
 | 
					                if PREVIEW.state() != "withdrawn" and ROOT.winfo_exists():
 | 
				
			||||||
                    ROOT.update_idletasks()
 | 
					                    ROOT.update_idletasks()
 | 
				
			||||||
                    ROOT.update()
 | 
					                    ROOT.update()
 | 
				
			||||||
                time.sleep(0.05)
 | 
					                    PREVIEW.after(50, wait_for_withdraw)
 | 
				
			||||||
 | 
					            wait_for_withdraw()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if not modules.globals.source_target_map and not modules.globals.simple_map:
 | 
					        if not modules.globals.source_target_map and not modules.globals.simple_map:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,16 +50,47 @@ class VideoCapturer:
 | 
				
			||||||
                        continue
 | 
					                        continue
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                # Unix-like systems (Linux/Mac) capture method
 | 
					                # Unix-like systems (Linux/Mac) capture method
 | 
				
			||||||
 | 
					                backend = getattr(self, "camera_backend", None)
 | 
				
			||||||
 | 
					                if backend is None:
 | 
				
			||||||
 | 
					                    import os
 | 
				
			||||||
 | 
					                    backend_env = os.environ.get("VIDEO_CAPTURE_BACKEND")
 | 
				
			||||||
 | 
					                    if backend_env is not None:
 | 
				
			||||||
 | 
					                        try:
 | 
				
			||||||
 | 
					                            backend = int(backend_env)
 | 
				
			||||||
 | 
					                        except ValueError:
 | 
				
			||||||
 | 
					                            backend = getattr(cv2, backend_env, None)
 | 
				
			||||||
                if platform.system() == "Darwin":  # macOS
 | 
					                if platform.system() == "Darwin":  # macOS
 | 
				
			||||||
 | 
					                    tried_backends = []
 | 
				
			||||||
 | 
					                    if backend is not None:
 | 
				
			||||||
 | 
					                        print(f"INFO: Attempting to use user-specified backend {backend} for macOS camera.")
 | 
				
			||||||
 | 
					                        self.cap = cv2.VideoCapture(self.device_index, backend)
 | 
				
			||||||
 | 
					                        tried_backends.append(backend)
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
                        print("INFO: Attempting to use cv2.CAP_AVFOUNDATION for macOS camera.")
 | 
					                        print("INFO: Attempting to use cv2.CAP_AVFOUNDATION for macOS camera.")
 | 
				
			||||||
                        self.cap = cv2.VideoCapture(self.device_index, cv2.CAP_AVFOUNDATION)
 | 
					                        self.cap = cv2.VideoCapture(self.device_index, cv2.CAP_AVFOUNDATION)
 | 
				
			||||||
 | 
					                        tried_backends.append(cv2.CAP_AVFOUNDATION)
 | 
				
			||||||
                    if not self.cap or not self.cap.isOpened():
 | 
					                    if not self.cap or not self.cap.isOpened():
 | 
				
			||||||
                        print("WARN: cv2.CAP_AVFOUNDATION failed to open camera. Trying default backend for macOS.")
 | 
					                        print("WARN: First backend failed to open camera. Trying cv2.CAP_QT for macOS.")
 | 
				
			||||||
                        # Release the failed attempt before trying again
 | 
					                        if self.cap:
 | 
				
			||||||
 | 
					                            self.cap.release()
 | 
				
			||||||
 | 
					                        if cv2.CAP_QT not in tried_backends:
 | 
				
			||||||
 | 
					                            self.cap = cv2.VideoCapture(self.device_index, cv2.CAP_QT)
 | 
				
			||||||
 | 
					                            tried_backends.append(cv2.CAP_QT)
 | 
				
			||||||
 | 
					                    if not self.cap or not self.cap.isOpened():
 | 
				
			||||||
 | 
					                        print("WARN: cv2.CAP_QT failed to open camera. Trying default backend for macOS.")
 | 
				
			||||||
                        if self.cap:
 | 
					                        if self.cap:
 | 
				
			||||||
                            self.cap.release()
 | 
					                            self.cap.release()
 | 
				
			||||||
                        self.cap = cv2.VideoCapture(self.device_index) # Fallback to default
 | 
					                        self.cap = cv2.VideoCapture(self.device_index) # Fallback to default
 | 
				
			||||||
                else:  # Other Unix-like systems (e.g., Linux)
 | 
					                else:  # Other Unix-like systems (e.g., Linux)
 | 
				
			||||||
 | 
					                    if backend is not None:
 | 
				
			||||||
 | 
					                        print(f"INFO: Attempting to use user-specified backend {backend} for camera.")
 | 
				
			||||||
 | 
					                        self.cap = cv2.VideoCapture(self.device_index, backend)
 | 
				
			||||||
 | 
					                        if not self.cap or not self.cap.isOpened():
 | 
				
			||||||
 | 
					                            print("WARN: User-specified backend failed. Trying default backend.")
 | 
				
			||||||
 | 
					                            if self.cap:
 | 
				
			||||||
 | 
					                                self.cap.release()
 | 
				
			||||||
 | 
					                            self.cap = cv2.VideoCapture(self.device_index)
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
                        self.cap = cv2.VideoCapture(self.device_index)
 | 
					                        self.cap = cv2.VideoCapture(self.device_index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if not self.cap or not self.cap.isOpened():
 | 
					            if not self.cap or not self.cap.isOpened():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue