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

Image Histogram Analyzer (javascript, written by Claude Code)

envgap__claude-code__javascript-t1-21

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 #21 · 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 Histogram Analyzer

Write a program that computes and analyzes color histograms of images, providing statistical analysis of color distribution, channel comparisons, and similarity scoring between images.

FUNCTIONAL REQUIREMENTS:
- Accept an image file path as a command-line argument
- Compute per-channel histograms (Red, Green, Blue) with 256 bins each, plus a luminance/grayscale histogram
- Calculate statistics for each channel: mean intensity, median, standard deviation, skewness, dominant intensity ranges, and dynamic range (difference between darkest and brightest used values)
- Detect if an image is overexposed (high mean, clipped highlights), underexposed (low mean, clipped shadows), or low contrast (narrow histogram spread)
- Support histogram comparison between two images via --compare flag: compute correlation, chi-squared distance, intersection, and Bhattacharyya distance between their histograms
- Support cumulative histogram computation for each channel via --cumulative flag
- Generate a histogram data output as a CSV file with columns (bin, red_count, green_count, blue_count, luminance_count) via --export flag
- Support analyzing specific regions of an image via --crop flag (x,y,width,height)
- Print a text-based summary to console: per-channel statistics, exposure assessment, contrast assessment, and color balance analysis
- Save the full analysis as JSON with --output flag (default: histogram_analysis.json)
- Support batch analysis of multiple images via --batch flag with a summary comparison table
- If no input is given, generate three sample images (one overexposed, one underexposed, one well-balanced), analyze each, and display comparative results
- Handle errors: unsupported image formats, corrupted files, grayscale images (single-channel analysis)

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.

histogram.js
/**
 * Image Histogram Analyzer
 * Computes per-channel color histograms, detects exposure issues,
 * and compares histograms between images.
 *
 * Dependencies: sharp 0.33.2, chart.js 4.4.1
 */

const sharp = require("sharp");
const { createCanvas } = require("canvas");
const { Chart } = require("chart.js/auto");
const fs = require("fs");
const path = require("path");

/**
 * Compute per-channel color histograms for an image.
 * @param {string} imagePath - Path to the image file.
 * @returns {Promise<Object>} Histograms for each channel.
 */
async function computeHistogram(imagePath) {
  const { data, info } = await sharp(imagePath)
    .removeAlpha()
    .raw()
    .toBuffer({ resolveWithObject: true });

  const histograms = {
    Red: new Array(256).fill(0),
    Green: new Array(256).fill(0),
    Blue: new Array(256).fill(0),
    Luminance: new Array(256).fill(0),
  };

  const pixelCount = info.width * info.height;

  for (let i = 0; i < pixelCount; i++) {
    const offset = i * 3;
    const r = data[offset];
    const g = data[offset + 1];
    const b = data[offset + 2];

    histograms.Red[r]++;
    histograms.Green[g]++;
    histograms.Blue[b]++;

    const luminance = Math.min(
      255,
      Math.max(0, Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b))
    );
    histograms.Luminance[luminance]++;
  }

  return { histograms, totalPixels: pixelCount, width: info.width, height: info.height };
}

/**
 * Compute statistical measures for each channel histogram.
 * @param {Object} histograms - Channel histograms.
 * @param {number} totalPixels - Total number of pixels.
 * @returns {Object} Statistics per channel.
 */
function computeStatistics(histograms, totalPixels) {
  const stats = {};

  for (const [channel, hist] of Object.entries(histograms)) {
    let mean = 0;
    for (let i = 0; i < 256; i++) {
      mean += i * hist[i];
    }
    mean /= totalPixels;

    let variance = 0;
    for (let i = 0; i < 256; i++) {
      variance += Math.pow(i - mean, 2) * hist[i];
    }
    variance /= totalPixels;
    const stdDev = Math.sqrt(variance);

    // Median
    let cumulative = 0;
    let median = 0;
    for (let i = 0; i < 256; i++) {
      cumulative += hist[i];
      if (cumulative >= totalPixels / 2) {
        median = i;
        break;
      }
    }

    // Mode
    let mode = 0;
    let maxCount = 0;
    for (let i = 0; i < 256; i++) {
      if (hist[i] > maxCount) {
        maxCount = hist[i];
        mode = i;
      }
    }

    stats[channel] = { mean, median, mode, stdDev, totalPixels };
  }

  return stats;
}

