Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
return nx1, ny1, nx2, ny2
|
| 178 |
|
| 179 |
src_face0 = src_faces[0]
|
| 180 |
tgt_face0 = tgt_faces[0]
|
| 181 |
|
| 182 |
-
#
|
| 183 |
-
s_x1, s_y1, s_x2, s_y2 = expand_bbox(src_face0.bbox, src_bgr.shape, scale=1.
|
| 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 |
-
#
|
| 194 |
-
t_x1, t_y1, t_x2, t_y2 = expand_bbox(tgt_face0.bbox, tgt_bgr_full.shape, scale=1.
|
| 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 |
-
#
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
try:
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
swapped_path = os.path.join(temp_dir, f"swapped_{uuid.uuid4().hex[:8]}.jpg")
|
| 221 |
-
cv2.imwrite(swapped_path,
|
| 222 |
|
| 223 |
else:
|
| 224 |
-
# Fallback: swap on full image
|
| 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)
|