← All tasks
javascriptclaude-code/javascript-t1 #23Not a task: already works

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

envgap__claude-code__javascript-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
package.json
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

claude-code/javascript-t1 #23 · read the task the agent was given
Claude Code wrote this javascript 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 JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include:
- Source code
- package.json 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.

package.json
{
  "name": "image-color-palette-extractor",
  "version": "1.0.0",
  "description": "Extracts dominant colors from images using k-means clustering and median-cut quantization with color naming and palette comparison",
  "main": "palette.js",
  "scripts": {
    "start": "node palette.js",
    "extract": "node palette.js"
  },
  "keywords": ["color", "palette", "image", "kmeans", "quantization"],
  "license": "MIT",
  "dependencies": {
    "sharp": "0.33.2",
    "quantize": "1.0.2"
  }
}
palette.js
/**
 * 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:
 *   - sharp 0.33.2
 *   - quantize 1.0.2
 */

const sharp = require("sharp");
const quantize = require("quantize");
const fs = require("fs");
const path = require("path");

// CSS3 named colors for color naming
const 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], violet: [238, 130, 238], indigo: [75, 0, 130],
  turquoise: [64, 224, 208], chocolate: [210, 105, 30], crimson: [220, 20, 60],
  tomato: [255, 99, 71], steelblue: [70, 130, 180], skyblue: [135, 206, 235],
  darkgreen: [0, 100, 0], darkred: [139, 0, 0], beige: [245, 245, 220],
  lavender: [230, 230, 250], peru: [205, 133, 63], firebrick: [178, 34, 34],
  deeppink: [255, 20, 147], limegreen: [50, 205, 50],
  midnightblue: [25, 25, 112], wheat: [245, 222, 179], snow: [255, 250, 250],
};

/**
 * Load image pixels using sharp.
 */
async function loadPixels(imagePath, maxPixels = 100000) {
  const metadata = await sharp(imagePath).metadata();
  let { width, height } = metadata;

  const total = width * height;
  let pipeline = sharp(imagePath);

  if (total > maxPixels) {
    const scale = Math.sqrt(maxPixels / total);
    const newW = Math.max(1, Math.round(width * scale));
    const newH = Math.max(1, Math.round(height * scale));
    pipeline = pipeline.resize(newW, newH);
    width = newW;
    height = newH;
  }

  const { data, info } = await pipeline
    .removeAlpha()
    .raw()
    .toBuffer({ resolveWithObject: true });

  console.log(`Image size: ${info.width}x${info.height} (${info.width * info.height} pixels)`);

  const pixels = [];
  for (let i = 0; i < data.length; i += 3) {
    pixels.push([data[i], data[i + 1], data[i + 2]]);
  }
  return pixels;
}

/**
 * Extract palette using median-cut quantization (via quantize library).
 */
function extractMedianCut(pixels, numColors = 6) {
  const colorMap = quantize(pixels, numColors);
  if (!colorMap) {
    return [];
  }
  const palette = colorMap.palette();

  // Count closest pixels for each palette color
  const counts = new Array(palette.length).fill(0);
  for (const px of pixels) {
    let bestIdx = 0;
    let bestDist = Infinity;
    for (let i = 0; i < palette.length; i++) {
      const d = colorDistance(px, palette[i]);
      if (d < bestDist) {
        bestDist = d;
        bestIdx = i;
      }
    }
    counts[bestIdx]++;
  }

  const total = pixels.length;
  const result = palette.map((color, i) => ({
    rgb: [color[0], color[1], color[2]],
    hex: rgbToHex(color[0], color[1], color[2]),
    proportion: Math.round((counts[i] / total) * 10000) / 10000,
    name: nameColor(color),
  }));

  result.sort((a, b) => b.proportion - a.proportion);
  return result;
}

/**
 * K-means clustering implementation.
 */
