Skip to content
Commits on Source (6)
exclude: '^$'
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.2.3
hooks:
- id: flake8
additional_dependencies: [flake8-docstrings, flake8-debugger, flake8-bugbear]
## Version 1.10.0 (2019/09/20)
### Pull Requests Merged
#### Bugs fixed
* [PR 53](https://github.com/pytroll/trollimage/pull/53) - Fix double format passing in saving functions
#### Features added
* [PR 55](https://github.com/pytroll/trollimage/pull/55) - Add enhancement-history to the image
* [PR 54](https://github.com/pytroll/trollimage/pull/54) - Add ability to use AreaDefinitions new "crs" property
* [PR 52](https://github.com/pytroll/trollimage/pull/52) - Add 'colors' and 'values' keyword arguments to Colormap
In this release 4 pull requests were closed.
## Version 1.9.0 (2019/06/18)
### Pull Requests Merged
......
trollimage (1.10.0-1) unstable; urgency=medium
* New upstream release.
* debian/patches:
- refresh all patches
* debian/control:
- explicit specification of Rules-Requires-Root
-- Antonio Valentino <antonio.valentino@tiscali.it> Sun, 22 Sep 2019 07:13:42 +0000
trollimage (1.9.0-2) unstable; urgency=medium
* Bump Standards-Version to 4.4.0, no changes.
......
......@@ -3,6 +3,7 @@ Maintainer: Debian GIS Project <pkg-grass-devel@lists.alioth.debian.org>
Uploaders: Antonio Valentino <antonio.valentino@tiscali.it>
Section: python
Testsuite: autopkgtest-pkg-python
Rules-Requires-Root: no
Priority: optional
Build-Depends: debhelper-compat (= 12),
dh-python,
......
......@@ -8,14 +8,14 @@ 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 4e7f34e..1b8cc74 100644
index 06f5fd0..890bd85 100644
--- a/trollimage/tests/test_image.py
+++ b/trollimage/tests/test_image.py
@@ -1859,6 +1859,7 @@ class TestXRImage(unittest.TestCase):
def test_putalpha(self):
@@ -1863,6 +1863,7 @@ class TestXRImage(unittest.TestCase):
"""Test putalpha."""
pass
+ @unittest.skip("no display")
def test_show(self):
"""Test that the show commands calls PIL.show"""
"""Test that the show commands calls PIL.show."""
import xarray as xr
......@@ -88,7 +88,11 @@ class Colormap(object):
"""
def __init__(self, *tuples):
def __init__(self, *tuples, **kwargs):
if 'colors' in kwargs and 'values' in kwargs:
values = kwargs['values']
colors = kwargs['colors']
else:
values = [a for (a, b) in tuples]
colors = [b for (a, b) in tuples]
self.values = np.array(values)
......
This diff is collapsed.
......@@ -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.9.0)"
git_full = "63fa32f2d40bb65ebc39c4be1fb1baf8f163db98"
git_date = "2019-06-18 06:13:40 -0500"
git_refnames = " (HEAD -> master, tag: v1.10.0)"
git_full = "b1fb06cbf6ef8b23e5816c423df1eeaf8e76d606"
git_date = "2019-09-20 09:41:02 +0200"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
......
......@@ -21,12 +21,14 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""This module defines the XRImage class. It overlaps largely with the PIL
library, but has the advantage of using :class:`~xarray.DataArray` objects
backed by :class:`dask arrays <dask.array.Array>` as pixel arrays. This
allows for invalid values to be tracked, metadata to be assigned, and
stretching to be lazy evaluated. With the optional ``rasterio`` library
installed dask array chunks can be saved in parallel.
"""This module defines the XRImage class.
It overlaps largely with the PIL library, but has the advantage of using
:class:`~xarray.DataArray` objects backed by :class:`dask arrays
<dask.array.Array>` as pixel arrays. This allows for invalid values to
be tracked, metadata to be assigned, and stretching to be lazy
evaluated. With the optional ``rasterio`` library installed dask array
chunks can be saved in parallel.
"""
......@@ -96,12 +98,14 @@ class RIOFile(object):
indexes=indexes)
def open(self, mode=None):
"""Open the file."""
mode = mode or self.mode
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))
......@@ -119,6 +123,7 @@ class RIOFile(object):
self.close()
def __del__(self):
"""Delete the instance."""
try:
self.close()
except (IOError, OSError):
......@@ -168,8 +173,13 @@ def color_interp(data):
class XRImage(object):
"""Image class using an :class:`xarray.DataArray` as internal storage.
It can be saved to a variety of image formats, but if Rasterio is installed,
it can save to geotiff and jpeg2000 with geographical information.
It can be saved to a variety of image formats, but if Rasterio is
installed, it can save to geotiff and jpeg2000 with geographical
information.
The enhancements functions are recording some parameters in the image's
data attribute called `enhancement_history`.
"""
def __init__(self, data):
......@@ -252,7 +262,8 @@ class XRImage(object):
saving with rasterio, used with
keep_palette=True. Should be uint8.
format_kwargs: Additional format options to pass to `rasterio`
or `PIL` saving methods.
or `PIL` saving methods. Any format argument passed
at this stage would be superseeded by `fformat`.
Returns:
Either `None` if `compute` is True or a `dask.Delayed` object or
......@@ -263,7 +274,8 @@ class XRImage(object):
the caller.
"""
fformat = fformat or os.path.splitext(filename)[1][1:4]
kwformat = format_kwargs.pop('format', None)
fformat = fformat or kwformat or os.path.splitext(filename)[1][1:]
if fformat in ('tif', 'jp2') and rasterio:
return self.rio_save(filename, fformat=fformat,
fill_value=fill_value, compute=compute,
......@@ -284,7 +296,7 @@ class XRImage(object):
img.rio_save('myfile.tif', overviews=[2, 4, 8, 16])
"""
fformat = fformat or os.path.splitext(filename)[1][1:4]
fformat = fformat or os.path.splitext(filename)[1][1:]
drivers = {'jpg': 'JPEG',
'png': 'PNG',
'tif': 'GTiff',
......@@ -318,6 +330,10 @@ class XRImage(object):
photometric_map[mode.upper()])
try:
area = data.attrs['area']
if hasattr(area, 'crs'):
crs = rasterio.crs.CRS.from_wkt(area.crs.to_wkt())
else:
crs = rasterio.crs.CRS(data.attrs['area'].proj_dict)
west, south, east, north = data.attrs['area'].area_extent
height, width = data.sizes['y'], data.sizes['x']
......@@ -380,10 +396,11 @@ class XRImage(object):
compute=True, **format_kwargs):
"""Save the image to the given *filename* using PIL.
For now, the compression level [0-9] is ignored, due to PIL's lack of
support. See also :meth:`save`.
For now, the compression level [0-9] is ignored, due to PIL's
lack of support. See also :meth:`save`.
"""
fformat = fformat or os.path.splitext(filename)[1][1:4]
fformat = fformat or os.path.splitext(filename)[1][1:]
fformat = check_image_format(fformat)
if fformat == 'png':
......@@ -402,6 +419,7 @@ class XRImage(object):
Inspired by:
public domain, Nick Galbreath
http://blog.modp.com/2007/08/python-pil-and-png-metadata-take-2.html
"""
reserved = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect')
......@@ -531,8 +549,7 @@ class XRImage(object):
return data
def _l2rgb(self, mode):
"""Convert from L (black and white) to RGB.
"""
"""Convert from L (black and white) to RGB."""
self._check_modes(("L", "LA"))
bands = ["L"] * 3
......@@ -543,6 +560,7 @@ class XRImage(object):
return data
def convert(self, mode):
"""Convert image to *mode*."""
if mode == self.mode:
return self.__class__(self.data)
......@@ -588,7 +606,7 @@ class XRImage(object):
return new_img
def _finalize(self, fill_value=None, dtype=np.uint8, keep_palette=False, cmap=None):
"""Wrapper around 'finalize' method for backwards compatibility."""
"""Wrap around 'finalize' method for backwards compatibility."""
import warnings
warnings.warn("'_finalize' is deprecated, use 'finalize' instead.",
DeprecationWarning)
......@@ -597,13 +615,14 @@ class XRImage(object):
def finalize(self, fill_value=None, dtype=np.uint8, keep_palette=False, cmap=None):
"""Finalize the image to be written to an output file.
This adds an alpha band or fills data with a fill_value (if specified).
It also scales float data to the output range of the data type (0-255
for uint8, default). For integer input data this method assumes the
data is already scaled to the proper desired range. It will still fill
in invalid values and add an alpha band if needed. Integer input
data's fill value is determined by a special ``_FillValue`` attribute
in the ``DataArray`` ``.attrs`` dictionary.
This adds an alpha band or fills data with a fill_value (if
specified). It also scales float data to the output range of the
data type (0-255 for uint8, default). For integer input data
this method assumes the data is already scaled to the proper
desired range. It will still fill in invalid values and add an
alpha band if needed. Integer input data's fill value is
determined by a special ``_FillValue`` attribute in the
``DataArray`` ``.attrs`` dictionary.
"""
if keep_palette and not self.mode.startswith('P'):
......@@ -674,19 +693,20 @@ class XRImage(object):
dims=['bands'],
coords={'bands': self.data['bands']})
def gamma(self, gamma=1.0):
def gamma(self, gamma=None):
"""Apply gamma correction to the channels of the image.
If *gamma* is a
tuple, then it should have as many elements as the channels of the
image, and the gamma correction is applied elementwise. If *gamma* is a
number, the same gamma correction is applied on every channel, if there
are several channels in the image. The behaviour of :func:`gamma` is
undefined outside the normal [0,1] range of the channels.
If *gamma* is a tuple, then it should have as many elements as
the channels of the image, and the gamma correction is applied
elementwise. If *gamma* is a number, the same gamma correction
is applied on every channel, if there are several channels in
the image. The behaviour of :func:`gamma` is undefined outside
the normal [0,1] range of the channels.
"""
if isinstance(gamma, (list, tuple)):
gamma = self.xrify_tuples(gamma)
elif gamma == 1.0:
elif gamma is None or gamma == 1.0:
return
logger.debug("Applying gamma %s", str(gamma))
......@@ -694,18 +714,21 @@ class XRImage(object):
self.data = self.data.clip(min=0)
self.data **= 1.0 / gamma
self.data.attrs = attrs
self.data.attrs.setdefault('enhancement_history', []).append({'gamma': gamma})
def stretch(self, stretch="crude", **kwargs):
"""Apply stretching to the current image.
The value of *stretch* sets the type of stretching applied. The values
"histogram", "linear", "crude" (or "crude-stretch") perform respectively
histogram equalization, contrast stretching (with 5% cutoff on both
sides), and contrast stretching without cutoff. The value "logarithmic"
or "log" will do a logarithmic enhancement towards white. If a tuple or
a list of two values is given as input, then a contrast stretching is
performed with the values as cutoff. These values should be normalized
in the range [0.0,1.0].
The value of *stretch* sets the type of stretching applied. The
values "histogram", "linear", "crude" (or "crude-stretch")
perform respectively histogram equalization, contrast stretching
(with 5% cutoff on both sides), and contrast stretching without
cutoff. The value "logarithmic" or "log" will do a logarithmic
enhancement towards white. If a tuple or a list of two values is
given as input, then a contrast stretching is performed with the
values as cutoff. These values should be normalized in the range
[0.0,1.0].
"""
logger.debug("Applying stretch %s with parameters %s",
stretch, str(kwargs))
......@@ -735,7 +758,7 @@ class XRImage(object):
@staticmethod
def _compute_quantile(data, dims, cutoffs):
"""Helper method for stretch_linear.
"""Compute quantile for stretch_linear.
Dask delayed functions need to be non-internal functions (created
inside a function) to be serializable on a multi-process scheduler.
......@@ -756,6 +779,7 @@ class XRImage(object):
"""Stretch linearly the contrast of the current image.
Use *cutoffs* for left and right trimming.
"""
logger.debug("Perform a linear contrast stretch.")
......@@ -786,8 +810,9 @@ class XRImage(object):
def crude_stretch(self, min_stretch=None, max_stretch=None):
"""Perform simple linear stretching.
This is done without any cutoff on the current image and normalizes to
the [0,1] range.
This is done without any cutoff on the current image and
normalizes to the [0,1] range.
"""
if min_stretch is None:
non_band_dims = tuple(x for x in self.data.dims if x != 'bands')
......@@ -808,9 +833,12 @@ class XRImage(object):
else:
scale_factor = 1.0 / delta
attrs = self.data.attrs
self.data -= min_stretch
offset = -min_stretch * scale_factor
self.data *= scale_factor
self.data += offset
self.data.attrs = attrs
self.data.attrs.setdefault('enhancement_history', []).append({'scale': scale_factor,
'offset': offset})
def stretch_hist_equalize(self, approximate=False):
"""Stretch the current image's colors through histogram equalization.
......@@ -858,6 +886,7 @@ class XRImage(object):
band_results.append(self.data.sel(bands='A'))
self.data.data = da.stack(band_results,
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."""
......@@ -885,6 +914,7 @@ class XRImage(object):
band_results.append(self.data.sel(bands='A'))
self.data.data = da.stack(band_results,
axis=self.data.dims.index('bands'))
self.data.attrs.setdefault('enhancement_history', []).append({'log_factor': factor})
def stretch_weber_fechner(self, k, s0):
"""Stretch according to the Weber-Fechner law.
......@@ -892,10 +922,12 @@ class XRImage(object):
p = k.ln(S/S0)
p is perception, S is the stimulus, S0 is the stimulus threshold (the
highest unpercieved stimulus), and k is the factor.
"""
attrs = self.data.attrs
self.data = k * xu.log(self.data / s0)
self.data.attrs = attrs
self.data.attrs.setdefault('enhancement_history', []).append({'weber_fechner': (k, s0)})
def invert(self, invert=True):
"""Inverts all the channels of a image according to *invert*.
......@@ -905,6 +937,7 @@ class XRImage(object):
Note: 'Inverting' means that black becomes white, and vice-versa, not
that the values are negated !
"""
logger.debug("Applying invert with parameters %s", str(invert))
if isinstance(invert, (tuple, list)):
......@@ -917,10 +950,11 @@ class XRImage(object):
attrs = self.data.attrs
self.data = self.data * scale + offset
self.data.attrs = attrs
self.data.attrs.setdefault('enhancement_history', []).append({'scale': scale,
'offset': offset})
def stack(self, img):
"""Stack the provided image on top of the current image.
"""
"""Stack the provided image on top of the current image."""
# TODO: Conversions between different modes with notification
# to the user, i.e. proper logging
if self.mode != img.mode:
......@@ -929,8 +963,10 @@ class XRImage(object):
self.data = self.data.where(img.data.isnull(), img.data)
def merge(self, img):
"""Use the provided image as background for the current *img* image,
that is if the current image has missing data.
"""Use the provided image as background for the current *img* image.
That is if the current image has missing data.
"""
raise NotImplementedError("This method has not be implemented for "
"xarray support.")
......@@ -966,7 +1002,6 @@ class XRImage(object):
Works only on "L" or "LA" images.
"""
if self.mode not in ("L", "LA"):
raise ValueError("Image should be grayscale to colorize")
......@@ -997,7 +1032,7 @@ class XRImage(object):
@staticmethod
def _palettize(data, colormap):
"""Helper for dask-friendly palettize operation."""
"""Operate in a dask-friendly manner."""
# returns data and palette, only need data
return colormap.palettize(data)[0]
......@@ -1009,7 +1044,6 @@ class XRImage(object):
Works only on "L" or "LA" images.
"""
if self.mode not in ("L", "LA"):
raise ValueError("Image should be grayscale to colorize")
......