Files changed (4) hide show
  1. README.md +16 -3
  2. app.py +94 -78
  3. reference.png +0 -0
  4. requirements.txt +7 -5
README.md CHANGED
@@ -1,6 +1,19 @@
1
  ---
2
- title: Face Swap Demo
 
 
 
3
  sdk: gradio
4
- python_version: "3.11"
5
- sdk_version: "6.5.1"
 
 
6
  ---
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Face Swap
3
+ emoji: 🎭
4
+ colorFrom: red
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
  ---
12
+
13
+ # Face Swap — InsightFace inswapper_128
14
+
15
+ Upload or snap a webcam photo of your face, click **Swap**, and it replaces the face in the target image (default: a Denmark fan). Runs on the free CPU tier.
16
+
17
+ Models:
18
+ - Detection + embedding: `buffalo_l` (auto-downloaded by insightface)
19
+ - Swap: `inswapper_128.onnx` (pulled from `ezioruan/inswapper_128.onnx`)
app.py CHANGED
@@ -1,82 +1,98 @@
1
- import gradio as gr
 
 
 
 
 
2
  import cv2
3
  import numpy as np
4
- import mediapipe as mp
5
-
6
- mp_fd = mp.solutions.face_detection.FaceDetection(model_selection=1, min_detection_confidence=0.6)
7
-
8
- def read_rgb(image):
9
- # gradioはnumpy(H,W,3) RGBで来る想定
10
- if image is None:
11
- return None
12
- return image.copy()
13
-
14
- def detect_face_bbox(rgb):
15
- h, w, _ = rgb.shape
16
- bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
17
- res = mp_fd.process(cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB))
18
- if not res.detections:
19
- return None
20
- # 最もスコアが高い顔を使う
21
- det = sorted(res.detections, key=lambda d: d.score[0], reverse=True)[0]
22
- bb = det.location_data.relative_bounding_box
23
- x1 = max(int(bb.xmin * w), 0)
24
- y1 = max(int(bb.ymin * h), 0)
25
- x2 = min(int((bb.xmin + bb.width) * w), w - 1)
26
- y2 = min(int((bb.ymin + bb.height) * h), h - 1)
27
- if x2 <= x1 or y2 <= y1:
28
- return None
29
- return (x1, y1, x2, y2)
30
-
31
- def naive_paste(source_rgb, target_rgb):
32
- """
33
- まずは“動く”こと優先の簡易版:
34
- targetの顔bboxにsourceの顔bboxをリサイズして貼り付け、簡易フェザーでなじませる。
35
- (高品質にするには、この後にFaceMesh + 三角ワープ + seamlessCloneへ進む)
36
- """
37
- sb = detect_face_bbox(source_rgb)
38
- tb = detect_face_bbox(target_rgb)
39
- if sb is None:
40
- raise gr.Error("source画像から顔を検出できませんでした。正面顔・明るさを調整して再試行してください。")
41
- if tb is None:
42
- raise gr.Error("target画像から顔を検出できませんでした。正面顔・明るさを調整して再試行してください。")
43
-
44
- sx1, sy1, sx2, sy2 = sb
45
- tx1, ty1, tx2, ty2 = tb
46
-
47
- s_face = source_rgb[sy1:sy2, sx1:sx2]
48
- t_h, t_w = (ty2 - ty1), (tx2 - tx1)
49
- s_face_rs = cv2.resize(s_face, (t_w, t_h), interpolation=cv2.INTER_LINEAR)
50
-
51
- out = target_rgb.copy()
52
-
53
- # マスク(楕円 + ぼかし)で簡易ブレンド
54
- mask = np.zeros((t_h, t_w), dtype=np.uint8)
55
- cv2.ellipse(mask, (t_w // 2, t_h // 2), (int(t_w * 0.45), int(t_h * 0.50)), 0, 0, 360, 255, -1)
56
- mask = cv2.GaussianBlur(mask, (0, 0), sigmaX=max(t_w, t_h) * 0.02)
57
-
58
- roi = out[ty1:ty2, tx1:tx2]
59
- # alpha合成
60
- alpha = (mask.astype(np.float32) / 255.0)[..., None]
61
- blended = (alpha * s_face_rs + (1 - alpha) * roi).astype(np.uint8)
62
- out[ty1:ty2, tx1:tx2] = blended
63
-
64
- return out
65
-
66
- def run(source_img, target_img):
67
- s = read_rgb(source_img)
68
- t = read_rgb(target_img)
69
- if s is None or t is None:
70
- raise gr.Error("source/target の両方の画像をアップロードしてください。")
71
- return naive_paste(s, t)
72
-
73
- with gr.Blocks() as demo:
74
- gr.Markdown("# Face Swap (HF Spaces / Python)\nアップロードした2枚の画像から顔を検出し、target側にsource顔を当て込みます。")
75
  with gr.Row():
76
- src = gr.Image(label="Source(当てたい顔)", type="numpy")
77
- tgt = gr.Image(label="Target(当てられる側)", type="numpy")
78
- btn = gr.Button("Swap")
79
- out = gr.Image(label="Output", type="numpy")
80
- btn.click(run, inputs=[src, tgt], outputs=out)
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- demo.launch()
 
 
1
+ """
2
+ Hugging Face Space — Face Swap with InsightFace inswapper_128.
3
+ Gradio app: webcam or upload a source face, swap it onto a target image.
4
+ """
5
+
6
+ import os
7
  import cv2
8
  import numpy as np
9
+ import gradio as gr
10
+ import insightface
11
+ from insightface.app import FaceAnalysis
12
+ from huggingface_hub import hf_hub_download
13
+
14
+ HERE = os.path.dirname(os.path.abspath(__file__))
15
+ DEFAULT_TARGET = os.path.join(HERE, "reference.png")
16
+
17
+ print("[boot] loading FaceAnalysis (buffalo_l)…", flush=True)
18
+ face_app = FaceAnalysis(name="buffalo_l", providers=["CPUExecutionProvider"])
19
+ face_app.prepare(ctx_id=0, det_size=(640, 640))
20
+
21
+ print("[boot] downloading inswapper_128…", flush=True)
22
+ MODEL_PATH = hf_hub_download(
23
+ repo_id="ezioruan/inswapper_128.onnx",
24
+ filename="inswapper_128.onnx",
25
+ )
26
+
27
+ print("[boot] loading inswapper_128…", flush=True)
28
+ swapper = insightface.model_zoo.get_model(
29
+ MODEL_PATH, providers=["CPUExecutionProvider"]
30
+ )
31
+
32
+ print("[boot] ready", flush=True)
33
+
34
+
35
+ def largest_face(faces):
36
+ return max(
37
+ faces,
38
+ key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]),
39
+ )
40
+
41
+
42
+ def swap(source_img, target_img):
43
+ if source_img is None:
44
+ return None, "Please provide a source face (webcam or upload)."
45
+
46
+ # Gradio gives RGB numpy — convert to BGR for OpenCV/InsightFace
47
+ source_bgr = cv2.cvtColor(source_img, cv2.COLOR_RGB2BGR)
48
+
49
+ if target_img is None:
50
+ target_bgr = cv2.imread(DEFAULT_TARGET)
51
+ if target_bgr is None:
52
+ return None, "Default target image is missing."
53
+ else:
54
+ target_bgr = cv2.cvtColor(target_img, cv2.COLOR_RGB2BGR)
55
+
56
+ source_faces = face_app.get(source_bgr)
57
+ if not source_faces:
58
+ return None, "No face detected in source image."
59
+ src_face = largest_face(source_faces)
60
+
61
+ target_faces = face_app.get(target_bgr)
62
+ if not target_faces:
63
+ return None, "No face detected in target image."
64
+ tgt_face = largest_face(target_faces)
65
+
66
+ result = swapper.get(target_bgr, tgt_face, src_face, paste_back=True)
67
+ return cv2.cvtColor(result, cv2.COLOR_BGR2RGB), "Done."
68
+
69
+
70
+ with gr.Blocks(title="Face Swap — InsightFace") as demo:
71
+ gr.Markdown(
72
+ "# Face Swap — InsightFace `inswapper_128`\n"
73
+ "Take a webcam photo (or upload), click **Swap**. "
74
+ "Leave the target empty to use the default Denmark fan, "
75
+ "or upload your own target image."
76
+ )
 
 
 
