28Senaru commited on
Commit
d9fb6ab
·
verified ·
1 Parent(s): ca2fa83

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -0
app.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import cv2
3
+ import numpy as np
4
+ from PIL import Image
5
+ import mediapipe as mp
6
+ import insightface
7
+ import torch
8
+ from scipy import stats
9
+
10
+ # -------------------- Setup --------------------
11
+ device = "cuda" if torch.cuda.is_available() else "cpu"
12
+
13
+ mp_mesh = mp.solutions.face_mesh.FaceMesh(
14
+ static_image_mode=True,
15
+ max_num_faces=1,
16
+ refine_landmarks=True
17
+ )
18
+
19
+ face_analyzer = insightface.app.FaceAnalysis(name="buffalo_l")
20
+ face_analyzer.prepare(ctx_id=0 if device == "cuda" else -1)
21
+
22
+ # -------------------- Utilities --------------------
23
+ def pil_to_cv(img):
24
+ return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
25
+
26
+ def cv_to_pil(img):
27
+ return Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
28
+
29
+ def get_landmarks(img):
30
+ res = mp_mesh.process(cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR))
31
+ if not res.multi_face_landmarks:
32
+ return None
33
+ lm = res.multi_face_landmarks[0].landmark
34
+ return np.array([[p.x, p.y] for p in lm], dtype=np.float32)
35
+
36
+ def align_face(src, tgt):
37
+ s = get_landmarks(src)
38
+ t = get_landmarks(tgt)
39
+ if s is None or t is None:
40
+ return None
41
+ idx = [33, 263, 1, 61, 291]
42
+ M, _ = cv2.estimateAffinePartial2D(s[idx], t[idx])
43
+ return cv_to_pil(cv2.warpAffine(pil_to_cv(src), M, tgt.size))
44
+
45
+ def identity_similarity(a, b):
46
+ ea = face_analyzer.get(np.array(a))
47
+ eb = face_analyzer.get(np.array(b))
48
+ if not ea or not eb:
49
+ return 0.0
50
+ v1, v2 = ea[0].embedding, eb[0].embedding
51
+ return float(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)))
52
+
53
+ def color_match(src, tgt):
54
+ src_lab = cv2.cvtColor(src, cv2.COLOR_BGR2LAB)
55
+ tgt_lab = cv2.cvtColor(tgt, cv2.COLOR_BGR2LAB)
56
+ for i in range(3):
57
+ src_lab[:,:,i] = stats.rankdata(src_lab[:,:,i]).reshape(src_lab[:,:,i].shape)
58
+ src_lab[:,:,i] = np.interp(src_lab[:,:,i],
59
+ (src_lab[:,:,i].min(), src_lab[:,:,i].max()),
60
+ (tgt_lab[:,:,i].min(), tgt_lab[:,:,i].max()))
61
+ return cv2.cvtColor(src_lab.astype(np.uint8), cv2.COLOR_LAB2BGR)
62
+
63
+ # -------------------- Core --------------------
64
+ def face_swap(src_img, tgt_img):
65
+ if src_img is None or tgt_img is None:
66
+ return "Upload both images", None
67
+
68
+ aligned = align_face(src_img, tgt_img)
69
+ if aligned is None:
70
+ return "Face alignment failed", None
71
+
72
+ src_cv = pil_to_cv(aligned)
73
+ tgt_cv = pil_to_cv(tgt_img)
74
+
75
+ src_cv = color_match(src_cv, tgt_cv)
76
+
77
+ mask = 255 * np.ones(src_cv.shape[:2], dtype=np.uint8)
78
+ center = (tgt_cv.shape[1]//2, tgt_cv.shape[0]//2)
79
+
80
+ blended = cv2.seamlessClone(src_cv, tgt_cv, mask, center, cv2.NORMAL_CLONE)
81
+ result = cv_to_pil(blended)
82
+
83
+ sim = identity_similarity(src_img, result)
84
+ if sim < 0.94:
85
+ return f"Similarity too low: {sim:.3f}", result
86
+
87
+ return f"Similarity OK: {sim:.3f}", result
88
+
89
+ # -------------------- UI --------------------
90
+ with gr.Blocks() as demo:
91
+ gr.Markdown("## Ultra-Realistic Face Swap (Photographic Only)")
92
+ s = gr.Image(label="Source Face", type="pil")
93
+ t = gr.Image(label="Target Image", type="pil")
94
+ b = gr.Button("Swap")
95
+ out_txt = gr.Textbox(label="Status")
96
+ out_img = gr.Image(label="Result")
97
+
98
+ b.click(face_swap, [s, t], [out_txt, out_img])
99
+
100
+ demo.launch()