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

Antonio Valentino gitlab at salsa.debian.org
Fri Mar 1 06:58:55 GMT 2019


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


Commits:
c6e918db by Antonio Valentino at 2019-03-01T06:51:15Z
New upstream version 1.7.0
- - - - -


9 changed files:

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


Changes:

=====================================
.travis.yml
=====================================
@@ -8,16 +8,16 @@ env:
         - PYTHON_VERSION=$TRAVIS_PYTHON_VERSION
         - NUMPY_VERSION=stable
         - MAIN_CMD='python setup.py'
-        - CONDA_DEPENDENCIES='pillow gdal xarray dask toolz mock coverage coveralls codecov'
+        - CONDA_DEPENDENCIES='pillow gdal xarray dask toolz mock coverage coveralls codecov rasterio'
         - SETUP_XVFB=False
         - EVENT_TYPE='push pull_request'
         - SETUP_CMD='test'
         - CONDA_CHANNELS='conda-forge'
+        - CONDA_CHANNEL_PRIORITY=True
+
 install:
-# Get rasterio 1.0
     - git clone --depth 1 git://github.com/astropy/ci-helpers.git
     - source ci-helpers/travis/setup_conda.sh
-    - conda install -c conda-forge/label/dev rasterio
 script: coverage run --source=trollimage setup.py test
 after_success:
     - if [[ $PYTHON_VERSION == 3.6 ]]; then coveralls; fi


=====================================
CHANGELOG.md
=====================================
@@ -1,5 +1,31 @@
-## Version 1.6.3 (2018/12/20)
+## Version 1.7.0 (2019/02/28)
+
+### Issues Closed
 
+* [Issue 27](https://github.com/pytroll/trollimage/issues/27) - Add "overviews" to save options
+* [Issue 5](https://github.com/pytroll/trollimage/issues/5) - Add alpha channel to Colormaps
+
+In this release 2 issues were closed.
+
+### Pull Requests Merged
+
+#### Bugs fixed
+
+* [PR 42](https://github.com/pytroll/trollimage/pull/42) - Fix stretch_linear to be dask serializable
+* [PR 41](https://github.com/pytroll/trollimage/pull/41) - Refactor XRImage pil_save to be serializable
+
+#### Features added
+
+* [PR 44](https://github.com/pytroll/trollimage/pull/44) - Add support for adding overviews to rasterio-managed files
+* [PR 43](https://github.com/pytroll/trollimage/pull/43) - Add support for jpeg2000 writing
+* [PR 40](https://github.com/pytroll/trollimage/pull/40) - Modify colorize routine to allow colorizing using colormaps with alpha channel
+* [PR 39](https://github.com/pytroll/trollimage/pull/39) - Add 'keep_palette' keyword argument 'XRImage.save' to prevent P -> RGB conversion on save
+* [PR 36](https://github.com/pytroll/trollimage/pull/36) - Add support for saving gcps
+
+In this release 7 pull requests were closed.
+
+
+## Version 1.6.3 (2018/12/20)
 
 ### Pull Requests Merged
 
@@ -12,7 +38,6 @@ In this release 1 pull request was closed.
 
 ## Version 1.6.2 (2018/12/20)
 
-
 ### Pull Requests Merged
 
 #### Bugs fixed
@@ -25,7 +50,6 @@ In this release 2 pull requests were closed.
 
 ## Version 1.6.1 (2018/12/19)
 
-
 ### Pull Requests Merged
 
 #### Bugs fixed


=====================================
appveyor.yml
=====================================
@@ -3,8 +3,9 @@ environment:
     PYTHON: "C:\\conda"
     MINICONDA_VERSION: "latest"
     CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci-helpers\\appveyor\\windows_sdk.cmd"
-    CONDA_DEPENDENCIES: "pillow gdal xarray dask toolz mock coverage coveralls codecov"
+    CONDA_DEPENDENCIES: "pillow gdal xarray dask toolz mock coverage coveralls codecov rasterio"
     CONDA_CHANNELS: "conda-forge"
+    CONDA_CHANNEL_PRIORITY: "True"
 
   matrix:
     - PYTHON: "C:\\Python27_64"


=====================================
trollimage/colormap.py
=====================================
@@ -26,6 +26,7 @@
 import numpy as np
 from trollimage.colorspaces import rgb2hcl, hcl2rgb
 
+
 def colorize(arr, colors, values):
     """Colorize a monochromatic array *arr*, based *colors* given for
     *values*. Interpolation is used. *values* must be in ascending order.
@@ -52,6 +53,7 @@ def colorize(arr, colors, values):
     except AttributeError:
         return channels
 
+
 def palettize(arr, colors, values):
     """From start *values* apply *colors* to *data*.
     """
@@ -122,7 +124,15 @@ class Colormap(object):
             max_val, min_val = min_val, max_val
         self.values = (((self.values * 1.0 - self.values.min()) /
                         (self.values.max() - self.values.min()))
-                        * (max_val - min_val) + min_val)
+                       * (max_val - min_val) + min_val)
+
+    def to_rio(self):
+        """Converts the colormap to a rasterio colormap.
+        """
+        self.colors = (((self.colors * 1.0 - self.colors.min()) /
+                        (self.colors.max() - self.colors.min())) * 255)
+        return dict(zip(self.values, tuple(map(tuple, self.colors))))
+
 
 # matlab jet "#00007F", "blue", "#007FFF", "cyan", "#7FFF7F", "yellow",
 # "#FF7F00", "red", "#7F0000"
@@ -143,7 +153,7 @@ rainbow = Colormap((0.000, (0.0, 0.0, 0.5)),
 # * Single hue *
 
 blues = Colormap((0.000, (247 / 255.0, 251 / 255.0, 1.0)),
-                (1.000, (8 / 255.0, 48 / 255.0, 107 / 255.0)))
+                 (1.000, (8 / 255.0, 48 / 255.0, 107 / 255.0)))
 
 greens = Colormap((0.000, (247 / 255.0, 252 / 255.0, 245 / 255.0)),
                   (1.000, (0.0, 68 / 255.0, 27 / 255.0)))
@@ -160,7 +170,6 @@ purples = Colormap((0.0, (252 / 255.0, 251 / 255.0, 253 / 255.0)),
 reds = Colormap((0.0, (1.0, 245 / 255.0, 240 / 255.0)),
                 (1.0, (103 / 255.0, 0.0, 13 / 255.0)))
 
-
 # * Multihue *
 
 # BuGn
@@ -231,7 +240,6 @@ sequential_colormaps = [blues, greens, greys, oranges, purples, reds,
                         bugn, bupu, gnbu, orrd, pubu, pubugn, purd, rdpu,
                         ylgn, ylgnbu, ylorbr, ylorrd]
 
-
 # * Diverging *
 
 brbg = Colormap((0.0, (84 / 255.0, 48 / 255.0, 5 / 255.0)),
@@ -246,7 +254,6 @@ brbg = Colormap((0.0, (84 / 255.0, 48 / 255.0, 5 / 255.0)),
                 (0.9, (1 / 255.0, 102 / 255.0, 94 / 255.0)),
                 (1.0, (0 / 255.0, 60 / 255.0, 48 / 255.0)))
 
-
 piyg = Colormap((0.0, (142 / 255.0, 1 / 255.0, 82 / 255.0)),
                 (0.1, (197 / 255.0, 27 / 255.0, 125 / 255.0)),
                 (0.2, (222 / 255.0, 119 / 255.0, 174 / 255.0)),
@@ -259,8 +266,6 @@ piyg = Colormap((0.0, (142 / 255.0, 1 / 255.0, 82 / 255.0)),
                 (0.9, (77 / 255.0, 146 / 255.0, 33 / 255.0)),
                 (1.0, (39 / 255.0, 100 / 255.0, 25 / 255.0)))
 
-
-
 prgn = Colormap((0.0, (64 / 255.0, 0 / 255.0, 75 / 255.0)),
                 (0.1, (118 / 255.0, 42 / 255.0, 131 / 255.0)),
                 (0.2, (153 / 255.0, 112 / 255.0, 171 / 255.0)),
@@ -273,7 +278,6 @@ prgn = Colormap((0.0, (64 / 255.0, 0 / 255.0, 75 / 255.0)),
                 (0.9, (27 / 255.0, 120 / 255.0, 55 / 255.0)),
                 (1.0, (0 / 255.0, 68 / 255.0, 27 / 255.0)))
 
-
 puor = Colormap((0.0, (127 / 255.0, 59 / 255.0, 8 / 255.0)),
                 (0.1, (179 / 255.0, 88 / 255.0, 6 / 255.0)),
                 (0.2, (224 / 255.0, 130 / 255.0, 20 / 255.0)),
@@ -286,7 +290,6 @@ puor = Colormap((0.0, (127 / 255.0, 59 / 255.0, 8 / 255.0)),
                 (0.9, (84 / 255.0, 39 / 255.0, 136 / 255.0)),
                 (1.0, (45 / 255.0, 0 / 255.0, 75 / 255.0)))
 
-
 rdbu = Colormap((0.0, (103 / 255.0, 0 / 255.0, 31 / 255.0)),
                 (0.1, (178 / 255.0, 24 / 255.0, 43 / 255.0)),
                 (0.2, (214 / 255.0, 96 / 255.0, 77 / 255.0)),
@@ -299,7 +302,6 @@ rdbu = Colormap((0.0, (103 / 255.0, 0 / 255.0, 31 / 255.0)),
                 (0.9, (33 / 255.0, 102 / 255.0, 172 / 255.0)),
                 (1.0, (5 / 255.0, 48 / 255.0, 97 / 255.0)))
 
-
 rdgy = Colormap((0.0, (103 / 255.0, 0 / 255.0, 31 / 255.0)),
                 (0.1, (178 / 255.0, 24 / 255.0, 43 / 255.0)),
                 (0.2, (214 / 255.0, 96 / 255.0, 77 / 255.0)),
@@ -312,7 +314,6 @@ rdgy = Colormap((0.0, (103 / 255.0, 0 / 255.0, 31 / 255.0)),
                 (0.9, (77 / 255.0, 77 / 255.0, 77 / 255.0)),
                 (1.0, (26 / 255.0, 26 / 255.0, 26 / 255.0)))
 
-
 rdylbu = Colormap((0.0, (165 / 255.0, 0 / 255.0, 38 / 255.0)),
                   (0.1, (215 / 255.0, 48 / 255.0, 39 / 255.0)),
                   (0.2, (244 / 255.0, 109 / 255.0, 67 / 255.0)),
@@ -325,7 +326,6 @@ rdylbu = Colormap((0.0, (165 / 255.0, 0 / 255.0, 38 / 255.0)),
                   (0.9, (69 / 255.0, 117 / 255.0, 180 / 255.0)),
                   (1.0, (49 / 255.0, 54 / 255.0, 149 / 255.0)))
 
-
 rdylgn = Colormap((0.0, (165 / 255.0, 0 / 255.0, 38 / 255.0)),
                   (0.1, (215 / 255.0, 48 / 255.0, 39 / 255.0)),
                   (0.2, (244 / 255.0, 109 / 255.0, 67 / 255.0)),
@@ -350,7 +350,6 @@ spectral = Colormap((0.0, (158 / 255.0, 1 / 255.0, 66 / 255.0)),
                     (0.9, (50 / 255.0, 136 / 255.0, 189 / 255.0)),
                     (1.0, (94 / 255.0, 79 / 255.0, 162 / 255.0)))
 
-
 diverging_colormaps = [brbg, piyg, prgn, puor, rdbu, rdgy, rdylbu, rdylgn,
                        spectral]
 
@@ -442,19 +441,21 @@ qualitative_colormaps = [set1, set2, set3,
                          paired, accent, dark2,
                          pastel1, pastel2]
 
+
 def colorbar(height, length, colormap):
     """Return the channels of a colorbar.
     """
-    cbar = np.tile(np.arange(length)*1.0/(length-1), (height, 1))
+    cbar = np.tile(np.arange(length) * 1.0 / (length - 1), (height, 1))
     cbar = (cbar * (colormap.values.max() - colormap.values.min())
             + colormap.values.min())
 
     return colormap.colorize(cbar)
 
+
 def palettebar(height, length, colormap):
     """Return the channels of a palettebar.
     """
-    cbar = np.tile(np.arange(length)*1.0/(length-1), (height, 1))
+    cbar = np.tile(np.arange(length) * 1.0 / (length - 1), (height, 1))
     cbar = (cbar * (colormap.values.max() + 1 - colormap.values.min())
             + colormap.values.min())
 


=====================================
trollimage/image.py
=====================================
@@ -76,6 +76,7 @@ def check_image_format(fformat):
              "png": "png",
              "xbm": "xbm",
              "xpm": "xpm",
+             'jp2': 'jp2',
              }
     fformat = fformat.lower()
     try:


=====================================
trollimage/tests/test_colormap.py
=====================================
@@ -23,14 +23,15 @@
 """Test colormap.py
 """
 
-
 import unittest
 from trollimage import colormap
 import numpy as np
 
+
 class TestColormapClass(unittest.TestCase):
     """Test case for the colormap object.
     """
+
     def test_colorize(self):
         """Test colorize
         """
@@ -79,8 +80,6 @@ class TestColormapClass(unittest.TestCase):
         self.assertTrue(np.allclose(colors, cm_.colors))
         self.assertTrue(all(channels == [0, 0, 1, 2, 3, 3]))
 
-
-
     def test_set_range(self):
         """Test set_range
         """
@@ -161,6 +160,20 @@ class TestColormapClass(unittest.TestCase):
         self.assertTrue(np.allclose(channel, np.arange(4)))
         self.assertTrue(np.allclose(palette, cm_.colors))
 
+    def test_to_rio(self):
+        """Test conversion to rasterio colormap
+        """
+        cm_ = colormap.Colormap((1, (1, 1, 0)),
+                                (2, (0, 1, 1)),
+                                (3, (1, 1, 1)),
+                                (4, (0, 0, 0)))
+
+        d = cm_.to_rio()
+        exp = {1: (255, 255, 0), 2: (0, 255, 255),
+               3: (255, 255, 255), 4: (0, 0, 0)}
+
+        self.assertEqual(d, exp)
+
 
 def suite():
     """The suite for test_colormap.


=====================================
trollimage/tests/test_image.py
=====================================
@@ -58,7 +58,6 @@ class CustomScheduler(object):
 
 
 class TestEmptyImage(unittest.TestCase):
-
     """Class for testing the mpop.imageo.image module
     """
 
@@ -251,7 +250,6 @@ class TestEmptyImage(unittest.TestCase):
 
 
 class TestImageCreation(unittest.TestCase):
-
     """Class for testing the mpop.imageo.image module
     """
 
@@ -332,15 +330,14 @@ class TestImageCreation(unittest.TestCase):
 
             for nb_chan in range(self.modes_len[i]):
                 self.assertTrue(np.all(self.img[mode].channels[nb_chan] ==
-                                    one_channel))
+                                       one_channel))
                 self.assertTrue(isinstance(self.img[mode].channels[nb_chan],
-                                        np.ma.core.MaskedArray))
+                                           np.ma.core.MaskedArray))
 
             i = i + 1
 
 
 class TestRegularImage(unittest.TestCase):
-
     """Class for testing the mpop.imageo.image module
     """
 
@@ -501,11 +498,11 @@ class TestRegularImage(unittest.TestCase):
             self.img.gamma(0.5)
             for i in range(len(self.img.channels)):
                 self.assertTrue(np.all(self.img.channels[i] -
-                                    old_channels[i] ** 2 < EPSILON))
+                                       old_channels[i] ** 2 < EPSILON))
             self.img.gamma(1)
             for i in range(len(self.img.channels)):
                 self.assertTrue(np.all(self.img.channels[i] -
-                                    old_channels[i] ** 2 < EPSILON))
+                                       old_channels[i] ** 2 < EPSILON))
 
             # self.img.gamma(2)
             # for i in range(len(self.img.channels)):
@@ -535,11 +532,11 @@ class TestRegularImage(unittest.TestCase):
             self.img.invert()
             for i in range(len(self.img.channels)):
                 self.assertTrue(np.all(self.img.channels[i] ==
-                                    1 - old_channels[i]))
+                                       1 - old_channels[i]))
             self.img.invert(True)
             for i in range(len(self.img.channels)):
                 self.assertTrue(np.all(self.img.channels[i] -
-                                    old_channels[i] < EPSILON))
+                                       old_channels[i] < EPSILON))
             self.assertRaises(ValueError, self.img.invert,
                               [True, False, True, False,
                                True, False, True, False])
