Hamza4100 commited on
Commit
25e0198
Β·
verified Β·
1 Parent(s): a016ccf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +112 -150
app.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
- Professional Facial Editing App (v4 - Optimized Manual Pipeline)
3
- Fast, realistic edits WITHOUT heavy AI models
4
- Optimized for speed and natural results similar to PicsArt/Hypic
5
  """
6
 
7
  import gradio as gr
@@ -10,6 +10,8 @@ import numpy as np
10
  from typing import Tuple, Optional, Dict
11
  import logging
12
  import os
 
 
13
 
14
  # Configure logging
15
  logging.basicConfig(level=logging.INFO)
@@ -19,7 +21,6 @@ logger = logging.getLogger(__name__)
19
  # FACE DETECTION
20
  # ============================================================================
21
 
22
- # Initialize insightface
23
  try:
24
  import insightface
25
  app = insightface.app.FaceAnalysis(name='buffalo_l', providers=['CPUProvider'])
@@ -31,7 +32,7 @@ except Exception as e:
31
 
32
 
33
  def detect_face_landmarks(image: np.ndarray) -> Optional[np.ndarray]:
34
- """Detect facial landmarks using InsightFace (106 points)."""
35
  try:
36
  if app is None:
37
  return None
@@ -45,7 +46,7 @@ def detect_face_landmarks(image: np.ndarray) -> Optional[np.ndarray]:
45
  face = faces[0]
46
  landmarks_106 = face.landmark_2d_106
47
 
48
- # Expand to 468-point format for compatibility
49
  landmarks_468 = np.zeros((468, 2), dtype=np.float32)
50
  landmarks_468[:106] = landmarks_106.astype(np.float32)
51
 
@@ -61,56 +62,42 @@ def detect_face_landmarks(image: np.ndarray) -> Optional[np.ndarray]:
61
 
62
 
63
  # ============================================================================
64
- # IMPROVED REGION MASKS - PRECISE & ARTIFACT-FREE
65
  # ============================================================================
66
 
67
  def create_region_masks(landmarks: np.ndarray, h: int, w: int) -> Dict[str, np.ndarray]:
68
- """
69
- Create precise facial region masks with controlled blending.
70
- Uses minimal dilation (3x3 max) and conservative blur (7-15 range).
71
- Ensures tight eyebrow mask to completely avoid eye region.
72
- """
73
  masks = {}
74
 
75
- # ===== LIP REGION =====
76
- # InsightFace points 55-71 (outer contour, 16 points)
77
  lip_points = landmarks[55:71].astype(np.int32)
78
  if len(lip_points) >= 4:
79
  lip_mask = np.zeros((h, w), dtype=np.uint8)
80
  cv2.fillPoly(lip_mask, [lip_points], 255)
81
- # Minimal dilation (only 1 iteration with 3x3)
82
  lip_mask = cv2.dilate(lip_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)), iterations=1)
83
- # Moderate blur for smooth blending
84
  lip_mask = cv2.GaussianBlur(lip_mask.astype(np.float32), (11, 11), 0)
85
  masks['lips'] = np.clip(lip_mask / 255.0, 0, 1)
86
 
87
- # ===== NOSE REGION =====
88
- # InsightFace points 51-56 (6 points)
89
  nose_points = landmarks[51:57].astype(np.int32)
90
  if len(nose_points) >= 3:
91
  nose_mask = np.zeros((h, w), dtype=np.uint8)
92
  cv2.fillPoly(nose_mask, [nose_points], 255)
93
- # Minimal dilation for tight coverage
94
  nose_mask = cv2.dilate(nose_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)), iterations=2)
95
- # Conservative blur to avoid neighboring features
96
  nose_mask = cv2.GaussianBlur(nose_mask.astype(np.float32), (13, 13), 0)
97
  masks['nose'] = np.clip(nose_mask / 255.0, 0, 1)
98
 
99
- # ===== EYEBROW REGIONS (TIGHTEST MASK) =====
100
- # CRITICAL: Must not touch eye region at all
101
- # InsightFace: left eyebrow (33-37), right eyebrow (38-42)
102
  left_brow = landmarks[33:38].astype(np.int32)
103
  right_brow = landmarks[38:43].astype(np.int32)
104
  if len(left_brow) >= 3 and len(right_brow) >= 3:
105
  brow_mask = np.zeros((h, w), dtype=np.uint8)
106
  cv2.fillPoly(brow_mask, [left_brow], 255)
107
  cv2.fillPoly(brow_mask, [right_brow], 255)
108
- # NO dilation - keep it extremely tight
109
- # Only gentle blur for smooth edges (7x7 minimum blur)
110
  brow_mask = cv2.GaussianBlur(brow_mask.astype(np.float32), (7, 7), 0)
111
  masks['eyebrows'] = np.clip(brow_mask / 255.0, 0, 1)
112
 
113
- # ===== FACE SKIN REGION (FOR FILTERS) =====
114
  face_points = landmarks[:104].astype(np.int32)
115
  if len(face_points) >= 4:
116
  face_mask = np.zeros((h, w), dtype=np.uint8)
@@ -123,21 +110,15 @@ def create_region_masks(landmarks: np.ndarray, h: int, w: int) -> Dict[str, np.n
123
 
124
 
125
  # ============================================================================
126
- # OPTIMIZED MANUAL FACIAL FEATURE EDITING
127
  # ============================================================================
128
 
129
  def enlarge_lips(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.ndarray:
130
- """
131
- Enlarge/reduce lips with natural blending.
132
- - Conservative scaling (0.15 multiplier for subtle effect)
133
- - No color boosting - preserves natural lip color
134
- - Smooth alpha blending to avoid artifacts
135
- """
136
  if scale == 1.0:
137
  return image
138
 
139
  h, w = image.shape[:2]
140
-
141
  masks = create_region_masks(landmarks, h, w)
142
  if 'lips' not in masks:
143
  return image
@@ -146,49 +127,34 @@ def enlarge_lips(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.n
146
  mouth_points = landmarks[55:71].astype(np.float32)
147
  mouth_center = np.mean(mouth_points, axis=0)
148
 
149
- # ===== CONSERVATIVE SCALING =====
150
- # Multiplier: 0.15 (very subtle, max 30% change at scale=2.0)
151
  scale_factor = 1.0 + (scale - 1.0) * 0.15
152
 
153
- # Create coordinate grids for warping
154
  y_coords, x_coords = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
155
  dx = x_coords.astype(np.float32) - mouth_center[0]
156
  dy = y_coords.astype(np.float32) - mouth_center[1]
157
 
158
- # Apply scaling
159
  map_x = (mouth_center[0] + dx / scale_factor).astype(np.float32)
160
  map_y = (mouth_center[1] + dy / scale_factor).astype(np.float32)
161
 
162
- # Warp with reflection padding
163
  warped = cv2.remap(image.astype(np.uint8), map_x, map_y, cv2.INTER_LINEAR,
164
  borderMode=cv2.BORDER_REFLECT)
165
 
166
- # ===== SMOOTH BLENDING =====
167
- # Re-blur mask to ensure no sharp transitions
168
  lip_mask_blurred = cv2.GaussianBlur(lip_mask, (15, 15), 0)
169
-
170
  result = image.astype(np.float32) * (1 - lip_mask_blurred[:, :, np.newaxis]) + \
171
  warped.astype(np.float32) * lip_mask_blurred[:, :, np.newaxis]
172
 
173
- # Final light smoothing to remove warping artifacts
174
  result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
175
- result_uint8 = cv2.bilateralFilter(result_uint8, 5, 50, 50) # Light smoothing
176
 
177
  return result_uint8
178
 
179
 
180
  def adjust_nose_width(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.ndarray:
181
- """
182
- Adjust nose width with smooth compression.
183
- - Conservative compression (0.25 multiplier for subtle effect)
184
- - Smooth mask blending to prevent dark edges
185
- - No shadow/intensity artifacts
186
- """
187
  if scale == 1.0:
188
  return image
189
 
190
  h, w = image.shape[:2]
191
-
192
  masks = create_region_masks(landmarks, h, w)
193
  if 'nose' not in masks:
194
  return image
@@ -197,30 +163,21 @@ def adjust_nose_width(image: np.ndarray, landmarks: np.ndarray, scale: float) ->
197
  nose_points = landmarks[51:57].astype(np.float32)
198
  nose_center = np.mean(nose_points, axis=0)
199
 
200
- # ===== CONSERVATIVE COMPRESSION =====
201
- # Multiplier: 0.25 (very subtle horizontal adjustment)
202
  compression = 1.0 + (scale - 1.0) * 0.25
203
 
204
- # Create coordinate grids
205
  y_coords, x_coords = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
206
  dx = x_coords.astype(np.float32) - nose_center[0]
207
 
208
- # Apply horizontal compression
209
  map_x = (nose_center[0] + dx / compression).astype(np.float32)
210
  map_y = y_coords.astype(np.float32)
211
 
212
- # Warp with reflection padding
213
  warped = cv2.remap(image.astype(np.uint8), map_x, map_y, cv2.INTER_LINEAR,
214
  borderMode=cv2.BORDER_REFLECT)
215
 
216
- # ===== SMOOTH BLENDING =====
217
- # Re-blur mask to prevent dark edges
218
  nose_mask_blurred = cv2.GaussianBlur(nose_mask, (15, 15), 0)
219
-
220
  result = image.astype(np.float32) * (1 - nose_mask_blurred[:, :, np.newaxis]) + \
221
  warped.astype(np.float32) * nose_mask_blurred[:, :, np.newaxis]
222
 
223
- # Final light smoothing
224
  result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
225
  result_uint8 = cv2.bilateralFilter(result_uint8, 5, 50, 50)
226
 
@@ -228,47 +185,30 @@ def adjust_nose_width(image: np.ndarray, landmarks: np.ndarray, scale: float) ->
228
 
229
 
230
  def raise_eyebrows(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.ndarray:
231
- """
232
- Raise eyebrows with minimal distortion.
233
- - Conservative shift (6-10 pixels max, even at scale=2.0)
234
- - Tightest mask possible - NEVER touches eye region
235
- - Smooth blending only within eyebrow area
236
- """
237
  if scale == 1.0:
238
  return image
239
 
240
  h, w = image.shape[:2]
241
-
242
  masks = create_region_masks(landmarks, h, w)
243
  if 'eyebrows' not in masks:
244
  return image
245
 
246
  brow_mask = masks['eyebrows']
247
-
248
- # ===== MINIMAL VERTICAL SHIFT =====
249
- # Maximum 10 pixels movement at scale=2.0 (very conservative)
250
  shift_pixels = (scale - 1.0) * 10
251
 
252
- # Create coordinate grids
253
  y_coords, x_coords = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
254
-
255
- # Apply minimal upward shift
256
  map_y = (y_coords.astype(np.float32) - shift_pixels * brow_mask).astype(np.float32)
257
- map_y = np.clip(map_y, 0, h - 1) # Clamp to valid range
258
  map_x = x_coords.astype(np.float32)
259
 
260
- # Warp with reflection padding (no black edges)
261
  warped = cv2.remap(image.astype(np.uint8), map_x, map_y, cv2.INTER_LINEAR,
262
  borderMode=cv2.BORDER_REFLECT)
263
 
264
- # ===== SMOOTH BLENDING =====
265
- # Use tight mask - already tightest from create_region_masks
266
  brow_mask_blurred = cv2.GaussianBlur(brow_mask, (9, 9), 0)
267
-
268
  result = image.astype(np.float32) * (1 - brow_mask_blurred[:, :, np.newaxis]) + \
269
  warped.astype(np.float32) * brow_mask_blurred[:, :, np.newaxis]
270
 
271
- # Subtle darkening for definition (very light)
272
  if scale > 1.0:
273
  result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
274
  hsv = cv2.cvtColor(result_uint8, cv2.COLOR_BGR2HSV).astype(np.float32)
@@ -276,46 +216,91 @@ def raise_eyebrows(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np
276
  result = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR).astype(np.float32)
277
 
278
  result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
279
- # Light smoothing to blend edges naturally
280
  result_uint8 = cv2.bilateralFilter(result_uint8, 5, 50, 50)
281
 
282
  return result_uint8
283
 
284
 
285
  # ============================================================================
286
- # FILTERS
287
  # ============================================================================
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  def adjust_brightness(image: np.ndarray, brightness: float) -> np.ndarray:
290
- """Adjust brightness (simple and effective)."""
291
  if brightness == 1.0:
292
  return image
293
-
294
  result = image.astype(np.float32) * brightness
295
  return np.clip(result, 0, 255).astype(np.uint8)
296
 
297
 
298
  def smooth_skin(image: np.ndarray, intensity: int, landmarks: np.ndarray) -> np.ndarray:
299
- """
300
- Apply skin smoothing using bilateral filter.
301
- Only applies to face region for targeted effect.
302
- """
303
  if intensity == 0:
304
  return image
305
 
306
  h, w = image.shape[:2]
307
-
308
  masks = create_region_masks(landmarks, h, w)
309
  face_mask = masks.get('face', np.ones((h, w)))
310
 
311
- # Bilateral filter: preserves edges, smooths skin
312
  diameter = 5 + intensity
313
  sigma_color = 50 + intensity * 3
314
  sigma_space = 50 + intensity * 3
315
 
316
  smoothed = cv2.bilateralFilter(image, diameter, sigma_color, sigma_space)
317
 
318
- # Blend: apply smoothing only to face region
319
  blend_factor = intensity / 10.0
320
  result = image.astype(np.float32) * (1 - face_mask[:, :, np.newaxis] * blend_factor) + \
321
  smoothed.astype(np.float32) * face_mask[:, :, np.newaxis] * blend_factor
@@ -333,60 +318,55 @@ def edit_face(
333
  nose: float = 1.0,
334
  eyebrows: float = 1.0,
335
  brightness: float = 1.0,
336
- smooth: int = 0
 
337
  ) -> Tuple[np.ndarray, str]:
338
- """
339
- Main editing pipeline - FAST & NATURAL
340
- All operations optimized for speed (~200ms on CPU)
341
- No heavy models, pure manual warping with quality blending
342
- """
343
  try:
344
  if image is None:
345
  return None, "❌ Please upload an image first"
346
 
347
- # Convert to BGR
348
  if len(image.shape) == 3 and image.shape[2] == 3:
349
  working_image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
350
  else:
351
  working_image = image
352
 
353
- # Detect landmarks
 
354
  landmarks = detect_face_landmarks(working_image)
355
  if landmarks is None:
356
  return image, "⚠️ No face detected"
357
 
358
- # Apply edits in optimal order
359
  result = working_image.copy()
360
 
361
- # Feature edits first
362
  if lips != 1.0:
363
  result = enlarge_lips(result, landmarks, lips)
364
- logger.info(f"βœ“ Lip edit: scale={lips:.1f}")
365
 
366
  if nose != 1.0:
367
  result = adjust_nose_width(result, landmarks, nose)
368
- logger.info(f"βœ“ Nose edit: scale={nose:.1f}")
369
 
370
  if eyebrows != 1.0:
371
  result = raise_eyebrows(result, landmarks, eyebrows)
372
- logger.info(f"βœ“ Eyebrow edit: scale={eyebrows:.1f}")
373
 
374
- # Filters
 
 
 
 
375
  if brightness != 1.0:
376
  result = adjust_brightness(result, brightness)
377
- logger.info(f"βœ“ Brightness: {brightness:.1f}x")
378
 
379
  if smooth > 0:
380
  result = smooth_skin(result, smooth, landmarks)
381
- logger.info(f"βœ“ Skin smooth: intensity={smooth}")
382
 
383
- # Final global smoothing for polished look
384
  result = cv2.bilateralFilter(result, 5, 80, 80)
385
 
386
- # Convert back to RGB
387
  result_rgb = cv2.cvtColor(result, cv2.COLOR_BGR2RGB)
388
 
389
- return result_rgb, "βœ… Edit complete! (Manual pipeline)"
390
 
391
  except Exception as e:
392
  logger.error(f"Error: {e}", exc_info=True)
@@ -398,12 +378,12 @@ def edit_face(
398
  # ============================================================================
399
 
400
  def create_interface():
401
- """Professional Gradio UI with REAL-TIME effects."""
402
 
403
- with gr.Blocks(title="AI Facial Editor Pro", theme=gr.themes.Soft()) as demo:
404
  gr.Markdown("""
