[Git][debian-gis-team/trollimage][upstream] New upstream version 1.20.0

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Mon Jan 16 06:49:47 GMT 2023



Antonio Valentino pushed to branch upstream at Debian GIS Project / trollimage


Commits:
d747012f by Antonio Valentino at 2023-01-14T08:33:13+00:00
New upstream version 1.20.0
- - - - -


13 changed files:

- + .github/dependabot.yml
- .github/workflows/ci.yaml
- .github/workflows/deploy-sdist.yaml
- + .readthedocs.yml
- CHANGELOG.md
- doc/conf.py
- rtd_requirements.txt
- setup.cfg
- trollimage/colormap.py
- trollimage/image.py
- trollimage/tests/test_colormap.py
- trollimage/tests/test_image.py
- trollimage/version.py


Changes:

=====================================
.github/dependabot.yml
=====================================
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "github-actions" # See documentation for possible values
+    directory: "/" # Location of package manifests
+    schedule:
+      interval: "weekly"


=====================================
.github/workflows/ci.yaml
=====================================
@@ -25,7 +25,7 @@ jobs:
 
     steps:
       - name: Checkout source
-        uses: actions/checkout at v2
+        uses: actions/checkout at v3
 
       - name: Setup Conda Environment
         uses: conda-incubator/setup-miniconda at v2
@@ -67,7 +67,7 @@ jobs:
           pytest --cov=trollimage trollimage/tests --cov-report=xml
 
       - name: Upload unittest coverage to Codecov
-        uses: codecov/codecov-action at v1
+        uses: codecov/codecov-action at v3
         with:
           flags: unittests
           file: ./coverage.xml


=====================================
.github/workflows/deploy-sdist.yaml
=====================================
@@ -11,7 +11,7 @@ jobs:
 
     steps:
       - name: Checkout source
-        uses: actions/checkout at v2
+        uses: actions/checkout at v3
 
       - name: Create sdist
         shell: bash -l {0}
@@ -19,7 +19,7 @@ jobs:
 
       - name: Publish package to PyPI
         if: github.event.action == 'published'
-        uses: pypa/gh-action-pypi-publish at v1.4.1
+        uses: pypa/gh-action-pypi-publish at v1.6.4
         with:
           user: __token__
           password: ${{ secrets.pypi_password }}
\ No newline at end of file


=====================================
.readthedocs.yml
=====================================
@@ -0,0 +1,13 @@
+version: 2
+
+build:
+  os: "ubuntu-20.04"
+  tools:
+    python: "3.10"
+sphinx:
+  configuration: doc/conf.py
+python:
+  install:
+    - requirements: rtd_requirements.txt
+    - method: pip
+      path: .


=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,23 @@
+## Version 1.20.0 (2023/01/11)
+
+
+### Pull Requests Merged
+
+#### Bugs fixed
+
+* [PR 118](https://github.com/pytroll/trollimage/pull/118) - Fix image colorization ([26](https://github.com/pytroll/pydecorate/issues/26))
+
+#### Features added
+
+* [PR 117](https://github.com/pytroll/trollimage/pull/117) - Refactor colormap creation ([2308](https://github.com/pytroll/satpy/issues/2308))
+
+#### Documentation changes
+
+* [PR 110](https://github.com/pytroll/trollimage/pull/110) - Add readthedocs config file to force newer dependencies
+
+In this release 3 pull requests were closed.
+
+
 ## Version 1.19.0 (2022/10/21)
 
 ### Issues Closed


=====================================
doc/conf.py
=====================================
@@ -121,7 +121,7 @@ pygments_style = 'sphinx'
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'default'
+html_theme = 'sphinx_rtd_theme'
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the


=====================================
rtd_requirements.txt
=====================================
@@ -3,3 +3,6 @@ rasterio
 xarray
 dask[array]
 docutils<0.18
+sphinx>=5,<6
+sphinx-rtd-theme>=1.1.0,<2.0
+


=====================================
setup.cfg
=====================================
@@ -7,6 +7,8 @@ universal=1
 
 [flake8]
 max-line-length = 120
+exclude = 
+    doc/conf.py
 
 [versioneer]
 VCS = git


=====================================
trollimage/colormap.py
=====================================
@@ -22,11 +22,14 @@
 """A simple colormap module."""
 
 import contextlib
+import copy
 import os
 from io import StringIO
 from typing import Optional
 import warnings
 from numbers import Number
+import pathlib
+import sys
 
 import numpy as np
 from trollimage.colorspaces import rgb2hcl, hcl2rgb
@@ -399,15 +402,14 @@ class Colormap(object):
     @classmethod
     def from_file(
             cls,
-            filename_or_string: str,
+            filename: str,
             colormap_mode: Optional[str] = None,
             color_scale: Number = 255,
     ):
         """Create Colormap from a comma-separated or binary file of colormap data.
 
         Args:
-            filename_or_string: Filename of a binary or CSV file or a
-                string version of the comma-separate data.
+            filename: Filename of a binary or CSV file
             colormap_mode: Force the scheme of the colormap data (ex. RGBA).
                 See information below on other possible values and how they
                 are interpreted. By default this is determined based on the
@@ -425,8 +427,7 @@ class Colormap(object):
         :func:`numpy.load`. All other extensions are
         read as a comma-separated file. For ``.npz`` files the data must be stored
         as a positional list where the first element represents the colormap to
-        use. See :func:`numpy.savez` for more information. The filename should
-        be an absolute path for consistency.
+        use. See :func:`numpy.savez` for more information.
 
         The colormap is interpreted as 1 of 4 different "colormap modes":
         ``RGB``, ``RGBA``, ``VRGB``, or ``VRGBA``. The
@@ -445,6 +446,13 @@ class Colormap(object):
         See the "Color Scale" section below for more information on the value
         range of provided numbers.
 
+        To read from a string containing CSV, use :meth:`~Colormap.from_csv`.
+
+        To get a named colormap, use :meth:`~Colormap.from_name` or load the
+        colormap directly as a module attribute.
+
+        To get a colormap from an ndarray, use :meth:`~Colormap.from_ndarray`.
+
         **Color Scale**
 
         By default colors are expected to be in a 0-255 range. This
@@ -452,16 +460,250 @@ class Colormap(object):
         A common alternative to 255 is ``1`` to specify floating
         point numbers between 0 and 1. The resulting Colormap uses the normalized
         color values (0-1).
+        """
+        if _is_actually_a_csv_string(filename):
+            warnings.warn(
+                "Passing a data string to Colormap.from_file is deprecated. "
+                "Please use Colormap.from_string.",
+                category=DeprecationWarning)
+            return cls.from_string(filename, colormap_mode, color_scale)
+        values, colors = _get_values_colors_from_file(filename, colormap_mode, color_scale)
+        return cls(values=values, colors=colors)
+
+    @classmethod
+    def from_string(cls, string, *args, **kwargs):
+        """Create colormap from string.
+
+        Create a colormap from a string that contains comma seperated values
+        (CSV).
+
+        To read from an external file that contains CSV, use :meth:`from_csv`.
+
+        Args:
+            string (str): String containing CSV.  Must have no less than three
+                and no more than five columns and describe entirely numeric
+                data.
+            colormap_mode (str or None): Optional. Can be None, "RGB", "RGBA", "VRGB", or
+                "VRGBA".  If None (default), this is inferred from the dimensions of
+                the data contained in the CSV.  Modes starting with V have in
+                the first column the values to which the color relates.
+            color_scale (number): The value that represents white in the
+                numbers describing the colors. Defaults to 255, could also be 1
+                or something else.
+        """
+        openfile = StringIO(string)
+        cmap_data = _read_colormap_data_from_file(openfile)
+        return cls.from_ndarray(cmap_data, *args, **kwargs)
+
+    @classmethod
+    def from_np(cls, path, *args, **kwargs):
+        """Create Colormap from a numpy-file.
+
+        Create a colormap from a numpy data file ``.npy`` or ``.npz``.
+
+        The data should contain at least three and at most five columns.
 
+        Args:
+            path (str or Pathlib.Path): Path to file containing numpy data.
+            colormap_mode (str or None): Optional. Can be None, "RGB", "RGBA", "VRGB", or
+                "VRGBA".  If None (default), this is inferred from the dimensions of
+                the data contained in the CSV.  Modes starting with V have in
+                the first column the values to which the color relates.
+            color_scale (number): The value that represents white in the
+                numbers describing the colors. Defaults to 255, could also be 1
+                or something else.
+        """
+        cmap_data = _read_colormap_data_from_np(path)
+        return cls.from_ndarray(cmap_data, *args, **kwargs)
+
+    @classmethod
+    def from_csv(cls, path, colormap_mode=None, color_scale=255):
+        """Create Colormap from CSV file.
+
+        Create a Colormap from a file that contains comma seperated values
+        (CSV).
+
+        To read from a string that contains CSV, use :meth:`from_string`.
+
+        Args:
+            string (str or pathlib.Path): Path to file containing CSV.
+                The CSV must have at least three and at most five columns and
+                describe entirely numeric data.
+            colormap_mode (str or None): Optional. Can be None, "RGB", "RGBA", "VRGB", or
+                "VRGBA".  If None (default), this is inferred from the dimensions of
+                the data contained in the CSV.  Modes starting with V have in
+                the first column the values to which the color relates.
+            color_scale (number): The value that represents white in the
+                numbers describing the colors. Defaults to 255, could also be 1
+                or something else.
+        """
+        cmap_data = np.loadtxt(path, delimiter=",")
+        return cls.from_ndarray(cmap_data, colormap_mode, color_scale)
+
+    @classmethod
+    def from_ndarray(cls, cmap_data, colormap_mode=None, color_scale=255):
+        """Create Colormap from ndarray.
+
+        Create a colormap from a numpy data array.
+
+        The data should contain at least three and at most five columns.
+
+        For historical reasons, this method exists alongside
+        :meth:`from_sequence_of_colors` and :meth:`from_array_with_metadata` despite similar
+        functionality.
+
+        Args:
+            cmap_data (ndarray): Array describing the colours.
+                Must have at least three and at most five columns and
+                have a numeric dtype.
+            colormap_mode (str or None): Optional. Can be None, "RGB", "RGBA", "VRGB", or
+                "VRGBA".  If None (default), this is inferred from the dimensions of
+                the data contained in the CSV.  Modes starting with V have in
+                the first column the values to which the color relates.
+            color_scale (number): The value that represents white in the
+                numbers describing the colors. Defaults to 255, could also be 1
+                or something else.
         """
-        if not os.path.isfile(filename_or_string):
-            filename_or_string = StringIO(filename_or_string)
-        values, colors = _get_values_colors_from_file(filename_or_string, colormap_mode, color_scale)
+        values, colors = _get_values_colors_from_ndarray(cmap_data, colormap_mode, color_scale)
         return cls(values=values, colors=colors)
 
+    @classmethod
+    def from_name(cls, name):
+        """Return named colormap.
+
+        Return a colormap by name.  Supported colormaps are the ones defined in
+        the module namespace.
+
+        Args:
+            name (str): Name of colormap.
+        """
+        cmap = getattr(sys.modules[__name__], name)
+        return copy.copy(cmap)
+
+    @classmethod
+    def from_sequence_of_colors(cls, colors, values=None, color_scale=255):
+        """Create Colormap from sequence of colors.
+
+        Create a colormap from a sequence of colors, such as a list of colors.
+        If values is not given, assume values between 0 and 1, linearly spaced
+        according to the total number of colors.
+
+        For historical reasons, this method exists alongside
+        :meth:`from_ndarray` and :meth:`from_array_with_metadata` despite similar
+        functionality.
+
+        Args:
+            colors (Sequence): List of colors, where each element must itself
+                be a sequence of 3 or 4 numbers (RGB or RGBA).
+            values (array, optional): Values associated with the colors.  If
+                not given, assume linear between 0 and 1.
+            color_scale (number): The value that represents white in the
+                numbers describing the colors. Defaults to 255, could also be 1
+                or something else.
+            """
+        # this method was moved from satpy. where it was in
+        # satpy.enhancements.create_colormap
+        # then it was refactored/rewritten
+        color_array = np.array(colors)
+        if values is None:
+            values = np.linspace(0, 1, len(colors))
+        else:
+            values = np.asarray(values)
+        color_array = np.concatenate((values[:, np.newaxis], color_array), axis=1)
+        return cls.from_ndarray(
+            color_array,
+            "VRGB" if color_array.shape[1] == 4 else "VRGBA",
+            color_scale=color_scale)
+
+    @classmethod
+    def from_array_with_metadata(
+            cls, palette, dtype, color_scale=255,
+            valid_range=None, scale_factor=1, add_offset=0,
+            remove_last=True):
+        """Create Colormap from an array with metadata.
+
+        Create a colormap from an array with associated metadata, either in
+        attributes or passed on to the function.
+
+        For historical reasons, this method exists alongside
+        :meth:`from_ndarray` and :meth:`from_sequence_of_colors` despite similar
+        functionality.
+
+        If ``palette`` is an xarray dataarray with the attribute
+        ``palette_meanings``, those meanings are interpreted as values
+        associated with the colormap.
+
+        If no values can be interpreted from the metadata, values will be
+        linearly interpolated between 0 and 255 (if ``dtype`` is ``np.uint8``)
+        or according to ``valid_range``.
+
+        Args:
+
+            palette (ndarray or xarray.DataArray)
+                Array describing colors, possibly with metadata.  If it has a
+                ``palette_meanings`` attribute, this will be used for color
+                interpretation.
+            dtype
+                dtype for the colormap
+            color_scale (number): The value that represents white in the
+                numbers describing the colors. Defaults to 255, could also be 1
+                or something else.
+            valid_range
+                valid range for colors, if colormap is not of dtype uint8
+            scale_factor
+                scale factor to apply to the colormap
+            add_offset
+                add offset to apply to the colormap
+            remove_last
+                Remove the last value if the array has no metadata associated.
+                Defaults to true for historical reasons.
+
+        """
+        # this method was moved from satpy, where it was in
+        # satpy.composites.ColormapCompositor.build_colormap
+        #
+        # then it was refactored/rewritten for trollimage
+        squeezed_palette = np.asanyarray(palette).squeeze()
+        set_range = True
+        if hasattr(palette, 'attrs') and 'palette_meanings' in palette.attrs:
+            set_range = False
+            values = np.asarray(palette.attrs['palette_meanings'])
+        else:
+            # remove the last value because monkeys don't like water sprays
+            # on a more serious note, I don't know why we are removing the last
+            # value here, but this behaviour was copied from ancient satpy code
+            values = np.arange(squeezed_palette.shape[0]-remove_last)
+            if remove_last:
+                squeezed_palette = squeezed_palette[:-remove_last, :]
+
+        color_array = np.concatenate((values[:, np.newaxis], squeezed_palette), axis=1)
+        colormap = cls.from_ndarray(
+            color_array,
+            "VRGB" if color_array.shape[1] == 4 else "VRGBA",
+            color_scale=color_scale)
+        if valid_range is not None:
+            if set_range:
+                colormap.set_range(
+                    *(np.array(valid_range) * scale_factor
+                      + add_offset))
+        else:
+            if dtype != np.dtype("uint8"):
+                raise AttributeError("Data need to have either a valid_range or be of type uint8" +
+                                     " in order to be displayable with an attached color-palette!")
+        return colormap
+
+
+def _is_actually_a_csv_string(string):
+    """Try to guess whether this string contains CSV."""
+    return string.count("\n") > 0 and string.count(",") > 0
+
 
 def _get_values_colors_from_file(filename, colormap_mode, color_scale):
     data = _read_colormap_data_from_file(filename)
+    return _get_values_colors_from_ndarray(data, colormap_mode, color_scale)
+
+
+def _get_values_colors_from_ndarray(data, colormap_mode, color_scale):
     cols = data.shape[1]
     default_modes = {
         3: 'RGB',
@@ -492,16 +734,21 @@ def _read_colormap_data_from_file(filename_or_file_obj):
     if isinstance(filename_or_file_obj, str):
         ext = os.path.splitext(filename_or_file_obj)[1]
         if ext in (".npy", ".npz"):
-            file_content = np.load(filename_or_file_obj)
-            if ext == ".npz":
-                # .npz is a collection
-                # assume position list-like and get the first element
-                file_content = file_content["arr_0"]
-            return file_content
+            return _read_colormap_data_from_np(filename_or_file_obj)
     # CSV file or file-like object of CSV string data
     return np.loadtxt(filename_or_file_obj, delimiter=",")
 
 
+def _read_colormap_data_from_np(path):
+    path = pathlib.Path(path)
+    file_content = np.load(path)
+    if path.suffix == ".npz":
+        # .npz is a collection
+        # assume position list-like and get the first element
+        file_content = file_content["arr_0"]
+    return file_content
+
+
 # matlab jet "#00007F", "blue", "#007FFF", "cyan", "#7FFF7F", "yellow",
 # "#FF7F00", "red", "#7F0000"
 


=====================================
trollimage/image.py
=====================================
@@ -117,14 +117,14 @@ class Image(object):
 
         self._secondary_mode = "RGB"
 
-        if(channels is not None and
-           not isinstance(channels, (tuple, set, list,
-                                     np.ndarray, np.ma.core.MaskedArray))):
+        if (channels is not None and
+            not isinstance(channels, (tuple, set, list,
+                                      np.ndarray, np.ma.core.MaskedArray))):
             raise TypeError("Image channels should a tuple, set, list, numpy "
                             "array, or masked array.")
 
-        if(isinstance(channels, (tuple, list)) and
-           len(channels) != len(re.findall("[A-Z]", mode))):
+        if (isinstance(channels, (tuple, list)) and
+                len(channels) != len(re.findall("[A-Z]", mode))):
             errmsg = ("Number of channels (" +
                       "{n}) does not match mode {mode}.".format(
                           n=len(channels), mode=mode))
@@ -136,20 +136,20 @@ class Image(object):
         if mode not in self.modes:
             raise ValueError("Unknown mode.")
 
-        if(color_range is not None and
-           not _is_pair(color_range) and
-           not _is_list_of_pairs(color_range)):
+        if (color_range is not None and
+            not _is_pair(color_range) and
+                not _is_list_of_pairs(color_range)):
             raise ValueError("Color_range should be a pair"
                              " or a list/tuple/set of pairs.")
-        if(color_range is not None and
-           _is_list_of_pairs(color_range) and
-           (channels is None or
+        if (color_range is not None and
+            _is_list_of_pairs(color_range) and
+            (channels is None or
                 len(color_range) != len(channels))):
             raise ValueError("Color_range length does not match number of "
                              "channels.")
 
-        if(color_range is not None and
-           (((mode == "L" or mode == "P") and not _is_pair(color_range)) and
+        if (color_range is not None and
+            (((mode == "L" or mode == "P") and not _is_pair(color_range)) and
                 (len(color_range) != len(re.findall("[A-Z]", mode))))):
             raise ValueError("Color_range does not match mode")
 
@@ -256,8 +256,8 @@ class Image(object):
 
     def is_empty(self):
         """Check for an empty image."""
-        if(((self.channels == []) and (not self.shape == (0, 0))) or
-           ((not self.channels == []) and (self.shape == (0, 0)))):
+        if (((self.channels == []) and (not self.shape == (0, 0))) or
+                ((not self.channels == []) and (self.shape == (0, 0)))):
             raise RuntimeError("Channels-shape mismatch.")
         return self.channels == [] and self.shape == (0, 0)
 
@@ -425,9 +425,7 @@ class Image(object):
     def putalpha(self, alpha):
         """Add an *alpha* channel to the current image, or replaces it with *alpha* if it already exists."""
         alpha = np.ma.array(alpha)
-        if(not (alpha.shape[0] == 0 and
-                self.shape[0] == 0) and
-           alpha.shape != self.shape):
+        if (not (alpha.shape[0] == 0 and self.shape[0] == 0) and alpha.shape != self.shape):
             raise ValueError("Alpha channel shape should match image shape")
 
         if not self.mode.endswith("A"):
@@ -736,8 +734,7 @@ class Image(object):
         else:
             factor[1] = self.width * 1.0 / shape[1]
 
-        if(int(factor[0]) != factor[0] or
-           int(factor[1]) != factor[1]):
+        if (int(factor[0]) != factor[0] or int(factor[1]) != factor[1]):
             raise ValueError("Resize not of integer factor!")
 
         factor[0] = int(factor[0])
@@ -822,8 +819,7 @@ class Image(object):
         are several channels in the image. The behaviour of :func:`gamma` is
         undefined outside the normal [0,1] range of the channels.
         """
-        if(isinstance(gamma, (list, tuple, set)) and
-           len(gamma) != len(self.channels)):
+        if (isinstance(gamma, (list, tuple, set)) and len(gamma) != len(self.channels)):
             raise ValueError("Number of channels and gamma components differ.")
         if isinstance(gamma, (tuple, list)):
             gamma_list = list(gamma)
@@ -876,8 +872,7 @@ class Image(object):
         if self.mode.endswith("A"):
             ch_len -= 1
 
-        if((isinstance(stretch, tuple) or
-                isinstance(stretch, list))):
+        if ((isinstance(stretch, tuple) or isinstance(stretch, list))):
             if len(stretch) == 2:
                 for i in range(ch_len):
                     self.stretch_linear(i, cutoffs=stretch, **kwargs)
@@ -911,8 +906,7 @@ class Image(object):
 
         Note: 'Inverting' means that black becomes white, and vice-versa, not that the values are negated!
         """
-        if(isinstance(invert, (tuple, list)) and
-           len(self.channels) != len(invert)):
+        if (isinstance(invert, (tuple, list)) and len(self.channels) != len(invert)):
             raise ValueError(
                 "Number of channels and invert components differ.")
 
@@ -929,8 +923,7 @@ class Image(object):
         """Stretch the current image's colors by performing histogram equalization on channel *ch_nb*."""
         logger.info("Perform a histogram equalized contrast stretch.")
 
-        if(self.channels[ch_nb].size ==
-           np.ma.count_masked(self.channels[ch_nb])):
+        if (self.channels[ch_nb].size == np.ma.count_masked(self.channels[ch_nb])):
             logger.warning("Nothing to stretch !")
             return
 
@@ -978,9 +971,8 @@ class Image(object):
         """
         logger.debug("Perform a linear contrast stretch.")
 
-        if((self.channels[ch_nb].size ==
-            np.ma.count_masked(self.channels[ch_nb])) or
-           self.channels[ch_nb].min() == self.channels[ch_nb].max()):
+        if ((self.channels[ch_nb].size == np.ma.count_masked(self.channels[ch_nb])) or
+                self.channels[ch_nb].min() == self.channels[ch_nb].max()):
             logger.warning("Nothing to stretch !")
             return
 
@@ -1019,9 +1011,8 @@ class Image(object):
         if isinstance(max_stretch, (list, tuple)):
             max_stretch = max_stretch[ch_nb]
 
-        if((not self.channels[ch_nb].mask.all()) and
-                abs(max_stretch - min_stretch) > 0):
-            stretched = self.channels[ch_nb].data.astype(np.float)
+        if ((not self.channels[ch_nb].mask.all()) and abs(max_stretch - min_stretch) > 0):
+            stretched = self.channels[ch_nb].data.astype(float)
             stretched -= min_stretch
             stretched /= max_stretch - min_stretch
             self.channels[ch_nb] = np.ma.array(stretched,
@@ -1060,7 +1051,7 @@ class Image(object):
             alpha = self.channels[1]
         else:
             alpha = None
-        self.channels = colormap.colorize(self.channels[0])
+        self.channels = list(colormap.colorize(self.channels[0]))
         if alpha is not None:
             self.channels.append(alpha)
             self.mode = "RGBA"


=====================================
trollimage/tests/test_colormap.py
=====================================
@@ -27,6 +27,7 @@ import unittest
 from trollimage import colormap
 import numpy as np
 from tempfile import NamedTemporaryFile
+import xarray
 
 import pytest
 
@@ -628,6 +629,96 @@ class TestFromFileCreation:
             with pytest.raises(ValueError):
                 colormap.Colormap.from_file(cmap_filename)
 
+    def test_cmap_from_np(self, tmp_path):
+        """Test creating a colormap from a numpy file."""
+        cmap_data = _generate_cmap_test_data(None, "RGB")
+        fnp = tmp_path / "test.npy"
+        np.save(fnp, cmap_data)
+        cmap = colormap.Colormap.from_np(fnp, color_scale=1)
+        np.testing.assert_allclose(cmap.values, [0, 0.33333333, 0.6666667, 1])
+        np.testing.assert_array_equal(cmap.colors, cmap_data)
+
+    def test_cmap_from_csv(self, tmp_path, color_scale=1):
+        """Test creating a colormap from a CSV file."""
+        cmap_data = _generate_cmap_test_data(None, "RGB")
+        fnp = tmp_path / "test.csv"
+        np.savetxt(fnp, cmap_data, delimiter=",")
+        cmap = colormap.Colormap.from_csv(fnp, color_scale=1)
+        np.testing.assert_allclose(cmap.values, [0, 0.33333333, 0.66666667, 1])
+        np.testing.assert_array_equal(cmap.colors, cmap_data)
+
+
+def test_cmap_from_string():
+    """Test creating a colormap from a string."""
+    s = "0,0,0,0\n1,1,1,1\n2,2,2,2"
+    cmap = colormap.Colormap.from_string(s, color_scale=1)
+    np.testing.assert_array_equal(cmap.values, [0, 1, 2])
+    np.testing.assert_array_equal(cmap.colors, [[0, 0, 0], [1, 1, 1], [2, 2, 2]])
+
+
+def test_cmap_from_ndarray():
+    """Test creating a colormap from a numpy array."""
+    cmap_data = _generate_cmap_test_data(None, "RGB")
+    cmap = colormap.Colormap.from_ndarray(cmap_data, color_scale=1)
+    np.testing.assert_allclose(cmap.values, [0, 0.33333333, 0.66666667, 1])
+    np.testing.assert_array_equal(cmap.colors, cmap_data)
+
+
+def test_cmap_from_name():
+    """Test creating a colormap from a string representing a name."""
+    cmap = colormap.Colormap.from_name("puor")
+    np.testing.assert_array_equal(cmap.values, colormap.puor.values)
+    np.testing.assert_array_equal(cmap.colors, colormap.puor.colors)
+
+
+def test_cmap_from_sequence_of_colors():
+    """Test creating a colormap from a sequence of colors."""
+    colors = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]
+    cmap = colormap.Colormap.from_sequence_of_colors(colors, color_scale=2)
+    np.testing.assert_allclose(cmap.values, [0, 0.33333333, 0.66666667, 1])
+    np.testing.assert_array_equal(cmap.colors*2, colors)
+
+    vals = [0, 5, 10, 15]
+    cmap = colormap.Colormap.from_sequence_of_colors(colors, values=vals, color_scale=2)
+    np.testing.assert_allclose(cmap.values, [0, 5, 10, 15])
+
+
+def test_build_colormap_with_int_data_and_without_meanings():
+    """Test colormap building."""
+    palette = np.array([[0, 0, 0], [127, 127, 127], [255, 255, 255]])
+    cmap = colormap.Colormap.from_array_with_metadata(palette, np.uint8)
+    np.testing.assert_array_equal(cmap.values, [0, 1])
+
+    with pytest.raises(AttributeError):
+        colormap.Colormap.from_array_with_metadata(palette/100, np.float32)
+
+    cmap = colormap.Colormap.from_array_with_metadata(
+            palette,
+            np.float32,
+            valid_range=[0, 100],
+            scale_factor=2,
+            remove_last=True)
+
+    np.testing.assert_array_equal(cmap.values, [0, 200])
+
+    cmap = colormap.Colormap.from_array_with_metadata(
+            palette,
+            np.float32,
+            valid_range=[0, 100],
+            scale_factor=2,
+            remove_last=False)
+
+    np.testing.assert_array_equal(cmap.values, [0, 100, 200])
+
+
+def test_build_colormap_with_int_data_and_with_meanings():
+    """Test colormap building."""
+    palette = xarray.DataArray(np.array([[0, 0, 0], [127, 127, 127], [255, 255, 255]]),
+                               dims=['value', 'band'])
+    palette.attrs['palette_meanings'] = [2, 3, 4]
+    cmap = colormap.Colormap.from_array_with_metadata(palette, np.uint8)
+    np.testing.assert_array_equal(cmap.values, [2, 3, 4])
+
 
 def _assert_monotonic_values(cmap, increasing=True):
     delta = np.diff(cmap.values)


=====================================
trollimage/tests/test_image.py
=====================================
@@ -1385,7 +1385,7 @@ class TestXRImage:
                 img.save(tmp.name, tiled=True, overviews=[], driver='COG')
                 with rio.open(tmp.name) as f:
                     # The COG driver should add a tag indicating layout
-                    assert(f.tags(ns='IMAGE_STRUCTURE')['LAYOUT'] == 'COG')
+                    assert (f.tags(ns='IMAGE_STRUCTURE')['LAYOUT'] == 'COG')
                     assert len(f.overviews(1)) == 2
 
     @pytest.mark.skipif(sys.platform.startswith('win'), reason="'NamedTemporaryFile' not supported on Windows")
@@ -2004,6 +2004,22 @@ class TestXRImage:
             pil_img.convert.assert_called_with('RGB')
 
 
+class TestImageColorize:
+    """Test the colorize method of the Image class."""
+
+    def test_colorize_la_rgb(self):
+        """Test colorizing an LA image with an RGB colormap."""
+        arr = np.arange(75).reshape(5, 15) / 74.
+        alpha = arr > 40.
+        img = image.Image(channels=[arr.copy(), alpha], mode="LA")
+        img.colorize(brbg)
+
+        expected = list(TestXRImageColorize._expected)
+        for i in range(3):
+            np.testing.assert_allclose(img.channels[i], expected[i])
+        np.testing.assert_allclose(img.channels[3], alpha)
+
+
 class TestXRImageColorize:
     """Test the colorize method of the XRImage class."""
 


=====================================
trollimage/version.py
=====================================
@@ -23,9 +23,9 @@ def get_keywords():
     # setup.py/versioneer.py will grep for the variable names, so they must
     # each be defined on a line of their own. _version.py will just call
     # get_keywords().
-    git_refnames = " (HEAD -> main, tag: v1.19.0)"
-    git_full = "93e547a45a1c3a13768da4a5169a3f5bf5ebc75a"
-    git_date = "2022-10-21 09:50:00 -0500"
+    git_refnames = " (tag: v1.20.0)"
+    git_full = "d87d61cdc9f29998fb26af354fa7e6a897760103"
+    git_date = "2023-01-11 12:33:44 +0100"
     keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
     return keywords
 



View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/commit/d747012fb184c6086d3a7581bd1544ba96202490

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/commit/d747012fb184c6086d3a7581bd1544ba96202490
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20230116/225a0b9d/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list