@@ -645,7 +642,7 @@ class TestRegularImage(unittest.TestCase):
                                 [0.5, 0.25, 0.25]])
             self.img.replace_luminance(luma)
             self.assertEqual(self.img.mode, mode)
-            if(self.img.mode.endswith("A")):
+            if (self.img.mode.endswith("A")):
                 chans = self.img.channels[:-1]
             else:
                 chans = self.img.channels
@@ -685,8 +682,8 @@ class TestRegularImage(unittest.TestCase):
         self.img.convert("L")
         newimg.merge(self.img)
         self.assertTrue(np.all(np.abs(newimg.channels[0] -
-                                   np.array([[0, 2, 3], [0.5, 0.25, 6]])) <
-                            EPSILON))
+                                      np.array([[0, 2, 3], [0.5, 0.25, 6]])) <
+                               EPSILON))
 
     def tearDown(self):
         """Clean up the mess.
@@ -696,7 +693,6 @@ class TestRegularImage(unittest.TestCase):
 
 
 class TestFlatImage(unittest.TestCase):
-
     """Test a flat image, ie an image where min == max.
     """
 
@@ -712,23 +708,22 @@ class TestFlatImage(unittest.TestCase):
         """
         self.img.stretch()
         self.assertTrue(self.img.channels[0].shape == (2, 3) and
-                     np.ma.count_masked(self.img.channels[0]) == 5)
+                        np.ma.count_masked(self.img.channels[0]) == 5)
         self.img.stretch("crude")
         self.assertTrue(self.img.channels[0].shape == (2, 3) and
-                     np.ma.count_masked(self.img.channels[0]) == 5)
+                        np.ma.count_masked(self.img.channels[0]) == 5)
         self.img.crude_stretch(1, 2)
         self.assertTrue(self.img.channels[0].shape == (2, 3) and
-                     np.ma.count_masked(self.img.channels[0]) == 5)
+                        np.ma.count_masked(self.img.channels[0]) == 5)
         self.img.stretch("linear")
         self.assertTrue(self.img.channels[0].shape == (2, 3) and
-                     np.ma.count_masked(self.img.channels[0]) == 5)
+                        np.ma.count_masked(self.img.channels[0]) == 5)
         self.img.stretch("histogram")
         self.assertTrue(self.img.channels[0].shape == (2, 3) and
-                     np.ma.count_masked(self.img.channels[0]) == 5)
+                        np.ma.count_masked(self.img.channels[0]) == 5)
 
 
 class TestNoDataImage(unittest.TestCase):
-
     """Test an image filled with no data.
     """
 
