MakeUp_TryOn / utils.py
ukzada's picture
Upload 5 files
9dc3fef verified
Raw
History Blame Contribute Delete
10 kB
import cv2
import numpy as np
from landmarks import detect_landmarks, normalize_landmarks, plot_landmarks
import mediapipe as mp
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]
face_conn = [10, 338, 297, 332, 284, 251, 389, 264, 447, 376, 433, 288, 367, 397, 365, 379, 378, 400, 377, 152,
148, 176, 149, 150, 136, 172, 138, 213, 147, 234, 127, 162, 21, 54, 103, 67, 109]
cheeks = [425, 205]
def apply_makeup(src: np.ndarray, is_stream: bool, feature: str, show_landmarks: bool = False):
"""
Takes in a source image and applies effects onto it.
"""
ret_landmarks = detect_landmarks(src, is_stream)
height, width, _ = src.shape
feature_landmarks = None
if feature == 'lips':
feature_landmarks = normalize_landmarks(ret_landmarks, height, width, upper_lip + lower_lip)
mask = lip_mask(src, feature_landmarks, [153, 0, 157])
output = cv2.addWeighted(src, 1.0, mask, 0.4, 0.0)
elif feature == 'blush':
feature_landmarks = normalize_landmarks(ret_landmarks, height, width, cheeks)
mask = blush_mask(src, feature_landmarks, [153, 0, 157], 50)
output = cv2.addWeighted(src, 1.0, mask, 0.3, 0.0)
else: # Defaults to blush for any other thing
skin_mask = mask_skin(src)
output = np.where(src * skin_mask >= 1, gamma_correction(src, 1.75), src)
if show_landmarks and feature_landmarks is not None:
plot_landmarks(src, feature_landmarks, True)
return output
def apply_feature(src: np.ndarray, feature: str, landmarks: list, normalize: bool = False,
show_landmarks: bool = False):
"""
Performs similar to `apply_makeup` but needs the landmarks explicitly
Specifically implemented to reduce the computation on the server
"""
height, width, _ = src.shape
if normalize:
landmarks = normalize_landmarks(landmarks, height, width)
if feature == 'lips':
mask = lip_mask(src, landmarks, [153, 0, 157])
output = cv2.addWeighted(src, 1.0, mask, 0.4, 0.0)
elif feature == 'blush':
mask = blush_mask(src, landmarks, [153, 0, 157], 50)
output = cv2.addWeighted(src, 1.0, mask, 0.3, 0.0)
else: # Does not require any landmarks for skin masking -> Foundation
skin_mask = mask_skin(src)
output = np.where(src * skin_mask >= 1, gamma_correction(src, 1.75), src)
if show_landmarks: # Refrain from using this during an API Call
plot_landmarks(src, landmarks, True)
return output
def lip_mask(src: np.ndarray, points: np.ndarray, color: list):
"""
Given a src image, points of lips and a desired color
Returns a colored mask that can be added to the src with improved quality.
Includes glossy finish, better blending, and edge feathering.
"""
mask = np.zeros_like(src) # Create a mask
# Ensure points is proper type for cv2.fillPoly
if isinstance(points, np.ndarray):
points = points.astype(np.int32)
else:
points = np.array(points, dtype=np.int32)
mask = cv2.fillPoly(mask, [points], color) # Mask for the required facial feature
# Multi-stage blurring for smoother, more natural edges
mask = cv2.GaussianBlur(mask, (11, 11), 3) # First pass - soften edges
mask = cv2.GaussianBlur(mask, (7, 7), 2) # Second pass - refined smoothing
# Create edge feathering for seamless blend
mask_float = mask.astype(np.float32) / 255.0
# Apply smooth falloff at edges using morphological operations
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
eroded = cv2.erode(mask, kernel, iterations=1)
eroded_float = eroded.astype(np.float32) / 255.0
# Blend between full mask and eroded mask for gradual transition
mask = (mask_float * 0.7 + eroded_float * 0.3) * 255
mask = mask.astype(np.uint8)
return mask
def blush_mask(src: np.ndarray, points: np.ndarray, color: list, radius: int):
"""
Given a src image, points of the cheeks, desired color and radius
Returns a colored mask that can be added to the src with improved quality.
Includes gradient blending, soft feathering, and natural-looking diffusion.
"""
mask = np.zeros_like(src) # Mask that will be used for the cheeks
# Create high-quality blush with gradient falloff
for point in points:
# Create a circular gradient mask for smooth transition
py, px = int(point[1]), int(point[0])
# Limit region to avoid processing entire image
y_min = max(0, py - radius)
y_max = min(src.shape[0], py + radius + 1)
x_min = max(0, px - radius)
x_max = min(src.shape[1], px + radius + 1)
# Create local gradient for efficiency
yy, xx = np.ogrid[y_min:y_max, x_min:x_max]
dist = np.sqrt((yy - py) ** 2 + (xx - px) ** 2)
# Smooth falloff using cosine function for natural gradient
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 opacity
for c in range(3): # For each color channel
color_val = color[c] if c < len(color) else 0
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)
)
# Apply Gaussian blur for ultra-smooth blending
blur_radius = int(radius * 0.4)
if blur_radius % 2 == 0:
blur_radius += 1
blur_radius = max(3, blur_radius) # Ensure minimum blur size
mask = cv2.GaussianBlur(mask, (blur_radius, blur_radius), radius // 3)
# Apply vignette for natural feathering at edges
for point in points:
x, y = int(point[0]) - radius, int(point[1]) - radius
x = max(0, x)
y = max(0, y)
end_x = min(src.shape[1], x + 2 * radius)
end_y = min(src.shape[0], y + 2 * radius)
if end_y - y > 0 and end_x - x > 0:
region = mask[y:end_y, x:end_x]
if region.size > 0:
vignetted = vignette(region, 8)
mask[y:end_y, x:end_x] = vignetted
return mask
def mask_skin(src: np.ndarray):
"""
Given a source image of a person (face image)
returns a mask that can be identified as the skin
"""
lower = np.array([0, 133, 77], dtype='uint8') # The lower bound of skin color
upper = np.array([255, 173, 127], dtype='uint8') # Upper bound of skin color
dst = cv2.cvtColor(src, cv2.COLOR_BGR2YCR_CB) # Convert to YCR_CB
skin_mask = cv2.inRange(dst, lower, upper) # Get the skin
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
skin_mask = cv2.dilate(skin_mask, kernel, iterations=2)[..., np.newaxis] # Dilate to fill in blobs
if skin_mask.ndim != 3:
skin_mask = np.expand_dims(skin_mask, axis=-1)
return (skin_mask / 255).astype("uint8") # A binary mask containing only 1s and 0s
def face_mask(src: np.ndarray, points: np.ndarray):
"""
Given a list of face landmarks, return a closed polygon mask for the same
"""
mask = np.zeros_like(src)
mask = cv2.fillPoly(mask, [points], (255, 255, 255))
return mask
def clicked_at(event, x, y, flags, params):
"""
A useful callback that spits out the landmark index when clicked on a particular landmark
Note: Very sensitive to location, should be clicked exactly on the pixel
"""
# TODO: Add some atol to np.allclose
if event == cv2.EVENT_LBUTTONDOWN:
print(f"Clicked at {x, y}")
point = np.array([x, y])
landmarks = params.get("landmarks", None)
image = params.get("image", None)
if landmarks is not None and image is not None:
for idx, landmark in enumerate(landmarks):
if np.allclose(landmark, point):
print(f"Landmark: {idx}")
break
print("Found no landmark close to the click")
def vignette(src: np.ndarray, sigma: int):
"""
Given a src image and a sigma, returns a vignette of the src
"""
height, width, _ = src.shape
kernel_x = cv2.getGaussianKernel(width, sigma)
kernel_y = cv2.getGaussianKernel(height, sigma)
kernel = kernel_y * kernel_x.T
mask = kernel / kernel.max()
blurred = cv2.convertScaleAbs(src.copy() * np.expand_dims(mask, axis=-1))
return blurred
def face_bbox(src: np.ndarray, offset_x: int = 0, offset_y: int = 0):
"""
Performs face detection on a src image, return bounding box coordinates with
an optional offset applied to the coordinates
"""
height, width, _ = src.shape
with mp.solutions.face_detection.FaceDetection(model_selection=0) as detector: # 0 -> dist <= 2mts from the camera
results = detector.process(cv2.cvtColor(src, cv2.COLOR_BGR2RGB))
if not results.detections:
return None
results = results.detections[0].location_data
x_min, y_min = results.relative_bounding_box.xmin, results.relative_bounding_box.ymin
box_height, box_width = results.relative_bounding_box.height, results.relative_bounding_box.width
x_min = int(width * x_min) - offset_x
y_min = int(height * y_min) - offset_y
box_height, box_width = int(height * box_height) + offset_y, int(width * box_width) + offset_x
return (x_min, y_min), (box_height, box_width)
def gamma_correction(src: np.ndarray, gamma: float, coefficient: int = 1):
"""
Performs gamma correction on a source image
gamma > 1 => Darker Image
gamma < 1 => Brighted Image
"""
dst = src.copy()
dst = dst / 255. # Converted to float64
dst = coefficient * np.power(dst, gamma)
dst = (dst * 255).astype('uint8')
return dst