diff -Nru trollimage-1.7.0/CHANGELOG.md trollimage-1.9.0/CHANGELOG.md --- trollimage-1.7.0/CHANGELOG.md 2019-02-28 18:49:51.000000000 +0000 +++ trollimage-1.9.0/CHANGELOG.md 2019-06-18 11:13:40.000000000 +0000 @@ -1,3 +1,39 @@ +## Version 1.9.0 (2019/06/18) + +### Pull Requests Merged + +#### Bugs fixed + +* [PR 51](https://github.com/pytroll/trollimage/pull/51) - Fix _FillValue not being respected when converting to alpha image + +#### Features added + +* [PR 49](https://github.com/pytroll/trollimage/pull/49) - Add a new method for image stacking. + +In this release 2 pull requests were closed. + + +## Version 1.8.0 (2019/05/10) + +### Issues Closed + +* [Issue 45](https://github.com/pytroll/trollimage/issues/45) - img.stretch gives TypeError where img.data is xarray.DataArray and img.data.data is a dask.array + +In this release 1 issue was closed. + +### Pull Requests Merged + +#### Bugs fixed + +* [PR 47](https://github.com/pytroll/trollimage/pull/47) - Fix xrimage palettize and colorize delaying internal functions + +#### Features added + +* [PR 46](https://github.com/pytroll/trollimage/pull/46) - Implement blend method for XRImage class + +In this release 2 pull requests were closed. + + ## Version 1.7.0 (2019/02/28) ### Issues Closed diff -Nru trollimage-1.7.0/debian/changelog trollimage-1.9.0/debian/changelog --- trollimage-1.7.0/debian/changelog 2019-03-01 06:54:52.000000000 +0000 +++ trollimage-1.9.0/debian/changelog 2019-07-09 06:35:44.000000000 +0000 @@ -1,3 +1,15 @@ +trollimage (1.9.0-1) unstable; urgency=medium + + [ Bas Couwenberg ] + * Update gbp.conf to use --source-only-changes by default. + + [ Antonio Valentino ] + * New upstream release. + * debian/pathces: + - refresh all patches + + -- Antonio Valentino Tue, 09 Jul 2019 06:35:44 +0000 + trollimage (1.7.0-1) unstable; urgency=medium * New upstream release. diff -Nru trollimage-1.7.0/debian/gbp.conf trollimage-1.9.0/debian/gbp.conf --- trollimage-1.7.0/debian/gbp.conf 2018-12-25 08:36:04.000000000 +0000 +++ trollimage-1.9.0/debian/gbp.conf 2019-07-07 08:22:04.000000000 +0000 @@ -14,3 +14,6 @@ # Always use pristine-tar. pristine-tar = True + +[buildpackage] +pbuilder-options = --source-only-changes diff -Nru trollimage-1.7.0/debian/patches/0001-No-display.patch trollimage-1.9.0/debian/patches/0001-No-display.patch --- trollimage-1.7.0/debian/patches/0001-No-display.patch 2019-03-01 06:54:52.000000000 +0000 +++ trollimage-1.9.0/debian/patches/0001-No-display.patch 2019-07-09 06:35:44.000000000 +0000 @@ -8,10 +8,10 @@ 1 file changed, 1 insertion(+) diff --git a/trollimage/tests/test_image.py b/trollimage/tests/test_image.py -index 816d4f2..a949c8f 100644 +index 4e7f34e..1b8cc74 100644 --- a/trollimage/tests/test_image.py +++ b/trollimage/tests/test_image.py -@@ -1790,6 +1790,7 @@ class TestXRImage(unittest.TestCase): +@@ -1859,6 +1859,7 @@ class TestXRImage(unittest.TestCase): def test_putalpha(self): pass diff -Nru trollimage-1.7.0/trollimage/tests/test_image.py trollimage-1.9.0/trollimage/tests/test_image.py --- trollimage-1.7.0/trollimage/tests/test_image.py 2019-02-28 18:49:51.000000000 +0000 +++ trollimage-1.9.0/trollimage/tests/test_image.py 2019-06-18 11:13:40.000000000 +0000 @@ -1504,12 +1504,15 @@ # L -> LA (int) with dask.config.set(scheduler=CustomScheduler(max_computes=1)): img = xrimage.XRImage((dataset1 * 150).astype(np.uint8)) + img.data.attrs['_FillValue'] = 0 # set fill value img = img.convert('LA') self.assertTrue(np.issubdtype(img.data.dtype, np.integer)) self.assertTrue(img.mode == 'LA') self.assertTrue(len(img.data.coords['bands']) == 2) - # make sure the alpha band is all opaque - np.testing.assert_allclose(img.data.sel(bands='A'), 255) + # make sure the alpha band is all opaque except the first pixel + alpha = img.data.sel(bands='A').values.ravel() + np.testing.assert_allclose(alpha[0], 0) + np.testing.assert_allclose(alpha[1:], 255) # L -> LA (float) with dask.config.set(scheduler=CustomScheduler(max_computes=1)): @@ -1778,11 +1781,77 @@ self.assertTupleEqual((1, 5, 15), values.shape) self.assertTupleEqual((2, 4), bw.colors.shape) + def test_stack(self): + + import xarray as xr + from trollimage import xrimage + + # background image + arr1 = np.zeros((2, 2)) + data1 = xr.DataArray(arr1, dims=['y', 'x']) + bkg = xrimage.XRImage(data1) + + # image to be stacked + arr2 = np.full((2, 2), np.nan) + arr2[0] = 1 + data2 = xr.DataArray(arr2, dims=['y', 'x']) + img = xrimage.XRImage(data2) + + # expected result + arr3 = arr1.copy() + arr3[0] = 1 + data3 = xr.DataArray(arr3, dims=['y', 'x']) + res = xrimage.XRImage(data3) + + # stack image over the background + bkg.stack(img) + + # check result + np.testing.assert_allclose(bkg.data, res.data, rtol=1e-05) + def test_merge(self): pass def test_blend(self): - pass + import xarray as xr + from trollimage import xrimage + + core1 = np.arange(75).reshape(5, 5, 3) / 75.0 + alpha1 = np.linspace(0, 1, 25).reshape(5, 5, 1) + arr1 = np.concatenate([core1, alpha1], 2) + data1 = xr.DataArray(arr1, dims=['y', 'x', 'bands'], + coords={'bands': ['R', 'G', 'B', 'A']}) + img1 = xrimage.XRImage(data1) + + core2 = np.arange(75, 0, -1).reshape(5, 5, 3) / 75.0 + alpha2 = np.linspace(1, 0, 25).reshape(5, 5, 1) + arr2 = np.concatenate([core2, alpha2], 2) + data2 = xr.DataArray(arr2, dims=['y', 'x', 'bands'], + coords={'bands': ['R', 'G', 'B', 'A']}) + img2 = xrimage.XRImage(data2) + + img3 = img1.blend(img2) + + np.testing.assert_allclose( + (alpha1 + alpha2 * (1 - alpha1)).squeeze(), + img3.data.sel(bands="A")) + + np.testing.assert_allclose( + img3.data.sel(bands="R").values, + np.array( + [[1., 0.95833635, 0.9136842, 0.8666667, 0.8180645], + [0.768815, 0.72, 0.6728228, 0.62857145, 0.5885714], + [0.55412847, 0.5264665, 0.50666666, 0.495612, 0.49394494], + [0.5020408, 0.52, 0.5476586, 0.5846154, 0.63027024], + [0.683871, 0.7445614, 0.81142855, 0.8835443, 0.96]])) + + with self.assertRaises(TypeError): + img1.blend("Salekhard") + + wrongimg = xrimage.XRImage( + xr.DataArray(np.zeros((0, 0)), dims=("y", "x"))) + with self.assertRaises(ValueError): + img1.blend(wrongimg) def test_replace_luminance(self): pass diff -Nru trollimage-1.7.0/trollimage/version.py trollimage-1.9.0/trollimage/version.py --- trollimage-1.7.0/trollimage/version.py 2019-02-28 18:49:51.000000000 +0000 +++ trollimage-1.9.0/trollimage/version.py 2019-06-18 11:13:40.000000000 +0000 @@ -23,9 +23,9 @@ # 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.7.0)" - git_full = "d35a7665ad475ff230e457085523e21f2cd3f454" - git_date = "2019-02-28 12:49:51 -0600" + git_refnames = " (tag: v1.9.0)" + git_full = "63fa32f2d40bb65ebc39c4be1fb1baf8f163db98" + git_date = "2019-06-18 06:13:40 -0500" keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords diff -Nru trollimage-1.7.0/trollimage/xrimage.py trollimage-1.9.0/trollimage/xrimage.py --- trollimage-1.7.0/trollimage/xrimage.py 2019-02-28 18:49:51.000000000 +0000 +++ trollimage-1.9.0/trollimage/xrimage.py 2019-06-18 11:13:40.000000000 +0000 @@ -449,11 +449,15 @@ If ``data`` is an integer type then the alpha band will be scaled to use the smallest (min) value as fully transparent and the largest - (max) value as fully opaque. For float types the alpha band spans - 0 to 1. + (max) value as fully opaque. If a `_FillValue` attribute is found for + integer type data then it is used to identify null values in the data. + Otherwise xarray's `isnull` is used. + + For float types the alpha band spans 0 to 1. """ - null_mask = alpha if alpha is not None else self._create_alpha(data) + fill_value = data.attrs.get('_FillValue', None) # integer fill value + null_mask = alpha if alpha is not None else self._create_alpha(data, fill_value) # if we are using integer data, then alpha needs to be min-int to max-int # otherwise for floats we want 0 to 1 if np.issubdtype(data.dtype, np.integer): @@ -914,6 +918,16 @@ self.data = self.data * scale + offset self.data.attrs = attrs + def stack(self, img): + """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: + raise NotImplementedError("Cannot stack images of different modes.") + + 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. @@ -937,6 +951,13 @@ self.channels[i].mask = np.logical_and(selfmask, img.channels[i].mask) + @staticmethod + def _colorize(l_data, colormap): + # 'l_data' is (1, rows, cols) + # 'channels' will be a list of 3 (RGB) or 4 (RGBA) arrays + channels = colormap.colorize(l_data) + return np.concatenate(channels, axis=0) + def colorize(self, colormap): """Colorize the current image using `colormap`. @@ -955,14 +976,7 @@ alpha = None l_data = self.data.sel(bands=['L']) - - def _colorize(l_data, colormap): - # 'l_data' is (1, rows, cols) - # 'channels' will be a list of 3 (RGB) or 4 (RGBA) arrays - channels = colormap.colorize(l_data) - return np.concatenate(channels, axis=0) - - new_data = l_data.data.map_blocks(_colorize, colormap, + new_data = l_data.data.map_blocks(self._colorize, colormap, chunks=(colormap.colors.shape[1],) + l_data.data.chunks[1:], dtype=np.float64) @@ -981,6 +995,12 @@ dims = self.data.dims self.data = xr.DataArray(new_data, coords=coords, attrs=attrs, dims=dims) + @staticmethod + def _palettize(data, colormap): + """Helper for dask-friendly palettize operation.""" + # returns data and palette, only need data + return colormap.palettize(data)[0] + def palettize(self, colormap): """Palettize the current image using `colormap`. @@ -994,12 +1014,7 @@ raise ValueError("Image should be grayscale to colorize") l_data = self.data.sel(bands=['L']) - - def _palettize(data): - # returns data and palette, only need data - return colormap.palettize(data)[0] - - new_data = l_data.data.map_blocks(_palettize, dtype=l_data.dtype) + new_data = l_data.data.map_blocks(self._palettize, colormap, dtype=l_data.dtype) self.palette = tuple(colormap.colors) if self.mode == "L": @@ -1011,22 +1026,61 @@ self.data.data = new_data self.data.coords['bands'] = list(mode) - def blend(self, other): - """Alpha blend *other* on top of the current image.""" - raise NotImplementedError("This method has not be implemented for " - "xarray support.") + def blend(self, src): + r"""Alpha blend *src* on top of the current image. + + Perform `alpha blending`_ of *src* on top of the current image. + Alpha blending is defined as: + + .. math:: + + \begin{cases} + \mathrm{out}_A = + \mathrm{src}_A + \mathrm{dst}_A (1 - \mathrm{src}_A) \\ + \mathrm{out}_{RGB} = + \bigl(\mathrm{src}_{RGB}\mathrm{src}_A + + \mathrm{dst}_{RGB} \mathrm{dst}_A + \left(1 - \mathrm{src}_A \right) \bigr) + \div \mathrm{out}_A \\ + \mathrm{out}_A = 0 \Rightarrow \mathrm{out}_{RGB} = 0 + \end{cases} - if self.mode != "RGBA" or other.mode != "RGBA": - raise ValueError("Images must be in RGBA") - src = other - dst = self - outa = src.channels[3] + dst.channels[3] * (1 - src.channels[3]) - for i in range(3): - dst.channels[i] = (src.channels[i] * src.channels[3] + - dst.channels[i] * dst.channels[3] * - (1 - src.channels[3])) / outa - dst.channels[i][outa == 0] = 0 - dst.channels[3] = outa + Both images must have mode ``"RGBA"``. + + Args: + src (:class:`XRImage` with mode ``"RGBA"``) + Image to be blended on top of current image. + + .. _alpha blending: https://en.wikipedia.org/w/index.php?title=Alpha_compositing&oldid=891033105#Alpha_blending + + Returns + XRImage with mode "RGBA", blended as described above + + """ + # NB: docstring maths copy-pasta from enwiki + + if self.mode != "RGBA": + raise ValueError( + "Expected self.mode='RGBA', got {md!s}".format( + md=self.mode)) + elif not isinstance(src, XRImage): + raise TypeError("Expected XRImage, got {tp!s}".format( + tp=type(src))) + elif src.mode != "RGBA": + raise ValueError("Expected src.mode='RGBA', got {sm!s}".format( + sm=src.mode)) + + srca = src.data.sel(bands="A") + dsta = self.data.sel(bands="A") + outa = srca + dsta * (1-srca) + bi = {"bands": ["R", "G", "B"]} + rgb = ((src.data.loc[bi] * srca + + self.data.loc[bi] * dsta * (1-srca)) + / outa).where(outa != 0, 0) + return self.__class__( + xr.concat( + [rgb, outa.expand_dims("bands")], + dim="bands")) def show(self): """Display the image on screen."""