@@ -756,7 +751,7 @@ class TestNoDataImage(unittest.TestCase):
 
 def random_string(length,
                   choices="abcdefghijklmnopqrstuvwxyz"
-                  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
+                          "ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
     """Generates a random string with elements from *set* of the specified
     *length*.
     """
@@ -789,12 +784,12 @@ class TestXRImage(unittest.TestCase):
         self.assertTupleEqual(img.data.dims, ('bands', 'x', 'y'))
 
         data = xr.DataArray(np.arange(75).reshape(5, 5, 3), dims=[
-                            'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
+            'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         self.assertEqual(img.mode, 'RGB')
 
         data = xr.DataArray(np.arange(100).reshape(5, 5, 4), dims=[
-                            'y', 'x', 'bands'], coords={'bands': ['Y', 'Cb', 'Cr', 'A']})
+            'y', 'x', 'bands'], coords={'bands': ['Y', 'Cb', 'Cr', 'A']})
         img = xrimage.XRImage(data)
         self.assertEqual(img.mode, 'YCbCrA')
 
@@ -805,16 +800,23 @@ class TestXRImage(unittest.TestCase):
         import dask.array as da
         from dask.delayed import Delayed
         from trollimage import xrimage
+        from trollimage.colormap import brbg, Colormap
 
-        data = xr.DataArray(np.arange(75).reshape(5, 5, 3) / 75., dims=[
-                            'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
+        # RGBA colormap
+        bw = Colormap(
+            (0.0, (1.0, 1.0, 1.0, 1.0)),
+            (1.0, (0.0, 0.0, 0.0, 0.5)),
+        )
+
+        data = xr.DataArray(np.arange(75).reshape(5, 5, 3) / 74., dims=[
+            'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         with NamedTemporaryFile(suffix='.png') as tmp:
             img.save(tmp.name)
 
         # Single band image
-        data = xr.DataArray(np.arange(75).reshape(15, 5, 1) / 75., dims=[
-                            'y', 'x', 'bands'], coords={'bands': ['L']})
+        data = xr.DataArray(np.arange(75).reshape(15, 5, 1) / 74., dims=[
+            'y', 'x', 'bands'], coords={'bands': ['L']})
         # Single band image to JPEG
         img = xrimage.XRImage(data)
         with NamedTemporaryFile(suffix='.jpg') as tmp:
@@ -824,7 +826,21 @@ class TestXRImage(unittest.TestCase):
         with NamedTemporaryFile(suffix='.png') as tmp:
             img.save(tmp.name)
 
-        data = xr.DataArray(da.from_array(np.arange(75).reshape(5, 5, 3) / 75.,
+        # Single band image palettized
+        data = xr.DataArray(np.arange(75).reshape(15, 5, 1) / 74., dims=[
+            'y', 'x', 'bands'], coords={'bands': ['L']})
+        # Single band image to JPEG
+        img = xrimage.XRImage(data)
+        img.palettize(brbg)
+        with NamedTemporaryFile(suffix='.png') as tmp:
+            img.save(tmp.name)
+        # RGBA colormap
+        img = xrimage.XRImage(data)
+        img.palettize(bw)
+        with NamedTemporaryFile(suffix='.png') as tmp:
+            img.save(tmp.name)
+
+        data = xr.DataArray(da.from_array(np.arange(75).reshape(5, 5, 3) / 74.,
                                           chunks=5),
                             dims=['y', 'x', 'bands'],
                             coords={'bands': ['R', 'G', 'B']})
@@ -832,7 +848,7 @@ class TestXRImage(unittest.TestCase):
         with NamedTemporaryFile(suffix='.png') as tmp:
             img.save(tmp.name)
 
-        data = data.where(data > (10 / 75.0))
+        data = data.where(data > (10 / 74.0))
         img = xrimage.XRImage(data)
         with NamedTemporaryFile(suffix='.png') as tmp:
             img.save(tmp.name)
@@ -852,8 +868,9 @@ class TestXRImage(unittest.TestCase):
         import rasterio as rio
 
         # numpy array image - scale to 0 to 1 first
-        data = xr.DataArray(np.arange(75.).reshape(5, 5, 3) / 75., dims=[
-            'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
+        data = xr.DataArray(np.arange(75).reshape(5, 5, 3) / 75.,
+                            dims=['y', 'x', 'bands'],
+                            coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         with NamedTemporaryFile(suffix='.tif') as tmp:
             img.save(tmp.name)
@@ -957,7 +974,7 @@ class TestXRImage(unittest.TestCase):
                 file_data = f.read()
             self.assertEqual(file_data.shape, (3, 5, 5))  # no alpha band
             exp = np.arange(75.).reshape(5, 5, 3) / 75.
-            exp2 = (exp * (2**16 - 1) - (2**15)).round()
+            exp2 = (exp * (2 ** 16 - 1) - (2 ** 15)).round()
             exp2[exp <= 10. / 75.] = -128.
             np.testing.assert_allclose(file_data[0], exp2[:, :, 0])
             np.testing.assert_allclose(file_data[1], exp2[:, :, 1])
@@ -1001,6 +1018,7 @@ class TestXRImage(unittest.TestCase):
         import dask.array as da
         from trollimage import xrimage
         import rasterio as rio
+        from rasterio.control import GroundControlPoint
 
         # numpy array image
         data = xr.DataArray(np.arange(75).reshape(5, 5, 3), dims=[
@@ -1044,6 +1062,90 @@ class TestXRImage(unittest.TestCase):
             da.store(*delay)
             delay[1].close()
 
+        # GCPs
+        class FakeArea():
+            def __init__(self, lons, lats):
+                self.lons = lons
+                self.lats = lats
+
+        gcps = [GroundControlPoint(1, 1, 100.0, 1000.0, z=0.0),
+                GroundControlPoint(2, 3, 400.0, 2000.0, z=0.0)]
+        crs = 'epsg:4326'
+
+        lons = xr.DataArray(da.from_array(np.arange(25).reshape(5, 5), chunks=5),
+                            dims=['y', 'x'],
+                            attrs={'gcps': gcps,
+                                   'crs': crs})
+
+        lats = xr.DataArray(da.from_array(np.arange(25).reshape(5, 5), chunks=5),
+                            dims=['y', 'x'],
+                            attrs={'gcps': gcps,
+                                   'crs': crs})
+
+        data = xr.DataArray(da.from_array(np.arange(75).reshape(5, 5, 3), chunks=5),
+                            dims=['y', 'x', 'bands'],
+                            coords={'bands': ['R', 'G', 'B']},
+                            attrs={'area': FakeArea(lons, lats)})
+        img = xrimage.XRImage(data)
+        with NamedTemporaryFile(suffix='.tif') as tmp:
+            img.save(tmp.name)
+            with rio.open(tmp.name) as f:
+                fgcps, fcrs = f.gcps
+            for ref, val in zip(gcps, fgcps):
+                self.assertEqual(ref.col, val.col)
+                self.assertEqual(ref.row, val.row)
+                self.assertEqual(ref.x, val.x)
+                self.assertEqual(ref.y, val.y)
+                self.assertEqual(ref.z, val.z)
+            self.assertEqual(crs, fcrs)
+
+        # with rasterio colormap provided
+        exp_cmap = {i: (i, 255 - i, i, 255) for i in range(256)}
+        data = xr.DataArray(da.from_array(np.arange(81).reshape(9, 9, 1), chunks=9),
+                            dims=['y', 'x', 'bands'],
+                            coords={'bands': ['P']})
+        img = xrimage.XRImage(data)
+        with NamedTemporaryFile(suffix='.tif') as tmp:
+            img.save(tmp.name, keep_palette=True, cmap=exp_cmap)
+            with rio.open(tmp.name) as f:
+                file_data = f.read()
+                cmap = f.colormap(1)
+            self.assertEqual(file_data.shape, (1, 9, 9))  # no alpha band
+            exp = np.arange(81).reshape(9, 9, 1)
+            np.testing.assert_allclose(file_data[0], exp[:, :, 0])
+            self.assertEqual(cmap, exp_cmap)
+
+        # with trollimage colormap provided
+        from trollimage.colormap import Colormap
+        t_cmap = Colormap(*tuple((i, (i, i, i)) for i in range(20)))
+        exp_cmap = {i: (int(i * 255 / 19), int(i * 255 / 19), int(i * 255 / 19), 255) for i in range(20)}
+        exp_cmap.update({i: (0, 0, 0, 255) for i in range(20, 256)})
+        data = xr.DataArray(da.from_array(np.arange(81).reshape(9, 9, 1), chunks=9),
+                            dims=['y', 'x', 'bands'],
+                            coords={'bands': ['P']})
+        img = xrimage.XRImage(data)
+        with NamedTemporaryFile(suffix='.tif') as tmp:
+            img.save(tmp.name, keep_palette=True, cmap=t_cmap)
+            with rio.open(tmp.name) as f:
+                file_data = f.read()
+                cmap = f.colormap(1)
+            self.assertEqual(file_data.shape, (1, 9, 9))  # no alpha band
+            exp = np.arange(81).reshape(9, 9, 1)
+            np.testing.assert_allclose(file_data[0], exp[:, :, 0])
+            self.assertEqual(cmap, exp_cmap)
+
+        # with bad colormap provided
+        bad_cmap = [[i, [i, i, i]] for i in range(256)]
+        data = xr.DataArray(da.from_array(np.arange(81).reshape(9, 9, 1), chunks=9),
+                            dims=['y', 'x', 'bands'],
+                            coords={'bands': ['P']})
+        img = xrimage.XRImage(data)
+        with NamedTemporaryFile(suffix='.tif') as tmp:
+            self.assertRaises(ValueError, img.save, tmp.name,
+                              keep_palette=True, cmap=bad_cmap)
+            self.assertRaises(ValueError, img.save, tmp.name,
+                              keep_palette=True, cmap=t_cmap, dtype='uint16')
+
         # with input fill value
         data = np.arange(75).reshape(5, 5, 3)
         # second pixel is all bad
@@ -1083,6 +1185,45 @@ class TestXRImage(unittest.TestCase):
             np.testing.assert_allclose(file_data[2], exp[:, :, 2])
             np.testing.assert_allclose(file_data[3], exp_alpha)
 
+    @unittest.skipIf(sys.platform.startswith('win'), "'NamedTemporaryFile' not supported on Windows")
+    def test_save_jp2_int(self):
+        """Test saving jp2000 when input data is int."""
+        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)
+        self.assertTrue(np.issubdtype(img.data.dtype, np.integer))
+        with NamedTemporaryFile(suffix='.jp2') as tmp:
+            img.save(tmp.name, quality=100, reversible=True)
+            with rio.open(tmp.name) as f:
+                file_data = f.read()
+            self.assertEqual(file_data.shape, (4, 5, 5))  # alpha band added
+            exp = np.arange(75).reshape(5, 5, 3)
+            np.testing.assert_allclose(file_data[0], exp[:, :, 0])
+            np.testing.assert_allclose(file_data[1], exp[:, :, 1])
+            np.testing.assert_allclose(file_data[2], exp[:, :, 2])
+            np.testing.assert_allclose(file_data[3], 255)
+
+    @unittest.skipIf(sys.platform.startswith('win'), "'NamedTemporaryFile' not supported on Windows")
+    def test_save_overviews(self):
+        """Test saving geotiffs with overviews."""
+        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)
+        self.assertTrue(np.issubdtype(img.data.dtype, np.integer))
+        with NamedTemporaryFile(suffix='.tif') as tmp:
+            img.save(tmp.name, overviews=[2, 4])
+            with rio.open(tmp.name) as f:
+                self.assertEqual(len(f.overviews(1)), 2)
 
     def test_gamma(self):
         """Test gamma correction."""
@@ -1159,30 +1300,30 @@ class TestXRImage(unittest.TestCase):
         img = xrimage.XRImage(data)
         img.stretch_linear()
         res = np.array([[[-0.005051, -0.005051, -0.005051],
-                         [ 0.037037,  0.037037,  0.037037],
-                         [ 0.079125,  0.079125,  0.079125],
-                         [ 0.121212,  0.121212,  0.121212],
-                         [ 0.1633  ,  0.1633  ,  0.1633  ]],
-                        [[ 0.205387,  0.205387,  0.205387],
-                         [ 0.247475,  0.247475,  0.247475],
-                         [ 0.289562,  0.289562,  0.289562],
-                         [ 0.33165 ,  0.33165 ,  0.33165 ],
-                         [ 0.373737,  0.373737,  0.373737]],
-                        [[ 0.415825,  0.415825,  0.415825],
-                         [ 0.457912,  0.457912,  0.457912],
-                         [ 0.5     ,  0.5     ,  0.5     ],
-                         [ 0.542088,  0.542088,  0.542088],
-                         [ 0.584175,  0.584175,  0.584175]],
-                        [[ 0.626263,  0.626263,  0.626263],
-                         [ 0.66835 ,  0.66835 ,  0.66835 ],
-                         [ 0.710438,  0.710438,  0.710438],
-                         [ 0.752525,  0.752525,  0.752525],
-                         [ 0.794613,  0.794613,  0.794613]],
-                        [[ 0.8367  ,  0.8367  ,  0.8367  ],
-                         [ 0.878788,  0.878788,  0.878788],
-                         [ 0.920875,  0.920875,  0.920875],
-                         [ 0.962963,  0.962963,  0.962963],
-                         [ 1.005051,  1.005051,  1.005051]]])
+                         [0.037037, 0.037037, 0.037037],
+                         [0.079125, 0.079125, 0.079125],
+                         [0.121212, 0.121212, 0.121212],
+                         [0.1633, 0.1633, 0.1633]],
+                        [[0.205387, 0.205387, 0.205387],
+                         [0.247475, 0.247475, 0.247475],
+                         [0.289562, 0.289562, 0.289562],
+                         [0.33165, 0.33165, 0.33165],
+                         [0.373737, 0.373737, 0.373737]],
+                        [[0.415825, 0.415825, 0.415825],
+                         [0.457912, 0.457912, 0.457912],
+                         [0.5, 0.5, 0.5],
+                         [0.542088, 0.542088, 0.542088],
+                         [0.584175, 0.584175, 0.584175]],
+                        [[0.626263, 0.626263, 0.626263],
+                         [0.66835, 0.66835, 0.66835],
+                         [0.710438, 0.710438, 0.710438],
+                         [0.752525, 0.752525, 0.752525],
+                         [0.794613, 0.794613, 0.794613]],
+                        [[0.8367, 0.8367, 0.8367],
+                         [0.878788, 0.878788, 0.878788],
+                         [0.920875, 0.920875, 0.920875],
+                         [0.962963, 0.962963, 0.962963],
+                         [1.005051, 1.005051, 1.005051]]])
 
         self.assertTrue(np.allclose(img.data.values, res, atol=1.e-6))
 
@@ -1195,35 +1336,35 @@ class TestXRImage(unittest.TestCase):
                             coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         img.stretch('histogram')
-        res = np.array([[[ 0.        ,  0.        ,  0.        ],
-                         [ 0.04166667,  0.04166667,  0.04166667],
-                         [ 0.08333333,  0.08333333,  0.08333333],
-                         [ 0.125     ,  0.125     ,  0.125     ],
-                         [ 0.16666667,  0.16666667,  0.16666667]],
-
-                        [[ 0.20833333,  0.20833333,  0.20833333],
-                         [ 0.25      ,  0.25      ,  0.25      ],
-                         [ 0.29166667,  0.29166667,  0.29166667],
-                         [ 0.33333333,  0.33333333,  0.33333333],
-                         [ 0.375     ,  0.375     ,  0.375     ]],
-
-                        [[ 0.41666667,  0.41666667,  0.41666667],
-                         [ 0.45833333,  0.45833333,  0.45833333],
-                         [ 0.5       ,  0.5       ,  0.5       ],
-                         [ 0.54166667,  0.54166667,  0.54166667],
-                         [ 0.58333333,  0.58333333,  0.58333333]],
-
-                        [[ 0.625     ,  0.625     ,  0.625     ],
-                         [ 0.66666667,  0.66666667,  0.66666667],
-                         [ 0.70833333,  0.70833333,  0.70833333],
-                         [ 0.75      ,  0.75      ,  0.75      ],
-                         [ 0.79166667,  0.79166667,  0.79166667]],
-
-                        [[ 0.83333333,  0.83333333,  0.83333333],
-                         [ 0.875     ,  0.875     ,  0.875     ],
-                         [ 0.91666667,  0.91666667,  0.91666667],
-                         [ 0.95833333,  0.95833333,  0.95833333],
-                         [ 0.99951172,  0.99951172,  0.99951172]]])
+        res = np.array([[[0., 0., 0.],
+                         [0.04166667, 0.04166667, 0.04166667],
+                         [0.08333333, 0.08333333, 0.08333333],
+                         [0.125, 0.125, 0.125],
+                         [0.16666667, 0.16666667, 0.16666667]],
+
+                        [[0.20833333, 0.20833333, 0.20833333],
+                         [0.25, 0.25, 0.25],
+                         [0.29166667, 0.29166667, 0.29166667],
+                         [0.33333333, 0.33333333, 0.33333333],
+                         [0.375, 0.375, 0.375]],
+
+                        [[0.41666667, 0.41666667, 0.41666667],
+                         [0.45833333, 0.45833333, 0.45833333],
+                         [0.5, 0.5, 0.5],
+                         [0.54166667, 0.54166667, 0.54166667],
+                         [0.58333333, 0.58333333, 0.58333333]],
+
+                        [[0.625, 0.625, 0.625],
+                         [0.66666667, 0.66666667, 0.66666667],
+                         [0.70833333, 0.70833333, 0.70833333],
+                         [0.75, 0.75, 0.75],
+                         [0.79166667, 0.79166667, 0.79166667]],
+
+                        [[0.83333333, 0.83333333, 0.83333333],
+                         [0.875, 0.875, 0.875],
+                         [0.91666667, 0.91666667, 0.91666667],
+                         [0.95833333, 0.95833333, 0.95833333],
+                         [0.99951172, 0.99951172, 0.99951172]]])
 
         self.assertTrue(np.allclose(img.data.values, res, atol=1.e-6))
 
@@ -1236,35 +1377,35 @@ class TestXRImage(unittest.TestCase):
                             coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         img.stretch(stretch='logarithmic')
-        res = np.array([[[ 0.        ,  0.        ,  0.        ],
-                         [ 0.35484693,  0.35484693,  0.35484693],
-                         [ 0.48307087,  0.48307087,  0.48307087],
-                         [ 0.5631469 ,  0.5631469 ,  0.5631469 ],
-                         [ 0.62151902,  0.62151902,  0.62151902]],
-
-                        [[ 0.66747806,  0.66747806,  0.66747806],
-                         [ 0.70538862,  0.70538862,  0.70538862],
-                         [ 0.73765396,  0.73765396,  0.73765396],
-                         [ 0.76573946,  0.76573946,  0.76573946],
-                         [ 0.79060493,  0.79060493,  0.79060493]],
-
-                        [[ 0.81291336,  0.81291336,  0.81291336],
-                         [ 0.83314196,  0.83314196,  0.83314196],
-                         [ 0.85164569,  0.85164569,  0.85164569],
-                         [ 0.86869572,  0.86869572,  0.86869572],
-                         [ 0.88450394,  0.88450394,  0.88450394]],
-
-                        [[ 0.899239  ,  0.899239  ,  0.899239  ],
-                         [ 0.9130374 ,  0.9130374 ,  0.9130374 ],
-                         [ 0.92601114,  0.92601114,  0.92601114],
-                         [ 0.93825325,  0.93825325,  0.93825325],
-                         [ 0.94984187,  0.94984187,  0.94984187]],
-
-                        [[ 0.96084324,  0.96084324,  0.96084324],
-                         [ 0.97131402,  0.97131402,  0.97131402],
-                         [ 0.98130304,  0.98130304,  0.98130304],
-                         [ 0.99085269,  0.99085269,  0.99085269],
-                         [ 1.        ,  1.        ,  1.        ]]])
+        res = np.array([[[0., 0., 0.],
+                         [0.35484693, 0.35484693, 0.35484693],
+                         [0.48307087, 0.48307087, 0.48307087],
+                         [0.5631469, 0.5631469, 0.5631469],
+                         [0.62151902, 0.62151902, 0.62151902]],
+
+                        [[0.66747806, 0.66747806, 0.66747806],
+                         [0.70538862, 0.70538862, 0.70538862],
+                         [0.73765396, 0.73765396, 0.73765396],
+                         [0.76573946, 0.76573946, 0.76573946],
+                         [0.79060493, 0.79060493, 0.79060493]],
+
+                        [[0.81291336, 0.81291336, 0.81291336],
+                         [0.83314196, 0.83314196, 0.83314196],
+                         [0.85164569, 0.85164569, 0.85164569],
+                         [0.86869572, 0.86869572, 0.86869572],
+                         [0.88450394, 0.88450394, 0.88450394]],
+
+                        [[0.899239, 0.899239, 0.899239],
+                         [0.9130374, 0.9130374, 0.9130374],
+                         [0.92601114, 0.92601114, 0.92601114],
+                         [0.93825325, 0.93825325, 0.93825325],
+                         [0.94984187, 0.94984187, 0.94984187]],
+
+                        [[0.96084324, 0.96084324, 0.96084324],
+                         [0.97131402, 0.97131402, 0.97131402],
+                         [0.98130304, 0.98130304, 0.98130304],
+                         [0.99085269, 0.99085269, 0.99085269],
+                         [1., 1., 1.]]])
 
         self.assertTrue(np.allclose(img.data.values, res, atol=1.e-6))
 
@@ -1278,35 +1419,35 @@ class TestXRImage(unittest.TestCase):
                             coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         img.stretch_weber_fechner(2.5, 0.2)
-        res = np.array([[[    -np.inf, -6.73656795, -5.0037    ],
+        res = np.array([[[-np.inf, -6.73656795, -5.0037],
                          [-3.99003723, -3.27083205, -2.71297317],
-                         [-2.25716928, -1.87179258, -1.5379641 ],
+                         [-2.25716928, -1.87179258, -1.5379641],
                          [-1.24350651, -0.98010522, -0.74182977],
                          [-0.52430133, -0.32419456, -0.13892463]],
 
-                        [[ 0.03355755,  0.19490385,  0.34646541],
-                         [ 0.48936144,  0.6245295 ,  0.75276273],
-                         [ 0.87473814,  0.99103818,  1.10216759],
-                         [ 1.20856662,  1.31062161,  1.40867339],
-                         [ 1.50302421,  1.59394332,  1.68167162]],
-
-                        [[ 1.7664255 ,  1.84840006,  1.92777181],
-                         [ 2.00470095,  2.07933336,  2.1518022 ],
-                         [ 2.22222939,  2.29072683,  2.35739745],
-                         [ 2.42233616,  2.48563068,  2.54736221],
-                         [ 2.60760609,  2.66643234,  2.72390613]],
-
-                        [[ 2.78008827,  2.83503554,  2.88880105],
-                         [ 2.94143458,  2.99298279,  3.04348956],
-                         [ 3.09299613,  3.14154134,  3.18916183],
-                         [ 3.23589216,  3.28176501,  3.32681127],
-                         [ 3.37106022,  3.41453957,  3.45727566]],
-
-                        [[ 3.49929345,  3.54061671,  3.58126801],
-                         [ 3.62126886,  3.66063976,  3.69940022],
-                         [ 3.7375689 ,  3.7751636 ,  3.81220131],
-                         [ 3.84869831,  3.88467015,  3.92013174],
-                         [ 3.95509735,  3.98958065,  4.02359478]]])
+                        [[0.03355755, 0.19490385, 0.34646541],
+                         [0.48936144, 0.6245295, 0.75276273],
+                         [0.87473814, 0.99103818, 1.10216759],
+                         [1.20856662, 1.31062161, 1.40867339],
+                         [1.50302421, 1.59394332, 1.68167162]],
+
+                        [[1.7664255, 1.84840006, 1.92777181],
+                         [2.00470095, 2.07933336, 2.1518022],
+                         [2.22222939, 2.29072683, 2.35739745],
+                         [2.42233616, 2.48563068, 2.54736221],
+                         [2.60760609, 2.66643234, 2.72390613]],
+
+                        [[2.78008827, 2.83503554, 2.88880105],
+                         [2.94143458, 2.99298279, 3.04348956],
+                         [3.09299613, 3.14154134, 3.18916183],
+                         [3.23589216, 3.28176501, 3.32681127],
+                         [3.37106022, 3.41453957, 3.45727566]],
+
+                        [[3.49929345, 3.54061671, 3.58126801],
+                         [3.62126886, 3.66063976, 3.69940022],
+                         [3.7375689, 3.7751636, 3.81220131],
+                         [3.84869831, 3.88467015, 3.92013174],
+                         [3.95509735, 3.98958065, 4.02359478]]])
 
         self.assertTrue(np.allclose(img.data.values, res, atol=1.e-6))
 
@@ -1335,7 +1476,13 @@ class TestXRImage(unittest.TestCase):
         import dask
         import xarray as xr
         from trollimage import xrimage
-        from trollimage.colormap import brbg
+        from trollimage.colormap import brbg, Colormap
+
+        # RGBA colormap
+        bw = Colormap(
+            (0.0, (1.0, 1.0, 1.0, 1.0)),
+            (1.0, (0.0, 0.0, 0.0, 0.5)),
+        )
 
         arr1 = np.arange(150).reshape(1, 15, 10) / 150.
         arr2 = np.append(arr1, np.ones(150).reshape(arr1.shape)).reshape(2, 15, 10)
@@ -1431,10 +1578,10 @@ class TestXRImage(unittest.TestCase):
             img.palettize(brbg)
             pal = img.palette
 
-            img = img.convert('RGBA')
-            self.assertTrue(np.issubdtype(img.data.dtype, np.floating))
-            self.assertTrue(img.mode == 'RGBA')
-            self.assertTrue(len(img.data.coords['bands']) == 4)
+            img2 = img.convert('RGBA')
+            self.assertTrue(np.issubdtype(img2.data.dtype, np.floating))
+            self.assertTrue(img2.mode == 'RGBA')
+            self.assertTrue(len(img2.data.coords['bands']) == 4)
 
         # PA -> RGB (float)
         img = xrimage.XRImage(dataset3)
@@ -1447,7 +1594,23 @@ class TestXRImage(unittest.TestCase):
 
         self.assertRaises(ValueError, img.convert, 'A')
 
+        # L -> palettize -> RGBA (float) with RGBA colormap
+        with dask.config.set(scheduler=CustomScheduler(max_computes=0)):
+            img = xrimage.XRImage(dataset1)
+            img.palettize(bw)
+
+            img2 = img.convert('RGBA')
+            self.assertTrue(np.issubdtype(img2.data.dtype, np.floating))
+            self.assertTrue(img2.mode == 'RGBA')
+            self.assertTrue(len(img2.data.coords['bands']) == 4)
+            # convert to RGB, use RGBA from colormap regardless
+            img2 = img.convert('RGB')
+            self.assertTrue(np.issubdtype(img2.data.dtype, np.floating))
+            self.assertTrue(img2.mode == 'RGBA')
+            self.assertTrue(len(img2.data.coords['bands']) == 4)
+
     def test_colorize(self):
+        """Test colorize with an RGB colormap."""
         import xarray as xr
         from trollimage import xrimage
         from trollimage.colormap import brbg
@@ -1459,83 +1622,83 @@ class TestXRImage(unittest.TestCase):
         values = img.data.compute()
 
         expected = np.array([[
-           [3.29409498e-01,   3.59108764e-01,   3.88800969e-01,
-            4.18486092e-01,   4.48164112e-01,   4.77835010e-01,
-            5.07498765e-01,   5.37155355e-01,   5.65419479e-01,
-            5.92686124e-01,   6.19861622e-01,   6.46945403e-01,
-            6.73936907e-01,   7.00835579e-01,   7.27640871e-01],
-           [7.58680358e-01,   8.01695237e-01,   8.35686284e-01,
-            8.60598212e-01,   8.76625002e-01,   8.84194741e-01,
-            8.83948647e-01,   8.76714923e-01,   8.95016030e-01,
-            9.14039881e-01,   9.27287161e-01,   9.36546985e-01,
-            9.43656076e-01,   9.50421050e-01,   9.58544227e-01],
-           [9.86916929e-01,   1.02423117e+00,   1.03591220e+00,
-            1.02666645e+00,   1.00491333e+00,   9.80759775e-01,
-            9.63746819e-01,   9.60798629e-01,   9.47739946e-01,
-            9.27428067e-01,   9.01184523e-01,   8.71168132e-01,
-            8.40161241e-01,   8.11290344e-01,   7.87705814e-01],
-           [7.57749840e-01,   7.20020026e-01,   6.82329616e-01,
-            6.44678929e-01,   6.07068282e-01,   5.69497990e-01,
-            5.31968369e-01,   4.94025422e-01,   4.54275131e-01,
-            4.14517560e-01,   3.74757709e-01,   3.35000583e-01,
-            2.95251189e-01,   2.55514533e-01,   2.15795621e-01],
-           [1.85805611e-01,   1.58245609e-01,   1.30686714e-01,
-            1.03128926e-01,   7.55722460e-02,   4.80166757e-02,
-            2.04622160e-02,   3.79809920e-03,   3.46310306e-03,
-            3.10070529e-03,   2.68579661e-03,   2.19341216e-03,
-            1.59875239e-03,   8.77203803e-04,   4.35952940e-06]],
-
-           [[1.88249866e-01,   2.05728128e-01,   2.23209861e-01,
-             2.40695072e-01,   2.58183766e-01,   2.75675949e-01,
-             2.93171625e-01,   3.10670801e-01,   3.32877903e-01,
-             3.58244116e-01,   3.83638063e-01,   4.09059827e-01,
-             4.34509485e-01,   4.59987117e-01,   4.85492795e-01],
-            [5.04317660e-01,   4.97523483e-01,   4.92879482e-01,
-             4.90522941e-01,   4.90521579e-01,   4.92874471e-01,
-             4.97514769e-01,   5.04314130e-01,   5.48356836e-01,
-             6.02679755e-01,   6.57930117e-01,   7.13582394e-01,
-             7.69129132e-01,   8.24101035e-01,   8.78084923e-01],
-            [9.05957986e-01,   9.00459829e-01,   9.01710827e-01,
-             9.09304816e-01,   9.21567297e-01,   9.36002510e-01,
-             9.49878533e-01,   9.60836244e-01,   9.50521017e-01,
-             9.42321192e-01,   9.36098294e-01,   9.31447978e-01,
-             9.27737112e-01,   9.24164130e-01,   9.19837458e-01],
-            [9.08479555e-01,   8.93119640e-01,   8.77756168e-01,
-             8.62389039e-01,   8.47018155e-01,   8.31643415e-01,
-             8.16264720e-01,   7.98248733e-01,   7.69688456e-01,
-             7.41111049e-01,   7.12515170e-01,   6.83899486e-01,
-             6.55262669e-01,   6.26603399e-01,   5.97920364e-01],
-            [5.71406981e-01,   5.45439361e-01,   5.19471340e-01,
-             4.93502919e-01,   4.67534097e-01,   4.41564875e-01,
-             4.15595252e-01,   3.91172349e-01,   3.69029170e-01,
-             3.46833147e-01,   3.24591169e-01,   3.02310146e-01,
-             2.79997004e-01,   2.57658679e-01,   2.35302110e-01]],
-
-           [[1.96102817e-02,   2.23037080e-02,   2.49835320e-02,
-             2.76497605e-02,   3.03024001e-02,   3.29414575e-02,
-             3.55669395e-02,   3.81788529e-02,   5.03598778e-02,
-             6.89209657e-02,   8.74757090e-02,   1.06024973e-01,
-             1.24569626e-01,   1.43110536e-01,   1.61648577e-01],
-            [1.82340027e-01,   2.15315774e-01,   2.53562955e-01,
-             2.95884521e-01,   3.41038527e-01,   3.87773687e-01,
-             4.34864157e-01,   4.81142673e-01,   5.00410360e-01,
-             5.19991397e-01,   5.47394263e-01,   5.82556639e-01,
-             6.25097005e-01,   6.74344521e-01,   7.29379582e-01],
-            [7.75227971e-01,   8.13001048e-01,   8.59395545e-01,
-             9.04577146e-01,   9.40342288e-01,   9.61653621e-01,
-             9.67479211e-01,   9.60799542e-01,   9.63421077e-01,
-             9.66445062e-01,   9.67352042e-01,   9.63790783e-01,
-             9.53840372e-01,   9.36234978e-01,   9.10530024e-01],
-            [8.86771441e-01,   8.67903107e-01,   8.48953980e-01,
-             8.29924111e-01,   8.10813555e-01,   7.91622365e-01,
-             7.72350598e-01,   7.51439565e-01,   7.24376642e-01,
-             6.97504841e-01,   6.70822717e-01,   6.44328750e-01,
-             6.18021348e-01,   5.91898843e-01,   5.65959492e-01],
-            [5.40017537e-01,   5.14048293e-01,   4.88079755e-01,
-             4.62111921e-01,   4.36144791e-01,   4.10178361e-01,
-             3.84212632e-01,   3.58028450e-01,   3.31935148e-01,
-             3.06445966e-01,   2.81566598e-01,   2.57302099e-01,
-             2.33656886e-01,   2.10634733e-01,   1.88238767e-01]]])
+            [3.29409498e-01, 3.59108764e-01, 3.88800969e-01,
+             4.18486092e-01, 4.48164112e-01, 4.77835010e-01,
+             5.07498765e-01, 5.37155355e-01, 5.65419479e-01,
+             5.92686124e-01, 6.19861622e-01, 6.46945403e-01,
+             6.73936907e-01, 7.00835579e-01, 7.27640871e-01],
+            [7.58680358e-01, 8.01695237e-01, 8.35686284e-01,
+             8.60598212e-01, 8.76625002e-01, 8.84194741e-01,
+             8.83948647e-01, 8.76714923e-01, 8.95016030e-01,
+             9.14039881e-01, 9.27287161e-01, 9.36546985e-01,
+             9.43656076e-01, 9.50421050e-01, 9.58544227e-01],
+            [9.86916929e-01, 1.02423117e+00, 1.03591220e+00,
+             1.02666645e+00, 1.00491333e+00, 9.80759775e-01,
+             9.63746819e-01, 9.60798629e-01, 9.47739946e-01,
+             9.27428067e-01, 9.01184523e-01, 8.71168132e-01,
+             8.40161241e-01, 8.11290344e-01, 7.87705814e-01],
+            [7.57749840e-01, 7.20020026e-01, 6.82329616e-01,
+             6.44678929e-01, 6.07068282e-01, 5.69497990e-01,
+             5.31968369e-01, 4.94025422e-01, 4.54275131e-01,
+             4.14517560e-01, 3.74757709e-01, 3.35000583e-01,
+             2.95251189e-01, 2.55514533e-01, 2.15795621e-01],
+            [1.85805611e-01, 1.58245609e-01, 1.30686714e-01,
+             1.03128926e-01, 7.55722460e-02, 4.80166757e-02,
+             2.04622160e-02, 3.79809920e-03, 3.46310306e-03,
+             3.10070529e-03, 2.68579661e-03, 2.19341216e-03,
+             1.59875239e-03, 8.77203803e-04, 4.35952940e-06]],
+
+            [[1.88249866e-01, 2.05728128e-01, 2.23209861e-01,
+              2.40695072e-01, 2.58183766e-01, 2.75675949e-01,
+              2.93171625e-01, 3.10670801e-01, 3.32877903e-01,
+              3.58244116e-01, 3.83638063e-01, 4.09059827e-01,
+              4.34509485e-01, 4.59987117e-01, 4.85492795e-01],
+             [5.04317660e-01, 4.97523483e-01, 4.92879482e-01,
+              4.90522941e-01, 4.90521579e-01, 4.92874471e-01,
+              4.97514769e-01, 5.04314130e-01, 5.48356836e-01,
+              6.02679755e-01, 6.57930117e-01, 7.13582394e-01,
+              7.69129132e-01, 8.24101035e-01, 8.78084923e-01],
+             [9.05957986e-01, 9.00459829e-01, 9.01710827e-01,
+              9.09304816e-01, 9.21567297e-01, 9.36002510e-01,
+              9.49878533e-01, 9.60836244e-01, 9.50521017e-01,
+              9.42321192e-01, 9.36098294e-01, 9.31447978e-01,
+              9.27737112e-01, 9.24164130e-01, 9.19837458e-01],
+             [9.08479555e-01, 8.93119640e-01, 8.77756168e-01,
+              8.62389039e-01, 8.47018155e-01, 8.31643415e-01,
+              8.16264720e-01, 7.98248733e-01, 7.69688456e-01,
+              7.41111049e-01, 7.12515170e-01, 6.83899486e-01,
+              6.55262669e-01, 6.26603399e-01, 5.97920364e-01],
+             [5.71406981e-01, 5.45439361e-01, 5.19471340e-01,
+              4.93502919e-01, 4.67534097e-01, 4.41564875e-01,
+              4.15595252e-01, 3.91172349e-01, 3.69029170e-01,
+              3.46833147e-01, 3.24591169e-01, 3.02310146e-01,
+              2.79997004e-01, 2.57658679e-01, 2.35302110e-01]],
+
+            [[1.96102817e-02, 2.23037080e-02, 2.49835320e-02,
+              2.76497605e-02, 3.03024001e-02, 3.29414575e-02,
+              3.55669395e-02, 3.81788529e-02, 5.03598778e-02,
+              6.89209657e-02, 8.74757090e-02, 1.06024973e-01,
+              1.24569626e-01, 1.43110536e-01, 1.61648577e-01],
+             [1.82340027e-01, 2.15315774e-01, 2.53562955e-01,
+              2.95884521e-01, 3.41038527e-01, 3.87773687e-01,
+              4.34864157e-01, 4.81142673e-01, 5.00410360e-01,
+              5.19991397e-01, 5.47394263e-01, 5.82556639e-01,
+              6.25097005e-01, 6.74344521e-01, 7.29379582e-01],
+             [7.75227971e-01, 8.13001048e-01, 8.59395545e-01,
+              9.04577146e-01, 9.40342288e-01, 9.61653621e-01,
+              9.67479211e-01, 9.60799542e-01, 9.63421077e-01,
+              9.66445062e-01, 9.67352042e-01, 9.63790783e-01,
+              9.53840372e-01, 9.36234978e-01, 9.10530024e-01],
+             [8.86771441e-01, 8.67903107e-01, 8.48953980e-01,
+              8.29924111e-01, 8.10813555e-01, 7.91622365e-01,
+              7.72350598e-01, 7.51439565e-01, 7.24376642e-01,
+              6.97504841e-01, 6.70822717e-01, 6.44328750e-01,
+              6.18021348e-01, 5.91898843e-01, 5.65959492e-01],
+             [5.40017537e-01, 5.14048293e-01, 4.88079755e-01,
+              4.62111921e-01, 4.36144791e-01, 4.10178361e-01,
+              3.84212632e-01, 3.58028450e-01, 3.31935148e-01,
+              3.06445966e-01, 2.81566598e-01, 2.57302099e-01,
+              2.33656886e-01, 2.10634733e-01, 1.88238767e-01]]])
 
         np.testing.assert_allclose(values, expected)
 
@@ -1553,7 +1716,29 @@ class TestXRImage(unittest.TestCase):
                                    alpha.reshape((1,) + alpha.shape)))
         np.testing.assert_allclose(values, expected)
 
+    def test_colorize_rgba(self):
+        """Test colorize with an RGBA colormap."""
+        import xarray as xr
+        from trollimage import xrimage
+        from trollimage.colormap import Colormap
+
+        # RGBA colormap
+        bw = Colormap(
+            (0.0, (1.0, 1.0, 1.0, 1.0)),
+            (1.0, (0.0, 0.0, 0.0, 0.5)),
+        )
+
+        arr = np.arange(75).reshape(5, 15) / 74.
+        data = xr.DataArray(arr.copy(), dims=['y', 'x'])
+        img = xrimage.XRImage(data)
+        img.colorize(bw)
+        values = img.data.compute()
+        self.assertTupleEqual((4, 5, 15), values.shape)
+        np.testing.assert_allclose(values[:, 0, 0], [1.0, 1.0, 1.0, 1.0], rtol=1e-03)
+        np.testing.assert_allclose(values[:, -1, -1], [0.0, 0.0, 0.0, 0.5])
+
     def test_palettize(self):
+        """Test palettize with an RGB colormap."""
         import xarray as xr
         from trollimage import xrimage
         from trollimage.colormap import brbg
@@ -1572,6 +1757,27 @@ class TestXRImage(unittest.TestCase):
             [8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10]]])
         np.testing.assert_allclose(values, expected)
 
