File size: 7,240 Bytes
d18d4d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""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()