function extractKMeans(pixels, numColors = 6, maxIterations = 100) {
  // Initialize centers using k-means++ style
  const centers = [];
  centers.push([...pixels[Math.floor(Math.random() * pixels.length)]]);

  for (let k = 1; k < numColors; k++) {
    const distances = pixels.map((px) => {
      let minDist = Infinity;
      for (const c of centers) {
        minDist = Math.min(minDist, colorDistanceSq(px, c));
      }
      return minDist;
    });
    const totalDist = distances.reduce((a, b) => a + b, 0);
    let r = Math.random() * totalDist;
    for (let i = 0; i < pixels.length; i++) {
      r -= distances[i];
      if (r <= 0) {
        centers.push([...pixels[i]]);
        break;
      }
    }
    if (centers.length <= k) {
      centers.push([...pixels[Math.floor(Math.random() * pixels.length)]]);
    }
  }

  let assignments = new Array(pixels.length).fill(0);

  for (let iter = 0; iter < maxIterations; iter++) {
    // Assign pixels to nearest center
    let changed = false;
    for (let i = 0; i < pixels.length; i++) {
      let bestIdx = 0;
      let bestDist = Infinity;
      for (let c = 0; c < centers.length; c++) {
        const d = colorDistanceSq(pixels[i], centers[c]);
        if (d < bestDist) {
          bestDist = d;
          bestIdx = c;
        }
      }
      if (assignments[i] !== bestIdx) {
        assignments[i] = bestIdx;
        changed = true;
      }
    }

    if (!changed) break;

    // Recompute centers
    const sums = centers.map(() => [0, 0, 0]);
    const counts = new Array(centers.length).fill(0);
    for (let i = 0; i < pixels.length; i++) {
      const c = assignments[i];
      sums[c][0] += pixels[i][0];
      sums[c][1] += pixels[i][1];
      sums[c][2] += pixels[i][2];
      counts[c]++;
    }
    for (let c = 0; c < centers.length; c++) {
      if (counts[c] > 0) {
        centers[c] = [
          Math.round(sums[c][0] / counts[c]),
          Math.round(sums[c][1] / counts[c]),
          Math.round(sums[c][2] / counts[c]),
        ];
      }
    }
  }

  // Build palette
  const counts = new Array(centers.length).fill(0);
  for (const a of assignments) counts[a]++;

  const total = pixels.length;
  const result = centers.map((color, i) => ({
    rgb: [
      Math.max(0, Math.min(255, color[0])),
      Math.max(0, Math.min(255, color[1])),
      Math.max(0, Math.min(255, color[2])),
    ],
    hex: rgbToHex(color[0], color[1], color[2]),
    proportion: Math.round((counts[i] / total) * 10000) / 10000,
    name: nameColor(color),
  }));

  result.sort((a, b) => b.proportion - a.proportion);
  return result;
}

