[Git][debian-gis-team/trollimage][upstream] New upstream version 1.18.0
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Fri Feb 25 07:55:25 GMT 2022
Antonio Valentino pushed to branch upstream at Debian GIS Project / trollimage
Commits:
675756cc by Antonio Valentino at 2022-02-25T07:38:40+00:00
New upstream version 1.18.0
- - - - -
8 changed files:
- .github/workflows/ci.yaml
- − .travis.yml
- CHANGELOG.md
- trollimage/colormap.py
- trollimage/tests/test_colormap.py
- trollimage/tests/test_image.py
- trollimage/version.py
- trollimage/xrimage.py
Changes:
=====================================
.github/workflows/ci.yaml
=====================================
@@ -10,10 +10,10 @@ jobs:
fail-fast: true
matrix:
os: ["windows-latest", "ubuntu-latest", "macos-latest"]
- python-version: ["3.7", "3.8"]
+ python-version: ["3.8", "3.9", "3.10"]
experimental: [false]
include:
- - python-version: "3.8"
+ - python-version: "3.9"
os: "ubuntu-latest"
experimental: true
=====================================
.travis.yml deleted
=====================================
@@ -1,63 +0,0 @@
-language: python
-env:
- global:
- # Set defaults to avoid repeating in most cases
- - PYTHON_VERSION=$TRAVIS_PYTHON_VERSION
- - NUMPY_VERSION=stable
- - MAIN_CMD='python setup.py'
- - CONDA_DEPENDENCIES='pillow gdal xarray dask coverage coveralls codecov rasterio pytest pytest-cov'
- - SETUP_XVFB=False
- - EVENT_TYPE='push pull_request'
- - SETUP_CMD='test'
- - CONDA_CHANNELS='conda-forge'
- - CONDA_CHANNEL_PRIORITY=True
- - UNSTABLE_DEPS=False
-
-matrix:
- include:
- - env: PYTHON_VERSION=3.7
- os: linux
- - env: PYTHON_VERSION=3.8
- os: linux
- - env:
- - PYTHON_VERSION=3.8
- - UNSTABLE_DEPS=True
- os: linux
- allow_failures:
- - env:
- - PYTHON_VERSION=3.8
- - UNSTABLE_DEPS=True
- os: linux
-
-install:
- - git clone --depth 1 git://github.com/astropy/ci-helpers.git
- - source ci-helpers/travis/setup_conda.sh
- - if [ "$UNSTABLE_DEPS" == "True" ]; then
- python -m pip install
- -f https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com
- --no-deps --pre --upgrade
- numpy
- pandas;
- python -m pip install
- --no-deps --upgrade
- git+https://github.com/dask/dask
- git+https://github.com/mapbox/rasterio
- git+https://github.com/pydata/xarray;
- fi
- - pip install -e . --no-deps
-script:
- - pytest --cov=trollimage trollimage/tests
-#after_success:
-# - if [[ $PYTHON_VERSION == 3.8 ]]; then coveralls; fi
-#deploy:
-# - provider: pypi
-# user: dhoese
-# password:
-# secure: "Cpo7kPgjbyWKNRjnzWDggxo5dqG8KdtcrBxYWwBpkkhgly/ngN9EkjIdscrwmp8sCqfjTd0RGyR811k6dzU7kKJVbc6V309+4mG8O0w4IvfHCn+NaHymAMrHleRIqyxbo5kvrBZoX+eB7YWOUppF6ofeohbrNWWgMQv+/d+Mufs="
-# distributions: sdist bdist_wheel
-# skip_existing: true
-# on:
-# repo: pytroll/trollimage
-# tags: true
-#notifications:
-# slack: pytroll:96mNSYSI1dBjGyzVXkBT6qFt
=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,17 @@
+## Version 1.18.0 (2022/02/24)
+
+### Pull Requests Merged
+
+#### Features added
+
+* [PR 101](https://github.com/pytroll/trollimage/pull/101) - Add to_csv/from_csv to Colormap
+* [PR 100](https://github.com/pytroll/trollimage/pull/100) - Update Colormap.set_range to support flipped values
+* [PR 99](https://github.com/pytroll/trollimage/pull/99) - Add colorize and palettize to XRImage enhancement_history
+* [PR 98](https://github.com/pytroll/trollimage/pull/98) - Change tested Python versions to 3.8, 3.9 and 3.10
+
+In this release 4 pull requests were closed.
+
+
## Version 1.17.0 (2021/12/07)
### Issues Closed
=====================================
trollimage/colormap.py
=====================================
@@ -19,15 +19,28 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
"""A simple colormap module."""
+import contextlib
+import os
+from io import StringIO
+from typing import Optional
import warnings
+from numbers import Number
import numpy as np
from trollimage.colorspaces import rgb2hcl, hcl2rgb
+ at contextlib.contextmanager
+def _file_or_stringio(filename_or_none):
+ if filename_or_none is None:
+ yield StringIO()
+ else:
+ with open(filename_or_none, "w") as file_obj:
+ yield file_obj
+
+
def colorize(arr, colors, values):
"""Colorize a monochromatic array *arr*, based *colors* given for *values*.
@@ -71,10 +84,16 @@ def _colorize(arr, colors, values):
def _interpolate_rgb_colors(arr, colors, values):
+ interp_xp_coords = np.array(values)
hcl_colors = _convert_rgb_list_to_hcl(colors)
+ interp_y_coords = np.array(hcl_colors)
+ if values[0] > values[-1]:
+ # monotonically decreasing
+ interp_xp_coords = interp_xp_coords[::-1]
+ interp_y_coords = interp_y_coords[::-1]
channels = [np.interp(arr,
- np.array(values),
- np.array(hcl_colors)[:, i])
+ interp_xp_coords,
+ interp_y_coords[:, i])
for i in range(3)]
channels = list(hcl2rgb(*channels))
return channels
@@ -100,11 +119,7 @@ def _interpolate_alpha(arr, colors, values):
def _mask_channels(channels, arr):
"""Mask the channels if arr is a masked array."""
- try:
- channels = [_mask_array(channel, arr) for channel in channels]
- except AttributeError:
- pass
- return channels
+ return [_mask_array(channel, arr) for channel in channels]
def _mask_array(new_array, arr):
@@ -145,10 +160,17 @@ def _palettize(arr, values):
def _digitize_array(arr, values):
- new_arr = np.digitize(arr.ravel(),
- np.concatenate((values,
- [max(np.nanmax(arr),
- values.max()) + 1])))
+ if values[0] <= values[-1]:
+ # monotonic increasing values
+ outside_range_bin = max(np.nanmax(arr), values.max()) + 1
+ right = False
+ else:
+ # monotonic decreasing values
+ outside_range_bin = min(np.nanmin(arr), values.min()) - 1
+ right = True
+ bins = np.concatenate((values, [outside_range_bin]))
+
+ new_arr = np.digitize(arr.ravel(), bins, right=right)
new_arr -= 1
new_arr = new_arr.clip(min=0, max=len(values) - 1)
return new_arr
@@ -163,7 +185,7 @@ class Colormap(object):
aren't provided.
values: One dimensional array-like of control points where
each corresponding color is applied. Must be the same number of
- elements as colors and must be monotonically increasing.
+ elements as colors and must be monotonic.
colors: Two dimensional array-like of RGB or RGBA colors where each
color is applied to a specific control point. Must be the same
number of colors as control points (values). Colors should be
@@ -256,15 +278,22 @@ class Colormap(object):
"""Append colormap together."""
old, other = self._normalize_color_arrays(self, other)
values = np.concatenate((old.values, other.values))
- if not (np.diff(values) >= 0).all():
+ if not self._monotonic_one_direction(values):
raise ValueError("Merged colormap 'values' are not monotonically "
- "increasing or equal.")
+ "increasing, monotonically decreasing, or equal.")
colors = np.concatenate((old.colors, other.colors))
return Colormap(
values=values,
colors=colors,
)
+ @staticmethod
+ def _monotonic_one_direction(values):
+ delta = np.diff(values)
+ all_increasing = (delta >= 0).all()
+ all_decreasing = (delta <= 0).all()
+ return all_increasing or all_decreasing
+
@staticmethod
def _normalize_color_arrays(cmap1, cmap2):
num_bands1 = cmap1.colors.shape[-1]
@@ -293,9 +322,9 @@ class Colormap(object):
def set_range(self, min_val, max_val, inplace=True):
"""Set the range of the colormap to [*min_val*, *max_val*].
- If min is greater than max then the Colormap's colors are reversed
- before values are updated to the new range. This is done because
- Colormap ``values`` must always be monotonically increasing.
+ The Colormap's values will match the range specified even if "min_val"
+ is greater than "max_val". To flip the order of the colors, use
+ :meth:`reversed`.
Args:
min_val (float): New minimum value for the control points in
@@ -306,14 +335,9 @@ class Colormap(object):
If False, return a new Colormap instance.
"""
- if min_val > max_val:
- cmap = self.reverse(inplace=inplace)
- max_val, min_val = min_val, max_val
- else:
- cmap = self
-
- values = (((cmap.values * 1.0 - cmap.values.min()) /
- (cmap.values.max() - cmap.values.min()))
+ cmap = self
+ values = (((cmap.values * 1.0 - cmap.values[0]) /
+ (cmap.values[-1] - cmap.values[0]))
* (max_val - min_val) + min_val)
if not inplace:
return Colormap(
@@ -330,6 +354,145 @@ class Colormap(object):
(self.colors.max() - self.colors.min())) * 255)
return dict(zip(self.values, tuple(map(tuple, colors))))
+ def to_csv(
+ self,
+ filename: Optional[str] = None,
+ color_scale: Number = 255,
+ ) -> Optional[str]:
+ """Save Colormap to a comma-separated text file or string.
+
+ The CSV data will have 4 to 5 columns for each row where each
+ each row will contain the value (V), red (R), green (B), blue (B),
+ and if configured alpha (A).
+
+ The values will remain in whatever range is currently set on the
+ colormap. The colors of the colormap (assumed to be between 0 and 1)
+ will be multiplied by 255 to produce a traditional unsigned 8-bit
+ integer value.
+
+ Args:
+ filename: The filename of the CSV file to save to.
+ If not provided or None a string is returned with the contents.
+ color_scale: Scale colors by this factor before converting to a
+ CSV. Colors are stored in the Colormap in a 0 to 1 range..
+ Defaults to 255. If not equal to 1 values are converted to
+ integers too.
+
+ """
+ with _file_or_stringio(filename) as csv_file:
+ for value, color in zip(self.values, self.colors):
+ scaled_color = [x * color_scale for x in color]
+ if color_scale != 1.0:
+ scaled_color = [int(x) for x in scaled_color]
+ csv_file.write(",".join(["{:0.6f}".format(value)] + [str(x) for x in scaled_color]) + "\n")
+ if isinstance(csv_file, StringIO):
+ return csv_file.getvalue()
+
+ @classmethod
+ def from_file(
+ cls,
+ filename_or_string: 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.
+ 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
+ number of columns in the data.
+ color_scale: The maximum possible color value in the colormap data
+ provided. For example, if the colors in the provided data were
+ 8-bit unsigned integers this should be 255 (the default). This
+ value will be used to normalize the colors from 0 to 1.
+
+ Colormaps can be loaded from ``.npy``, ``.npz``, or comma-separated text
+ files. Numpy (npy/npz) files should be 2D arrays with rows for each color.
+ Comma-separated files should have a row for each color with each column
+ representing a single value/channel. A filename
+ ending with ``.npy`` or ``.npz`` is read as a numpy file with
+ :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.
+
+ The colormap is interpreted as 1 of 4 different "colormap modes":
+ ``RGB``, ``RGBA``, ``VRGB``, or ``VRGBA``. The
+ colormap mode can be forced with the ``colormap_mode`` keyword
+ argument. If it is not provided then a default will be chosen
+ based on the number of columns in the array (3: RGB, 4: VRGB, 5: VRGBA).
+
+ The "V" in the possible colormap modes represents the control value of
+ where that color should be applied. If "V" is not provided in the colormap
+ data it defaults to the row index in the colormap array (0, 1, 2, ...)
+ divided by the total number of colors to produce a number between 0 and 1.
+ See the "Set Range" section below for more information.
+ The remaining elements in the colormap array represent the Red (R),
+ Green (G), and Blue (B) color to be mapped to.
+
+ See the "Color Scale" section below for more information on the value
+ range of provided numbers.
+
+ **Color Scale**
+
+ By default colors are expected to be in a 0-255 range. This
+ can be overridden by specifying ``color_scale`` keyword argument..
+ 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 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)
+ return cls(values=values, colors=colors)
+
+
+def _get_values_colors_from_file(filename, colormap_mode, color_scale):
+ data = _read_colormap_data_from_file(filename)
+ cols = data.shape[1]
+ default_modes = {
+ 3: 'RGB',
+ 4: 'VRGB',
+ 5: 'VRGBA'
+ }
+ default_mode = default_modes.get(cols)
+ if colormap_mode is None:
+ colormap_mode = default_mode
+ if colormap_mode is None or len(colormap_mode) != cols:
+ raise ValueError(
+ "Unexpected colormap shape for mode '{}'".format(colormap_mode))
+ rows = data.shape[0]
+ if colormap_mode[0] == 'V':
+ colors = data[:, 1:]
+ if color_scale != 1:
+ colors = data[:, 1:] / float(color_scale)
+ values = data[:, 0]
+ else:
+ colors = data
+ if color_scale != 1:
+ colors = colors / float(color_scale)
+ values = np.arange(rows) / float(rows - 1)
+ return values, colors
+
+
+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
+ # CSV file or file-like object of CSV string data
+ return np.loadtxt(filename_or_file_obj, delimiter=",")
+
# matlab jet "#00007F", "blue", "#007FFF", "cyan", "#7FFF7F", "yellow",
# "#FF7F00", "red", "#7F0000"
=====================================
trollimage/tests/test_colormap.py
=====================================
@@ -21,9 +21,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Test colormap.py."""
+import os
+import contextlib
import unittest
from trollimage import colormap
import numpy as np
+from tempfile import NamedTemporaryFile
import pytest
@@ -38,95 +41,6 @@ class TestColormapClass(unittest.TestCase):
(3, (1, 1, 1)),
(4, (0, 0, 0)))
- def test_colorize_no_interpolation(self):
- """Test colorize."""
- data = np.array([1, 2, 3, 4])
-
- channels = self.colormap.colorize(data)
- for i in range(3):
- self.assertTrue(np.allclose(channels[i],
- self.colormap.colors[:, i],
- atol=0.001))
-
- def test_colorize_with_interpolation(self):
- """Test colorize."""
- data = np.array([1.5, 2.5, 3.5, 4])
-
- expected_channels = [np.array([0.22178232, 0.61069262, 0.50011605, 0.]),
- np.array([1.08365532, 0.94644083, 0.50000605, 0.]),
- np.array([0.49104964, 1.20509947, 0.49989589, 0.])]
-
- channels = self.colormap.colorize(data)
- for i in range(3):
- self.assertTrue(np.allclose(channels[i],
- expected_channels[i],
- atol=0.001))
-
- def test_colorize_dask_with_interpolation(self):
- """Test colorize dask arrays."""
- import dask.array as da
- data = da.from_array(np.array([[1.5, 2.5, 3.5, 4],
- [1.5, 2.5, 3.5, 4],
- [1.5, 2.5, 3.5, 4]]), chunks=2)
-
- expected_channels = [np.array([[0.22178232, 0.61069262, 0.50011605, 0.],
- [0.22178232, 0.61069262, 0.50011605, 0.],
- [0.22178232, 0.61069262, 0.50011605, 0.]]),
- np.array([[1.08365532, 0.94644083, 0.50000605, 0.],
- [1.08365532, 0.94644083, 0.50000605, 0.],
- [1.08365532, 0.94644083, 0.50000605, 0.]]),
- np.array([[0.49104964, 1.20509947, 0.49989589, 0.],
- [0.49104964, 1.20509947, 0.49989589, 0.],
- [0.49104964, 1.20509947, 0.49989589, 0.]])]
-
- channels = self.colormap.colorize(data)
- for i, expected_channel in enumerate(expected_channels):
- current_channel = channels[i, :, :]
- assert isinstance(current_channel, da.Array)
- self.assertTrue(np.allclose(current_channel.compute(),
- expected_channel,
- atol=0.001))
-
- def test_palettize(self):
- """Test palettize."""
- data = np.array([1, 2, 3, 4])
-
- channels, colors = self.colormap.palettize(data)
- self.assertTrue(np.allclose(colors, self.colormap.colors))
- self.assertTrue(all(channels == [0, 1, 2, 3]))
-
- cm_ = colormap.Colormap((0, (0.0, 0.0, 0.0)),
- (1, (1.0, 1.0, 1.0)),
- (2, (2, 2, 2)),
- (3, (3, 3, 3)))
-
- data = np.arange(-1, 5)
-
- channels, colors = cm_.palettize(data)
- self.assertTrue(np.allclose(colors, cm_.colors))
- self.assertTrue(all(channels == [0, 0, 1, 2, 3, 3]))
-
- data = np.arange(-1.0, 5.0)
- data[-1] = np.nan
-
- channels, colors = cm_.palettize(data)
- self.assertTrue(np.allclose(colors, cm_.colors))
- self.assertTrue(all(channels == [0, 0, 1, 2, 3, 3]))
-
- def test_palettize_dask(self):
- """Test palettize on a dask array."""
- import dask.array as da
- data = da.from_array(np.array([[1, 2, 3, 4],
- [1, 2, 3, 4],
- [1, 2, 3, 4]]), chunks=2)
- channels, colors = self.colormap.palettize(data)
- assert isinstance(channels, da.Array)
- self.assertTrue(np.allclose(colors, self.colormap.colors))
- self.assertTrue(np.allclose(channels.compute(), [[0, 1, 2, 3],
- [0, 1, 2, 3],
- [0, 1, 2, 3]]))
- assert channels.dtype == int
-
def test_set_range(self):
"""Test set_range."""
cm_ = colormap.Colormap((1, (1.0, 1.0, 0.0)),
@@ -146,8 +60,11 @@ class TestColormapClass(unittest.TestCase):
(4, (0, 0, 0)))
cm_.set_range(8, 0)
- self.assertTrue(cm_.values[0] == 0)
- self.assertTrue(cm_.values[-1] == 8)
+ assert cm_.values[0] == 8
+ assert cm_.values[-1] == 0
+ _assert_monotonic_values(cm_, increasing=False)
+ np.testing.assert_allclose(cm_.colors[0], (1.0, 1.0, 0.0))
+ np.testing.assert_allclose(cm_.colors[-1], (0.0, 0.0, 0.0))
def test_reverse(self):
"""Test reverse."""
@@ -233,6 +150,28 @@ COLORS_RGBA1 = np.array([
])
+def _mono_inc_colormap():
+ """Create fake monotonically increasing colormap."""
+ values = [1, 2, 3, 4]
+ cmap = colormap.Colormap(values=values, colors=_four_rgb_colors())
+ return cmap
+
+
+def _mono_dec_colormap():
+ values = [4, 3, 2, 1]
+ cmap = colormap.Colormap(values=values, colors=_four_rgb_colors())
+ return cmap
+
+
+def _four_rgb_colors():
+ return [
+ (1.0, 1.0, 0.0),
+ (0.0, 1.0, 1.0),
+ (1, 1, 1),
+ (0, 0, 0),
+ ]
+
+
class TestColormap:
"""Pytest tests for colormap objects."""
@@ -373,6 +312,21 @@ class TestColormap:
# this should succeed
_ = cmap1 + cmap2
+ def test_merge_monotonic_decreasing(self):
+ """Test that merged colormaps can be monotonically decreasing."""
+ cmap1 = colormap.Colormap(
+ colors=np.arange(5 * 3).reshape((5, 3)),
+ values=np.linspace(2, 1, 5),
+ )
+ cmap2 = colormap.Colormap(
+ colors=np.arange(5 * 3).reshape((5, 3)),
+ values=np.linspace(1, 0, 5),
+ )
+ _assert_monotonic_values(cmap1, increasing=False)
+ _assert_monotonic_values(cmap2, increasing=False)
+ # this should succeed
+ _ = cmap1 + cmap2
+
@pytest.mark.parametrize('inplace', [False, True])
def test_reverse(self, inplace):
"""Test colormap reverse."""
@@ -383,9 +337,9 @@ class TestColormap:
cmap = colormap.Colormap(values=values, colors=colors)
new_cmap = cmap.reverse(inplace)
- self._assert_inplace_worked(cmap, new_cmap, inplace)
- self._compare_reversed_colors(cmap, new_cmap, inplace, orig_colors)
- self._assert_unchanged_values(cmap, new_cmap, inplace, orig_values)
+ _assert_inplace_worked(cmap, new_cmap, inplace)
+ _assert_reversed_colors(cmap, new_cmap, inplace, orig_colors)
+ _assert_unchanged_values(cmap, new_cmap, inplace, orig_values)
@pytest.mark.parametrize(
'new_range',
@@ -405,46 +359,316 @@ class TestColormap:
cmap = colormap.Colormap(values=values, colors=colors)
new_cmap = cmap.set_range(*new_range, inplace)
- self._assert_inplace_worked(cmap, new_cmap, inplace)
- self._assert_monotonic_values(cmap)
- self._assert_monotonic_values(new_cmap)
- self._assert_values_changed(cmap, new_cmap, inplace, orig_values)
- if new_range[0] > new_range[1]:
- self._compare_reversed_colors(cmap, new_cmap, inplace, orig_colors)
-
- @staticmethod
- def _assert_monotonic_values(cmap):
- np.testing.assert_allclose(np.diff(cmap.values) > 0, True)
-
- @staticmethod
- def _assert_unchanged_values(cmap, new_cmap, inplace, orig_values):
- if inplace:
- np.testing.assert_allclose(cmap.values, orig_values)
- else:
- np.testing.assert_allclose(cmap.values, orig_values)
- np.testing.assert_allclose(new_cmap.values, orig_values)
-
- @staticmethod
- def _compare_reversed_colors(cmap, new_cmap, inplace, orig_colors):
- if inplace:
- assert cmap is new_cmap
- np.testing.assert_allclose(cmap.colors, orig_colors[::-1])
- else:
- assert cmap is not new_cmap
- np.testing.assert_allclose(cmap.colors, orig_colors)
- np.testing.assert_allclose(new_cmap.colors, orig_colors[::-1])
-
- @staticmethod
- def _assert_values_changed(cmap, new_cmap, inplace, orig_values):
- assert not np.allclose(new_cmap.values, orig_values)
- if not inplace:
- np.testing.assert_allclose(cmap.values, orig_values)
- else:
- assert not np.allclose(cmap.values, orig_values)
+ flipped_range = new_range[0] > new_range[1]
+ _assert_inplace_worked(cmap, new_cmap, inplace)
+ _assert_monotonic_values(cmap, increasing=not inplace or not flipped_range)
+ _assert_monotonic_values(new_cmap, increasing=not flipped_range)
+ _assert_values_changed(cmap, new_cmap, inplace, orig_values)
+ _assert_unchanged_colors(cmap, new_cmap, orig_colors)
- @staticmethod
- def _assert_inplace_worked(cmap, new_cmap, inplace):
- if not inplace:
- assert new_cmap is not cmap
+ @pytest.mark.parametrize(
+ ("input_cmap_func", "expected_result"),
+ [
+ (_mono_inc_colormap, (0, 1, 2, 3)),
+ (_mono_dec_colormap, (3, 2, 1, 0)),
+ ]
+ )
+ def test_palettize_in_range(self, input_cmap_func, expected_result):
+ """Test palettize with values inside the set range."""
+ data = np.array([1, 2, 3, 4])
+ cm = input_cmap_func()
+ channels, colors = cm.palettize(data)
+ np.testing.assert_allclose(colors, cm.colors)
+ assert all(channels == expected_result)
+
+ def test_palettize_mono_inc_out_range(self):
+ """Test palettize with a value outside the colormap values."""
+ cm = colormap.Colormap(values=[0, 1, 2, 3],
+ colors=_four_rgb_colors())
+ data = np.arange(-1, 5)
+ channels, colors = cm.palettize(data)
+ np.testing.assert_allclose(colors, cm.colors)
+ assert all(channels == [0, 0, 1, 2, 3, 3])
+
+ def test_palettize_mono_inc_nan(self):
+ """Test palettize with monotonic increasing values with a NaN."""
+ cm = colormap.Colormap(values=[0, 1, 2, 3],
+ colors=_four_rgb_colors())
+ data = np.arange(-1.0, 5.0)
+ data[-1] = np.nan
+ channels, colors = cm.palettize(data)
+ np.testing.assert_allclose(colors, cm.colors)
+ assert all(channels == [0, 0, 1, 2, 3, 3])
+
+ def test_palettize_mono_inc_in_range_dask(self):
+ """Test palettize on a dask array."""
+ import dask.array as da
+ data = da.from_array(np.array([[1, 2, 3, 4],
+ [1, 2, 3, 4],
+ [1, 2, 3, 4]]), chunks=2)
+ cm = _mono_inc_colormap()
+ channels, colors = cm.palettize(data)
+ assert isinstance(channels, da.Array)
+ np.testing.assert_allclose(colors, cm.colors)
+ np.testing.assert_allclose(channels.compute(), [[0, 1, 2, 3],
+ [0, 1, 2, 3],
+ [0, 1, 2, 3]])
+ assert channels.dtype == int
+
+ @pytest.mark.parametrize(
+ ("input_cmap_func", "expected_result"),
+ [
+ (_mono_inc_colormap, _four_rgb_colors()),
+ (_mono_dec_colormap, _four_rgb_colors()[::-1]),
+ ]
+ )
+ def test_colorize_no_interpolation(self, input_cmap_func, expected_result):
+ """Test colorize."""
+ data = np.array([1, 2, 3, 4])
+ cm = input_cmap_func()
+ channels = cm.colorize(data)
+ output_colors = [channels[:, i] for i in range(data.size)]
+ for output_color, expected_color in zip(output_colors, expected_result):
+ np.testing.assert_allclose(output_color, expected_color, atol=0.001)
+
+ @pytest.mark.parametrize(
+ ("input_cmap_func", "expected_result"),
+ [
+ (_mono_inc_colormap,
+ np.array([
+ [0.22178232, 1.08365532, 0.49104964],
+ [0.61069262, 0.94644083, 1.20509947],
+ [0.50011605, 0.50000605, 0.49989589],
+ [0.0, 0.0, 0.0]])),
+ (_mono_dec_colormap,
+ np.array([
+ [0.50011605, 0.50000605, 0.49989589],
+ [0.61069262, 0.94644083, 1.20509947],
+ [0.22178232, 1.08365532, 0.49104964],
+ [1.0, 1.0, 0.0]])),
+ ]
+ )
+ def test_colorize_with_interpolation(self, input_cmap_func, expected_result):
+ """Test colorize where data values require interpolation between colors."""
+ data = np.array([1.5, 2.5, 3.5, 4])
+ cm = input_cmap_func()
+ channels = cm.colorize(data)
+ output_colors = [channels[:, i] for i in range(data.size)]
+ for output_color, expected_color in zip(output_colors, expected_result):
+ np.testing.assert_allclose(output_color, expected_color, atol=0.001)
+
+ def test_colorize_dask_with_interpolation(self):
+ """Test colorize dask arrays."""
+ import dask.array as da
+ data = da.from_array(np.array([[1.5, 2.5, 3.5, 4],
+ [1.5, 2.5, 3.5, 4],
+ [1.5, 2.5, 3.5, 4]]), chunks=2)
+
+ expected_channels = [np.array([[0.22178232, 0.61069262, 0.50011605, 0.],
+ [0.22178232, 0.61069262, 0.50011605, 0.],
+ [0.22178232, 0.61069262, 0.50011605, 0.]]),
+ np.array([[1.08365532, 0.94644083, 0.50000605, 0.],
+ [1.08365532, 0.94644083, 0.50000605, 0.],
+ [1.08365532, 0.94644083, 0.50000605, 0.]]),
+ np.array([[0.49104964, 1.20509947, 0.49989589, 0.],
+ [0.49104964, 1.20509947, 0.49989589, 0.],
+ [0.49104964, 1.20509947, 0.49989589, 0.]])]
+
+ cm = _mono_inc_colormap()
+ channels = cm.colorize(data)
+ for i, expected_channel in enumerate(expected_channels):
+ current_channel = channels[i, :, :]
+ assert isinstance(current_channel, da.Array)
+ np.testing.assert_allclose(current_channel.compute(),
+ expected_channel,
+ atol=0.001)
+
+
+ at contextlib.contextmanager
+def closed_named_temp_file(**kwargs):
+ """Named temporary file context manager that closes the file after creation.
+
+ This helps with Windows systems which can get upset with opening or
+ deleting a file that is already open.
+
+ """
+ try:
+ with NamedTemporaryFile(delete=False, **kwargs) as tmp_cmap:
+ yield tmp_cmap.name
+ finally:
+ os.remove(tmp_cmap.name)
+
+
+def _write_cmap_to_file(cmap_filename, cmap_data):
+ ext = os.path.splitext(cmap_filename)[1]
+ if ext in (".npy",):
+ np.save(cmap_filename, cmap_data)
+ elif ext in (".npz",):
+ np.savez(cmap_filename, cmap_data)
+ else:
+ np.savetxt(cmap_filename, cmap_data, delimiter=",")
+
+
+def _generate_cmap_test_data(color_scale, colormap_mode):
+ cmap_data = np.array([
+ [1, 0, 0],
+ [1, 1, 0],
+ [1, 1, 1],
+ [0, 0, 1],
+ ], dtype=np.float64)
+ if len(colormap_mode) != 3:
+ _cmap_data = cmap_data
+ cmap_data = np.empty((cmap_data.shape[0], len(colormap_mode)),
+ dtype=np.float64)
+ if colormap_mode.startswith("V") or colormap_mode.endswith("A"):
+ cmap_data[:, 0] = np.array([128, 130, 132, 134]) / 255.0
+ cmap_data[:, -3:] = _cmap_data
+ if colormap_mode.startswith("V") and colormap_mode.endswith("A"):
+ cmap_data[:, 1] = np.array([128, 130, 132, 134]) / 255.0
+ if color_scale is None or color_scale == 255:
+ cmap_data = (cmap_data * 255).astype(np.uint8)
+ return cmap_data
+
+
+class TestFromFileCreation:
+ """Tests for loading Colormaps from files."""
+
+ @pytest.mark.parametrize("csv_filename", [None, "test.cmap"])
+ @pytest.mark.parametrize("new_range", [None, (25.0, 75.0)])
+ @pytest.mark.parametrize("color_scale", [1.0, 255, 65535])
+ def test_csv_roundtrip(self, tmp_path, csv_filename, new_range, color_scale):
+ """Test saving and loading a Colormap from a CSV file."""
+ orig_cmap = colormap.brbg
+ if new_range is not None:
+ orig_cmap = orig_cmap.set_range(*new_range, inplace=False)
+ if isinstance(csv_filename, str):
+ csv_filename = str(tmp_path / csv_filename)
+ res = orig_cmap.to_csv(csv_filename, color_scale=color_scale)
+ assert res is None
+ new_cmap = colormap.Colormap.from_file(csv_filename, color_scale=color_scale)
else:
- assert new_cmap is cmap
+ res = orig_cmap.to_csv(None, color_scale=color_scale)
+ assert isinstance(res, str)
+ new_cmap = colormap.Colormap.from_file(res, color_scale=color_scale)
+ np.testing.assert_allclose(orig_cmap.values, new_cmap.values)
+ np.testing.assert_allclose(orig_cmap.colors, new_cmap.colors)
+
+ @pytest.mark.parametrize("color_scale", [None, 1.0])
+ @pytest.mark.parametrize("colormap_mode", ["RGB", "VRGB", "VRGBA"])
+ @pytest.mark.parametrize("filename_suffix", [".npy", ".npz", ".csv"])
+ def test_cmap_from_file(self, color_scale, colormap_mode, filename_suffix):
+ """Test that colormaps can be loaded from a binary or CSV file."""
+ # create the colormap file on disk
+ with closed_named_temp_file(suffix=filename_suffix) as cmap_filename:
+ cmap_data = _generate_cmap_test_data(color_scale, colormap_mode)
+ _write_cmap_to_file(cmap_filename, cmap_data)
+
+ unset_first_value = 128.0 / 255.0 if colormap_mode.startswith("V") else 0.0
+ unset_last_value = 134.0 / 255.0 if colormap_mode.startswith("V") else 1.0
+ if (color_scale is None or color_scale == 255) and colormap_mode.startswith("V"):
+ unset_first_value *= 255
+ unset_last_value *= 255
+
+ first_color = [1.0, 0.0, 0.0]
+ if colormap_mode == "VRGBA":
+ first_color = [128.0 / 255.0] + first_color
+
+ kwargs1 = {}
+ if color_scale is not None:
+ kwargs1["color_scale"] = color_scale
+
+ cmap = colormap.Colormap.from_file(cmap_filename, **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
+
+ def test_cmap_vrgb_as_rgba(self):
+ """Test that data created as VRGB still reads as RGBA."""
+ with closed_named_temp_file(suffix=".npy") as cmap_filename:
+ cmap_data = _generate_cmap_test_data(None, "VRGB")
+ np.save(cmap_filename, cmap_data)
+ cmap = colormap.Colormap.from_file(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
+
+ @pytest.mark.parametrize(
+ ("real_mode", "forced_mode"),
+ [
+ ("VRGBA", "RGBA"),
+ ("VRGBA", "VRGB"),
+ ("RGBA", "RGB"),
+ ]
+ )
+ @pytest.mark.parametrize("filename_suffix", [".npy", ".csv"])
+ def test_cmap_bad_mode(self, real_mode, forced_mode, filename_suffix):
+ """Test that reading colormaps with the wrong mode fails."""
+ with closed_named_temp_file(suffix=filename_suffix) as cmap_filename:
+ cmap_data = _generate_cmap_test_data(None, real_mode)
+ _write_cmap_to_file(cmap_filename, cmap_data)
+ # Force colormap_mode VRGBA to RGBA and we should see an exception
+ with pytest.raises(ValueError):
+ colormap.Colormap.from_file(cmap_filename, colormap_mode=forced_mode)
+
+ def test_cmap_from_file_bad_shape(self):
+ """Test that unknown array shape causes an error."""
+ with closed_named_temp_file(suffix='.npy') as cmap_filename:
+ np.save(cmap_filename, np.array([
+ [0],
+ [64],
+ [128],
+ [255],
+ ]))
+
+ with pytest.raises(ValueError):
+ colormap.Colormap.from_file(cmap_filename)
+
+
+def _assert_monotonic_values(cmap, increasing=True):
+ delta = np.diff(cmap.values)
+ np.testing.assert_allclose(delta > 0, increasing)
+
+
+def _assert_unchanged_values(cmap, new_cmap, inplace, orig_values):
+ if inplace:
+ assert cmap is new_cmap
+ np.testing.assert_allclose(cmap.values, orig_values)
+ else:
+ assert cmap is not new_cmap
+ np.testing.assert_allclose(cmap.values, orig_values)
+ np.testing.assert_allclose(new_cmap.values, orig_values)
+
+
+def _assert_unchanged_colors(cmap, new_cmap, orig_colors):
+ np.testing.assert_allclose(cmap.colors, orig_colors)
+ np.testing.assert_allclose(new_cmap.colors, orig_colors)
+
+
+def _assert_reversed_colors(cmap, new_cmap, inplace, orig_colors):
+ if inplace:
+ assert cmap is new_cmap
+ np.testing.assert_allclose(cmap.colors, orig_colors[::-1])
+ else:
+ assert cmap is not new_cmap
+ np.testing.assert_allclose(cmap.colors, orig_colors)
+ np.testing.assert_allclose(new_cmap.colors, orig_colors[::-1])
+
+
+def _assert_values_changed(cmap, new_cmap, inplace, orig_values):
+ assert not np.allclose(new_cmap.values, orig_values)
+ if not inplace:
+ np.testing.assert_allclose(cmap.values, orig_values)
+ else:
+ assert not np.allclose(cmap.values, orig_values)
+
+
+def _assert_inplace_worked(cmap, new_cmap, inplace):
+ if not inplace:
+ assert new_cmap is not cmap
+ else:
+ assert new_cmap is cmap
=====================================
trollimage/tests/test_image.py
=====================================
@@ -27,8 +27,13 @@ from collections import OrderedDict
from tempfile import NamedTemporaryFile
import numpy as np
+import xarray as xr
+import rasterio as rio
import pytest
-from trollimage import image
+
+from trollimage import image, xrimage
+from trollimage.colormap import Colormap, brbg
+
EPSILON = 0.0001
@@ -715,7 +720,6 @@ class TestXRImage:
def test_init(self):
"""Test object initialization."""
- import xarray as xr
from trollimage import xrimage
data = xr.DataArray([[0, 0.5, 0.5], [0.5, 0.25, 0.25]], dims=['y', 'x'])
img = xrimage.XRImage(data)
@@ -752,7 +756,6 @@ class TestXRImage:
Xarray >0.15 makes data read-only after expand_dims.
"""
- import xarray as xr
from trollimage import xrimage
data = xr.DataArray([[0, 0.5, 0.5], [0.5, 0.25, 0.25]], dims=['y', 'x'])
img = xrimage.XRImage(data)
@@ -763,7 +766,6 @@ class TestXRImage:
def test_regression_double_format_save(self):
"""Test that double format information isn't passed to save."""
- import xarray as xr
from trollimage import xrimage
data = xr.DataArray(np.arange(75).reshape(5, 5, 3) / 74., dims=[
@@ -778,10 +780,8 @@ class TestXRImage:
reason="'NamedTemporaryFile' not supported on Windows")
def test_rgb_save(self):
"""Test saving RGB/A data to simple image formats."""
- import xarray as xr
from dask.delayed import Delayed
from trollimage import xrimage
- import rasterio as rio
data = xr.DataArray(np.arange(75).reshape(5, 5, 3) / 74., dims=[
'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
@@ -812,9 +812,7 @@ class TestXRImage:
reason="'NamedTemporaryFile' not supported on Windows")
def test_save_single_band_jpeg(self):
"""Test saving single band to jpeg formats."""
- import xarray as xr
from trollimage import xrimage
- import rasterio as rio
# Single band image
data = np.arange(75).reshape(15, 5, 1) / 74.
@@ -841,9 +839,7 @@ class TestXRImage:
reason="'NamedTemporaryFile' not supported on Windows")
def test_save_single_band_png(self):
"""Test saving single band images to simple image formats."""
- import xarray as xr
from trollimage import xrimage
- import rasterio as rio
# Single band image
data = np.arange(75).reshape(15, 5, 1) / 74.
@@ -888,7 +884,6 @@ class TestXRImage:
reason="'NamedTemporaryFile' not supported on Windows")
def test_save_palettes(self):
"""Test saving paletted images to simple image formats."""
- import xarray as xr
from trollimage import xrimage
# Single band image palettized
@@ -914,10 +909,8 @@ class TestXRImage:
reason="'NamedTemporaryFile' not supported on Windows")
def test_save_geotiff_float(self):
"""Test saving geotiffs when input data is float."""
- import xarray as xr
import dask.array as da
from trollimage import xrimage
- import rasterio as rio
# numpy array image - scale to 0 to 1 first
data = xr.DataArray(np.arange(75).reshape(5, 5, 3) / 75.,
@@ -1067,7 +1060,6 @@ class TestXRImage:
reason="'NamedTemporaryFile' not supported on Windows")
def test_save_geotiff_datetime(self):
"""Test saving geotiffs when start_time is in the attributes."""
- import xarray as xr
import datetime as dt
data = xr.DataArray(np.arange(75).reshape(5, 5, 3), dims=[
@@ -1087,10 +1079,8 @@ class TestXRImage:
reason="'NamedTemporaryFile' not supported on Windows")
def test_save_geotiff_int(self):
"""Test saving geotiffs when input data is int."""
- import xarray as xr
import dask.array as da
from trollimage import xrimage
- import rasterio as rio
from rasterio.control import GroundControlPoint
# numpy array image
@@ -1280,10 +1270,8 @@ class TestXRImage:
to.
"""
- import xarray as xr
import dask.array as da
from trollimage import xrimage
- import rasterio as rio
# numpy array image
data = xr.DataArray(np.arange(75).reshape(5, 5, 3), dims=[
@@ -1307,9 +1295,7 @@ class TestXRImage:
@pytest.mark.skipif(sys.platform.startswith('win'), reason="'NamedTemporaryFile' not supported on Windows")
def test_save_jp2_int(self):
"""Test saving jp2000 when input data is int."""
- import xarray as xr
from trollimage import xrimage
- import rasterio as rio
# numpy array image
data = xr.DataArray(np.arange(75).reshape(5, 5, 3), dims=[
@@ -1330,9 +1316,7 @@ class TestXRImage:
@pytest.mark.skipif(sys.platform.startswith('win'), reason="'NamedTemporaryFile' not supported on Windows")
def test_save_cloud_optimized_geotiff(self):
"""Test saving cloud optimized geotiffs."""
- import xarray as xr
from trollimage import xrimage
- import rasterio as rio
# trigger COG driver to create 2 overview levels
# COG driver is only available in GDAL 3.1 or later
@@ -1351,9 +1335,7 @@ class TestXRImage:
@pytest.mark.skipif(sys.platform.startswith('win'), reason="'NamedTemporaryFile' not supported on Windows")
def test_save_overviews(self):
"""Test saving geotiffs with overviews."""
- import xarray as xr
from trollimage import xrimage
- import rasterio as rio
# numpy array image
data = xr.DataArray(np.arange(75).reshape(5, 5, 3), dims=[
@@ -1392,9 +1374,7 @@ class TestXRImage:
@pytest.mark.skipif(sys.platform.startswith('win'), reason="'NamedTemporaryFile' not supported on Windows")
def test_save_tags(self):
"""Test saving geotiffs with tags."""
- import xarray as xr
from trollimage import xrimage
- import rasterio as rio
# numpy array image
data = xr.DataArray(np.arange(75).reshape(5, 5, 3), dims=[
@@ -1410,7 +1390,6 @@ class TestXRImage:
def test_gamma(self):
"""Test gamma correction."""
- import xarray as xr
from trollimage import xrimage
arr = np.arange(75).reshape(5, 5, 3) / 75.
@@ -1427,7 +1406,6 @@ class TestXRImage:
def test_crude_stretch(self):
"""Check crude stretching."""
- import xarray as xr
from trollimage import xrimage
arr = np.arange(75).reshape(5, 5, 3)
@@ -1456,7 +1434,6 @@ class TestXRImage:
def test_invert(self):
"""Check inversion of the image."""
- import xarray as xr
from trollimage import xrimage
arr = np.arange(75).reshape(5, 5, 3) / 75.
@@ -1482,7 +1459,6 @@ class TestXRImage:
def test_linear_stretch(self):
"""Test linear stretching with cutoffs."""
- import xarray as xr
from trollimage import xrimage
arr = np.arange(75).reshape(5, 5, 3) / 74.
@@ -1523,7 +1499,6 @@ class TestXRImage:
def test_histogram_stretch(self):
"""Test histogram stretching."""
- import xarray as xr
from trollimage import xrimage
arr = np.arange(75).reshape(5, 5, 3) / 74.
@@ -1575,7 +1550,6 @@ class TestXRImage:
@pytest.mark.parametrize("base", ["e", "10", "2"])
def test_logarithmic_stretch(self, min_stretch, max_stretch, base):
"""Test logarithmic strecthing."""
- import xarray as xr
from trollimage import xrimage
from .utils import assert_maximum_dask_computes
@@ -1624,7 +1598,6 @@ class TestXRImage:
def test_weber_fechner_stretch(self):
"""Test applying S=2.3klog10I+C to the data."""
- import xarray as xr
from trollimage import xrimage
arr = np.arange(75).reshape(5, 5, 3) / 74.
@@ -1697,7 +1670,6 @@ class TestXRImage:
def test_convert_modes(self):
"""Test modes convertions."""
import dask
- import xarray as xr
from trollimage import xrimage
from trollimage.colormap import brbg, Colormap
@@ -1838,7 +1810,6 @@ class TestXRImage:
def test_final_mode(self):
"""Test final_mode."""
- import xarray as xr
from trollimage import xrimage
# numpy array image
@@ -1848,178 +1819,8 @@ class TestXRImage:
assert img.final_mode(None) == 'RGBA'
assert img.final_mode(0) == 'RGB'
- def test_colorize(self):
- """Test colorize with an RGB colormap."""
- import xarray as xr
- from trollimage import xrimage
- from trollimage.colormap import brbg
-
- arr = np.arange(75).reshape(5, 15) / 74.
- data = xr.DataArray(arr.copy(), dims=['y', 'x'])
- img = xrimage.XRImage(data)
- img.colorize(brbg)
- values = img.data.compute()
-
- expected = np.array([[
- [3.29409498e-01, 3.59108764e-01, 3.88800969e-01,
- 4.18486092e-01, 4.48164112e-01, 4.77835010e-01,
- 5.07498765e-01, 5.37155355e-01, 5.65419479e-01,
- 5.92686124e-01, 6.19861622e-01, 6.46945403e-01,
- 6.73936907e-01, 7.00835579e-01, 7.27640871e-01],
- [7.58680358e-01, 8.01695237e-01, 8.35686284e-01,
- 8.60598212e-01, 8.76625002e-01, 8.84194741e-01,
- 8.83948647e-01, 8.76714923e-01, 8.95016030e-01,
- 9.14039881e-01, 9.27287161e-01, 9.36546985e-01,
- 9.43656076e-01, 9.50421050e-01, 9.58544227e-01],
- [9.86916929e-01, 1.02423117e+00, 1.03591220e+00,
- 1.02666645e+00, 1.00491333e+00, 9.80759775e-01,
- 9.63746819e-01, 9.60798629e-01, 9.47739946e-01,
- 9.27428067e-01, 9.01184523e-01, 8.71168132e-01,
- 8.40161241e-01, 8.11290344e-01, 7.87705814e-01],
- [7.57749840e-01, 7.20020026e-01, 6.82329616e-01,
- 6.44678929e-01, 6.07068282e-01, 5.69497990e-01,
- 5.31968369e-01, 4.94025422e-01, 4.54275131e-01,
- 4.14517560e-01, 3.74757709e-01, 3.35000583e-01,
- 2.95251189e-01, 2.55514533e-01, 2.15795621e-01],
- [1.85805611e-01, 1.58245609e-01, 1.30686714e-01,
- 1.03128926e-01, 7.55722460e-02, 4.80166757e-02,
- 2.04622160e-02, 3.79809920e-03, 3.46310306e-03,
- 3.10070529e-03, 2.68579661e-03, 2.19341216e-03,
- 1.59875239e-03, 8.77203803e-04, 4.35952940e-06]],
-
- [[1.88249866e-01, 2.05728128e-01, 2.23209861e-01,
- 2.40695072e-01, 2.58183766e-01, 2.75675949e-01,
- 2.93171625e-01, 3.10670801e-01, 3.32877903e-01,
- 3.58244116e-01, 3.83638063e-01, 4.09059827e-01,
- 4.34509485e-01, 4.59987117e-01, 4.85492795e-01],
- [5.04317660e-01, 4.97523483e-01, 4.92879482e-01,
- 4.90522941e-01, 4.90521579e-01, 4.92874471e-01,
- 4.97514769e-01, 5.04314130e-01, 5.48356836e-01,
- 6.02679755e-01, 6.57930117e-01, 7.13582394e-01,
- 7.69129132e-01, 8.24101035e-01, 8.78084923e-01],
- [9.05957986e-01, 9.00459829e-01, 9.01710827e-01,
- 9.09304816e-01, 9.21567297e-01, 9.36002510e-01,
- 9.49878533e-01, 9.60836244e-01, 9.50521017e-01,
- 9.42321192e-01, 9.36098294e-01, 9.31447978e-01,
- 9.27737112e-01, 9.24164130e-01, 9.19837458e-01],
- [9.08479555e-01, 8.93119640e-01, 8.77756168e-01,
- 8.62389039e-01, 8.47018155e-01, 8.31643415e-01,
- 8.16264720e-01, 7.98248733e-01, 7.69688456e-01,
- 7.41111049e-01, 7.12515170e-01, 6.83899486e-01,
- 6.55262669e-01, 6.26603399e-01, 5.97920364e-01],
- [5.71406981e-01, 5.45439361e-01, 5.19471340e-01,
- 4.93502919e-01, 4.67534097e-01, 4.41564875e-01,
- 4.15595252e-01, 3.91172349e-01, 3.69029170e-01,
- 3.46833147e-01, 3.24591169e-01, 3.02310146e-01,
- 2.79997004e-01, 2.57658679e-01, 2.35302110e-01]],
-
- [[1.96102817e-02, 2.23037080e-02, 2.49835320e-02,
- 2.76497605e-02, 3.03024001e-02, 3.29414575e-02,
- 3.55669395e-02, 3.81788529e-02, 5.03598778e-02,
- 6.89209657e-02, 8.74757090e-02, 1.06024973e-01,
- 1.24569626e-01, 1.43110536e-01, 1.61648577e-01],
- [1.82340027e-01, 2.15315774e-01, 2.53562955e-01,
- 2.95884521e-01, 3.41038527e-01, 3.87773687e-01,
- 4.34864157e-01, 4.81142673e-01, 5.00410360e-01,
- 5.19991397e-01, 5.47394263e-01, 5.82556639e-01,
- 6.25097005e-01, 6.74344521e-01, 7.29379582e-01],
- [7.75227971e-01, 8.13001048e-01, 8.59395545e-01,
- 9.04577146e-01, 9.40342288e-01, 9.61653621e-01,
- 9.67479211e-01, 9.60799542e-01, 9.63421077e-01,
- 9.66445062e-01, 9.67352042e-01, 9.63790783e-01,
- 9.53840372e-01, 9.36234978e-01, 9.10530024e-01],
- [8.86771441e-01, 8.67903107e-01, 8.48953980e-01,
- 8.29924111e-01, 8.10813555e-01, 7.91622365e-01,
- 7.72350598e-01, 7.51439565e-01, 7.24376642e-01,
- 6.97504841e-01, 6.70822717e-01, 6.44328750e-01,
- 6.18021348e-01, 5.91898843e-01, 5.65959492e-01],
- [5.40017537e-01, 5.14048293e-01, 4.88079755e-01,
- 4.62111921e-01, 4.36144791e-01, 4.10178361e-01,
- 3.84212632e-01, 3.58028450e-01, 3.31935148e-01,
- 3.06445966e-01, 2.81566598e-01, 2.57302099e-01,
- 2.33656886e-01, 2.10634733e-01, 1.88238767e-01]]])
-
- np.testing.assert_allclose(values, expected)
-
- # try it with an RGB
- arr = np.arange(75).reshape(5, 15) / 74.
- alpha = arr > 40.
- data = xr.DataArray([arr.copy(), alpha],
- dims=['bands', 'y', 'x'],
- coords={'bands': ['L', 'A']})
- img = xrimage.XRImage(data)
- img.colorize(brbg)
-
- values = img.data.values
- expected = np.concatenate((expected,
- alpha.reshape((1,) + alpha.shape)))
- np.testing.assert_allclose(values, expected)
-
- def test_colorize_rgba(self):
- """Test colorize with an RGBA colormap."""
- import xarray as xr
- from trollimage import xrimage
- from trollimage.colormap import Colormap
-
- # RGBA colormap
- bw = Colormap(
- (0.0, (1.0, 1.0, 1.0, 1.0)),
- (1.0, (0.0, 0.0, 0.0, 0.5)),
- )
-
- arr = np.arange(75).reshape(5, 15) / 74.
- data = xr.DataArray(arr.copy(), dims=['y', 'x'])
- img = xrimage.XRImage(data)
- img.colorize(bw)
- values = img.data.compute()
- assert (4, 5, 15) == values.shape
- np.testing.assert_allclose(values[:, 0, 0], [1.0, 1.0, 1.0, 1.0], rtol=1e-03)
- np.testing.assert_allclose(values[:, -1, -1], [0.0, 0.0, 0.0, 0.5])
-
- def test_palettize(self):
- """Test palettize with an RGB colormap."""
- import xarray as xr
- from trollimage import xrimage
- from trollimage.colormap import brbg
-
- arr = np.arange(75).reshape(5, 15) / 74.
- data = xr.DataArray(arr.copy(), dims=['y', 'x'])
- img = xrimage.XRImage(data)
- img.palettize(brbg)
-
- values = img.data.values
- expected = np.array([[
- [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
- [2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3],
- [4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5],
- [6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7],
- [8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10]]])
- np.testing.assert_allclose(values, expected)
-
- def test_palettize_rgba(self):
- """Test palettize with an RGBA colormap."""
- import xarray as xr
- from trollimage import xrimage
- from trollimage.colormap import Colormap
-
- # RGBA colormap
- bw = Colormap(
- (0.0, (1.0, 1.0, 1.0, 1.0)),
- (1.0, (0.0, 0.0, 0.0, 0.5)),
- )
-
- arr = np.arange(75).reshape(5, 15) / 74.
- data = xr.DataArray(arr.copy(), dims=['y', 'x'])
- img = xrimage.XRImage(data)
- img.palettize(bw)
-
- values = img.data.values
- assert (1, 5, 15) == values.shape
- assert (2, 4) == bw.colors.shape
-
def test_stack(self):
"""Test stack."""
- import xarray as xr
from trollimage import xrimage
# background image
@@ -2051,7 +1852,6 @@ class TestXRImage:
def test_blend(self):
"""Test blend."""
- import xarray as xr
from trollimage import xrimage
core1 = np.arange(75).reshape(5, 5, 3) / 75.0
@@ -2101,7 +1901,6 @@ class TestXRImage:
def test_show(self):
"""Test that the show commands calls PIL.show."""
- import xarray as xr
from trollimage import xrimage
data = xr.DataArray(np.arange(75).reshape(5, 5, 3) / 75., dims=[
@@ -2113,7 +1912,6 @@ class TestXRImage:
def test_apply_pil(self):
"""Test the apply_pil method."""
- import xarray as xr
from trollimage import xrimage
np_data = np.arange(75).reshape(5, 5, 3) / 75.
@@ -2169,12 +1967,251 @@ class TestXRImage:
pil_img.convert.assert_called_with('RGB')
+class TestXRImageColorize:
+ """Test the colorize method of the XRImage class."""
+
+ _expected = np.array([[
+ [3.29409498e-01, 3.59108764e-01, 3.88800969e-01,
+ 4.18486092e-01, 4.48164112e-01, 4.77835010e-01,
+ 5.07498765e-01, 5.37155355e-01, 5.65419479e-01,
+ 5.92686124e-01, 6.19861622e-01, 6.46945403e-01,
+ 6.73936907e-01, 7.00835579e-01, 7.27640871e-01],
+ [7.58680358e-01, 8.01695237e-01, 8.35686284e-01,
+ 8.60598212e-01, 8.76625002e-01, 8.84194741e-01,
+ 8.83948647e-01, 8.76714923e-01, 8.95016030e-01,
+ 9.14039881e-01, 9.27287161e-01, 9.36546985e-01,
+ 9.43656076e-01, 9.50421050e-01, 9.58544227e-01],
+ [9.86916929e-01, 1.02423117e+00, 1.03591220e+00,
+ 1.02666645e+00, 1.00491333e+00, 9.80759775e-01,
+ 9.63746819e-01, 9.60798629e-01, 9.47739946e-01,
+ 9.27428067e-01, 9.01184523e-01, 8.71168132e-01,
+ 8.40161241e-01, 8.11290344e-01, 7.87705814e-01],
+ [7.57749840e-01, 7.20020026e-01, 6.82329616e-01,
+ 6.44678929e-01, 6.07068282e-01, 5.69497990e-01,
+ 5.31968369e-01, 4.94025422e-01, 4.54275131e-01,
+ 4.14517560e-01, 3.74757709e-01, 3.35000583e-01,
+ 2.95251189e-01, 2.55514533e-01, 2.15795621e-01],
+ [1.85805611e-01, 1.58245609e-01, 1.30686714e-01,
+ 1.03128926e-01, 7.55722460e-02, 4.80166757e-02,
+ 2.04622160e-02, 3.79809920e-03, 3.46310306e-03,
+ 3.10070529e-03, 2.68579661e-03, 2.19341216e-03,
+ 1.59875239e-03, 8.77203803e-04, 4.35952940e-06]],
+
+ [[1.88249866e-01, 2.05728128e-01, 2.23209861e-01,
+ 2.40695072e-01, 2.58183766e-01, 2.75675949e-01,
+ 2.93171625e-01, 3.10670801e-01, 3.32877903e-01,
+ 3.58244116e-01, 3.83638063e-01, 4.09059827e-01,
+ 4.34509485e-01, 4.59987117e-01, 4.85492795e-01],
+ [5.04317660e-01, 4.97523483e-01, 4.92879482e-01,
+ 4.90522941e-01, 4.90521579e-01, 4.92874471e-01,
+ 4.97514769e-01, 5.04314130e-01, 5.48356836e-01,
+ 6.02679755e-01, 6.57930117e-01, 7.13582394e-01,
+ 7.69129132e-01, 8.24101035e-01, 8.78084923e-01],
+ [9.05957986e-01, 9.00459829e-01, 9.01710827e-01,
+ 9.09304816e-01, 9.21567297e-01, 9.36002510e-01,
+ 9.49878533e-01, 9.60836244e-01, 9.50521017e-01,
+ 9.42321192e-01, 9.36098294e-01, 9.31447978e-01,
+ 9.27737112e-01, 9.24164130e-01, 9.19837458e-01],
+ [9.08479555e-01, 8.93119640e-01, 8.77756168e-01,
+ 8.62389039e-01, 8.47018155e-01, 8.31643415e-01,
+ 8.16264720e-01, 7.98248733e-01, 7.69688456e-01,
+ 7.41111049e-01, 7.12515170e-01, 6.83899486e-01,
+ 6.55262669e-01, 6.26603399e-01, 5.97920364e-01],
+ [5.71406981e-01, 5.45439361e-01, 5.19471340e-01,
+ 4.93502919e-01, 4.67534097e-01, 4.41564875e-01,
+ 4.15595252e-01, 3.91172349e-01, 3.69029170e-01,
+ 3.46833147e-01, 3.24591169e-01, 3.02310146e-01,
+ 2.79997004e-01, 2.57658679e-01, 2.35302110e-01]],
+
+ [[1.96102817e-02, 2.23037080e-02, 2.49835320e-02,
+ 2.76497605e-02, 3.03024001e-02, 3.29414575e-02,
+ 3.55669395e-02, 3.81788529e-02, 5.03598778e-02,
+ 6.89209657e-02, 8.74757090e-02, 1.06024973e-01,
+ 1.24569626e-01, 1.43110536e-01, 1.61648577e-01],
+ [1.82340027e-01, 2.15315774e-01, 2.53562955e-01,
+ 2.95884521e-01, 3.41038527e-01, 3.87773687e-01,
+ 4.34864157e-01, 4.81142673e-01, 5.00410360e-01,
+ 5.19991397e-01, 5.47394263e-01, 5.82556639e-01,
+ 6.25097005e-01, 6.74344521e-01, 7.29379582e-01],
+ [7.75227971e-01, 8.13001048e-01, 8.59395545e-01,
+ 9.04577146e-01, 9.40342288e-01, 9.61653621e-01,
+ 9.67479211e-01, 9.60799542e-01, 9.63421077e-01,
+ 9.66445062e-01, 9.67352042e-01, 9.63790783e-01,
+ 9.53840372e-01, 9.36234978e-01, 9.10530024e-01],
+ [8.86771441e-01, 8.67903107e-01, 8.48953980e-01,
+ 8.29924111e-01, 8.10813555e-01, 7.91622365e-01,
+ 7.72350598e-01, 7.51439565e-01, 7.24376642e-01,
+ 6.97504841e-01, 6.70822717e-01, 6.44328750e-01,
+ 6.18021348e-01, 5.91898843e-01, 5.65959492e-01],
+ [5.40017537e-01, 5.14048293e-01, 4.88079755e-01,
+ 4.62111921e-01, 4.36144791e-01, 4.10178361e-01,
+ 3.84212632e-01, 3.58028450e-01, 3.31935148e-01,
+ 3.06445966e-01, 2.81566598e-01, 2.57302099e-01,
+ 2.33656886e-01, 2.10634733e-01, 1.88238767e-01]]])
+
+ @pytest.mark.parametrize("colormap_tag", [None, "colormap"])
+ def test_colorize_geotiff_tag(self, tmp_path, colormap_tag):
+ """Test that a colorized colormap can be saved to a geotiff tag."""
+ new_range = (0.0, 0.5)
+ arr = np.arange(75).reshape(5, 15) / 74.
+ data = xr.DataArray(arr.copy(), dims=['y', 'x'])
+ new_brbg = brbg.set_range(*new_range, inplace=False)
+ img = xrimage.XRImage(data)
+ img.colorize(new_brbg)
+
+ dst = str(tmp_path / "test.tif")
+ img.save(dst, colormap_tag=colormap_tag)
+ with rio.open(dst, "r") as gtiff_file:
+ metadata = gtiff_file.tags()
+ if colormap_tag is None:
+ assert "colormap" not in metadata
+ else:
+ assert "colormap" in metadata
+ loaded_brbg = Colormap.from_file(metadata["colormap"])
+ np.testing.assert_allclose(new_brbg.values, loaded_brbg.values)
+ np.testing.assert_allclose(new_brbg.colors, loaded_brbg.colors)
+
+ @pytest.mark.parametrize(
+ ("new_range", "input_scale", "input_offset", "expected_scale", "expected_offset"),
+ [
+ ((0.0, 1.0), 1.0, 0.0, 1.0, 0.0),
+ ((0.0, 0.5), 1.0, 0.0, 2.0, 0.0),
+ ((2.0, 4.0), 2.0, 2.0, 0.5, -1.0),
+ ],
+ )
+ def test_colorize_l_rgb(self, new_range, input_scale, input_offset, expected_scale, expected_offset):
+ """Test colorize with an RGB colormap."""
+ arr = np.arange(75).reshape(5, 15) / 74. * input_scale + input_offset
+ data = xr.DataArray(arr.copy(), dims=['y', 'x'])
+ new_brbg = brbg.set_range(*new_range, inplace=False)
+ img = xrimage.XRImage(data)
+ img.colorize(new_brbg)
+ values = img.data.compute()
+
+ if new_range[1] == 0.5:
+ expected2 = self._expected.copy().reshape((3, 75))
+ flat_expected = self._expected.reshape((3, 75))
+ expected2[:, :38] = flat_expected[:, ::2]
+ expected2[:, 38:] = flat_expected[:, -1:]
+ expected = expected2.reshape((3, 5, 15))
+ else:
+ expected = self._expected
+ np.testing.assert_allclose(values, expected)
+ assert "enhancement_history" in img.data.attrs
+ assert img.data.attrs["enhancement_history"][-1]["scale"] == expected_scale
+ assert img.data.attrs["enhancement_history"][-1]["offset"] == expected_offset
+ assert isinstance(img.data.attrs["enhancement_history"][-1]["colormap"], Colormap)
+
+ 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.
+ data = xr.DataArray([arr.copy(), alpha],
+ dims=['bands', 'y', 'x'],
+ coords={'bands': ['L', 'A']})
+ img = xrimage.XRImage(data)
+ img.colorize(brbg)
+
+ values = img.data.values
+ expected = np.concatenate((self._expected,
+ alpha.reshape((1,) + alpha.shape)))
+ np.testing.assert_allclose(values, expected)
+ assert "enhancement_history" in img.data.attrs
+ assert img.data.attrs["enhancement_history"][-1]["scale"] == 1.0
+ assert img.data.attrs["enhancement_history"][-1]["offset"] == 0.0
+ assert isinstance(img.data.attrs["enhancement_history"][-1]["colormap"], Colormap)
+
+ def test_colorize_rgba(self):
+ """Test colorize with an RGBA colormap."""
+ from trollimage import xrimage
+ from trollimage.colormap import Colormap
+
+ # RGBA colormap
+ bw = Colormap(
+ (0.0, (1.0, 1.0, 1.0, 1.0)),
+ (1.0, (0.0, 0.0, 0.0, 0.5)),
+ )
+
+ arr = np.arange(75).reshape(5, 15) / 74.
+ data = xr.DataArray(arr.copy(), dims=['y', 'x'])
+ img = xrimage.XRImage(data)
+ img.colorize(bw)
+ values = img.data.compute()
+ assert (4, 5, 15) == values.shape
+ np.testing.assert_allclose(values[:, 0, 0], [1.0, 1.0, 1.0, 1.0], rtol=1e-03)
+ np.testing.assert_allclose(values[:, -1, -1], [0.0, 0.0, 0.0, 0.5])
+ assert "enhancement_history" in img.data.attrs
+ assert img.data.attrs["enhancement_history"][-1]["scale"] == 1.0
+ assert img.data.attrs["enhancement_history"][-1]["offset"] == 0.0
+ assert isinstance(img.data.attrs["enhancement_history"][-1]["colormap"], Colormap)
+
+
+class TestXRImagePalettize:
+ """Test the XRImage palettize method."""
+
+ @pytest.mark.parametrize(
+ ("new_range", "input_scale", "input_offset"),
+ [
+ ((0.0, 1.0), 1.0, 0.0),
+ ((0.0, 0.5), 1.0, 0.0),
+ ((2.0, 4.0), 2.0, 2.0),
+ ],
+ )
+ def test_palettize(self, new_range, input_scale, input_offset):
+ """Test palettize with an RGB colormap."""
+ from trollimage import xrimage
+ from trollimage.colormap import brbg
+
+ arr = np.arange(75).reshape(5, 15) / 74. * input_scale + input_offset
+ data = xr.DataArray(arr.copy(), dims=['y', 'x'])
+ img = xrimage.XRImage(data)
+ new_brbg = brbg.set_range(*new_range, inplace=False)
+ img.palettize(new_brbg)
+
+ values = img.data.values
+ expected = np.array([[
+ [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
+ [2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3],
+ [4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5],
+ [6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7],
+ [8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10]]])
+ if new_range[1] == 0.5:
+ flat_expected = expected.reshape((1, 75))
+ expected2 = flat_expected.copy()
+ expected2[:, :38] = flat_expected[:, ::2]
+ expected2[:, 38:] = flat_expected[:, -1:]
+ expected = expected2.reshape((1, 5, 15))
+ np.testing.assert_allclose(values, expected)
+ assert "enhancement_history" in img.data.attrs
+ assert img.data.attrs["enhancement_history"][-1]["scale"] == 0.1
+ assert img.data.attrs["enhancement_history"][-1]["offset"] == 0.0
+
+ def test_palettize_rgba(self):
+ """Test palettize with an RGBA colormap."""
+ from trollimage import xrimage
+ from trollimage.colormap import Colormap
+
+ # RGBA colormap
+ bw = Colormap(
+ (0.0, (1.0, 1.0, 1.0, 1.0)),
+ (1.0, (0.0, 0.0, 0.0, 0.5)),
+ )
+
+ arr = np.arange(75).reshape(5, 15) / 74.
+ data = xr.DataArray(arr.copy(), dims=['y', 'x'])
+ img = xrimage.XRImage(data)
+ img.palettize(bw)
+
+ values = img.data.values
+ assert (1, 5, 15) == values.shape
+ assert (2, 4) == bw.colors.shape
+
+
class TestXRImageSaveScaleOffset(unittest.TestCase):
"""Test case for saving an image with scale and offset tags."""
def setUp(self) -> None:
"""Set up the test case."""
- import xarray as xr
from trollimage import xrimage
data = xr.DataArray(np.arange(25).reshape(5, 5, 1), dims=[
'y', 'x', 'bands'], coords={'bands': ['L']})
=====================================
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.17.0)"
- git_full = "cf4dd6eeb6a5151caf83ba5043aa4cd739666171"
- git_date = "2021-12-07 14:59:27 -0600"
+ git_refnames = " (HEAD -> main, tag: v1.18.0)"
+ git_full = "b5e69f5a2055f3f5f4c9e23305cbdc91171ed7ec"
+ git_date = "2022-02-24 09:00:27 -0600"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
=====================================
trollimage/xrimage.py
=====================================
@@ -459,6 +459,7 @@ class XRImage(object):
overviews_minsize=256, overviews_resampling=None,
include_scale_offset_tags=False,
scale_offset_tags=None,
+ colormap_tag=None,
driver=None,
**format_kwargs):
"""Save the image using rasterio.
@@ -502,10 +503,16 @@ class XRImage(object):
provided. Common values include `nearest` (default),
`bilinear`, `average`, and many others. See the rasterio
documentation for more information.
- scale_offset_tags (Tuple[str, str] or None)
+ scale_offset_tags (Tuple[str, str] or None):
If set to a ``(str, str)`` tuple, scale and offset will be
stored in GDALMetaData tags. Those can then be used to
retrieve the original data values from pixel values.
+ colormap_tag (str or None):
+ If set and the image was colorized or palettized, a tag will
+ be added with this name with the value of a comma-separated
+ version of the Colormap that was used. See
+ :meth:`trollimage.colormap.Colormap.to_csv` for more
+ information.
Returns:
The delayed or computed result of the saving.
@@ -588,6 +595,8 @@ class XRImage(object):
elif driver == 'JPEG' and 'A' in mode:
raise ValueError('JPEG does not support alpha')
+ if colormap_tag and "colormap" in data.attrs.get('enhancement_history', [{}])[-2]:
+ tags[colormap_tag] = data.attrs['enhancement_history'][-2]['colormap'].to_csv()
if scale_offset_tags:
scale_label, offset_label = scale_offset_tags
scale, offset = self.get_scaling_from_history(data.attrs.get('enhancement_history', []))
@@ -1500,6 +1509,15 @@ class XRImage(object):
attrs = self.data.attrs
dims = self.data.dims
self.data = xr.DataArray(new_data, coords=coords, attrs=attrs, dims=dims)
+ cmap_min = colormap.values[0]
+ cmap_max = colormap.values[-1]
+ scale_factor = 1.0 / (cmap_max - cmap_min)
+ offset = -cmap_min * scale_factor
+ self.data.attrs.setdefault('enhancement_history', []).append({
+ 'scale': scale_factor,
+ 'offset': offset,
+ 'colormap': colormap,
+ })
def palettize(self, colormap):
"""Palettize the current image using ``colormap``.
@@ -1537,6 +1555,17 @@ class XRImage(object):
self.data.data = new_data
self.data.coords['bands'] = list(mode)
+ # the data values are now indexes into the palette array of colors
+ # so it can't be more than the maximum index (len - 1)
+ palette_num_values = len(self.palette) - 1
+ scale_factor = 1.0 / palette_num_values
+ offset = 0
+
+ self.data.attrs.setdefault('enhancement_history', []).append({
+ 'scale': scale_factor,
+ 'offset': offset,
+ 'colormap': colormap,
+ })
def blend(self, src):
r"""Alpha blend *src* on top of the current image.
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/commit/675756ccdb4995dd8f58f138143d79b81db53f67
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/commit/675756ccdb4995dd8f58f138143d79b81db53f67
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/20220225/4abfb793/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list