NihalGazi commited on
Commit
4c06029
·
verified ·
1 Parent(s): 4ad58c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +83 -225
app.py CHANGED
@@ -7,15 +7,12 @@ import tempfile
7
  import os
8
 
9
  # --- MediaPipe Initialization ---
10
- # Use try-except block for robustness if mediapipe is not installed correctly
11
  try:
12
  mp_face_mesh = mp.solutions.face_mesh
13
- # NOTE: refine_landmarks=True gives 478 landmarks. False gives 468.
14
- # We will control density by sub-sampling rather than this boolean for more control.
15
  face_mesh = mp_face_mesh.FaceMesh(
16
  static_image_mode=True,
17
  max_num_faces=1,
18
- refine_landmarks=True, # Keep this on for the best potential quality
19
  min_detection_confidence=0.5
20
  )
21
  print("MediaPipe Face Mesh initialized successfully.")
@@ -26,284 +23,145 @@ except (ImportError, AttributeError):
26
  # --- Helper Functions ---
27
 
28
  def get_landmarks(img, landmark_step=1):
29
- """
30
- Detects face landmarks using MediaPipe Face Mesh.
31
- Includes sub-sampling for performance.
32
- - landmark_step: Step to sample landmarks. 1 = all, 2 = half, etc.
33
- """
34
  if img is None:
35
- print("Warning: Input image is None in get_landmarks.")
36
  return None
37
  if face_mesh is None:
38
- print("Error: MediaPipe Face Mesh not available.")
39
  return None
40
-
41
  img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
42
  try:
43
  results = face_mesh.process(img_rgb)
44
- except Exception as e:
45
- print(f"Error processing image with MediaPipe: {e}")
46
  return None
47
-
48
  if not results.multi_face_landmarks:
49
- print("Warning: No face detected.")
50
  return None
51
-
52
  landmarks_mp = results.multi_face_landmarks[0]
53
  h, w, _ = img.shape
54
-
55
- # Get all landmarks first
56
  full_landmarks = np.array([(pt.x * w, pt.y * h) for pt in landmarks_mp.landmark], dtype=np.float32)
57
-
58
- # --- NEW: Sub-sample landmarks for speed ---
59
- if landmark_step > 1:
60
- # Sample with a step, ensuring correspondence is maintained between faces
61
- landmarks = full_landmarks[::landmark_step]
62
- else:
63
- landmarks = full_landmarks
64
-
65
  if not np.all(np.isfinite(landmarks)):
66
- print("Warning: Invalid landmark coordinates detected (NaN/inf).")
67
  return None
 
 
68
 
69
- corners = np.array([
70
- [0, 0], [w - 1, 0], [0, h - 1], [w - 1, h - 1]
71
- ], dtype=np.float32)
72
-
73
- # Always include corners for stable warping
74
- landmarks = np.vstack((landmarks, corners))
75
-
76
- return landmarks
77
 
78
  def calculate_delaunay_triangles(rect, points):
79
- """Calculates Delaunay triangulation for a set of points. (No changes needed here)"""
80
  if points is None or len(points) < 3:
81
  return []
82
- if not np.all(np.isfinite(points)):
83
- points = points[np.all(np.isfinite(points), axis=1)]
84
- if len(points) < 3: return []
85
-
86
  points[:, 0] = np.clip(points[:, 0], rect[0], rect[0] + rect[2] - 1)
87
  points[:, 1] = np.clip(points[:, 1], rect[1], rect[1] + rect[3] - 1)
88
-
89
  subdiv = cv2.Subdiv2D(rect)
90
- point_map = { (int(p[0]), int(p[1])): i for i, p in enumerate(points) }
91
- inserted_points_map = {}
92
-
93
  for i, p in enumerate(points):
94
- point_tuple = (int(p[0]), int(p[1]))
95
- if point_tuple not in inserted_points_map:
96
  try:
