| | """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 |
| | arr[50:, :, 2] = 200 |
| | arr[:, :, 1] = 100 |
| | 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) |
| | |
| | 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 |
| | |
| | total_pct = sum(stats.zone_distribution.values()) |
| | assert 0.99 <= total_pct <= 1.01 |
| |
|
| | 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.""" |
| | |
| | arr = np.zeros((100, 100), dtype=np.uint8) |
| | arr[:50] = 0 |
| | 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) |
| |
|
| | |
| | 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() |
| |
|