Spaces:
Sleeping
Sleeping
| """ | |
| 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()) | |