[Git][debian-gis-team/trollimage][upstream] New upstream version 1.17.0
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Sat Dec 11 14:22:52 GMT 2021
Antonio Valentino pushed to branch upstream at Debian GIS Project / trollimage
Commits:
eeaed2d5 by Antonio Valentino at 2021-12-11T14:12:09+00:00
New upstream version 1.17.0
- - - - -
5 changed files:
- CHANGELOG.md
- 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
=====================================
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/-/commit/eeaed2d561929db8aea0c1123b1d837222ebb125
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/commit/eeaed2d561929db8aea0c1123b1d837222ebb125
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/edbd9cb3/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list