← All tasks
cppcodex/cpp-t1 #21Not a task: repair not recorded

Image Histogram Analyzer (cpp, written by Codex)

envgap__codex__cpp-t1-21

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

01 / FAILURE SIGNATURE

As the study recorded it

OpenCV headers not found - missing target_include_directories
Not a benchmark task.
  • It was made to work, but its repair cannot be rebuilt from the saved files (the saved copy shows no change, or not all of the changes the study's notes describe), so there is no fix to score against.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
CMakeLists.txt
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/cpp-t1 #21 · read the task the agent was given
Codex wrote this cpp 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 C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include:
- Source code
- CMakeLists.txt 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.

CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(image_histogram_analyzer_cpp VERSION 1.0.0 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

include(FetchContent)

FetchContent_Declare(
  opencv
  URL https://github.com/opencv/opencv/archive/refs/tags/4.10.0.zip
)
set(BUILD_LIST "core,imgproc,imgcodecs" CACHE STRING "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_PERF_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(opencv)

FetchContent_Declare(
  nlohmann_json
  URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
)
FetchContent_MakeAvailable(nlohmann_json)

add_executable(image_histogram_analyzer src/main.cpp)
target_link_libraries(image_histogram_analyzer PRIVATE opencv_core opencv_imgproc opencv_imgcodecs nlohmann_json::nlohmann_json)
README.md
# Image Histogram Analyzer (C++)

Computes RGB and luminance histograms, channel statistics, exposure/contrast assessment, histogram similarity metrics, CSV export, JSON output, and batch image analysis.

## Requirements

- Ubuntu 22.04
- G++ 12+
- CMake 3.22+

## Dependencies (Pinned)

- OpenCV `4.10.0` (via `FetchContent`)
- nlohmann/json `3.11.3` (via `FetchContent`)

## Build

```bash
cmake -S . -B build
cmake --build build -j
```

## Run

```bash
./build/image_histogram_analyzer input.jpg
./build/image_histogram_analyzer input.jpg --crop 50,50,400,300 --cumulative --export histogram.csv --output analysis.json
./build/image_histogram_analyzer input.jpg --compare other.jpg
./build/image_histogram_analyzer ./images --batch --output batch_histogram_analysis.json
./build/image_histogram_analyzer
```

## Output

- Console summary includes per-channel statistics, exposure assessment, contrast assessment, and color balance analysis.
- JSON output defaults to `histogram_analysis.json`.
- CSV columns: `bin,red_count,green_count,blue_count,luminance_count`.
- No-input mode generates overexposed, underexposed, and balanced sample images and prints comparative summary output.
src/main.cpp
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <nlohmann/json.hpp>

#include <algorithm>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <numeric>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

namespace fs = std::filesystem;
using json = nlohmann::json;

struct CliOptions {
    std::map<std::string, std::string> kv;
    std::vector<std::string> positional;
};

struct CropRect {
    int x;
    int y;
    int width;
    int height;
};

struct ChannelStats {
    double mean{};
    double median{};
    double stddev{};
    double skewness{};
    int dynamicRange{};
    std::vector<std::pair<std::string, long long>> dominantRanges;
};

struct Assessment {
    std::string exposure;
    bool lowContrast{};
    double clippedHighlightsRatio{};
    double clippedShadowsRatio{};
};

struct ColorBalance {
    bool balanced{};
    std::string dominantChannel;
    double diffRG{};
    double diffRB{};
    double diffGB{};
};

struct Analysis {
    std::string image;
    int width{};
    int height{};
    CropRect crop{};
    bool grayscale{};
    std::vector<int> red;
    std::vector<int> green;
    std::vector<int> blue;
    std::vector<int> luminance;
    ChannelStats redStats;
    ChannelStats greenStats;
    ChannelStats blueStats;
    ChannelStats lumStats;
    Assessment assessment;
    ColorBalance colorBalance;
    std::vector<int> redCum;
    std::vector<int> greenCum;
    std::vector<int> blueCum;
    std::vector<int> lumCum;
    bool includeCumulative{};
};

const std::set<std::string> kSupported = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"};

CliOptions parseArgs(int argc, char** argv) {
    CliOptions out;
    for (int i = 1; i < argc; ++i) {
        std::string token = argv[i];
        if (token.rfind("--", 0) == 0) {
            std::string key = token.substr(2);
            if (i + 1 < argc) {
                std::string next = argv[i + 1];
                if (next.rfind("--", 0) != 0) {
                    out.kv[key] = next;
                    ++i;
                    continue;
                }
            }
            out.kv[key] = "true";
        } else {
            out.positional.push_back(token);
        }
    }
    return out;
}

std::string lower(std::string s) {
    for (char& c : s) {
        if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
    }
    return s;
}

bool supportedPath(const fs::path& p) {
    if (!p.has_extension()) return false;
    return kSupported.count(lower(p.extension().string())) > 0;
}

CropRect parseCrop(const std::string& raw, int width, int height) {
    if (raw.empty()) return {0, 0, width, height};
    std::stringstream ss(raw);
    std::string a, b, c, d;
    if (!std::getline(ss, a, ',') || !std::getline(ss, b, ',') || !std::getline(ss, c, ',') || !std::getline(ss, d, ',')) {
        throw std::invalid_argument("Invalid --crop. Use x,y,width,height.");
    }
    int x = std::stoi(a);
    int y = std::stoi(b);
    int w = std::stoi(c);
    int h = std::stoi(d);
    if (w <= 0 || h <= 0) throw std::invalid_argument("Crop width/height must be positive.");
    x = std::max(0, x);
    y = std::max(0, y);
    w = std::min(w, width - x);
    h = std::min(h, height - y);
    if (w <= 0 || h <= 0) throw std::invalid_argument("Crop is outside image bounds.");
    return {x, y, w, h};
}

std::vector<double> normalize(const std::vector<int>& hist) {
    double total = std::accumulate(hist.begin(), hist.end(), 0.0);
    std::vector<double> out(256, 0.0);
    if (total <= 0) return out;
    for (int i = 0; i < 256; ++i) out[i] = hist[i] / total;
    return out;
}

std::vector<int> cumulative(const std::vector<int>& hist) {
    std::vector<int> out(256, 0);
    int run = 0;
    for (int i = 0; i < 256; ++i) {
        run += hist[i];
        out[i] = run;
    }
    return out;
}

ChannelStats statsFromHist(const std::vector<int>& hist) {
    long long total = std::accumulate(hist.begin(), hist.end(), 0LL);
    ChannelStats out{};
    if (total == 0) return out;

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

    double variance = 0.0;
    for (int i = 0; i < 256; ++i) variance += (i - mean) * (i - mean) * hist[i];
    variance /= static_cast<double>(total);
    double stddev = std::sqrt(variance);

    double skew = 0.0;
    if (stddev > 0) {
        double m3 = 0.0;
        for (int i = 0; i < 256; ++i) m3 += std::pow(i - mean, 3) * hist[i];
        m3 /= static_cast<double>(total);
        skew = m3 / std::pow(stddev, 3);
    }

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

    int minUsed = 0;
    int maxUsed = 255;
    while (minUsed < 256 && hist[minUsed] == 0) ++minUsed;
    while (maxUsed >= 0 && hist[maxUsed] == 0) --maxUsed;
    int dynamic = maxUsed >= minUsed ? (maxUsed - minUsed) : 0;

    std::vector<std::pair<std::string, long long>> ranges;
    for (int start = 0; start < 256; start += 16) {
        long long sum = 0;
        for (int i = start; i < start + 16; ++i) sum += hist[i];
        ranges.emplace_back(std::to_string(start) + "-" + std::to_string(start + 15), sum);
    }
    std::sort(ranges.begin(), ranges.end(), [](const auto& a, const auto& b) { return a.second > b.second; });

    out.mean = mean;
    out.median = median;
    out.stddev = stddev;
    out.skewness = skew;
    out.dynamicRange = dynamic;
    out.dominantRanges.assign(ranges.begin(), ranges.begin() + 3);
    return out;
}

Assessment assessExposure(const ChannelStats& lumStats, const std::vector<int>& lumHist) {
    long long total = std::accumulate(lumHist.begin(), lumHist.end(), 0LL);
    if (total == 0) total = 1;
    double high = lumHist[255] / static_cast<double>(total);
    double low = lumHist[0] / static_cast<double>(total);
    std::string exposure = "normal";
    if (lumStats.mean > 190.0 && high > 0.015) exposure = "overexposed";
    else if (lumStats.mean < 65.0 && low > 0.015) exposure = "underexposed";
    bool lowContrast = lumStats.dynamicRange < 80 || lumStats.stddev < 35.0;
    return {exposure, lowContrast, high, low};
}

ColorBalance detectColorBalance(const ChannelStats& r, const ChannelStats& g, const ChannelStats& b) {
    double rg = std::abs(r.mean - g.mean);
    double rb = std::abs(r.mean - b.mean);
    double gb = std::abs(g.mean - b.mean);
    bool balanced = rg < 10 && rb < 10 && gb < 10;
    std::string dominant = "red";
    if (g.mean > r.mean && g.mean >= b.mean) dominant = "green";
    else if (b.mean > r.mean && b.mean >= g.mean) dominant = "blue";
    return {balanced, dominant, rg, rb, gb};
}

Analysis analyzeImage(const fs::path& imagePath, const CliOptions& options) {
    if (!supportedPath(imagePath)) {
        throw std::invalid_argument("Unsupported image format: " + imagePath.string());
    }
    cv::Mat image = cv::imread(imagePath.string(), cv::IMREAD_COLOR);
    if (image.empty()) throw std::runtime_error("Corrupted/invalid image: " + imagePath.string());

    CropRect crop = parseCrop(options.kv.count("crop") ? options.kv.at("crop") : "", image.cols, image.rows);
    cv::Rect roi(crop.x, crop.y, crop.width, crop.height);
    cv::Mat cropped = image(roi).clone();

    std::vector<int> red(256, 0), green(256, 0), blue(256, 0), lum(256, 0);
    bool grayscale = true;
    for (int y = 0; y < cropped.rows; ++y) {
        for (int x = 0; x < cropped.cols; ++x) {
            cv::Vec3b px = cropped.at<cv::Vec3b>(y, x);
            int b = px[0];
            int g = px[1];
            int r = px[2];
            if (grayscale && !(r == g && g == b)) grayscale = false;
            red[r]++;
            green[g]++;
            blue[b]++;
            int yv = static_cast<int>(std::round(0.2126 * r + 0.7152 * g + 0.0722 * b));
            lum[yv]++;
        }
    }
    if (grayscale) {
        green = red;
        blue = red;
    }

    Analysis out{};
    out.image = fs::absolute(imagePath).string();
    out.width = cropped.cols;
    out.height = cropped.rows;
    out.crop = crop;
    out.grayscale = grayscale;
    out.red = red;
    out.green = green;
    out.blue = blue;
    out.luminance = lum;
    out.redStats = statsFromHist(red);
    out.greenStats = statsFromHist(green);
    out.blueStats = statsFromHist(blue);
    out.lumStats = statsFromHist(lum);
    out.assessment = assessExposure(out.lumStats, lum);
    out.colorBalance = detectColorBalance(out.redStats, out.greenStats, out.blueStats);
    out.includeCumulative = options.kv.count("cumulative") > 0;
    if (out.includeCumulative) {
        out.redCum = cumulative(red);
        out.greenCum = cumulative(green);
        out.blueCum = cumulative(blue);
        out.lumCum = cumulative(lum);
    }
    return out;
}

json statsToJson(const ChannelStats& s) {
    json ranges = json::array();
    for (const auto& r : s.dominantRanges) {
        ranges.push_back({{"range", r.first}, {"count", r.second}});
    }
    return {
        {"mean", s.mean},
        {"median", s.median},
        {"stddev", s.stddev},
        {"skewness", s.skewness},
        {"dominant_ranges", ranges},
        {"dynamic_range", s.dynamicRange}
    };
}

json analysisToJson(const Analysis& a) {
    json j = {
        {"image", a.image},
        {"width", a.width},
        {"height", a.height},
        {"crop", {{"x", a.crop.x}, {"y", a.crop.y}, {"width", a.crop.width}, {"height", a.crop.height}}},
        {"grayscale", a.grayscale},
        {"stats", {
            {"red", statsToJson(a.redStats)},
            {"green", statsToJson(a.greenStats)},
            {"blue", statsToJson(a.blueStats)},
            {"luminance", statsToJson(a.lumStats)}
        }},
        {"assessment", {
            {"exposure", a.assessment.exposure},
            {"low_contrast", a.assessment.lowContrast},
            {"clipped_highlights_ratio", a.assessment.clippedHighlightsRatio},
            {"clipped_shadows_ratio", a.assessment.clippedShadowsRatio}
        }},
        {"color_balance", {
            {"balanced", a.colorBalance.balanced},
            {"dominant_channel", a.colorBalance.dominantChannel},
            {"mean_differences", {
                {"rg", a.colorBalance.diffRG},
                {"rb", a.colorBalance.diffRB},
                {"gb", a.colorBalance.diffGB}
            }}
        }},
        {"histograms", {
            {"red", a.red},
            {"green", a.green},
            {"blue", a.blue},
            {"luminance", a.luminance}
        }}
    };

    if (a.includeCumulative) {
        j["cumulative_histograms"] = {
            {"red", a.redCum},
            {"green", a.greenCum},
            {"blue", a.blueCum},
            {"luminance", a.lumCum}
        };
    }
    return j;
}

json compareHistograms(const std::vector<int>& h1, const std::vector<int>& h2) {
    std::vector<double> a = normalize(h1);
    std::vector<double> b = normalize(h2);
    double meanA = std::accumulate(a.begin(), a.end(), 0.0) / 256.0;
    double meanB = std::accumulate(b.begin(), b.end(), 0.0) / 256.0;

    double cov = 0, varA = 0, varB = 0, chi2 = 0, inter = 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);
        inter += std::min(a[i], b[i]);
        bc += std::sqrt(a[i] * b[i]);
    }
    double corr = (varA > 0 && varB > 0) ? (cov / std::sqrt(varA * varB)) : 0;
    double bhatta = std::sqrt(std::max(0.0, 1.0 - bc));
    return {
        {"correlation", corr},
        {"chi_squared", chi2},
        {"intersection", inter},
        {"bhattacharyya", bhatta}
    };
}

void printSummary(const Analysis& a) {
    auto printStats = [](const std::string& name, const ChannelStats& s) {
        std::cout << name << ": mean=" << std::fixed << std::setprecision(3) << s.mean
                  << " median=" << s.median
                  << " stddev=" << s.stddev
                  << " skew=" << s.skewness
                  << " dynamic=" << s.dynamicRange << "\n";
    };

    std::cout << "Image: " << a.image << "\n";
    std::cout << "Dimensions: " << a.width << "x" << a.height << (a.grayscale ? " | Grayscale" : "") << "\n";
    printStats("RED", a.redStats);
    printStats("GREEN", a.greenStats);
    printStats("BLUE", a.blueStats);
    printStats("LUMINANCE", a.lumStats);
    std::cout << "Exposure assessment: " << a.assessment.exposure << "\n";
    std::cout << "Contrast assessment: " << (a.assessment.lowContrast ? "low contrast" : "normal contrast") << "\n";
    std::cout << "Color balance: " << (a.colorBalance.balanced ? "balanced" : "cast toward " + a.colorBalance.dominantChannel) << "\n\n";
}

void printBatchTable(const std::vector<Analysis>& analyses) {
    std::cout << "Batch summary:\n";
    std::cout << "Image | Mean(L) | Std(L) | Exposure | Contrast\n";
    std::cout << "----- | ------- | ------ | -------- | --------\n";
    for (const auto& a : analyses) {
        std::cout << fs::path(a.image).filename().string() << " | "
                  << std::fixed << std::setprecision(2) << a.lumStats.mean << " | "
                  << a.lumStats.stddev << " | "
                  << a.assessment.exposure << " | "
                  << (a.assessment.lowContrast ? "low" : "normal") << "\n";
    }
    std::cout << "\n";
}

void writeJson(const fs::path& out, const json& payload) {
    fs::create_directories(out.parent_path());
    std::ofstream ofs(out);
    ofs << payload.dump(2) << "\n";
}

void exportCsv(const fs::path& out, const Analysis& a) {
    fs::create_directories(out.parent_path());
    std::ofstream ofs(out);
    ofs << "bin,red_count,green_count,blue_count,luminance_count\n";
    for (int i = 0; i < 256; ++i) {
        ofs << i << "," << a.red[i] << "," << a.green[i] << "," << a.blue[i] << "," << a.luminance[i] << "\n";
    }
}

void createSample(const std::string& kind, const fs::path& outPath) {
    const int width = 640;
    const int height = 420;
    cv::Mat image(height, width, CV_8UC3);
    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {
            int base = 0;
            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);
            int noise = ((x * 17 + y * 31) % 23) - 11;
            int v = std::clamp(base + noise, 0, 255);
            int g = std::clamp(v + (kind == "balanced" ? 3 : 0), 0, 255);
            int b = std::clamp(v - (kind == "balanced" ? 3 : 0), 0, 255);
            image.at<cv::Vec3b>(y, x) = cv::Vec3b(static_cast<uchar>(b), static_cast<uchar>(g), static_cast<uchar>(v));
        }
    }
    fs::create_directories(outPath.parent_path());
    cv::imwrite(outPath.string(), image);
}

