ptpd-calibration-studio / tests /unit /test_histogram_analyzer.py
Claude
feat: Add comprehensive feature set for Pt/Pd printing workflow
d18d4d2 unverified
"""Tests for histogram analysis module."""
import numpy as np
import pytest
from PIL import Image
from ptpd_calibration.imaging import (
HistogramAnalyzer,
HistogramResult,
HistogramStats,
HistogramScale,
)
@pytest.fixture
def analyzer():
"""Create histogram analyzer."""
return HistogramAnalyzer()
@pytest.fixture
def gray_gradient_image():
"""Create a grayscale gradient image."""
arr = np.linspace(0, 255, 256).reshape(1, 256).repeat(100, axis=0).astype(np.uint8)
return Image.fromarray(arr, mode="L")
@pytest.fixture
def rgb_test_image():
"""Create a test RGB image."""
arr = np.zeros((100, 100, 3), dtype=np.uint8)
arr[:50, :, 0] = 200 # Red top half
arr[50:, :, 2] = 200 # Blue bottom half
arr[:, :, 1] = 100 # Green everywhere
return Image.fromarray(arr, mode="RGB")
@pytest.fixture
def dark_image():
"""Create a dark image."""
arr = np.full((100, 100), 30, dtype=np.uint8)
return Image.fromarray(arr, mode="L")
@pytest.fixture
def bright_image():
"""Create a bright image."""
arr = np.full((100, 100), 220, dtype=np.uint8)
return Image.fromarray(arr, mode="L")
class TestHistogramAnalyzer:
"""Test histogram analyzer functionality."""
def test_analyze_grayscale_image(self, analyzer, gray_gradient_image):
"""Test analyzing a grayscale gradient."""
result = analyzer.analyze(gray_gradient_image)
assert isinstance(result, HistogramResult)
assert result.histogram is not None
assert len(result.histogram) == 256
assert result.image_mode == "L"
assert result.total_pixels == 256 * 100
def test_analyze_rgb_image(self, analyzer, rgb_test_image):
"""Test analyzing an RGB image."""
result = analyzer.analyze(rgb_test_image, include_rgb=True)
assert result.red_histogram is not None
assert result.green_histogram is not None
assert result.blue_histogram is not None
assert len(result.red_histogram) == 256
def test_analyze_numpy_array(self, analyzer):
"""Test analyzing from numpy array."""
arr = np.random.randint(0, 256, (100, 100), dtype=np.uint8)
result = analyzer.analyze(arr)
assert result.image_size == (100, 100)
assert result.total_pixels == 10000
def test_histogram_stats_basic(self, analyzer, gray_gradient_image):
"""Test basic statistics computation."""
result = analyzer.analyze(gray_gradient_image)
stats = result.stats
assert isinstance(stats, HistogramStats)
# Gradient should have mean around 127
assert 100 < stats.mean < 160
assert stats.min_value == 0
assert stats.max_value == 255
def test_histogram_stats_dark_image(self, analyzer, dark_image):
"""Test statistics for dark image."""
result = analyzer.analyze(dark_image)
stats = result.stats
assert stats.brightness < 0.2
assert stats.mean < 50
assert "dark" in " ".join(stats.notes).lower() or len(stats.notes) >= 0
def test_histogram_stats_bright_image(self, analyzer, bright_image):
"""Test statistics for bright image."""
result = analyzer.analyze(bright_image)
stats = result.stats
assert stats.brightness > 0.8
assert stats.mean > 200
def test_zone_distribution(self, analyzer, gray_gradient_image):
"""Test zone distribution calculation."""
result = analyzer.analyze(gray_gradient_image)
stats = result.stats
assert len(stats.zone_distribution) == 11
# Gradient should have some pixels in each zone
total_pct = sum(stats.zone_distribution.values())
assert 0.99 <= total_pct <= 1.01 # Should sum to ~100%
def test_clipping_detection_no_clipping(self, analyzer):
"""Test clipping detection with no clipping."""
arr = np.full((100, 100), 128, dtype=np.uint8)
result = analyzer.analyze(arr)
assert result.stats.shadow_clipping_percent < 1
assert result.stats.highlight_clipping_percent < 1
def test_clipping_detection_with_clipping(self, analyzer):
"""Test clipping detection with clipping."""
# Create image with heavy shadows
arr = np.zeros((100, 100), dtype=np.uint8)
arr[:50] = 0 # Half pure black
arr[50:] = 128
result = analyzer.analyze(arr)
assert result.stats.shadow_clipping_percent > 40
def test_dynamic_range(self, analyzer, gray_gradient_image):
"""Test dynamic range calculation."""
result = analyzer.analyze(gray_gradient_image)
# Gradient should have high dynamic range
assert result.stats.dynamic_range > 5
def test_compare_histograms(self, analyzer, gray_gradient_image, dark_image):
"""Test histogram comparison."""
comparison = analyzer.compare_histograms(gray_gradient_image, dark_image)
assert "similarity" in comparison
assert "changes" in comparison
assert "histogram_intersection" in comparison["similarity"]
assert "mean_shift" in comparison["changes"]
def test_create_histogram_plot(self, analyzer, gray_gradient_image):
"""Test histogram plot creation."""
result = analyzer.analyze(gray_gradient_image)
fig = analyzer.create_histogram_plot(result)
assert fig is not None
def test_create_histogram_plot_logarithmic(self, analyzer, gray_gradient_image):
"""Test logarithmic histogram plot."""
result = analyzer.analyze(gray_gradient_image)
fig = analyzer.create_histogram_plot(result, scale=HistogramScale.LOGARITHMIC)
assert fig is not None
def test_create_histogram_plot_with_rgb(self, analyzer, rgb_test_image):
"""Test histogram plot with RGB channels."""
result = analyzer.analyze(rgb_test_image, include_rgb=True)
fig = analyzer.create_histogram_plot(result, show_rgb=True)
assert fig is not None
def test_create_histogram_plot_with_zones(self, analyzer, gray_gradient_image):
"""Test histogram plot with zone boundaries."""
result = analyzer.analyze(gray_gradient_image)
fig = analyzer.create_histogram_plot(result, show_zones=True)
assert fig is not None
def test_to_dict(self, analyzer, gray_gradient_image):
"""Test result conversion to dictionary."""
result = analyzer.analyze(gray_gradient_image)
d = result.to_dict()
assert "image_size" in d
assert "statistics" in d
assert "total_pixels" in d
def test_stats_to_dict(self, analyzer, gray_gradient_image):
"""Test stats conversion to dictionary."""
result = analyzer.analyze(gray_gradient_image)
d = result.stats.to_dict()
assert "mean" in d
assert "median" in d
assert "zone_distribution" in d
def test_zone_descriptions(self):
"""Test zone descriptions retrieval."""
descs = HistogramAnalyzer.get_zone_descriptions()
assert len(descs) == 11
assert 0 in descs
assert 10 in descs
assert "black" in descs[0].lower()
assert "white" in descs[10].lower()