MakeUp_TryOn / app.py
ukzada's picture
Update app.py
4a3b485 verified
Raw
History Blame Contribute Delete
13.2 kB
#!/usr/bin/env python3
"""
Virtual Makeup App using MediaPipe FaceMesh
Simple approach: landmarks-based masks for lipstick, blush, foundation
"""
import cv2
import numpy as np
import gradio as gr
from PIL import Image
import os
from landmarks import detect_landmarks, normalize_landmarks
from utils import mask_skin, gamma_correction
# Color presets for lipstick
LIPSTICK_COLORS = {
"Red": (0, 0, 255),
"Pink": (203, 192, 255),
"Burgundy": (75, 0, 130),
"Orange": (0, 165, 255),
"Nude": (180, 140, 200),
"Wine": (0, 0, 120),
"Coral": (100, 165, 255),
}
# Color presets for blush
BLUSH_COLORS = {
"Coral": (66, 135, 245),
"Pink": (192, 135, 220),
"Peach": (152, 165, 255),
"Rose": (100, 70, 200),
"Berry": (140, 50, 150),
"Apricot": (140, 140, 230),
}
# Landmark indices for different regions
UPPER_LIP = [61, 185, 40, 39, 37, 0, 267, 269, 270, 408, 415, 272, 271, 268, 12, 38, 41, 42, 191, 78, 76]
LOWER_LIP = [61, 146, 91, 181, 84, 17, 314, 405, 320, 307, 308, 324, 318, 402, 317, 14, 87, 178, 88, 95]
CHEEKS = [425, 205] # Left and right cheek points
# Foundation shade presets (BGR 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 apply_lipstick(image, color_rgb, landmarks, alpha=0.4):
"""Apply lipstick to lips using landmarks."""
if landmarks is None:
return image
h, w = image.shape[:2]
mask = np.zeros_like(image)
lip_points = normalize_landmarks(landmarks, h, w, UPPER_LIP + LOWER_LIP)
if len(lip_points) > 0:
lip_points = lip_points.astype(np.int32)
cv2.fillPoly(mask, [lip_points], color_rgb)
# Smooth edges with Gaussian blur
mask = cv2.GaussianBlur(mask, (15, 15), 3)
# Blend with original image
output = cv2.addWeighted(image, 1.0, mask, alpha, 0)
return output
def apply_blush(image, color_rgb, landmarks, intensity=0.3, radius=40):
"""Apply blush to cheeks with intensity control (0-1)."""
if landmarks is None:
return image
h, w = image.shape[:2]
mask = np.zeros_like(image)
cheek_points = normalize_landmarks(landmarks, h, w, CHEEKS)
for point in cheek_points:
x, y = int(point[0]), int(point[1])
# Create circular blush with gradient
y_min = max(0, y - radius)
y_max = min(h, y + radius + 1)
x_min = max(0, x - radius)
x_max = min(w, x + radius + 1)
yy, xx = np.ogrid[y_min:y_max, x_min:x_max]
dist = np.sqrt((yy - y) ** 2 + (xx - x) ** 2)
# Smooth gradient with cosine falloff
gradient = np.zeros_like(dist, dtype=np.float32)
valid = dist <= radius
gradient[valid] = (1.0 + np.cos(np.pi * dist[valid] / radius)) / 2.0
# Apply color with gradient
for c in range(3):
color_val = color_rgb[c]
mask[y_min:y_max, x_min:x_max, c] = np.maximum(
mask[y_min:y_max, x_min:x_max, c],
(color_val * gradient).astype(np.uint8)
)
# Blur for smooth blending
blur_rad = max(3, radius // 3)
if blur_rad % 2 == 0:
blur_rad += 1
mask = cv2.GaussianBlur(mask, (blur_rad, blur_rad), blur_rad // 2)
# Apply with intensity control
alpha = intensity * 0.5 # Scale down so it's subtle
output = cv2.addWeighted(image, 1.0, mask, alpha, 0)
return output
def auto_detect_shade(image):
"""Analyze skin tone to suggest the best foundation shade."""
if image is None:
return "Medium"
skin_mask_binary = mask_skin(image)
if skin_mask_binary.ndim == 3:
skin_mask_binary = skin_mask_binary[:, :, 0]
skin_pixels = image[skin_mask_binary > 0]
if len(skin_pixels) == 0:
return "Medium"
avg_b, avg_g, avg_r = np.mean(skin_pixels, axis=0)
luminance = 0.299 * avg_r + 0.587 * avg_g + 0.114 * avg_b
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."""
if image is None:
return image
skin_mask_binary = mask_skin(image)
if skin_mask_binary.ndim == 3:
skin_mask_binary = skin_mask_binary[:, :, 0]
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"]
corrected = gamma_correction(image, gamma_val, coefficient=1)
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)
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)
if smoothing > 0:
corrected = cv2.bilateralFilter(corrected, smoothing, 75, 75)
skin_mask_float = skin_mask_binary.astype(np.float32)
if blur_kernel % 2 == 0:
blur_kernel += 1
skin_mask_float = cv2.GaussianBlur(skin_mask_float, (blur_kernel, blur_kernel), 0)
skin_mask_float = np.power(skin_mask_float, feather)
skin_mask_float = np.expand_dims(skin_mask_float, axis=-1)
skin_mask_float = np.repeat(skin_mask_float, 3, axis=2)
output = image.astype(np.float32)
corrected = corrected.astype(np.float32)
output = output * (1.0 - skin_mask_float * intensity) + corrected * (skin_mask_float * intensity)
return output.astype(np.uint8)
def process_foundation(image, shade, warmth):
"""Process image to apply foundation."""
if image is None:
return None, "No image provided", ""
if isinstance(image, Image.Image):
img = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
else:
img = image.copy()
coverage = "Full"
blending = "Natural"
output = apply_foundation_advanced(img, shade, coverage, blending, warmth)
output_rgb = cv2.cvtColor(output, cv2.COLOR_BGR2RGB)
coverage_info = COVERAGE_LEVELS.get(coverage, COVERAGE_LEVELS["Medium"])
status = (
f"Applied {shade} foundation with {coverage} coverage "
f"({coverage_info['description']}) using {blending} blending"
)
return output_rgb, status, shade
def process_image(image, apply_lipstick_flag, lipstick_color,
apply_blush_flag, blush_color, blush_intensity,
apply_foundation_flag, foundation_shade, warmth):
"""Process image with selected makeup features."""
if image is None:
return image, "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()
# Detect landmarks
landmarks = detect_landmarks(img)
if landmarks is None:
return cv2.cvtColor(img, cv2.COLOR_BGR2RGB), "No face detected"
output = img.copy()
applied_features = []
# Apply selected features
if apply_lipstick_flag:
color = LIPSTICK_COLORS.get(lipstick_color, LIPSTICK_COLORS["Red"])
output = apply_lipstick(output, color, landmarks, alpha=0.4)
applied_features.append("Lipstick")
if apply_blush_flag:
color = BLUSH_COLORS.get(blush_color, BLUSH_COLORS["Pink"])
# Blush intensity is percentage (0-100), convert to 0-1
intensity = blush_intensity / 100.0
output = apply_blush(output, color, landmarks, intensity=intensity)
applied_features.append(f"Blush ({blush_intensity}%)")
if apply_foundation_flag:
foundation_image, foundation_status, used_shade = process_foundation(
output, foundation_shade, warmth
)
output = cv2.cvtColor(foundation_image, cv2.COLOR_RGB2BGR)
applied_features.append(f"Foundation ({used_shade}, Full, Natural)")
# Convert back to RGB for display
output_rgb = cv2.cvtColor(output, cv2.COLOR_BGR2RGB)
status = f"Applied: {', '.join(applied_features) if applied_features else 'None'}"
return output_rgb, status
# Create Gradio interface
with gr.Blocks(title="Virtual Makeup App") as demo:
gr.Markdown("# πŸ’„ Virtual Makeup App")
gr.Markdown("Upload a photo and apply virtual makeup with customizable intensity")
with gr.Row():
with gr.Column():
image_input = gr.Image(label="Upload Image", type="pil")
# Lipstick controls
lipstick_check = gr.Checkbox(label="Apply Lipstick", value=True)
lipstick_color = gr.Dropdown(
choices=list(LIPSTICK_COLORS.keys()),
value="Red",
label="Lipstick Color"
)
# Blush controls
blush_check = gr.Checkbox(label="Apply Blush", value=True)
blush_color = gr.Dropdown(
choices=list(BLUSH_COLORS.keys()),
value="Pink",
label="Blush Color"
)
blush_intensity = gr.Slider(
minimum=0, maximum=100, value=50, step=5,
label="Blush Intensity (%)"
)
# Foundation controls
foundation_check = gr.Checkbox(label="Apply Foundation", value=True)
gr.Markdown("### 🎨 Shade Selection")
foundation_shade = gr.Dropdown(
choices=list(FOUNDATION_SHADES.keys()),
value="Medium",
label="Foundation Shade",
interactive=True,
)
shade_desc = gr.Markdown(
f"*{FOUNDATION_SHADES['Medium']['description']}*",
visible=True
)
gr.Markdown("### 🌑️ Warmth Adjustment")
warmth_slider = gr.Slider(
minimum=-20,
maximum=20,
value=0,
step=2,
label="Warmth (Cool ← β†’ Warm)",
interactive=True,
)
apply_btn = gr.Button("✨ Apply Makeup", variant="primary")
with gr.Column():
image_output = gr.Image(label="Result", type="pil")
status_text = gr.Textbox(label="Status", interactive=False)
def update_shade_desc(shade):
desc = FOUNDATION_SHADES.get(shade, FOUNDATION_SHADES["Medium"])["description"]
return f"*{desc}*"
shade_dropdown = foundation_shade
shade_dropdown.change(
fn=update_shade_desc,
inputs=[shade_dropdown],
outputs=[shade_desc]
)
# Link button to processing function
apply_btn.click(
fn=process_image,
inputs=[
image_input,
lipstick_check, lipstick_color,
blush_check, blush_color, blush_intensity,
foundation_check, foundation_shade, warmth_slider
],
outputs=[image_output, status_text],
api_name="apply_makeup"
)
if __name__ == "__main__":
# Hugging Face Spaces launch configuration
port = int(os.environ.get("PORT", 7860))
demo.launch(server_name="0.0.0.0", server_port=port, share=True)