LogicGoInfotechSpaces commited on
Commit
b890dc6
·
verified ·
1 Parent(s): b1fa781

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +416 -210
app.py CHANGED
@@ -27,7 +27,7 @@ import gradio as gr
27
  from gradio import mount_gradio_app
28
  from PIL import Image
29
  import io
30
-
31
  # DigitalOcean Spaces
32
  import boto3
33
  from botocore.client import Config
@@ -109,6 +109,415 @@ def ensure_codeformer():
109
 
110
  ensure_codeformer()
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  # --------------------- FastAPI ---------------------
114
  fastapi_app = FastAPI()
@@ -719,7 +1128,12 @@ async def face_swap_api(
719
  # ------------------------------------------------------------------
720
  # FACE SWAP EXECUTION
721
  # ------------------------------------------------------------------
722
- final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
 
 
 
 
 
723
  if err:
724
  raise HTTPException(500, err)
725
 
@@ -851,214 +1265,6 @@ async def multi_face_swap_api(
851
  except Exception as e:
852
  raise HTTPException(status_code=500, detail=str(e))
853
 
854
- # @fastapi_app.post("/face-swap-couple", dependencies=[Depends(verify_token)])
855
- # async def face_swap_api(
856
- # image1: UploadFile = File(...),
857
- # image2: Optional[UploadFile] = File(None),
858
- # target_category_id: str = Form(None),
859
- # new_category_id: str = Form(None),
860
- # user_id: Optional[str] = Form(None),
861
- # credentials: HTTPAuthorizationCredentials = Security(security)
862
- # ):
863
- # """
864
- # Production-ready face swap endpoint supporting:
865
- # - Multiple source images (image1 + optional image2)
866
- # - Gender-based pairing
867
- # - Merged faces from multiple sources
868
- # - Mandatory CodeFormer enhancement
869
- # """
870
- # start_time = datetime.utcnow()
871
-
872
- # try:
873
- # # -----------------------------
874
- # # Validate input
875
- # # -----------------------------
876
- # if target_category_id == "":
877
- # target_category_id = None
878
- # if new_category_id == "":
879
- # new_category_id = None
880
- # if user_id == "":
881
- # user_id = None
882
-
883
- # if target_category_id and new_category_id:
884
- # raise HTTPException(400, "Provide only one of new_category_id or target_category_id.")
885
- # if not target_category_id and not new_category_id:
886
- # raise HTTPException(400, "Either new_category_id or target_category_id is required.")
887
-
888
- # logger.info(f"[FaceSwap] Incoming request → target_category_id={target_category_id}, new_category_id={new_category_id}, user_id={user_id}")
889
-
890
- # # -----------------------------
891
- # # Read source images
892
- # # -----------------------------
893
- # src_images = []
894
- # img1_bytes = await image1.read()
895
- # src1 = cv2.imdecode(np.frombuffer(img1_bytes, np.uint8), cv2.IMREAD_COLOR)
896
- # if src1 is None:
897
- # raise HTTPException(400, "Invalid image1 data")
898
- # src_images.append(cv2.cvtColor(src1, cv2.COLOR_BGR2RGB))
899
-
900
- # if image2:
901
- # img2_bytes = await image2.read()
902
- # src2 = cv2.imdecode(np.frombuffer(img2_bytes, np.uint8), cv2.IMREAD_COLOR)
903
- # if src2 is not None:
904
- # src_images.append(cv2.cvtColor(src2, cv2.COLOR_BGR2RGB))
905
-
906
- # # -----------------------------
907
- # # Determine target image
908
- # # -----------------------------
909
- # target_bytes = None
910
-
911
- # if new_category_id:
912
- # doc = await subcategories_col.find_one({"asset_images._id": ObjectId(new_category_id)})
913
- # if not doc:
914
- # raise HTTPException(404, "Asset image not found")
915
- # asset = next((img for img in doc["asset_images"] if str(img["_id"]) == new_category_id), None)
916
- # if not asset:
917
- # raise HTTPException(404, "Asset image URL not found")
918
- # target_url = asset["url"]
919
-
920
- # elif target_category_id:
921
- # client = get_spaces_client()
922
- # base_prefix = "faceswap/target/"
923
- # resp = client.list_objects_v2(Bucket=DO_SPACES_BUCKET, Prefix=base_prefix, Delimiter="/")
924
- # categories = [p["Prefix"].split("/")[2] for p in resp.get("CommonPrefixes", [])]
925
- # target_url = None
926
- # for category in categories:
927
- # original_prefix = f"faceswap/target/{category}/original/"
928
- # objects = client.list_objects_v2(Bucket=DO_SPACES_BUCKET, Prefix=original_prefix).get("Contents", [])
929
- # original_filenames = sorted([obj["Key"].split("/")[-1] for obj in objects if obj["Key"].endswith(".png")])
930
- # for idx, filename in enumerate(original_filenames, start=1):
931
- # cid = f"{category.lower()}image_{idx}"
932
- # if cid == target_category_id:
933
- # target_url = f"{DO_SPACES_ENDPOINT}/{DO_SPACES_BUCKET}/{original_prefix}{filename}"
934
- # break
935
- # if target_url:
936
- # break
937
- # if not target_url:
938
- # raise HTTPException(404, "Target categoryId not found")
939
-
940
- # # -----------------------------
941
- # # Download target image
942
- # # -----------------------------
943
- # async with httpx.AsyncClient(timeout=30.0) as client:
944
- # resp = await client.get(target_url)
945
- # resp.raise_for_status()
946
- # target_bytes = resp.content
947
-
948
- # tgt_bgr = cv2.imdecode(np.frombuffer(target_bytes, np.uint8), cv2.IMREAD_COLOR)
949
- # if tgt_bgr is None:
950
- # raise HTTPException(400, "Invalid target image data")
951
- # target_rgb = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2RGB)
952
-
953
- # # -----------------------------
954
- # # Merge all source faces
955
- # # -----------------------------
956
- # all_src_faces = []
957
- # for img in src_images:
958
- # faces = face_analysis_app.get(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
959
- # all_src_faces.extend(faces)
960
-
961
- # if not all_src_faces:
962
- # raise HTTPException(400, "No faces detected in source images")
963
-
964
- # tgt_faces = face_analysis_app.get(tgt_bgr)
965
- # if not tgt_faces:
966
- # raise HTTPException(400, "No faces detected in target image")
967
-
968
- # # -----------------------------
969
- # # Gender-based pairing
970
- # # -----------------------------
971
- # def face_sort_key(face):
972
- # x1, y1, x2, y2 = face.bbox
973
- # area = (x2 - x1) * (y2 - y1)
974
- # cx = (x1 + x2) / 2
975
- # return (-area, cx)
976
-
977
- # # Separate by gender
978
- # src_male = sorted([f for f in all_src_faces if f.gender == 1], key=face_sort_key)
979
- # src_female = sorted([f for f in all_src_faces if f.gender == 0], key=face_sort_key)
980
- # tgt_male = sorted([f for f in tgt_faces if f.gender == 1], key=face_sort_key)
981
- # tgt_female = sorted([f for f in tgt_faces if f.gender == 0], key=face_sort_key)
982
-
983
- # pairs = []
984
- # for s, t in zip(src_male, tgt_male):
985
- # pairs.append((s, t))
986
- # for s, t in zip(src_female, tgt_female):
987
- # pairs.append((s, t))
988
-
989
- # # fallback if gender mismatch
990
- # if not pairs:
991
- # src_all = sorted(all_src_faces, key=face_sort_key)
992
- # tgt_all = sorted(tgt_faces, key=face_sort_key)
993
- # pairs = list(zip(src_all, tgt_all))
994
-
995
- # # -----------------------------
996
- # # Perform face swap
997
- # # -----------------------------
998
- # with swap_lock:
999
- # result_img = tgt_bgr.copy()
1000
- # for src_face, _ in pairs:
1001
- # current_faces = sorted(face_analysis_app.get(result_img), key=face_sort_key)
1002
- # candidates = [f for f in current_faces if f.gender == src_face.gender] or current_faces
1003
- # target_face = candidates[0]
1004
- # result_img = swapper.get(result_img, target_face, src_face, paste_back=True)
1005
-
1006
- # result_rgb = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
1007
-
1008
- # # -----------------------------
1009
- # # Mandatory enhancement
1010
- # # -----------------------------
1011
- # enhanced_rgb = mandatory_enhancement(result_rgb)
1012
- # enhanced_bgr = cv2.cvtColor(enhanced_rgb, cv2.COLOR_RGB2BGR)
1013
-
1014
- # # -----------------------------
1015
- # # Save, upload, compress
1016
- # # -----------------------------
1017
- # temp_dir = tempfile.mkdtemp(prefix="faceswap_")
1018
- # final_path = os.path.join(temp_dir, "result.png")
1019
- # cv2.imwrite(final_path, enhanced_bgr)
1020
-
1021
- # with open(final_path, "rb") as f:
1022
- # result_bytes = f.read()
1023
-
1024
- # result_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced.png"
1025
- # result_url = upload_to_spaces(result_bytes, result_key)
1026
-
1027
- # compressed_bytes = compress_image(result_bytes, max_size=(1280, 1280), quality=72)
1028
- # compressed_key = f"faceswap/result/{uuid.uuid4().hex}_enhanced_compressed.jpg"
1029
- # compressed_url = upload_to_spaces(compressed_bytes, compressed_key, content_type="image/jpeg")
1030
-
1031
- # # -----------------------------
1032
- # # Log API usage
1033
- # # -----------------------------
1034
- # end_time = datetime.utcnow()
1035
- # response_time_ms = (end_time - start_time).total_seconds() * 1000
1036
- # if database is not None:
1037
- # await database.api_logs.insert_one({
1038
- # "endpoint": "/face-swap",
1039
- # "status": "success",
1040
- # "response_time_ms": response_time_ms,
1041
- # "timestamp": end_time
1042
- # })
1043
-
1044
- # return {
1045
- # "result_key": result_key,
1046
- # "result_url": result_url,
1047
- # "compressed_url": compressed_url
1048
- # }
1049
-
1050
- # except Exception as e:
1051
- # end_time = datetime.utcnow()
1052
- # response_time_ms = (end_time - start_time).total_seconds() * 1000
1053
- # if database is not None:
1054
- # await database.api_logs.insert_one({
1055
- # "endpoint": "/face-swap",
1056
- # "status": "fail",
1057
- # "response_time_ms": response_time_ms,
1058
- # "timestamp": end_time,
1059
- # "error": str(e)
1060
- # })
1061
- # raise HTTPException(500, f"Face swap failed: {str(e)}")
1062
 
1063
  @fastapi_app.post("/face-swap-couple", dependencies=[Depends(verify_token)])
1064
  async def face_swap_api(
 
27
  from gradio import mount_gradio_app
28
  from PIL import Image
29
  import io
30
+ from scipy import ndimage
31
  # DigitalOcean Spaces
32
  import boto3
33
  from botocore.client import Config
 
109
 
110
  ensure_codeformer()
111
 
112
+ class NaturalFaceSwapper:
113
+ """Enhanced face swapping with natural blending techniques"""
114
+
115
+ def __init__(self, swapper, face_app):
116
+ self.swapper = swapper
117
+ self.face_app = face_app
118
+
119
+ def match_color_histogram(self, source, target, mask=None):
120
+ """Match color histogram of source to target for better blending"""
121
+ if mask is None:
122
+ mask = np.ones(source.shape[:2], dtype=np.uint8) * 255
123
+
124
+ result = source.copy()
125
+ for i in range(3): # Process each channel
126
+ source_channel = source[:, :, i]
127
+ target_channel = target[:, :, i]
128
+
129
+ # Only use masked regions
130
+ source_masked = source_channel[mask > 0]
131
+ target_masked = target_channel[mask > 0]
132
+
133
+ if len(source_masked) > 0 and len(target_masked) > 0:
134
+ # Match histograms
135
+ matched = self._match_histogram_channel(
136
+ source_channel, source_masked, target_masked
137
+ )
138
+ result[:, :, i] = matched
139
+
140
+ return result
141
+ def subtle_skin_smooth(img, strength=0.3, preserve_details=True):
142
+ """
143
+ Subtle bilateral filter for natural skin smoothing
144
+
145
+ Args:
146
+ img: Input image (BGR format)
147
+ strength: Smoothing strength (0.1-0.5 recommended, default 0.3)
148
+ preserve_details: If True, uses edge-preserving filter
149
+
150
+ Returns:
151
+ Smoothed image
152
+ """
153
+ if preserve_details:
154
+ # Bilateral filter preserves edges while smoothing
155
+ smoothed = cv2.bilateralFilter(img, d=9, sigmaColor=75, sigmaSpace=75)
156
+ else:
157
+ # Gaussian blur (faster but less detail preservation)
158
+ smoothed = cv2.GaussianBlur(img, (9, 9), 0)
159
+
160
+ # Blend with original
161
+ result = cv2.addWeighted(img, 1-strength, smoothed, strength, 0)
162
+ return result
163
+
164
+
165
+ def advanced_skin_smooth(img, strength=0.3):
166
+ """
167
+ Advanced skin smoothing with frequency separation
168
+ Smooths skin while preserving pores and texture
169
+
170
+ Args:
171
+ img: Input image (BGR format)
172
+ strength: Smoothing strength (0.2-0.5 recommended)
173
+
174
+ Returns:
175
+ Smoothed image with preserved texture
176
+ """
177
+ # Convert to float for better precision
178
+ img_float = img.astype(np.float32) / 255.0
179
+
180
+ # Low frequency (color and tone)
181
+ low_freq = cv2.GaussianBlur(img_float, (0, 0), sigmaX=3, sigmaY=3)
182
+
183
+ # High frequency (details and texture)
184
+ high_freq = img_float - low_freq
185
+
186
+ # Smooth only the low frequency
187
+ low_freq_smoothed = cv2.bilateralFilter(
188
+ (low_freq * 255).astype(np.uint8),
189
+ d=9,
190
+ sigmaColor=75,
191
+ sigmaSpace=75
192
+ ).astype(np.float32) / 255.0
193
+
194
+ # Blend smoothed low frequency with original
195
+ low_freq_final = cv2.addWeighted(low_freq, 1-strength, low_freq_smoothed, strength, 0)
196
+
197
+ # Recombine with high frequency to preserve texture
198
+ result = low_freq_final + high_freq
199
+ result = np.clip(result * 255, 0, 255).astype(np.uint8)
200
+
201
+ return result
202
+
203
+
204
+ def skin_tone_aware_smooth(img, face_analysis_app, strength=0.3):
205
+ """
206
+ Smooth only skin regions (more advanced)
207
+ Detects face and creates skin mask
208
+
209
+ Args:
210
+ img: Input image (BGR format)
211
+ face_analysis_app: InsightFace app for face detection
212
+ strength: Smoothing strength
213
+
214
+ Returns:
215
+ Image with skin-only smoothing
216
+ """
217
+ # Detect faces to create skin mask
218
+ faces = face_analysis_app.get(img)
219
+
220
+ if not faces:
221
+ # No face detected, smooth entire image
222
+ return subtle_skin_smooth(img, strength)
223
+
224
+ # Create skin mask based on face regions
225
+ mask = np.zeros(img.shape[:2], dtype=np.uint8)
226
+
227
+ for face in faces:
228
+ x1, y1, x2, y2 = [int(v) for v in face.bbox]
229
+
230
+ # Expand bbox to include more skin area
231
+ padding_x = int((x2 - x1) * 0.2)
232
+ padding_y = int((y2 - y1) * 0.3)
233
+
234
+ x1 = max(0, x1 - padding_x)
235
+ y1 = max(0, y1 - padding_y)
236
+ x2 = min(img.shape[1], x2 + padding_x)
237
+ y2 = min(img.shape[0], y2 + padding_y)
238
+
239
+ # Create elliptical mask for natural look
240
+ center = ((x1 + x2) // 2, (y1 + y2) // 2)
241
+ axes = ((x2 - x1) // 2, (y2 - y1) // 2)
242
+ cv2.ellipse(mask, center, axes, 0, 0, 360, 255, -1)
243
+
244
+ # Blur mask for smooth transition
245
+ mask = cv2.GaussianBlur(mask, (31, 31), 0)
246
+ mask_float = mask.astype(float) / 255.0
247
+ mask_3ch = np.stack([mask_float] * 3, axis=2)
248
+
249
+ # Apply smoothing
250
+ smoothed = cv2.bilateralFilter(img, 9, 75, 75)
251
+
252
+ # Blend only where mask is present
253
+ result = (smoothed * mask_3ch * strength +
254
+ img * (1 - mask_3ch * strength)).astype(np.uint8)
255
+
256
+ return result
257
+
258
+ def _match_histogram_channel(self, channel, source_vals, target_vals):
259
+ """Match histogram for single channel"""
260
+ # Compute CDFs
261
+ source_hist, _ = np.histogram(source_vals, 256, [0, 256])
262
+ target_hist, _ = np.histogram(target_vals, 256, [0, 256])
263
+
264
+ source_cdf = source_hist.cumsum()
265
+ target_cdf = target_hist.cumsum()
266
+
267
+ # Normalize
268
+ source_cdf = source_cdf / source_cdf[-1]
269
+ target_cdf = target_cdf / target_cdf[-1]
270
+
271
+ # Create mapping
272
+ mapping = np.zeros(256, dtype=np.uint8)
273
+ for i in range(256):
274
+ # Find closest value in target CDF
275
+ idx = np.argmin(np.abs(target_cdf - source_cdf[i]))
276
+ mapping[i] = idx
277
+
278
+ return mapping[channel]
279
+
280
+ def seamless_clone_blend(self, source, target, mask, center):
281
+ """Use Poisson blending for seamless integration"""
282
+ try:
283
+ # OpenCV's seamlessClone for natural blending
284
+ result = cv2.seamlessClone(
285
+ source, target, mask, center,
286
+ cv2.NORMAL_CLONE # Try MIXED_CLONE for different effect
287
+ )
288
+ return result
289
+ except:
290
+ # Fallback to alpha blending if seamlessClone fails
291
+ return self.alpha_blend_with_feather(source, target, mask)
292
+
293
+ def alpha_blend_with_feather(self, source, target, mask, feather_amount=15):
294
+ """Alpha blend with feathered edges for smooth transition"""
295
+ # Create feathered mask
296
+ mask_float = mask.astype(float) / 255.0
297
+
298
+ # Apply Gaussian blur for feathering
299
+ feathered_mask = cv2.GaussianBlur(mask_float, (feather_amount*2+1, feather_amount*2+1), 0)
300
+ feathered_mask = np.clip(feathered_mask, 0, 1)
301
+
302
+ # Expand mask to 3 channels
303
+ feathered_mask_3ch = np.stack([feathered_mask] * 3, axis=2)
304
+
305
+ # Blend
306
+ blended = (source * feathered_mask_3ch +
307
+ target * (1 - feathered_mask_3ch)).astype(np.uint8)
308
+
309
+ return blended
310
+
311
+ def laplacian_pyramid_blend(self, source, target, mask, levels=6):
312
+ """Multi-resolution blending using Laplacian pyramids"""
313
+ # Generate Gaussian pyramid for mask
314
+ mask_float = mask.astype(float) / 255.0
315
+ gaussian_mask = [mask_float]
316
+
317
+ for i in range(levels):
318
+ mask_float = cv2.pyrDown(mask_float)
319
+ gaussian_mask.append(mask_float)
320
+
321
+ # Generate Laplacian pyramids
322
+ def build_laplacian_pyramid(img, levels):
323
+ gaussian = [img.astype(float)]
324
+ for i in range(levels):
325
+ img = cv2.pyrDown(img)
326
+ gaussian.append(img)
327
+
328
+ laplacian = []
329
+ for i in range(levels):
330
+ size = (gaussian[i].shape[1], gaussian[i].shape[0])
331
+ upsampled = cv2.pyrUp(gaussian[i + 1], dstsize=size)
332
+ laplacian.append(gaussian[i] - upsampled)
333
+ laplacian.append(gaussian[levels])
334
+
335
+ return laplacian
336
+
337
+ lp_source = build_laplacian_pyramid(source, levels)
338
+ lp_target = build_laplacian_pyramid(target, levels)
339
+
340
+ # Blend each level
341
+ blended_pyramid = []
342
+ for ls, lt, gm in zip(lp_source, lp_target, gaussian_mask):
343
+ # Resize mask if needed
344
+ if gm.shape[:2] != ls.shape[:2]:
345
+ gm = cv2.resize(gm, (ls.shape[1], ls.shape[0]))
346
+ gm_3ch = np.stack([gm] * 3, axis=2)
347
+ blended = ls * gm_3ch + lt * (1 - gm_3ch)
348
+ blended_pyramid.append(blended)
349
+
350
+ # Reconstruct
351
+ result = blended_pyramid[-1]
352
+ for i in range(levels - 1, -1, -1):
353
+ size = (blended_pyramid[i].shape[1], blended_pyramid[i].shape[0])
354
+ result = cv2.pyrUp(result, dstsize=size)
355
+ result += blended_pyramid[i]
356
+
357
+ return np.clip(result, 0, 255).astype(np.uint8)
358
+
359
+ def match_lighting(self, swapped_face, target_img, face_bbox):
360
+ """Match lighting conditions between swapped face and target"""
361
+ x1, y1, x2, y2 = [int(v) for v in face_bbox]
362
+
363
+ # Extract face region from target
364
+ target_face = target_img[y1:y2, x1:x2]
365
+
366
+ if target_face.size == 0 or swapped_face.size == 0:
367
+ return swapped_face
368
+
369
+ # Resize if needed
370
+ if swapped_face.shape[:2] != target_face.shape[:2]:
371
+ target_face = cv2.resize(target_face,
372
+ (swapped_face.shape[1], swapped_face.shape[0]))
373
+
374
+ # Convert to LAB color space
375
+ swapped_lab = cv2.cvtColor(swapped_face, cv2.COLOR_BGR2LAB).astype(float)
376
+ target_lab = cv2.cvtColor(target_face, cv2.COLOR_BGR2LAB).astype(float)
377
+
378
+ # Match mean and std of L channel (luminance)
379
+ swapped_l = swapped_lab[:, :, 0]
380
+ target_l = target_lab[:, :, 0]
381
+
382
+ swapped_l_mean, swapped_l_std = swapped_l.mean(), swapped_l.std()
383
+ target_l_mean, target_l_std = target_l.mean(), target_l.std()
384
+
385
+ if swapped_l_std > 0:
386
+ swapped_lab[:, :, 0] = ((swapped_l - swapped_l_mean) / swapped_l_std *
387
+ target_l_std + target_l_mean)
388
+
389
+ # Convert back
390
+ result = cv2.cvtColor(swapped_lab.astype(np.uint8), cv2.COLOR_LAB2BGR)
391
+ return result
392
+
393
+ def adjust_face_mask(self, mask, erosion=3, dilation=5):
394
+ """Adjust mask to avoid harsh edges"""
395
+ # Slightly erode to avoid edge artifacts
396
+ kernel_erode = np.ones((erosion, erosion), np.uint8)
397
+ mask = cv2.erode(mask, kernel_erode, iterations=1)
398
+
399
+ # Then dilate to smooth
400
+ kernel_dilate = np.ones((dilation, dilation), np.uint8)
401
+ mask = cv2.dilate(mask, kernel_dilate, iterations=1)
402
+
403
+ # Gaussian blur for soft edges
404
+ mask = cv2.GaussianBlur(mask, (15, 15), 0)
405
+
406
+ return mask
407
+
408
+ def natural_face_swap(self, src_img, tgt_img, use_laplacian=True):
409
+ """
410
+ Complete natural face swap pipeline
411
+
412
+ Args:
413
+ src_img: Source image (RGB)
414
+ tgt_img: Target image (RGB)
415
+ use_laplacian: Use Laplacian pyramid blending (slower but better)
416
+
417
+ Returns:
418
+ Naturally blended face-swapped image
419
+ """
420
+ src_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR)
421
+ tgt_bgr = cv2.cvtColor(tgt_img, cv2.COLOR_RGB2BGR)
422
+
423
+ # Detect faces
424
+ src_faces = self.face_app.get(src_bgr)
425
+ tgt_faces = self.face_app.get(tgt_bgr)
426
+
427
+ if not src_faces or not tgt_faces:
428
+ raise ValueError("No faces detected")
429
+
430
+ # Get largest faces
431
+ src_face = max(src_faces, key=lambda f: (f.bbox[2]-f.bbox[0])*(f.bbox[3]-f.bbox[1]))
432
+ tgt_face = max(tgt_faces, key=lambda f: (f.bbox[2]-f.bbox[0])*(f.bbox[3]-f.bbox[1]))
433
+
434
+ # Perform basic swap
435
+ swapped_bgr = self.swapper.get(tgt_bgr, tgt_face, src_face, paste_back=True)
436
+
437
+ # Create face mask
438
+ x1, y1, x2, y2 = [int(v) for v in tgt_face.bbox]
439
+ mask = np.zeros(tgt_bgr.shape[:2], dtype=np.uint8)
440
+
441
+ # Use landmarks for better mask if available
442
+ if hasattr(tgt_face, 'kps') and tgt_face.kps is not None:
443
+ kps = tgt_face.kps.astype(np.int32)
444
+ hull = cv2.convexHull(kps)
445
+ cv2.fillConvexPoly(mask, hull, 255)
446
+ else:
447
+ # Fallback to bbox with some padding
448
+ padding = int((x2 - x1) * 0.1)
449
+ cv2.ellipse(mask,
450
+ ((x1 + x2) // 2, (y1 + y2) // 2),
451
+ ((x2 - x1) // 2 + padding, (y2 - y1) // 2 + padding),
452
+ 0, 0, 360, 255, -1)
453
+
454
+ # Adjust mask for softer edges
455
+ mask = self.adjust_face_mask(mask)
456
+
457
+ # Color histogram matching
458
+ swapped_bgr = self.match_color_histogram(swapped_bgr, tgt_bgr, mask)
459
+
460
+ # Lighting adjustment
461
+ swapped_face_region = swapped_bgr[y1:y2, x1:x2]
462
+ adjusted_face = self.match_lighting(swapped_face_region, tgt_bgr, tgt_face.bbox)
463
+ swapped_bgr[y1:y2, x1:x2] = adjusted_face
464
+
465
+ # Final blending
466
+ if use_laplacian:
467
+ # Best quality but slower
468
+ result = self.laplacian_pyramid_blend(swapped_bgr, tgt_bgr, mask)
469
+ else:
470
+ # Faster alternative: Seamless cloning
471
+ center = ((x1 + x2) // 2, (y1 + y2) // 2)
472
+ result = self.seamless_clone_blend(swapped_bgr, tgt_bgr, mask, center)
473
+
474
+ return cv2.cvtColor(result, cv2.COLOR_BGR2RGB)
475
+
476
+
477
+ # ============================================
478
+ # Integration into your existing code
479
+ # ============================================
480
+
481
+ def enhanced_face_swap_and_enhance(src_img, tgt_img, swapper, face_app, temp_dir=None):
482
+ """
483
+ Enhanced version of your face_swap_and_enhance function
484
+ """
485
+ try:
486
+ # Initialize natural swapper
487
+ natural_swapper = NaturalFaceSwapper(swapper, face_app)
488
+
489
+ # Perform natural swap
490
+ swapped_rgb = natural_swapper.natural_face_swap(
491
+ src_img, tgt_img,
492
+ use_laplacian=True # Set False for faster processing
493
+ )
494
+
495
+ # Apply CodeFormer enhancement
496
+ enhanced_rgb = enhance_image_with_codeformer(swapped_rgb, temp_dir)
497
+
498
+ # Post-enhancement sharpening (subtle)
499
+ kernel_sharpen = np.array([[-0.5, -0.5, -0.5],
500
+ [-0.5, 5.0, -0.5],
501
+ [-0.5, -0.5, -0.5]]) * 0.3
502
+ enhanced_bgr = cv2.cvtColor(enhanced_rgb, cv2.COLOR_RGB2BGR)
503
+ sharpened = cv2.filter2D(enhanced_bgr, -1, kernel_sharpen)
504
+
505
+ # Blend sharpened with original (60% sharp, 40% original)
506
+ final_bgr = cv2.addWeighted(sharpened, 0.6, enhanced_bgr, 0.4, 0)
507
+ final_rgb = cv2.cvtColor(final_bgr, cv2.COLOR_BGR2RGB)
508
+
509
+ # Save result
510
+ if temp_dir is None:
511
+ temp_dir = os.path.join(tempfile.gettempdir(), f"faceswap_{uuid.uuid4().hex[:8]}")
512
+ os.makedirs(temp_dir, exist_ok=True)
513
+
514
+ final_path = os.path.join(temp_dir, "enhanced.png")
515
+ cv2.imwrite(final_path, final_bgr)
516
+
517
+ return final_rgb, final_path, ""
518
+
519
+ except Exception as e:
520
+ return None, None, f"❌ Error: {str(e)}"
521
 
522
  # --------------------- FastAPI ---------------------
523
  fastapi_app = FastAPI()
 
1128
  # ------------------------------------------------------------------
1129
  # FACE SWAP EXECUTION
1130
  # ------------------------------------------------------------------
1131
+ # final_img, final_path, err = face_swap_and_enhance(src_rgb, tgt_rgb)
1132
+
1133
+ #--------------------Version 2.0 ----------------------------------------#
1134
+ final_img, final_path, err = enhanced_face_swap_and_enhance(src_rgb, tgt_rgb)
1135
+ #--------------------Version 2.0 ----------------------------------------#
1136
+
1137
  if err:
1138
  raise HTTPException(500, err)
1139
 
 
1265
  except Exception as e:
1266
  raise HTTPException(status_code=500, detail=str(e))
1267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1268
 
1269
  @fastapi_app.post("/face-swap-couple", dependencies=[Depends(verify_token)])
1270
  async def face_swap_api(