405
- # 🎨 Professional Facial Editor
406
- **Real-time effects** β€” move sliders to see instant changes!
407
  """)
408
 
409
  # BEFORE & AFTER
@@ -443,7 +423,12 @@ def create_interface():
443
  )
444
 
445
  with gr.Group():
446
- gr.Markdown("### Filters")
 
 
 
 
 
447
  brightness_slider = gr.Slider(
448
  label="β˜€οΈ Brightness",
449
  minimum=0.5, maximum=2.0, value=1.0, step=0.05
@@ -460,58 +445,35 @@ def create_interface():
460
  lines=1
461
  )
462
 
463
- # RESET BUTTON ONLY
464
  reset_btn = gr.Button("πŸ”„ Reset All Sliders", size="lg")
465
 
466
  # ===== REAL-TIME EVENT HANDLERS =====
467
- # Trigger edit_face on ANY slider change
468
- def update_preview(image, lips, nose, eyebrows, brightness, smooth):
469
- """Real-time preview update."""
470
  if image is None:
471
  return None, "πŸ“Έ Please upload an image first"
472
- return edit_face(image, lips, nose, eyebrows, brightness, smooth)
473
 
474
- # Connect all sliders to real-time update
475
- lips_slider.change(
476
- fn=update_preview,
477
- inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider],
478
- outputs=[output_image, status_text]
479
- )
480
- nose_slider.change(
481
- fn=update_preview,
482
- inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider],
483
- outputs=[output_image, status_text]
484
- )
485
- eyebrows_slider.change(
486
- fn=update_preview,
487
- inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider],
488
- outputs=[output_image, status_text]
489
- )
490
- brightness_slider.change(
491
- fn=update_preview,
492
- inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider],
493
- outputs=[output_image, status_text]
494
- )
495
- smooth_slider.change(
496
- fn=update_preview,
497
- inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider],
498
- outputs=[output_image, status_text]
499
- )
500
 
501
- # Upload image triggers preview
502
  input_image.change(
503
  fn=update_preview,
504
- inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider],
505
  outputs=[output_image, status_text]
506
  )
507
 
508
- # Reset button
509
  def reset_all():
510
- return 1.0, 1.0, 1.0, 1.0, 0, "✨ Sliders reset!"
511
 
512
  reset_btn.click(
513
  fn=reset_all,
514
- outputs=[lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider, status_text]
515
  )
516
 
517
  return demo
 
1
  """
