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

Antonio Valentino gitlab at salsa.debian.org
Mon Oct 28 07:15:19 GMT 2019



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


Commits:
54d7955b by Antonio Valentino at 2019-10-28T06:53:00Z
New upstream version 1.11.0
- - - - -


6 changed files:

- .travis.yml
- CHANGELOG.md
- appveyor.yml
- trollimage/tests/test_image.py
- trollimage/version.py
- trollimage/xrimage.py


Changes:

=====================================
.travis.yml
=====================================
@@ -1,7 +1,4 @@
 language: python
-python:
-- '2.7'
-- '3.6'
 env:
     global:
         # Set defaults to avoid repeating in most cases
@@ -15,8 +12,19 @@ env:
         - CONDA_CHANNELS='conda-forge'
         - CONDA_CHANNEL_PRIORITY=True
 
+matrix:
+  include:
+  - env:
+    - PYTHON_VERSION=2.7
+    - NUMPY_VERSION=1.16
+    os: linux
+  - env: PYTHON_VERSION=3.6
+    os: linux
+  - env: PYTHON_VERSION=3.7
+    os: linux
+
 install:
-    - git clone --depth 1 -b all-the-fixes git://github.com/djhoese/ci-helpers.git
+    - git clone --depth 1 git://github.com/astropy/ci-helpers.git
     - source ci-helpers/travis/setup_conda.sh
 script: coverage run --source=trollimage setup.py test
 after_success:


=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,20 @@
+## Version 1.11.0 (2019/10/24)
+
+### Pull Requests Merged
+
+#### Bugs fixed
+
+* [PR 58](https://github.com/pytroll/trollimage/pull/58) - Make tags containing values to compute use store for saving
+
+#### Features added
+
+* [PR 60](https://github.com/pytroll/trollimage/pull/60) - Add tests on py 3.7
+* [PR 59](https://github.com/pytroll/trollimage/pull/59) - Add scale and offset inclusion utility when rio saving
+* [PR 57](https://github.com/pytroll/trollimage/pull/57) - Add the `apply_pil` method
+
+In this release 4 pull requests were closed.
+
+
 ## Version 1.10.1 (2019/09/26)
 
 ### Pull Requests Merged


=====================================
appveyor.yml
=====================================
@@ -18,9 +18,13 @@ environment:
       PYTHON_ARCH: "64"
       NUMPY_VERSION: "stable"
 
+    - PYTHON: "C:\\Python37_64"
+      PYTHON_VERSION: "3.7"
+      PYTHON_ARCH: "64"
+      NUMPY_VERSION: "stable"
+
 install:
-#    - "git clone --depth 1 git://github.com/astropy/ci-helpers.git"
-    - "git clone --depth 1 -b all-the-fixes git://github.com/djhoese/ci-helpers.git"
+    - "git clone --depth 1 git://github.com/astropy/ci-helpers.git"
     - "powershell ci-helpers/appveyor/install-miniconda.ps1"
     - "conda activate test"
 


=====================================
trollimage/tests/test_image.py
=====================================
@@ -23,12 +23,15 @@
 # along with mpop.  If not, see <http://www.gnu.org/licenses/>.
 """Module for testing the image and xrimage modules."""
 import os
-import sys
 import random
-import unittest
+import sys
 import tempfile
+import unittest
+from collections import OrderedDict
 from tempfile import NamedTemporaryFile
+
 import numpy as np
+
 from trollimage import image
 
 try:
@@ -958,7 +961,7 @@ class TestXRImage(unittest.TestCase):
             delay = img.save(tmp.name, compute=False)
             self.assertIsInstance(delay, tuple)
             self.assertIsInstance(delay[0], da.Array)
-            self.assertIsInstance(delay[1], xrimage.RIOFile)
+            self.assertIsInstance(delay[1], xrimage.RIODataset)
             da.store(*delay)
             delay[1].close()
 
@@ -1031,7 +1034,7 @@ class TestXRImage(unittest.TestCase):
             delay = img.save(tmp.name, compute=False)
             self.assertIsInstance(delay, tuple)
             self.assertIsInstance(delay[0], da.Array)
-            self.assertIsInstance(delay[1], xrimage.RIOFile)
+            self.assertIsInstance(delay[1], xrimage.RIODataset)
             da.store(*delay)
             delay[1].close()
 
@@ -1198,6 +1201,44 @@ class TestXRImage(unittest.TestCase):
             with rio.open(tmp.name) as f:
                 self.assertEqual(len(f.overviews(1)), 2)
 
+    @unittest.skipIf(sys.platform.startswith('win'), "'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=[
+            'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
+        img = xrimage.XRImage(data)
+        tags = {'avg': img.data.mean(), 'current_song': 'disco inferno'}
+        self.assertTrue(np.issubdtype(img.data.dtype, np.integer))
+        with NamedTemporaryFile(suffix='.tif') as tmp:
+            img.save(tmp.name, tags=tags)
+            tags['avg'] = '37.0'
+            with rio.open(tmp.name) as f:
+                self.assertEqual(f.tags(), tags)
+
+    @unittest.skipIf(sys.platform.startswith('win'), "'NamedTemporaryFile' not supported on Windows")
+    def test_save_scale_offset(self):
+        """Test saving geotiffs with tags."""
+        import xarray as xr
+        from trollimage import xrimage
+        import rasterio as rio
+
+        data = xr.DataArray(np.arange(25).reshape(5, 5, 1), dims=[
+            'y', 'x', 'bands'], coords={'bands': ['L']})
+        img = xrimage.XRImage(data)
+        img.stretch()
+        with NamedTemporaryFile(suffix='.tif') as tmp:
+            img.save(tmp.name, include_scale_offset_tags=True)
+            tags = {'scale': 24.0 / 255, 'offset': 0}
+            with rio.open(tmp.name) as f:
+                ftags = f.tags()
+                for key, val in tags.items():
+                    self.assertAlmostEqual(float(ftags[key]), val)
+
     def test_gamma(self):
         """Test gamma correction."""
         import xarray as xr
@@ -1612,6 +1653,18 @@ class TestXRImage(unittest.TestCase):
             self.assertTrue(img2.mode == 'RGBA')
             self.assertTrue(len(img2.data.coords['bands']) == 4)
 
+    def test_final_mode(self):
+        """Test final_mode."""
+        import xarray as xr
+        from trollimage import xrimage
+
+        # numpy array image
+        data = xr.DataArray(np.arange(75).reshape(5, 5, 3), dims=[
+            'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
+        img = xrimage.XRImage(data)
+        self.assertEqual(img.final_mode(None), 'RGBA')
+        self.assertEqual(img.final_mode(0), 'RGB')
+
     def test_colorize(self):
         """Test colorize with an RGB colormap."""
         import xarray as xr
@@ -1875,6 +1928,42 @@ class TestXRImage(unittest.TestCase):
             img.show()
             s.assert_called_once()
 
+    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.
+        data = xr.DataArray(np_data, dims=[
+            'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
+
+        dummy_args = [(OrderedDict(), ), {}]
+
+        def dummy_fun(pil_obj, *args, **kwargs):
+            dummy_args[0] = args
+            dummy_args[1] = kwargs
+            return pil_obj
+
+        img = xrimage.XRImage(data)
+        pi = mock.MagicMock()
+        img.pil_image = pi
+        res = img.apply_pil(dummy_fun, 'RGB')
+        # check that the pil image generation is delayed
+        pi.assert_not_called()
+        # make it happen
+        res.data.data.compute()
+        pi.return_value.convert.assert_called_with('RGB')
+
+        img = xrimage.XRImage(data)
+        pi = mock.MagicMock()
+        img.pil_image = pi
+        res = img.apply_pil(dummy_fun, 'RGB',
+                            fun_args=('Hey', 'Jude'),
+                            fun_kwargs={'chorus': "La lala lalalala"})
+        self.assertEqual(dummy_args, [({}, ), {}])
+        res.data.data.compute()
+        self.assertEqual(dummy_args, [(OrderedDict(), 'Hey', 'Jude'), {'chorus': "La lala lalalala"}])
+
 
 def suite():
     """Create the suite for test_image."""


=====================================
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 -> master, tag: v1.10.1)"
-    git_full = "9130e96fae8e880ccf843298b508712a4d80a481"
-    git_date = "2019-09-26 15:08:59 -0500"
+    git_refnames = " (HEAD -> master, tag: v1.11.0)"
+    git_full = "103b269144d33bbcf106c8afa20de95618d5a890"
+    git_date = "2019-10-24 20:12:03 +0200"
     keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
     return keywords
 


=====================================
trollimage/xrimage.py
=====================================
@@ -34,13 +34,14 @@ chunks can be saved in parallel.
 
 import logging
 import os
+import threading
 
 import numpy as np
 from PIL import Image as PILImage
 import xarray as xr
-import xarray.ufuncs as xu
 import dask
 import dask.array as da
+from dask.delayed import delayed
 
 from trollimage.image import check_image_format
 
@@ -71,47 +72,24 @@ class RIOFile(object):
         self.mode = mode
         self.kwargs = kwargs
         self.rfile = None
-        self._closed = True
-        self.overviews = kwargs.pop('overviews', None)
+        self.lock = threading.Lock()
 
-    def __setitem__(self, key, item):
-        """Put the data chunk in the image."""
-        if len(key) == 3:
-            indexes = list(range(
-                key[0].start + 1,
-                key[0].stop + 1,
-                key[0].step or 1
-            ))
-            y = key[1]
-            x = key[2]
-        else:
-            indexes = 1
-            y = key[0]
-            x = key[1]
-        chy_off = y.start
-        chy = y.stop - y.start
-        chx_off = x.start
-        chx = x.stop - x.start
-
-        # band indexes
-        self.rfile.write(item, window=Window(chx_off, chy_off, chx, chy),
-                         indexes=indexes)
+    @property
+    def closed(self):
+        """Check if the file is closed."""
+        return self.rfile is None or self.rfile.closed
 
     def open(self, mode=None):
         """Open the file."""
         mode = mode or self.mode
-        if self._closed:
+        if self.closed:
             self.rfile = rasterio.open(self.path, mode, **self.kwargs)
-            self._closed = False
 
     def close(self):
         """Close the file."""
-        if not self._closed:
-            if self.overviews:
-                logger.debug('Building overviews %s', str(self.overviews))
-                self.rfile.build_overviews(self.overviews)
-            self.rfile.close()
-            self._closed = True
+        with self.lock:
+            if not self.closed:
+                self.rfile.close()
 
     def __enter__(self):
         """Enter method."""
@@ -142,6 +120,82 @@ class RIOFile(object):
         else:
             self.rfile.colorinterp = val
 
+    def write(self, *args, **kwargs):
+        """Write to the file."""
+        with self.lock:
+            self.open('a')
+            return self.rfile.write(*args, **kwargs)
+
+    def build_overviews(self, *args, **kwargs):
+        """Write overviews."""
+        with self.lock:
+            self.open('a')
+            return self.rfile.build_overviews(*args, **kwargs)
+
+    def update_tags(self, *args, **kwargs):
+        """Update tags."""
+        with self.lock:
+            self.open('a')
+            return self.rfile.update_tags(*args, **kwargs)
+
+
+class RIOTag:
+    """Rasterio wrapper to allow da.store on tag."""
+
+    def __init__(self, rfile, name):
+        """Init the rasterio tag."""
+        self.rfile = rfile
+        self.name = name
+
+    def __setitem__(self, key, item):
+        """Put the data in the tag."""
+        kwargs = {self.name: item.item()}
+        self.rfile.update_tags(**kwargs)
+
+    def close(self):
+        """Close the file."""
+        return self.rfile.close()
+
+
+class RIODataset:
+    """A wrapper for a rasterio dataset."""
+
+    def __init__(self, rfile, overviews=None):
+        """Init the rasterio dataset."""
+        self.rfile = rfile
+        self.overviews = overviews
+
+    def __setitem__(self, key, item):
+        """Put the data chunk in the image."""
+        if len(key) == 3:
+            indexes = list(range(
+                key[0].start + 1,
+                key[0].stop + 1,
+                key[0].step or 1
+            ))
+            y = key[1]
+            x = key[2]
+        else:
+            indexes = 1
+            y = key[0]
+            x = key[1]
+        chy_off = y.start
+        chy = y.stop - y.start
+        chx_off = x.start
+        chx = x.stop - x.start
+
+        # band indexes
+        self.rfile.write(item, window=Window(chx_off, chy_off, chx, chy),
+                         indexes=indexes)
+
+    def close(self):
+        """Close the file."""
+        if self.overviews:
+            logger.debug('Building overviews %s', str(self.overviews))
+            self.rfile.build_overviews(self.overviews)
+
+        return self.rfile.close()
+
 
 def color_interp(data):
     """Get the color interpretation for this image."""
@@ -170,6 +224,45 @@ def color_interp(data):
         return [colors[band] for band in data['bands'].values]
 
 
+def combine_scales_offsets(*args):
+    """Combine ``(scale, offset)`` tuples in one, considering they are applied from left to right.
+
+    For example, if we have our base data called ```orig_data`` and apply to it
+    ``(scale_1, offset_1)``, then ``(scale_2, offset_2)`` such that::
+
+      data_1 = orig_data * scale_1 + offset_1
+      data_2 = data_1 * scale_2 + offset_2
+
+    this function will return the tuple ``(scale, offset)`` such that::
+
+      data_2 = orig_data * scale + offset
+
+    given the arguments ``(scale_1, offset_1), (scale_2, offset_2)``.
+
+    """
+    cscale = 1
+    coffset = 0
+    for scale, offset in args:
+        cscale *= scale
+        coffset = coffset * scale + offset
+    return cscale, coffset
+
+
+def invert_scale_offset(scale, offset):
+    """Invert scale and offset to allow reverse transformation.
+
+    Ie, it will return ``rscale, roffset`` such that::
+
+      orig_data = rscale * data + roffset
+
+    if::
+
+      data = scale * orig_data + offset
+
+    """
+    return 1 / scale, -offset / scale
+
+
 class XRImage(object):
     """Image class using an :class:`xarray.DataArray` as internal storage.
 
@@ -287,13 +380,36 @@ class XRImage(object):
 
     def rio_save(self, filename, fformat=None, fill_value=None,
                  dtype=np.uint8, compute=True, tags=None,
-                 keep_palette=False, cmap=None,
+                 keep_palette=False, cmap=None, overviews=None,
+                 include_scale_offset_tags=False,
                  **format_kwargs):
         """Save the image using rasterio.
 
-        Overviews can be added to the file using the `overviews` kwarg, eg::
+        Args:
+            filename (string): The filename to save to.
+            fformat (string): The format to save to. If not specified (default),
+                it will be infered from the file extension.
+            fill_value (number): The value to fill the missing data with.
+                Default is ``None``, translating to trying to keep the data
+                transparent.
+            dtype (np.dtype): The type to save the data to. Defaults to
+                np.uint8.
+            compute (bool): Whether (default) or not to compute the lazy data.
+            tags (dict): Tags to include in the file.
+            keep_palette (bool): Whether or not (default) to keep the image in
+                P mode.
+            cmap (colormap): The colormap to use for the data.
+            overviews (list): The reduction factors of the overviews to include
+                in the image, eg::
+
+                    img.rio_save('myfile.tif', overviews=[2, 4, 8, 16])
+
+            include_scale_offset_tags (bool): Whether or not (default) to
+                include a ``scale`` and an ``offset`` tag in the data that would
+                help retrieving original data values from pixel values.
 
-          img.rio_save('myfile.tif', overviews=[2, 4, 8, 16])
+        Returns:
+            The delayed or computed result of the saving.
 
         """
         fformat = fformat or os.path.splitext(filename)[1][1:]
@@ -309,7 +425,6 @@ class XRImage(object):
         data, mode = self.finalize(fill_value, dtype=dtype,
                                    keep_palette=keep_palette, cmap=cmap)
         data = data.transpose('bands', 'y', 'x')
-        data.attrs = self.data.attrs
 
         crs = None
         gcps = None
@@ -361,6 +476,10 @@ class XRImage(object):
         elif driver == 'JPEG' and 'A' in mode:
             raise ValueError('JPEG does not support alpha')
 
+        if include_scale_offset_tags:
+            scale, offset = self.get_scaling_from_history(data.attrs.get('enhancement_history', []))
+            tags['scale'], tags['offset'] = invert_scale_offset(scale, offset)
+
         # FIXME add metadata
         r_file = RIOFile(filename, 'w', driver=driver,
                          width=data.sizes['x'], height=data.sizes['y'],
@@ -374,7 +493,6 @@ class XRImage(object):
         r_file.open()
         if not keep_palette:
             r_file.colorinterp = color_interp(data)
-        r_file.rfile.update_tags(**tags)
 
         if keep_palette and cmap is not None:
             if data.dtype != 'uint8':
@@ -386,15 +504,35 @@ class XRImage(object):
             except AttributeError:
                 raise ValueError("Colormap is not formatted correctly")
 
+        da_tags = []
+        for key, val in list(tags.items()):
+            try:
+                if isinstance(val.data, da.Array):
+                    da_tags.append((val.data, RIOTag(r_file, key)))
+                    tags.pop(key)
+            except AttributeError:
+                continue
+
+        r_file.rfile.update_tags(**tags)
+        r_dataset = RIODataset(r_file, overviews)
+
+        to_store = (data.data, r_dataset)
+        if da_tags:
+            to_store = list(zip(*([to_store] + da_tags)))
+
         if compute:
             # write data to the file now
-            res = da.store(data.data, r_file)
-            r_file.close()
+            res = da.store(*to_store)
+            to_close = to_store[1]
+            if not isinstance(to_close, tuple):
+                to_close = [to_close]
+            for item in to_close:
+                item.close()
             return res
         # provide the data object and the opened file so the caller can
         # store them when they would like. Caller is responsible for
         # closing the file
-        return data.data, r_file
+        return to_store
 
     def pil_save(self, filename, fformat=None, fill_value=None,
                  compute=True, **format_kwargs):
@@ -417,6 +555,62 @@ class XRImage(object):
             return delay.compute()
         return delay
 
+    def get_scaling_from_history(self, history=None):
+        """Merge the scales and offsets from the history.
+
+        If ``history`` isn't provided, the history of the current image will be
+        used.
+        """
+        if history is None:
+            history = self.data.attrs.get('enhancement_history', [])
+        try:
+            scaling = [(item['scale'], item['offset']) for item in history]
+        except KeyError as err:
+            raise NotImplementedError('Can only get combine scaling from a list of scaling operations: %s' % str(err))
+        return combine_scales_offsets(*scaling)
+
+    @delayed(nout=1, pure=True)
+    def _delayed_apply_pil(self, fun, pil_args, pil_kwargs, fun_args, fun_kwargs,
+                           image_metadata=None, output_mode=None):
+        if pil_args is None:
+            pil_args = tuple()
+        if pil_kwargs is None:
+            pil_kwargs = dict()
+        if fun_args is None:
+            fun_args = tuple()
+        if fun_kwargs is None:
+            fun_kwargs = dict()
+        if image_metadata is None:
+            image_metadata = dict()
+        new_img = fun(self.pil_image(*pil_args, **pil_kwargs), image_metadata, *fun_args, **fun_kwargs)
+        if output_mode is not None:
+            new_img = new_img.convert(output_mode)
+        return np.array(new_img) / self.data.dtype.type(255.0)
+
+    def apply_pil(self, fun, output_mode, pil_args=None, pil_kwargs=None, fun_args=None, fun_kwargs=None):
+        """Apply a function `fun` on the pillow image corresponding to the instance of the XRImage.
+
+        The function shall take a pil image as first argument, and is then passed fun_args and fun_kwargs.
+        In addition, the current images's metadata is passed as a keyword argument called `image_mda`.
+        It is expected to return the modified pil image.
+        This function returns a new XRImage instance with the modified image data.
+
+        The pil_args and pil_kwargs are passed to the `pil_image` method of the XRImage instance.
+
+        """
+        new_array = self._delayed_apply_pil(fun, pil_args, pil_kwargs, fun_args, fun_kwargs,
+                                            self.data.attrs, output_mode)
+        bands = len(output_mode)
+        arr = da.from_delayed(new_array, dtype=self.data.dtype,
+                              shape=(self.data.sizes['y'], self.data.sizes['x'], bands))
+
+        new_data = xr.DataArray(arr, dims=['y', 'x', 'bands'],
+                                coords={'y': self.data.coords['y'],
+                                        'x': self.data.coords['x'],
+                                        'bands': list(output_mode)},
+                                attrs=self.data.attrs)
+        return XRImage(new_data)
+
     def _pngmeta(self):
         """Return GeoImage.tags as a PNG metadata object.
 
@@ -485,7 +679,9 @@ class XRImage(object):
         if np.issubdtype(data.dtype, np.integer):
             # xarray sometimes upcasts this calculation, so cast again
             null_mask = self._scale_to_dtype(null_mask, data.dtype).astype(data.dtype)
+        attrs = data.attrs.copy()
         data = xr.concat([data, null_mask], dim="bands")
+        data.attrs = attrs
         return data
 
     def _scale_to_dtype(self, data, dtype):
@@ -497,6 +693,7 @@ class XRImage(object):
         be in the 0-1 range already.
 
         """
+        attrs = data.attrs.copy()
         if np.issubdtype(dtype, np.integer):
             if np.issubdtype(data, np.integer):
                 # preserve integer data type
@@ -504,8 +701,12 @@ class XRImage(object):
             else:
                 # scale float data (assumed to be 0 to 1) to full integer space
                 dinfo = np.iinfo(dtype)
-                data = data.clip(0, 1) * (dinfo.max - dinfo.min) + dinfo.min
+                scale = dinfo.max - dinfo.min
+                offset = dinfo.min
+                data = data.clip(0, 1) * scale + offset
+                attrs.setdefault('enhancement_history', list()).append({'scale': scale, 'offset': offset})
             data = data.round()
+        data.attrs = attrs
         return data
 
     def _check_modes(self, modes):
@@ -616,6 +817,13 @@ class XRImage(object):
                       DeprecationWarning)
         return self.finalize(fill_value, dtype, keep_palette, cmap)
 
+    def final_mode(self, fill_value=None):
+        """Get the mode of the finalized image when provided this fill_value."""
+        if fill_value is None and not self.mode.endswith('A'):
+            return self.mode + 'A'
+        else:
+            return self.mode
+
     def finalize(self, fill_value=None, dtype=np.uint8, keep_palette=False, cmap=None):
         """Finalize the image to be written to an output file.
 
@@ -645,31 +853,42 @@ class XRImage(object):
                            "setting fill_value to 0")
             fill_value = 0
 
-        final_data = self.data
+        final_data = self.data.copy()
+        try:
+            final_data.attrs['enhancement_history'] = list(self.data.attrs['enhancement_history'])
+        except KeyError:
+            pass
+        attrs = final_data.attrs
         # if the data are integers then this fill value will be used to check for invalid values
-        ifill = final_data.attrs.get('_FillValue') if np.issubdtype(final_data, np.integer) else None
-        if not keep_palette:
-            if fill_value is None and not self.mode.endswith('A'):
-                # We don't have a fill value or an alpha, let's add an alpha
-                alpha = self._create_alpha(final_data, fill_value=ifill)
-                final_data = self._scale_to_dtype(final_data, dtype).astype(dtype)
-                final_data = self._add_alpha(final_data, alpha=alpha)
-            else:
-                # scale float data to the proper dtype
-                # this method doesn't cast yet so that we can keep track of NULL values
-                final_data = self._scale_to_dtype(final_data, dtype)
-                # Add fill_value after all other calculations have been done to
-                # make sure it is not scaled for the data type
-                if ifill is not None and fill_value is not None:
-                    # cast fill value to output type so we don't change data type
-                    fill_value = dtype(fill_value)
-                    # integer fields have special fill values
-                    final_data = final_data.where(final_data != ifill, dtype(fill_value))
-                elif fill_value is not None:
-                    final_data = final_data.fillna(dtype(fill_value))
-
-        final_data = final_data.astype(dtype)
-        final_data.attrs = self.data.attrs
+        with xr.set_options(keep_attrs=True):
+            ifill = final_data.attrs.get('_FillValue') if np.issubdtype(final_data, np.integer) else None
+            if not keep_palette:
+                if fill_value is None and not self.mode.endswith('A'):
+                    # We don't have a fill value or an alpha, let's add an alpha
+                    alpha = self._create_alpha(final_data, fill_value=ifill)
+                    final_data = self._scale_to_dtype(final_data, dtype)
+                    attrs = final_data.attrs
+                    final_data = final_data.astype(dtype)
+                    final_data = self._add_alpha(final_data, alpha=alpha)
+                    final_data.attrs = attrs
+                else:
+                    # scale float data to the proper dtype
+                    # this method doesn't cast yet so that we can keep track of NULL values
+                    final_data = self._scale_to_dtype(final_data, dtype)
+                    attrs = final_data.attrs
+                    # Add fill_value after all other calculations have been done to
+                    # make sure it is not scaled for the data type
+                    if ifill is not None and fill_value is not None:
+                        # cast fill value to output type so we don't change data type
+                        fill_value = dtype(fill_value)
+                        # integer fields have special fill values
+                        final_data = final_data.where(final_data != ifill, dtype(fill_value))
+                    elif fill_value is not None:
+                        final_data = final_data.fillna(dtype(fill_value))
+
+            final_data = final_data.astype(dtype)
+            final_data.attrs = attrs
+
         return final_data, ''.join(final_data['bands'].values)
 
     def pil_image(self, fill_value=None, compute=True):
@@ -929,7 +1148,7 @@ class XRImage(object):
 
         """
         attrs = self.data.attrs
-        self.data = k * xu.log(self.data / s0)
+        self.data = k * np.log(self.data / s0)
         self.data.attrs = attrs
         self.data.attrs.setdefault('enhancement_history', []).append({'weber_fechner': (k, s0)})
 



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

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/commit/54d7955b35df379cc486e39a76882e332445c14d
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/20191028/42076587/attachment-0001.html>


More information about the Pkg-grass-devel mailing list