Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -6,288 +6,262 @@ import time
|
|
6 |
import tempfile
|
7 |
import os
|
8 |
|
9 |
-
# --- MediaPipe Initialization
|
10 |
-
# Optimized for video by setting static_image_mode to False
|
11 |
-
# This enables tracking and significantly improves speed and stability.
|
12 |
try:
|
13 |
mp_face_mesh = mp.solutions.face_mesh
|
14 |
face_mesh = mp_face_mesh.FaceMesh(
|
15 |
-
static_image_mode=
|
16 |
max_num_faces=1,
|
17 |
refine_landmarks=True,
|
18 |
-
min_detection_confidence=0.5
|
19 |
-
min_tracking_confidence=0.5 # Confidence for tracking across frames
|
20 |
)
|
21 |
-
print("MediaPipe Face Mesh initialized
|
22 |
except (ImportError, AttributeError):
|
23 |
print("Error: Could not initialize MediaPipe Face Mesh. Is mediapipe installed correctly?")
|
24 |
face_mesh = None
|
25 |
|
26 |
-
# --- Helper Functions
|
27 |
|
28 |
-
def get_landmarks_from_result(results, img_shape):
|
29 |
-
"""Extracts landmarks from a MediaPipe results object."""
|
30 |
-
if not results or not results.multi_face_landmarks:
|
31 |
-
return None
|
32 |
-
h, w = img_shape[:2]
|
33 |
-
# Note: Using a NumPy array directly is faster than list comprehensions for this.
|
34 |
-
landmarks = np.array([(lm.x * w, lm.y * h) for lm in results.multi_face_landmarks[0].landmark], dtype=np.float32)
|
35 |
-
|
36 |
-
# Add image corners to landmarks for robust triangulation
|
37 |
-
corners = np.array([[0, 0], [w - 1, 0], [0, h - 1], [w - 1, h - 1]], dtype=np.float32)
|
38 |
-
return np.vstack((landmarks, corners))
|
39 |
|
40 |
|
41 |
-
def
|
42 |
-
|
43 |
-
h, w = img_shape[:2]
|
44 |
-
# We only need the facial landmarks, not the corners we added
|
45 |
-
face_landmarks = landmarks[:-4]
|
46 |
-
|
47 |
-
hull = cv2.convexHull(face_landmarks.astype(np.int32))
|
48 |
mask = np.zeros((h, w), dtype=np.uint8)
|
|
|
|
|
|
|
|
|
|
|
49 |
cv2.fillConvexPoly(mask, hull, 255)
|
50 |
-
|
51 |
x, y, bw, bh = cv2.boundingRect(hull)
|
52 |
-
|
53 |
-
# Apply padding
|
54 |
x_pad = max(x - padding, 0)
|
55 |
y_pad = max(y - padding, 0)
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
#
|
60 |
-
if feather > 0:
|
61 |
k = int(feather)
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
-
box = (x_pad, y_pad, x2_pad - x_pad, y2_pad - y_pad)
|
66 |
-
mask_roi = mask[y_pad:y2_pad, x_pad:x2_pad]
|
67 |
-
|
68 |
-
return mask_roi, box
|
69 |
|
70 |
def calculate_delaunay_triangles(rect, points):
|
71 |
-
|
72 |
-
if points is None or len(points) < 3:
|
73 |
return []
|
|
|
|
|
74 |
subdiv = cv2.Subdiv2D(rect)
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
pt2 = (int(t[2]), int(t[3]))
|
86 |
-
pt3 = (int(t[4]), int(t[5]))
|
87 |
-
|
88 |
-
# Check if all points are within the image boundaries
|
89 |
-
if rect[0] <= pt1[0] < rect[0] + rect[2] and rect[1] <= pt1[1] < rect[1] + rect[3] and \
|
90 |
-
rect[0] <= pt2[0] < rect[0] + rect[2] and rect[1] <= pt2[1] < rect[1] + rect[3] and \
|
91 |
-
rect[0] <= pt3[0] < rect[0] + rect[2] and rect[1] <= pt3[1] < rect[1] + rect[3]:
|
92 |
-
|
93 |
-
# Get the indices from our original point list
|
94 |
-
idx1 = point_map.get(pt1)
|
95 |
-
idx2 = point_map.get(pt2)
|
96 |
-
idx3 = point_map.get(pt3)
|
97 |
-
|
98 |
-
if idx1 is not None and idx2 is not None and idx3 is not None:
|
99 |
-
delaunay_triangles.append([idx1, idx2, idx3])
|
100 |
-
|
101 |
-
return delaunay_triangles
|
102 |
|
|
|
|
|
|
|
103 |
|
104 |
-
|
105 |
-
|
106 |
-
r1 = cv2.boundingRect(t1)
|
107 |
-
r2 = cv2.boundingRect(t2)
|
108 |
-
|
109 |
-
# Crop triangle ROI
|
110 |
-
t1_rect = t1 - r1[:2]
|
111 |
-
t2_rect = t2 - r2[:2]
|
112 |
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
tris = calculate_delaunay_triangles((0, 0, dim, dim), morphed_landmarks)
|
139 |
-
if not tris: # If triangulation fails, just cross-fade
|
140 |
-
return cv2.addWeighted(img1, 1 - alpha, img2, alpha, 0)
|
141 |
-
|
142 |
-
morphed_img = np.zeros_like(img1, dtype=np.float32)
|
143 |
-
img1_f = img1.astype(np.float32)
|
144 |
-
img2_f = img2.astype(np.float32)
|
145 |
-
|
146 |
for ids in tris:
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
tm = morphed_landmarks[ids].astype(np.float32)
|
151 |
-
|
152 |
-
# Warp both images to the intermediate mesh
|
153 |
-
warped1 = np.zeros_like(morphed_img)
|
154 |
-
warped2 = np.zeros_like(morphed_img)
|
155 |
-
warp_triangle(img1_f, warped1, t1, tm)
|
156 |
-
warp_triangle(img2_f, warped2, t2, tm)
|
157 |
-
|
158 |
-
# Blend the two warped images
|
159 |
-
morphed_triangle = (1 - alpha) * warped1 + alpha * warped2
|
160 |
-
|
161 |
-
# Add the blended triangle to the final image
|
162 |
-
mask = np.zeros((dim, dim), dtype=np.uint8)
|
163 |
-
cv2.fillConvexPoly(mask, tm.astype(np.int32), 255)
|
164 |
-
morphed_img[mask > 0] = morphed_triangle[mask > 0]
|
165 |
-
|
166 |
-
return np.uint8(morphed_img)
|
167 |
|
168 |
-
# --- Main Video Processing Function (Optimized) ---
|
169 |
|
170 |
def process_video(video_path, ref_img, trans, res, step, feather, padding):
|
171 |
cap = cv2.VideoCapture(video_path)
|
172 |
fps = cap.get(cv2.CAP_PROP_FPS) or 24
|
173 |
-
|
174 |
|
175 |
-
#
|
176 |
ref_bgr = cv2.cvtColor(ref_img, cv2.COLOR_RGB2BGR)
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
# Scale reference landmarks to the 'res x res' morphing space
|
188 |
-
ref_landmarks_scaled = (ref_landmarks_full - [xr, yr]) * [res/wr, res/hr]
|
189 |
-
ref_landmarks_scaled = ref_landmarks_scaled[::step] # Apply sub-sampling
|
190 |
-
|
191 |
-
# === 2. Setup Output Video ===
|
192 |
w_o = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
193 |
h_o = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
194 |
tmp_vid = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
|
195 |
out_vid = cv2.VideoWriter(tmp_vid, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w_o, h_o))
|
196 |
-
|
197 |
-
# Storage for first frame previews
|
198 |
first_crop = None
|
|
|
|
|
199 |
first_morphed = None
|
200 |
|
201 |
-
|
202 |
-
for i in range(total_frames):
|
203 |
ret, frame = cap.read()
|
204 |
if not ret: break
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
# Process frame ONCE
|
210 |
-
results_src = face_mesh.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
211 |
-
|
212 |
-
if not results_src.multi_face_landmarks:
|
213 |
-
out_vid.write(frame) # Write original frame if no face is found
|
214 |
continue
|
215 |
-
|
216 |
-
src_landmarks_full = get_landmarks_from_result(results_src, frame.shape)
|
217 |
-
mask_roi, box = get_face_mask_box_from_landmarks(src_landmarks_full, frame.shape, feather, padding)
|
218 |
x, y, w, h = box
|
219 |
-
|
220 |
-
# Crop and resize source face
|
221 |
crop = frame[y:y+h, x:x+w]
|
222 |
crop_resized = cv2.resize(crop, (res, res))
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
# Perform morph
|
229 |
-
alpha = float(np.clip((trans + 1) / 2, 0, 1))
|
230 |
-
morphed_face = morph_faces(crop_resized, ref_morph_in, src_landmarks_scaled, ref_landmarks_scaled, alpha, res)
|
231 |
-
|
232 |
-
# Store first frame for preview
|
233 |
if i == 0:
|
234 |
first_crop = crop_resized.copy()
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
#
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
# Composite the morphed face
|
245 |
-
region = frame[y:y+h, x:x+w]
|
246 |
-
blended = region * (1 - mask_expanded) + morphed_face_resized * mask_expanded
|
247 |
frame[y:y+h, x:x+w] = blended.astype(np.uint8)
|
248 |
-
|
249 |
out_vid.write(frame)
|
250 |
|
251 |
-
cap.release()
|
252 |
-
out_vid.release()
|
253 |
|
254 |
-
#
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
|
|
|
|
|
|
|
|
|
|
260 |
|
261 |
# --- Gradio App ---
|
262 |
css = """video, img { object-fit: contain !important; }"""
|
263 |
-
with gr.Blocks(css=css
|
264 |
-
gr.Markdown("#
|
265 |
-
gr.Markdown("This version uses MediaPipe's video tracking for a **faster and smoother** result. Jitter is reduced by maintaining landmark context between frames.")
|
266 |
with gr.Row():
|
267 |
vid = gr.Video(label='Input Video')
|
268 |
ref = gr.Image(type='numpy', label='Reference Image')
|
269 |
with gr.Row():
|
270 |
-
res = gr.Dropdown([256,
|
271 |
-
step = gr.Slider(1,
|
272 |
-
feather = gr.Slider(0,
|
273 |
-
padding = gr.Slider(0,
|
274 |
-
trans = gr.Slider(-1.0,
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
with gr.Row():
|
281 |
-
out_crop = gr.Image(label='First Frame Crop')
|
282 |
-
out_ref = gr.Image(label='Reference Face')
|
283 |
-
out_morph = gr.Image(label='Morphed First Frame')
|
284 |
|
285 |
btn.click(
|
286 |
fn=process_video,
|
287 |
-
inputs=[vid,
|
288 |
-
outputs=[out_vid,
|
289 |
-
show_progress=
|
290 |
)
|
|
|
291 |
|
292 |
-
if __name__
|
293 |
-
iface.launch(debug=True)
|
|
|
6 |
import tempfile
|
7 |
import os
|
8 |
|
9 |
+
# --- MediaPipe Initialization ---
|
|
|
|
|
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.5
|
|
|
17 |
)
|
18 |
+
print("MediaPipe Face Mesh initialized successfully.")
|
19 |
except (ImportError, AttributeError):
|
20 |
print("Error: Could not initialize MediaPipe Face Mesh. Is mediapipe installed correctly?")
|
21 |
face_mesh = None
|
22 |
|
23 |
+
# --- Helper Functions ---
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
|
26 |
|
27 |
+
def get_face_mask_box(img, feather, padding=0):
|
28 |
+
h, w = img.shape[:2]
|
|
|
|
|
|
|
|
|
|
|
29 |
mask = np.zeros((h, w), dtype=np.uint8)
|
30 |
+
results = face_mesh.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
31 |
+
if not results.multi_face_landmarks:
|
32 |
+
return None, None
|
33 |
+
pts = np.array([(int(p.x * w), int(p.y * h)) for p in results.multi_face_landmarks[0].landmark], np.int32)
|
34 |
+
hull = cv2.convexHull(pts)
|
35 |
cv2.fillConvexPoly(mask, hull, 255)
|
|
|
36 |
x, y, bw, bh = cv2.boundingRect(hull)
|
37 |
+
# apply padding
|
|
|
38 |
x_pad = max(x - padding, 0)
|
39 |
y_pad = max(y - padding, 0)
|
40 |
+
x2 = min(x + bw + padding, w)
|
41 |
+
y2 = min(y + bh + padding, h)
|
42 |
+
mask_roi = mask[y_pad:y2, x_pad:x2]
|
43 |
+
# inside feather
|
44 |
+
if feather > 0 and mask_roi.size > 0:
|
45 |
k = int(feather)
|
46 |
+
mask_roi = cv2.GaussianBlur(mask_roi, (k*2+1, k*2+1), 0)
|
47 |
+
return mask_roi, (x_pad, y_pad, x2 - x_pad, y2 - y_pad)
|
48 |
+
|
49 |
+
|
50 |
+
def cut_and_feather(img, feather):
|
51 |
+
h, w = img.shape[:2]
|
52 |
+
mask = np.zeros((h, w), dtype=np.uint8)
|
53 |
+
results = face_mesh.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
54 |
+
if not results.multi_face_landmarks:
|
55 |
+
return np.zeros_like(img), None, None
|
56 |
+
pts = np.array([(int(p.x * w), int(p.y * h)) for p in results.multi_face_landmarks[0].landmark], np.int32)
|
57 |
+
hull = cv2.convexHull(pts)
|
58 |
+
cv2.fillConvexPoly(mask, hull, 255)
|
59 |
+
# bounding box
|
60 |
+
x, y, bw, bh = cv2.boundingRect(hull)
|
61 |
+
# feather mask
|
62 |
+
k = int(feather)
|
63 |
+
if k > 0:
|
64 |
+
mask = cv2.GaussianBlur(mask, (k*2+1, k*2+1), 0)
|
65 |
+
# extract face ROI
|
66 |
+
face_roi = img[y:y+bh, x:x+bw]
|
67 |
+
mask_roi = mask[y:y+bh, x:x+bw]
|
68 |
+
# apply mask
|
69 |
+
fg = cv2.bitwise_and(face_roi, face_roi, mask=mask_roi)
|
70 |
+
# prepare alpha
|
71 |
+
alpha = mask_roi.astype(np.float32) / 255.0
|
72 |
+
# composite onto transparent background same size
|
73 |
+
out = (fg.astype(np.float32) * alpha[..., None]).astype(np.uint8)
|
74 |
+
return out, mask_roi, (x, y, bw, bh)
|
75 |
+
|
76 |
+
def get_landmarks(img, landmark_step=1):
|
77 |
+
if img is None or face_mesh is None:
|
78 |
+
return None
|
79 |
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
80 |
+
try:
|
81 |
+
results = face_mesh.process(img_rgb)
|
82 |
+
except Exception:
|
83 |
+
return None
|
84 |
+
if not results.multi_face_landmarks:
|
85 |
+
return None
|
86 |
+
landmarks_mp = results.multi_face_landmarks[0]
|
87 |
+
h, w, _ = img.shape
|
88 |
+
pts = np.array([(pt.x * w, pt.y * h) for pt in landmarks_mp.landmark], dtype=np.float32)
|
89 |
+
landmarks = pts[::landmark_step] if landmark_step > 1 else pts
|
90 |
+
if not np.all(np.isfinite(landmarks)):
|
91 |
+
return None
|
92 |
+
corners = np.array([[0,0],[w-1,0],[0,h-1],[w-1,h-1]], dtype=np.float32)
|
93 |
+
return np.vstack((landmarks, corners))
|
94 |
|
|
|
|
|
|
|
|
|
95 |
|
96 |
def calculate_delaunay_triangles(rect, points):
|
97 |
+
if points is None or len(points)<3:
|
|
|
98 |
return []
|
99 |
+
points[:,0] = np.clip(points[:,0], rect[0], rect[0]+rect[2]-1)
|
100 |
+
points[:,1] = np.clip(points[:,1], rect[1], rect[1]+rect[3]-1)
|
101 |
subdiv = cv2.Subdiv2D(rect)
|
102 |
+
inserted = {}
|
103 |
+
for i,p in enumerate(points):
|
104 |
+
key = (int(p[0]), int(p[1]))
|
105 |
+
if key not in inserted:
|
106 |
+
try:
|
107 |
+
subdiv.insert(key)
|
108 |
+
inserted[key]=i
|
109 |
+
except cv2.error:
|
110 |
+
continue
|
111 |
+
tris = subdiv.getTriangleList()
|
112 |
+
delaunay=[]
|
113 |
+
for t in tris:
|
114 |
+
coords=[(int(t[0]),int(t[1])),(int(t[2]),int(t[3])),(int(t[4]),int(t[5]))]
|
115 |
+
if all(rect[0]<=x<rect[0]+rect[2] and rect[1]<=y<rect[1]+rect[3] for x,y in coords):
|
116 |
+
idxs=[inserted.get(c) for c in coords]
|
117 |
+
if all(i is not None for i in idxs) and len(set(idxs))==3:
|
118 |
+
delaunay.append(idxs)
|
119 |
+
return delaunay
|
120 |
+
|
121 |
+
|
122 |
+
def warp_triangle(img1,img2,t1,t2):
|
123 |
+
if len(t1)!=3 or len(t2)!=3:
|
124 |
+
return
|
125 |
+
r1=cv2.boundingRect(np.float32([t1]))
|
126 |
+
r2=cv2.boundingRect(np.float32([t2]))
|
127 |
|
128 |
+
if r1[2] <= 0 or r1[3] <= 0 or r2[2] <= 0 or r2[3] <= 0:
|
129 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
|
131 |
+
img1_rect = img1[r1[1]:r1[1]+r1[3], r1[0]:r1[0]+r1[2]]
|
132 |
+
if img1_rect.size == 0:
|
133 |
+
return
|
134 |
|
135 |
+
t1r=[(t1[i][0]-r1[0],t1[i][1]-r1[1]) for i in range(3)]
|
136 |
+
t2r=[(t2[i][0]-r2[0],t2[i][1]-r2[1]) for i in range(3)]
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
|
138 |
+
mask=np.zeros((r2[3],r2[2],3),dtype=np.float32)
|
139 |
+
|
140 |
+
cv2.fillConvexPoly(mask,np.int32(t2r),(1,1,1),16)
|
141 |
+
|
142 |
+
src=img1[r1[1]:r1[1]+r1[3],r1[0]:r1[0]+r1[2]]
|
143 |
+
M=cv2.getAffineTransform(np.float32(t1r),np.float32(t2r))
|
144 |
+
warped=cv2.warpAffine(src,M,(r2[2],r2[3]),flags=cv2.INTER_LINEAR,borderMode=cv2.BORDER_REFLECT_101)
|
145 |
+
warped*=mask
|
146 |
+
y1,y2=r2[1],r2[1]+r2[3]; x1,x2=r2[0],r2[0]+r2[2]
|
147 |
+
img2[y1:y2,x1:x2]=img2[y1:y2,x1:x2]*(1-mask)+warped
|
148 |
+
|
149 |
+
|
150 |
+
def morph_faces(img1, img2, alpha, dim, step):
|
151 |
+
if img1 is None or img2 is None:
|
152 |
+
return np.zeros((dim,dim,3),dtype=np.uint8)
|
153 |
+
a=cv2.resize(img1,(dim,dim)); b=cv2.resize(img2,(dim,dim))
|
154 |
+
l1=get_landmarks(a,step); l2=get_landmarks(b,step)
|
155 |
+
if l1 is None or l2 is None or l1.shape!=l2.shape:
|
156 |
+
return cv2.addWeighted(a,1-alpha,b,alpha,0)
|
157 |
+
m=(1-alpha)*l1+alpha*l2
|
158 |
+
tris=calculate_delaunay_triangles((0,0,dim,dim),m)
|
159 |
+
if not tris:
|
160 |
+
return cv2.addWeighted(a,1-alpha,b,alpha,0)
|
161 |
+
A=a.astype(np.float32)/255; B=b.astype(np.float32)/255
|
162 |
+
Wa=np.zeros_like(A); Wb=np.zeros_like(B)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
for ids in tris:
|
164 |
+
warp_triangle(A,Wa,l1[ids],m[ids]); warp_triangle(B,Wb,l2[ids],m[ids])
|
165 |
+
out=(1-alpha)*Wa+alpha*Wb
|
166 |
+
return (out*255).astype(np.uint8)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
|
|
|
168 |
|
169 |
def process_video(video_path, ref_img, trans, res, step, feather, padding):
|
170 |
cap = cv2.VideoCapture(video_path)
|
171 |
fps = cap.get(cv2.CAP_PROP_FPS) or 24
|
172 |
+
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
173 |
|
174 |
+
# Prepare masked reference
|
175 |
ref_bgr = cv2.cvtColor(ref_img, cv2.COLOR_RGB2BGR)
|
176 |
+
mask_ref, ref_box = get_face_mask_box(ref_bgr, feather, padding)
|
177 |
+
if mask_ref is None:
|
178 |
+
return None, None, None, None
|
179 |
+
x_r, y_r, w_r, h_r = ref_box
|
180 |
+
ref_cut = ref_bgr[y_r:y_r+h_r, x_r:x_r+w_r]
|
181 |
+
mask_ref_norm = mask_ref.astype(np.float32)[..., None] / 255.0
|
182 |
+
ref_masked = (ref_cut.astype(np.float32) * mask_ref_norm).astype(np.uint8)
|
183 |
+
ref_morph = cv2.resize(ref_masked, (res, res))
|
184 |
+
|
185 |
+
# Output video setup
|
|
|
|
|
|
|
|
|
|
|
186 |
w_o = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
187 |
h_o = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
188 |
tmp_vid = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
|
189 |
out_vid = cv2.VideoWriter(tmp_vid, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w_o, h_o))
|
190 |
+
|
|
|
191 |
first_crop = None
|
192 |
+
first_mask = None
|
193 |
+
first_ref = None
|
194 |
first_morphed = None
|
195 |
|
196 |
+
for i in range(total):
|
|
|
197 |
ret, frame = cap.read()
|
198 |
if not ret: break
|
199 |
+
mask_roi, box = get_face_mask_box(frame, feather, padding)
|
200 |
+
if mask_roi is None:
|
201 |
+
out_vid.write(frame)
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
continue
|
|
|
|
|
|
|
203 |
x, y, w, h = box
|
204 |
+
# Crop and resize original ROI
|
|
|
205 |
crop = frame[y:y+h, x:x+w]
|
206 |
crop_resized = cv2.resize(crop, (res, res))
|
207 |
+
# Morph
|
208 |
+
alpha = float(np.clip((trans+1)/2, 0, 1))
|
209 |
+
mor = morph_faces(crop_resized, ref_morph, alpha, res, step)
|
210 |
+
# Store first
|
|
|
|
|
|
|
|
|
|
|
|
|
211 |
if i == 0:
|
212 |
first_crop = crop_resized.copy()
|
213 |
+
first_ref = ref_morph.copy()
|
214 |
+
first_mask = cv2.resize(mask_roi, (res, res), interpolation=cv2.INTER_LINEAR)
|
215 |
+
first_morphed = mor.copy()
|
216 |
+
# Resize morphed back
|
217 |
+
mor_back = cv2.resize(mor, (w, h))
|
218 |
+
# Composite with shape mask
|
219 |
+
mask_n = (mask_roi.astype(np.float32)[..., None] / 255.0)
|
220 |
+
region = frame[y:y+h, x:x+w].astype(np.float32)
|
221 |
+
blended = region * (1-mask_n) + mor_back.astype(np.float32) * mask_n
|
|
|
|
|
|
|
222 |
frame[y:y+h, x:x+w] = blended.astype(np.uint8)
|
|
|
223 |
out_vid.write(frame)
|
224 |
|
225 |
+
cap.release(); out_vid.release()
|
|
|
226 |
|
227 |
+
# Apply mask to first_morphed for preview
|
228 |
+
if first_morphed is not None and first_mask is not None:
|
229 |
+
mask_n0 = first_mask.astype(np.float32)[..., None] / 255.0
|
230 |
+
first_morphed = (first_morphed.astype(np.float32) * mask_n0).astype(np.uint8)
|
231 |
+
else:
|
232 |
+
first_morphed = np.zeros((res, res, 3), dtype=np.uint8)
|
233 |
+
first_crop = first_crop if first_crop is not None else np.zeros((res, res,3),np.uint8)
|
234 |
+
first_ref = first_ref if first_ref is not None else ref_morph.copy()
|
235 |
+
|
236 |
+
# Convert for Gradio
|
237 |
+
return tmp_vid, cv2.cvtColor(first_crop, cv2.COLOR_BGR2RGB), cv2.cvtColor(first_ref, cv2.COLOR_BGR2RGB), cv2.cvtColor(first_morphed, cv2.COLOR_BGR2RGB)
|
238 |
|
239 |
# --- Gradio App ---
|
240 |
css = """video, img { object-fit: contain !important; }"""
|
241 |
+
with gr.Blocks(css=css) as iface:
|
242 |
+
gr.Markdown("# Morph with Face-Shaped Composite and Padding")
|
|
|
243 |
with gr.Row():
|
244 |
vid = gr.Video(label='Input Video')
|
245 |
ref = gr.Image(type='numpy', label='Reference Image')
|
246 |
with gr.Row():
|
247 |
+
res = gr.Dropdown([256,384,512,768], value=512, label='Resolution')
|
248 |
+
step = gr.Slider(1,4,value=4,step=1,label='Landmark Sub-sampling')
|
249 |
+
feather = gr.Slider(0,50,value=10,step=1,label='Feather Radius')
|
250 |
+
padding = gr.Slider(0,100,value=24,step=1,label='Crop Padding (px)')
|
251 |
+
trans = gr.Slider(-1.0,1.0,value=-0.35,step=0.05,label='Transition Level')
|
252 |
+
btn = gr.Button('Generate Morph 🚀')
|
253 |
+
out_vid = gr.Video(label='Morphed Video')
|
254 |
+
out_crop = gr.Image(label='First Frame Crop')
|
255 |
+
out_ref = gr.Image(label='Masked Reference')
|
256 |
+
out_morph = gr.Image(label='Masked Morphed First Frame')
|
|
|
|
|
|
|
|
|
257 |
|
258 |
btn.click(
|
259 |
fn=process_video,
|
260 |
+
inputs=[vid,ref,trans,res,step,feather,padding],
|
261 |
+
outputs=[out_vid,out_crop,out_ref,out_morph],
|
262 |
+
show_progress=True
|
263 |
)
|
264 |
+
gr.Markdown("---\n*Added padding to the face crop for better framing.*")
|
265 |
|
266 |
+
if __name__=='__main__':
|
267 |
+
iface.launch(debug=True)
|