77
  with gr.Row():
78
+ with gr.Column():
79
+ src_in = gr.Image(
80
+ sources=["webcam", "upload"],
81
+ type="numpy",
82
+ label="Your face (webcam or upload)",
83
+ )
84
+ tgt_in = gr.Image(
85
+ sources=["upload"],
86
+ type="numpy",
87
+ label="Target image (optional — defaults to reference.png)",
88
+ value=DEFAULT_TARGET,
89
+ )
90
+ btn = gr.Button("Swap", variant="primary")
91
+ with gr.Column():
92
+ out_img = gr.Image(label="Result", type="numpy")
93
+ status = gr.Textbox(label="Status", interactive=False)
94
+
95
+ btn.click(swap, inputs=[src_in, tgt_in], outputs=[out_img, status])
96
 
97
+ if __name__ == "__main__":
98
+ demo.launch()
reference.png ADDED
requirements.txt CHANGED
@@ -1,5 +1,7 @@
1
- gradio==6.5.1
2
- opencv-python
3
- numpy==1.26.4
4
- protobuf<5
5
- mediapipe==0.10.14
 
 
 
1
+ gradio==4.44.0
2
+ insightface==0.7.3
3
+ onnxruntime==1.18.1
4
+ opencv-python-headless==4.10.0.84
5
+ numpy<2
6
+ huggingface_hub>=0.24.0
7
+ cython