+    def test_palettize_rgba(self):
+        """Test palettize with an RGBA colormap."""
+        import xarray as xr
+        from trollimage import xrimage
+        from trollimage.colormap import Colormap
+
+        # RGBA colormap
+        bw = Colormap(
+            (0.0, (1.0, 1.0, 1.0, 1.0)),
+            (1.0, (0.0, 0.0, 0.0, 0.5)),
+        )
+
+        arr = np.arange(75).reshape(5, 15) / 74.
+        data = xr.DataArray(arr.copy(), dims=['y', 'x'])
+        img = xrimage.XRImage(data)
+        img.palettize(bw)
+
+        values = img.data.values
+        self.assertTupleEqual((1, 5, 15), values.shape)
+        self.assertTupleEqual((2, 4), bw.colors.shape)
+
     def test_merge(self):
         pass
 


=====================================
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.6.3)"
-    git_full = "69942415f7bfbd9160189534ca60782eaa2e4b59"
-    git_date = "2018-12-20 15:10:36 -0600"
+    git_refnames = " (HEAD -> master, tag: v1.7.0)"
+    git_full = "d35a7665ad475ff230e457085523e21f2cd3f454"
+    git_date = "2019-02-28 12:49:51 -0600"
     keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
     return keywords
 


=====================================
trollimage/xrimage.py
=====================================
@@ -47,7 +47,6 @@ try:
 except ImportError:
     rasterio = None
 