97
- subdiv.insert(point_tuple)
98
- inserted_points_map[point_tuple] = i
99
  except cv2.error:
100
  continue
101
-
102
- triangle_list = subdiv.getTriangleList()
103
- delaunay_triangles = []
104
- for t in triangle_list:
105
- pts_coords = [(int(t[0]), int(t[1])), (int(t[2]), int(t[3])), (int(t[4]), int(t[5]))]
106
- if all(rect[0] <= p[0] < rect[0] + rect[2] and rect[1] <= p[1] < rect[1] + rect[3] for p in pts_coords):
107
- indices = [inserted_points_map.get(coord) for coord in pts_coords]
108
- if all(idx is not None for idx in indices) and len(set(indices)) == 3:
109
- delaunay_triangles.append(indices)
110
- return delaunay_triangles
111
 
112
 
113
  def warp_triangle(img1, img2, t1, t2):
114
- """Warps a triangle from img1 to img2. (No changes needed here)"""
115
- if len(t1) != 3 or len(t2) != 3 or not np.all(np.isfinite(t1)) or not np.all(np.isfinite(t2)):
116
  return
117
- try:
118
- r1 = cv2.boundingRect(np.float32([t1]))
119
- r2 = cv2.boundingRect(np.float32([t2]))
120
-
121
- if r1[2] <= 0 or r1[3] <= 0 or r2[2] <= 0 or r2[3] <= 0: return
122
-
123
- t1_rect = [(t1[i][0] - r1[0], t1[i][1] - r1[1]) for i in range(3)]
124
- t2_rect = [(t2[i][0] - r2[0], t2[i][1] - r2[1]) for i in range(3)]
125
-
126
- mask = np.zeros((r2[3], r2[2], 3), dtype=np.float32)
127
- cv2.fillConvexPoly(mask, np.int32(t2_rect), (1.0, 1.0, 1.0), 16, 0)
128
-
129
- img1_rect = img1[r1[1]:r1[1] + r1[3], r1[0]:r1[0] + r1[2]]
130
- if img1_rect.size == 0: return
131
-
132
- size = (r2[2], r2[3])
133
- warp_mat = cv2.getAffineTransform(np.float32(t1_rect), np.float32(t2_rect))
134
- img2_rect = cv2.warpAffine(img1_rect, warp_mat, size, None, flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
135
-
136
- img2_rect *= mask
137
-
138
- y_start, y_end = r2[1], r2[1] + r2[3]
139
- x_start, x_end = r2[0], r2[0] + r2[2]
140
-
141
- h_img2, w_img2, _ = img2.shape
142
- if y_start >= h_img2 or x_start >= w_img2: return
143
 
144
- img2[y_start:y_end, x_start:x_end] = img2[y_start:y_end, x_start:x_end] * (1.0 - mask) + img2_rect
145
- except (cv2.error, IndexError):
146
- pass # Ignore degenerate triangles or slicing errors
147
 
148
- # --- Main Morphing Function (Modified) ---
149
  def morph_faces(img1_orig, img2_orig, alpha, resize_dim, landmark_step):
150
- """
151
- Morphs two faces with a seamless blending strategy to avoid artifacts.
152
- """
153
- start_time = time.time()
154
  if img1_orig is None or img2_orig is None:
155
  return np.zeros((resize_dim, resize_dim, 3), dtype=np.uint8)
156
-
157
- # --- Preprocessing with dynamic resize_dim ---
158
- try:
159
- img1 = cv2.resize(img1_orig, (resize_dim, resize_dim), interpolation=cv2.INTER_LINEAR)
160
- img2 = cv2.resize(img2_orig, (resize_dim, resize_dim), interpolation=cv2.INTER_LINEAR)
161
- except cv2.error:
162
- return np.zeros((resize_dim, resize_dim, 3), dtype=np.uint8)
163
-
164
- h, w, _ = img1.shape
165
- rect = (0, 0, w, h)
166
-
167
- # --- Landmark Detection with dynamic landmark_step ---
168
  landmarks1 = get_landmarks(img1, landmark_step)
169
  landmarks2 = get_landmarks(img2, landmark_step)
170
-
171
  if landmarks1 is None or landmarks2 is None or landmarks1.shape != landmarks2.shape:
172
- print("Landmark error. Falling back to simple alpha blend.")
173
- return cv2.addWeighted(img1, 1 - alpha, img2, alpha, 0)
174
-
175
- # --- Landmark Interpolation (determines the shape of the output face) ---
176
- landmarks_morphed = (1 - alpha) * landmarks1 + alpha * landmarks2
177
-
178
- # --- Triangulation (based on the final morphed shape) ---
179
- try:
180
- triangles_indices = calculate_delaunay_triangles(rect, landmarks_morphed.copy())
181
- if not triangles_indices:
182
- print("Triangulation failed. Falling back to simple alpha blend.")
183
- return cv2.addWeighted(img1, 1 - alpha, img2, alpha, 0)
184
- except Exception as e:
185
- print(f"Error during triangulation: {e}. Falling back to simple alpha blend.")
186
- return cv2.addWeighted(img1, 1 - alpha, img2, alpha, 0)
187
-
188
-
189
- # --- SEAMLESS WARPING AND BLENDING ---
190
-
191
- # 1. Create two empty canvases for the fully warped images
192
- img1_float = img1.astype(np.float32) / 255.0
193
- img2_float = img2.astype(np.float32) / 255.0
194
- img1_warped = np.zeros_like(img1_float)
195
- img2_warped = np.zeros_like(img2_float)
196
-
197
- # 2. Warp triangles from each source to their morphed positions on the respective canvases
198
- for indices in triangles_indices:
199
- if any(idx >= len(landmarks1) for idx in indices): continue # Safety check
200
-
201
- # Get triangle vertices for source 1, source 2, and the morphed shape
202
- t1 = landmarks1[indices]
203
- t2 = landmarks2[indices]
204
- t_morphed = landmarks_morphed[indices]
205
-
206
- # Warp the triangle from img1 to the morphed position on the img1_warped canvas
207
- warp_triangle(img1_float, img1_warped, t1, t_morphed)
208
-
209
- # Warp the triangle from img2 to the morphed position on the img2_warped canvas
210
- warp_triangle(img2_float, img2_warped, t2, t_morphed)
211
 
212
- # 3. Perform a single, final alpha blend of the two completed warped images
213
- morphed_img_float = (1.0 - alpha) * img1_warped + alpha * img2_warped
214
-
215
- # --- Final Conversion ---
216
- morphed_img = (morphed_img_float * 255.0).clip(0, 255).astype(np.uint8)
217
- end_time = time.time()
218
- print(f"Frame morph ({w}x{h}, {len(landmarks1)} landmarks) took: {end_time - start_time:.4f}s")
219
- return morphed_img
220
 
221
- # --- Video Processing Function (Modified) ---
222
  def process_video(video_path, target_img, transition_level, resolution, landmark_sampling):
223
- """
224
- Callback function that now receives resolution and landmark settings from the UI.
225
- """
226
-
227
- target_img = cv2.cvtColor(target_img, cv2.COLOR_RGB2BGR)
228
-
229
  if video_path is None or target_img is None:
230
- # Create a dummy video to avoid Gradio errors on empty inputs
231
- dummy_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
232
- fourcc = cv2.VideoWriter_fourcc(*'mp4v')
233
- out = cv2.VideoWriter(dummy_path, fourcc, 24, (resolution, resolution))
234
  out.release()
235
- return dummy_path
236
-
237
- alpha = (transition_level + 1.0) / 2.0
238
- alpha = float(np.clip(alpha, 0.0, 1.0))
239
-
240
  cap = cv2.VideoCapture(video_path)
241
- if not cap.isOpened():
242
- raise IOError(f"Cannot open video file: {video_path}")
243
-
244
  fps = cap.get(cv2.CAP_PROP_FPS) or 24
245
- fourcc = cv2.VideoWriter_fourcc(*"mp4v")
246
- tmp_out = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
247
-
248
- # --- Use dynamic resolution for the output video ---
249
- out = cv2.VideoWriter(tmp_out.name, fourcc, fps, (resolution, resolution))
250
-
251
- frame_count = 0
252
  while True:
253
  ret, frame = cap.read()
254
- if not ret:
255
- break
256
-
257
- # Pass the new parameters to the morphing function
258
- morphed = morph_faces(frame, target_img, alpha, resolution, landmark_sampling)
259
- out.write(morphed)
260
- frame_count += 1
261
-
262
- print(f"Processed {frame_count} frames.")
263
- cap.release()
264
- out.release()
265
- return tmp_out.name
266
 
267
- # --- Gradio App (Modified) ---
268
  css = """video, img { object-fit: contain !important; }"""
269
  with gr.Blocks(css=css) as iface:
270
  gr.Markdown("# Real-Time Video Face Morph 🚀")
271
- gr.Markdown("Adjust resolution and landmark density for a trade-off between speed and quality.")
272
  with gr.Row():
273
  video_input = gr.Video(label="Input Video")
274
  img_input = gr.Image(type="numpy", label="Target Face Image")
275
-
276
  with gr.Row():
277
- # --- NEW: UI controls for performance ---
278
- resolution_slider = gr.Dropdown(
279
- [256, 384, 512, 768],
280
- value=512,
281
- label="Processing Resolution",
282
- info="Lower resolution means much faster processing."
283
- )
284
- landmark_slider = gr.Slider(
285
- 1, 4,
286
- value=1,
287
- step=1,
288
- label="Landmark Sub-sampling",
289
- info="1=Max Quality (~478 landmarks), 4=Max Speed (~120 landmarks)"
290
- )
291
-
292
- slider = gr.Slider(-1.0, 1.0, value=0.0, step=0.05, label="Transition Level (-1 = Video, 1 = Image)")
293
  video_output = gr.Video(label="Morphed Video")
294
-
295
- # Gather all input components
296
- inputs = [video_input, img_input, slider, resolution_slider, landmark_slider]
297
-
298
- # Trigger processing on any input change
299
- for component in inputs:
300
- component.change(
301
- fn=process_video,
302
- inputs=inputs,
303
- outputs=video_output,
304
- show_progress="full"
305
- )
306
  gr.Markdown("---\n*Built with Gradio, OpenCV & MediaPipe.*")
307
 
308
  if __name__ == "__main__":
309
- iface.launch(debug=True)
 
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.")
 
23
  # --- Helper Functions ---
24
 
25
  def get_landmarks(img, landmark_step=1):
 
 
 
 
 
26
  if img is None:
 
27
  return None
28
  if face_mesh is None:
 
29
  return None
 
30
  img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
31
  try:
32
  results = face_mesh.process(img_rgb)
33
+ except Exception:
 
34
  return None
 
35
  if not results.multi_face_landmarks:
 
36
  return None
 
37
  landmarks_mp = results.multi_face_landmarks[0]
38
  h, w, _ = img.shape
 
 
39
  full_landmarks = np.array([(pt.x * w, pt.y * h) for pt in landmarks_mp.landmark], dtype=np.float32)
40
+ landmarks = full_landmarks[::landmark_step] if landmark_step > 1 else full_landmarks
 
 
 
 
 
 
 
41
  if not np.all(np.isfinite(landmarks)):
 
42
  return None
43
+ corners = np.array([[0, 0], [w - 1, 0], [0, h - 1], [w - 1, h - 1]], dtype=np.float32)
44
+ return np.vstack((landmarks, corners))
45
 
 
 
 
 
 
 
 
 
46
 
47
  def calculate_delaunay_triangles(rect, points):
 
48
  if points is None or len(points) < 3:
49
  return []
 
 
 
 
50
  points[:, 0] = np.clip(points[:, 0], rect[0], rect[0] + rect[2] - 1)
51
  points[:, 1] = np.clip(points[:, 1], rect[1], rect[1] + rect[3] - 1)
 
52
  subdiv = cv2.Subdiv2D(rect)
53
+ inserted = {}
 
 
54
  for i, p in enumerate(points):
55
+ tup = (int(p[0]), int(p[1]))
56
+ if tup not in inserted:
57
  try:
58
+ subdiv.insert(tup)
59
+ inserted[tup] = i
60
  except cv2.error:
61
  continue
62
+ triangles = subdiv.getTriangleList()
63
+ delaunay = []
64
+ for t in triangles:
65
+ coords = [(int(t[0]), int(t[1])), (int(t[2]), int(t[3])), (int(t[4]), int(t[5]))]
66
+ if all(rect[0] <= x < rect[0] + rect[2] and rect[1] <= y < rect[1] + rect[3] for x, y in coords):
67
+ idxs = [inserted.get(c) for c in coords]
68
+ if all(i is not None for i in idxs) and len(set(idxs)) == 3:
69
+ delaunay.append(idxs)
70
+ return delaunay
 
71
 
72
 
73
  def warp_triangle(img1, img2, t1, t2):
74
+ if len(t1) != 3 or len(t2) != 3:
 
75
  return
76
+ r1 = cv2.boundingRect(np.float32([t1]))
77
+ r2 = cv2.boundingRect(np.float32([t2]))
78
+ if r1[2] == 0 or r1[3] == 0 or r2[2] == 0 or r2[3] == 0:
79
+ return
80
+ t1_rect = [(t1[i][0] - r1[0], t1[i][1] - r1[1]) for i in range(3)]
81
+ t2_rect = [(t2[i][0] - r2[0], t2[i][1] - r2[1]) for i in range(3)]
82
+ mask = np.zeros((r2[3], r2[2], 3), dtype=np.float32)
83
+ cv2.fillConvexPoly(mask, np.int32(t2_rect), (1.0, 1.0, 1.0), 16, 0)
84
+ img1_rect = img1[r1[1]:r1[1]+r1[3], r1[0]:r1[0]+r1[2]]
85
+ if img1_rect.size == 0:
86
+ return
87
+ warp_mat = cv2.getAffineTransform(np.float32(t1_rect), np.float32(t2_rect))
88
+ img2_rect = cv2.warpAffine(img1_rect, warp_mat, (r2[2], r2[3]), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
89
+ img2_rect *= mask
90
+ y1, y2 = r2[1], r2[1] + r2[3]
91
+ x1, x2 = r2[0], r2[0] + r2[2]
92
+ img2[y1:y2, x1:x2] = img2[y1:y2, x1:x2] * (1 - mask) + img2_rect
 
 
 
 
 
 
 
 
 
93
 
 
 
 
94
 
 
95
  def morph_faces(img1_orig, img2_orig, alpha, resize_dim, landmark_step):
 
 
 
 
96
  if img1_orig is None or img2_orig is None:
97
  return np.zeros((resize_dim, resize_dim, 3), dtype=np.uint8)
98
+ img1 = cv2.resize(img1_orig, (resize_dim, resize_dim))
99
+ img2 = cv2.resize(img2_orig, (resize_dim, resize_dim))
 
 
 
 
 
 
 
 
 
 
100
  landmarks1 = get_landmarks(img1, landmark_step)
101
  landmarks2 = get_landmarks(img2, landmark_step)
 
102
  if landmarks1 is None or landmarks2 is None or landmarks1.shape != landmarks2.shape:
103
+ return cv2.addWeighted(img1, 1-alpha, img2, alpha, 0)
104
+ morphed_pts = (1-alpha)*landmarks1 + alpha*landmarks2
105
+ rect = (0, 0, resize_dim, resize_dim)
106
+ tris = calculate_delaunay_triangles(rect, morphed_pts)
107
+ if not tris:
108
+ return cv2.addWeighted(img1, 1-alpha, img2, alpha, 0)
109
+ img1_f = img1.astype(np.float32)/255.0
110
+ img2_f = img2.astype(np.float32)/255.0
111
+ w1 = np.zeros_like(img1_f)
112
+ w2 = np.zeros_like(img2_f)
113
+ for ids in tris:
114
+ t1 = landmarks1[ids]; t2 = landmarks2[ids]; tm = morphed_pts[ids]
115
+ warp_triangle(img1_f, w1, t1, tm)
116
+ warp_triangle(img2_f, w2, t2, tm)
117
+ morph = (1-alpha)*w1 + alpha*w2
118
+ return (morph*255).astype(np.uint8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
 
 
 
 
 
 
 
 
120
 
 
121
  def process_video(video_path, target_img, transition_level, resolution, landmark_sampling):
 
 
 
 
 
 
122
  if video_path is None or target_img is None:
123
+ dummy = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
124
+ out = cv2.VideoWriter(dummy, cv2.VideoWriter_fourcc(*'mp4v'), 24, (resolution, resolution))
 
 
125
  out.release()
126
+ return dummy
127
+ target_bgr = cv2.cvtColor(target_img, cv2.COLOR_RGB2BGR)
128
+ alpha = float(np.clip((transition_level+1)/2,0,1))
 
 
129
  cap = cv2.VideoCapture(video_path)
 
 
 
130
  fps = cap.get(cv2.CAP_PROP_FPS) or 24
131
+ out_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
132
+ out = cv2.VideoWriter(out_file, cv2.VideoWriter_fourcc(*'mp4v'), fps, (resolution, resolution))
 
 
 
 
 
133
  while True:
134
  ret, frame = cap.read()
135
+ if not ret: break
136
+ mor = morph_faces(frame, target_bgr, alpha, resolution, landmark_sampling)
137
+ out.write(mor)
138
+ cap.release(); out.release()
139
+ return out_file
 
 
 
 
 
 
 
140
 
141
+ # --- Gradio App ---
142
  css = """video, img { object-fit: contain !important; }"""
143
  with gr.Blocks(css=css) as iface:
144
  gr.Markdown("# Real-Time Video Face Morph 🚀")
145
+ gr.Markdown("Use the button below to generate and show a progress bar during processing.")
146
  with gr.Row():
147
  video_input = gr.Video(label="Input Video")
148
  img_input = gr.Image(type="numpy", label="Target Face Image")
 
149
  with gr.Row():
150
+ resolution_slider = gr.Dropdown([256,384,512,768], value=512, label="Resolution")
151
+ landmark_slider = gr.Slider(1,4,value=1,step=1, label="Landmark Sub-sampling")
152
+ transition_slider = gr.Slider(-1.0,1.0,value=0.0,step=0.05, label="Transition Level")
153
+ generate_btn = gr.Button("Generate Morph 🚀", variant="primary")
154
+ progress_bar = gr.Progress()
 
 
 
 
 
 
 
 
 
 
 
155
  video_output = gr.Video(label="Morphed Video")
156
+
157
+ generate_btn.click(
158
+ fn=process_video,
159
+ inputs=[video_input, img_input, transition_slider, resolution_slider, landmark_slider],
160
+ outputs=video_output,
161
+ show_progress=True
162
+ )
163
+
 
 
 
 
164
  gr.Markdown("---\n*Built with Gradio, OpenCV & MediaPipe.*")
165
 
166
  if __name__ == "__main__":
167
+ iface.launch(debug=True)