ColorPalette / src /analyze.py
HardikUppal's picture
clean up changes
953a2bf
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