28Senaru commited on
Commit
37520c2
·
verified ·
1 Parent(s): 0b91b6f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -0
app.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import cv2
3
+ import numpy as np
4
+ from PIL import Image
5
+ import torch
6
+ import face_alignment
7
+ import insightface
8
+ from scipy import stats
9
+
10
+ # -------------------- Device --------------------
11
+ device = "cuda" if torch.cuda.is_available() else "cpu"
12
+
13
+ # -------------------- Face Alignment --------------------
14
+ fa = face_alignment.FaceAlignment(
15
+ face_alignment.LandmarksType["2D"],
16
+ device=device,
17
+ flip_input=False
18
+ )
19
+
20
+ # -------------------- Identity Model --------------------
21
+ face_analyzer = insightface.app.FaceAnalysis(name="buffalo_l")
22
+ face_analyzer.prepare(ctx_id=0 if device == "cuda" else -1)
23
+
24
+ # -------------------- Utilities --------------------
25
+ def pil_to_cv(img):
26
+ return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
27
+
28
+ def cv_to_pil(img):
29
+ return Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
30
+
31
+ def get_landmarks(img):
32
+ preds = fa.get_landmarks(np.array(img))
33
+ if preds is None or len(preds) == 0:
34
+ return None
35
+ return preds[0].astype(np.float32) # (68,2)
36
+
37
+ def align_face(src, tgt):
38
+ src_lm = get_landmarks(src)
39
+ tgt_lm = get_landmarks(tgt)
40
+ if src_lm is None or tgt_lm is None:
41
+ return None
42
+
43
+ # Use 5 key landmarks for affine transform: eyes, nose tip, mouth corners
44
+ idx = [36, 45, 30, 48, 54]
45
+ M, _ = cv2.estimateAffinePartial2D(src_lm[idx], tgt_lm[idx])
46
+ if M is None:
47
+ return None
48
+
49
+ aligned = cv2.warpAffine(
50
+ pil_to_cv(src),
51
+ M,
52
+ (tgt.width, tgt.height),
53
+ flags=cv2.INTER_LINEAR
54
+ )
55
+ return cv_to_pil(aligned)
56
+
57
+ def identity_similarity(a, b):
58
+ ea = face_analyzer.get(np.array(a))
59
+ eb = face_analyzer.get(np.array(b))
60
+ if not ea or not eb:
61
+ return 0.0
62
+ v1 = ea[0].embedding
63
+ v2 = eb[0].embedding
64
+ return float(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)))
65
+
66
+ def color_match(src, tgt):
67
+ src_lab = cv2.cvtColor(src, cv2.COLOR_BGR2LAB)
68
+ tgt_lab = cv2.cvtColor(tgt, cv2.COLOR_BGR2LAB)
69
+
70
+ for i in range(3):
71
+ s = src_lab[:, :, i].flatten()
72
+ t = tgt_lab[:, :, i].flatten()
73
+ s_rank = stats.rankdata(s)
74
+ s_norm = (s_rank - s_rank.min()) / (s_rank.max() - s_rank.min() + 1e-6)
75
+ t_sorted = np.sort(t)
76
+ src_lab[:, :, i] = t_sorted[(s_norm * (len(t_sorted) - 1)).astype(int)].reshape(src_lab[:, :, i].shape)
77
+
78
+ return cv2.cvtColor(src_lab.astype(np.uint8), cv2.COLOR_LAB2BGR)
79
+
80
+ # -------------------- Core Face Swap --------------------
81
+ def face_swap(src_img, tgt_img):
82
+ if src_img is None or tgt_img is None:
83
+ return "Upload both images", None
84
+
85
+ aligned = align_face(src_img, tgt_img)
86
+ if aligned is None:
87
+ return "Face alignment failed", None
88
+
89
+ src_cv = pil_to_cv(aligned)
90
+ tgt_cv = pil_to_cv(tgt_img)
91
+
92
+ # Color harmonization
93
+ src_cv = color_match(src_cv, tgt_cv)
94
+
95
+ # Poisson blending
96
+ mask = 255 * np.ones(src_cv.shape[:2], dtype=np.uint8)
97
+ center = (tgt_cv.shape[1] // 2, tgt_cv.shape[0] // 2)
98
+ blended = cv2.seamlessClone(src_cv, tgt_cv, mask, center, cv2.NORMAL_CLONE)
99
+
100
+ result = cv_to_pil(blended)
101
+
102
+ # Identity validation
103
+ sim = identity_similarity(src_img, result)
104
+ if sim < 0.94:
105
+ return f"Identity similarity too low: {sim:.3f}", result
106
+
107
+ return f"Identity similarity OK: {sim:.3f}", result
108
+
109
+ # -------------------- Gradio UI --------------------
110
+ with gr.Blocks() as demo:
111
+ gr.Markdown("## Ultra-Realistic Face Swap (Photographic Only)")
112
+ gr.Markdown(
113
+ "- Strict 2D face swap\n"
114
+ "- Identity similarity ≥0.94\n"
115
+ "- Zero AI / 3D look\n"
116
+ )
117
+
118
+ src = gr.Image(label="Source Face", type="pil")
119
+ tgt = gr.Image(label="Target Image", type="pil")
120
+ btn = gr.Button("Run Face Swap")
121
+ status = gr.Textbox(label="Status")
122
+ output = gr.Image(label="Result")
123
+
124
+ btn.click(face_swap, [src, tgt], [status, output])
125
+
126
+ demo.launch()