From c924d9db7834e0bb3017faf11e31f0796ed8b0a3 Mon Sep 17 00:00:00 2001 From: Gerrit Holl Date: Mon, 5 Aug 2024 17:17:43 +0200 Subject: [PATCH 1/4] Include gradient/axisintercept for mode p NinJo wants that gradient=1 and axisintercept=0 for images of mode p. It shall be so. --- satpy/tests/writer_tests/test_ninjogeotiff.py | 8 ++++---- satpy/writers/ninjogeotiff.py | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/satpy/tests/writer_tests/test_ninjogeotiff.py b/satpy/tests/writer_tests/test_ninjogeotiff.py index e05150a571..38f6a83ff3 100644 --- a/satpy/tests/writer_tests/test_ninjogeotiff.py +++ b/satpy/tests/writer_tests/test_ninjogeotiff.py @@ -577,8 +577,8 @@ def test_write_and_read_file_P(test_image_small_arctic_P, tmp_path): test_image_small_arctic_P, filename=fn, fill_value=255, - PhysicUnit="N/A", - PhysicValue="N/A", + PhysicUnit="satdata", + PhysicValue="satdata", SatelliteNameID=6400014, ChannelID=900015, DataType="PPRN", @@ -591,8 +591,8 @@ def test_write_and_read_file_P(test_image_small_arctic_P, tmp_path): tgs = src.tags() assert tgs["ninjo_FileName"] == fn assert tgs["ninjo_DataSource"] == "dowsing rod" - assert "ninjo_Gradient" not in tgs - assert "ninjo_AxisIntercept" not in tgs + assert tgs["ninjo_Gradient"] == "1.0" + assert tgs["ninjo_AxisIntercept"] == "0.0" def test_write_and_read_file_units( diff --git a/satpy/writers/ninjogeotiff.py b/satpy/writers/ninjogeotiff.py index 5f88cc52ed..cf7174c51c 100644 --- a/satpy/writers/ninjogeotiff.py +++ b/satpy/writers/ninjogeotiff.py @@ -74,11 +74,13 @@ NinJo has a functionality to read the corresponding quantity (example: brightness temperature or reflectance). To make this possible, the writer adds the tags ``Gradient`` and ``AxisIntercept``. Those tags are added if -and only if the image has mode ``L`` or ``LA`` and ``PhysicUnit`` is not set +and only if the image has mode ``L``, ``P``, or ``LA`` and ``PhysicUnit`` is not set to ``"N/A"``. In other words, to suppress those tags for images with mode ``L`` or ``LA`` (for example, for the composite ``vis_with_ir``, where the physical interpretation of individual pixels is lost), one should set ``PhysicUnit`` to ``"N/A"``, ``"n/a"``, ``"1"``, or ``""`` (empty string). +If the image has mode ``P``, ``Gradient`` is set to ``1.0`` and ``AxisIntercept`` +to ``0.0`` (as expected by NinJo). """ import copy @@ -236,7 +238,7 @@ def _fix_units(self, image, quantity, unit): def _check_include_scale_offset(self, image, unit): """Check if scale-offset tags should be included.""" - if image.mode.startswith("L") and unit.lower() not in ("n/a", "1", ""): + if image.mode[0] in "LP" and unit.lower() not in ("n/a", "1", ""): return True return False From 3181a80dd74f0f0dbd2801f690355b601be4c622 Mon Sep 17 00:00:00 2001 From: Gerrit Holl Date: Thu, 8 Aug 2024 16:14:29 +0200 Subject: [PATCH 2/4] Simulate palettized-p-mode in test In the ninjogeotiff writer, when testing p-mode images, add an enhancement history such as palettize would add, to get a more realistic test. --- satpy/tests/writer_tests/test_ninjogeotiff.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/satpy/tests/writer_tests/test_ninjogeotiff.py b/satpy/tests/writer_tests/test_ninjogeotiff.py index 38f6a83ff3..e937a682b5 100644 --- a/satpy/tests/writer_tests/test_ninjogeotiff.py +++ b/satpy/tests/writer_tests/test_ninjogeotiff.py @@ -20,6 +20,7 @@ import datetime import logging import os +from unittest.mock import Mock import dask.array as da import numpy as np @@ -247,6 +248,12 @@ def test_image_small_arctic_P(test_area_tiny_stereographic_wgs84): "start_time": datetime.datetime(2027, 8, 2, 8, 20), "area": test_area_tiny_stereographic_wgs84, "mode": "P"}) + # simulate an enhancement history such as palettize may add + arr.attrs["enhancement_history"] = [ + {"scale": np.float64(0.01), + "offset": np.float64(0.0), + "colormap": Mock()}] + return to_image(arr) From cb4f64639fd06b9507425dba8e8a9f8047fa9100 Mon Sep 17 00:00:00 2001 From: Gerrit Holl Date: Thu, 8 Aug 2024 16:31:40 +0200 Subject: [PATCH 3/4] Force scale/offse to 1/0 for ninjogeotiff mode p For ninjogeotiff mode p, force scale/offset to be 1 and 0, respectively. As a workaround for https://github.com/pytroll/satpy/issues/2300, do not check image.mode, but use image.data.attrs["mode"] instead. --- satpy/writers/ninjogeotiff.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/satpy/writers/ninjogeotiff.py b/satpy/writers/ninjogeotiff.py index cf7174c51c..2aadb0a367 100644 --- a/satpy/writers/ninjogeotiff.py +++ b/satpy/writers/ninjogeotiff.py @@ -206,11 +206,18 @@ def save_image( # noqa: D417 overviews_minsize=overviews_minsize, overviews_resampling=overviews_resampling, tags={**(tags or {}), **ninjo_tags}, - scale_offset_tags=(self.scale_offset_tag_names - if self._check_include_scale_offset(image, PhysicUnit) - else None), + scale_offset_tags=self._get_scale_offset_tags(image, PhysicUnit), **gdal_opts) + def _get_scale_offset_tags(self, image, unit): + """Get scale offset tags (tuple or dict).""" + if self._check_include_scale_offset(image, unit): + # image.mode cannot be trusted https://github.com/pytroll/satpy/issues/2300 + if image.data.attrs["mode"][0] == "P": + return dict(zip(self.scale_offset_tag_names, (1, 0))) + return self.scale_offset_tag_names + return None # explicit is better than implicit + def _fix_units(self, image, quantity, unit): """Adapt units between °C and K. @@ -238,7 +245,7 @@ def _fix_units(self, image, quantity, unit): def _check_include_scale_offset(self, image, unit): """Check if scale-offset tags should be included.""" - if image.mode[0] in "LP" and unit.lower() not in ("n/a", "1", ""): + if image.data.attrs["mode"][0] in "LP" and unit.lower() not in ("n/a", "1", ""): return True return False @@ -380,16 +387,17 @@ def get_central_meridian(self): def get_color_depth(self): """Return the color depth.""" - if self.image.mode in ("L", "P"): + # image.mode cannot be trusted https://github.com/pytroll/satpy/issues/2300 + if self.image.data.attrs["mode"] in ("L", "P"): return 8 - if self.image.mode in ("LA", "PA"): + if self.image.data.attrs["mode"] in ("LA", "PA"): return 16 - if self.image.mode == "RGB": + if self.image.data.attrs["mode"] == "RGB": return 24 - if self.image.mode == "RGBA": + if self.image.data.attrs["mode"] == "RGBA": return 32 raise ValueError( - f"Unsupported image mode: {self.image.mode:s}") + f"Unsupported image mode: {self.image.data.attrs['mode']:s}") # Set unix epoch here explicitly, because datetime.timestamp() is # apparently not supported on Windows. From 2440dc0a9ca253aa7e3468ac22473b0c86449461 Mon Sep 17 00:00:00 2001 From: Gerrit Holl Date: Thu, 8 Aug 2024 18:01:23 +0200 Subject: [PATCH 4/4] Use data attribute fallback conditionally only The fallback/workaround to use image.data.attrs["mode"] instead of image.mode can and should only be used after to_image has been called. Before to_image has been called, the former is unavailable and the latter is reliable, I think. --- satpy/writers/ninjogeotiff.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/satpy/writers/ninjogeotiff.py b/satpy/writers/ninjogeotiff.py index 2aadb0a367..1d5cfb69ac 100644 --- a/satpy/writers/ninjogeotiff.py +++ b/satpy/writers/ninjogeotiff.py @@ -213,7 +213,11 @@ def _get_scale_offset_tags(self, image, unit): """Get scale offset tags (tuple or dict).""" if self._check_include_scale_offset(image, unit): # image.mode cannot be trusted https://github.com/pytroll/satpy/issues/2300 - if image.data.attrs["mode"][0] == "P": + try: + mod = image.data.attrs["mode"] + except KeyError: + mod = image.mode + if mod == "P": return dict(zip(self.scale_offset_tag_names, (1, 0))) return self.scale_offset_tag_names return None # explicit is better than implicit @@ -245,7 +249,7 @@ def _fix_units(self, image, quantity, unit): def _check_include_scale_offset(self, image, unit): """Check if scale-offset tags should be included.""" - if image.data.attrs["mode"][0] in "LP" and unit.lower() not in ("n/a", "1", ""): + if image.mode[0] in "LP" and unit.lower() not in ("n/a", "1", ""): return True return False @@ -387,17 +391,16 @@ def get_central_meridian(self): def get_color_depth(self): """Return the color depth.""" - # image.mode cannot be trusted https://github.com/pytroll/satpy/issues/2300 - if self.image.data.attrs["mode"] in ("L", "P"): + if self.image.mode in ("L", "P"): return 8 - if self.image.data.attrs["mode"] in ("LA", "PA"): + if self.image.mode in ("LA", "PA"): return 16 - if self.image.data.attrs["mode"] == "RGB": + if self.image.mode == "RGB": return 24 - if self.image.data.attrs["mode"] == "RGBA": + if self.image.mode == "RGBA": return 32 raise ValueError( - f"Unsupported image mode: {self.image.data.attrs['mode']:s}") + f"Unsupported image mode: {self.image.mode:s}") # Set unix epoch here explicitly, because datetime.timestamp() is # apparently not supported on Windows.