diff --git a/server/mapcontrol_server/services/geotiff_service.py b/server/mapcontrol_server/services/geotiff_service.py index 1bad514..a5760ae 100644 --- a/server/mapcontrol_server/services/geotiff_service.py +++ b/server/mapcontrol_server/services/geotiff_service.py @@ -335,9 +335,21 @@ def normalize_band(arr, m): normalized[m] = 0.0 return np.clip(normalized, 0, 255).astype(np.uint8) - r_u8 = normalize_band(r, mask) - g_u8 = normalize_band(g, mask) - b_u8 = normalize_band(b_arr, mask) + if all(ds.dtypes[b - 1] == "uint8" for b in bands): + # 8-bit RGB is already display-ready (a true-colour product, or a + # writer that applied its own fixed stretch). Re-stretching it per + # band lets the scene set the colours: an ocean-dominated image has + # its noise amplified and its colour balance shifted. + def as_is(arr, m): + out = arr.copy() + out[m] = 0.0 + return out.astype(np.uint8) + + r_u8, g_u8, b_u8 = as_is(r, mask), as_is(g, mask), as_is(b_arr, mask) + else: + r_u8 = normalize_band(r, mask) + g_u8 = normalize_band(g, mask) + b_u8 = normalize_band(b_arr, mask) # Alpha channel: fully opaque for valid pixels, transparent for nodata/NaN # (actual translucency is handled by OpenLayers layer opacity, not baked into PNG) diff --git a/server/tests/test_geotiff_masking.py b/server/tests/test_geotiff_masking.py index 238a315..d92fb06 100644 --- a/server/tests/test_geotiff_masking.py +++ b/server/tests/test_geotiff_masking.py @@ -93,3 +93,22 @@ def test_singleband_honors_mask_and_nodata(tmp_path): assert (alpha[3, :] == 0).all() assert alpha[1, 1] == 0 # value 5 = nodata tag assert alpha[0, 0] == 255 and alpha[2, 3] == 255 + + +def test_uint8_rgb_is_drawn_as_is_without_restretching(tmp_path): + data = np.full((3, 4, 4), 12, dtype=np.uint8) # dark ocean, display-scaled + data[:, 0, 0] = (180, 150, 90) # one bright land pixel + data[:, 3, 3] = 0 # no-data + rgba, ds = _process_rgb(_write(tmp_path / "a.tif", data, nodata=0), [1, 2, 3], 1.0, None) + ds.close() + assert tuple(rgba[1, 1, :3]) == (12, 12, 12) # not stretched up to mid-grey + assert tuple(rgba[0, 0, :3]) == (180, 150, 90) + assert rgba[3, 3, 3] == 0 and rgba[1, 1, 3] == 255 + + +def test_non_uint8_rgb_is_still_stretched(tmp_path): + data = np.full((3, 4, 4), 1200, dtype=np.uint16) + data[:, 0, 0] = 3000 + rgba, ds = _process_rgb(_write(tmp_path / "a.tif", data), [1, 2, 3], 1.0, None) + ds.close() + assert rgba[0, 0, 0] == 255 and rgba[1, 1, 0] == 0