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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +31 -215
app.py CHANGED
@@ -144,129 +144,7 @@ async def log_faceswap_hit(token: str, status: str = "success"):
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,43 +158,30 @@ 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,93 +190,43 @@ def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
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,18 +245,19 @@ def face_swap_and_enhance(src_img, tgt_img, temp_dir="/tmp/faceswap_work"):
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)
 
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
  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
  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
  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)