Spaces:
Sleeping
Sleeping
from flask import Flask, request, jsonify | |
from flask_cors import CORS | |
import cv2 | |
import numpy as np | |
from PIL import Image | |
import io | |
import base64 | |
import colorsys | |
from sklearn.cluster import KMeans | |
import webcolors | |
import math | |
from collections import Counter | |
import json | |
import re | |
import traceback | |
import logging | |
# Configure logging | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
app = Flask(__name__) | |
CORS(app, origins=["*"]) | |
# Add a simple root route | |
def home(): | |
return jsonify({ | |
'message': 'AI-Powered Color Palette API', | |
'version': '1.0.0', | |
'endpoints': { | |
'extract_palette': '/extract-palette (POST)', | |
'health': '/health (GET)' | |
}, | |
'status': 'running' | |
}) | |
class AdvancedColorExtractor: | |
def __init__(self): | |
# Color psychology and context mappings | |
self.color_moods = { | |
'red': ['passionate', 'energetic', 'bold', 'exciting'], | |
'orange': ['warm', 'creative', 'enthusiastic', 'adventurous'], | |
'yellow': ['optimistic', 'cheerful', 'innovative', 'bright'], | |
'green': ['natural', 'peaceful', 'growth', 'harmony'], | |
'blue': ['trustworthy', 'calm', 'professional', 'serene'], | |
'purple': ['luxurious', 'creative', 'mysterious', 'royal'], | |
'pink': ['playful', 'romantic', 'gentle', 'nurturing'], | |
'brown': ['earthy', 'stable', 'rustic', 'organic'], | |
'gray': ['sophisticated', 'neutral', 'balanced', 'modern'], | |
'black': ['elegant', 'powerful', 'dramatic', 'sleek'], | |
'white': ['clean', 'pure', 'minimal', 'fresh'] | |
} | |
self.creative_names = { | |
'red': ['Crimson Fire', 'Ruby Passion', 'Scarlet Dream', 'Cherry Burst'], | |
'orange': ['Sunset Glow', 'Autumn Spice', 'Coral Reef', 'Amber Light'], | |
'yellow': ['Golden Hour', 'Sunflower Joy', 'Lemon Zest', 'Honey Drip'], | |
'green': ['Forest Whisper', 'Emerald Isle', 'Sage Wisdom', 'Mint Fresh'], | |
'blue': ['Ocean Depth', 'Sky Canvas', 'Midnight Azure', 'Arctic Breeze'], | |
'purple': ['Lavender Dreams', 'Royal Velvet', 'Mystic Plum', 'Amethyst Glow'], | |
'pink': ['Rose Petal', 'Blush Soft', 'Flamingo Pink', 'Cotton Candy'], | |
'brown': ['Cocoa Rich', 'Earth Clay', 'Leather Warm', 'Coffee Bean'], | |
'gray': ['Storm Cloud', 'Silver Mist', 'Charcoal Deep', 'Dove Wing'], | |
'black': ['Midnight Black', 'Obsidian Dark', 'Shadow Deep', 'Carbon Night'], | |
'white': ['Pure Snow', 'Cloud White', 'Pearl Shine', 'Arctic White'] | |
} | |
def extract_colors_kmeans(self, image_data, n_colors=8): | |
"""Extract colors using improved K-means clustering""" | |
# Convert to RGB and reshape | |
image_rgb = cv2.cvtColor(image_data, cv2.COLOR_BGR2RGB) | |
image_rgb = image_rgb.reshape((-1, 3)) | |
# Remove very dark and very light pixels for better clustering | |
brightness = np.mean(image_rgb, axis=1) | |
filtered_pixels = image_rgb[(brightness > 20) & (brightness < 235)] | |
if len(filtered_pixels) < n_colors: | |
filtered_pixels = image_rgb | |
# Apply K-means clustering | |
kmeans = KMeans(n_clusters=n_colors, random_state=42, n_init=10) | |
kmeans.fit(filtered_pixels) | |
colors = kmeans.cluster_centers_.astype(int) | |
# Sort colors by frequency | |
labels = kmeans.labels_ | |
label_counts = Counter(labels) | |
sorted_colors = [colors[i] for i in sorted(label_counts.keys(), | |
key=lambda x: label_counts[x], reverse=True)] | |
return sorted_colors | |
def rgb_to_hex(self, rgb): | |
"""Convert RGB to HEX""" | |
return "#{:02x}{:02x}{:02x}".format(int(rgb[0]), int(rgb[1]), int(rgb[2])) | |
def rgb_to_hsl(self, rgb): | |
"""Convert RGB to HSL""" | |
r, g, b = rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0 | |
h, l, s = colorsys.rgb_to_hls(r, g, b) | |
return { | |
'h': int(h * 360), | |
's': int(s * 100), | |
'l': int(l * 100) | |
} | |
def rgb_to_cmyk(self, rgb): | |
"""Convert RGB to CMYK""" | |
r, g, b = rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0 | |
k = 1 - max(r, g, b) | |
if k == 1: | |
return {'c': 0, 'm': 0, 'y': 0, 'k': 100} | |
c = (1 - r - k) / (1 - k) | |
m = (1 - g - k) / (1 - k) | |
y = (1 - b - k) / (1 - k) | |
return { | |
'c': int(c * 100), | |
'm': int(m * 100), | |
'y': int(y * 100), | |
'k': int(k * 100) | |
} | |
def get_color_name(self, rgb): | |
"""Get creative color name based on RGB values""" | |
try: | |
# Try to get closest web color name | |
closest_name = webcolors.rgb_to_name(rgb) | |
base_color = self.categorize_color(rgb) | |
return self.creative_names.get(base_color, ['Unique Color'])[0] | |
except ValueError: | |
# Generate creative name based on color category | |
base_color = self.categorize_color(rgb) | |
names = self.creative_names.get(base_color, ['Mystery Color']) | |
# Use brightness and saturation to pick variation | |
brightness = sum(rgb) / 3 | |
if brightness > 200: | |
return f"Light {names[0]}" | |
elif brightness < 80: | |
return f"Deep {names[0]}" | |
else: | |
return names[0] | |
def categorize_color(self, rgb): | |
"""Categorize RGB color into basic color families""" | |
r, g, b = rgb | |
# Convert to HSV for better color categorization | |
hsv = colorsys.rgb_to_hsv(r/255, g/255, b/255) | |
h, s, v = hsv[0] * 360, hsv[1] * 100, hsv[2] * 100 | |
# Handle grayscale | |
if s < 10: | |
if v > 90: | |
return 'white' | |
elif v < 10: | |
return 'black' | |
else: | |
return 'gray' | |
# Categorize by hue | |
if h < 15 or h >= 345: | |
return 'red' | |
elif h < 45: | |
return 'orange' | |
elif h < 75: | |
return 'yellow' | |
elif h < 165: | |
return 'green' | |
elif h < 255: | |
return 'blue' | |
elif h < 285: | |
return 'purple' | |
elif h < 345: | |
return 'pink' | |
else: | |
return 'red' | |
def analyze_palette_mood(self, colors): | |
"""Analyze overall mood and context of the palette""" | |
moods = [] | |
color_categories = [] | |
for color in colors: | |
category = self.categorize_color(color) | |
color_categories.append(category) | |
moods.extend(self.color_moods.get(category, [])) | |
# Count mood frequencies | |
mood_counts = Counter(moods) | |
dominant_moods = [mood for mood, count in mood_counts.most_common(3)] | |
# Suggest use cases based on color combination | |
suggestions = self.suggest_use_cases(color_categories, dominant_moods) | |
return { | |
'moods': dominant_moods, | |
'suggested_uses': suggestions, | |
'color_distribution': dict(Counter(color_categories)) | |
} | |
def suggest_use_cases(self, color_categories, moods): | |
"""Suggest use cases based on color analysis""" | |
suggestions = [] | |
# Nature-heavy palettes | |
if color_categories.count('green') >= 2 or color_categories.count('brown') >= 2: | |
suggestions.extend(['Outdoor brands', 'Environmental websites', 'Natural products']) | |
# Blue-heavy palettes | |
if color_categories.count('blue') >= 2: | |
suggestions.extend(['Corporate websites', 'Healthcare', 'Technology apps']) | |
# Warm color palettes | |
warm_colors = color_categories.count('red') + color_categories.count('orange') + color_categories.count('yellow') | |
if warm_colors >= 3: | |
suggestions.extend(['Food & dining', 'Creative agencies', 'Entertainment']) | |
# Mood-based suggestions | |
if 'energetic' in moods: | |
suggestions.append('Fitness & sports') | |
if 'luxurious' in moods: | |
suggestions.append('Premium brands') | |
if 'calm' in moods: | |
suggestions.append('Wellness & spa') | |
return list(set(suggestions))[:5] # Return unique suggestions, max 5 | |
def generate_variations(self, colors): | |
"""Generate palette variations""" | |
variations = {} | |
# Brighter version | |
brighter = [] | |
for color in colors: | |
hsv = colorsys.rgb_to_hsv(color[0]/255, color[1]/255, color[2]/255) | |
new_hsv = (hsv[0], min(1.0, hsv[1] * 1.2), min(1.0, hsv[2] * 1.1)) | |
new_rgb = colorsys.hsv_to_rgb(*new_hsv) | |
brighter.append([int(new_rgb[0] * 255), int(new_rgb[1] * 255), int(new_rgb[2] * 255)]) | |
variations['brighter'] = brighter | |
# Softer version | |
softer = [] | |
for color in colors: | |
hsv = colorsys.rgb_to_hsv(color[0]/255, color[1]/255, color[2]/255) | |
new_hsv = (hsv[0], hsv[1] * 0.6, hsv[2] * 0.9 + 0.1) | |
new_rgb = colorsys.hsv_to_rgb(*new_hsv) | |
softer.append([int(new_rgb[0] * 255), int(new_rgb[1] * 255), int(new_rgb[2] * 255)]) | |
variations['softer'] = softer | |
# Monochrome version (based on dominant color) | |
if colors: | |
dominant_color = colors[0] | |
base_hue = colorsys.rgb_to_hsv(dominant_color[0]/255, dominant_color[1]/255, dominant_color[2]/255)[0] | |
monochrome = [] | |
for i in range(8): | |
sat = 0.2 + (i * 0.1) | |
val = 0.3 + (i * 0.08) | |
rgb = colorsys.hsv_to_rgb(base_hue, min(1.0, sat), min(1.0, val)) | |
monochrome.append([int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)]) | |
variations['monochrome'] = monochrome | |
return variations | |
def suggest_complementary_colors(self, colors): | |
"""Suggest complementary colors that might be missing""" | |
suggestions = [] | |
if not colors: | |
return suggestions | |
# Analyze what's missing | |
color_categories = [self.categorize_color(color) for color in colors] | |
# Check for missing neutrals | |
if 'gray' not in color_categories and 'white' not in color_categories: | |
suggestions.append({ | |
'color': [200, 200, 200], | |
'reason': 'Add a neutral gray for balance', | |
'name': 'Neutral Gray' | |
}) | |
# Check for missing warm accent | |
warm_colors = ['red', 'orange', 'yellow'] | |
if not any(cat in warm_colors for cat in color_categories): | |
suggestions.append({ | |
'color': [255, 140, 60], | |
'reason': 'Consider a warm accent color', | |
'name': 'Warm Accent' | |
}) | |
# Check for missing cool tone | |
cool_colors = ['blue', 'green', 'purple'] | |
if not any(cat in cool_colors for cat in color_categories): | |
suggestions.append({ | |
'color': [60, 140, 200], | |
'reason': 'A cool tone could add depth', | |
'name': 'Cool Depth' | |
}) | |
return suggestions[:3] # Max 3 suggestions | |
extractor = AdvancedColorExtractor() | |
def extract_palette(): | |
try: | |
# Get image from request | |
if 'image' not in request.files: | |
return jsonify({'error': 'No image provided'}), 400 | |
image_file = request.files['image'] | |
# Convert to OpenCV format | |
image_stream = io.BytesIO(image_file.read()) | |
pil_image = Image.open(image_stream) | |
opencv_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) | |
# Extract colors | |
colors = extractor.extract_colors_kmeans(opencv_image, n_colors=8) | |
# Process each color | |
processed_colors = [] | |
for i, color in enumerate(colors): | |
rgb = [int(c) for c in color] | |
color_data = { | |
'id': i, | |
'rgb': rgb, | |
'hex': extractor.rgb_to_hex(rgb), | |
'hsl': extractor.rgb_to_hsl(rgb), | |
'cmyk': extractor.rgb_to_cmyk(rgb), | |
'name': extractor.get_color_name(rgb), | |
'category': extractor.categorize_color(rgb) | |
} | |
processed_colors.append(color_data) | |
# Analyze palette | |
analysis = extractor.analyze_palette_mood([c['rgb'] for c in processed_colors]) | |
# Generate variations | |
variations = extractor.generate_variations([c['rgb'] for c in processed_colors]) | |
# Process variations to include all formats | |
processed_variations = {} | |
for var_name, var_colors in variations.items(): | |
processed_variations[var_name] = [] | |
for color in var_colors: | |
processed_variations[var_name].append({ | |
'rgb': color, | |
'hex': extractor.rgb_to_hex(color), | |
'name': extractor.get_color_name(color) | |
}) | |
# Get complementary suggestions | |
suggestions = extractor.suggest_complementary_colors([c['rgb'] for c in processed_colors]) | |
# Generate export formats | |
exports = { | |
'css_variables': generate_css_variables(processed_colors), | |
'scss_variables': generate_scss_variables(processed_colors), | |
'figma_tokens': generate_figma_tokens(processed_colors), | |
'adobe_ase': 'Base64 encoded ASE file would go here', # Placeholder | |
'tailwind_config': generate_tailwind_config(processed_colors) | |
} | |
response = { | |
'colors': processed_colors, | |
'analysis': analysis, | |
'variations': processed_variations, | |
'suggestions': suggestions, | |
'exports': exports, | |
'metadata': { | |
'total_colors': len(processed_colors), | |
'dominant_category': analysis['color_distribution'], | |
'palette_id': f"palette_{hash(str(processed_colors)) % 100000}" | |
} | |
} | |
return jsonify(response) | |
except Exception as e: | |
return jsonify({'error': str(e)}), 500 | |
def generate_css_variables(colors): | |
"""Generate CSS custom properties""" | |
css = ":root {\n" | |
for i, color in enumerate(colors): | |
css += f" --color-{i+1}: {color['hex']};\n" | |
css += f" --color-{color['name'].lower().replace(' ', '-')}: {color['hex']};\n" | |
css += "}" | |
return css | |
def generate_scss_variables(colors): | |
"""Generate SCSS variables""" | |
scss = "" | |
for i, color in enumerate(colors): | |
scss += f"$color-{i+1}: {color['hex']};\n" | |
scss += f"$color-{color['name'].lower().replace(' ', '-')}: {color['hex']};\n" | |
return scss | |
def generate_figma_tokens(colors): | |
"""Generate design tokens JSON for Figma""" | |
tokens = { | |
"color": {} | |
} | |
for i, color in enumerate(colors): | |
tokens["color"][f"color-{i+1}"] = { | |
"value": color['hex'], | |
"type": "color", | |
"description": f"{color['name']} - {color['category']}" | |
} | |
return json.dumps(tokens, indent=2) | |
def generate_tailwind_config(colors): | |
"""Generate Tailwind CSS config""" | |
config = "module.exports = {\n theme: {\n extend: {\n colors: {\n" | |
for i, color in enumerate(colors): | |
safe_name = re.sub(r'[^a-zA-Z0-9]', '', color['name'].lower()) | |
config += f" '{safe_name}': '{color['hex']}',\n" | |
config += " }\n }\n }\n}" | |
return config | |
def health_check(): | |
return jsonify({'status': 'healthy', 'service': 'Advanced Color Palette API'}) | |
if __name__ == '__main__': | |
app.run(host='0.0.0.0', port=7860, debug=False) |