void runDemo(const CliOptions& options, const fs::path& outputPath) {
    fs::path dir = fs::absolute("sample_histograms");
    fs::path over = dir / "overexposed.png";
    fs::path under = dir / "underexposed.png";
    fs::path balanced = dir / "balanced.png";
    createSample("overexposed", over);
    createSample("underexposed", under);
    createSample("balanced", balanced);

    std::vector<Analysis> analyses;
    analyses.push_back(analyzeImage(over, options));
    analyses.push_back(analyzeImage(under, options));
    analyses.push_back(analyzeImage(balanced, options));
    for (const auto& a : analyses) printSummary(a);
    printBatchTable(analyses);

    json payload;
    payload["mode"] = "demo";
    payload["analyses"] = json::array();
    for (const auto& a : analyses) payload["analyses"].push_back(analysisToJson(a));
    writeJson(outputPath, payload);
}

std::vector<fs::path> listImages(const fs::path& dir) {
    if (!fs::exists(dir) || !fs::is_directory(dir)) {
        throw std::invalid_argument("--batch requires a directory input.");
    }
    std::vector<fs::path> files;
    for (const auto& entry : fs::directory_iterator(dir)) {
        if (entry.is_regular_file() && supportedPath(entry.path())) {
            files.push_back(entry.path());
        }
    }
    return files;
}

