NihalGazi commited on
Commit
820fdb5
·
verified ·
1 Parent(s): 65f437b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +217 -192
app.py CHANGED
@@ -6,263 +6,288 @@ import time
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=False,
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)
268
-
 
6
  import tempfile
7
  import os
8
 
9
+ # --- MediaPipe Initialization for Video ---
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=False, # <-- The most important change for video
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 for VIDEO successfully.")
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 (Refactored for Efficiency) ---
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 get_face_mask_box_from_landmarks(landmarks, img_shape, feather, padding=0):
42
+ """Generates a face mask and bounding box from pre-computed landmarks."""
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
+ x2_pad = min(x + bw + padding, w)
57
+ y2_pad = min(y + bh + padding, h)
 
 
 
 
 
 
58
 
59
+ # Feather the mask for smoother blending
60
+ if feather > 0:
61
+ k = int(feather)
62
+ if k % 2 == 0: k += 1 # Kernel size must be odd
63
+ mask = cv2.GaussianBlur(mask, (k, k), 0)
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
+ """Calculates Delaunay triangles for a set of points."""
72
+ if points is None or len(points) < 3:
73
  return []
 
 
74
  subdiv = cv2.Subdiv2D(rect)
75
+ # Using a dictionary is faster for checking existing points
76
+ point_map = { (int(p[0]), int(p[1])): i for i, p in enumerate(points) }
77
+ for p in point_map.keys():
78
+ subdiv.insert(p)
79
+
80
+ triangle_list = subdiv.getTriangleList()
81
+ delaunay_triangles = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ for t in triangle_list:
84
+ pt1 = (int(t[0]), int(t[1]))
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
+ def warp_triangle(img1, img2, t1, t2):
105
+ """Warps a triangle from img1 to img2."""
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
+ img1_cropped = img1[r1[1]:r1[1]+r1[3], r1[0]:r1[0]+r1[2]]
114
+
115
+ # Get affine transform
116
+ warp_mat = cv2.getAffineTransform(t1_rect, t2_rect)
117
+ img2_cropped = cv2.warpAffine(img1_cropped, warp_mat, (r2[2], r2[3]), None,
118
+ flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
119
+
120
+ # Create mask for blending
121
+ mask = np.zeros((r2[3], r2[2]), dtype=np.uint8)
122
+ cv2.fillConvexPoly(mask, t2_rect.astype(np.int32), 255)
123
+
124
+ # Blend the warped triangle
125
+ img2_rect = img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]]
126
+ img2_rect[mask > 0] = img2_cropped[mask > 0]
127
+
128
+
129
+ def morph_faces(img1, img2, l1, l2, alpha, dim):
130
+ """
131
+ Morphs two faces using pre-computed landmarks.
132
+ This function no longer performs landmark detection itself.
133
+ """
134
+ # Create intermediate landmarks
135
+ morphed_landmarks = (1 - alpha) * l1 + alpha * l2
136
+
137
+ # Triangulate the intermediate mesh
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
+ # Get triangles from each set of landmarks
148
+ t1 = l1[ids].astype(np.float32)
149
+ t2 = l2[ids].astype(np.float32)
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
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
174
 
175
+ # === 1. Process Reference Image ONCE before the loop ===
176
  ref_bgr = cv2.cvtColor(ref_img, cv2.COLOR_RGB2BGR)
177
+ ref_results = face_mesh.process(ref_bgr)
178
+ ref_landmarks_full = get_landmarks_from_result(ref_results, ref_bgr.shape)
179
+ if ref_landmarks_full is None:
180
+ raise gr.Error("No face detected in the reference image. Please use a clear photo.")
181
+
182
+ _, ref_box = get_face_mask_box_from_landmarks(ref_landmarks_full, ref_bgr.shape, feather, padding)
183
+ xr, yr, wr, hr = ref_box
184
+ ref_cut = ref_bgr[yr:yr+hr, xr:xr+wr]
185
+ ref_morph_in = cv2.resize(ref_cut, (res, res))
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
+ # === 3. Process Video Frame by Frame ===
202
+ for i in range(total_frames):
203
  ret, frame = cap.read()
204
  if not ret: break
205
+
206
+ # Timestamp is crucial for tracking in video mode
207
+ timestamp_ms = int(cap.get(cv2.CAP_PROP_POS_MSEC))
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
+ # Scale source landmarks to the 'res x res' morphing space
225
+ src_landmarks_scaled = (src_landmarks_full - [x, y]) * [res/w, res/h]
226
+ src_landmarks_scaled = src_landmarks_scaled[::step] # Apply sub-sampling
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
+ first_morphed = morphed_face.copy()
236
+
237
+ # Resize morphed face back and blend it onto the original frame
238
+ morphed_face_resized = cv2.resize(morphed_face, (w, h))
239
+
240
+ # Create a feathered mask for seamless blending
241
+ mask_norm = mask_roi.astype(np.float32) / 255.0
242
+ mask_expanded = mask_norm[..., None] # Add channel dimension for broadcasting
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
+ # Convert preview images for Gradio output
255
+ first_crop_rgb = cv2.cvtColor(first_crop, cv2.COLOR_BGR2RGB) if first_crop is not None else np.zeros((res,res,3), np.uint8)
256
+ ref_morph_in_rgb = cv2.cvtColor(ref_morph_in, cv2.COLOR_BGR2RGB)
257
+ first_morphed_rgb = cv2.cvtColor(first_morphed, cv2.COLOR_BGR2RGB) if first_morphed is not None else np.zeros((res,res,3), np.uint8)
258
+
259
+ return tmp_vid, first_crop_rgb, ref_morph_in_rgb, first_morphed_rgb
260
 
261
  # --- Gradio App ---
262
  css = """video, img { object-fit: contain !important; }"""
263
+ with gr.Blocks(css=css, theme=gr.themes.Soft()) as iface:
264
+ gr.Markdown("# Optimized Face Morphing ")
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, 384, 512], value=384, label='Morph Resolution')
271
+ step = gr.Slider(1, 4, value=4, step=1, label='Landmark Sub-sampling', info="Higher value is faster but less detailed.")
272
+ feather = gr.Slider(0, 50, value=15, step=1, label='Feather Radius', info="Softens the blend edge.")
273
+ padding = gr.Slider(0, 100, value=25, step=1, label='Crop Padding (px)', info="Expands the face area.")
274
+ trans = gr.Slider(-1.0, 1.0, value=-0.3, step=0.05, label='Morph Transition', info="-1.0 is original face, 1.0 is reference face.")
275
+
276
+ btn = gr.Button('Generate Morph 🚀', variant='primary')
277
+
278
+ with gr.Row():
279
+ out_vid = gr.Video(label='Morphed Video')
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, ref, trans, res, step, feather, padding],
288
+ outputs=[out_vid, out_crop, out_ref, out_morph],
289
+ show_progress='full'
290
  )
 
291
 
292
+ if __name__ == '__main__':
293
  iface.launch(debug=True)