-
 try:
     # rasterio 1.0+
     from rasterio.windows import Window
@@ -58,7 +57,6 @@ except ImportError:
         """Replace the missing Window object in rasterio < 1.0."""
         return (y_off, y_off + y_size), (x_off, x_off + x_size)
 
-
 logger = logging.getLogger(__name__)
 
 
@@ -72,6 +70,7 @@ class RIOFile(object):
         self.kwargs = kwargs
         self.rfile = None
         self._closed = True
+        self.overviews = kwargs.pop('overviews', None)
 
     def __setitem__(self, key, item):
         """Put the data chunk in the image."""
@@ -104,6 +103,9 @@ class RIOFile(object):
 
     def close(self):
         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
 
@@ -164,7 +166,11 @@ def color_interp(data):
 
 
 class XRImage(object):
-    """Image class using an :class:`xarray.DataArray` as internal storage."""
+    """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.
+    """
 
     def __init__(self, data):
         """Initialize the image with a :class:`~xarray.DataArray`."""
@@ -219,7 +225,7 @@ class XRImage(object):
         return ''.join(self.data['bands'].values)
 
     def save(self, filename, fformat=None, fill_value=None, compute=True,
-             **format_kwargs):
+             keep_palette=False, cmap=None, **format_kwargs):
         """Save the image to the given *filename*.
 
         Args:
@@ -229,6 +235,9 @@ class XRImage(object):
                            `rasterio` or `PIL` libraries ('jpg', 'png',
                            'tif'). By default this is determined by the
                            extension of the provided filename.
+                           If the format allows, geographical information will
+                           be saved to the ouput file, in the form of grid
+                           mapping or ground control points.
             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.
@@ -237,6 +246,11 @@ class XRImage(object):
                             a `dask.Delayed` object or a tuple of
                             ``(source, target)`` to be passed to
                             `dask.array.store`.
+            keep_palette (bool): Saves the palettized version of the image if
+                                 set to True. False by default.
+            cmap (Colormap or dict): Colormap to be applied to the image when
+                                     saving with rasterio, used with
+                                     keep_palette=True. Should be uint8.
             format_kwargs: Additional format options to pass to `rasterio`
                            or `PIL` saving methods.
 
@@ -250,33 +264,45 @@ class XRImage(object):
 
         """
         fformat = fformat or os.path.splitext(filename)[1][1:4]
-        if fformat == 'tif' and rasterio:
+        if fformat in ('tif', 'jp2') and rasterio:
             return self.rio_save(filename, fformat=fformat,
                                  fill_value=fill_value, compute=compute,
+                                 keep_palette=keep_palette, cmap=cmap,
                                  **format_kwargs)
         else:
             return self.pil_save(filename, fformat, fill_value,
                                  compute=compute, **format_kwargs)
 
     def rio_save(self, filename, fformat=None, fill_value=None,
-                 dtype=np.uint8, compute=True, tags=None, **format_kwargs):
-        """Save the image using rasterio."""
+                 dtype=np.uint8, compute=True, tags=None,
+                 keep_palette=False, cmap=None,
+                 **format_kwargs):
+        """Save the image using rasterio.
+
+        Overviews can be added to the file using the `overviews` kwarg, eg::
+
+          img.rio_save('myfile.tif', overviews=[2, 4, 8, 16])
+
+        """
         fformat = fformat or os.path.splitext(filename)[1][1:4]
         drivers = {'jpg': 'JPEG',
                    'png': 'PNG',
-                   'tif': 'GTiff'}
+                   'tif': 'GTiff',
+                   'jp2': 'JP2OpenJPEG'}
         driver = drivers.get(fformat, fformat)
 
         if tags is None:
             tags = {}
 
