Update app.py
Browse files
app.py
CHANGED
|
@@ -4,12 +4,11 @@ import numpy as np
|
|
| 4 |
import gradio as gr
|
| 5 |
from insightface.app import FaceAnalysis
|
| 6 |
from insightface.model_zoo import get_model # For swapper
|
| 7 |
-
from PIL import Image
|
| 8 |
import tempfile
|
| 9 |
import logging
|
| 10 |
import onnxruntime
|
| 11 |
-
from typing import List, Optional, Tuple
|
| 12 |
-
import copy # Import for deepcopy
|
| 13 |
|
| 14 |
# --- Configuration & Setup ---
|
| 15 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
@@ -44,24 +43,6 @@ face_analyzer: Optional[FaceAnalysis] = None
|
|
| 44 |
swapper = None
|
| 45 |
face_restorer: Optional[onnxruntime.InferenceSession] = None
|
| 46 |
|
| 47 |
-
# --- Create Directories and Dummy Example Files (Moved Earlier) ---
|
| 48 |
-
os.makedirs("models", exist_ok=True)
|
| 49 |
-
os.makedirs("examples", exist_ok=True)
|
| 50 |
-
example_files_to_create = {
|
| 51 |
-
"examples/source_example_1.jpg": ('blue', (100,100)),
|
| 52 |
-
"examples/source_example_2.png": ('green', (100,100)),
|
| 53 |
-
"examples/target_example_1.jpg": ('yellow', (200,200)),
|
| 54 |
-
"examples/target_example_2.png": ('magenta', (200,200)),
|
| 55 |
-
}
|
| 56 |
-
for f_path, (color, size) in example_files_to_create.items():
|
| 57 |
-
if not os.path.exists(f_path):
|
| 58 |
-
try:
|
| 59 |
-
Image.new('RGB', size, color=color).save(f_path)
|
| 60 |
-
logging.info(f"Created dummy example file: {f_path}")
|
| 61 |
-
except Exception as e:
|
| 62 |
-
logging.warning(f"Could not create dummy example file {f_path}: {e}")
|
| 63 |
-
|
| 64 |
-
|
| 65 |
def initialize_models():
|
| 66 |
global face_analyzer, swapper, face_restorer
|
| 67 |
try:
|
|
@@ -117,7 +98,7 @@ def convert_cv2_to_pil(cv2_image: Optional[np.ndarray]) -> Optional[Image.Image]
|
|
| 117 |
logging.error(f"Error converting CV2 to PIL: {e}")
|
| 118 |
return None
|
| 119 |
|
| 120 |
-
def get_faces_from_image(img_np: np.ndarray) -> List
|
| 121 |
if not core_models_loaded_successfully or face_analyzer is None:
|
| 122 |
logging.error("Face analyzer not available for get_faces_from_image. Core models might have failed to load.")
|
| 123 |
return []
|
|
@@ -131,11 +112,10 @@ def get_faces_from_image(img_np: np.ndarray) -> List[Any]:
|
|
| 131 |
logging.error(f"Error during face detection: {e}", exc_info=True)
|
| 132 |
return []
|
| 133 |
|
| 134 |
-
def draw_detected_faces(img_np: np.ndarray, faces: List
|
| 135 |
img_with_boxes = img_np.copy()
|
| 136 |
for i, face in enumerate(faces):
|
| 137 |
-
|
| 138 |
-
box = face.bbox.astype(int)
|
| 139 |
x1, y1, x2, y2 = box[0], box[1], box[2], box[3]
|
| 140 |
cv2.rectangle(img_with_boxes, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
| 141 |
label_position = (x1, max(0, y1 - 10))
|
|
@@ -225,124 +205,29 @@ def histogram_match_color(source_img_bgr: np.ndarray, target_img_bgr: np.ndarray
|
|
| 225 |
logging.error(f"Error during color histogram matching: {e}", exc_info=True)
|
| 226 |
return source_img_bgr
|
| 227 |
|
| 228 |
-
def process_face_swap(
|
| 229 |
-
target_pil_img: Optional[Image.Image],
|
| 230 |
target_face_index: int, apply_enhancement: bool, apply_color_correction: bool,
|
| 231 |
progress=gr.Progress(track_tqdm=True)):
|
| 232 |
progress(0, desc="Initializing process...")
|
| 233 |
if not core_models_loaded_successfully:
|
| 234 |
gr.Error("CRITICAL: Core models (Face Analyzer or Swapper) not loaded. Cannot proceed.")
|
| 235 |
return Image.new('RGB', (100, 100), color='lightgrey'), None, "Core models failed to load."
|
|
|
|
|
|
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
if target_pil_img is None:
|
| 240 |
-
raise gr.Error("Target image not provided. Please upload a target scene image.")
|
| 241 |
-
|
| 242 |
-
progress(0.05, desc="Reading source images...")
|
| 243 |
-
source_pil_list: List[Image.Image] = []
|
| 244 |
-
for file_obj in source_file_objects:
|
| 245 |
-
try:
|
| 246 |
-
img = Image.open(file_obj.name).convert('RGB')
|
| 247 |
-
source_pil_list.append(img)
|
| 248 |
-
except Exception as e:
|
| 249 |
-
logging.warning(f"Could not read or convert one of the source images ({file_obj.name}): {e}")
|
| 250 |
-
gr.Info(f"Skipping one source image due to read error: {os.path.basename(file_obj.name)}")
|
| 251 |
-
|
| 252 |
-
if not source_pil_list:
|
| 253 |
-
raise gr.Error("No valid source images could be read. Please check the uploaded files.")
|
| 254 |
-
|
| 255 |
-
progress(0.10, desc=f"Extracting features from {len(source_pil_list)} source image(s)...")
|
| 256 |
-
valid_source_face_objects: List[Any] = []
|
| 257 |
-
|
| 258 |
-
for idx, pil_img in enumerate(source_pil_list):
|
| 259 |
-
source_np_item = convert_pil_to_cv2(pil_img)
|
| 260 |
-
if source_np_item is None:
|
| 261 |
-
logging.warning(f"Could not convert source image {idx+1} to CV2 format.")
|
| 262 |
-
gr.Info(f"Skipping source image {idx+1} due to conversion error.")
|
| 263 |
-
continue
|
| 264 |
-
|
| 265 |
-
current_source_faces = get_faces_from_image(source_np_item)
|
| 266 |
-
if not current_source_faces:
|
| 267 |
-
logging.warning(f"No face found in source image {idx+1}. It will be skipped.")
|
| 268 |
-
gr.Info(f"No face found in source image {idx+1}, skipping it.")
|
| 269 |
-
continue
|
| 270 |
-
|
| 271 |
-
detected_face = current_source_faces[0]
|
| 272 |
-
# Check for embedding attribute first, then key
|
| 273 |
-
current_embedding = None
|
| 274 |
-
if hasattr(detected_face, 'embedding') and detected_face.embedding is not None:
|
| 275 |
-
current_embedding = detected_face.embedding
|
| 276 |
-
elif 'embedding' in detected_face and detected_face['embedding'] is not None:
|
| 277 |
-
current_embedding = detected_face['embedding']
|
| 278 |
-
|
| 279 |
-
if current_embedding is not None:
|
| 280 |
-
valid_source_face_objects.append(detected_face)
|
| 281 |
-
else:
|
| 282 |
-
logging.warning(f"Valid embedding not found for face in source image {idx+1}.")
|
| 283 |
-
gr.Info(f"Valid embedding not found in source image {idx+1}, skipping.")
|
| 284 |
-
|
| 285 |
-
if not valid_source_face_objects:
|
| 286 |
-
raise gr.Error("No usable faces (with embeddings) found in any of the uploaded source images.")
|
| 287 |
-
|
| 288 |
-
source_face_for_swap: Optional[Any] = None
|
| 289 |
-
if len(valid_source_face_objects) == 1:
|
| 290 |
-
source_face_for_swap = valid_source_face_objects[0]
|
| 291 |
-
logging.info("Using single source face for swap.")
|
| 292 |
-
gr.Info("Using features from 1 source image.")
|
| 293 |
-
else:
|
| 294 |
-
logging.info(f"Averaging embeddings from {len(valid_source_face_objects)} source faces.")
|
| 295 |
-
gr.Info(f"Averaging features from {len(valid_source_face_objects)} source images for potentially better results.")
|
| 296 |
-
|
| 297 |
-
embeddings_to_average = []
|
| 298 |
-
for face_obj in valid_source_face_objects:
|
| 299 |
-
emb_val = None
|
| 300 |
-
if hasattr(face_obj, 'embedding') and face_obj.embedding is not None:
|
| 301 |
-
emb_val = face_obj.embedding
|
| 302 |
-
elif 'embedding' in face_obj and face_obj['embedding'] is not None:
|
| 303 |
-
emb_val = face_obj['embedding']
|
| 304 |
-
|
| 305 |
-
if emb_val is None: # Should have been caught, but defensive
|
| 306 |
-
logging.error("A face object in valid_source_face_objects lacked an embedding unexpectedly.")
|
| 307 |
-
raise gr.Error("Internal error: Missing embedding in a validated source face.")
|
| 308 |
-
embeddings_to_average.append(emb_val)
|
| 309 |
-
|
| 310 |
-
first_emb_shape = embeddings_to_average[0].shape
|
| 311 |
-
for i, emb in enumerate(embeddings_to_average):
|
| 312 |
-
if emb.shape != first_emb_shape:
|
| 313 |
-
logging.error(f"Embeddings have inconsistent shapes. Cannot average. Emb {i} shape: {emb.shape}, Expected: {first_emb_shape}")
|
| 314 |
-
raise gr.Error("Source face embeddings have inconsistent structures. Cannot average.")
|
| 315 |
-
|
| 316 |
-
avg_embedding = np.mean(np.array(embeddings_to_average), axis=0)
|
| 317 |
-
avg_embedding = avg_embedding / np.linalg.norm(avg_embedding)
|
| 318 |
-
|
| 319 |
-
source_face_for_swap = copy.deepcopy(valid_source_face_objects[0]) # Deepcopy to preserve class and attributes
|
| 320 |
-
|
| 321 |
-
# Set 'embedding' (primary)
|
| 322 |
-
if hasattr(source_face_for_swap, 'embedding'):
|
| 323 |
-
source_face_for_swap.embedding = avg_embedding
|
| 324 |
-
elif 'embedding' in source_face_for_swap:
|
| 325 |
-
source_face_for_swap['embedding'] = avg_embedding
|
| 326 |
-
else: # If somehow neither exists, create it (less likely for Face objects)
|
| 327 |
-
source_face_for_swap['embedding'] = avg_embedding
|
| 328 |
-
|
| 329 |
-
# Set 'normed_embedding' if it exists, as this was the original error trigger
|
| 330 |
-
if hasattr(source_face_for_swap, 'normed_embedding'):
|
| 331 |
-
source_face_for_swap.normed_embedding = avg_embedding
|
| 332 |
-
elif 'normed_embedding' in source_face_for_swap:
|
| 333 |
-
source_face_for_swap['normed_embedding'] = avg_embedding
|
| 334 |
-
# If 'normed_embedding' does not exist, we assume the swapper might fall back to 'embedding'
|
| 335 |
-
# or that the original Face object structure usually has both as the same if already normalized.
|
| 336 |
-
|
| 337 |
-
if source_face_for_swap is None:
|
| 338 |
-
raise gr.Error("Failed to prepare a source face for swapping.")
|
| 339 |
-
|
| 340 |
-
progress(0.20, desc="Converting target image...")
|
| 341 |
target_np = convert_pil_to_cv2(target_pil_img)
|
| 342 |
-
if target_np is None:
|
| 343 |
-
raise gr.Error("
|
| 344 |
target_h, target_w = target_np.shape[:2]
|
| 345 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
progress(0.25, desc="Detecting faces in target image...")
|
| 347 |
target_faces = get_faces_from_image(target_np)
|
| 348 |
if not target_faces:
|
|
@@ -351,15 +236,12 @@ def process_face_swap(source_file_objects: Optional[List[Any]],
|
|
| 351 |
err_msg = (f"Selected target face index ({target_face_index}) is out of range. "
|
| 352 |
f"Detected {len(target_faces)} faces (indices 0 to {len(target_faces)-1}). Please re-preview and select a valid face.")
|
| 353 |
raise gr.Error(err_msg)
|
| 354 |
-
target_face_to_swap_info = target_faces[int(target_face_index)]
|
| 355 |
-
|
| 356 |
swapped_bgr_img = target_np.copy()
|
| 357 |
try:
|
| 358 |
progress(0.4, desc="Performing face swap...")
|
| 359 |
-
swapped_bgr_img = swapper.get(swapped_bgr_img, target_face_to_swap_info,
|
| 360 |
-
|
| 361 |
-
# target_face_to_swap_info is directly from insightface, .bbox should be safe
|
| 362 |
-
bbox_coords = target_face_to_swap_info.bbox.astype(int)
|
| 363 |
|
| 364 |
if apply_enhancement:
|
| 365 |
if restoration_model_loaded_successfully and face_restorer is not None:
|
|
@@ -403,6 +285,9 @@ def process_face_swap(source_file_objects: Optional[List[Any]],
|
|
| 403 |
logging.warning("Skipping color correction due to invalid bounding box for region extraction.")
|
| 404 |
except Exception as e:
|
| 405 |
logging.error(f"Error during face swapping or post-processing: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
| 406 |
raise gr.Error(f"An error occurred during processing: {str(e)}. Check logs for details.")
|
| 407 |
|
| 408 |
progress(0.9, desc="Finalizing image conversion...")
|
|
@@ -436,7 +321,7 @@ def preview_target_faces(target_pil_img: Optional[Image.Image]):
|
|
| 436 |
if target_np is None:
|
| 437 |
gr.Warning("Could not process target image for preview. Please try a different image.")
|
| 438 |
return blank_preview_placeholder, reset_slider
|
| 439 |
-
faces = get_faces_from_image(target_np)
|
| 440 |
if not faces:
|
| 441 |
max_h, max_w = DETECTION_SIZE[1], DETECTION_SIZE[0]
|
| 442 |
h, w = target_np.shape[:2]
|
|
@@ -449,8 +334,7 @@ def preview_target_faces(target_pil_img: Optional[Image.Image]):
|
|
| 449 |
if preview_pil_img is None: preview_pil_img = blank_preview_placeholder
|
| 450 |
gr.Info("No faces were detected in the target image.")
|
| 451 |
return preview_pil_img, reset_slider
|
| 452 |
-
|
| 453 |
-
preview_np_with_boxes = draw_detected_faces(target_np, faces)
|
| 454 |
preview_pil_img_with_boxes = convert_cv2_to_pil(preview_np_with_boxes)
|
| 455 |
if preview_pil_img_with_boxes is None:
|
| 456 |
preview_pil_img_with_boxes = blank_preview_placeholder
|
|
@@ -461,13 +345,13 @@ def preview_target_faces(target_pil_img: Optional[Image.Image]):
|
|
| 461 |
return preview_pil_img_with_boxes, slider_update
|
| 462 |
|
| 463 |
# --- Gradio UI Definition ---
|
| 464 |
-
with gr.Blocks(title="Ultimate Face Swap AI π v3.
|
| 465 |
gr.Markdown(
|
| 466 |
"""
|
| 467 |
<div style="text-align: center; border-bottom: 1px solid #eee; padding-bottom:10px;">
|
| 468 |
-
<h1>π Ultimate Face Swap AI <span style="font-size:0.8em; color: #555;">v3.
|
| 469 |
-
<p>Upload <strong>
|
| 470 |
-
<p style="font-size:0.9em;">
|
| 471 |
</div>
|
| 472 |
"""
|
| 473 |
)
|
|
@@ -476,15 +360,13 @@ with gr.Blocks(title="Ultimate Face Swap AI π v3.5 Multi-Source", theme=gr.th
|
|
| 476 |
gr.Error(
|
| 477 |
"π΄ CRITICAL ERROR: Core models (Face Analyzer or Swapper) failed to load. "
|
| 478 |
"The application UI will load but WILL NOT FUNCTION correctly. "
|
| 479 |
-
"Please check the console logs for details
|
|
|
|
| 480 |
)
|
| 481 |
|
| 482 |
with gr.Row():
|
| 483 |
with gr.Column(scale=1, min_width=300):
|
| 484 |
-
source_image_input = gr.
|
| 485 |
-
label="π€ Source Face Images (Upload one or more clear face images)",
|
| 486 |
-
file_types=["image"],
|
| 487 |
-
)
|
| 488 |
with gr.Column(scale=1, min_width=300):
|
| 489 |
target_image_input = gr.Image(label="πΌοΈ Target Scene Image (Upload image to swap a face into)", type="pil", sources=["upload", "clipboard"], height=380)
|
| 490 |
|
|
@@ -501,6 +383,7 @@ with gr.Blocks(title="Ultimate Face Swap AI π v3.5 Multi-Source", theme=gr.th
|
|
| 501 |
)
|
| 502 |
gr.Markdown("<p style='font-size:0.8em; color:#555;'>After uploading target, click 'Preview'. Then use slider to select face.</p>")
|
| 503 |
|
|
|
|
| 504 |
gr.HTML("<hr style='margin-top: 5px; margin-bottom: 5px; border-style: solid; border-width: thin; border-color: #ddd;'>")
|
| 505 |
|
| 506 |
enhance_checkbox_label = "β¨ Apply Selective Face Restoration"
|
|
@@ -512,9 +395,9 @@ with gr.Blocks(title="Ultimate Face Swap AI π v3.5 Multi-Source", theme=gr.th
|
|
| 512 |
interactive=restoration_model_loaded_successfully
|
| 513 |
)
|
| 514 |
if not restoration_model_loaded_successfully:
|
| 515 |
-
gr.Markdown("<p style='color: orange; font-size:0.8em;'>β οΈ Face restoration model not loaded. This feature is disabled
|
| 516 |
else:
|
| 517 |
-
gr.Markdown("<p style='font-size:0.8em; color:#555;'>Improves quality of the swapped face region.</p>")
|
| 518 |
|
| 519 |
color_correction_checkbox = gr.Checkbox(
|
| 520 |
label="π¨ Apply Color Correction",
|
|
@@ -585,57 +468,29 @@ with gr.Blocks(title="Ultimate Face Swap AI π v3.5 Multi-Source", theme=gr.th
|
|
| 585 |
gr.HTML("<hr style='margin-top: 20px; margin-bottom: 10px;'>")
|
| 586 |
gr.Markdown("### π Example Usage")
|
| 587 |
|
| 588 |
-
def process_face_swap_for_examples(
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
mock_source_file_objects = []
|
| 592 |
-
if source_file_paths_list:
|
| 593 |
-
for path in source_file_paths_list:
|
| 594 |
-
if not os.path.exists(path):
|
| 595 |
-
logging.error(f"Example source file not found: {path}. Skipping this example or part of it.")
|
| 596 |
-
continue
|
| 597 |
-
mock_file_obj = type('MockFile', (object,), {'name': path})()
|
| 598 |
-
mock_source_file_objects.append(mock_file_obj)
|
| 599 |
-
|
| 600 |
-
target_pil = None
|
| 601 |
-
if target_img_path and os.path.exists(target_img_path):
|
| 602 |
-
try:
|
| 603 |
-
target_pil = Image.open(target_img_path)
|
| 604 |
-
except Exception as e:
|
| 605 |
-
logging.error(f"Failed to load example target image {target_img_path}: {e}")
|
| 606 |
-
return Image.new('RGB', (100,100), 'red'), None, f"Error loading example target: {os.path.basename(target_img_path)}"
|
| 607 |
-
elif target_img_path and not os.path.exists(target_img_path):
|
| 608 |
-
logging.error(f"Example target file not found: {target_img_path}")
|
| 609 |
-
return Image.new('RGB', (100,100), 'red'), None, f"Example target missing: {os.path.basename(target_img_path)}"
|
| 610 |
-
|
| 611 |
-
if not mock_source_file_objects and not target_pil: # Both sources and target failed or were not provided
|
| 612 |
-
return Image.new('RGB', (100,100), 'red'), None, "Error loading example images."
|
| 613 |
-
if not mock_source_file_objects and target_pil : # Sources failed/missing but target is ok
|
| 614 |
-
# This case means process_face_swap will raise an error about no source images.
|
| 615 |
-
# Let's try to call it anyway and let its internal error handling manage the gr.Error.
|
| 616 |
-
# Or, we can return a specific message here.
|
| 617 |
-
# For consistency, let process_face_swap handle the "no source" error.
|
| 618 |
-
pass
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
return process_face_swap(mock_source_file_objects, target_pil, idx, enhance, color_correct, progress)
|
| 622 |
|
| 623 |
gr.Examples(
|
| 624 |
examples=[
|
| 625 |
-
[
|
| 626 |
-
[
|
| 627 |
-
[
|
| 628 |
],
|
| 629 |
inputs=[source_image_input, target_image_input, face_index_slider, enhance_checkbox, color_correction_checkbox],
|
| 630 |
outputs=[swapped_image_output, download_output_file, status_message_output],
|
| 631 |
fn=process_face_swap_for_examples,
|
| 632 |
cache_examples=False,
|
| 633 |
-
label="Example Face Swaps (
|
| 634 |
)
|
| 635 |
|
| 636 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
| 637 |
print("\n" + "="*70)
|
| 638 |
-
print("π ULTIMATE FACE SWAP AI - v3.
|
| 639 |
print("="*70)
|
| 640 |
print(f"Execution Providers Selected: {EXECUTION_PROVIDERS}")
|
| 641 |
|
|
@@ -656,5 +511,5 @@ if __name__ == "__main__":
|
|
| 656 |
print("\nπ The Gradio interface will launch, but swapping functionality will be broken. Please address errors.")
|
| 657 |
else:
|
| 658 |
print("\nπ Launching Gradio Interface... Access it in your browser (usually at http://127.0.0.1:7860).")
|
| 659 |
-
|
| 660 |
demo.launch()
|
|
|
|
| 4 |
import gradio as gr
|
| 5 |
from insightface.app import FaceAnalysis
|
| 6 |
from insightface.model_zoo import get_model # For swapper
|
| 7 |
+
from PIL import Image
|
| 8 |
import tempfile
|
| 9 |
import logging
|
| 10 |
import onnxruntime
|
| 11 |
+
from typing import List, Optional, Tuple # For improved type hinting
|
|
|
|
| 12 |
|
| 13 |
# --- Configuration & Setup ---
|
| 14 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
| 43 |
swapper = None
|
| 44 |
face_restorer: Optional[onnxruntime.InferenceSession] = None
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
def initialize_models():
|
| 47 |
global face_analyzer, swapper, face_restorer
|
| 48 |
try:
|
|
|
|
| 98 |
logging.error(f"Error converting CV2 to PIL: {e}")
|
| 99 |
return None
|
| 100 |
|
| 101 |
+
def get_faces_from_image(img_np: np.ndarray) -> List:
|
| 102 |
if not core_models_loaded_successfully or face_analyzer is None:
|
| 103 |
logging.error("Face analyzer not available for get_faces_from_image. Core models might have failed to load.")
|
| 104 |
return []
|
|
|
|
| 112 |
logging.error(f"Error during face detection: {e}", exc_info=True)
|
| 113 |
return []
|
| 114 |
|
| 115 |
+
def draw_detected_faces(img_np: np.ndarray, faces: List) -> np.ndarray:
|
| 116 |
img_with_boxes = img_np.copy()
|
| 117 |
for i, face in enumerate(faces):
|
| 118 |
+
box = face.bbox.astype(int)
|
|
|
|
| 119 |
x1, y1, x2, y2 = box[0], box[1], box[2], box[3]
|
| 120 |
cv2.rectangle(img_with_boxes, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
| 121 |
label_position = (x1, max(0, y1 - 10))
|
|
|
|
| 205 |
logging.error(f"Error during color histogram matching: {e}", exc_info=True)
|
| 206 |
return source_img_bgr
|
| 207 |
|
| 208 |
+
def process_face_swap(source_pil_img: Optional[Image.Image], target_pil_img: Optional[Image.Image],
|
|
|
|
| 209 |
target_face_index: int, apply_enhancement: bool, apply_color_correction: bool,
|
| 210 |
progress=gr.Progress(track_tqdm=True)):
|
| 211 |
progress(0, desc="Initializing process...")
|
| 212 |
if not core_models_loaded_successfully:
|
| 213 |
gr.Error("CRITICAL: Core models (Face Analyzer or Swapper) not loaded. Cannot proceed.")
|
| 214 |
return Image.new('RGB', (100, 100), color='lightgrey'), None, "Core models failed to load."
|
| 215 |
+
if source_pil_img is None: raise gr.Error("Source image not provided. Please upload a source face image.")
|
| 216 |
+
if target_pil_img is None: raise gr.Error("Target image not provided. Please upload a target scene image.")
|
| 217 |
|
| 218 |
+
progress(0.05, desc="Converting images to OpenCV format...")
|
| 219 |
+
source_np = convert_pil_to_cv2(source_pil_img)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
target_np = convert_pil_to_cv2(target_pil_img)
|
| 221 |
+
if source_np is None or target_np is None:
|
| 222 |
+
raise gr.Error("Image conversion failed. Ensure images are valid (e.g., JPG, PNG) and not corrupted.")
|
| 223 |
target_h, target_w = target_np.shape[:2]
|
| 224 |
|
| 225 |
+
progress(0.15, desc="Detecting face in source image...")
|
| 226 |
+
source_faces = get_faces_from_image(source_np)
|
| 227 |
+
if not source_faces:
|
| 228 |
+
raise gr.Error("No face found in the source image. Please use a clear, well-lit image of a single, front-facing face.")
|
| 229 |
+
source_face = source_faces[0]
|
| 230 |
+
|
| 231 |
progress(0.25, desc="Detecting faces in target image...")
|
| 232 |
target_faces = get_faces_from_image(target_np)
|
| 233 |
if not target_faces:
|
|
|
|
| 236 |
err_msg = (f"Selected target face index ({target_face_index}) is out of range. "
|
| 237 |
f"Detected {len(target_faces)} faces (indices 0 to {len(target_faces)-1}). Please re-preview and select a valid face.")
|
| 238 |
raise gr.Error(err_msg)
|
| 239 |
+
target_face_to_swap_info = target_faces[int(target_face_index)]
|
|
|
|
| 240 |
swapped_bgr_img = target_np.copy()
|
| 241 |
try:
|
| 242 |
progress(0.4, desc="Performing face swap...")
|
| 243 |
+
swapped_bgr_img = swapper.get(swapped_bgr_img, target_face_to_swap_info, source_face, paste_back=True)
|
| 244 |
+
bbox_coords = target_face_to_swap_info.bbox.astype(int)
|
|
|
|
|
|
|
| 245 |
|
| 246 |
if apply_enhancement:
|
| 247 |
if restoration_model_loaded_successfully and face_restorer is not None:
|
|
|
|
| 285 |
logging.warning("Skipping color correction due to invalid bounding box for region extraction.")
|
| 286 |
except Exception as e:
|
| 287 |
logging.error(f"Error during face swapping or post-processing: {e}", exc_info=True)
|
| 288 |
+
swapped_pil_img_on_error = convert_cv2_to_pil(swapped_bgr_img)
|
| 289 |
+
if swapped_pil_img_on_error is None:
|
| 290 |
+
swapped_pil_img_on_error = Image.new('RGB', (target_w, target_h), color='lightgrey')
|
| 291 |
raise gr.Error(f"An error occurred during processing: {str(e)}. Check logs for details.")
|
| 292 |
|
| 293 |
progress(0.9, desc="Finalizing image conversion...")
|
|
|
|
| 321 |
if target_np is None:
|
| 322 |
gr.Warning("Could not process target image for preview. Please try a different image.")
|
| 323 |
return blank_preview_placeholder, reset_slider
|
| 324 |
+
faces = get_faces_from_image(target_np)
|
| 325 |
if not faces:
|
| 326 |
max_h, max_w = DETECTION_SIZE[1], DETECTION_SIZE[0]
|
| 327 |
h, w = target_np.shape[:2]
|
|
|
|
| 334 |
if preview_pil_img is None: preview_pil_img = blank_preview_placeholder
|
| 335 |
gr.Info("No faces were detected in the target image.")
|
| 336 |
return preview_pil_img, reset_slider
|
| 337 |
+
preview_np_with_boxes = draw_detected_faces(target_np, faces)
|
|
|
|
| 338 |
preview_pil_img_with_boxes = convert_cv2_to_pil(preview_np_with_boxes)
|
| 339 |
if preview_pil_img_with_boxes is None:
|
| 340 |
preview_pil_img_with_boxes = blank_preview_placeholder
|
|
|
|
| 345 |
return preview_pil_img_with_boxes, slider_update
|
| 346 |
|
| 347 |
# --- Gradio UI Definition ---
|
| 348 |
+
with gr.Blocks(title="Ultimate Face Swap AI π v3.2 Lite", theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky)) as demo:
|
| 349 |
gr.Markdown(
|
| 350 |
"""
|
| 351 |
<div style="text-align: center; border-bottom: 1px solid #eee; padding-bottom:10px;">
|
| 352 |
+
<h1>π Ultimate Face Swap AI <span style="font-size:0.8em; color: #555;">v3.2 Lite</span> β¨</h1>
|
| 353 |
+
<p>Upload a <strong>source face</strong>, a <strong>target image</strong>, and let the AI work its magic!</p>
|
| 354 |
+
<p style="font-size:0.9em;">Optionally enhance with <strong>face restoration</strong> and <strong>color correction</strong> for improved realism.</p>
|
| 355 |
</div>
|
| 356 |
"""
|
| 357 |
)
|
|
|
|
| 360 |
gr.Error(
|
| 361 |
"π΄ CRITICAL ERROR: Core models (Face Analyzer or Swapper) failed to load. "
|
| 362 |
"The application UI will load but WILL NOT FUNCTION correctly. "
|
| 363 |
+
"Please check the console logs for details (e.g., model file paths, ONNX Runtime issues) "
|
| 364 |
+
"and restart the application after fixing the underlying problem."
|
| 365 |
)
|
| 366 |
|
| 367 |
with gr.Row():
|
| 368 |
with gr.Column(scale=1, min_width=300):
|
| 369 |
+
source_image_input = gr.Image(label="π€ Source Face Image (Clear, single face recommended)", type="pil", sources=["upload", "clipboard"], height=380)
|
|
|
|
|
|
|
|
|
|
| 370 |
with gr.Column(scale=1, min_width=300):
|
| 371 |
target_image_input = gr.Image(label="πΌοΈ Target Scene Image (Upload image to swap a face into)", type="pil", sources=["upload", "clipboard"], height=380)
|
| 372 |
|
|
|
|
| 383 |
)
|
| 384 |
gr.Markdown("<p style='font-size:0.8em; color:#555;'>After uploading target, click 'Preview'. Then use slider to select face.</p>")
|
| 385 |
|
| 386 |
+
# Changed from gr.Markdown("---", style="...") to gr.HTML for wider compatibility
|
| 387 |
gr.HTML("<hr style='margin-top: 5px; margin-bottom: 5px; border-style: solid; border-width: thin; border-color: #ddd;'>")
|
| 388 |
|
| 389 |
enhance_checkbox_label = "β¨ Apply Selective Face Restoration"
|
|
|
|
| 395 |
interactive=restoration_model_loaded_successfully
|
| 396 |
)
|
| 397 |
if not restoration_model_loaded_successfully:
|
| 398 |
+
gr.Markdown("<p style='color: orange; font-size:0.8em;'>β οΈ Face restoration model not loaded. This feature is disabled. Check console logs for details on `RESTORATION_MODEL_PATH`.</p>")
|
| 399 |
else:
|
| 400 |
+
gr.Markdown("<p style='font-size:0.8em; color:#555;'>Improves quality of the swapped face region. Requires restoration model.</p>")
|
| 401 |
|
| 402 |
color_correction_checkbox = gr.Checkbox(
|
| 403 |
label="π¨ Apply Color Correction",
|
|
|
|
| 468 |
gr.HTML("<hr style='margin-top: 20px; margin-bottom: 10px;'>")
|
| 469 |
gr.Markdown("### π Example Usage")
|
| 470 |
|
| 471 |
+
def process_face_swap_for_examples(src_img, tgt_img, idx, enhance, color_correct, progress=gr.Progress(track_tqdm=True)):
|
| 472 |
+
img, file_path, status = process_face_swap(src_img, tgt_img, idx, enhance, color_correct, progress)
|
| 473 |
+
return img, file_path, status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
gr.Examples(
|
| 476 |
examples=[
|
| 477 |
+
["examples/source_example_1.jpg", "examples/target_example_1.jpg", 0, True, True],
|
| 478 |
+
["examples/source_example_2.png", "examples/target_example_2.png", 1, True, False],
|
| 479 |
+
["examples/source_example_1.jpg", "examples/target_example_1.jpg", 0, False, True],
|
| 480 |
],
|
| 481 |
inputs=[source_image_input, target_image_input, face_index_slider, enhance_checkbox, color_correction_checkbox],
|
| 482 |
outputs=[swapped_image_output, download_output_file, status_message_output],
|
| 483 |
fn=process_face_swap_for_examples,
|
| 484 |
cache_examples=False,
|
| 485 |
+
label="Example Face Swaps (Click to run - ensure example images exist in 'examples' folder)"
|
| 486 |
)
|
| 487 |
|
| 488 |
if __name__ == "__main__":
|
| 489 |
+
os.makedirs("models", exist_ok=True)
|
| 490 |
+
os.makedirs("examples", exist_ok=True)
|
| 491 |
+
|
| 492 |
print("\n" + "="*70)
|
| 493 |
+
print("π ULTIMATE FACE SWAP AI - v3.2 Lite STARTUP STATUS π") # Version bump
|
| 494 |
print("="*70)
|
| 495 |
print(f"Execution Providers Selected: {EXECUTION_PROVIDERS}")
|
| 496 |
|
|
|
|
| 511 |
print("\nπ The Gradio interface will launch, but swapping functionality will be broken. Please address errors.")
|
| 512 |
else:
|
| 513 |
print("\nπ Launching Gradio Interface... Access it in your browser (usually at http://127.0.0.1:7860).")
|
| 514 |
+
|
| 515 |
demo.launch()
|