← All tasks
pythonclaude-code/python-t2 #21Lite task

Image Histogram Analyzer (python, written by Claude Code)

envgap__claude-code__python-t2-21

Written by a coding agent; not on GitHubWritten 2026-02-27

01 / FAILURE SIGNATURE

Captured in a clean container

ImportError: libGL.so.1: cannot open shared object file: No such file or directory

02 / ENVIRONMENT RECIPE

Base commit
47c08db32f1be3dc08fc3582707baa5cfdbd4fc4
Manifest
requirements.txt
Reproduce
true
Run under trace
rc=0; out=$(timeout 60 python3 histogram.py < /dev/null 2>&1 | { head -c 1000000; cat > /dev/null; }; exit ${PIPESTATUS[0]}) || rc=$?; printf '%s\n' "$out"; env_error='(ModuleNotFoundError|ImportError|No module named|cannot open shared object file|DLL load failed|shared library|cannot load library|Library not loaded|Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_REQUIRE_ESM|compiled against a different Node|Could not find or load main class|ClassNotFoundException|NoClassDefFoundError|UnsupportedClassVersionError|UnsatisfiedLinkError|NoSuchMethodError|NoSuchFieldError|AbstractMethodError|IncompatibleClassChangeError|IllegalAccessError|ServiceConfigurationError|error while loading shared libraries|symbol lookup error|version `[^'"'"']*'"'"' not found|command not found)'; asked='(^| )[[:blank:]]*usage:|the following arguments are required|missing (required )?(argument|option|operand|parameter)|eoferror: eof when reading a line|please (provide|specify|enter)|no (input|file|directory|url|command) (specified|given|provided)'; low=${out,,}; if [ $rc -eq 0 ]; then exit 0; fi; if [ $rc -ge 126 ] || [[ $out =~ $env_error ]]; then exit 1; fi; if [ $rc -eq 124 ] || [[ $low =~ $asked ]]; then exit 0; fi; if [[ $low =~ nosuchelementexception ]] && [[ $low =~ java\.util\.scanner ]]; then exit 0; fi; exit 1
Reference environment fix used for admission
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+# System packages this project needs on a clean Ubuntu machine.
+set -e
+export DEBIAN_FRONTEND=noninteractive
+apt-get update -qq
+apt-get install -y -qq --no-install-recommends libgl1-mesa-glx libglib2.0-0

03 / TASK AND FAILURE

claude-code/python-t2 #21 · read the task the agent was given
Claude Code wrote this python 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 Python project for a clean Ubuntu 22.04 machine with only Python 3.10+ installed. Include:
- Source code
- requirements.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 checked by running the task · needs human review

underspecification
Label rules and the text that matched
[
  {
    "category": "underspecification",
    "rule": "signature.missing_system_requirement",
    "source": "failure_signature",
    "excerpt": "ImportError: libGL.so.1: cannot open shared object file: No such file or directory"
  },
  {
    "category": "underspecification",
    "rule": "diff.adds_external_environment_requirement",
    "source": "manifest_diff:setup.sh",
    "excerpt": "export DEBIAN_FRONTEND=noninteractive"
  },
  {
    "category": "underspecification",
    "rule": "diff.adds_external_environment_requirement",
    "source": "manifest_diff:setup.sh",
    "excerpt": "apt-get install -y -qq --no-install-recommends libgl1-mesa-glx libglib2.0-0"
  }
]

Written by Claude Code (study run M1T2P21L1). It failed as written and was repaired by changing only its environment.

Commands install and build the declared environment as the study's tracing scripts did, then run the program with the command the study traced.

Preparation dates registries as the oracle does: PyPI index files filtered by upload_time <= the registry date, and yanks applied only when dated at or before it; alternate indexes, direct URLs, apt and arbitrary setup downloads are not network-enforced.

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

histogram.py
"""
Image Histogram Analyzer
Computes per-channel color histograms, detects exposure issues,
and compares histograms between images.

Dependencies: opencv-python 4.9.0.80, numpy 1.26.4
"""

import sys
import argparse
import numpy as np
import cv2
from pathlib import Path