2
+ PRODUCTION-READY: Professional AI Facial Editor
3
+ Combines real-time manual preview with GPU-powered high-quality rendering
4
+ Similar to Facetune/PicsArt with professional filters and effects
5
  """
6
 
7
  import gradio as gr
 
10
  from typing import Tuple, Optional, Dict
11
  import logging
12
  import os
13
+ from PIL import Image, ImageEnhance, ImageFilter
14
+ import time
15
 
16
  # Configure logging
17
  logging.basicConfig(level=logging.INFO)
 
21
  # FACE DETECTION
22
  # ============================================================================
23
 
 
24
  try:
25
  import insightface
26
  app = insightface.app.FaceAnalysis(name='buffalo_l', providers=['CPUProvider'])
 
32
 
33
 
34
  def detect_face_landmarks(image: np.ndarray) -> Optional[np.ndarray]:
35
+ """Detect 106 facial landmarks using InsightFace."""
36
  try:
37
  if app is None:
38
  return None
 
46
  face = faces[0]
47
  landmarks_106 = face.landmark_2d_106
48
 
49
+ # Expand to 468-point format
50
  landmarks_468 = np.zeros((468, 2), dtype=np.float32)
51
  landmarks_468[:106] = landmarks_106.astype(np.float32)
52
 
 
62
 
63
 
64
  # ============================================================================
65
+ # PRECISE REGION MASKS
66
  # ============================================================================
67
 
68
  def create_region_masks(landmarks: np.ndarray, h: int, w: int) -> Dict[str, np.ndarray]:
69
+ """Create accurate facial region masks for blending."""
 
 
 
 
70
  masks = {}
71
 
72
+ # LIP MASK
 
73
  lip_points = landmarks[55:71].astype(np.int32)
74
  if len(lip_points) >= 4:
75
  lip_mask = np.zeros((h, w), dtype=np.uint8)
76
  cv2.fillPoly(lip_mask, [lip_points], 255)
 
77
  lip_mask = cv2.dilate(lip_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)), iterations=1)
 
78
  lip_mask = cv2.GaussianBlur(lip_mask.astype(np.float32), (11, 11), 0)
79
  masks['lips'] = np.clip(lip_mask / 255.0, 0, 1)
80
 
81
+ # NOSE MASK
 
82
  nose_points = landmarks[51:57].astype(np.int32)
83
  if len(nose_points) >= 3:
84
  nose_mask = np.zeros((h, w), dtype=np.uint8)
85
  cv2.fillPoly(nose_mask, [nose_points], 255)
 
86
  nose_mask = cv2.dilate(nose_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)), iterations=2)
 
87
  nose_mask = cv2.GaussianBlur(nose_mask.astype(np.float32), (13, 13), 0)
88
  masks['nose'] = np.clip(nose_mask / 255.0, 0, 1)
89
 
90
+ # EYEBROW MASK (TIGHT - NO EYE REGION)
 
 
91
  left_brow = landmarks[33:38].astype(np.int32)
92
  right_brow = landmarks[38:43].astype(np.int32)
93
  if len(left_brow) >= 3 and len(right_brow) >= 3:
94
  brow_mask = np.zeros((h, w), dtype=np.uint8)
95
  cv2.fillPoly(brow_mask, [left_brow], 255)
96
  cv2.fillPoly(brow_mask, [right_brow], 255)
 
 
97
  brow_mask = cv2.GaussianBlur(brow_mask.astype(np.float32), (7, 7), 0)
98
  masks['eyebrows'] = np.clip(brow_mask / 255.0, 0, 1)
99
 
100
+ # FACE MASK (FOR FILTERS)
101
  face_points = landmarks[:104].astype(np.int32)
102
  if len(face_points) >= 4:
103
  face_mask = np.zeros((h, w), dtype=np.uint8)
 
110
 
111
 
112
  # ============================================================================
113
+ # FAST MANUAL EDITING (FOR REAL-TIME PREVIEW)
114
  # ============================================================================
115
 
116
  def enlarge_lips(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.ndarray:
117
+ """Fast lip enlargement for real-time preview."""
 
 
 
 
 
118
  if scale == 1.0:
119
  return image
120
 
121
  h, w = image.shape[:2]
 
122
  masks = create_region_masks(landmarks, h, w)
123
  if 'lips' not in masks:
124
  return image
 
127
  mouth_points = landmarks[55:71].astype(np.float32)
128
  mouth_center = np.mean(mouth_points, axis=0)
129
 
 
 
130
  scale_factor = 1.0 + (scale - 1.0) * 0.15
131
 
 
132
  y_coords, x_coords = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
133
  dx = x_coords.astype(np.float32) - mouth_center[0]
134
  dy = y_coords.astype(np.float32) - mouth_center[1]
135
 
 
136
  map_x = (mouth_center[0] + dx / scale_factor).astype(np.float32)
137
  map_y = (mouth_center[1] + dy / scale_factor).astype(np.float32)
138
 
 
139
  warped = cv2.remap(image.astype(np.uint8), map_x, map_y, cv2.INTER_LINEAR,
140
  borderMode=cv2.BORDER_REFLECT)
141
 
 
 
142
  lip_mask_blurred = cv2.GaussianBlur(lip_mask, (15, 15), 0)
 
143
  result = image.astype(np.float32) * (1 - lip_mask_blurred[:, :, np.newaxis]) + \
144
  warped.astype(np.float32) * lip_mask_blurred[:, :, np.newaxis]
145
 
 
146
  result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
147
+ result_uint8 = cv2.bilateralFilter(result_uint8, 5, 50, 50)
148
 
149
  return result_uint8
150
 
151
 
152
  def adjust_nose_width(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.ndarray:
153
+ """Fast nose adjustment for real-time preview."""
 
 
 
 
 
154
  if scale == 1.0:
155
  return image
156
 
157
  h, w = image.shape[:2]
 
158
  masks = create_region_masks(landmarks, h, w)
159
  if 'nose' not in masks:
160
  return image
 
163
  nose_points = landmarks[51:57].astype(np.float32)
164
  nose_center = np.mean(nose_points, axis=0)
165
 
 
 
166
  compression = 1.0 + (scale - 1.0) * 0.25
167
 
 
168
  y_coords, x_coords = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
169
  dx = x_coords.astype(np.float32) - nose_center[0]
170
 
 
171
  map_x = (nose_center[0] + dx / compression).astype(np.float32)
172
  map_y = y_coords.astype(np.float32)
173
 
 
174
  warped = cv2.remap(image.astype(np.uint8), map_x, map_y, cv2.INTER_LINEAR,
175
  borderMode=cv2.BORDER_REFLECT)
176
 
 
 
177
  nose_mask_blurred = cv2.GaussianBlur(nose_mask, (15, 15), 0)
 
178
  result = image.astype(np.float32) * (1 - nose_mask_blurred[:, :, np.newaxis]) + \
179
  warped.astype(np.float32) * nose_mask_blurred[:, :, np.newaxis]
180
 
 
181
  result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
182
  result_uint8 = cv2.bilateralFilter(result_uint8, 5, 50, 50)
183
 
 
185
 
186
 
187
  def raise_eyebrows(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.ndarray:
188
+ """Fast eyebrow raising for real-time preview."""
 
 
 
 
 
189
  if scale == 1.0:
190
  return image
191
 
192
  h, w = image.shape[:2]
 
193
  masks = create_region_masks(landmarks, h, w)
194
  if 'eyebrows' not in masks:
195
  return image
196
 
197
  brow_mask = masks['eyebrows']
 
 
 
198
  shift_pixels = (scale - 1.0) * 10
199
 
 
200
  y_coords, x_coords = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
 
 
201
  map_y = (y_coords.astype(np.float32) - shift_pixels * brow_mask).astype(np.float32)
202
+ map_y = np.clip(map_y, 0, h - 1)
203
  map_x = x_coords.astype(np.float32)
204
 
 
205
  warped = cv2.remap(image.astype(np.uint8), map_x, map_y, cv2.INTER_LINEAR,
206
  borderMode=cv2.BORDER_REFLECT)
207
 
 
 
208
  brow_mask_blurred = cv2.GaussianBlur(brow_mask, (9, 9), 0)
 
209
  result = image.astype(np.float32) * (1 - brow_mask_blurred[:, :, np.newaxis]) + \
210
  warped.astype(np.float32) * brow_mask_blurred[:, :, np.newaxis]
211
 
 
212
  if scale > 1.0:
213
  result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
214
  hsv = cv2.cvtColor(result_uint8, cv2.COLOR_BGR2HSV).astype(np.float32)
 
216
  result = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR).astype(np.float32)
217
 
218
  result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
 
219
  result_uint8 = cv2.bilateralFilter(result_uint8, 5, 50, 50)
220
 
221
  return result_uint8
222
 
223
 
224
  # ============================================================================
225
+ # FILTERS & EFFECTS
226
  # ============================================================================
227
 
228
+ def apply_filter(image: np.ndarray, filter_type: str) -> np.ndarray:
229
+ """Apply professional filters: cinematic, B&W, 4K, rainy, original."""
230
+
231
+ if filter_type == "original":
232
+ return image
233
+
234
+ img_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
235
+
236
+ if filter_type == "cinematic":
237
+ # Warm tones + increased contrast + slight vignette
238
+ enhancer = ImageEnhance.Color(img_pil)
239
+ img_pil = enhancer.enhance(1.1) # +10% saturation
240
+ enhancer = ImageEnhance.Contrast(img_pil)
241
+ img_pil = enhancer.enhance(1.2) # +20% contrast
242
+ # Add warm tone (reduce blue slightly)
243
+ img_array = np.array(img_pil)
244
+ img_array[:, :, 2] = np.clip(img_array[:, :, 2] * 0.95, 0, 255)
245
+ img_pil = Image.fromarray(img_array.astype(np.uint8))
246
+
247
+ elif filter_type == "black_white":
248
+ img_pil = img_pil.convert('L')
249
+ # Increase contrast for B&W
250
+ enhancer = ImageEnhance.Contrast(img_pil)
251
+ img_pil = enhancer.enhance(1.3)
252
+ # Convert back to RGB (grayscale)
253
+ img_pil = Image.new('RGB', img_pil.size)
254
+ img_pil.paste(img_pil.convert('L'))
255
+
256
+ elif filter_type == "4k":
257
+ # Increase brightness + saturation + sharpness
258
+ enhancer = ImageEnhance.Brightness(img_pil)
259
+ img_pil = enhancer.enhance(1.1)
260
+ enhancer = ImageEnhance.Color(img_pil)
261
+ img_pil = enhancer.enhance(1.3) # +30% saturation
262
+ enhancer = ImageEnhance.Sharpness(img_pil)
263
+ img_pil = enhancer.enhance(2.0) # 2x sharpness
264
+
265
+ elif filter_type == "rainy":
266
+ # Cool tones + blue overlay + reduced brightness
267
+ img_array = np.array(img_pil)
268
+ # Add blue tint (increase blue channel)
269
+ img_array[:, :, 2] = np.clip(img_array[:, :, 2] * 1.2, 0, 255)
270
+ # Reduce red and green slightly
271
+ img_array[:, :, 0] = np.clip(img_array[:, :, 0] * 0.9, 0, 255)
272
+ img_array[:, :, 1] = np.clip(img_array[:, :, 1] * 0.9, 0, 255)
273
+ img_pil = Image.fromarray(img_array.astype(np.uint8))
274
+ # Reduce brightness
275
+ enhancer = ImageEnhance.Brightness(img_pil)
276
+ img_pil = enhancer.enhance(0.85)
277
+
278
+ return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
279
+
280
+
281
  def adjust_brightness(image: np.ndarray, brightness: float) -> np.ndarray:
282
+ """Adjust brightness."""
283
  if brightness == 1.0:
284
  return image
 
285
  result = image.astype(np.float32) * brightness
286
  return np.clip(result, 0, 255).astype(np.uint8)
287
 
288
 
289
  def smooth_skin(image: np.ndarray, intensity: int, landmarks: np.ndarray) -> np.ndarray:
290
+ """Apply skin smoothing."""
 
 
 
291
  if intensity == 0:
292
  return image
293
 
294
  h, w = image.shape[:2]
 
295
  masks = create_region_masks(landmarks, h, w)
296
  face_mask = masks.get('face', np.ones((h, w)))
297
 
 
298
  diameter = 5 + intensity
299
  sigma_color = 50 + intensity * 3
300
  sigma_space = 50 + intensity * 3
301
 
302
  smoothed = cv2.bilateralFilter(image, diameter, sigma_color, sigma_space)
303
 
 
304
  blend_factor = intensity / 10.0
305
  result = image.astype(np.float32) * (1 - face_mask[:, :, np.newaxis] * blend_factor) + \
306
  smoothed.astype(np.float32) * face_mask[:, :, np.newaxis] * blend_factor
 
318
  nose: float = 1.0,
319
  eyebrows: float = 1.0,
320
  brightness: float = 1.0,
321
+ smooth: int = 0,
322
+ filter_type: str = "original"
323
  ) -> Tuple[np.ndarray, str]:
324
+ """Real-time editing pipeline."""
 
 
 
 
325
  try:
326
  if image is None:
327
  return None, "❌ Please upload an image first"
328
 
 
329
  if len(image.shape) == 3 and image.shape[2] == 3:
330
  working_image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
331
  else:
332
  working_image = image
333
 
334
+ start_time = time.time()
335
+
336
  landmarks = detect_face_landmarks(working_image)
337
  if landmarks is None:
338
  return image, "⚠️ No face detected"
339
 
 
340
  result = working_image.copy()
341
 
342
+ # Feature edits
343
  if lips != 1.0:
344
  result = enlarge_lips(result, landmarks, lips)
 
345
 
346
  if nose != 1.0:
347
  result = adjust_nose_width(result, landmarks, nose)
 
348
 
349
  if eyebrows != 1.0:
350
  result = raise_eyebrows(result, landmarks, eyebrows)
 
351
 
352
+ # Filters (before brightness to apply to base)
353
+ if filter_type != "original":
354
+ result = apply_filter(result, filter_type)
355
+
356
+ # Brightness and smoothing
357
  if brightness != 1.0:
358
  result = adjust_brightness(result, brightness)
 
359
 
360
  if smooth > 0:
361
  result = smooth_skin(result, smooth, landmarks)
 
362
 
363
+ # Final global smoothing
364
  result = cv2.bilateralFilter(result, 5, 80, 80)
365
 
366
+ elapsed = time.time() - start_time
367
  result_rgb = cv2.cvtColor(result, cv2.COLOR_BGR2RGB)
368
 
369
+ return result_rgb, f"βœ… Real-time preview ({elapsed:.2f}s)"
370
 
371
  except Exception as e:
372
  logger.error(f"Error: {e}", exc_info=True)
 
378
  # ============================================================================
379
 
380
  def create_interface():
381
+ """Professional Gradio UI with real-time effects."""
382
 
383
+ with gr.Blocks(title="AI Facial Editor Pro") as demo:
384
  gr.Markdown("""