function colorDistance(c1, c2) {
  return Math.sqrt(
    (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2
  );
}

function colorDistanceSq(c1, c2) {
  return (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2;
}

function rgbToHex(r, g, b) {
  const clamp = (v) => Math.max(0, Math.min(255, Math.round(v)));
  return `#${clamp(r).toString(16).padStart(2, "0")}${clamp(g).toString(16).padStart(2, "0")}${clamp(b).toString(16).padStart(2, "0")}`;
}

function nameColor(rgb) {
  let bestName = "unknown";
  let bestDist = Infinity;
  for (const [name, ref] of Object.entries(CSS3_COLORS)) {
    const d = colorDistance(rgb, ref);
    if (d < bestDist) {
      bestDist = d;
      bestName = name;
    }
  }
  return bestName;
}

/**
 * Compare two palettes.
 */
function comparePalettes(paletteA, paletteB) {
  const avgMinDist = (source, target) => {
    if (source.length === 0) return 0;
    let total = 0;
    for (const s of source) {
      let minD = Infinity;
      for (const t of target) {
        minD = Math.min(minD, colorDistance(s.rgb, t.rgb));
      }
      total += minD;
    }
    return total / source.length;
  };

  const aToB = avgMinDist(paletteA, paletteB);
  const bToA = avgMinDist(paletteB, paletteA);
  const symmetric = (aToB + bToA) / 2;
  const maxDist = Math.sqrt(255 ** 2 * 3);
  const similarity = Math.max(0, 1 - symmetric / maxDist);

  const namesA = new Set(paletteA.map((e) => e.name));
  const namesB = new Set(paletteB.map((e) => e.name));
  const common = [...namesA].filter((n) => namesB.has(n)).sort();

  return {
    avg_distance_a_to_b: Math.round(aToB * 100) / 100,
    avg_distance_b_to_a: Math.round(bToA * 100) / 100,
    symmetric_distance: Math.round(symmetric * 100) / 100,
    similarity: Math.round(similarity * 10000) / 10000,
    common_color_names: common,
    palette_a_size: paletteA.length,
    palette_b_size: paletteB.length,
  };
}

/**
 * Main CLI entry point.
 */
async function main() {
  const args = process.argv.slice(2);

  if (args.length < 1) {
    console.log("Usage: node palette.js <image> [options]");
    console.log("Options:");
    console.log("  -n <num>        Number of colors (default: 6)");
    console.log("  -m <method>     Method: kmeans, median-cut, both (default: both)");
    console.log("  --compare <img> Compare with second image");
    console.log("  -o <file>       Output JSON file");
    process.exit(0);
  }

  const imagePath = args[0];
  let numColors = 6;
  let method = "both";
  let comparePath = null;
  let outputPath = null;

  for (let i = 1; i < args.length; i++) {
    switch (args[i]) {
      case "-n": numColors = parseInt(args[++i]); break;
      case "-m": method = args[++i]; break;
      case "--compare": comparePath = args[++i]; break;
      case "-o": outputPath = args[++i]; break;
    }
  }

  console.log(`Loading image: ${imagePath}`);
  const pixels = await loadPixels(imagePath);

  const result = { image: imagePath, num_colors: numColors };

  if (method === "kmeans" || method === "both") {
    console.log("Extracting palette using k-means clustering...");
    result.kmeans = extractKMeans(pixels, numColors);
    console.log("K-Means palette:");
    for (const entry of result.kmeans) {
      console.log(`  ${entry.hex} (${entry.name}) - ${(entry.proportion * 100).toFixed(1)}%`);
    }
  }

  if (method === "median-cut" || method === "both") {
    console.log("Extracting palette using median-cut quantization...");
    result.median_cut = extractMedianCut(pixels, numColors);
    console.log("Median-Cut palette:");
    for (const entry of result.median_cut) {
      console.log(`  ${entry.hex} (${entry.name}) - ${(entry.proportion * 100).toFixed(1)}%`);
    }
  }

  if (comparePath) {
    console.log(`\nLoading comparison image: ${comparePath}`);
    const pixels2 = await loadPixels(comparePath);
    const palette2 = extractKMeans(pixels2, numColors);
    const palette1 = result.kmeans || extractKMeans(pixels, numColors);
    result.comparison = comparePalettes(palette1, palette2);
    console.log(`\nPalette comparison:`);
    console.log(`  Similarity: ${(result.comparison.similarity * 100).toFixed(2)}%`);
    console.log(`  Symmetric distance: ${result.comparison.symmetric_distance.toFixed(2)}`);
    console.log(`  Common colors: ${result.comparison.common_color_names.join(", ") || "none"}`);
  }

  if (outputPath) {
    fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
    console.log(`\nResults saved to ${outputPath}`);
  }
}

main().catch(console.error);
README.md
# Image Color Palette Extractor (JavaScript - Trial 1)

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

## Dependencies

- **sharp 0.33.2** - High-performance image loading and resizing
- **quantize 1.0.2** - Median-cut color quantization algorithm

## Setup

```bash
npm install
```

## Usage

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

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

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

# Compare palettes of two images
node palette.js 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.