def compute_histogram(image_path: str) -> dict:
    """
    Compute per-channel color histograms for an image.

    Args:
        image_path: Path to the image file.

    Returns:
        Dictionary with channel names as keys and histogram arrays as values.
    """
    img = cv2.imread(image_path, cv2.IMREAD_COLOR)
    if img is None:
        raise ValueError(f"Failed to read image: {image_path}")

    # OpenCV loads BGR
    channels = cv2.split(img)
    histograms = {}

    channel_names = ["Blue", "Green", "Red"]
    for i, name in enumerate(channel_names):
        hist = cv2.calcHist([channels[i]], [0], None, [256], [0, 256])
        histograms[name] = hist.flatten()

    # Reorder to R, G, B
    histograms = {
        "Red": histograms["Red"],
        "Green": histograms["Green"],
        "Blue": histograms["Blue"],
    }

    # Compute luminance histogram
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    lum_hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
    histograms["Luminance"] = lum_hist.flatten()

    return histograms


def compute_statistics(histograms: dict) -> dict:
    """
    Compute statistical measures for each channel histogram.

    Args:
        histograms: Dictionary of channel histograms.

    Returns:
        Dictionary of statistics per channel.
    """
    stats = {}
    for channel, hist in histograms.items():
        total_pixels = hist.sum()
        values = np.arange(256, dtype=np.float64)
        mean = np.average(values, weights=hist)
        variance = np.average((values - mean) ** 2, weights=hist)
        std_dev = np.sqrt(variance)

        # Find median
        cumulative = np.cumsum(hist)
        median_idx = np.searchsorted(cumulative, total_pixels / 2)

        # Find mode
        mode = np.argmax(hist)

        stats[channel] = {
            "mean": float(mean),
            "median": int(median_idx),
            "mode": int(mode),
            "std_dev": float(std_dev),
            "total_pixels": int(total_pixels),
        }

    return stats


def detect_exposure_issues(histograms: dict, threshold: float = 0.25) -> dict:
    """
    Detect exposure issues based on histogram distribution.

    Analyzes the luminance histogram to determine if an image is
    overexposed, underexposed, or has low contrast.

    Args:
        histograms: Dictionary of channel histograms.
        threshold: Fraction of pixels threshold for detecting issues.

    Returns:
        Dictionary describing detected exposure issues.
    """
    lum_hist = histograms.get("Luminance")
    if lum_hist is None:
        return {"error": "No luminance histogram available"}

    total_pixels = lum_hist.sum()
    if total_pixels == 0:
        return {"error": "Empty histogram"}

    # Analyze distribution regions
    shadow_pixels = lum_hist[:64].sum() / total_pixels
    midtone_pixels = lum_hist[64:192].sum() / total_pixels
    highlight_pixels = lum_hist[192:].sum() / total_pixels

    # Check for clipping
    shadow_clipping = lum_hist[:5].sum() / total_pixels
    highlight_clipping = lum_hist[251:].sum() / total_pixels

    issues = []
    severity = "normal"

    if shadow_pixels > threshold and highlight_pixels < 0.1:
        issues.append("Image appears underexposed (heavy shadows)")
        severity = "underexposed"

    if highlight_pixels > threshold and shadow_pixels < 0.1:
        issues.append("Image appears overexposed (heavy highlights)")
        severity = "overexposed"

    if shadow_clipping > 0.05:
        issues.append(
            f"Shadow clipping detected ({shadow_clipping * 100:.1f}% of pixels)"
        )

    if highlight_clipping > 0.05:
        issues.append(
            f"Highlight clipping detected ({highlight_clipping * 100:.1f}% of pixels)"
        )

    # Check contrast
    values = np.arange(256, dtype=np.float64)
    mean = np.average(values, weights=lum_hist)
    variance = np.average((values - mean) ** 2, weights=lum_hist)
    std_dev = np.sqrt(variance)

    if std_dev < 30:
        issues.append(f"Low contrast detected (std dev: {std_dev:.1f})")
        if severity == "normal":
            severity = "low_contrast"

    if std_dev > 80:
        issues.append(f"High contrast detected (std dev: {std_dev:.1f})")
        if severity == "normal":
            severity = "high_contrast"

    if not issues:
        issues.append("No exposure issues detected")

    return {
        "severity": severity,
        "issues": issues,
        "shadow_fraction": float(shadow_pixels),
        "midtone_fraction": float(midtone_pixels),
        "highlight_fraction": float(highlight_pixels),
        "shadow_clipping": float(shadow_clipping),
        "highlight_clipping": float(highlight_clipping),
    }


def compare_histograms(hist1: dict, hist2: dict) -> dict:
    """
    Compare histograms between two images using multiple metrics.

    Args:
        hist1: Histograms of the first image.
        hist2: Histograms of the second image.

    Returns:
        Dictionary of comparison metrics per channel.
    """
    comparison = {}

    for channel in hist1:
        if channel not in hist2:
            continue

        h1 = hist1[channel].astype(np.float64)
        h2 = hist2[channel].astype(np.float64)

        # Normalize histograms
        h1_norm = h1 / (h1.sum() + 1e-10)
        h2_norm = h2 / (h2.sum() + 1e-10)

        # OpenCV histogram comparison methods
        h1_cv = h1_norm.astype(np.float32)
        h2_cv = h2_norm.astype(np.float32)

        correlation = float(cv2.compareHist(h1_cv, h2_cv, cv2.HISTCMP_CORREL))
        chi_squared = float(cv2.compareHist(h1_cv, h2_cv, cv2.HISTCMP_CHISQR))
        bhattacharyya = float(cv2.compareHist(h1_cv, h2_cv, cv2.HISTCMP_BHATTACHARYYA))
        intersection = float(cv2.compareHist(h1_cv, h2_cv, cv2.HISTCMP_INTERSECT))

        # Earth Mover's Distance (1D Wasserstein)
        cdf1 = np.cumsum(h1_norm)
        cdf2 = np.cumsum(h2_norm)
        emd = float(np.sum(np.abs(cdf1 - cdf2)))

        comparison[channel] = {
            "correlation": correlation,
            "chi_squared": chi_squared,
            "bhattacharyya": bhattacharyya,
            "earth_movers_distance": emd,
            "intersection": intersection,
        }

    return comparison


def plot_histogram(
    histograms: dict, title: str = "Image Histogram", output_path: str = None
):
    """
    Plot per-channel histograms using OpenCV drawing and save as image.

    Args:
        histograms: Dictionary of channel histograms.
        title: Title for the plot.
        output_path: If provided, save the plot to this path.
    """
    hist_w, hist_h = 800, 600
    hist_image = np.ones((hist_h, hist_w, 3), dtype=np.uint8) * 255

    colors = {
        "Red": (0, 0, 255),
        "Green": (0, 180, 0),
        "Blue": (255, 0, 0),
        "Luminance": (128, 128, 128),
    }

    # Normalize all histograms for display
    max_val = max(h.max() for h in histograms.values())
    draw_height = hist_h - 80

    for channel, hist in histograms.items():
        normalized = (hist / (max_val + 1e-10) * draw_height).astype(np.int32)
        color = colors.get(channel, (0, 0, 0))
        bin_w = hist_w / 256.0

        for i in range(1, 256):
            pt1 = (int((i - 1) * bin_w), hist_h - 40 - int(normalized[i - 1]))
            pt2 = (int(i * bin_w), hist_h - 40 - int(normalized[i]))
            cv2.line(hist_image, pt1, pt2, color, 1)

    # Add title
    cv2.putText(hist_image, title, (10, 25),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1)

    # Add legend
    legend_x = hist_w - 120
    for i, (channel, color) in enumerate(colors.items()):
        y = 20 + i * 20
        cv2.line(hist_image, (legend_x, y), (legend_x + 20, y), color, 2)
        cv2.putText(hist_image, channel, (legend_x + 25, y + 5),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 0), 1)

    if output_path:
        cv2.imwrite(output_path, hist_image)
        print(f"Histogram plot saved to: {output_path}")
    else:
        cv2.imshow(title, hist_image)
        cv2.waitKey(0)
        cv2.destroyAllWindows()


