← All tasks
javacodex/java-t1 #21Not a task: already works

Image Histogram Analyzer (java, written by Codex)

envgap__codex__java-t1-21

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

None
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
pom.xml
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/java-t1 #21 · read the task the agent was given
Codex wrote this java 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 Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include:
- Source code
- pom.xml 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.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.tmlr</groupId>
    <artifactId>image-histogram-analyzer</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.twelvemonkeys.imageio</groupId>
            <artifactId>imageio-core</artifactId>
            <version>3.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.twelvemonkeys.imageio</groupId>
            <artifactId>imageio-jpeg</artifactId>
            <version>3.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.twelvemonkeys.imageio</groupId>
            <artifactId>imageio-bmp</artifactId>
            <version>3.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.twelvemonkeys.imageio</groupId>
            <artifactId>imageio-tiff</artifactId>
            <version>3.11.0</version>
        </dependency>
        <dependency>
            <groupId>org.sejda.imageio</groupId>
            <artifactId>webp-imageio</artifactId>
            <version>0.1.6</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.11.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <release>17</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>ImageHistogramAnalyzer</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
README.md
# Image Histogram Analyzer (Java)

Computes per-channel and luminance histograms, statistical summaries, exposure/contrast detection, histogram comparison metrics, CSV export, JSON report output, and batch analysis.

## Requirements

- Ubuntu 22.04
- JDK 17+
- Maven 3.8+

## Dependencies (Pinned)

- `com.twelvemonkeys.imageio:imageio-core:3.11.0`
- `com.twelvemonkeys.imageio:imageio-jpeg:3.11.0`
- `com.twelvemonkeys.imageio:imageio-bmp:3.11.0`
- `com.twelvemonkeys.imageio:imageio-tiff:3.11.0`
- `org.sejda.imageio:webp-imageio:0.1.6`
- `com.google.code.gson:gson:2.11.0`

## Build

```bash
mvn clean package
```

## Run

```bash
java -cp target/image-histogram-analyzer-1.0.0.jar ImageHistogramAnalyzer input.jpg
java -cp target/image-histogram-analyzer-1.0.0.jar ImageHistogramAnalyzer input.jpg --crop 50,50,400,300 --cumulative --export histogram.csv --output analysis.json
java -cp target/image-histogram-analyzer-1.0.0.jar ImageHistogramAnalyzer input.jpg --compare other.jpg
java -cp target/image-histogram-analyzer-1.0.0.jar ImageHistogramAnalyzer ./images --batch --output batch_histogram_analysis.json
java -cp target/image-histogram-analyzer-1.0.0.jar ImageHistogramAnalyzer
```

## Output

- Console summary with per-channel stats, exposure assessment, contrast assessment, and color balance analysis.
- JSON report defaults to `histogram_analysis.json`.
- CSV schema: `bin,red_count,green_count,blue_count,luminance_count`.
- No-input mode generates overexposed, underexposed, and balanced samples and prints comparative output.
src/main/java/ImageHistogramAnalyzer.java
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

public final class ImageHistogramAnalyzer {
    private static final Set<String> SUPPORTED = Set.of("png", "jpg", "jpeg", "bmp", "tif", "tiff", "webp");
    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();

    private record ParsedArgs(Map<String, String> options, List<String> positional) {}
    private record CropRect(int x, int y, int width, int height) {}

    private static final class DominantRange {
        String range;
        long count;
    }

    private static final class ChannelStats {
        double mean;
        double median;
        double stddev;
        double skewness;
        List<DominantRange> dominantRanges;
        int dynamicRange;
    }

    private static final class Assessment {
        String exposure;
        boolean lowContrast;
        double clippedHighlightsRatio;
        double clippedShadowsRatio;
    }

    private static final class MeanDifferences {
        double rg;
        double rb;
        double gb;
    }

    private static final class ColorBalance {
        boolean balanced;
        String dominantChannel;
        MeanDifferences meanDifferences;
    }

    private static final class Histograms {
        int[] red;
        int[] green;
        int[] blue;
        int[] luminance;
    }

    private static final class ImageAnalysis {
        String image;
        int width;
        int height;
        CropRect crop;
        boolean grayscale;
        Map<String, ChannelStats> stats;
        Assessment assessment;
        ColorBalance colorBalance;
        Histograms histograms;
        Map<String, int[]> cumulativeHistograms;
    }