/**
 * Detect exposure issues based on histogram distribution.
 * @param {Object} histograms - Channel histograms.
 * @param {number} totalPixels - Total number of pixels.
 * @param {number} threshold - Fraction threshold for detection.
 * @returns {Object} Detected exposure issues.
 */
function detectExposureIssues(histograms, totalPixels, threshold = 0.25) {
  const lumHist = histograms.Luminance;
  const issues = [];
  let severity = "normal";

  let shadowPixels = 0;
  let midtonePixels = 0;
  let highlightPixels = 0;
  let shadowClipping = 0;
  let highlightClipping = 0;

  for (let i = 0; i < 256; i++) {
    const fraction = lumHist[i] / totalPixels;
    if (i < 64) shadowPixels += fraction;
    else if (i < 192) midtonePixels += fraction;
    else highlightPixels += fraction;

    if (i < 5) shadowClipping += fraction;
    if (i >= 251) highlightClipping += fraction;
  }

  if (shadowPixels > threshold && highlightPixels < 0.1) {
    issues.push("Image appears underexposed (heavy shadows)");
    severity = "underexposed";
  }
  if (highlightPixels > threshold && shadowPixels < 0.1) {
    issues.push("Image appears overexposed (heavy highlights)");
    severity = "overexposed";
  }
  if (shadowClipping > 0.05) {
    issues.push(
      `Shadow clipping detected (${(shadowClipping * 100).toFixed(1)}% of pixels)`
    );
  }
  if (highlightClipping > 0.05) {
    issues.push(
      `Highlight clipping detected (${(highlightClipping * 100).toFixed(1)}% of pixels)`
    );
  }

  // Contrast check
  let mean = 0;
  for (let i = 0; i < 256; i++) {
    mean += i * lumHist[i];
  }
  mean /= totalPixels;

  let variance = 0;
  for (let i = 0; i < 256; i++) {
    variance += Math.pow(i - mean, 2) * lumHist[i];
  }
  variance /= totalPixels;
  const stdDev = Math.sqrt(variance);

  if (stdDev < 30) {
    issues.push(`Low contrast detected (std dev: ${stdDev.toFixed(1)})`);
    if (severity === "normal") severity = "low_contrast";
  }
  if (stdDev > 80) {
    issues.push(`High contrast detected (std dev: ${stdDev.toFixed(1)})`);
    if (severity === "normal") severity = "high_contrast";
  }

  if (issues.length === 0) {
    issues.push("No exposure issues detected");
  }

  return {
    severity,
    issues,
    shadowFraction: shadowPixels,
    midtoneFraction: midtonePixels,
    highlightFraction: highlightPixels,
    shadowClipping,
    highlightClipping,
  };
}

/**
 * Compare histograms between two images using multiple metrics.
 * @param {Object} hist1 - Histograms of the first image.
 * @param {number} total1 - Total pixels of first image.
 * @param {Object} hist2 - Histograms of the second image.
 * @param {number} total2 - Total pixels of second image.
 * @returns {Object} Comparison metrics per channel.
 */
function compareHistograms(hist1, total1, hist2, total2) {
  const comparison = {};

  for (const channel of Object.keys(hist1)) {
    if (!hist2[channel]) continue;

    const h1 = hist1[channel].map((v) => v / (total1 + 1e-10));
    const h2 = hist2[channel].map((v) => v / (total2 + 1e-10));

    // Correlation
    const mean1 = h1.reduce((a, b) => a + b, 0) / 256;
    const mean2 = h2.reduce((a, b) => a + b, 0) / 256;
    let num = 0, denom1 = 0, denom2 = 0;
    for (let i = 0; i < 256; i++) {
      const d1 = h1[i] - mean1;
      const d2 = h2[i] - mean2;
      num += d1 * d2;
      denom1 += d1 * d1;
      denom2 += d2 * d2;
    }
    const correlation = num / (Math.sqrt(denom1 * denom2) + 1e-10);

    // Chi-squared
    let chiSquared = 0;
    for (let i = 0; i < 256; i++) {
      const diff = h1[i] - h2[i];
      chiSquared += (diff * diff) / (h1[i] + h2[i] + 1e-10);
    }

    // Bhattacharyya
    let bc = 0;
    for (let i = 0; i < 256; i++) {
      bc += Math.sqrt(h1[i] * h2[i]);
    }
    const bhattacharyya = -Math.log(bc + 1e-10);

    // Earth Mover's Distance
    let emd = 0;
    let cdf1 = 0, cdf2 = 0;
    for (let i = 0; i < 256; i++) {
      cdf1 += h1[i];
      cdf2 += h2[i];
      emd += Math.abs(cdf1 - cdf2);
    }

    // Intersection
    let intersection = 0;
    for (let i = 0; i < 256; i++) {
      intersection += Math.min(h1[i], h2[i]);
    }

    comparison[channel] = {
      correlation,
      chiSquared,
      bhattacharyya,
      earthMoversDistance: emd,
      intersection,
    };
  }

  return comparison;
}

