LogicGoInfotechSpaces commited on
Commit
f07e524
·
verified ·
1 Parent(s): 25afbcd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +216 -32
app.py CHANGED
@@ -144,7 +144,129 @@ async def log_faceswap_hit(token: str, status: str = "success"):
144
  "timestamp": datetime.utcnow()
145
  })
146
 
147
- # --------------------- Face Swap Pipeline ---------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  swap_lock = threading.Lock()
149
 
150
  def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
@@ -158,30 +280,43 @@ def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
158
  src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
159
  tgt_bgr_full = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
160
 
 
161
  src_faces = face_analysis_app.get(src_bgr)
162
  tgt_faces = face_analysis_app.get(tgt_bgr_full)
163
 
164
  if not src_faces or not tgt_faces:
165
  return None, None, "❌ Face not detected in source or target image"
166
 
167
- def expand_bbox(bbox, img_shape, scale=1.6):
 
 
 
 
 
168
  ih, iw = img_shape[:2]
169
  x1, y1, x2, y2 = map(int, bbox)
170
  w, h = x2 - x1, y2 - y1
171
  cx, cy = x1 + w // 2, y1 + h // 2
172
- new_w, new_h = int(w * scale), int(h * scale)
173
- nx1 = max(0, cx - new_w // 2)
174
- ny1 = max(0, cy - new_h // 2)
175
- nx2 = min(iw, cx + new_w // 2)
176
- ny2 = min(ih, cy + new_h // 2)
 
 
 
 
 
177
  return nx1, ny1, nx2, ny2
178
 
179
  src_face0 = src_faces[0]
180
  tgt_face0 = tgt_faces[0]
181
 
182
- # More accurate source face crop with slight expansion
183
- s_x1, s_y1, s_x2, s_y2 = expand_bbox(src_face0.bbox, src_bgr.shape, scale=1.4)
184
  src_crop = src_bgr[s_y1:s_y2, s_x1:s_x2]
 
 
185
  src_crop_faces = face_analysis_app.get(src_crop)
186
  if src_crop_faces:
187
  src_for_swap = src_crop
@@ -190,43 +325,93 @@ def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
190
  src_for_swap = src_bgr
191
  src_face_for_swap = src_face0
192
 
193
- # More aggressive target crop for precise landmark detection
194
- t_x1, t_y1, t_x2, t_y2 = expand_bbox(tgt_face0.bbox, tgt_bgr_full.shape, scale=1.6)
195
  tgt_crop = tgt_bgr_full[t_y1:t_y2, t_x1:t_x2]
 
 
196
  tgt_crop_faces = face_analysis_app.get(tgt_crop)
197
 
198
  if tgt_crop_faces:
199
  tgt_for_swap = tgt_crop
200
  tgt_face_for_swap = tgt_crop_faces[0]
201
 
 
202
  swapped_crop = swapper.get(tgt_for_swap, tgt_face_for_swap, src_face_for_swap)
203
  if swapped_crop is None:
204
  return None, None, "❌ Face swap failed on crop"
205
 
206
- # Create mask with threshold for seamlessClone
207
- mask = cv2.cvtColor(swapped_crop, cv2.COLOR_BGR2GRAY)
208
- _, mask = cv2.threshold(mask, 1, 255, cv2.THRESH_BINARY)
209
-
210
- center = ((t_x1 + t_x2) // 2, (t_y1 + t_y2) // 2)
211
-
 
 
 
 
 
 
 
 
 
 
 
212
  try:
213
- blended = cv2.seamlessClone(swapped_crop, tgt_bgr_full, mask, center, cv2.NORMAL_CLONE)
214
- except Exception:
215
- # Fallback to direct paste if seamlessClone fails
216
- blended = tgt_bgr_full.copy()
217
- h, w = swapped_crop.shape[:2]
218
- blended[t_y1:t_y1+h, t_x1:t_x1+w] = swapped_crop
219
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
221
- cv2.imwrite(swapped_path, blended)
222
 
223
  else:
224
- # Fallback: swap on full image if crop detection fails
225
  swapped_bgr_full = swapper.get(tgt_bgr_full, tgt_face0, src_face0)
226
  if swapped_bgr_full is None:
227
  return None, None, "❌ Face swap failed on full image"
 
 
 
 
 
 
 
 
 
 
 
 
228
  swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
229
- cv2.imwrite(swapped_path, swapped_bgr_full)
230
 
231
  # Run CodeFormer enhancement on the swapped image
232
  cmd = f"python {CODEFORMER_PATH} -w 0.7 --input_path {swapped_path} --output_path {temp_dir} --bg_upsampler realesrgan --face_upsample"
@@ -245,19 +430,18 @@ def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
245
  return final_img, final_path, ""
246
 
247
  except Exception as e:
 
248
  return None, None, f"❌ Error: {str(e)}"
249
 
250
-
251
-
252
  # --------------------- Gradio ---------------------
253
  with gr.Blocks() as demo:
254
- gr.Markdown("Face Swap")
255
 
256
  with gr.Row():
257
  src_input = gr.Image(type="numpy", label="Upload Your Face")
258
  tgt_input = gr.Image(type="numpy", label="Upload Target Image")
259
 
260
- btn = gr.Button("Swap Face")
261
  output_img = gr.Image(type="numpy", label="Enhanced Output")
262
  download = gr.File(label="⬇️ Download Enhanced Image")
263
  error_box = gr.Textbox(label="Logs / Errors", interactive=False)
@@ -383,4 +567,4 @@ async def preview_result(result_key: str):
383
  fastapi_app = mount_gradio_app(fastapi_app, demo, path="/gradio")
384
 
385
  if __name__ == "__main__":
386
- uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
 
144
  "timestamp": datetime.utcnow()
145
  })
146
 
147
+ # --------------------- Enhanced Face Swap Utilities ---------------------
148
+
149
+ def match_color_histogram(src_face, tgt_face):
150
+ """Match color histogram of source to target for better color consistency"""
151
+ src_lab = cv2.cvtColor(src_face, cv2.COLOR_BGR2LAB)
152
+ tgt_lab = cv2.cvtColor(tgt_face, cv2.COLOR_BGR2LAB)
153
+
154
+ matched_channels = []
155
+ for i in range(3):
156
+ src_channel = src_lab[:, :, i]
157
+ tgt_channel = tgt_lab[:, :, i]
158
+
159
+ src_mean, src_std = cv2.meanStdDev(src_channel)
160
+ tgt_mean, tgt_std = cv2.meanStdDev(tgt_channel)
161
+
162
+ matched_channel = ((src_channel - src_mean) * (tgt_std / (src_std + 1e-6))) + tgt_mean
163
+ matched_channel = np.clip(matched_channel, 0, 255).astype(np.uint8)
164
+ matched_channels.append(matched_channel)
165
+
166
+ matched_lab = cv2.merge(matched_channels)
167
+ return cv2.cvtColor(matched_lab, cv2.COLOR_LAB2BGR)
168
+
169
+ def create_seamless_mask(face_bbox, img_shape, feather_amount=15):
170
+ """Create a smooth feathered mask for seamless blending"""
171
+ mask = np.zeros(img_shape[:2], dtype=np.uint8)
172
+ x1, y1, x2, y2 = map(int, face_bbox)
173
+
174
+ # Create elliptical mask
175
+ center = ((x1 + x2) // 2, (y1 + y2) // 2)
176
+ axes = ((x2 - x1) // 2, (y2 - y1) // 2)
177
+ cv2.ellipse(mask, center, axes, 0, 0, 360, 255, -1)
178
+
179
+ # Apply Gaussian blur for feathering
180
+ mask = cv2.GaussianBlur(mask, (feather_amount*2+1, feather_amount*2+1), 0)
181
+
182
+ return mask
183
+
184
+ def poisson_blend(src, tgt, mask, center):
185
+ """Enhanced Poisson blending with error handling"""
186
+ try:
187
+ # Ensure mask is proper format
188
+ if len(mask.shape) == 3:
189
+ mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
190
+
191
+ # Ensure mask is binary
192
+ _, mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)
193
+
194
+ # Try mixed clone first (better for face swaps)
195
+ result = cv2.seamlessClone(src, tgt, mask, center, cv2.MIXED_CLONE)
196
+ return result
197
+ except Exception as e:
198
+ logger.warning(f"Poisson blend failed: {e}, using alpha blending")
199
+ # Fallback to alpha blending
200
+ mask_3ch = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) / 255.0
201
+ return (src * mask_3ch + tgt * (1 - mask_3ch)).astype(np.uint8)
202
+
203
+ def multi_band_blending(src, tgt, mask, levels=4):
204
+ """Multi-band blending for smoother transitions"""
205
+ # Build Gaussian pyramids
206
+ src_pyr = [src.copy()]
207
+ tgt_pyr = [tgt.copy()]
208
+ mask_pyr = [mask.copy()]
209
+
210
+ for i in range(levels):
211
+ src_pyr.append(cv2.pyrDown(src_pyr[i]))
212
+ tgt_pyr.append(cv2.pyrDown(tgt_pyr[i]))
213
+ mask_pyr.append(cv2.pyrDown(mask_pyr[i]))
214
+
215
+ # Build Laplacian pyramids
216
+ src_lap = [src_pyr[levels]]
217
+ tgt_lap = [tgt_pyr[levels]]
218
+
219
+ for i in range(levels, 0, -1):
220
+ size = (src_pyr[i-1].shape[1], src_pyr[i-1].shape[0])
221
+ src_lap.append(cv2.subtract(src_pyr[i-1], cv2.pyrUp(src_pyr[i], dstsize=size)))
222
+ tgt_lap.append(cv2.subtract(tgt_pyr[i-1], cv2.pyrUp(tgt_pyr[i], dstsize=size)))
223
+
224
+ # Blend each level
225
+ blended = []
226
+ for i in range(len(src_lap)):
227
+ if len(mask_pyr) > i:
228
+ mask_norm = mask_pyr[len(mask_pyr)-1-i].astype(float) / 255.0
229
+ if len(mask_norm.shape) == 2:
230
+ mask_norm = np.stack([mask_norm]*3, axis=2)
231
+ ls = src_lap[i] * mask_norm + tgt_lap[i] * (1 - mask_norm)
232
+ blended.append(ls.astype(src.dtype))
233
+
234
+ # Reconstruct
235
+ result = blended[0]
236
+ for i in range(1, len(blended)):
237
+ size = (blended[i].shape[1], blended[i].shape[0])
238
+ result = cv2.add(cv2.pyrUp(result, dstsize=size), blended[i])
239
+
240
+ return result
241
+
242
+ def enhance_face_details(img):
243
+ """Apply subtle sharpening to enhance facial details"""
244
+ kernel = np.array([[-1,-1,-1],
245
+ [-1, 9,-1],
246
+ [-1,-1,-1]]) / 9
247
+ sharpened = cv2.filter2D(img, -1, kernel)
248
+ # Blend 70% original with 30% sharpened for subtle enhancement
249
+ return cv2.addWeighted(img, 0.7, sharpened, 0.3, 0)
250
+
251
+ def adjust_face_lighting(swapped_face, target_face):
252
+ """Match lighting conditions between swapped and target face"""
253
+ # Convert to LAB color space
254
+ swapped_lab = cv2.cvtColor(swapped_face, cv2.COLOR_BGR2LAB).astype(float)
255
+ target_lab = cv2.cvtColor(target_face, cv2.COLOR_BGR2LAB).astype(float)
256
+
257
+ # Match L channel (luminance)
258
+ swapped_l = swapped_lab[:, :, 0]
259
+ target_l = target_lab[:, :, 0]
260
+
261
+ swapped_mean = swapped_l.mean()
262
+ target_mean = target_l.mean()
263
+
264
+ # Adjust luminance
265
+ swapped_lab[:, :, 0] = np.clip(swapped_l + (target_mean - swapped_mean) * 0.5, 0, 255)
266
+
267
+ return cv2.cvtColor(swapped_lab.astype(np.uint8), cv2.COLOR_LAB2BGR)
268
+
269
+ # --------------------- Enhanced Face Swap Pipeline ---------------------
270
  swap_lock = threading.Lock()
271
 
272
  def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
 
280
  src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
281
  tgt_bgr_full = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
282
 
283
+ # Detect faces with higher confidence
284
  src_faces = face_analysis_app.get(src_bgr)
285
  tgt_faces = face_analysis_app.get(tgt_bgr_full)
286
 
287
  if not src_faces or not tgt_faces:
288
  return None, None, "❌ Face not detected in source or target image"
289
 
290
+ # Sort by face size (use largest faces)
291
+ src_faces.sort(key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]), reverse=True)
292
+ tgt_faces.sort(key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]), reverse=True)
293
+
294
+ def expand_bbox(bbox, img_shape, scale=1.5):
295
+ """Smart bbox expansion with padding"""
296
  ih, iw = img_shape[:2]
