Update app.py
Browse files
app.py
CHANGED
|
@@ -1,239 +1,34 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
import
|
| 3 |
-
import numpy as np
|
| 4 |
-
import mediapipe as mp
|
| 5 |
-
import time
|
| 6 |
-
import tempfile
|
| 7 |
-
import os
|
| 8 |
|
| 9 |
-
|
| 10 |
-
try:
|
| 11 |
-
mp_face_mesh = mp.solutions.face_mesh
|
| 12 |
-
face_mesh = mp_face_mesh.FaceMesh(
|
| 13 |
-
static_image_mode=True,
|
| 14 |
-
max_num_faces=1,
|
| 15 |
-
refine_landmarks=True,
|
| 16 |
-
min_detection_confidence=0.4,
|
| 17 |
-
min_tracking_confidence=0.4
|
| 18 |
-
)
|
| 19 |
-
print("MediaPipe Face Mesh initialized successfully.")
|
| 20 |
-
except (ImportError, AttributeError):
|
| 21 |
-
print("Error: Could not initialize MediaPipe Face Mesh. Is mediapipe installed correctly?")
|
| 22 |
-
face_mesh = None
|
| 23 |
-
|
| 24 |
-
# --- Helper Functions ---
|
| 25 |
-
|
| 26 |
-
def get_face_mask_box(img, feather_pct, padding_pct):
|
| 27 |
-
h, w = img.shape[:2]
|
| 28 |
-
mask = np.zeros((h, w), dtype=np.uint8)
|
| 29 |
-
results = face_mesh.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
| 30 |
-
if not results.multi_face_landmarks:
|
| 31 |
-
return None, None
|
| 32 |
-
pts = np.array([(int(p.x * w), int(p.y * h)) for p in results.multi_face_landmarks[0].landmark], np.int32)
|
| 33 |
-
hull = cv2.convexHull(pts)
|
| 34 |
-
cv2.fillConvexPoly(mask, hull, 255)
|
| 35 |
-
x, y, bw, bh = cv2.boundingRect(hull)
|
| 36 |
-
pad = int(max(bw, bh) * padding_pct)
|
| 37 |
-
x_pad = max(x - pad, 0)
|
| 38 |
-
y_pad = max(y - pad, 0)
|
| 39 |
-
x2 = min(x + bw + pad, w)
|
| 40 |
-
y2 = min(y + bh + pad, h)
|
| 41 |
-
mask_roi = mask[y_pad:y2, x_pad:x2]
|
| 42 |
-
if feather_pct > 0 and mask_roi.size > 0:
|
| 43 |
-
k = int(min(mask_roi.shape[0], mask_roi.shape[1]) * feather_pct)
|
| 44 |
-
if k % 2 == 0: k += 1
|
| 45 |
-
mask_roi = cv2.GaussianBlur(mask_roi, (k, k), 0)
|
| 46 |
-
return mask_roi, (x_pad, y_pad, x2 - x_pad, y2 - y_pad)
|
| 47 |
-
|
| 48 |
-
def get_landmarks(img, landmark_step=1):
|
| 49 |
-
if img is None or face_mesh is None:
|
| 50 |
-
return None
|
| 51 |
-
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 52 |
-
try:
|
| 53 |
-
results = face_mesh.process(img_rgb)
|
| 54 |
-
except Exception:
|
| 55 |
-
return None
|
| 56 |
-
if not results.multi_face_landmarks:
|
| 57 |
-
return None
|
| 58 |
-
landmarks_mp = results.multi_face_landmarks[0]
|
| 59 |
-
h, w, _ = img.shape
|
| 60 |
-
pts = np.array([(pt.x * w, pt.y * h) for pt in landmarks_mp.landmark], dtype=np.float32)
|
| 61 |
-
landmarks = pts[::landmark_step] if landmark_step > 1 else pts
|
| 62 |
-
corners = np.array([[0,0],[w-1,0],[0,h-1],[w-1,h-1]], dtype=np.float32)
|
| 63 |
-
return np.vstack((landmarks, corners))
|
| 64 |
-
|
| 65 |
-
def calculate_delaunay_triangles(rect, points):
|
| 66 |
-
if points is None or len(points) < 3:
|
| 67 |
-
return []
|
| 68 |
-
points[:,0] = np.clip(points[:,0], rect[0], rect[0]+rect[2]-1)
|
| 69 |
-
points[:,1] = np.clip(points[:,1], rect[1], rect[1]+rect[3]-1)
|
| 70 |
-
subdiv = cv2.Subdiv2D(rect)
|
| 71 |
-
inserted = {}
|
| 72 |
-
for i, p in enumerate(points):
|
| 73 |
-
key = (int(p[0]), int(p[1]))
|
| 74 |
-
if key not in inserted:
|
| 75 |
-
try:
|
| 76 |
-
subdiv.insert(key)
|
| 77 |
-
inserted[key] = i
|
| 78 |
-
except cv2.error:
|
| 79 |
-
continue
|
| 80 |
-
tris = subdiv.getTriangleList()
|
| 81 |
-
delaunay = []
|
| 82 |
-
for t in tris:
|
| 83 |
-
coords = [(int(t[0]), int(t[1])), (int(t[2]), int(t[3])), (int(t[4]), int(t[5]))]
|
| 84 |
-
if all(rect[0] <= x < rect[0]+rect[2] and rect[1] <= y < rect[1]+rect[3] for x, y in coords):
|
| 85 |
-
idxs = [inserted.get(c) for c in coords]
|
| 86 |
-
if all(i is not None for i in idxs) and len(set(idxs)) == 3:
|
| 87 |
-
delaunay.append(idxs)
|
| 88 |
-
return delaunay
|
| 89 |
-
|
| 90 |
-
def warp_triangle(img1, img2, t1, t2):
|
| 91 |
-
if len(t1) != 3 or len(t2) != 3: return
|
| 92 |
-
r1 = cv2.boundingRect(np.float32([t1]))
|
| 93 |
-
r2 = cv2.boundingRect(np.float32([t2]))
|
| 94 |
-
if r1[2] <= 0 or r1[3] <= 0 or r2[2] <= 0 or r2[3] <= 0:
|
| 95 |
-
return
|
| 96 |
-
src = img1[r1[1]:r1[1]+r1[3], r1[0]:r1[0]+r1[2]]
|
| 97 |
-
if src.size == 0: return
|
| 98 |
-
t1r = [(t1[i][0]-r1[0], t1[i][1]-r1[1]) for i in range(3)]
|
| 99 |
-
t2r = [(t2[i][0]-r2[0], t2[i][1]-r2[1]) for i in range(3)]
|
| 100 |
-
mask = np.zeros((r2[3], r2[2], 3), dtype=np.float32)
|
| 101 |
-
cv2.fillConvexPoly(mask, np.int32(t2r), (1,1,1), 16)
|
| 102 |
-
M = cv2.getAffineTransform(np.float32(t1r), np.float32(t2r))
|
| 103 |
-
warped = cv2.warpAffine(src, M, (r2[2], r2[3]), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
|
| 104 |
-
warped = warped.astype(np.float32) * mask
|
| 105 |
-
y1, y2 = r2[1], r2[1]+r2[3]
|
| 106 |
-
x1, x2 = r2[0], r2[0]+r2[2]
|
| 107 |
-
img2[y1:y2, x1:x2] = img2[y1:y2, x1:x2] * (1-mask) + warped
|
| 108 |
-
|
| 109 |
-
def morph_faces(img1, img2, alpha, dim, step):
|
| 110 |
-
if img1 is None or img2 is None:
|
| 111 |
-
return np.zeros((dim, dim, 3), dtype=np.uint8)
|
| 112 |
-
a = cv2.resize(img1, (dim, dim)).astype(np.float32) / 255.0
|
| 113 |
-
b = cv2.resize(img2, (dim, dim)).astype(np.float32) / 255.0
|
| 114 |
-
l1 = get_landmarks((a*255).astype(np.uint8), step)
|
| 115 |
-
l2 = get_landmarks((b*255).astype(np.uint8), step)
|
| 116 |
-
if l1 is None or l2 is None or l1.shape != l2.shape:
|
| 117 |
-
return cv2.addWeighted((a*255).astype(np.uint8), 1-alpha, (b*255).astype(np.uint8), alpha, 0)
|
| 118 |
-
m = (1-alpha)*l1 + alpha*l2
|
| 119 |
-
tris = calculate_delaunay_triangles((0, 0, dim, dim), m)
|
| 120 |
-
if not tris:
|
| 121 |
-
return cv2.addWeighted((a*255).astype(np.uint8), 1-alpha, (b*255).astype(np.uint8), alpha, 0)
|
| 122 |
-
Wa = np.zeros_like(a)
|
| 123 |
-
Wb = np.zeros_like(b)
|
| 124 |
-
for ids in tris:
|
| 125 |
-
warp_triangle(a, Wa, l1[ids], m[ids])
|
| 126 |
-
warp_triangle(b, Wb, l2[ids], m[ids])
|
| 127 |
-
out = (1-alpha)*Wa + alpha*Wb
|
| 128 |
-
return (out*255).astype(np.uint8)
|
| 129 |
-
|
| 130 |
-
def process_video(
|
| 131 |
-
video_path, ref_img, trans, res, step, feather_pct, padding_pct,
|
| 132 |
-
progress=gr.Progress()
|
| 133 |
-
):
|
| 134 |
-
# --- Initialization ---
|
| 135 |
-
cap = cv2.VideoCapture(video_path)
|
| 136 |
-
fps = cap.get(cv2.CAP_PROP_FPS) or 24
|
| 137 |
-
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 138 |
-
progress(0.0, desc="Initializing")
|
| 139 |
-
|
| 140 |
-
# --- Prepare masked reference ---
|
| 141 |
-
ref_bgr = cv2.cvtColor(ref_img, cv2.COLOR_RGB2BGR)
|
| 142 |
-
mask_ref, ref_box = get_face_mask_box(ref_bgr, feather_pct, padding_pct)
|
| 143 |
-
if mask_ref is None:
|
| 144 |
-
progress(None) # hide on error
|
| 145 |
-
return None, None, None, None
|
| 146 |
-
x_r, y_r, w_r, h_r = ref_box
|
| 147 |
-
ref_cut = ref_bgr[y_r:y_r+h_r, x_r:x_r+w_r]
|
| 148 |
-
mask_ref_norm = mask_ref.astype(np.float32)[..., None] / 255.0
|
| 149 |
-
ref_masked = (ref_cut.astype(np.float32) * mask_ref_norm).astype(np.uint8)
|
| 150 |
-
ref_morph = cv2.resize(ref_masked, (res, res))
|
| 151 |
-
progress(0.1, desc="Reference ready")
|
| 152 |
-
|
| 153 |
-
# --- Output setup ---
|
| 154 |
-
w_o = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 155 |
-
h_o = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 156 |
-
tmp_vid = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
|
| 157 |
-
out_vid = cv2.VideoWriter(tmp_vid, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w_o, h_o))
|
| 158 |
-
|
| 159 |
-
first_crop = first_ref = first_mask = first_morphed = None
|
| 160 |
-
|
| 161 |
-
# --- Frame-by-frame processing ---
|
| 162 |
-
for i in range(total):
|
| 163 |
-
ret, frame = cap.read()
|
| 164 |
-
if not ret:
|
| 165 |
-
break
|
| 166 |
-
progress(0.1 + 0.8 * (i / total), desc=f"Processing frame {i+1}/{total}")
|
| 167 |
|
| 168 |
-
mask_roi, box = get_face_mask_box(frame, feather_pct, padding_pct)
|
| 169 |
-
if mask_roi is None:
|
| 170 |
-
out_vid.write(frame)
|
| 171 |
-
continue
|
| 172 |
-
x, y, w, h = box
|
| 173 |
-
crop = frame[y:y+h, x:x+w]
|
| 174 |
-
crop_resized = cv2.resize(crop, (res, res))
|
| 175 |
-
alpha = float(np.clip((trans+1)/2, 0, 1))
|
| 176 |
-
mor = morph_faces(crop_resized, ref_morph, alpha, res, step)
|
| 177 |
-
|
| 178 |
-
if i == 0:
|
| 179 |
-
first_crop = crop_resized.copy()
|
| 180 |
-
first_ref = ref_morph.copy()
|
| 181 |
-
first_mask = cv2.resize(mask_roi, (res, res), interpolation=cv2.INTER_LINEAR)
|
| 182 |
-
first_morphed = mor.copy()
|
| 183 |
-
|
| 184 |
-
mor_back = cv2.resize(mor, (w, h))
|
| 185 |
-
mask_n = (mask_roi.astype(np.float32)[..., None] / 255.0)
|
| 186 |
-
region = frame[y:y+h, x:x+w].astype(np.float32)
|
| 187 |
-
blended = region * (1-mask_n) + mor_back.astype(np.float32) * mask_n
|
| 188 |
-
frame[y:y+h, x:x+w] = blended.astype(np.uint8)
|
| 189 |
-
out_vid.write(frame)
|
| 190 |
-
|
| 191 |
-
cap.release()
|
| 192 |
-
out_vid.release()
|
| 193 |
-
|
| 194 |
-
# --- First-frame outputs ---
|
| 195 |
-
if first_morphed is not None and first_mask is not None:
|
| 196 |
-
mask_n0 = first_mask.astype(np.float32)[..., None] / 255.0
|
| 197 |
-
first_morphed = (first_morphed.astype(np.float32) * mask_n0).astype(np.uint8)
|
| 198 |
-
else:
|
| 199 |
-
zero = np.zeros((res, res, 3), dtype=np.uint8)
|
| 200 |
-
first_crop = first_crop or zero
|
| 201 |
-
first_ref = first_ref or ref_morph
|
| 202 |
-
first_morphed = zero
|
| 203 |
-
|
| 204 |
-
progress(1.0, desc="Done")
|
| 205 |
-
return tmp_vid, \
|
| 206 |
-
cv2.cvtColor(first_crop, cv2.COLOR_BGR2RGB), \
|
| 207 |
-
cv2.cvtColor(first_ref, cv2.COLOR_BGR2RGB), \
|
| 208 |
-
cv2.cvtColor(first_morphed, cv2.COLOR_BGR2RGB)
|
| 209 |
-
|
| 210 |
-
# --- Gradio App ---
|
| 211 |
-
css = """video, img { object-fit: contain !important; }"""
|
| 212 |
with gr.Blocks(css=css) as iface:
|
| 213 |
-
gr.Markdown("#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
with gr.Row():
|
| 215 |
-
vid = gr.Video(label=
|
| 216 |
-
ref = gr.Image(type=
|
|
|
|
| 217 |
with gr.Row():
|
| 218 |
-
res = gr.Dropdown([256,384,512,768], value=512, label=
|
| 219 |
-
|
| 220 |
-
feather = gr.Slider(0.
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
btn = gr.Button(
|
| 224 |
-
out_vid = gr.Video(label=
|
| 225 |
-
out_crop = gr.Image(label='First Frame Crop')
|
| 226 |
-
out_ref = gr.Image(label='Masked Reference')
|
| 227 |
-
out_morph = gr.Image(label='Masked Morphed First Frame')
|
| 228 |
|
| 229 |
btn.click(
|
| 230 |
fn=process_video,
|
| 231 |
-
inputs=[vid, ref,
|
| 232 |
-
outputs=[out_vid
|
| 233 |
show_progress=True
|
| 234 |
)
|
| 235 |
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
# Enable queueing so progress updates render
|
| 239 |
-
iface.queue().launch(debug=True)
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
from faceflux import process_video
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
css = """video { object-fit: contain !important; }"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
with gr.Blocks(css=css) as iface:
|
| 7 |
+
gr.Markdown("# Super Fast Face Swap – FACEFLUX")
|
| 8 |
+
gr.Markdown(
|
| 9 |
+
"**FACEFLUX**: Ultra-lightweight, CPU-only face swap. "
|
| 10 |
+
"Ideal for small or distant faces; offline & privacy-preserving. "
|
| 11 |
+
"Weakness: large up-close or extreme angles."
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
with gr.Row():
|
| 15 |
+
vid = gr.Video(label="Input Video")
|
| 16 |
+
ref = gr.Image(type="numpy", label="Reference Image")
|
| 17 |
+
|
| 18 |
with gr.Row():
|
| 19 |
+
res = gr.Dropdown([256,384,512,768], value=512, label="Resolution")
|
| 20 |
+
quality = gr.Slider(1,4, value=1, step=1, label="Swap Quality")
|
| 21 |
+
feather = gr.Slider(0.12, 0.24, value=0.12, step=0.01, label="Feather (%)")
|
| 22 |
+
strength = gr.Slider(-0.35, -0.15, value=-0.25, step=0.05, label="Strength")
|
| 23 |
+
|
| 24 |
+
btn = gr.Button("Generate Morph 🚀")
|
| 25 |
+
out_vid = gr.Video(label="Morphed Video")
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
btn.click(
|
| 28 |
fn=process_video,
|
| 29 |
+
inputs=[vid, ref, strength, res, quality, feather, 0.24],
|
| 30 |
+
outputs=[out_vid],
|
| 31 |
show_progress=True
|
| 32 |
)
|
| 33 |
|
| 34 |
+
iface.queue().launch(debug=True)
|
|
|
|
|
|
|
|
|