Spaces:
Running
Running
| 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 | |