Hamza4100's picture
Update app.py
25e0198 verified
"""
PRODUCTION-READY: Professional AI Facial Editor
Combines real-time manual preview with GPU-powered high-quality rendering
Similar to Facetune/PicsArt with professional filters and effects
"""
import gradio as gr
import cv2
import numpy as np
from typing import Tuple, Optional, Dict
import logging
import os
from PIL import Image, ImageEnhance, ImageFilter
import time
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ============================================================================
# FACE DETECTION
# ============================================================================
try:
import insightface
app = insightface.app.FaceAnalysis(name='buffalo_l', providers=['CPUProvider'])
app.prepare(ctx_id=-1, det_size=(640, 640))
logger.info("βœ“ InsightFace loaded")
except Exception as e:
logger.error(f"InsightFace error: {e}")
app = None
def detect_face_landmarks(image: np.ndarray) -> Optional[np.ndarray]:
"""Detect 106 facial landmarks using InsightFace."""
try:
if app is None:
return None
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
faces = app.get(rgb_image)
if len(faces) == 0:
return None
face = faces[0]
landmarks_106 = face.landmark_2d_106
# Expand to 468-point format
landmarks_468 = np.zeros((468, 2), dtype=np.float32)
landmarks_468[:106] = landmarks_106.astype(np.float32)
for i in range(106, 468):
idx = i % 106
landmarks_468[i] = landmarks_106[idx]
return landmarks_468
except Exception as e:
logger.error(f"Landmark detection error: {e}")
return None
# ============================================================================
# PRECISE REGION MASKS
# ============================================================================
def create_region_masks(landmarks: np.ndarray, h: int, w: int) -> Dict[str, np.ndarray]:
"""Create accurate facial region masks for blending."""
masks = {}
# LIP MASK
lip_points = landmarks[55:71].astype(np.int32)
if len(lip_points) >= 4:
lip_mask = np.zeros((h, w), dtype=np.uint8)
cv2.fillPoly(lip_mask, [lip_points], 255)
lip_mask = cv2.dilate(lip_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)), iterations=1)
lip_mask = cv2.GaussianBlur(lip_mask.astype(np.float32), (11, 11), 0)
masks['lips'] = np.clip(lip_mask / 255.0, 0, 1)
# NOSE MASK
nose_points = landmarks[51:57].astype(np.int32)
if len(nose_points) >= 3:
nose_mask = np.zeros((h, w), dtype=np.uint8)
cv2.fillPoly(nose_mask, [nose_points], 255)
nose_mask = cv2.dilate(nose_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)), iterations=2)
nose_mask = cv2.GaussianBlur(nose_mask.astype(np.float32), (13, 13), 0)
masks['nose'] = np.clip(nose_mask / 255.0, 0, 1)
# EYEBROW MASK (TIGHT - NO EYE REGION)
left_brow = landmarks[33:38].astype(np.int32)
right_brow = landmarks[38:43].astype(np.int32)
if len(left_brow) >= 3 and len(right_brow) >= 3:
brow_mask = np.zeros((h, w), dtype=np.uint8)
cv2.fillPoly(brow_mask, [left_brow], 255)
cv2.fillPoly(brow_mask, [right_brow], 255)
brow_mask = cv2.GaussianBlur(brow_mask.astype(np.float32), (7, 7), 0)
masks['eyebrows'] = np.clip(brow_mask / 255.0, 0, 1)
# FACE MASK (FOR FILTERS)
face_points = landmarks[:104].astype(np.int32)
if len(face_points) >= 4:
face_mask = np.zeros((h, w), dtype=np.uint8)
hull = cv2.convexHull(face_points)
cv2.fillPoly(face_mask, [hull], 255)
face_mask = cv2.GaussianBlur(face_mask.astype(np.float32), (25, 25), 0)
masks['face'] = np.clip(face_mask / 255.0, 0, 1)
return masks
# ============================================================================
# FAST MANUAL EDITING (FOR REAL-TIME PREVIEW)
# ============================================================================
def enlarge_lips(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.ndarray:
"""Fast lip enlargement for real-time preview."""
if scale == 1.0:
return image
h, w = image.shape[:2]
masks = create_region_masks(landmarks, h, w)
if 'lips' not in masks:
return image
lip_mask = masks['lips']
mouth_points = landmarks[55:71].astype(np.float32)
mouth_center = np.mean(mouth_points, axis=0)
scale_factor = 1.0 + (scale - 1.0) * 0.15
y_coords, x_coords = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
dx = x_coords.astype(np.float32) - mouth_center[0]
dy = y_coords.astype(np.float32) - mouth_center[1]
map_x = (mouth_center[0] + dx / scale_factor).astype(np.float32)
map_y = (mouth_center[1] + dy / scale_factor).astype(np.float32)
warped = cv2.remap(image.astype(np.uint8), map_x, map_y, cv2.INTER_LINEAR,
borderMode=cv2.BORDER_REFLECT)
lip_mask_blurred = cv2.GaussianBlur(lip_mask, (15, 15), 0)
result = image.astype(np.float32) * (1 - lip_mask_blurred[:, :, np.newaxis]) + \
warped.astype(np.float32) * lip_mask_blurred[:, :, np.newaxis]
result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
result_uint8 = cv2.bilateralFilter(result_uint8, 5, 50, 50)
return result_uint8
def adjust_nose_width(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.ndarray:
"""Fast nose adjustment for real-time preview."""
if scale == 1.0:
return image
h, w = image.shape[:2]
masks = create_region_masks(landmarks, h, w)
if 'nose' not in masks:
return image
nose_mask = masks['nose']
nose_points = landmarks[51:57].astype(np.float32)
nose_center = np.mean(nose_points, axis=0)
compression = 1.0 + (scale - 1.0) * 0.25
y_coords, x_coords = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
dx = x_coords.astype(np.float32) - nose_center[0]
map_x = (nose_center[0] + dx / compression).astype(np.float32)
map_y = y_coords.astype(np.float32)
warped = cv2.remap(image.astype(np.uint8), map_x, map_y, cv2.INTER_LINEAR,
borderMode=cv2.BORDER_REFLECT)
nose_mask_blurred = cv2.GaussianBlur(nose_mask, (15, 15), 0)
result = image.astype(np.float32) * (1 - nose_mask_blurred[:, :, np.newaxis]) + \
warped.astype(np.float32) * nose_mask_blurred[:, :, np.newaxis]
result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
result_uint8 = cv2.bilateralFilter(result_uint8, 5, 50, 50)
return result_uint8
def raise_eyebrows(image: np.ndarray, landmarks: np.ndarray, scale: float) -> np.ndarray:
"""Fast eyebrow raising for real-time preview."""
if scale == 1.0:
return image
h, w = image.shape[:2]
masks = create_region_masks(landmarks, h, w)
if 'eyebrows' not in masks:
return image
brow_mask = masks['eyebrows']
shift_pixels = (scale - 1.0) * 10
y_coords, x_coords = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
map_y = (y_coords.astype(np.float32) - shift_pixels * brow_mask).astype(np.float32)
map_y = np.clip(map_y, 0, h - 1)
map_x = x_coords.astype(np.float32)
warped = cv2.remap(image.astype(np.uint8), map_x, map_y, cv2.INTER_LINEAR,
borderMode=cv2.BORDER_REFLECT)
brow_mask_blurred = cv2.GaussianBlur(brow_mask, (9, 9), 0)
result = image.astype(np.float32) * (1 - brow_mask_blurred[:, :, np.newaxis]) + \
warped.astype(np.float32) * brow_mask_blurred[:, :, np.newaxis]
if scale > 1.0:
result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
hsv = cv2.cvtColor(result_uint8, cv2.COLOR_BGR2HSV).astype(np.float32)
hsv[:, :, 2] = np.clip(hsv[:, :, 2] * (1 - brow_mask * (scale - 1.0) * 0.08), 0, 255)
result = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR).astype(np.float32)
result_uint8 = np.clip(result, 0, 255).astype(np.uint8)
result_uint8 = cv2.bilateralFilter(result_uint8, 5, 50, 50)
return result_uint8
# ============================================================================
# FILTERS & EFFECTS
# ============================================================================
def apply_filter(image: np.ndarray, filter_type: str) -> np.ndarray:
"""Apply professional filters: cinematic, B&W, 4K, rainy, original."""
if filter_type == "original":
return image
img_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
if filter_type == "cinematic":
# Warm tones + increased contrast + slight vignette
enhancer = ImageEnhance.Color(img_pil)
img_pil = enhancer.enhance(1.1) # +10% saturation
enhancer = ImageEnhance.Contrast(img_pil)
img_pil = enhancer.enhance(1.2) # +20% contrast
# Add warm tone (reduce blue slightly)
img_array = np.array(img_pil)
img_array[:, :, 2] = np.clip(img_array[:, :, 2] * 0.95, 0, 255)
img_pil = Image.fromarray(img_array.astype(np.uint8))
elif filter_type == "black_white":
img_pil = img_pil.convert('L')
# Increase contrast for B&W
enhancer = ImageEnhance.Contrast(img_pil)
img_pil = enhancer.enhance(1.3)
# Convert back to RGB (grayscale)
img_pil = Image.new('RGB', img_pil.size)
img_pil.paste(img_pil.convert('L'))
elif filter_type == "4k":
# Increase brightness + saturation + sharpness
enhancer = ImageEnhance.Brightness(img_pil)
img_pil = enhancer.enhance(1.1)
enhancer = ImageEnhance.Color(img_pil)
img_pil = enhancer.enhance(1.3) # +30% saturation
enhancer = ImageEnhance.Sharpness(img_pil)
img_pil = enhancer.enhance(2.0) # 2x sharpness
elif filter_type == "rainy":
# Cool tones + blue overlay + reduced brightness
img_array = np.array(img_pil)
# Add blue tint (increase blue channel)
img_array[:, :, 2] = np.clip(img_array[:, :, 2] * 1.2, 0, 255)
# Reduce red and green slightly
img_array[:, :, 0] = np.clip(img_array[:, :, 0] * 0.9, 0, 255)
img_array[:, :, 1] = np.clip(img_array[:, :, 1] * 0.9, 0, 255)
img_pil = Image.fromarray(img_array.astype(np.uint8))
# Reduce brightness
enhancer = ImageEnhance.Brightness(img_pil)
img_pil = enhancer.enhance(0.85)
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
def adjust_brightness(image: np.ndarray, brightness: float) -> np.ndarray:
"""Adjust brightness."""
if brightness == 1.0:
return image
result = image.astype(np.float32) * brightness
return np.clip(result, 0, 255).astype(np.uint8)
def smooth_skin(image: np.ndarray, intensity: int, landmarks: np.ndarray) -> np.ndarray:
"""Apply skin smoothing."""
if intensity == 0:
return image
h, w = image.shape[:2]
masks = create_region_masks(landmarks, h, w)
face_mask = masks.get('face', np.ones((h, w)))
diameter = 5 + intensity
sigma_color = 50 + intensity * 3
sigma_space = 50 + intensity * 3
smoothed = cv2.bilateralFilter(image, diameter, sigma_color, sigma_space)
blend_factor = intensity / 10.0
result = image.astype(np.float32) * (1 - face_mask[:, :, np.newaxis] * blend_factor) + \
smoothed.astype(np.float32) * face_mask[:, :, np.newaxis] * blend_factor
return np.clip(result, 0, 255).astype(np.uint8)
# ============================================================================
# MAIN EDITING PIPELINE
# ============================================================================
def edit_face(
image: np.ndarray,
lips: float = 1.0,
nose: float = 1.0,
eyebrows: float = 1.0,
brightness: float = 1.0,
smooth: int = 0,
filter_type: str = "original"
) -> Tuple[np.ndarray, str]:
"""Real-time editing pipeline."""
try:
if image is None:
return None, "❌ Please upload an image first"
if len(image.shape) == 3 and image.shape[2] == 3:
working_image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
else:
working_image = image
start_time = time.time()
landmarks = detect_face_landmarks(working_image)
if landmarks is None:
return image, "⚠️ No face detected"
result = working_image.copy()
# Feature edits
if lips != 1.0:
result = enlarge_lips(result, landmarks, lips)
if nose != 1.0:
result = adjust_nose_width(result, landmarks, nose)
if eyebrows != 1.0:
result = raise_eyebrows(result, landmarks, eyebrows)
# Filters (before brightness to apply to base)
if filter_type != "original":
result = apply_filter(result, filter_type)
# Brightness and smoothing
if brightness != 1.0:
result = adjust_brightness(result, brightness)
if smooth > 0:
result = smooth_skin(result, smooth, landmarks)
# Final global smoothing
result = cv2.bilateralFilter(result, 5, 80, 80)
elapsed = time.time() - start_time
result_rgb = cv2.cvtColor(result, cv2.COLOR_BGR2RGB)
return result_rgb, f"βœ… Real-time preview ({elapsed:.2f}s)"
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
return image, f"❌ Error: {str(e)}"
# ============================================================================
# GRADIO INTERFACE
# ============================================================================
def create_interface():
"""Professional Gradio UI with real-time effects."""
with gr.Blocks(title="AI Facial Editor Pro") as demo:
gr.Markdown("""
# 🎨 Professional AI Facial Editor
**Real-time effects** β€” Move sliders to see instant changes!
""")
# BEFORE & AFTER
gr.Markdown("## πŸ“Έ Before & After")
with gr.Row():
with gr.Column():
gr.Markdown("### Original")
input_image = gr.Image(
label="Upload Face Image",
type="numpy",
sources=["upload", "webcam"]
)
with gr.Column():
gr.Markdown("### Live Preview")
output_image = gr.Image(
label="Real-time Edit",
type="numpy"
)
# CONTROLS
gr.Markdown("## πŸŽ›οΈ Adjust Features (Real-Time)")
with gr.Group():
gr.Markdown("### Facial Features")
lips_slider = gr.Slider(
label="πŸ’‹ Lips Size",
minimum=0.5, maximum=2.0, value=1.0, step=0.05
)
nose_slider = gr.Slider(
label="πŸ‘ƒ Nose Width",
minimum=0.5, maximum=2.0, value=1.0, step=0.05
)
eyebrows_slider = gr.Slider(
label="🀨 Eyebrow Height",
minimum=0.5, maximum=2.0, value=1.0, step=0.05
)
with gr.Group():
gr.Markdown("### Filters & Adjustment")
filter_dropdown = gr.Dropdown(
label="✨ Filter",
choices=["original", "cinematic", "black_white", "4k", "rainy"],
value="original"
)
brightness_slider = gr.Slider(
label="β˜€οΈ Brightness",
minimum=0.5, maximum=2.0, value=1.0, step=0.05
)
smooth_slider = gr.Slider(
label="🧴 Skin Smoothing",
minimum=0, maximum=10, value=0, step=1
)
status_text = gr.Textbox(
label="Status",
interactive=False,
value="πŸ“Έ Upload image β€” effects update in real-time!",
lines=1
)
reset_btn = gr.Button("πŸ”„ Reset All Sliders", size="lg")
# ===== REAL-TIME EVENT HANDLERS =====
def update_preview(image, lips, nose, eyebrows, brightness, smooth, filter_type):
"""Real-time preview."""
if image is None:
return None, "πŸ“Έ Please upload an image first"
return edit_face(image, lips, nose, eyebrows, brightness, smooth, filter_type)
# Connect all controls to real-time update
for control in [lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider, filter_dropdown]:
control.change(
fn=update_preview,
inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider, filter_dropdown],
outputs=[output_image, status_text]
)
input_image.change(
fn=update_preview,
inputs=[input_image, lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider, filter_dropdown],
outputs=[output_image, status_text]
)
def reset_all():
return 1.0, 1.0, 1.0, 1.0, 0, "original", "✨ Reset!"
reset_btn.click(
fn=reset_all,
outputs=[lips_slider, nose_slider, eyebrows_slider, brightness_slider, smooth_slider, filter_dropdown, status_text]
)
return demo
# ============================================================================
# MAIN
# ============================================================================
if __name__ == "__main__":
demo = create_interface()
demo.launch(share=False, theme=gr.themes.Soft())