from PIL import Image import numpy as np from sklearn.cluster import KMeans from collections import Counter from src.color_utils import calculate_color_distance_lab, rgb_to_lab import cv2 class SkinTonePalette: def __init__(self): self.palette = { "Ebony": ((55, 48, 40), "#373028"), "Deep Mocha": ((66, 40, 17), "#422811"), "Dark Chocolate": ((81, 59, 46), "#513b2e"), "Warm Almond": ((111, 80, 60), "#6f503c"), "Golden Bronze": ((129, 101, 79), "#81654f"), "Honey": ((157, 122, 84), "#9d7a54"), "Caramel": ((190, 160, 126), "#bea07e"), "Light Tan": ((229, 200, 166), "#e5c8a6"), "Peach": ((231, 193, 184), "#e7c1b8"), "Fair": ((243, 218, 214), "#f3dad6"), "Ivory": ((251, 242, 243), "#fbf2f3"), } def get_dominant_colors(self, image_np, n_colors): pixels = image_np.reshape((-1, 3)) kmeans = KMeans(n_clusters=n_colors) kmeans.fit(pixels) dominant_colors = kmeans.cluster_centers_ counts = Counter(kmeans.labels_) dominant_colors = [dominant_colors[i] for i in counts.keys()] return dominant_colors def get_closest_color(self, image, mask, n_colors=3): image_np = np.array(image) mask_np = np.array(mask) if image_np.shape[:2] != mask_np.shape[:2]: raise ValueError("Image and mask must have the same dimensions") skin_pixels = image_np[mask_np > 0] dominant_colors = self.get_dominant_colors(skin_pixels, n_colors) closest_color = None closest_hex = None min_distance = float("inf") for dom_color in dominant_colors: for color_name, (color_value, color_hex) in self.palette.items(): distance = calculate_color_distance_lab(dom_color, color_value) if distance < min_distance: min_distance = distance closest_color = color_name closest_hex = color_hex return closest_color, closest_hex def calculate_ita(self, rgb_color): lab_color = rgb_to_lab(rgb_color) L = lab_color.lab_l b = lab_color.lab_b ita = np.arctan2(L - 50, b) * (180 / np.pi) return ita def is_within_vectorscope_skin_tone_line(self, rgb_color): ycbcr_color = cv2.cvtColor(np.uint8([[rgb_color]]), cv2.COLOR_RGB2YCrCb)[0][0] cb, cr = ycbcr_color[1], ycbcr_color[2] return 80 <= cb <= 120 and 133 <= cr <= 173