Spaces:
Runtime error
Runtime error
Upload 23 files
Browse files- apply_color_transfer.py +38 -19
- roop/face_analyser.py +90 -0
- roop/processors/frame/face_enhancer.py +1 -1
- roop/processors/frame/face_swapper.py +1 -1
- segmentation.py +10 -0
apply_color_transfer.py
CHANGED
|
@@ -88,8 +88,7 @@ def histogram_matching_with_pixels(source, reference_pixels):
|
|
| 88 |
|
| 89 |
def apply_color_transfer_to_output(swapped_img, original_bg, opacity=0.7, color_region=None):
|
| 90 |
"""
|
| 91 |
-
Apply color transfer to
|
| 92 |
-
Automatically uses face+forehead region (excluding hair) as color reference by default.
|
| 93 |
|
| 94 |
Args:
|
| 95 |
swapped_img: PIL Image - The image after face swapping
|
|
@@ -98,9 +97,11 @@ def apply_color_transfer_to_output(swapped_img, original_bg, opacity=0.7, color_
|
|
| 98 |
color_region: tuple or None - (x, y, width, height) for specific color sampling region
|
| 99 |
|
| 100 |
Returns:
|
| 101 |
-
PIL Image - The color-corrected result
|
| 102 |
"""
|
| 103 |
try:
|
|
|
|
|
|
|
| 104 |
# Clamp opacity to valid range
|
| 105 |
opacity = max(0.0, min(1.0, opacity))
|
| 106 |
|
|
@@ -108,32 +109,50 @@ def apply_color_transfer_to_output(swapped_img, original_bg, opacity=0.7, color_
|
|
| 108 |
swapped_np = np.array(swapped_img.convert('RGB'))
|
| 109 |
original_np = np.array(original_bg.convert('RGB'))
|
| 110 |
|
| 111 |
-
#
|
|
|
|
|
|
|
|
|
|
| 112 |
face_pixels = get_face_forehead_region_no_hair(original_bg)
|
| 113 |
|
| 114 |
-
if face_pixels is not None:
|
| 115 |
-
print(f"✅
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
elif color_region is not None:
|
| 120 |
# Fallback to manual region if provided
|
| 121 |
x, y, w, h = color_region
|
| 122 |
reference_region = original_np[y:y+h, x:x+w]
|
| 123 |
-
if reference_region.size > 0:
|
| 124 |
-
print(f"⚙️
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
else:
|
| 127 |
-
print("⚠️ Invalid manual region
|
| 128 |
-
|
| 129 |
|
| 130 |
else:
|
| 131 |
-
#
|
| 132 |
-
print("⚠️ No face detection,
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
# Blend original and color-corrected image based on opacity
|
| 136 |
-
result = blend_images(swapped_np, color_corrected, opacity)
|
| 137 |
|
| 138 |
return Image.fromarray(result)
|
| 139 |
|
|
|
|
| 88 |
|
| 89 |
def apply_color_transfer_to_output(swapped_img, original_bg, opacity=0.7, color_region=None):
|
| 90 |
"""
|
| 91 |
+
Apply color transfer ONLY to face regions, using face+forehead (excluding hair) as color reference.
|
|
|
|
| 92 |
|
| 93 |
Args:
|
| 94 |
swapped_img: PIL Image - The image after face swapping
|
|
|
|
| 97 |
color_region: tuple or None - (x, y, width, height) for specific color sampling region
|
| 98 |
|
| 99 |
Returns:
|
| 100 |
+
PIL Image - The color-corrected result with face-only color transfer
|
| 101 |
"""
|
| 102 |
try:
|
| 103 |
+
from segmentation import detect_face_and_forehead_no_hair
|
| 104 |
+
|
| 105 |
# Clamp opacity to valid range
|
| 106 |
opacity = max(0.0, min(1.0, opacity))
|
| 107 |
|
|
|
|
| 109 |
swapped_np = np.array(swapped_img.convert('RGB'))
|
| 110 |
original_np = np.array(original_bg.convert('RGB'))
|
| 111 |
|
| 112 |
+
# Get face mask for TARGET (where to apply color transfer)
|
| 113 |
+
target_face_mask = detect_face_and_forehead_no_hair(swapped_img)
|
| 114 |
+
|
| 115 |
+
# Try advanced face+forehead detection for REFERENCE color
|
| 116 |
face_pixels = get_face_forehead_region_no_hair(original_bg)
|
| 117 |
|
| 118 |
+
if face_pixels is not None and target_face_mask.sum() > 0:
|
| 119 |
+
print(f"✅ Face-only color transfer: {len(face_pixels)} reference pixels → {target_face_mask.sum()} target pixels")
|
| 120 |
+
|
| 121 |
+
# Apply color transfer using extracted face+forehead pixels
|
| 122 |
+
color_corrected_full = histogram_matching_with_pixels(swapped_np, face_pixels)
|
| 123 |
+
|
| 124 |
+
# Create result by blending ONLY face regions
|
| 125 |
+
result = swapped_np.copy()
|
| 126 |
+
face_indices = target_face_mask == 1
|
| 127 |
+
|
| 128 |
+
# Blend only face pixels
|
| 129 |
+
result[face_indices] = (
|
| 130 |
+
swapped_np[face_indices].astype(np.float32) * (1 - opacity) +
|
| 131 |
+
color_corrected_full[face_indices].astype(np.float32) * opacity
|
| 132 |
+
).astype(np.uint8)
|
| 133 |
|
| 134 |
elif color_region is not None:
|
| 135 |
# Fallback to manual region if provided
|
| 136 |
x, y, w, h = color_region
|
| 137 |
reference_region = original_np[y:y+h, x:x+w]
|
| 138 |
+
if reference_region.size > 0 and target_face_mask.sum() > 0:
|
| 139 |
+
print(f"⚙️ Face-only with manual region: {color_region}")
|
| 140 |
+
color_corrected_full = histogram_matching_with_region(swapped_np, original_np, reference_region)
|
| 141 |
+
|
| 142 |
+
result = swapped_np.copy()
|
| 143 |
+
face_indices = target_face_mask == 1
|
| 144 |
+
result[face_indices] = (
|
| 145 |
+
swapped_np[face_indices].astype(np.float32) * (1 - opacity) +
|
| 146 |
+
color_corrected_full[face_indices].astype(np.float32) * opacity
|
| 147 |
+
).astype(np.uint8)
|
| 148 |
else:
|
| 149 |
+
print("⚠️ Invalid manual region or no face detected")
|
| 150 |
+
result = swapped_np
|
| 151 |
|
| 152 |
else:
|
| 153 |
+
# No face detection - return original
|
| 154 |
+
print("⚠️ No face detection, no color transfer applied")
|
| 155 |
+
result = swapped_np
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
return Image.fromarray(result)
|
| 158 |
|
roop/face_analyser.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import threading
|
| 2 |
from typing import Any
|
| 3 |
import insightface
|
|
|
|
|
|
|
| 4 |
|
| 5 |
import roop.globals
|
| 6 |
from roop.typing import Frame
|
|
@@ -19,7 +21,69 @@ def get_face_analyser() -> Any:
|
|
| 19 |
return FACE_ANALYSER
|
| 20 |
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
def get_one_face(frame: Frame) -> Any:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
face = get_face_analyser().get(frame)
|
| 24 |
try:
|
| 25 |
selected_face = min(face, key=lambda x: x.bbox[0])
|
|
@@ -27,8 +91,34 @@ def get_one_face(frame: Frame) -> Any:
|
|
| 27 |
except ValueError:
|
| 28 |
return None
|
| 29 |
|
|
|
|
| 30 |
def get_many_faces(frame: Frame) -> Any:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
try:
|
| 32 |
return get_face_analyser().get(frame)
|
| 33 |
except IndexError:
|
| 34 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import threading
|
| 2 |
from typing import Any
|
| 3 |
import insightface
|
| 4 |
+
import numpy as np
|
| 5 |
+
from PIL import Image
|
| 6 |
|
| 7 |
import roop.globals
|
| 8 |
from roop.typing import Frame
|
|
|
|
| 21 |
return FACE_ANALYSER
|
| 22 |
|
| 23 |
|
| 24 |
+
def get_precise_face_mask(frame: Frame) -> Any:
|
| 25 |
+
"""
|
| 26 |
+
Get precise face mask using advanced segmentation (same as detect_face_and_forehead_no_hair).
|
| 27 |
+
Returns both InsightFace detection and precise mask.
|
| 28 |
+
"""
|
| 29 |
+
try:
|
| 30 |
+
# Import the precise detection function
|
| 31 |
+
import sys
|
| 32 |
+
import os
|
| 33 |
+
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
| 34 |
+
from segmentation import detect_face_and_forehead_no_hair
|
| 35 |
+
|
| 36 |
+
# Convert frame to PIL Image
|
| 37 |
+
if isinstance(frame, np.ndarray):
|
| 38 |
+
pil_image = Image.fromarray(frame)
|
| 39 |
+
else:
|
| 40 |
+
pil_image = frame
|
| 41 |
+
|
| 42 |
+
# Get precise face mask (clean skin only)
|
| 43 |
+
precise_mask = detect_face_and_forehead_no_hair(pil_image)
|
| 44 |
+
|
| 45 |
+
# Also get InsightFace detection for face swapping compatibility
|
| 46 |
+
insightface_faces = get_face_analyser().get(frame)
|
| 47 |
+
|
| 48 |
+
return {
|
| 49 |
+
'precise_mask': precise_mask,
|
| 50 |
+
'insightface_faces': insightface_faces,
|
| 51 |
+
'has_face': precise_mask.sum() > 0 and len(insightface_faces) > 0
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"Precise face detection failed: {e}")
|
| 56 |
+
# Fallback to regular InsightFace
|
| 57 |
+
insightface_faces = get_face_analyser().get(frame)
|
| 58 |
+
return {
|
| 59 |
+
'precise_mask': None,
|
| 60 |
+
'insightface_faces': insightface_faces,
|
| 61 |
+
'has_face': len(insightface_faces) > 0
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
def get_one_face(frame: Frame) -> Any:
|
| 66 |
+
"""
|
| 67 |
+
Get one face with enhanced precision detection.
|
| 68 |
+
"""
|
| 69 |
+
# Get precise detection info
|
| 70 |
+
face_info = get_precise_face_mask(frame)
|
| 71 |
+
|
| 72 |
+
if face_info['has_face'] and face_info['insightface_faces']:
|
| 73 |
+
try:
|
| 74 |
+
# Select face (leftmost) for compatibility
|
| 75 |
+
selected_face = min(face_info['insightface_faces'], key=lambda x: x.bbox[0])
|
| 76 |
+
|
| 77 |
+
# Add precise mask info to face object
|
| 78 |
+
if face_info['precise_mask'] is not None:
|
| 79 |
+
selected_face.precise_mask = face_info['precise_mask']
|
| 80 |
+
print(f"✅ Enhanced face detection: {face_info['precise_mask'].sum()} precise pixels")
|
| 81 |
+
|
| 82 |
+
return selected_face
|
| 83 |
+
except (ValueError, IndexError):
|
| 84 |
+
return None
|
| 85 |
+
|
| 86 |
+
# Fallback to original method
|
| 87 |
face = get_face_analyser().get(frame)
|
| 88 |
try:
|
| 89 |
selected_face = min(face, key=lambda x: x.bbox[0])
|
|
|
|
| 91 |
except ValueError:
|
| 92 |
return None
|
| 93 |
|
| 94 |
+
|
| 95 |
def get_many_faces(frame: Frame) -> Any:
|
| 96 |
+
"""
|
| 97 |
+
Get many faces with enhanced precision detection.
|
| 98 |
+
"""
|
| 99 |
+
# Get precise detection info
|
| 100 |
+
face_info = get_precise_face_mask(frame)
|
| 101 |
+
|
| 102 |
+
if face_info['has_face'] and face_info['insightface_faces']:
|
| 103 |
+
faces = face_info['insightface_faces']
|
| 104 |
+
|
| 105 |
+
# Add precise mask info to all face objects
|
| 106 |
+
if face_info['precise_mask'] is not None:
|
| 107 |
+
for face in faces:
|
| 108 |
+
face.precise_mask = face_info['precise_mask']
|
| 109 |
+
|
| 110 |
+
print(f"✅ Enhanced multi-face detection: {len(faces)} faces with precise masks")
|
| 111 |
+
return faces
|
| 112 |
+
|
| 113 |
+
# Fallback to original method
|
| 114 |
try:
|
| 115 |
return get_face_analyser().get(frame)
|
| 116 |
except IndexError:
|
| 117 |
return None
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def has_precise_face_mask(face_obj) -> bool:
|
| 121 |
+
"""
|
| 122 |
+
Check if face object has precise mask attached.
|
| 123 |
+
"""
|
| 124 |
+
return hasattr(face_obj, 'precise_mask') and face_obj.precise_mask is not None
|
roop/processors/frame/face_enhancer.py
CHANGED
|
@@ -37,7 +37,7 @@ def get_face_enhancer() -> Any:
|
|
| 37 |
def pre_check() -> bool:
|
| 38 |
download_directory_path = resolve_relative_path('../models')
|
| 39 |
# conditional_download(download_directory_path, ['https://huggingface.co/henryruhs/roop/resolve/main/GFPGANv1.4.pth'])
|
| 40 |
-
conditional_download(download_directory_path, ['https://
|
| 41 |
return True
|
| 42 |
|
| 43 |
|
|
|
|
| 37 |
def pre_check() -> bool:
|
| 38 |
download_directory_path = resolve_relative_path('../models')
|
| 39 |
# conditional_download(download_directory_path, ['https://huggingface.co/henryruhs/roop/resolve/main/GFPGANv1.4.pth'])
|
| 40 |
+
conditional_download(download_directory_path, ['https://huggingface.co/VanNguyen1214/detect_face/resolve/main/GFPGANv1.4.pth'])
|
| 41 |
return True
|
| 42 |
|
| 43 |
|
roop/processors/frame/face_swapper.py
CHANGED
|
@@ -28,7 +28,7 @@ def get_face_swapper() -> Any:
|
|
| 28 |
def pre_check() -> bool:
|
| 29 |
download_directory_path = resolve_relative_path('../models')
|
| 30 |
# conditional_download(download_directory_path, ['https://huggingface.co/henryruhs/roop/resolve/main/inswapper_128.onnx'])
|
| 31 |
-
conditional_download(download_directory_path, ['https://huggingface.co/
|
| 32 |
return True
|
| 33 |
|
| 34 |
|
|
|
|
| 28 |
def pre_check() -> bool:
|
| 29 |
download_directory_path = resolve_relative_path('../models')
|
| 30 |
# conditional_download(download_directory_path, ['https://huggingface.co/henryruhs/roop/resolve/main/inswapper_128.onnx'])
|
| 31 |
+
conditional_download(download_directory_path, ['https://huggingface.co/VanNguyen1214/detect_face/resolve/main/inswapper_128.onnx'])
|
| 32 |
return True
|
| 33 |
|
| 34 |
|
segmentation.py
CHANGED
|
@@ -5,6 +5,16 @@ import torch.nn.functional as F
|
|
| 5 |
import numpy as np
|
| 6 |
import mediapipe as mp
|
| 7 |
import cv2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
# Load model
|
| 10 |
processor = SegformerImageProcessor.from_pretrained("VanNguyen1214/get_face_and_hair")
|
|
|
|
| 5 |
import numpy as np
|
| 6 |
import mediapipe as mp
|
| 7 |
import cv2
|
| 8 |
+
import os
|
| 9 |
+
import warnings
|
| 10 |
+
|
| 11 |
+
# Suppress MediaPipe warnings
|
| 12 |
+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
| 13 |
+
warnings.filterwarnings('ignore')
|
| 14 |
+
|
| 15 |
+
# Suppress MediaPipe logs
|
| 16 |
+
import logging
|
| 17 |
+
logging.getLogger('mediapipe').setLevel(logging.ERROR)
|
| 18 |
|
| 19 |
# Load model
|
| 20 |
processor = SegformerImageProcessor.from_pretrained("VanNguyen1214/get_face_and_hair")
|