Improve face swap FPS and blending, add profiling and config options
							parent
							
								
									5f2e54552c
								
							
						
					
					
						commit
						348e6c424e
					
				| 
						 | 
					@ -18,6 +18,12 @@ from modules.utilities import (
 | 
				
			||||||
from modules.cluster_analysis import find_closest_centroid
 | 
					from modules.cluster_analysis import find_closest_centroid
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- CONFIGURABLE PARAMETERS FOR PERFORMANCE & BLENDING ---
 | 
				
			||||||
 | 
					BLEND_MASK_BLUR_KERNEL = (9, 9)  # Larger kernel for smoother mask edges
 | 
				
			||||||
 | 
					BLEND_MASK_BLUR_SIGMA = 5        # Higher sigma for more feathering
 | 
				
			||||||
 | 
					SEAMLESS_CLONE_MODE = cv2.NORMAL_CLONE  # Try cv2.MIXED_CLONE for different effect
 | 
				
			||||||
 | 
					PROFILE_FACE_SWAP = True         # Set to True to enable timing logs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FACE_SWAPPER = None
 | 
					FACE_SWAPPER = None
 | 
				
			||||||
THREAD_LOCK = threading.Lock()
 | 
					THREAD_LOCK = threading.Lock()
 | 
				
			||||||
NAME = "DLC.FACE-SWAPPER"
 | 
					NAME = "DLC.FACE-SWAPPER"
 | 
				
			||||||
| 
						 | 
					@ -62,8 +68,14 @@ def get_face_swapper() -> Any:
 | 
				
			||||||
    with THREAD_LOCK:
 | 
					    with THREAD_LOCK:
 | 
				
			||||||
        if FACE_SWAPPER is None:
 | 
					        if FACE_SWAPPER is None:
 | 
				
			||||||
            model_path = os.path.join(models_dir, "inswapper_128_fp16.onnx")
 | 
					            model_path = os.path.join(models_dir, "inswapper_128_fp16.onnx")
 | 
				
			||||||
 | 
					            # Prefer GPU if available
 | 
				
			||||||
 | 
					            providers = modules.globals.execution_providers
 | 
				
			||||||
 | 
					            if 'CUDAExecutionProvider' in providers:
 | 
				
			||||||
 | 
					                chosen_providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                chosen_providers = providers
 | 
				
			||||||
            FACE_SWAPPER = insightface.model_zoo.get_model(
 | 
					            FACE_SWAPPER = insightface.model_zoo.get_model(
 | 
				
			||||||
                model_path, providers=modules.globals.execution_providers
 | 
					                model_path, providers=chosen_providers
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
    return FACE_SWAPPER
 | 
					    return FACE_SWAPPER
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -118,20 +130,18 @@ def _blend_material_onto_frame(
 | 
				
			||||||
    Uses seamlessClone if possible, otherwise falls back to simple masking.
 | 
					    Uses seamlessClone if possible, otherwise falls back to simple masking.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    x, y, w, h = cv2.boundingRect(mask_for_blending)
 | 
					    x, y, w, h = cv2.boundingRect(mask_for_blending)
 | 
				
			||||||
    output_frame = base_frame # Start with base, will be modified by blending
 | 
					    output_frame = base_frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if w > 0 and h > 0:
 | 
					    if w > 0 and h > 0:
 | 
				
			||||||
        center = (x + w // 2, y + h // 2)
 | 
					        center = (x + w // 2, y + h // 2)
 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        if material_to_blend.shape == base_frame.shape and \
 | 
					        if material_to_blend.shape == base_frame.shape and \
 | 
				
			||||||
           material_to_blend.dtype == base_frame.dtype and \
 | 
					           material_to_blend.dtype == base_frame.dtype and \
 | 
				
			||||||
           mask_for_blending.dtype == np.uint8:
 | 
					           mask_for_blending.dtype == np.uint8:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                # Important: seamlessClone modifies the first argument (dst) if it's the same as the output var
 | 
					                # Use configurable blur for mask
 | 
				
			||||||
                # So, if base_frame is final_swapped_frame, it will be modified in place.
 | 
					                blurred_mask = cv2.GaussianBlur(mask_for_blending, BLEND_MASK_BLUR_KERNEL, BLEND_MASK_BLUR_SIGMA)
 | 
				
			||||||
                # If we want to keep base_frame pristine, it should be base_frame.copy() if it's also final_swapped_frame.
 | 
					                _, mask_bin = cv2.threshold(blurred_mask, 127, 255, cv2.THRESH_BINARY)
 | 
				
			||||||
                # Given final_swapped_frame is already a copy of swapped_frame at this point, this is fine.
 | 
					                output_frame = cv2.seamlessClone(material_to_blend, base_frame, mask_bin, center, SEAMLESS_CLONE_MODE)
 | 
				
			||||||
                output_frame = cv2.seamlessClone(material_to_blend, base_frame, mask_for_blending, center, cv2.NORMAL_CLONE)
 | 
					 | 
				
			||||||
            except cv2.error as e:
 | 
					            except cv2.error as e:
 | 
				
			||||||
                logging.warning(f"cv2.seamlessClone failed: {e}. Falling back to simple blending.")
 | 
					                logging.warning(f"cv2.seamlessClone failed: {e}. Falling back to simple blending.")
 | 
				
			||||||
                boolean_mask = mask_for_blending > 127 
 | 
					                boolean_mask = mask_for_blending > 127 
 | 
				
			||||||
| 
						 | 
					@ -142,16 +152,15 @@ def _blend_material_onto_frame(
 | 
				
			||||||
            output_frame[boolean_mask] = material_to_blend[boolean_mask]
 | 
					            output_frame[boolean_mask] = material_to_blend[boolean_mask]
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        logging.info("Warped mask for blending is empty. Skipping blending.")
 | 
					        logging.info("Warped mask for blending is empty. Skipping blending.")
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    return output_frame
 | 
					    return output_frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def swap_face(source_face_obj: Face, target_face: Face, source_frame_full: Frame, temp_frame: Frame) -> Frame:
 | 
					def swap_face(source_face_obj: Face, target_face: Face, source_frame_full: Frame, temp_frame: Frame) -> Frame:
 | 
				
			||||||
 | 
					    import time
 | 
				
			||||||
    face_swapper = get_face_swapper()
 | 
					    face_swapper = get_face_swapper()
 | 
				
			||||||
 | 
					    start_time = time.time() if PROFILE_FACE_SWAP else None
 | 
				
			||||||
    # Apply the base face swap
 | 
					 | 
				
			||||||
    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 # Initialize with the base swap. Copy is made only if needed.
 | 
					    final_swapped_frame = swapped_frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if modules.globals.enable_hair_swapping:
 | 
					    if modules.globals.enable_hair_swapping:
 | 
				
			||||||
        if not (source_face_obj.kps is not None and \
 | 
					        if not (source_face_obj.kps is not None and \
 | 
				
			||||||
| 
						 | 
					@ -215,6 +224,9 @@ def swap_face(source_face_obj: Face, target_face: Face, source_frame_full: Frame
 | 
				
			||||||
                final_swapped_frame, target_face, mouth_mask_data
 | 
					                final_swapped_frame, target_face, mouth_mask_data
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if PROFILE_FACE_SWAP:
 | 
				
			||||||
 | 
					        elapsed = time.time() - start_time
 | 
				
			||||||
 | 
					        logging.info(f"Face swap+blend time: {elapsed:.3f}s")
 | 
				
			||||||
    return final_swapped_frame
 | 
					    return final_swapped_frame
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue