import cv2 import numpy as np import math from sklearn.cluster import KMeans from collections import Counter from src.color_utils import rgb_to_hex, hex_to_bgr, hex_to_rgb from src.hair_utils import HairColorPalette from src.skin_utils import SkinTonePalette from src.eyes_utils import EyeColorPalette from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt # Function to create a color bar def create_color_bar(height, width, color): bar = np.zeros((height, width, 3), dtype=np.uint8) bar[:] = color return bar # Function to get dominant colors and their percentages def get_dominant_colors(image, mask, n_colors, debug=True): image_np = image[mask > 0] pixels = image_np.reshape((-1, 3)) n_colors_elbow = optimal_clusters_elbow(pixels, max_clusters=15) # n_colors_silhouette = optimal_clusters_silhouette(pixels, max_clusters=5) # kmeans_silhouette = KMeans(n_clusters=n_colors_silhouette) # kmeans_silhouette.fit(pixels) kmeans_elbow = KMeans(n_clusters=n_colors_elbow) kmeans_elbow.fit(pixels) dominant_colors = kmeans_elbow.cluster_centers_ counts = Counter(kmeans_elbow.labels_) total_count = sum(counts.values()) dominant_colors = [dominant_colors[i] for i in counts.keys()] dominant_percentages = [counts[i] / total_count for i in counts.keys()] if debug: visualize_clusters(image, mask, kmeans_elbow, tag="elbow") # visualize_clusters(image, mask, kmeans_silhouette, tag="silhouette") return dominant_colors, dominant_percentages def optimal_clusters_elbow(skin_pixels, max_clusters=10): distortions = [] for i in range(1, max_clusters + 1): kmeans = KMeans(n_clusters=i, random_state=42) kmeans.fit(skin_pixels) distortions.append(kmeans.inertia_) # Compute the second derivative to find the "elbow" point second_derivative = np.diff(np.diff(distortions)) optimal_k_elbow = ( np.argmax(second_derivative) + 2 ) # +2 because of the second derivative plt.figure(figsize=(10, 8)) plt.plot(range(1, max_clusters + 1), distortions, marker="o") plt.xlabel("Number of clusters") plt.ylabel("Distortion (Inertia)") plt.title("Elbow Method For Optimal Clusters") plt.axvline( x=optimal_k_elbow, linestyle="--", color="r", label=f"Optimal k={optimal_k_elbow}", ) plt.legend() plt.savefig("workspace/kmeans_elbow.png") return optimal_k_elbow def optimal_clusters_silhouette(skin_pixels, max_clusters=10): silhouette_scores = [] for i in range(2, max_clusters + 1): # Silhouette score is undefined for k=1 kmeans = KMeans(n_clusters=i, random_state=42) kmeans.fit(skin_pixels) score = silhouette_score(skin_pixels, kmeans.labels_) silhouette_scores.append(score) optimal_k_silhouette = ( np.argmax(silhouette_scores) + 2 ) # +2 because range starts at 2 plt.figure(figsize=(10, 8)) plt.plot(range(2, max_clusters + 1), silhouette_scores, marker="o") plt.xlabel("Number of clusters") plt.ylabel("Silhouette Score") plt.title("Silhouette Method For Optimal Clusters") plt.axvline( x=optimal_k_silhouette, linestyle="--", color="r", label=f"Optimal k={optimal_k_silhouette}", ) plt.legend() plt.savefig("workspace/kmeans_sillouette.png") return optimal_k_silhouette def visualize_clusters(image, mask, kmeans, tag="_none"): clustered_image = np.zeros_like(image) mask = mask > 0 labels = kmeans.labels_ cluster_centers = kmeans.cluster_centers_ skin_coords = np.where(mask) for label, (x, y) in zip(labels, zip(skin_coords[0], skin_coords[1])): clustered_image[x, y] = cluster_centers[label] clustered_image = clustered_image.astype(np.uint8) plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) plt.title("Original Image") plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) plt.subplot(1, 2, 2) plt.title("Clustered Image") plt.imshow(cv2.cvtColor(clustered_image, cv2.COLOR_BGR2RGB)) plt.savefig(f"workspace/kmeans_visual_{tag}.png") # Function to get the closest color from the palette def get_closest_color(dominant_colors, palette): min_distance = float("inf") for dom_color in dominant_colors: for color_name, (color_value, color_hex) in palette.items(): distance = np.linalg.norm(dom_color - np.array(color_value)) if distance < min_distance: min_distance = distance closest_color = color_name closest_hex = color_hex return closest_color, closest_hex, min_distance # Function to create the dominant color bar def create_dominant_color_bar( report_image, dominant_colors, dominant_percentages, bar_width ): color_bars = [] total_height = 0 for color, pct in zip(dominant_colors, dominant_percentages): bar_height = int(math.floor(report_image.shape[0] * pct)) total_height += bar_height bar = create_color_bar(bar_height, bar_width, color) color_bars.append(bar) padding_height = report_image.shape[0] - total_height if padding_height > 0: padding = create_color_bar(padding_height, bar_width, (255, 255, 255)) color_bars.append(padding) return np.vstack(color_bars) # Function to create the tone palette bar def create_tone_palette_bar(report_image, tone_id, skin_tone_palette, bar_width): palette_bars = [] tone_height = report_image.shape[0] // len(skin_tone_palette) tone_bgrs = [] for tone in skin_tone_palette.values(): color_bgr = hex_to_bgr(tone[1]) tone_bgrs.append(color_bgr) bar = create_color_bar(tone_height, bar_width, color_bgr) palette_bars.append(bar) padding_height = report_image.shape[0] - tone_height * len(skin_tone_palette) if padding_height > 0: padding = create_color_bar(padding_height, bar_width, (255, 255, 255)) palette_bars.append(padding) bar = np.vstack(palette_bars) padding = 1 start_point = (padding, tone_id * tone_height + padding) end_point = (bar_width - padding, (tone_id + 1) * tone_height) bar = cv2.rectangle(bar, start_point, end_point, (255, 0, 0), 2) return bar # Function to create the message bar def create_message_bar( dominant_colors, dominant_percentages, tone_hex, distance, img_shape ): bar_width = img_shape[1] bar_height = img_shape[0] // 30 msg_bar = create_color_bar( height=bar_height, width=bar_width, color=(243, 239, 214) ) b, g, r = np.around(dominant_colors[0]).astype(int) dominant_color_hex = "#%02X%02X%02X" % (r, g, b) pct = f"{dominant_percentages[0] * 100:.2f}%" font, font_scale, txt_color, thickness, line_type = ( cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 1, cv2.LINE_AA, ) x, y = 2, 15 msg = f"- Dominant color: {dominant_color_hex}, percent: {pct}" cv2.putText(msg_bar, msg, (x, y), font, font_scale, txt_color, thickness, line_type) text_size, _ = cv2.getTextSize(msg, font, font_scale, thickness) line_height = text_size[1] + 10 accuracy = round(100 - distance, 2) cv2.putText( msg_bar, f"- Skin tone: {tone_hex}, accuracy: {accuracy}", (x, y + line_height), font, font_scale, txt_color, thickness, cv2.LINE_AA, ) return msg_bar def color_analysis(skin, hair, eyes): analysis = {} # Determine Season if ( skin == "light" and (hair in ["golden blonde", "light brown"]) and (eyes in ["blue", "green"]) ): analysis["season"] = "Spring" elif ( skin == "light" and (hair in ["ash blonde", "light brown"]) and (eyes in ["blue", "green"]) ): analysis["season"] = "Summer" elif ( skin in ["medium", "dark"] and (hair in ["red", "brown"]) and (eyes in ["green", "hazel"]) ): analysis["season"] = "Autumn" elif ( skin in ["medium", "dark"] and (hair in ["dark brown", "black"]) and (eyes in ["blue", "brown"]) ): analysis["season"] = "Winter" # Determine Warm/Cool if skin in ["light", "medium", "dark"] and hair in ["golden", "red", "caramel"]: analysis["warm/cool"] = "Warm" else: analysis["warm/cool"] = "Cool" # Determine Intensity if eyes in ["bright blue", "bright green"] or hair in ["black", "vivid red"]: analysis["intensity"] = "High" else: analysis["intensity"] = "Low" # Determine Value if ( skin == "light" and hair in ["blonde", "light brown"] and eyes in ["blue", "green"] ): analysis["value"] = "Light" elif skin == "medium" and hair in ["brown", "red"] and eyes in ["green", "hazel"]: analysis["value"] = "Medium" else: analysis["value"] = "Dark" # Determine Tone if analysis["value"] == "Light" and analysis["intensity"] == "Low": analysis["tone"] = "Light and Soft" elif analysis["value"] == "Light" and analysis["intensity"] == "High": analysis["tone"] = "Light and Bright" elif analysis["value"] == "Dark" and analysis["intensity"] == "Low": analysis["tone"] = "Dark and Soft" else: analysis["tone"] = "Dark and Bright" # Determine Saturation if hair in ["bright red", "black"] or eyes in ["clear blue"]: analysis["saturation"] = "High" else: analysis["saturation"] = "Low" return analysis # # Example usage # result = color_analysis("light", "golden blonde", "blue") # print(result) def create_combined_overlay(image, hair_mask, skin_mask, eye_mask): """ Create an overlay image by combining the original image with the hair, skin, and eye masks. :param image: Original image as a numpy array. :param hair_mask: Hair mask as a numpy array. :param skin_mask: Skin mask as a numpy array. :param eye_mask: Eye mask as a numpy array. :return: Combined overlay image as a numpy array. """ # Create overlays for different parts hair_overlay = np.zeros_like(image) skin_overlay = np.zeros_like(image) eye_overlay = np.zeros_like(image) # Color the masks hair_overlay[hair_mask > 0] = [0, 255, 0] # Green for hair skin_overlay[skin_mask > 0] = [255, 0, 0] # Red for skin eye_overlay[eye_mask > 0] = [0, 0, 255] # Blue for eyes # Combine the overlays with the original image combined_overlay = cv2.addWeighted(image, 0.8, hair_overlay, 0.2, 0) combined_overlay = cv2.addWeighted(combined_overlay, 0.8, skin_overlay, 0.2, 0) combined_overlay = cv2.addWeighted(combined_overlay, 0.8, eye_overlay, 0.2, 0) return combined_overlay def analyze_and_visualize(image, hair_mask, skin_mask, eye_mask, n_colors=3): image_np = np.array(image) hair_mask_np = np.array(hair_mask) skin_mask_np = np.array(skin_mask) eye_mask_np = np.array(eye_mask) if not ( image_np.shape[:2] == hair_mask_np.shape[:2] == skin_mask_np.shape[:2] == eye_mask_np.shape[:2] ): raise ValueError("Image and all masks must have the same dimensions") hair_palette = HairColorPalette() hair_dominant_colors, hair_dominant_percentages = get_dominant_colors( image_np, hair_mask_np, n_colors, debug=True ) hair_color, hair_hex, hair_distance = get_closest_color( hair_dominant_colors, hair_palette.palette ) skin_palette = SkinTonePalette() skin_dominant_colors, skin_dominant_percentages = get_dominant_colors( image_np, skin_mask_np, n_colors, debug=True ) skin_color, skin_hex, skin_distance = get_closest_color( skin_dominant_colors, skin_palette.palette ) # Calculate ITA for the dominant skin color dominant_skin_color = skin_dominant_colors[0] ita = skin_palette.calculate_ita(dominant_skin_color) vectorscope_check = skin_palette.is_within_vectorscope_skin_tone_line( dominant_skin_color ) eye_palette = EyeColorPalette() eye_dominant_colors, eye_dominant_percentages = get_dominant_colors( image_np, eye_mask_np, n_colors, debug=True ) eye_color, eye_hex, eye_distance = get_closest_color( eye_dominant_colors, eye_palette.palette ) combined_overlay = create_combined_overlay( image_np, hair_mask_np, skin_mask_np, eye_mask_np ) bar_width = 50 hair_color_bar = create_dominant_color_bar( image_np, hair_dominant_colors, hair_dominant_percentages, bar_width ) skin_color_bar = create_dominant_color_bar( image_np, skin_dominant_colors, skin_dominant_percentages, bar_width ) eye_color_bar = create_dominant_color_bar( image_np, eye_dominant_colors, eye_dominant_percentages, bar_width ) hair_palette_bar = create_tone_palette_bar( image_np, list(hair_palette.palette.keys()).index(hair_color), hair_palette.palette, bar_width, ) skin_palette_bar = create_tone_palette_bar( image_np, list(skin_palette.palette.keys()).index(skin_color), skin_palette.palette, bar_width, ) eye_palette_bar = create_tone_palette_bar( image_np, list(eye_palette.palette.keys()).index(eye_color), eye_palette.palette, bar_width, ) output_image = np.hstack( [ combined_overlay, hair_color_bar, hair_palette_bar, skin_color_bar, skin_palette_bar, eye_color_bar, eye_palette_bar, ] ) img_shape = output_image.shape msg_bar_hair = create_message_bar( hair_dominant_colors, hair_dominant_percentages, hair_hex, hair_distance, img_shape, ) msg_bar_skin = create_message_bar( skin_dominant_colors, skin_dominant_percentages, skin_hex, skin_distance, img_shape, ) msg_bar_eye = create_message_bar( eye_dominant_colors, eye_dominant_percentages, eye_hex, eye_distance, img_shape ) output_image = np.vstack([output_image, msg_bar_hair, msg_bar_skin, msg_bar_eye]) analysis_record = { "hair": { "dominant_colors": [rgb_to_hex(color) for color in hair_dominant_colors], "dominant_percentages": hair_dominant_percentages, "closest_color": hair_color, "closest_color_hex": hair_hex, "distance": hair_distance, }, "skin": { "dominant_colors": [rgb_to_hex(color) for color in skin_dominant_colors], "dominant_percentages": skin_dominant_percentages, "closest_color": skin_color, "closest_color_hex": skin_hex, "distance": skin_distance, "ita": ita, # "vectorscope_check": vectorscope_check, }, "eyes": { "dominant_colors": [rgb_to_hex(color) for color in eye_dominant_colors], "dominant_percentages": eye_dominant_percentages, "closest_color": eye_color, "closest_color_hex": eye_hex, "distance": eye_distance, }, } return output_image, analysis_record