chazzu / hass-animated-scenes

Custom component for Home Assistant which allows you to create animated color and brightness scenes with many options.
87 stars 11 forks source link

Python script to analyse picture #39

Open walduino opened 4 months ago

walduino commented 4 months ago

Not an issue actually.... I've put together a python script to help put together a "scene" ; The script analyzes an image to find the most prominent colors and their weights. It filters out colors with a value below 50 or a saturation below 30 (in HSV), sorts the remaining colors by their hue,.

The filtered and sorted color information is printed in a formatted output, that should match the input expected by the animated scenes integration.

It also generates an image with color blocks proportional to the weights of the prominent colors,just to give you an idea what it will look like.

I hope someone finds this usefull, nut sure if i should make a PR...

USAGE ./coloranalysis_animated_scenes.py ./powders.jpeg 20 this wil analyse an image called powders.jpg and will return a config for the 20 most prominent colors. It will also create an image like this: image

SCRIPT

#!/usr/bin/python3
import sys
import numpy as np
from PIL import Image, ImageDraw
from sklearn.cluster import KMeans
import colorsys

def get_dominant_colors(image_path, num_colors):
    try:
        # Load the image
        image = Image.open(image_path)
    except Exception as e:
        print(f"Error loading image: {e}")
        return []

    # Resize the image to reduce the number of pixels (optional)
    image = image.resize((150, 150))

    # Convert image data to a format suitable for clustering
    image_np = np.array(image)
    pixels = image_np.reshape(-1, 3)

    # Start with an initial higher number of clusters
    clusters = num_colors * 3

    while clusters < num_colors * 10:  # Limit to avoid infinite loop
        # Use KMeans clustering to find the most prominent colors
        kmeans = KMeans(n_clusters=clusters)
        kmeans.fit(pixels)

        # Get the cluster centers (dominant colors)
        dominant_colors = kmeans.cluster_centers_

        # Get the labels for each pixel
        labels = kmeans.labels_

        # Calculate the weight of each color
        weights = np.bincount(labels) / len(labels) * 100

        # Convert the dominant colors to integers
        dominant_colors = dominant_colors.round(0).astype(int)

        # Format the output
        output = []
        for color, weight in zip(dominant_colors, weights):
            output.append({
                'color_type': 'RGB',
                'color': list(color),
                'one_change_per_tick': True,
                'weight': weight
            })

        # Filter the colors by HSV value and saturation
        filtered_colors = filter_colors_by_hsv(output)

        # If we have enough colors after filtering, break the loop
        if len(filtered_colors) >= num_colors:
            # Sort the colors by hue
            filtered_colors.sort(key=lambda x: rgb_to_hsv(x['color'])[0])
            return filtered_colors[:num_colors]

        # Otherwise, increase the number of clusters
        clusters += num_colors

    return filtered_colors[:num_colors]

def format_output(dominant_colors):
    formatted_output = ""
    for color_info in dominant_colors:
        formatted_output += "- color_type: rgb_color\n"
        formatted_output += "  color:\n"
        for value in color_info['color']:
            formatted_output += f"    - {value}\n"
        formatted_output += "  one_change_per_tick: true\n"
        formatted_output += f"  weight: {color_info['weight']:.2f}\n"
    return formatted_output

def rgb_to_hsv(color):
    r, g, b = [x / 255.0 for x in color]
    return colorsys.rgb_to_hsv(r, g, b)

def filter_colors_by_hsv(dominant_colors):
    filtered_colors = []
    for color_info in dominant_colors:
        hsv = rgb_to_hsv(color_info['color'])
        value = hsv[2] * 100
        saturation = hsv[1] * 100
        if value >= 50 and saturation >= 30:
            filtered_colors.append(color_info)
    return filtered_colors

def create_color_block_image(dominant_colors, output_image_path):
    # Calculate the width of each block based on the weight
    width, height = 500, 100  # Size of the output image
    total_weight = sum(color_info['weight'] for color_info in dominant_colors)

    # Create a new image
    block_image = Image.new("RGB", (width, height))
    draw = ImageDraw.Draw(block_image)

    current_x = 0
    for color_info in dominant_colors:
        block_width = int((color_info['weight'] / total_weight) * width)
        color = tuple(color_info['color'])
        draw.rectangle([current_x, 0, current_x + block_width, height], fill=color)
        current_x += block_width

    block_image.save(output_image_path)
    print(f"Color block image saved to {output_image_path}")

def main():
    if len(sys.argv) != 3:
        print("Usage: python script.py <image_path> <num_colors>")
        sys.exit(1)

    image_path = sys.argv[1]
    try:
        num_colors = int(sys.argv[2])
    except ValueError:
        print("The number of colors must be an integer.")
        sys.exit(1)

    dominant_colors = get_dominant_colors(image_path, num_colors)
    if dominant_colors:
        formatted_output = format_output(dominant_colors)
        print(formatted_output)

        output_image_path = "color_blocks.png"
        create_color_block_image(dominant_colors, output_image_path)

if __name__ == "__main__":
    main()
danishru commented 1 week ago

I hope someone finds this usefull, nut sure if i should make a PR...

Be sure to make a PR, the feature is cool. And if you can combine it with a feature request https://github.com/chazzu/hass-animated-scenes/issues/47 in the style of Philips Hue, then it will be just a great feature. That is, you upload a photo, your script analyzes the colors, and creates a card \ entity in the HA UI, which consists of the photo itself, the colors that will be used in Animated Scenes in the form of circles or some other representation to show the color weight for the scene, and the values ​​​​themselves somehow written to RGB Selectors, which are then passed to Animated Scenes.

For the user experience, it would be cool if the integration allowed you to simply create color profiles from user images, and then just as easily select them in scripts \ automation \ creation of an animation device.