← All tasks
pythonclaude-code/python-t1 #23Not a task: already works

Image Color Palette Extractor (python, written by Claude Code)

envgap__claude-code__python-t1-23

Written by a coding agent; not on GitHubWritten 2026-02-27

01 / FAILURE SIGNATURE

As the study recorded it

No identifying execution failure has been captured.
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
requirements.txt
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

claude-code/python-t1 #23 · read the task the agent was given
Claude Code wrote this python project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: Image Color Palette Extractor

Write a program that extracts the dominant color palette from images using color quantization algorithms, outputting the palette in multiple formats with percentage breakdowns.

FUNCTIONAL REQUIREMENTS:
- Accept an image file path as a command-line argument
- Extract a configurable number of dominant colors via --colors flag (default: 8, range 2-32)
- Use k-means clustering or median cut algorithm for color quantization (selectable via --algorithm flag)
- Output each color in multiple formats: hex (#RRGGBB), RGB (r,g,b), and HSL (h,s%,l%)
- Report the percentage of the image each dominant color represents
- Support color name mapping: find the closest named CSS/HTML color for each extracted color
- Generate a color palette visualization as a PNG image showing color swatches with hex labels via --visual flag
- Support extracting palette from a specific region of the image via --crop flag (x,y,width,height)
- Support color space analysis: report if the image is primarily warm-toned, cool-toned, or neutral based on the palette hue distribution
- Compare palettes between two images via --compare flag, showing common colors and unique colors with delta-E color difference scores
- Print the palette to console as a formatted table with color swatches represented by ANSI colored blocks
- Save the palette data as JSON with --output flag (default: palette.json)
- Support batch extraction from a directory of images via --batch flag with a summary showing all palettes
- If no input is given, generate three sample images (a sunset scene using warm gradients, an ocean scene using cool gradients, a forest scene using green tones), extract palettes from each, and display comparative results
- Handle errors: unsupported formats, very small images, images with very few unique colors

Create a complete Python project for a clean Ubuntu 22.04 machine with only Python 3.10+ installed. Include:
- Source code
- requirements.txt with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

palette.py
"""
Image Color Palette Extractor

Extracts dominant colors from images using k-means clustering and median-cut
quantization. Provides color naming and palette comparison functionality.

Dependencies:
    - Pillow 10.2.0
    - scikit-learn 1.4.0
    - numpy 1.26.4
"""

import argparse
import json
import math
import sys
from collections import Counter

import numpy as np
from PIL import Image
from sklearn.cluster import KMeans


# CSS3 named colors for color naming
CSS3_COLORS = {
    "black": (0, 0, 0), "white": (255, 255, 255), "red": (255, 0, 0),
    "lime": (0, 255, 0), "blue": (0, 0, 255), "yellow": (255, 255, 0),
    "cyan": (0, 255, 255), "magenta": (255, 0, 255), "silver": (192, 192, 192),
    "gray": (128, 128, 128), "maroon": (128, 0, 0), "olive": (128, 128, 0),
    "green": (0, 128, 0), "purple": (128, 0, 128), "teal": (0, 128, 128),
    "navy": (0, 0, 128), "orange": (255, 165, 0), "pink": (255, 192, 203),
    "coral": (255, 127, 80), "salmon": (250, 128, 114), "gold": (255, 215, 0),
    "khaki": (240, 230, 140), "plum": (221, 160, 221), "violet": (238, 130, 238),
    "indigo": (75, 0, 130), "turquoise": (64, 224, 208), "tan": (210, 180, 140),
    "sienna": (160, 82, 45), "chocolate": (210, 105, 30), "crimson": (220, 20, 60),
    "tomato": (255, 99, 71), "orchid": (218, 112, 214), "slategray": (112, 128, 144),
    "steelblue": (70, 130, 180), "royalblue": (65, 105, 225),
    "dodgerblue": (30, 144, 255), "skyblue": (135, 206, 235),
    "lightgreen": (144, 238, 144), "darkgreen": (0, 100, 0),
    "darkred": (139, 0, 0), "beige": (245, 245, 220),
    "ivory": (255, 255, 240), "lavender": (230, 230, 250),
    "mistyrose": (255, 228, 225), "peru": (205, 133, 63),
    "saddlebrown": (139, 69, 19), "firebrick": (178, 34, 34),
    "darkorange": (255, 140, 0), "deeppink": (255, 20, 147),
    "hotpink": (255, 105, 180), "limegreen": (50, 205, 50),
    "springgreen": (0, 255, 127), "aquamarine": (127, 255, 212),
    "midnightblue": (25, 25, 112), "darkblue": (0, 0, 139),
    "mediumblue": (0, 0, 205), "wheat": (245, 222, 179),
    "linen": (250, 240, 230), "snow": (255, 250, 250),
}


def load_image(path, max_pixels=100000):
    """Load an image and resize if needed to limit pixel count."""
    img = Image.open(path).convert("RGB")
    total = img.width * img.height
    if total > max_pixels:
        scale = math.sqrt(max_pixels / total)
        new_w = max(1, int(img.width * scale))
        new_h = max(1, int(img.height * scale))
        img = img.resize((new_w, new_h), Image.LANCZOS)
    return img


def image_to_pixels(img):
    """Convert PIL Image to numpy array of shape (N, 3)."""
    return np.array(img).reshape(-1, 3).astype(np.float64)


def extract_kmeans(pixels, n_colors=6, random_state=42):
    """Extract dominant colors using k-means clustering."""
    kmeans = KMeans(n_clusters=n_colors, random_state=random_state, n_init=10)
    kmeans.fit(pixels)
    centers = kmeans.cluster_centers_.astype(int)
    labels = kmeans.labels_
    counts = Counter(labels)
    total = len(labels)
    palette = []
    for i in range(n_colors):
        r, g, b = centers[i]
        proportion = counts[i] / total
        palette.append({
            "rgb": [int(r), int(g), int(b)],
            "hex": "#{:02x}{:02x}{:02x}".format(int(r), int(g), int(b)),
            "proportion": round(proportion, 4),
        })
    palette.sort(key=lambda c: c["proportion"], reverse=True)
    return palette


def median_cut_split(pixels, depth):
    """Recursively split pixel groups using median-cut quantization."""
    if depth == 0 or len(pixels) == 0:
        if len(pixels) == 0:
            return []
        mean_color = pixels.mean(axis=0).astype(int)
        return [(mean_color, len(pixels))]

    ranges = pixels.max(axis=0) - pixels.min(axis=0)
    channel = np.argmax(ranges)
    sorted_pixels = pixels[pixels[:, channel].argsort()]
    mid = len(sorted_pixels) // 2

    left = median_cut_split(sorted_pixels[:mid], depth - 1)
    right = median_cut_split(sorted_pixels[mid:], depth - 1)
    return left + right


def extract_median_cut(pixels, n_colors=6):
    """Extract dominant colors using median-cut quantization."""
    depth = int(math.ceil(math.log2(max(n_colors, 2))))
    groups = median_cut_split(pixels, depth)
    groups.sort(key=lambda g: g[1], reverse=True)
    groups = groups[:n_colors]
    total = sum(count for _, count in groups)
    palette = []
    for color, count in groups:
        r, g, b = int(color[0]), int(color[1]), int(color[2])
        palette.append({
            "rgb": [r, g, b],
            "hex": "#{:02x}{:02x}{:02x}".format(r, g, b),
            "proportion": round(count / total, 4),
        })
    return palette


def color_distance(c1, c2):
    """Compute Euclidean distance between two RGB colors."""
    return math.sqrt(sum((a - b) ** 2 for a, b in zip(c1, c2)))


def name_color(rgb):
    """Find the closest CSS3 named color for a given RGB tuple."""
    best_name = "unknown"
    best_dist = float("inf")
    for name, ref in CSS3_COLORS.items():
        d = color_distance(rgb, ref)
        if d < best_dist:
            best_dist = d
            best_name = name
    return best_name


def add_color_names(palette):
    """Add human-readable color names to each entry in the palette."""
    for entry in palette:
        entry["name"] = name_color(tuple(entry["rgb"]))
    return palette


def rgb_to_hsl(r, g, b):
    """Convert RGB (0-255) to HSL."""
    r, g, b = r / 255.0, g / 255.0, b / 255.0
    mx = max(r, g, b)
    mn = min(r, g, b)
    l = (mx + mn) / 2.0
    if mx == mn:
        h = s = 0.0
    else:
        d = mx - mn
        s = d / (2.0 - mx - mn) if l > 0.5 else d / (mx + mn)
        if mx == r:
            h = (g - b) / d + (6 if g < b else 0)
        elif mx == g:
            h = (b - r) / d + 2
        else:
            h = (r - g) / d + 4
        h /= 6.0
    return h * 360, s * 100, l * 100


def compare_palettes(palette_a, palette_b):
    """Compare two palettes and compute similarity metrics."""
    colors_a = [tuple(e["rgb"]) for e in palette_a]
    colors_b = [tuple(e["rgb"]) for e in palette_b]

    # Average minimum distance from A to B and B to A
    def avg_min_dist(source, target):
        total = 0
        for c in source:
            dists = [color_distance(c, t) for t in target]
            total += min(dists) if dists else 0
        return total / len(source) if source else 0

    a_to_b = avg_min_dist(colors_a, colors_b)
    b_to_a = avg_min_dist(colors_b, colors_a)
    symmetric_dist = (a_to_b + b_to_a) / 2.0

    # Normalize to a 0-1 similarity score (max possible distance ~441.67)
    max_dist = math.sqrt(255 ** 2 * 3)
    similarity = max(0.0, 1.0 - symmetric_dist / max_dist)

    # Common named colors
    names_a = set(e.get("name", "") for e in palette_a)
    names_b = set(e.get("name", "") for e in palette_b)
    common_names = names_a & names_b

    return {
        "avg_distance_a_to_b": round(a_to_b, 2),
        "avg_distance_b_to_a": round(b_to_a, 2),
        "symmetric_distance": round(symmetric_dist, 2),
        "similarity": round(similarity, 4),
        "common_color_names": sorted(common_names),
        "palette_a_size": len(palette_a),
        "palette_b_size": len(palette_b),
    }


def main():
    parser = argparse.ArgumentParser(
        description="Image Color Palette Extractor"
    )
    parser.add_argument("image", help="Path to the input image")
    parser.add_argument(
        "-n", "--num-colors", type=int, default=6,
        help="Number of dominant colors to extract (default: 6)"
    )
    parser.add_argument(
        "-m", "--method", choices=["kmeans", "median-cut", "both"],
        default="both",
        help="Extraction method (default: both)"
    )
    parser.add_argument(
        "--compare", metavar="IMAGE2",
        help="Second image for palette comparison"
    )
    parser.add_argument(
        "-o", "--output", help="Output JSON file path"
    )
    args = parser.parse_args()

    print(f"Loading image: {args.image}")
    img = load_image(args.image)
    pixels = image_to_pixels(img)
    print(f"Image size: {img.width}x{img.height} ({len(pixels)} pixels)")

    result = {"image": args.image, "num_colors": args.num_colors}

    if args.method in ("kmeans", "both"):
        print("Extracting palette using k-means clustering...")
        kmeans_palette = extract_kmeans(pixels, args.num_colors)
        kmeans_palette = add_color_names(kmeans_palette)
        result["kmeans"] = kmeans_palette
        print("K-Means palette:")
        for entry in kmeans_palette:
            print(f"  {entry['hex']} ({entry['name']}) - {entry['proportion']:.1%}")

    if args.method in ("median-cut", "both"):
        print("Extracting palette using median-cut quantization...")
        mc_palette = extract_median_cut(pixels, args.num_colors)
        mc_palette = add_color_names(mc_palette)
        result["median_cut"] = mc_palette
        print("Median-Cut palette:")
        for entry in mc_palette:
            print(f"  {entry['hex']} ({entry['name']}) - {entry['proportion']:.1%}")

    if args.compare:
        print(f"\nLoading comparison image: {args.compare}")
        img2 = load_image(args.compare)
        pixels2 = image_to_pixels(img2)
        palette2 = extract_kmeans(pixels2, args.num_colors)
        palette2 = add_color_names(palette2)
        palette1 = result.get("kmeans", extract_kmeans(pixels, args.num_colors))
        comparison = compare_palettes(palette1, palette2)
        result["comparison"] = comparison
        print(f"\nPalette comparison:")
        print(f"  Similarity: {comparison['similarity']:.2%}")
        print(f"  Symmetric distance: {comparison['symmetric_distance']:.2f}")
        print(f"  Common colors: {', '.join(comparison['common_color_names']) or 'none'}")

    if args.output:
        with open(args.output, "w") as f:
            json.dump(result, f, indent=2)
        print(f"\nResults saved to {args.output}")

    return result


if __name__ == "__main__":
    main()
README.md
# Image Color Palette Extractor (Python - Trial 1)

Extracts dominant colors from images using k-means clustering and median-cut quantization. Provides color naming and palette comparison.

## Dependencies

- **Pillow 10.2.0** - Image loading and manipulation
- **scikit-learn 1.4.0** - K-means clustering algorithm
- **numpy 1.26.4** - Numerical array operations

## Setup

```bash
pip install -r requirements.txt
```

## Usage

```bash
# Extract palette using both methods (default 6 colors)
python palette.py image.png

# Extract 8 colors using k-means only
python palette.py image.png -n 8 -m kmeans

# Extract using median-cut and save results
python palette.py image.png -m median-cut -o result.json

# Compare palettes of two images
python palette.py image1.png --compare image2.png -o comparison.json
```

## Output

JSON output includes RGB values, hex codes, proportion, and named colors for each dominant color. Comparison mode outputs similarity scores and common color names.
requirements.txt
Pillow==10.2.0
scikit-learn==1.4.0
numpy==1.26.4