import cv2 import numpy as np from scipy.ndimage import binary_dilation, gaussian_filter def seamless_hair_to_neck_blend( source_img, target_img, src_masks, tgt_masks, src_landmarks, tgt_landmarks, hair_preserve=0.8, neck_strength=0.75, blend_strength=0.85 ): """ Perform seamless full-head swap: hairline -> face -> neck. No hard-paste boundaries. Uses gradient-domain + Laplacian blending. """ h, w = target_img.shape[:2] # -- 1. FACE MASK ---------------------------------------------------------- face_mask = tgt_masks['face_mask'].astype(np.float32) / 255.0 # -- 2. HAIR MASK (expanded from face region via segmentation) ------------- hair_mask_raw = tgt_masks['hair_mask'].astype(np.uint8) hair_mask_dilated = binary_dilation(hair_mask_raw, iterations=12).astype(np.float32) # Feather the hair boundary with Gaussian hair_boundary = np.abs(hair_mask_dilated - face_mask) hair_boundary_soft = gaussian_filter(hair_boundary, sigma=18) # -- 3. NECK MASK (from jawline to collar) ---------------------------------- neck_mask_raw = tgt_masks['neck_mask'].astype(np.float32) / 255.0 neck_mask_feathered = gaussian_filter(neck_mask_raw, sigma=10) # -- 4. COMPOSITE BLEND WEIGHT MAP ----------------------------------------- # Inside face: high weight source # Hair boundary: gradual fade (preserve target hair) # Neck: partial blend (match neck tone) composite = ( face_mask * blend_strength + hair_boundary_soft * (1.0 - hair_preserve) + neck_mask_feathered * (neck_strength * 0.4) ) composite = np.clip(composite, 0.0, 1.0) # Stack to 3 channels alpha = np.stack([composite] * 3, axis=-1) # -- 5. BLEND SOURCE ONTO TARGET ------------------------------------------- blended = (source_img.astype(np.float32) * alpha + target_img.astype(np.float32) * (1.0 - alpha)) blended = blended.astype(np.uint8) # -- 6. NECK TONE GRADIENT -------------------------------------------------- # Neck skin is naturally 5-15% darker than face blended = _apply_neck_darkening(blended, neck_mask_feathered, factor=0.92) # -- 7. FINAL EDGE SMOOTHING ----------------------------------------------- blended = _smooth_blend_boundary(blended, target_img, composite) return blended def _apply_neck_darkening(image, neck_mask, factor=0.92): """Darken neck region to match natural skin gradient.""" img_float = image.astype(np.float32) mask_3ch = np.stack([neck_mask] * 3, axis=-1) darkened = img_float * factor result = img_float * (1 - mask_3ch) + darkened * mask_3ch return result.astype(np.uint8) def _smooth_blend_boundary(blended, target, composite_mask): """Apply bilateral filter at blend boundaries for smooth transitions.""" boundary = cv2.dilate((composite_mask * 255).astype(np.uint8), np.ones((5, 5), np.uint8), iterations=3) boundary = cv2.erode(boundary, np.ones((5, 5), np.uint8), iterations=2) smooth = cv2.bilateralFilter(blended, d=9, sigmaColor=75, sigmaSpace=75) mask = (boundary > 0).astype(np.float32) mask_3 = np.stack([mask] * 3, axis=-1) result = (smooth.astype(np.float32) * mask_3 + blended.astype(np.float32) * (1 - mask_3)) return result.astype(np.uint8)