    private static final class ComparisonMetrics {
        double correlation;
        double chiSquared;
        double intersection;
        double bhattacharyya;
    }

    private static final class ComparisonResult {
        String targetImage;
        ComparisonMetrics red;
        ComparisonMetrics green;
        ComparisonMetrics blue;
        ComparisonMetrics luminance;
    }

    private ImageHistogramAnalyzer() {}

    public static void main(String[] args) {
        try {
            ParsedArgs parsed = parseArgs(args);
            String outputFile = parsed.options.getOrDefault("output", "histogram_analysis.json");
            Path outputPath = Paths.get(outputFile).toAbsolutePath();

            if (parsed.positional.isEmpty() && !parsed.options.containsKey("batch")) {
                runDemo(parsed.options, outputPath);
                return;
            }
            if (parsed.positional.isEmpty()) {
                throw new IllegalArgumentException("Input path required.");
            }

            Path input = Paths.get(parsed.positional.get(0)).toAbsolutePath();
            if (parsed.options.containsKey("batch")) {
                if (!Files.isDirectory(input)) throw new IllegalArgumentException("--batch requires a directory input.");
                List<ImageAnalysis> analyses = new ArrayList<>();
                for (Path p : listImages(input)) {
                    ImageAnalysis analysis = analyzeImage(p, parsed.options);
                    analyses.add(analysis);
                    printSummary(analysis);
                }
                printBatchTable(analyses);
                Map<String, Object> payload = new HashMap<>();
                payload.put("mode", "batch");
                payload.put("analyses", analyses);
                writeJson(outputPath, payload);
                return;
            }

            if (!Files.isRegularFile(input)) throw new IllegalArgumentException("Input file not found: " + input);
            ImageAnalysis analysis = analyzeImage(input, parsed.options);
            printSummary(analysis);

            ComparisonResult comparison = null;
            if (parsed.options.containsKey("compare")) {
                Path otherPath = Paths.get(parsed.options.get("compare")).toAbsolutePath();
                ImageAnalysis other = analyzeImage(otherPath, parsed.options);
                comparison = new ComparisonResult();
                comparison.targetImage = other.image;
                comparison.red = compareHistograms(analysis.histograms.red, other.histograms.red);
                comparison.green = compareHistograms(analysis.histograms.green, other.histograms.green);
                comparison.blue = compareHistograms(analysis.histograms.blue, other.histograms.blue);
                comparison.luminance = compareHistograms(analysis.histograms.luminance, other.histograms.luminance);
                System.out.println("Comparison metrics (luminance):");
                System.out.println(GSON.toJson(comparison.luminance));
                System.out.println();
            }

            if (parsed.options.containsKey("export")) {
                exportCsv(Paths.get(parsed.options.get("export")).toAbsolutePath(), analysis);
            }
            Map<String, Object> payload = new HashMap<>();
            payload.put("mode", "single");
            payload.put("analysis", analysis);
            payload.put("comparison", comparison);
            writeJson(outputPath, payload);
        } catch (Exception ex) {
            System.err.println("Error: " + ex.getMessage());
            System.exit(1);
        }
    }

