Image Histogram Analyzer (javascript, written by Codex)
envgap__codex__javascript-t1-21
Written by a coding agent; not on GitHubWritten 2026-03-03
01 / FAILURE SIGNATURE
As the study recorded it
Expected integer for left but received undefined - extract() needs left/top not x/y
Not a benchmark task.
- Its repair changed source code, so it is not an environment task.
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
codex/javascript-t1 #21 · read the task the agent was given
Codex wrote this javascript project from the task below. It does not run 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
4 files, exactly as written, before any repair.
package-lock.json
{
"name": "image-histogram-analyzer",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "image-histogram-analyzer",
"version": "1.0.0",
"dependencies": {
"sharp": "0.33.5"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/sharp": {
"version": "0.33.5"
}
}
}
package.json
{
"name": "image-histogram-analyzer",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"sharp": "0.33.5"
}
}
README.md
# Image Histogram Analyzer (JavaScript) Computes RGB/luminance histograms, channel statistics, exposure/contrast assessment, image-to-image similarity metrics, CSV export, JSON report output, and batch analysis. ## Requirements - Ubuntu 22.04 - Node.js 20+ (LTS) ## Dependency (Pinned) - `sharp@0.33.5` ## Setup ```bash npm install ``` ## Run ```bash node src/index.js input.jpg node src/index.js input.jpg --crop 50,50,400,300 --cumulative --export histogram.csv --output analysis.json node src/index.js input.jpg --compare other.jpg node src/index.js ./images --batch --output batch_histogram_analysis.json node src/index.js ``` ## Output - Console summary includes per-channel statistics, exposure assessment, contrast assessment, and color balance analysis. - JSON output defaults to `histogram_analysis.json`. - CSV export schema: `bin,red_count,green_count,blue_count,luminance_count`. - No-arg mode generates overexposed, underexposed, and balanced sample images and compares them.
src/index.js
import fs from "fs";
import path from "path";
import sharp from "sharp";
const SUPPORTED = new Set([".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"]);
function parseArgs(argv) {
const options = {};
const positional = [];
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token.startsWith("--")) {
const key = token.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith("--")) {
options[key] = next;
i += 1;
} else {
options[key] = true;
}
} else {
positional.push(token);
}
}
return { options, positional };
}
function parseCrop(raw, width, height) {
if (!raw) return { x: 0, y: 0, width, height };
const m = /^(-?\d+),(-?\d+),(\d+),(\d+)$/.exec(String(raw));
if (!m) throw new Error("Invalid --crop. Use x,y,width,height.");
let x = Number(m[1]);
let y = Number(m[2]);
let w = Number(m[3]);
let h = Number(m[4]);
if (w <= 0 || h <= 0) throw new Error("Crop width/height must be positive.");
x = Math.max(0, x);
y = Math.max(0, y);
w = Math.min(w, width - x);
h = Math.min(h, height - y);
if (w <= 0 || h <= 0) throw new Error("Crop is outside image bounds.");
return { x, y, width: w, height: h };
}
function normalizeHist(hist) {
const total = hist.reduce((a, b) => a + b, 0);
if (total === 0) return hist.map(() => 0);
return hist.map((v) => v / total);
}
function statsFromHist(hist) {
const total = hist.reduce((a, b) => a + b, 0);
if (total === 0) {
return { mean: 0, median: 0, stddev: 0, skewness: 0, dynamicRange: 0, dominantRanges: [] };
}
let mean = 0;
for (let i = 0; i < 256; i += 1) mean += i * hist[i];
mean /= total;
let variance = 0;
for (let i = 0; i < 256; i += 1) variance += ((i - mean) ** 2) * hist[i];
variance /= total;
const stddev = Math.sqrt(variance);
let skewNum = 0;
if (stddev > 0) {
for (let i = 0; i < 256; i += 1) skewNum += ((i - mean) ** 3) * hist[i];
skewNum /= total;
}
const skewness = stddev > 0 ? skewNum / (stddev ** 3) : 0;
const half = total / 2;
let running = 0;
let median = 0;
for (let i = 0; i < 256; i += 1) {
running += hist[i];
if (running >= half) {
median = i;
break;
}
}
let minUsed = 0;
let maxUsed = 255;
while (minUsed < 256 && hist[minUsed] === 0) minUsed += 1;
while (maxUsed >= 0 && hist[maxUsed] === 0) maxUsed -= 1;
const dynamicRange = maxUsed >= minUsed ? (maxUsed - minUsed) : 0;
const ranges = [];
for (let start = 0; start < 256; start += 16) {
let sum = 0;
for (let i = start; i < start + 16; i += 1) sum += hist[i];
ranges.push({ range: `${start}-${start + 15}`, count: sum });
}
ranges.sort((a, b) => b.count - a.count);
const dominantRanges = ranges.slice(0, 3);
return { mean, median, stddev, skewness, dynamicRange, dominantRanges };
}
function cumulativeHist(hist) {
const out = new Array(256).fill(0);
let running = 0;
for (let i = 0; i < 256; i += 1) {
running += hist[i];
out[i] = running;
}
return out;
}
function exposureAssessment(lStats, lHist) {
const total = lHist.reduce((a, b) => a + b, 0) || 1;
const clipHigh = lHist[255] / total;
const clipLow = lHist[0] / total;
let exposure = "normal";
if (lStats.mean > 190 && clipHigh > 0.015) exposure = "overexposed";
else if (lStats.mean < 65 && clipLow > 0.015) exposure = "underexposed";
const lowContrast = lStats.dynamicRange < 80 || lStats.stddev < 35;
return {
exposure,
lowContrast,
clippedHighlightsRatio: clipHigh,
clippedShadowsRatio: clipLow,
};
}
function colorBalance(stats) {
const diffRG = Math.abs(stats.red.mean - stats.green.mean);
const diffRB = Math.abs(stats.red.mean - stats.blue.mean);
const diffGB = Math.abs(stats.green.mean - stats.blue.mean);
const balanced = diffRG < 10 && diffRB < 10 && diffGB < 10;
const dominant = [
{ channel: "red", v: stats.red.mean },
{ channel: "green", v: stats.green.mean },
{ channel: "blue", v: stats.blue.mean },
].sort((a, b) => b.v - a.v)[0].channel;
return { balanced, dominantChannel: dominant, meanDifferences: { rg: diffRG, rb: diffRB, gb: diffGB } };
}
function compareHists(histA, histB) {
const a = normalizeHist(histA);
const b = normalizeHist(histB);
const meanA = a.reduce((s, v) => s + v, 0) / a.length;
const meanB = b.reduce((s, v) => s + v, 0) / b.length;
let cov = 0;
let varA = 0;
let varB = 0;
let chi2 = 0;
let intersection = 0;
let bc = 0;
for (let i = 0; i < 256; i += 1) {
const da = a[i] - meanA;
const db = b[i] - meanB;
cov += da * db;
varA += da * da;
varB += db * db;
chi2 += ((a[i] - b[i]) ** 2) / (a[i] + b[i] + 1e-12);
intersection += Math.min(a[i], b[i]);
bc += Math.sqrt(a[i] * b[i]);
}
const correlation = varA > 0 && varB > 0 ? cov / Math.sqrt(varA * varB) : 0;
const bhattacharyya = Math.sqrt(Math.max(0, 1 - bc));
return { correlation, chiSquared: chi2, intersection, bhattacharyya };
}
function printSummary(analysis) {
const fmt = (n) => Number(n).toFixed(3);
console.log(`Image: ${analysis.image}`);
console.log(`Dimensions: ${analysis.width}x${analysis.height}${analysis.grayscale ? " | Grayscale" : ""}`);
for (const channel of ["red", "green", "blue", "luminance"]) {
const s = analysis.stats[channel];
console.log(`${channel.toUpperCase()}: mean=${fmt(s.mean)} median=${fmt(s.median)} stddev=${fmt(s.stddev)} skew=${fmt(s.skewness)} dynamic=${s.dynamicRange}`);
}
console.log(`Exposure assessment: ${analysis.assessment.exposure}`);
console.log(`Contrast assessment: ${analysis.assessment.lowContrast ? "low contrast" : "normal contrast"}`);
console.log(`Color balance: ${analysis.colorBalance.balanced ? "balanced" : `cast toward ${analysis.colorBalance.dominantChannel}`}`);
console.log("");
}
async function loadPixels(imagePath, cropArg) {
const base = sharp(imagePath).ensureAlpha();
const meta = await base.metadata();
if (!meta.width || !meta.height) throw new Error(`Corrupted/invalid image: ${imagePath}`);
const crop = parseCrop(cropArg, meta.width, meta.height);
const extracted = sharp(imagePath).ensureAlpha().extract(crop);
const { data, info } = await extracted.raw().toBuffer({ resolveWithObject: true });
return { data, info, crop };
}
async function analyzeImage(imagePath, options) {
const ext = path.extname(imagePath).toLowerCase();
if (!SUPPORTED.has(ext)) throw new Error(`Unsupported image format: ${imagePath}`);
const { data, info, crop } = await loadPixels(imagePath, options.crop);
const red = new Array(256).fill(0);
const green = new Array(256).fill(0);
const blue = new Array(256).fill(0);
const lum = new Array(256).fill(0);
let grayscale = true;
const channels = info.channels;
for (let i = 0; i < data.length; i += channels) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
if (grayscale && (r !== g || g !== b)) grayscale = false;
red[r] += 1;
green[g] += 1;
blue[b] += 1;
const y = Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);
lum[y] += 1;
}
if (grayscale) {
for (let i = 0; i < 256; i += 1) {
green[i] = red[i];
blue[i] = red[i];
}
}
const stats = {
red: statsFromHist(red),
green: statsFromHist(green),
blue: statsFromHist(blue),
luminance: statsFromHist(lum),
};
const assessment = exposureAssessment(stats.luminance, lum);
const balance = colorBalance(stats);
const result = {
image: path.resolve(imagePath),
width: crop.width,
height: crop.height,
crop,
grayscale,
stats,
assessment,
colorBalance: balance,
histograms: { red, green, blue, luminance: lum },
};
if (options.cumulative) {
result.cumulativeHistograms = {
red: cumulativeHist(red),
green: cumulativeHist(green),
blue: cumulativeHist(blue),
luminance: cumulativeHist(lum),
};
}
return result;
}
function writeCsv(exportPath, analysis) {
const rows = ["bin,red_count,green_count,blue_count,luminance_count"];
for (let i = 0; i < 256; i += 1) {
rows.push(`${i},${analysis.histograms.red[i]},${analysis.histograms.green[i]},${analysis.histograms.blue[i]},${analysis.histograms.luminance[i]}`);
}
fs.mkdirSync(path.dirname(exportPath), { recursive: true });
fs.writeFileSync(exportPath, `${rows.join("\n")}\n`, "utf8");
}
async function generateSample(kind, outputPath) {
const width = 640;
const height = 420;
const raw = Buffer.alloc(width * height * 3);
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
const idx = (y * width + x) * 3;
let base;
if (kind === "overexposed") base = 210 + ((x + y) % 45);
else if (kind === "underexposed") base = 15 + ((x + y) % 40);
else base = 65 + ((x * 3 + y * 2) % 130);
const noise = ((x * 17 + y * 31) % 23) - 11;
const v = Math.max(0, Math.min(255, base + noise));
raw[idx] = v;
raw[idx + 1] = Math.max(0, Math.min(255, v + (kind === "balanced" ? 3 : 0)));
raw[idx + 2] = Math.max(0, Math.min(255, v - (kind === "balanced" ? 3 : 0)));
}
}
await sharp(raw, { raw: { width, height, channels: 3 } }).png().toFile(outputPath);
}
function printBatchTable(analyses) {
console.log("Batch summary:");
console.log("Image | Mean(L) | Std(L) | Exposure | Contrast");
console.log("----- | ------- | ------ | -------- | --------");
for (const a of analyses) {
console.log(`${path.basename(a.image)} | ${a.stats.luminance.mean.toFixed(2)} | ${a.stats.luminance.stddev.toFixed(2)} | ${a.assessment.exposure} | ${a.assessment.lowContrast ? "low" : "normal"}`);
}
console.log("");
}
async function main() {
const { options, positional } = parseArgs(process.argv.slice(2));
const outputJson = path.resolve(options.output || "histogram_analysis.json");
if (!positional.length && !options.batch) {
const demoDir = path.resolve("sample_histograms");
fs.mkdirSync(demoDir, { recursive: true });
const samples = [
path.join(demoDir, "overexposed.png"),
path.join(demoDir, "underexposed.png"),
path.join(demoDir, "balanced.png"),
];
await generateSample("overexposed", samples[0]);
await generateSample("underexposed", samples[1]);
await generateSample("balanced", samples[2]);
const analyses = [];
for (const file of samples) analyses.push(await analyzeImage(file, options));
analyses.forEach(printSummary);
printBatchTable(analyses);
fs.writeFileSync(outputJson, JSON.stringify({ mode: "demo", analyses }, null, 2));
return;
}
if (!positional.length) throw new Error("Input path required.");
const input = path.resolve(positional[0]);
if (options.batch) {
if (!fs.existsSync(input) || !fs.statSync(input).isDirectory()) {
throw new Error("--batch requires a directory input.");
}
const files = fs.readdirSync(input)
.map((name) => path.join(input, name))
.filter((p) => fs.statSync(p).isFile() && SUPPORTED.has(path.extname(p).toLowerCase()));
const analyses = [];
for (const file of files) {
const analysis = await analyzeImage(file, options);
analyses.push(analysis);
printSummary(analysis);
}
printBatchTable(analyses);
fs.writeFileSync(outputJson, JSON.stringify({ mode: "batch", analyses }, null, 2));
return;
}
const analysis = await analyzeImage(input, options);
printSummary(analysis);
let comparison = null;
if (options.compare) {
const other = await analyzeImage(path.resolve(String(options.compare)), options);
comparison = {
targetImage: other.image,
red: compareHists(analysis.histograms.red, other.histograms.red),
green: compareHists(analysis.histograms.green, other.histograms.green),
blue: compareHists(analysis.histograms.blue, other.histograms.blue),
luminance: compareHists(analysis.histograms.luminance, other.histograms.luminance),
};
console.log("Comparison metrics (luminance):");
console.log(JSON.stringify(comparison.luminance, null, 2));
console.log("");
}
if (options.export) {
writeCsv(path.resolve(String(options.export)), analysis);
}
fs.writeFileSync(outputJson, JSON.stringify({ mode: "single", analysis, comparison }, null, 2));
}
main().catch((error) => {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});