diff --git a/continuous_integration/environment.yaml b/continuous_integration/environment.yaml index 6479ce6b53..54c9df57ad 100644 --- a/continuous_integration/environment.yaml +++ b/continuous_integration/environment.yaml @@ -60,6 +60,6 @@ dependencies: - pip: - pytest-lazy-fixtures - trollsift - - trollimage>=1.23 + - trollimage>=1.24 - pyspectral - pyorbital diff --git a/doc/source/enhancements.rst b/doc/source/enhancements.rst index 9623c1b120..ce4e80c3b9 100644 --- a/doc/source/enhancements.rst +++ b/doc/source/enhancements.rst @@ -100,6 +100,15 @@ the example here:: - {colors: spectral, min_value: 193.15, max_value: 253.149999} - {colors: greys, min_value: 253.15, max_value: 303.15} +In addition, it is also possible to add a linear alpha channel to the colormap, as in the +following example:: + + - name: colorize + method: !!python/name:satpy.enhancements.colorize + kwargs: + palettes: + - {colors: ylorrd, min_alpha: 100, max_alpha: 255} + It is also possible to provide your own custom defined color mapping by specifying a list of RGB values and the corresponding min and max values between which to apply the colors. This is for instance a common use case for diff --git a/pyproject.toml b/pyproject.toml index 673405cfb1..fb80bf7a1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "pyproj>=2.2", "pyresample>=1.24.0", "pyyaml>=5.1", - "trollimage>=1.23", + "trollimage>=1.24", "trollsift", "xarray>=0.14.1", "zarr", diff --git a/satpy/composites/glm.py b/satpy/composites/glm.py index e1c9b676c6..3c13d23311 100644 --- a/satpy/composites/glm.py +++ b/satpy/composites/glm.py @@ -50,10 +50,10 @@ def __init__(self, name, min_highlight=0.0, max_highlight=10.0, # noqa: D417 Args: min_highlight (float): Minimum raw value of the "highlight" data that will be used for linearly scaling the data along with - ``max_hightlight``. + ``max_highlight``. max_highlight (float): Maximum raw value of the "highlight" data that will be used for linearly scaling the data along with - ``min_hightlight``. + ``min_highlight``. max_factor (tuple): Maximum effect that the highlight data can have on each channel of the primary image data. This will be multiplied by the linearly scaled highlight data and then @@ -61,8 +61,8 @@ def __init__(self, name, min_highlight=0.0, max_highlight=10.0, # noqa: D417 docstring for more information. By default this is set to ``(0.8, 0.8, -0.8, 0)`` meaning the Red and Green channel will be added to by at most 0.8, the Blue channel will be - subtracted from by at most 0.8, and the Alpha channel will - not be effected. + subtracted from by at most 0.8 (resulting in yellow highlights), + and the Alpha channel will not be affected. """ self.min_highlight = min_highlight diff --git a/satpy/enhancements/__init__.py b/satpy/enhancements/__init__.py index 00a0f8dd4e..a44ca590cf 100644 --- a/satpy/enhancements/__init__.py +++ b/satpy/enhancements/__init__.py @@ -409,7 +409,7 @@ def create_colormap(palette, img=None): # noqa: D417 Colormaps can be loaded from lists of colors provided by the ``colors`` key in the provided dictionary. Each element in the list represents a single color to be mapped to and can be 3 (RGB) or 4 (RGBA) elements long. - By default the value or control point for a color is determined by the + By default, the value or control point for a color is determined by the index in the list (0, 1, 2, ...) divided by the total number of colors to produce a number between 0 and 1. This can be overridden by providing a ``values`` key in the provided dictionary. See the "Set Range" section @@ -455,12 +455,37 @@ def create_colormap(palette, img=None): # noqa: D417 ``max_value``. See :meth:`trollimage.colormap.Colormap.set_range` for more information. + **Set Alpha Range** + + The alpha channel of a created colormap can be added and/or modified by + specifying ``min_alpha`` and ``max_alpha``. + See :meth:`trollimage.colormap.Colormap.set_alpha_range` for more info. + """ + # are colors between 0-255 or 0-1 + color_scale = palette.get("color_scale", 255) + cmap = _get_cmap_from_palette_info(palette, img, color_scale) + + if palette.get("reverse", False): + cmap.reverse() + if "min_value" in palette and "max_value" in palette: + cmap.set_range(palette["min_value"], palette["max_value"]) + elif "min_value" in palette or "max_value" in palette: + raise ValueError("Both 'min_value' and 'max_value' must be specified (or neither).") + + if "min_alpha" in palette and "max_alpha" in palette: + cmap.set_alpha_range(palette["min_alpha"] / color_scale, + palette["max_alpha"] / color_scale) + elif "min_alpha" in palette or "max_alpha" in palette: + raise ValueError("Both 'min_alpha' and 'max_alpha' must be specified (or neither).") + + return cmap + + +def _get_cmap_from_palette_info(palette, img, color_scale): fname = palette.get("filename", None) colors = palette.get("colors", None) dataset = palette.get("dataset", None) - # are colors between 0-255 or 0-1 - color_scale = palette.get("color_scale", 255) if fname: if not os.path.exists(fname): fname = get_config_path(fname) @@ -473,17 +498,10 @@ def create_colormap(palette, img=None): # noqa: D417 cmap = _create_colormap_from_dataset(img, dataset, color_scale) else: raise ValueError("Unknown colormap format: {}".format(palette)) - - if palette.get("reverse", False): - cmap.reverse() - if "min_value" in palette and "max_value" in palette: - cmap.set_range(palette["min_value"], palette["max_value"]) - elif "min_value" in palette or "max_value" in palette: - raise ValueError("Both 'min_value' and 'max_value' must be specified (or neither)") - return cmap + def _create_colormap_from_dataset(img, dataset, color_scale): """Create a colormap from an auxiliary variable in a source file.""" match = find_in_ancillary(img.data, dataset) diff --git a/satpy/tests/enhancement_tests/test_enhancements.py b/satpy/tests/enhancement_tests/test_enhancements.py index ca0d56f11f..747d6fc0cd 100644 --- a/satpy/tests/enhancement_tests/test_enhancements.py +++ b/satpy/tests/enhancement_tests/test_enhancements.py @@ -331,11 +331,12 @@ def test_cmap_from_file(self, color_scale, colormap_mode, extra_kwargs, filename kwargs1["color_scale"] = color_scale cmap = create_colormap(kwargs1) - assert cmap.colors.shape[0] == 4 - np.testing.assert_equal(cmap.colors[0], first_color) - assert cmap.values.shape[0] == 4 - assert cmap.values[0] == unset_first_value - assert cmap.values[-1] == unset_last_value + + assert cmap.colors.shape[0] == 4 + np.testing.assert_equal(cmap.colors[0], first_color) + assert cmap.values.shape[0] == 4 + assert cmap.values[0] == unset_first_value + assert cmap.values[-1] == unset_last_value def test_cmap_vrgb_as_rgba(self): """Test that data created as VRGB still reads as RGBA.""" @@ -343,12 +344,44 @@ def test_cmap_vrgb_as_rgba(self): cmap_data = _generate_cmap_test_data(None, "VRGB") np.save(cmap_filename, cmap_data) cmap = create_colormap({"filename": cmap_filename, "colormap_mode": "RGBA"}) - assert cmap.colors.shape[0] == 4 - assert cmap.colors.shape[1] == 4 # RGBA - np.testing.assert_equal(cmap.colors[0], [128 / 255., 1.0, 0, 0]) - assert cmap.values.shape[0] == 4 - assert cmap.values[0] == 0 - assert cmap.values[-1] == 1.0 + + assert cmap.colors.shape[0] == 4 + assert cmap.colors.shape[1] == 4 # RGBA + np.testing.assert_equal(cmap.colors[0], [128 / 255., 1.0, 0, 0]) + assert cmap.values.shape[0] == 4 + assert cmap.values[0] == 0 + assert cmap.values[-1] == 1.0 + + def test_cmap_with_alpha_set(self): + """Test that the min_alpha and max_alpha arguments set the alpha channel correctly.""" + with closed_named_temp_file(suffix=".npy") as cmap_filename: + cmap_data = _generate_cmap_test_data(None, "RGB") + np.save(cmap_filename, cmap_data) + cmap = create_colormap({"filename": cmap_filename, "min_alpha": 100, "max_alpha": 255}) + + assert cmap.colors.shape[0] == 4 + assert cmap.colors.shape[1] == 4 # RGBA + # check that we start from min_alpha + np.testing.assert_equal(cmap.colors[0], [1.0, 0, 0, 100/255.]) + # two thirds of the linear scale + np.testing.assert_almost_equal(cmap.colors[2], [1., 1., 1., (100+(2/3)*(255-100))/255]) + # check that we end at max_alpha + np.testing.assert_equal(cmap.colors[3], [0, 0, 1., 1.0]) + # check that values have not been changed + assert cmap.values.shape[0] == 4 + assert cmap.values[0] == 0 + assert cmap.values[-1] == 1.0 + + @pytest.mark.parametrize("alpha_arg", ["max_alpha", "min_alpha"]) + def test_cmap_error_with_only_one_alpha_set(self, alpha_arg): + """Test that when only min_alpha or max_alpha arguments are set an error is raised.""" + with closed_named_temp_file(suffix=".npy") as cmap_filename: + cmap_data = _generate_cmap_test_data(None, "RGB") + np.save(cmap_filename, cmap_data) + + # check that if a value is missing we raise a ValueError + with pytest.raises(ValueError, match="Both 'min_alpha' and 'max_alpha' must be specified*."): + create_colormap({"filename": cmap_filename, alpha_arg: 255}) @pytest.mark.parametrize( ("real_mode", "forced_mode"), @@ -397,12 +430,13 @@ def test_cmap_from_config_path(self, tmp_path): with satpy.config.set(config_path=[tmp_path]): rel_cmap_filename = os.path.join("colormaps", "my_colormap.npy") cmap = create_colormap({"filename": rel_cmap_filename, "colormap_mode": "RGBA"}) - assert cmap.colors.shape[0] == 4 - assert cmap.colors.shape[1] == 4 # RGBA - np.testing.assert_equal(cmap.colors[0], [128 / 255., 1.0, 0, 0]) - assert cmap.values.shape[0] == 4 - assert cmap.values[0] == 0 - assert cmap.values[-1] == 1.0 + + assert cmap.colors.shape[0] == 4 + assert cmap.colors.shape[1] == 4 # RGBA + np.testing.assert_equal(cmap.colors[0], [128 / 255., 1.0, 0, 0]) + assert cmap.values.shape[0] == 4 + assert cmap.values[0] == 0 + assert cmap.values[-1] == 1.0 def test_cmap_from_trollimage(self): """Test that colormaps in trollimage can be loaded."""