Spaces:
Sleeping
Sleeping
| #!/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() |