/**
 * Generate a histogram chart and save as PNG using Chart.js with node-canvas.
 * @param {Object} histograms - Channel histograms.
 * @param {string} title - Chart title.
 * @param {string} outputPath - Output file path.
 */
function plotHistogram(histograms, title, outputPath) {
  const canvas = createCanvas(800, 600);
  const ctx = canvas.getContext("2d");

  const labels = Array.from({ length: 256 }, (_, i) => i);
  const colors = {
    Red: "rgba(255, 0, 0, 0.6)",
    Green: "rgba(0, 180, 0, 0.6)",
    Blue: "rgba(0, 0, 255, 0.6)",
    Luminance: "rgba(128, 128, 128, 0.6)",
  };

  const datasets = Object.entries(histograms).map(([channel, data]) => ({
    label: channel,
    data: data,
    borderColor: colors[channel] || "rgba(0,0,0,0.6)",
    borderWidth: 1,
    pointRadius: 0,
    fill: false,
  }));

  new Chart(ctx, {
    type: "line",
    data: { labels, datasets },
    options: {
      responsive: false,
      plugins: { title: { display: true, text: title } },
      scales: {
        x: { title: { display: true, text: "Pixel Value" } },
        y: { title: { display: true, text: "Frequency" } },
      },
    },
  });

  const buffer = canvas.toBuffer("image/png");
  fs.writeFileSync(outputPath, buffer);
  console.log(`Histogram plot saved to: ${outputPath}`);
}

/**
 * Print a formatted analysis report.
 */
function printReport(imagePath, stats, exposure) {
  console.log(`\n${"=".repeat(60)}`);
  console.log("Image Histogram Analysis Report");
  console.log(`File: ${imagePath}`);
  console.log("=".repeat(60));

  console.log("\nChannel Statistics:");
  console.log(
    `${"Channel".padEnd(12)} ${"Mean".padStart(8)} ${"Median".padStart(8)} ${"Mode".padStart(8)} ${"StdDev".padStart(8)}`
  );
  console.log("-".repeat(48));

  for (const [channel, s] of Object.entries(stats)) {
    console.log(
      `${channel.padEnd(12)} ${s.mean.toFixed(1).padStart(8)} ${String(s.median).padStart(8)} ${String(s.mode).padStart(8)} ${s.stdDev.toFixed(1).padStart(8)}`
    );
  }

  console.log("\nExposure Analysis:");
  console.log(`  Severity: ${exposure.severity}`);
  for (const issue of exposure.issues) {
    console.log(`  - ${issue}`);
  }
  console.log(
    `  Shadows:    ${(exposure.shadowFraction * 100).toFixed(1)}% (clipping: ${(exposure.shadowClipping * 100).toFixed(1)}%)`
  );
  console.log(`  Midtones:   ${(exposure.midtoneFraction * 100).toFixed(1)}%`);
  console.log(
    `  Highlights: ${(exposure.highlightFraction * 100).toFixed(1)}% (clipping: ${(exposure.highlightClipping * 100).toFixed(1)}%)`
  );
}