def print_report(image_path: str, stats: dict, exposure: dict):
    """Print a formatted analysis report for an image."""
    print(f"\n{'=' * 60}")
    print(f"Image Histogram Analysis Report")
    print(f"File: {image_path}")
    print(f"{'=' * 60}")

    print(f"\nChannel Statistics:")
    print(f"{'Channel':<12} {'Mean':>8} {'Median':>8} {'Mode':>8} {'StdDev':>8}")
    print(f"{'-' * 48}")

    for channel, s in stats.items():
        print(
            f"{channel:<12} {s['mean']:>8.1f} {s['median']:>8d} "
            f"{s['mode']:>8d} {s['std_dev']:>8.1f}"
        )

    print(f"\nExposure Analysis:")
    print(f"  Severity: {exposure['severity']}")
    for issue in exposure["issues"]:
        print(f"  - {issue}")
    print(
        f"  Shadows:    {exposure['shadow_fraction'] * 100:.1f}% "
        f"(clipping: {exposure['shadow_clipping'] * 100:.1f}%)"
    )
    print(
        f"  Midtones:   {exposure['midtone_fraction'] * 100:.1f}%"
    )
    print(
        f"  Highlights: {exposure['highlight_fraction'] * 100:.1f}% "
        f"(clipping: {exposure['highlight_clipping'] * 100:.1f}%)"
    )
    print()


def main():
    parser = argparse.ArgumentParser(
        description="Image Histogram Analyzer - Compute and compare color histograms"
    )
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # Analyze command
    analyze_parser = subparsers.add_parser(
        "analyze", help="Analyze histogram of a single image"
    )
    analyze_parser.add_argument("image", help="Path to the image file")
    analyze_parser.add_argument(
        "--plot", "-p", metavar="OUTPUT", help="Save histogram plot to file"
    )
    analyze_parser.add_argument(
        "--threshold",
        type=float,
        default=0.25,
        help="Exposure detection threshold (default: 0.25)",
    )

    # Compare command
    compare_parser = subparsers.add_parser(
        "compare", help="Compare histograms of two images"
    )
    compare_parser.add_argument("image1", help="Path to the first image")
    compare_parser.add_argument("image2", help="Path to the second image")
    compare_parser.add_argument(
        "--plot", "-p", metavar="OUTPUT", help="Save comparison plot to file"
    )

    args = parser.parse_args()

    if args.command is None:
        parser.print_help()
        sys.exit(1)

    if args.command == "analyze":
        if not Path(args.image).exists():
            print(f"Error: File not found: {args.image}")
            sys.exit(1)

        histograms = compute_histogram(args.image)
        stats = compute_statistics(histograms)
        exposure = detect_exposure_issues(histograms, threshold=args.threshold)

        print_report(args.image, stats, exposure)

        if args.plot:
            plot_histogram(
                histograms,
                title=f"Histogram: {Path(args.image).name}",
                output_path=args.plot,
            )

    elif args.command == "compare":
        for img_path in [args.image1, args.image2]:
            if not Path(img_path).exists():
                print(f"Error: File not found: {img_path}")
                sys.exit(1)

        hist1 = compute_histogram(args.image1)
        hist2 = compute_histogram(args.image2)

        stats1 = compute_statistics(hist1)
        stats2 = compute_statistics(hist2)
        exposure1 = detect_exposure_issues(hist1)
        exposure2 = detect_exposure_issues(hist2)

        print_report(args.image1, stats1, exposure1)
        print_report(args.image2, stats2, exposure2)

        comparison = compare_histograms(hist1, hist2)

        print(f"\n{'=' * 60}")
        print("Histogram Comparison Metrics")
        print(f"{'=' * 60}")

        for channel, metrics in comparison.items():
            print(f"\n  {channel}:")
            print(f"    Correlation:           {metrics['correlation']:.4f}")
            print(f"    Chi-Squared Distance:  {metrics['chi_squared']:.4f}")
            print(f"    Bhattacharyya Dist:    {metrics['bhattacharyya']:.4f}")
            print(f"    Earth Mover's Dist:    {metrics['earth_movers_distance']:.4f}")
            print(f"    Intersection:          {metrics['intersection']:.4f}")

        if args.plot:
            plot_histogram(
                hist1,
                title="Histogram Comparison",
                output_path=args.plot,
            )


if __name__ == "__main__":
    main()
README.md
# Image Histogram Analyzer (Python - OpenCV)

Computes per-channel color histograms, detects exposure issues, and compares histograms between images.

## Dependencies
- opencv-python 4.9.0.80
- numpy 1.26.4

## Installation
```bash
pip install -r requirements.txt
```

## Usage
```bash
python histogram.py analyze <image> [--plot output.png]
python histogram.py compare <image1> <image2> [--plot output.png]
```
requirements.txt
opencv-python==4.9.0.80
numpy==1.26.4