#!/usr/bin/env python3 """ Foundation Virtual Makeup App Dedicated app for applying foundation with adjustable coverage Features: Coverage levels (Light/Medium/Full), Shade matching, Blending options """ import cv2 import numpy as np import gradio as gr from PIL import Image import os from utils import mask_skin, gamma_correction # Foundation shade presets (RGB adjustments) FOUNDATION_SHADES = { "Fair": {"shift": (-10, -5, 0), "description": "Very light skin tones"}, "Light": {"shift": (-5, 0, 5), "description": "Light skin tones"}, "Medium": {"shift": (0, 5, 10), "description": "Medium skin tones"}, "Tan": {"shift": (5, 10, 15), "description": "Tan skin tones"}, "Deep": {"shift": (10, 15, 20), "description": "Deep skin tones"}, "Rich": {"shift": (15, 20, 25), "description": "Rich, dark skin tones"}, } # Coverage level presets COVERAGE_LEVELS = { "Light": { "intensity": 0.25, "gamma": 1.15, "smoothing": 5, "description": "Sheer coverage for natural look" }, "Medium": { "intensity": 0.45, "gamma": 1.30, "smoothing": 7, "description": "Balanced coverage for everyday wear" }, "Full": { "intensity": 0.70, "gamma": 1.50, "smoothing": 9, "description": "Maximum coverage for flawless finish" }, } # Blending options BLENDING_MODES = { "Natural": {"blur_kernel": 11, "feather": 0.9, "description": "Soft, seamless blend"}, "Buildable": {"blur_kernel": 7, "feather": 0.7, "description": "Layered, controlled blend"}, "Airbrushed": {"blur_kernel": 15, "feather": 0.95, "description": "Ultra-smooth, flawless blend"}, } def auto_detect_shade(image): """ Analyze skin tone to suggest the best foundation shade. Returns suggested shade name. """ if image is None: return "Medium" # Get skin mask skin_mask_binary = mask_skin(image) if skin_mask_binary.ndim == 3: skin_mask_binary = skin_mask_binary[:, :, 0] # Extract skin pixels skin_pixels = image[skin_mask_binary > 0] if len(skin_pixels) == 0: return "Medium" # Calculate average skin tone (BGR format) avg_b, avg_g, avg_r = np.mean(skin_pixels, axis=0) # Calculate brightness (luminance) luminance = 0.299 * avg_r + 0.587 * avg_g + 0.114 * avg_b # Map luminance to shade if luminance < 100: return "Rich" elif luminance < 130: return "Deep" elif luminance < 160: return "Tan" elif luminance < 190: return "Medium" elif luminance < 220: return "Light" else: return "Fair" def apply_foundation_advanced(image, shade_name="Medium", coverage="Medium", blending="Natural", warmth_adjust=0): """ Apply foundation with advanced controls. Args: image: Input image (BGR format) shade_name: Foundation shade name coverage: Coverage level (Light/Medium/Full) blending: Blending mode (Natural/Buildable/Airbrushed) warmth_adjust: Warmth adjustment (-20 to +20) Returns: Image with foundation applied """ if image is None: return image # Get skin mask skin_mask_binary = mask_skin(image) if skin_mask_binary.ndim == 3: skin_mask_binary = skin_mask_binary[:, :, 0] # Get parameters shade_info = FOUNDATION_SHADES.get(shade_name, FOUNDATION_SHADES["Medium"]) coverage_info = COVERAGE_LEVELS.get(coverage, COVERAGE_LEVELS["Medium"]) blend_info = BLENDING_MODES.get(blending, BLENDING_MODES["Natural"]) intensity = coverage_info["intensity"] gamma_val = coverage_info["gamma"] smoothing = coverage_info["smoothing"] blur_kernel = blend_info["blur_kernel"] feather = blend_info["feather"] # Apply gamma correction for brightness corrected = gamma_correction(image, gamma_val, coefficient=1) # Apply shade adjustment corrected = corrected.astype(np.float32) b_shift, g_shift, r_shift = shade_info["shift"] corrected[:, :, 0] = np.clip(corrected[:, :, 0] + b_shift, 0, 255) corrected[:, :, 1] = np.clip(corrected[:, :, 1] + g_shift, 0, 255) corrected[:, :, 2] = np.clip(corrected[:, :, 2] + r_shift, 0, 255) # Apply warmth adjustment (affects red channel) if warmth_adjust != 0: warmth_factor = 1.0 + (warmth_adjust / 100.0) corrected[:, :, 2] = np.clip(corrected[:, :, 2] * warmth_factor, 0, 255) corrected = corrected.astype(np.uint8) # Smooth skin texture based on coverage level if smoothing > 0: # Bilateral filter preserves edges while smoothing corrected = cv2.bilateralFilter(corrected, smoothing, 75, 75) # Create feathered mask for seamless blending skin_mask_float = skin_mask_binary.astype(np.float32) # Apply blur for soft edges if blur_kernel % 2 == 0: blur_kernel += 1 skin_mask_float = cv2.GaussianBlur(skin_mask_float, (blur_kernel, blur_kernel), 0) # Apply feathering skin_mask_float = np.power(skin_mask_float, feather) # Expand mask to 3 channels skin_mask_float = np.expand_dims(skin_mask_float, axis=-1) skin_mask_float = np.repeat(skin_mask_float, 3, axis=2) # Blend corrected with original using feathered mask output = image.astype(np.float32) corrected = corrected.astype(np.float32) # Apply intensity-weighted blend only where skin is detected output = output * (1.0 - skin_mask_float * intensity) + corrected * (skin_mask_float * intensity) return output.astype(np.uint8) def process_foundation(image, shade, coverage, blending, warmth, auto_match): """ Process image to apply foundation. Args: image: Input image shade: Foundation shade name coverage: Coverage level blending: Blending mode warmth: Warmth adjustment value auto_match: Whether to auto-detect shade Returns: Tuple of (processed_image, status_message, suggested_shade) """ if image is None: return None, "No image provided", "" # Convert PIL to OpenCV format if isinstance(image, Image.Image): img = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) else: img = image.copy() # Auto-detect shade if requested suggested_shade = "" if auto_match: suggested_shade = auto_detect_shade(img) shade = suggested_shade status_prefix = f"Auto-matched to {suggested_shade} shade. " else: status_prefix = "" # Apply foundation output = apply_foundation_advanced(img, shade, coverage, blending, warmth) # Convert back to RGB output_rgb = cv2.cvtColor(output, cv2.COLOR_BGR2RGB) coverage_info = COVERAGE_LEVELS.get(coverage, COVERAGE_LEVELS["Medium"]) status = (f"{status_prefix}Applied {shade} foundation with {coverage} coverage " f"({coverage_info['description']}) using {blending} blending") return output_rgb, status, suggested_shade # Create Gradio interface with gr.Blocks(title="Foundation Virtual Makeup", theme=gr.themes.Soft()) as demo: gr.Markdown("# ✨ Foundation Virtual Makeup App") gr.Markdown("Apply professional foundation with AI-powered shade matching and customizable coverage") with gr.Row(): with gr.Column(scale=1): # Image upload image_input = gr.Image(label="Upload Your Photo", type="pil") # Shade matching section gr.Markdown("### 🎨 Shade Selection") auto_match_btn = gr.Button("🔍 Auto-Match My Shade", variant="secondary") shade_info_text = gr.Textbox( label="Detected Shade", interactive=False, placeholder="Click 'Auto-Match' to detect your shade" ) shade_dropdown = gr.Dropdown( choices=list(FOUNDATION_SHADES.keys()), value="Medium", label="Foundation Shade", interactive=True ) # Show shade description shade_desc = gr.Markdown( f"*{FOUNDATION_SHADES['Medium']['description']}*", visible=True ) # Coverage level gr.Markdown("### 📊 Coverage Level") coverage_radio = gr.Radio( choices=list(COVERAGE_LEVELS.keys()), value="Medium", label="Coverage", interactive=True ) coverage_desc = gr.Markdown( f"*{COVERAGE_LEVELS['Medium']['description']}*", visible=True ) # Blending mode gr.Markdown("### 🖌️ Blending Mode") blending_radio = gr.Radio( choices=list(BLENDING_MODES.keys()), value="Natural", label="Blending Style", interactive=True ) # Warmth adjustment gr.Markdown("### 🌡️ Warmth Adjustment") warmth_slider = gr.Slider( minimum=-20, maximum=20, value=0, step=2, label="Warmth (Cool ← → Warm)", interactive=True ) # Apply button apply_btn = gr.Button("✨ Apply Foundation", variant="primary", size="lg") with gr.Column(scale=1): # Output image image_output = gr.Image(label="Result", type="pil") # Status message status_text = gr.Textbox( label="Status", interactive=False, lines=3 ) # Shade reference guide gr.Markdown(""" ### 📖 Shade Guide - **Fair**: Very light, porcelain skin - **Light**: Light, ivory skin - **Medium**: Medium, beige skin - **Tan**: Tan, bronze skin - **Deep**: Deep, caramel skin - **Rich**: Rich, dark cocoa skin ### 💡 Tips - Use AI auto-match for best results - Start with Light coverage, build up as needed - Natural blending for everyday look - Airbrushed for special occasions """) # Hidden state for auto-match flag auto_match_state = gr.State(False) # Update shade description when shade changes def update_shade_desc(shade): desc = FOUNDATION_SHADES.get(shade, FOUNDATION_SHADES["Medium"])["description"] return f"*{desc}*" shade_dropdown.change( fn=update_shade_desc, inputs=[shade_dropdown], outputs=[shade_desc] ) # Update coverage description when coverage changes def update_coverage_desc(coverage): desc = COVERAGE_LEVELS.get(coverage, COVERAGE_LEVELS["Medium"])["description"] return f"*{desc}*" coverage_radio.change( fn=update_coverage_desc, inputs=[coverage_radio], outputs=[coverage_desc] ) # Auto-match shade def auto_match_shade(image): if image is None: return "Medium", "Please upload an image first", False if isinstance(image, Image.Image): img = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) else: img = image.copy() detected_shade = auto_detect_shade(img) desc = FOUNDATION_SHADES.get(detected_shade, FOUNDATION_SHADES["Medium"])["description"] return detected_shade, f"Detected: {detected_shade} ({desc})", True auto_match_btn.click( fn=auto_match_shade, inputs=[image_input], outputs=[shade_dropdown, shade_info_text, auto_match_state] ) # Apply foundation apply_btn.click( fn=process_foundation, inputs=[image_input, shade_dropdown, coverage_radio, blending_radio, warmth_slider, auto_match_state], outputs=[image_output, status_text, gr.State()] ) if __name__ == "__main__": print("\n" + "="*60) print("🌸 Foundation Virtual Makeup App") print("="*60 + "\n") demo.launch()