-        data, mode = self.finalize(fill_value, dtype=dtype)
+        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
         transform = None
-        if driver == 'GTiff':
+        if driver in ['GTiff', 'JP2OpenJPEG']:
             if not np.issubdtype(data.dtype, np.floating):
                 format_kwargs.setdefault('compress', 'DEFLATE')
             photometric_map = {
@@ -298,13 +324,20 @@ class XRImage(object):
                 transform = rasterio.transform.from_bounds(west, south,
                                                            east, north,
                                                            width, height)
-                if "start_time" in data.attrs:
-                    stime = data.attrs['start_time']
-                    stime_str = stime.strftime("%Y:%m:%d %H:%M:%S")
-                    tags.setdefault('TIFFTAG_DATETIME', stime_str)
 
-            except (KeyError, AttributeError):
+            except KeyError:  # No area
                 logger.info("Couldn't create geotransform")
+            except AttributeError:
+                try:
+                    gcps = data.attrs['area'].lons.attrs['gcps']
+                    crs = data.attrs['area'].lons.attrs['crs']
+                except KeyError:
+                    logger.info("Couldn't create geotransform")
+
+            if "start_time" in data.attrs:
+                stime = data.attrs['start_time']
+                stime_str = stime.strftime("%Y:%m:%d %H:%M:%S")
+                tags.setdefault('TIFFTAG_DATETIME', stime_str)
         elif driver == 'JPEG' and 'A' in mode:
             raise ValueError('JPEG does not support alpha')
 
@@ -314,11 +347,25 @@ class XRImage(object):
                          count=data.sizes['bands'],
                          dtype=dtype,
                          nodata=fill_value,
-                         crs=crs, transform=transform, **format_kwargs)
+                         crs=crs,
+                         transform=transform,
+                         gcps=gcps,
+                         **format_kwargs)
         r_file.open()