297
  x1, y1, x2, y2 = map(int, bbox)
298
  w, h = x2 - x1, y2 - y1
299
  cx, cy = x1 + w // 2, y1 + h // 2
300
+
301
+ # Use larger dimension for square crop
302
+ size = max(w, h)
303
+ new_size = int(size * scale)
304
+
305
+ nx1 = max(0, cx - new_size // 2)
306
+ ny1 = max(0, cy - new_size // 2)
307
+ nx2 = min(iw, cx + new_size // 2)
308
+ ny2 = min(ih, cy + new_size // 2)
309
+
310
  return nx1, ny1, nx2, ny2
311
 
312
  src_face0 = src_faces[0]
313
  tgt_face0 = tgt_faces[0]
314
 
315
+ # Extract and align source face with optimal crop
316
+ s_x1, s_y1, s_x2, s_y2 = expand_bbox(src_face0.bbox, src_bgr.shape, scale=1.3)
317
  src_crop = src_bgr[s_y1:s_y2, s_x1:s_x2]
318
+
319
+ # Re-detect face in crop for better precision
320
  src_crop_faces = face_analysis_app.get(src_crop)
321
  if src_crop_faces:
322
  src_for_swap = src_crop
 
325
  src_for_swap = src_bgr
326
  src_face_for_swap = src_face0
327
 
328
+ # Extract target face with larger context for better blending
329
+ t_x1, t_y1, t_x2, t_y2 = expand_bbox(tgt_face0.bbox, tgt_bgr_full.shape, scale=1.7)
330
  tgt_crop = tgt_bgr_full[t_y1:t_y2, t_x1:t_x2]
331
+
332
+ # Re-detect in target crop
333
  tgt_crop_faces = face_analysis_app.get(tgt_crop)
334
 
335
  if tgt_crop_faces:
336
  tgt_for_swap = tgt_crop
337
  tgt_face_for_swap = tgt_crop_faces[0]
338
 
339
+ # Perform face swap on crop
340
  swapped_crop = swapper.get(tgt_for_swap, tgt_face_for_swap, src_face_for_swap)
341
  if swapped_crop is None:
342
  return None, None, "❌ Face swap failed on crop"
343
 
344
+ # Extract face region from swapped crop for color matching
345
+ f_x1, f_y1, f_x2, f_y2 = map(int, tgt_face_for_swap.bbox)
346
+ swapped_face_region = swapped_crop[f_y1:f_y2, f_x1:f_x2]
347
+ target_face_region = tgt_for_swap[f_y1:f_y2, f_x1:f_x2]
348
+
349
+ # Match colors for better consistency
350
+ if swapped_face_region.size > 0 and target_face_region.size > 0:
351
+ color_matched = match_color_histogram(swapped_face_region, target_face_region)
352
+ swapped_crop[f_y1:f_y2, f_x1:f_x2] = color_matched
353
+
354
+ # Adjust lighting
355
+ swapped_crop = adjust_face_lighting(swapped_crop, tgt_for_swap)
356
+
357
+ # Create advanced seamless mask
358
+ mask = create_seamless_mask(tgt_face_for_swap.bbox, tgt_for_swap.shape, feather_amount=20)
359
+
360
+ # Multi-band blending for natural transition
361
  try:
362
+ # Resize mask to match crop size
363
+ mask_resized = cv2.resize(mask, (swapped_crop.shape[1], swapped_crop.shape[0]))
364
+ blended_crop = multi_band_blending(swapped_crop, tgt_for_swap, mask_resized, levels=4)
365
+ except Exception as e:
366
+ logger.warning(f"Multi-band blending failed: {e}, using Poisson")
367
+ # Fallback to Poisson blending
368
+ center = ((t_x1 + t_x2) // 2 - t_x1, (t_y1 + t_y2) // 2 - t_y1)
369
+ mask_3ch = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
370
+ blended_crop = poisson_blend(swapped_crop, tgt_for_swap, mask_3ch, center)
371
+
372
+ # Place blended crop back into full image
373
+ blended_full = tgt_bgr_full.copy()
374
+ h, w = blended_crop.shape[:2]
375
+
376
+ # Create soft edge mask for final compositing
377
+ edge_mask = np.ones((h, w), dtype=np.float32)
378
+ edge_feather = 30
379
+ edge_mask = cv2.GaussianBlur(edge_mask, (edge_feather*2+1, edge_feather*2+1), 0)
380
+ edge_mask = np.stack([edge_mask]*3, axis=2)
381
+
382
+ # Composite with soft edges
383
+ crop_region = blended_full[t_y1:t_y1+h, t_x1:t_x1+w]
384
+ blended_full[t_y1:t_y1+h, t_x1:t_x1+w] = (
385
+ blended_crop * edge_mask + crop_region * (1 - edge_mask)
386
+ ).astype(np.uint8)
387
+
388
+ # Enhance facial details
389
+ face_region = blended_full[t_y1:t_y2, t_x1:t_x2]
390
+ enhanced_face = enhance_face_details(face_region)
391
+ blended_full[t_y1:t_y2, t_x1:t_x2] = enhanced_face
392
+
393
  swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
394
+ cv2.imwrite(swapped_path, blended_full, [cv2.IMWRITE_JPEG_QUALITY, 95])
395
 
396
  else:
397
+ # Fallback: swap on full image with enhancements
398
  swapped_bgr_full = swapper.get(tgt_bgr_full, tgt_face0, src_face0)
399
  if swapped_bgr_full is None:
400
  return None, None, "❌ Face swap failed on full image"
401
+
402
+ # Apply color matching and lighting adjustment
403
+ t_x1, t_y1, t_x2, t_y2 = map(int, tgt_face0.bbox)
404
+ swapped_face = swapped_bgr_full[t_y1:t_y2, t_x1:t_x2]
405
+ target_face = tgt_bgr_full[t_y1:t_y2, t_x1:t_x2]
406
+
407
+ if swapped_face.size > 0 and target_face.size > 0:
408
+ color_matched = match_color_histogram(swapped_face, target_face)
409
+ swapped_bgr_full[t_y1:t_y2, t_x1:t_x2] = color_matched
410
+
411
+ swapped_bgr_full = adjust_face_lighting(swapped_bgr_full, tgt_bgr_full)
412
+
413
  swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
414
+ cv2.imwrite(swapped_path, swapped_bgr_full, [cv2.IMWRITE_JPEG_QUALITY, 95])
415
 
416
  # Run CodeFormer enhancement on the swapped image
417
  cmd = f"python {CODEFORMER_PATH} -w 0.7 --input_path {swapped_path} --output_path {temp_dir} --bg_upsampler realesrgan --face_upsample"
 
430
  return final_img, final_path, ""
431
 
432
  except Exception as e:
433
+ logger.exception("Face swap error")
434
  return None, None, f"❌ Error: {str(e)}"
435
 
 
 
436
  # --------------------- Gradio ---------------------
437
  with gr.Blocks() as demo:
438
+ gr.Markdown("# 🎭 Enhanced Face Swap\n### With improved color matching, blending, and detail preservation")
439
 
440
  with gr.Row():
441
  src_input = gr.Image(type="numpy", label="Upload Your Face")
442
  tgt_input = gr.Image(type="numpy", label="Upload Target Image")
443
 
444
+ btn = gr.Button("Swap Face", variant="primary")
445
  output_img = gr.Image(type="numpy", label="Enhanced Output")
446
  download = gr.File(label="⬇️ Download Enhanced Image")
447
  error_box = gr.Textbox(label="Logs / Errors", interactive=False)
 
567
  fastapi_app = mount_gradio_app(fastapi_app, demo, path="/gradio")
568
 
569
  if __name__ == "__main__":
570
+ uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)