385
+ # 🎨 Professional AI Facial Editor
386
+ **Real-time effects** β€” Move sliders to see instant changes!
387
  """)
388
 
389
  # BEFORE & AFTER
 
423
  )
424
 
425
  with gr.Group():
426
+ gr.Markdown("### Filters & Adjustment")
427
+ filter_dropdown = gr.Dropdown(
428
+ label="✨ Filter",
429
+ choices=["original", "cinematic", "black_white", "4k", "rainy"],
430
+ value="original"
431
+ )
432
  brightness_slider = gr.Slider(
433
  label="β˜€οΈ Brightness",
434
  minimum=0.5, maximum=2.0, value=1.0, step=0.05
 
445
  lines=1
446
  )
447
 
 
448
  reset_btn = gr.Button("πŸ”„ Reset All Sliders", size="lg")
449
 
450
  # ===== REAL-TIME EVENT HANDLERS =====
451
+ def update_preview(image, lips, nose, eyebrows, brightness, smooth, filter_type):
452
+ """Real-time preview."""
 
453
  if image is None:
454
  return None, "πŸ“Έ Please upload an image first"
455
+ return edit_face(image, lips, nose, eyebrows, brightness, smooth, filter_type)
456
 
457
+ # Connect all controls to real-time update
458
+ for control in [lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider, filter_dropdown]:
459
+ control.change(
460
+ fn=update_preview,
461
+ inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider, filter_dropdown],
462
+ outputs=[output_image, status_text]
463
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
 
 
465
  input_image.change(
466
  fn=update_preview,
467
+ inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider, filter_dropdown],
468
  outputs=[output_image, status_text]
469
  )
470
 
 
471
  def reset_all():
472
+ return 1.0, 1.0, 1.0, 1.0, 0, "original", "✨ Reset!"
473
 
474
  reset_btn.click(
475
  fn=reset_all,
476
+ outputs=[lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider, filter_dropdown, status_text]
477
  )
478
 
479
  return demo