← All tasks
javascriptcodex/javascript-t1 #21Not a task: repair changed code

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);
});