-        r_file.colorinterp = color_interp(data)
+        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':
+                raise ValueError('Rasterio only supports 8-bit colormaps')
+            try:
+                from trollimage.colormap import Colormap
+                cmap = cmap.to_rio() if isinstance(cmap, Colormap) else cmap
+                r_file.rfile.write_colormap(1, cmap)
+            except AttributeError:
+                raise ValueError("Colormap is not formatted correctly")
+
         if compute:
             # write data to the file now
             res = da.store(data.data, r_file)
@@ -343,11 +390,8 @@ class XRImage(object):
             # Take care of GeoImage.tags (if any).
             format_kwargs['pnginfo'] = self._pngmeta()
 
-        def _create_save_image(fill_value, filename, fformat, format_kwargs):
-            img = self.pil_image(fill_value)
-            img.save(filename, fformat, **format_kwargs)
-        delay = dask.delayed(_create_save_image)(
-            fill_value, filename, fformat, format_kwargs)
+        img = self.pil_image(fill_value, compute=False)
+        delay = img.save(filename, fformat, **format_kwargs)
         if compute:
             return delay.compute()
         return delay
@@ -449,28 +493,36 @@ class XRImage(object):
         """Convert the image from P or PA to RGB or RGBA."""
         self._check_modes(("P", "PA"))
 
-        if self.mode.endswith("A"):
-            alpha = self.data.sel(bands=["A"]).data
-            mode = mode + "A" if not mode.endswith("A") else mode
-        else:
-            alpha = None
-
         if not self.palette:
             raise RuntimeError("Can't convert palettized image, missing palette.")
-
         pal = np.array(self.palette)
         pal = da.from_array(pal, chunks=pal.shape)
-        flat_indexes = self.data.data[0].ravel().astype('int64')
-        new_shape = (3,) + self.data.shape[1:3]
+
+        if pal.shape[1] == 4:
+            # colormap's alpha overrides data alpha
+            mode = "RGBA"
+            alpha = None
+        elif self.mode.endswith("A"):
+            # add a new/fake 'bands' dimension to the end
+            alpha = self.data.sel(bands="A").data[..., None]
+            mode = mode + "A" if not mode.endswith("A") else mode
+        else:
+            alpha = None
+
+        flat_indexes = self.data.sel(bands='P').data.ravel().astype('int64')
+        dim_sizes = ((key, val) for key, val in self.data.sizes.items() if key != 'bands')
+        dims, new_shape = zip(*dim_sizes)
+        dims = dims + ('bands',)
+        new_shape = new_shape + (pal.shape[1],)
         new_data = pal[flat_indexes].reshape(new_shape)
         coords = dict(self.data.coords)
         coords["bands"] = list(mode)
 
         if alpha is not None:
-            new_arr = da.concatenate((new_data, alpha), axis=0)
-            data = xr.DataArray(new_arr, coords=coords, attrs=self.data.attrs, dims=self.data.dims)
+            new_arr = da.concatenate((new_data, alpha), axis=-1)
+            data = xr.DataArray(new_arr, coords=coords, attrs=self.data.attrs, dims=dims)
         else:
-            data = xr.DataArray(new_data, coords=coords, attrs=self.data.attrs, dims=self.data.dims)
+            data = xr.DataArray(new_data, coords=coords, attrs=self.data.attrs, dims=dims)
 
         return data
 
@@ -531,14 +583,14 @@ class XRImage(object):
             new_img.palette = self.palette
         return new_img
 
-    def _finalize(self, fill_value=None, dtype=np.uint8):
+    def _finalize(self, fill_value=None, dtype=np.uint8, keep_palette=False, cmap=None):
         """Wrapper around 'finalize' method for backwards compatibility."""
         import warnings
         warnings.warn("'_finalize' is deprecated, use 'finalize' instead.",
                       DeprecationWarning)
-        return self.finalize(fill_value, dtype)
+        return self.finalize(fill_value, dtype, keep_palette, cmap)
 
-    def finalize(self, fill_value=None, dtype=np.uint8):
+    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).
@@ -550,10 +602,16 @@ class XRImage(object):
         in the ``DataArray`` ``.attrs`` dictionary.
 
         """
-        if self.mode == "P":
-            return self.convert("RGB").finalize(fill_value=fill_value, dtype=dtype)
-        if self.mode == "PA":
-            return self.convert("RGBA").finalize(fill_value=fill_value, dtype=dtype)
+        if keep_palette and not self.mode.startswith('P'):
+            keep_palette = False
+
+        if not keep_palette:
+            if self.mode == "P":
+                return self.convert("RGB").finalize(fill_value=fill_value, dtype=dtype,
+                                                    keep_palette=keep_palette, cmap=cmap)
+            if self.mode == "PA":
+                return self.convert("RGBA").finalize(fill_value=fill_value, dtype=dtype,
+                                                     keep_palette=keep_palette, cmap=cmap)
 
         if np.issubdtype(dtype, np.floating) and fill_value is None:
             logger.warning("Image with floats cannot be transparent, so "
@@ -563,34 +621,48 @@ class XRImage(object):
         final_data = self.data
         # 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 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))
+        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
         return final_data, ''.join(final_data['bands'].values)
 
-    def pil_image(self, fill_value=None):
-        """Return a PIL image from the current image."""
+    def pil_image(self, fill_value=None, compute=True):
+        """Return a PIL image from the current image.
+
+        Args:
+            fill_value (int or float): Value to use for NaN null values.
+                See :meth:`~trollimage.xrimage.XRImage.finalize` for more
+                info.
+            compute (bool): Whether to return a fully computed PIL.Image
+                object (True) or return a dask Delayed object representing
+                the Image (False). This is True by default.
+
+        """
         channels, mode = self.finalize(fill_value)
-        res = np.asanyarray(channels.transpose('y', 'x', 'bands').values)
-        return PILImage.fromarray(np.squeeze(res), mode)
+        res = channels.transpose('y', 'x', 'bands')
+        img = dask.delayed(PILImage.fromarray)(np.squeeze(res.data), mode)
+        if compute:
+            img = img.compute()
+        return img
 
     def xrify_tuples(self, tup):
         """Make xarray.DataArray from tuple."""
@@ -609,7 +681,7 @@ class XRImage(object):
         undefined outside the normal [0,1] range of the channels.
         """
         if isinstance(gamma, (list, tuple)):
-           gamma = self.xrify_tuples(gamma)
+            gamma = self.xrify_tuples(gamma)
         elif gamma == 1.0:
             return
 
@@ -657,6 +729,25 @@ class XRImage(object):
         else:
             raise TypeError("Stretch parameter must be a string or a tuple.")
 
+    @staticmethod
+    def _compute_quantile(data, dims, cutoffs):
+        """Helper method for stretch_linear.
+
+        Dask delayed functions need to be non-internal functions (created
+        inside a function) to be serializable on a multi-process scheduler.
+
+        Quantile requires the data to be loaded since it not supported on
+        dask arrays yet.
+
+        """
+        # numpy doesn't get a 'quantile' function until 1.15
+        # for better backwards compatibility we use xarray's version
+        data_arr = xr.DataArray(data, dims=dims)
+        # delayed will provide us the fully computed xarray with ndarray
+        left, right = data_arr.quantile([cutoffs[0], 1. - cutoffs[1]], dim=['x', 'y'])
+        logger.debug("Interval: left=%s, right=%s", str(left), str(right))
+        return left.data, right.data
+
     def stretch_linear(self, cutoffs=(0.005, 0.005)):
         """Stretch linearly the contrast of the current image.
 
@@ -668,21 +759,13 @@ class XRImage(object):
         logger.debug("Left and right quantiles: " +
                      str(cutoffs[0]) + " " + str(cutoffs[1]))
 
-        # Quantile requires the data to be loaded, not supported on dask arrays
-        def _compute_quantile(data, cutoffs):
-            # delayed will provide us the fully computed xarray with ndarray
-            left, right = data.quantile([cutoffs[0], 1. - cutoffs[1]],
-                                        dim=['x', 'y'])
-            logger.debug("Interval: left=%s, right=%s", str(left), str(right))
-            return left.data, right.data
-
         cutoff_type = np.float64
         # numpy percentile (which quantile calls) returns 64-bit floats
         # unless the value is a higher order float
         if np.issubdtype(self.data.dtype, np.floating) and \
                 np.dtype(self.data.dtype).itemsize > 8:
             cutoff_type = self.data.dtype
-        left, right = dask.delayed(_compute_quantile, nout=2)(self.data, cutoffs)
+        left, right = dask.delayed(self._compute_quantile, nout=2)(self.data.data, self.data.dims, cutoffs)
         left_data = da.from_delayed(left,
                                     shape=(self.data.sizes['bands'],),
                                     dtype=cutoff_type)
@@ -807,7 +890,7 @@ class XRImage(object):
         highest unpercieved stimulus), and k is the factor.
         """
         attrs = self.data.attrs
-        self.data = k*xu.log(self.data / s0)
+        self.data = k * xu.log(self.data / s0)
         self.data.attrs = attrs
 
     def invert(self, invert=True):
@@ -880,9 +963,12 @@ class XRImage(object):
             return np.concatenate(channels, axis=0)
 
         new_data = l_data.data.map_blocks(_colorize, colormap,
-                                          chunks=(3,) + l_data.data.chunks[1:], dtype=np.float64)
+                                          chunks=(colormap.colors.shape[1],) + l_data.data.chunks[1:],
+                                          dtype=np.float64)
 
-        if alpha is not None:
+        if colormap.colors.shape[1] == 4:
+            mode = "RGBA"
+        elif alpha is not None:
             new_data = da.concatenate([new_data, alpha.data], axis=0)
             mode = "RGBA"
         else:



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

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/commit/c6e918db1f44da9b0f675df0e830f9815cf4ce70
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/20190301/4d27efe8/attachment-0001.html>


More information about the Pkg-grass-devel mailing list