int main(int argc, char** argv) {
    try {
        CliOptions options = parseArgs(argc, argv);
        fs::path outputJson = fs::absolute(options.kv.count("output") ? options.kv.at("output") : "histogram_analysis.json");

        if (options.positional.empty() && options.kv.count("batch") == 0) {
            runDemo(options, outputJson);
            return 0;
        }
        if (options.positional.empty()) {
            throw std::invalid_argument("Input path required.");
        }

        fs::path input = fs::absolute(options.positional.front());
        if (options.kv.count("batch")) {
            std::vector<Analysis> analyses;
            for (const auto& file : listImages(input)) {
                Analysis a = analyzeImage(file, options);
                analyses.push_back(a);
                printSummary(a);
            }
            printBatchTable(analyses);
            json payload;
            payload["mode"] = "batch";
            payload["analyses"] = json::array();
            for (const auto& a : analyses) payload["analyses"].push_back(analysisToJson(a));
            writeJson(outputJson, payload);
            return 0;
        }

        if (!fs::exists(input) || !fs::is_regular_file(input)) {
            throw std::invalid_argument("Input file not found: " + input.string());
        }

        Analysis analysis = analyzeImage(input, options);
        printSummary(analysis);
        json comparison = nullptr;
        if (options.kv.count("compare")) {
            Analysis other = analyzeImage(fs::absolute(options.kv.at("compare")), options);
            comparison = {
                {"target_image", other.image},
                {"red", compareHistograms(analysis.red, other.red)},
                {"green", compareHistograms(analysis.green, other.green)},
                {"blue", compareHistograms(analysis.blue, other.blue)},
                {"luminance", compareHistograms(analysis.luminance, other.luminance)}
            };
            std::cout << "Comparison metrics (luminance):\n" << comparison["luminance"].dump(2) << "\n\n";
        }

        if (options.kv.count("export")) {
            exportCsv(fs::absolute(options.kv.at("export")), analysis);
        }

        json payload;
        payload["mode"] = "single";
        payload["analysis"] = analysisToJson(analysis);
        payload["comparison"] = comparison;
        writeJson(outputJson, payload);
        return 0;
    } catch (const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << "\n";
        return 1;
    }
}