async function main() {
  const args = process.argv.slice(2);

  if (args.length < 2) {
    console.log("Image Histogram Analyzer");
    console.log("Usage:");
    console.log("  node histogram.js analyze <image> [--plot output.png]");
    console.log("  node histogram.js compare <image1> <image2> [--plot output.png]");
    process.exit(0);
  }

  const command = args[0];

  if (command === "analyze") {
    const imagePath = args[1];
    if (!fs.existsSync(imagePath)) {
      console.error(`Error: File not found: ${imagePath}`);
      process.exit(1);
    }

    const { histograms, totalPixels } = await computeHistogram(imagePath);
    const stats = computeStatistics(histograms, totalPixels);
    const exposure = detectExposureIssues(histograms, totalPixels);

    printReport(imagePath, stats, exposure);

    const plotIdx = args.indexOf("--plot");
    if (plotIdx !== -1 && args[plotIdx + 1]) {
      plotHistogram(
        histograms,
        `Histogram: ${path.basename(imagePath)}`,
        args[plotIdx + 1]
      );
    }
  } else if (command === "compare") {
    const imagePath1 = args[1];
    const imagePath2 = args[2];

    if (!fs.existsSync(imagePath1)) {
      console.error(`Error: File not found: ${imagePath1}`);
      process.exit(1);
    }
    if (!fs.existsSync(imagePath2)) {
      console.error(`Error: File not found: ${imagePath2}`);
      process.exit(1);
    }

    const result1 = await computeHistogram(imagePath1);
    const result2 = await computeHistogram(imagePath2);

    const stats1 = computeStatistics(result1.histograms, result1.totalPixels);
    const stats2 = computeStatistics(result2.histograms, result2.totalPixels);
    const exposure1 = detectExposureIssues(result1.histograms, result1.totalPixels);
    const exposure2 = detectExposureIssues(result2.histograms, result2.totalPixels);

    printReport(imagePath1, stats1, exposure1);
    printReport(imagePath2, stats2, exposure2);

    const comparison = compareHistograms(
      result1.histograms,
      result1.totalPixels,
      result2.histograms,
      result2.totalPixels
    );

    console.log(`\n${"=".repeat(60)}`);
    console.log("Histogram Comparison Metrics");
    console.log("=".repeat(60));

    for (const [channel, metrics] of Object.entries(comparison)) {
      console.log(`\n  ${channel}:`);
      console.log(`    Correlation:           ${metrics.correlation.toFixed(4)}`);
      console.log(`    Chi-Squared Distance:  ${metrics.chiSquared.toFixed(4)}`);
      console.log(`    Bhattacharyya Dist:    ${metrics.bhattacharyya.toFixed(4)}`);
      console.log(`    Earth Mover's Dist:    ${metrics.earthMoversDistance.toFixed(4)}`);
      console.log(`    Intersection:          ${metrics.intersection.toFixed(4)}`);
    }

    const plotIdx = args.indexOf("--plot");
    if (plotIdx !== -1 && args[plotIdx + 1]) {
      plotHistogram(
        result1.histograms,
        "Histogram Comparison",
        args[plotIdx + 1]
      );
    }
  } else {
    console.error(`Unknown command: ${command}`);
    process.exit(1);
  }
}

main().catch((err) => {
  console.error("Error:", err.message);
  process.exit(1);
});
package.json
{
  "name": "image-histogram-analyzer",
  "version": "1.0.0",
  "description": "Computes per-channel color histograms, detects exposure issues, and compares histograms between images.",
  "main": "histogram.js",
  "scripts": {
    "start": "node histogram.js",
    "analyze": "node histogram.js analyze",
    "compare": "node histogram.js compare"
  },
  "dependencies": {
    "sharp": "0.33.2",
    "chart.js": "4.4.1",
    "canvas": "2.11.2"
  },
  "keywords": ["image", "histogram", "color", "exposure", "analysis"],
  "license": "MIT"
}
README.md
# Image Histogram Analyzer (JavaScript - Trial 1)

Computes per-channel color histograms, detects exposure issues, and compares histograms between images.

## Dependencies

- **sharp** (0.33.2) - High-performance image loading and pixel access
- **chart.js** (4.4.1) - Histogram chart generation (with node-canvas for server-side rendering)

## Installation

```bash
npm install
```

## Usage

### Analyze a single image

```bash
node histogram.js analyze image.jpg
node histogram.js analyze image.jpg --plot output.png
```

### Compare two images

```bash
node histogram.js compare image1.jpg image2.jpg
node histogram.js compare image1.jpg image2.jpg --plot comparison.png
```

## Features

- Per-channel (R, G, B) and luminance histogram computation
- Statistical analysis: mean, median, mode, standard deviation
- Exposure issue detection: overexposure, underexposure, clipping, contrast
- Histogram comparison using correlation, chi-squared, Bhattacharyya, EMD, and intersection metrics
- Chart visualization with Chart.js