[Git][debian-gis-team/trollimage][master] 5 commits: New upstream version 1.17.0

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Sat Dec 11 14:22:41 GMT 2021



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


Commits:
eeaed2d5 by Antonio Valentino at 2021-12-11T14:12:09+00:00
New upstream version 1.17.0
- - - - -
60b4a699 by Antonio Valentino at 2021-12-11T14:12:17+00:00
Update upstream source from tag 'upstream/1.17.0'

Update to upstream version '1.17.0'
with Debian dir 410d6997c012db37cbdd33e4a5fa04598781f8ed
- - - - -
59d07df4 by Antonio Valentino at 2021-12-11T14:13:37+00:00
New usptream release

- - - - -
1e2fff07 by Antonio Valentino at 2021-12-11T14:16:26+00:00
Refresh all patches

- - - - -
3b3c74f8 by Antonio Valentino at 2021-12-11T14:17:29+00:00
Set diatribution to unstable

- - - - -


7 changed files:

- CHANGELOG.md
- debian/changelog
- debian/patches/0001-No-display.patch
- trollimage/tests/test_image.py
- + trollimage/tests/utils.py
- trollimage/version.py
- trollimage/xrimage.py


Changes:

=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,21 @@
+## Version 1.17.0 (2021/12/07)
+
+### Issues Closed
+
+* [Issue 93](https://github.com/pytroll/trollimage/issues/93) - Add support for Cloud Optimized GeoTIFF ([PR 94](https://github.com/pytroll/trollimage/pull/94) by [@howff](https://github.com/howff))
+
+In this release 1 issue was closed.
+
+### Pull Requests Merged
+
+#### Features added
+
+* [PR 97](https://github.com/pytroll/trollimage/pull/97) - Improve 'log' stretching with static limits and choosing log base
+* [PR 94](https://github.com/pytroll/trollimage/pull/94) - Use COG driver to write cloud optimized GeoTIFF ([93](https://github.com/pytroll/trollimage/issues/93))
+
+In this release 2 pull requests were closed.
+
+
 ## Version 1.16.1 (2021/11/17)
 
 ### Pull Requests Merged


=====================================
debian/changelog
=====================================
@@ -1,3 +1,11 @@
+trollimage (1.17.0-1) unstable; urgency=medium
+
+  * New upstream release.
+  * debian/patches:
+    - refresh all patches.
+
+ -- Antonio Valentino <antonio.valentino at tiscali.it>  Sat, 11 Dec 2021 14:17:05 +0000
+
 trollimage (1.16.1-1) unstable; urgency=medium
 
   * New upstream releae.


=====================================
debian/patches/0001-No-display.patch
=====================================
@@ -8,10 +8,10 @@ Skip tests that require display.
  1 file changed, 1 insertion(+)
 
 diff --git a/trollimage/tests/test_image.py b/trollimage/tests/test_image.py
-index daa81b9..b945293 100644
+index 467e135..260d52c 100644
 --- a/trollimage/tests/test_image.py
 +++ b/trollimage/tests/test_image.py
-@@ -2070,6 +2070,7 @@ class TestXRImage:
+@@ -2099,6 +2099,7 @@ class TestXRImage:
          """Test putalpha."""
          pass
  


=====================================
trollimage/tests/test_image.py
=====================================
@@ -1,26 +1,21 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
-# Copyright (c) 2009-2015, 2017.
-
-# Author(s):
-
-#   Martin Raspaud <martin.raspaud at smhi.se>
-#   Adam Dybbroe <adam.dybbroe at smhi.se>
-
-# This file is part of mpop.
-
-# mpop is free software: you can redistribute it and/or modify it
+# Copyright (c) 2009-2021 trollimage developers
+#
+# This file is part of trollimage.
+#
+# trollimage is free software: you can redistribute it and/or modify it
 # under the terms of the GNU General Public License as published by
 # the Free Software Foundation, either version 3 of the License, or
 # (at your option) any later version.
-
-# mpop is distributed in the hope that it will be useful, but
+#
+# trollimage is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
-
+#
 # You should have received a copy of the GNU General Public License
-# along with mpop.  If not, see <http://www.gnu.org/licenses/>.
+# along with trollimage.  If not, see <http://www.gnu.org/licenses/>.
 """Module for testing the image and xrimage modules."""
 import os
 import random
@@ -1332,6 +1327,27 @@ class TestXRImage:
             np.testing.assert_allclose(file_data[2], exp[:, :, 2])
             np.testing.assert_allclose(file_data[3], 255)
 
+    @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
+        if rio.__gdal_version__ >= '3.1':
+            data = xr.DataArray(np.arange(1200*1200*3).reshape(1200, 1200, 3), dims=[
+                'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
+            img = xrimage.XRImage(data)
+            assert np.issubdtype(img.data.dtype, np.integer)
+            with NamedTemporaryFile(suffix='.tif') as tmp:
+                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 len(f.overviews(1)) == 2
+
     @pytest.mark.skipif(sys.platform.startswith('win'), reason="'NamedTemporaryFile' not supported on Windows")
     def test_save_overviews(self):
         """Test saving geotiffs with overviews."""
@@ -1549,16 +1565,29 @@ class TestXRImage:
 
         np.testing.assert_allclose(img.data.values, res, atol=1.e-6)
 
-    def test_logarithmic_stretch(self):
+    @pytest.mark.parametrize(
+        ("min_stretch", "max_stretch"),
+        [
+            (None, None),
+            ([0.0, 1.0 / 74.0, 2.0 / 74.0], [72.0 / 74.0, 73.0 / 74.0, 1.0]),
+        ]
+    )
+    @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
 
         arr = np.arange(75).reshape(5, 5, 3) / 74.
         data = xr.DataArray(arr.copy(), dims=['y', 'x', 'bands'],
                             coords={'bands': ['R', 'G', 'B']})
-        img = xrimage.XRImage(data)
-        img.stretch(stretch='logarithmic')
+        with assert_maximum_dask_computes(0):
+            img = xrimage.XRImage(data)
+            img.stretch(stretch='logarithmic',
+                        min_stretch=min_stretch,
+                        max_stretch=max_stretch,
+                        base=base)
         enhs = img.data.attrs['enhancement_history'][0]
         assert enhs == {'log_factor': 100.0}
         res = np.array([[[0., 0., 0.],


=====================================
trollimage/tests/utils.py
=====================================
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 trollimage developers
+#
+# This file is part of trollimage.
+#
+# trollimage is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# trollimage is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with trollimage.  If not, see <http://www.gnu.org/licenses/>.
+"""Helper classes and functions for running and writing tests."""
+
+from contextlib import contextmanager
+
+
+class CustomScheduler:
+    """Scheduler raising an exception if data are computed too many times."""
+
+    def __init__(self, max_computes=1):
+        """Set starting and maximum compute counts."""
+        self.max_computes = max_computes
+        self.total_computes = 0
+
+    def __call__(self, dsk, keys, **kwargs):
+        """Compute dask task and keep track of number of times we do so."""
+        import dask
+        self.total_computes += 1
+        if self.total_computes > self.max_computes:
+            raise RuntimeError("Too many dask computations were scheduled: "
+                               "{}".format(self.total_computes))
+        return dask.get(dsk, keys, **kwargs)
+
+
+ at contextmanager
+def assert_maximum_dask_computes(max_computes=1):
+    """Context manager to make sure dask computations are not executed more than ``max_computes`` times."""
+    import dask
+    with dask.config.set(scheduler=CustomScheduler(max_computes=max_computes)) as new_config:
+        yield new_config


=====================================
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 = " (tag: v1.16.1)"
-    git_full = "5c23f01c984be3dc685eae5750ee227aff41d0e7"
-    git_date = "2021-11-17 15:32:46 -0600"
+    git_refnames = " (HEAD -> main, tag: v1.17.0)"
+    git_full = "cf4dd6eeb6a5151caf83ba5043aa4cd739666171"
+    git_date = "2021-12-07 14:59:27 -0600"
     keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
     return keywords
 


=====================================
trollimage/xrimage.py
=====================================
@@ -372,8 +372,28 @@ class XRImage(object):
         """Mode of the image."""
         return ''.join(self.data['bands'].values)
 
+    @staticmethod
+    def _gtiff_to_cog_kwargs(format_kwargs):
+        """Convert GDAL Geotiff driver options to COG driver options.
+
+        The COG driver automatically sets some format options
+        but zlevel is called level and blockxsize is called blocksize.
+        Convert kwargs to save() from GTiff driver to COG driver.
+
+        """
+        format_kwargs.pop('photometric', None)
+        if 'zlevel' in format_kwargs:
+            format_kwargs['level'] = format_kwargs.pop('zlevel')
+        if 'jpeg_quality' in format_kwargs:
+            format_kwargs['quality'] = format_kwargs.pop('jpeg_quality')
+        format_kwargs.pop('tiled', None)
+        if 'blockxsize' in format_kwargs:
+            format_kwargs['blocksize'] = format_kwargs.pop('blockxsize')
+        format_kwargs.pop('blockysize', None)
+        return format_kwargs
+
     def save(self, filename, fformat=None, fill_value=None, compute=True,
-             keep_palette=False, cmap=None, **format_kwargs):
+             keep_palette=False, cmap=None, driver=None, **format_kwargs):
         """Save the image to the given *filename*.
 
         Args:
@@ -386,6 +406,12 @@ class XRImage(object):
                            If the format allows, geographical information will
                            be saved to the ouput file, in the form of grid
                            mapping or ground control points.
+            driver (str): can override the choice of rasterio/gdal driver
+                        which is normally selected from the filename or fformat.
+                        This is an implementation detail normally avoided but
+                        can be necessary if you wish to distinguish between
+                        GeoTIFF drivers ("GTiff" is the default, but you can
+                        specify "COG" to write a Cloud-Optimized GeoTIFF).
             fill_value (float): Replace invalid data values with this value
                                 and do not produce an Alpha band. Default
                                 behavior is to create an alpha band.
@@ -420,7 +446,7 @@ class XRImage(object):
         fformat = fformat or kwformat or os.path.splitext(filename)[1][1:]
         if fformat in ('tif', 'tiff', 'jp2') and rasterio:
 
-            return self.rio_save(filename, fformat=fformat,
+            return self.rio_save(filename, fformat=fformat, driver=driver,
                                  fill_value=fill_value, compute=compute,
                                  keep_palette=keep_palette, cmap=cmap,
                                  **format_kwargs)
@@ -433,6 +459,7 @@ class XRImage(object):
                  overviews_minsize=256, overviews_resampling=None,
                  include_scale_offset_tags=False,
                  scale_offset_tags=None,
+                 driver=None,
                  **format_kwargs):
         """Save the image using rasterio.
 
@@ -440,6 +467,10 @@ class XRImage(object):
             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.
+            driver (string): The gdal driver to use. If not specified (default),
+                it will be inferred from the fformat, but you can override the
+                default GeoTIFF driver ("GTiff") with "COG" if you want to create
+                a Cloud_Optimized GeoTIFF (and set `tiled=True,overviews=[]`).
             fill_value (number): The value to fill the missing data with.
                 Default is ``None``, translating to trying to keep the data
                 transparent.
@@ -457,7 +488,9 @@ class XRImage(object):
 
                 If provided as an empty list, then levels will be
                 computed as powers of two until the last level has less
-                pixels than `overviews_minsize`.
+                pixels than `overviews_minsize`.  If driver='COG' then use
+                `overviews=[]` to get a Cloud-Optimized GeoTIFF with a correct
+                set of overviews created automatically.
                 Default is to not add overviews.
             overviews_minsize (int): Minimum number of pixels for the smallest
                 overview size generated when `overviews` is auto-generated.
@@ -484,7 +517,13 @@ class XRImage(object):
                    'tif': 'GTiff',
                    'tiff': 'GTiff',
                    'jp2': 'JP2OpenJPEG'}
-        driver = drivers.get(fformat, fformat)
+        # If fformat is specified but not driver then convert it into a driver
+        driver = driver or drivers.get(fformat, fformat)
+        # The COG driver adds overviews so we don't need to create them ourself.
+        # One thing we can't do is prevent any overviews, if we use None then
+        # the COG driver will create automatically, we can't pass OVERVIEWS=NONE.
+        if driver == 'COG' and overviews == []:
+            overviews = None
         if include_scale_offset_tags:
             warnings.warn(
                 "include_scale_offset_tags is deprecated, please use "
@@ -502,7 +541,7 @@ class XRImage(object):
         crs = None
         gcps = None
         transform = None
-        if driver in ['GTiff', 'JP2OpenJPEG']:
+        if driver in ['COG', 'GTiff', 'JP2OpenJPEG']:
             if not np.issubdtype(data.dtype, np.floating):
                 format_kwargs.setdefault('compress', 'DEFLATE')
             photometric_map = {
@@ -554,6 +593,10 @@ class XRImage(object):
             scale, offset = self.get_scaling_from_history(data.attrs.get('enhancement_history', []))
             tags[scale_label], tags[offset_label] = invert_scale_offset(scale, offset)
 
+        # If we are changing the driver then use appropriate kwargs
+        if driver == 'COG':
+            format_kwargs = self._gtiff_to_cog_kwargs(format_kwargs)
+
         # FIXME add metadata
         r_file = RIOFile(filename, 'w', driver=driver,
                          width=data.sizes['x'], height=data.sizes['y'],
@@ -1279,34 +1322,69 @@ class XRImage(object):
                                   axis=self.data.dims.index('bands'))
         self.data.attrs.setdefault('enhancement_history', []).append({'hist_equalize': True})
 
-    def stretch_logarithmic(self, factor=100.):
-        """Move data into range [1:factor] through normalized logarithm."""
+    def stretch_logarithmic(self, factor=100., base="e", min_stretch=None, max_stretch=None):
+        """Move data into range [1:factor] through normalized logarithm.
+
+        Args:
+            factor (float): Maximum of the range data will be scaled to
+                before applying the log function. Image data will be scaled
+                to a 1 to ``factor`` range.
+            base (str): Type of log to use. Defaults to natural log ("e"),
+                but can also be "10" for base 10 log or "2" for base 2 log.
+            min_stretch (float or list): Minimum input value to scale from.
+                Data will be clipped to this value before being scaled to
+                the 1:factor range. By default (None), the limits are computed
+                on the fly but with a performance penalty. May also be a list
+                for multi-band images.
+            max_stretch (float or list): Maximum input value to scale from.
+                Data will be clipped to this value before being scaled to
+                the 1:factor range. By default (None), the limits are computed
+                on the fly but with a performance penalty. May also be a list
+                for multi-band images.
+
+        """
         logger.debug("Perform a logarithmic contrast stretch.")
         crange = (0., 1.0)
+        log_func = np.log if base == "e" else getattr(np, "log" + base)
+        min_stretch, max_stretch = self._convert_log_minmax_stretch(min_stretch, max_stretch)
 
-        b__ = float(crange[1] - crange[0]) / np.log(factor)
+        b__ = float(crange[1] - crange[0]) / log_func(factor)
         c__ = float(crange[0])
 
-        def _band_log(arr):
-            slope = (factor - 1.) / float(arr.max() - arr.min())
-            arr = 1. + (arr - arr.min()) * slope
-            arr = c__ + b__ * da.log(arr)
+        def _band_log(arr, min_input, max_input):
+            slope = (factor - 1.) / (max_input - min_input)
+            arr = np.clip(arr, min_input, max_input)
+            arr = 1. + (arr - min_input) * slope
+            arr = c__ + b__ * log_func(arr)
             return arr
 
         band_results = []
-        for band in self.data['bands'].values:
+        for band_idx, band in enumerate(self.data['bands'].values):
             if band == 'A':
                 continue
             band_data = self.data.sel(bands=band)
-            res = _band_log(band_data.data)
+            res = _band_log(band_data.data,
+                            min_stretch[band_idx],
+                            max_stretch[band_idx])
             band_results.append(res)
 
         if 'A' in self.data.coords['bands'].values:
             band_results.append(self.data.sel(bands='A'))
-        self.data.data = da.stack(band_results,
-                                  axis=self.data.dims.index('bands'))
+        self.data.data = da.stack(band_results, axis=self.data.dims.index('bands'))
         self.data.attrs.setdefault('enhancement_history', []).append({'log_factor': factor})
 
+    def _convert_log_minmax_stretch(self, min_stretch, max_stretch):
+        non_band_dims = tuple(x for x in self.data.dims if x != 'bands')
+        if min_stretch is None:
+            min_stretch = [m.data for m in self.data.min(dim=non_band_dims)]
+        if max_stretch is None:
+            max_stretch = [m.data for m in self.data.max(dim=non_band_dims)]
+        if not isinstance(min_stretch, (list, tuple)):
+            min_stretch = [min_stretch] * self.data.sizes.get("bands", 1)
+        if not isinstance(max_stretch, (list, tuple)):
+            max_stretch = [max_stretch] * self.data.sizes.get("bands", 1)
+        return min_stretch, max_stretch
+
     def stretch_weber_fechner(self, k, s0):
         """Stretch according to the Weber-Fechner law.
 



View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/compare/36c89abcdd42208cc3c2e0feb1abf80bdc25504b...3b3c74f8cc0506f507bc7f8821749151093df5bf

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/compare/36c89abcdd42208cc3c2e0feb1abf80bdc25504b...3b3c74f8cc0506f507bc7f8821749151093df5bf
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/20211211/d27dd90f/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list