"""Tests for linear-light blending in the per-LED reduction. Verifies the sRGB↔linear conversion correctness and that the edge-reduction kernel, when ``linear=True``, blends in linear light (a mid-grey from black + white is brighter than the gamma-space mean) — plus the CalibrationConfig round-trip of the opt-in flag. """ from __future__ import annotations import numpy as np from ledgrab.core.capture.calibration import calibration_from_dict, calibration_to_dict from ledgrab.core.capture.edge_interpolation import average_edge_to_leds from ledgrab.utils.linear_light import ( SRGB_TO_LINEAR_LUT, linear_to_srgb_uint8, srgb_to_linear, ) class TestConversions: def test_lut_endpoints_and_monotonic(self): assert SRGB_TO_LINEAR_LUT.shape == (256,) assert SRGB_TO_LINEAR_LUT[0] == 0.0 assert abs(SRGB_TO_LINEAR_LUT[255] - 1.0) < 1e-6 assert np.all(np.diff(SRGB_TO_LINEAR_LUT) > 0) # strictly increasing def test_mid_grey_decodes_below_half(self): # sRGB 0.5 (≈128) is ~0.214 in linear light, not 0.5. assert 0.18 < float(SRGB_TO_LINEAR_LUT[128]) < 0.25 def test_round_trip_is_near_identity(self): ramp = np.arange(256, dtype=np.uint8) back = linear_to_srgb_uint8(srgb_to_linear(ramp)) assert np.max(np.abs(back.astype(int) - ramp.astype(int))) <= 1 def test_linear_mean_of_black_and_white_is_brighter(self): lin = (srgb_to_linear(np.array([0, 255], dtype=np.uint8))).mean() encoded = int(linear_to_srgb_uint8(np.array([lin], dtype=np.float32))[0]) assert encoded > 127 # brighter than the sRGB mean (127) assert 180 < encoded < 195 # ~188 class TestEdgeReductionLinear: def _edge(self): # top edge (axis=0): shape (rows=2, width=4, 3); two black + two white cols e = np.zeros((2, 4, 3), dtype=np.uint8) e[:, 2:, :] = 255 return e def test_srgb_blend_is_plain_mean(self): out = average_edge_to_leds(self._edge(), "top", 1, {}, "k", linear=False) assert int(out[0, 0]) == 127 def test_linear_blend_is_brighter(self): out = average_edge_to_leds(self._edge(), "top", 1, {}, "k", linear=True) assert int(out[0, 0]) > 127 assert 180 < int(out[0, 0]) < 195 def test_uniform_edge_unchanged_by_linear(self): e = np.full((2, 4, 3), 200, dtype=np.uint8) out = average_edge_to_leds(e, "top", 1, {}, "k", linear=True) # A flat colour survives the decode→mean→encode round-trip (±1). assert abs(int(out[0, 0]) - 200) <= 1 class TestCalibrationRoundTrip: def test_linear_blend_round_trips_simple(self): cfg = calibration_from_dict( { "mode": "simple", "layout": "clockwise", "start_position": "bottom_left", "leds_top": 5, "linear_blend": True, } ) assert cfg.linear_blend is True assert calibration_to_dict(cfg).get("linear_blend") is True def test_default_is_off_and_omitted(self): cfg = calibration_from_dict( { "mode": "simple", "layout": "clockwise", "start_position": "bottom_left", "leds_top": 5, } ) assert cfg.linear_blend is False assert "linear_blend" not in calibration_to_dict(cfg)