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