    private static ParsedArgs parseArgs(String[] args) {
        Map<String, String> options = new HashMap<>();
        List<String> positional = new ArrayList<>();
        for (int i = 0; i < args.length; i++) {
            String token = args[i];
            if (token.startsWith("--")) {
                String key = token.substring(2);
                if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
                    options.put(key, args[++i]);
                } else {
                    options.put(key, "true");
                }
            } else {
                positional.add(token);
            }
        }
        return new ParsedArgs(options, positional);
    }

    private static List<Path> listImages(Path folder) throws Exception {
        List<Path> files = new ArrayList<>();
        try (var stream = Files.list(folder)) {
            stream.filter(Files::isRegularFile).forEach(p -> {
                if (SUPPORTED.contains(ext(p))) files.add(p.toAbsolutePath());
            });
        }
        return files;
    }

    private static String ext(Path p) {
        String n = p.getFileName().toString();
        int idx = n.lastIndexOf('.');
        if (idx < 0) return "";
        return n.substring(idx + 1).toLowerCase(Locale.ROOT);
    }

    private static CropRect parseCrop(String raw, int width, int height) {
        if (raw == null || raw.isBlank()) return new CropRect(0, 0, width, height);
        String[] parts = raw.split(",", 4);
        if (parts.length != 4) throw new IllegalArgumentException("Invalid --crop. Use x,y,width,height.");
        int x = Integer.parseInt(parts[0].trim());
        int y = Integer.parseInt(parts[1].trim());
        int w = Integer.parseInt(parts[2].trim());
        int h = Integer.parseInt(parts[3].trim());
        if (w <= 0 || h <= 0) throw new IllegalArgumentException("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 IllegalArgumentException("Crop is outside image bounds.");
        return new CropRect(x, y, w, h);
    }

    private static ImageAnalysis analyzeImage(Path imagePath, Map<String, String> options) throws Exception {
        if (!SUPPORTED.contains(ext(imagePath))) throw new IllegalArgumentException("Unsupported image format: " + imagePath);
        BufferedImage image = ImageIO.read(imagePath.toFile());
        if (image == null) throw new IllegalArgumentException("Corrupted/invalid image: " + imagePath);
        CropRect crop = parseCrop(options.get("crop"), image.getWidth(), image.getHeight());

        int[] red = new int[256];
        int[] green = new int[256];
        int[] blue = new int[256];
        int[] lum = new int[256];

        boolean grayscale = true;
        for (int y = crop.y; y < crop.y + crop.height; y++) {
            for (int x = crop.x; x < crop.x + crop.width; x++) {
                int rgb = image.getRGB(x, y);
                int r = (rgb >> 16) & 0xFF;
                int g = (rgb >> 8) & 0xFF;
                int b = rgb & 0xFF;
                if (grayscale && (r != g || g != b)) grayscale = false;
                red[r]++;
                green[g]++;
                blue[b]++;
                int yv = (int) Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);
                lum[yv]++;
            }
        }
        if (grayscale) {
            System.arraycopy(red, 0, green, 0, 256);
            System.arraycopy(red, 0, blue, 0, 256);
        }

        ChannelStats redStats = statsFromHist(red);
        ChannelStats greenStats = statsFromHist(green);
        ChannelStats blueStats = statsFromHist(blue);
        ChannelStats lumStats = statsFromHist(lum);

        Map<String, ChannelStats> stats = new HashMap<>();
        stats.put("red", redStats);
        stats.put("green", greenStats);
        stats.put("blue", blueStats);
        stats.put("luminance", lumStats);

        Histograms h = new Histograms();
        h.red = red;
        h.green = green;
        h.blue = blue;
        h.luminance = lum;

        ImageAnalysis analysis = new ImageAnalysis();
        analysis.image = imagePath.toAbsolutePath().toString();
        analysis.width = crop.width;
        analysis.height = crop.height;
        analysis.crop = crop;
        analysis.grayscale = grayscale;
        analysis.stats = stats;
        analysis.assessment = assessExposure(lumStats, lum);
        analysis.colorBalance = colorBalance(redStats, greenStats, blueStats);
        analysis.histograms = h;
        if (options.containsKey("cumulative")) {
            Map<String, int[]> cumulative = new HashMap<>();
            cumulative.put("red", cumulative(red));
            cumulative.put("green", cumulative(green));
            cumulative.put("blue", cumulative(blue));
            cumulative.put("luminance", cumulative(lum));
            analysis.cumulativeHistograms = cumulative;
        }
        return analysis;
    }

    private static ChannelStats statsFromHist(int[] hist) {
        long total = 0;
        for (int v : hist) total += v;
        ChannelStats out = new ChannelStats();
        if (total == 0) {
            out.mean = 0;
            out.median = 0;
            out.stddev = 0;
            out.skewness = 0;
            out.dynamicRange = 0;
            out.dominantRanges = List.of();
            return out;
        }

        double mean = 0;
        for (int i = 0; i < 256; i++) mean += i * (double) hist[i];
        mean /= total;

        double variance = 0;
        for (int i = 0; i < 256; i++) variance += (i - mean) * (i - mean) * hist[i];
        variance /= total;
        double stddev = Math.sqrt(variance);

        double skewness = 0;
        if (stddev > 0) {
            double m3 = 0;
            for (int i = 0; i < 256; i++) m3 += Math.pow(i - mean, 3) * hist[i];
            m3 /= total;
            skewness = m3 / Math.pow(stddev, 3);
        }

        long half = total / 2;
        long run = 0;
        int median = 0;
        for (int i = 0; i < 256; i++) {
            run += hist[i];
            if (run >= half) {
                median = i;
                break;
            }
        }

        int min = 0;
        int max = 255;
        while (min < 256 && hist[min] == 0) min++;
        while (max >= 0 && hist[max] == 0) max--;
        int dynamic = Math.max(0, max - min);

        List<DominantRange> ranges = new ArrayList<>();
        for (int start = 0; start < 256; start += 16) {
            long count = 0;
            for (int i = start; i < start + 16; i++) count += hist[i];
            DominantRange dr = new DominantRange();
            dr.range = start + "-" + (start + 15);
            dr.count = count;
            ranges.add(dr);
        }
        ranges.sort((a, b) -> Long.compare(b.count, a.count));

        out.mean = mean;
        out.median = median;
        out.stddev = stddev;
        out.skewness = skewness;
        out.dynamicRange = dynamic;
        out.dominantRanges = ranges.subList(0, 3);
        return out;
    }

    private static int[] cumulative(int[] hist) {
        int[] out = new int[hist.length];
        int run = 0;
        for (int i = 0; i < hist.length; i++) {
            run += hist[i];
            out[i] = run;
        }
        return out;
    }

    private static Assessment assessExposure(ChannelStats luminanceStats, int[] lum) {
        long total = 0;
        for (int v : lum) total += v;
        if (total == 0) total = 1;
        double high = lum[255] / (double) total;
        double low = lum[0] / (double) total;
        String exposure = "normal";
        if (luminanceStats.mean > 190 && high > 0.015) exposure = "overexposed";
        else if (luminanceStats.mean < 65 && low > 0.015) exposure = "underexposed";

        Assessment out = new Assessment();
        out.exposure = exposure;
        out.lowContrast = luminanceStats.dynamicRange < 80 || luminanceStats.stddev < 35;
        out.clippedHighlightsRatio = high;
        out.clippedShadowsRatio = low;
        return out;
    }

    private static ColorBalance colorBalance(ChannelStats red, ChannelStats green, ChannelStats blue) {
        double rg = Math.abs(red.mean - green.mean);
        double rb = Math.abs(red.mean - blue.mean);
        double gb = Math.abs(green.mean - blue.mean);

        MeanDifferences diffs = new MeanDifferences();
        diffs.rg = rg;
        diffs.rb = rb;
        diffs.gb = gb;

        String dominant = "red";
        if (green.mean > red.mean && green.mean >= blue.mean) dominant = "green";
        else if (blue.mean > red.mean && blue.mean >= green.mean) dominant = "blue";

        ColorBalance out = new ColorBalance();
        out.balanced = rg < 10 && rb < 10 && gb < 10;
        out.dominantChannel = dominant;
        out.meanDifferences = diffs;
        return out;
    }

    private static double[] normalize(int[] hist) {
        double total = 0;
        for (int v : hist) total += v;
        double[] out = new double[hist.length];
        if (total == 0) return out;
        for (int i = 0; i < hist.length; i++) out[i] = hist[i] / total;
        return out;
    }

    private static ComparisonMetrics compareHistograms(int[] histA, int[] histB) {
        double[] a = normalize(histA);
        double[] b = normalize(histB);
        double meanA = 0;
        double meanB = 0;
        for (int i = 0; i < 256; i++) {
            meanA += a[i];
            meanB += b[i];
        }
        meanA /= 256.0;
        meanB /= 256.0;

        double cov = 0, varA = 0, varB = 0, chi2 = 0, intersection = 0, bc = 0;
        for (int i = 0; i < 256; i++) {
            double da = a[i] - meanA;
            double db = b[i] - meanB;
            cov += da * db;
            varA += da * da;
            varB += db * db;
            chi2 += ((a[i] - b[i]) * (a[i] - b[i])) / (a[i] + b[i] + 1e-12);
            intersection += Math.min(a[i], b[i]);
            bc += Math.sqrt(a[i] * b[i]);
        }

        ComparisonMetrics out = new ComparisonMetrics();
        out.correlation = (varA > 0 && varB > 0) ? cov / Math.sqrt(varA * varB) : 0;
        out.chiSquared = chi2;
        out.intersection = intersection;
        out.bhattacharyya = Math.sqrt(Math.max(0, 1 - bc));
        return out;
    }

    private static void exportCsv(Path csvPath, ImageAnalysis analysis) throws Exception {
        Files.createDirectories(csvPath.toAbsolutePath().getParent());
        List<String> lines = new ArrayList<>();
        lines.add("bin,red_count,green_count,blue_count,luminance_count");
        for (int i = 0; i < 256; i++) {
            lines.add(i + "," + analysis.histograms.red[i] + "," + analysis.histograms.green[i] + "," +
                analysis.histograms.blue[i] + "," + analysis.histograms.luminance[i]);
        }
        Files.write(csvPath, lines, StandardCharsets.UTF_8);
    }

    private static void writeJson(Path outputPath, Object payload) throws Exception {
        Files.createDirectories(outputPath.toAbsolutePath().getParent());
        Files.writeString(outputPath, GSON.toJson(payload), StandardCharsets.UTF_8);
    }

    private static void printSummary(ImageAnalysis analysis) {
        System.out.println("Image: " + analysis.image);
        System.out.println("Dimensions: " + analysis.width + "x" + analysis.height + (analysis.grayscale ? " | Grayscale" : ""));
        for (String channel : List.of("red", "green", "blue", "luminance")) {
            ChannelStats s = analysis.stats.get(channel);
            System.out.printf(
                "%s: mean=%.3f median=%.3f stddev=%.3f skew=%.3f dynamic=%d%n",
                channel.toUpperCase(Locale.ROOT), s.mean, s.median, s.stddev, s.skewness, s.dynamicRange
            );
        }
        System.out.println("Exposure assessment: " + analysis.assessment.exposure);
        System.out.println("Contrast assessment: " + (analysis.assessment.lowContrast ? "low contrast" : "normal contrast"));
        System.out.println("Color balance: " + (analysis.colorBalance.balanced ? "balanced" : "cast toward " + analysis.colorBalance.dominantChannel));
        System.out.println();
    }

    private static void printBatchTable(List<ImageAnalysis> analyses) {
        System.out.println("Batch summary:");
        System.out.println("Image | Mean(L) | Std(L) | Exposure | Contrast");
        System.out.println("----- | ------- | ------ | -------- | --------");
        for (ImageAnalysis analysis : analyses) {
            ChannelStats l = analysis.stats.get("luminance");
            System.out.printf(
                "%s | %.2f | %.2f | %s | %s%n",
                Paths.get(analysis.image).getFileName(),
                l.mean,
                l.stddev,
                analysis.assessment.exposure,
                analysis.assessment.lowContrast ? "low" : "normal"
            );
        }
        System.out.println();
    }

    private static void createSample(String kind, Path out) throws Exception {
        int width = 640;
        int height = 420;
        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int base;
                if ("overexposed".equals(kind)) base = 210 + ((x + y) % 45);
                else if ("underexposed".equals(kind)) base = 15 + ((x + y) % 40);
                else base = 65 + ((x * 3 + y * 2) % 130);
                int noise = ((x * 17 + y * 31) % 23) - 11;
                int v = Math.max(0, Math.min(255, base + noise));
                int r = v;
                int g = Math.max(0, Math.min(255, v + ("balanced".equals(kind) ? 3 : 0)));
                int b = Math.max(0, Math.min(255, v - ("balanced".equals(kind) ? 3 : 0)));
                img.setRGB(x, y, new Color(r, g, b).getRGB());
            }
        }
        Files.createDirectories(out.toAbsolutePath().getParent());
        ImageIO.write(img, "png", out.toFile());
    }

    private static void runDemo(Map<String, String> options, Path outputPath) throws Exception {
        Path dir = Paths.get("sample_histograms").toAbsolutePath();
        Path over = dir.resolve("overexposed.png");
        Path under = dir.resolve("underexposed.png");
        Path balanced = dir.resolve("balanced.png");
        createSample("overexposed", over);
        createSample("underexposed", under);
        createSample("balanced", balanced);

        List<ImageAnalysis> analyses = new ArrayList<>();
        analyses.add(analyzeImage(over, options));
        analyses.add(analyzeImage(under, options));
        analyses.add(analyzeImage(balanced, options));
        for (ImageAnalysis analysis : analyses) printSummary(analysis);
        printBatchTable(analyses);

        Map<String, Object> payload = new HashMap<>();
        payload.put("mode", "demo");
        payload.put("analyses", analyses);
        writeJson(outputPath, payload);
    }
}