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

Antonio Valentino gitlab at salsa.debian.org
Sun Sep 22 08:18:33 BST 2019



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


Commits:
b2240f1c by Antonio Valentino at 2019-09-22T06:57:07Z
New upstream version 1.10.0
- - - - -


6 changed files:

- + .pre-commit-config.yaml
- CHANGELOG.md
- trollimage/colormap.py
- trollimage/tests/test_image.py
- trollimage/version.py
- trollimage/xrimage.py


Changes:

=====================================
.pre-commit-config.yaml
=====================================
@@ -0,0 +1,8 @@
+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]


=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,20 @@
+## 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/colormap.py
=====================================
@@ -88,9 +88,13 @@ class Colormap(object):
 
     """
 
-    def __init__(self, *tuples):
-        values = [a for (a, b) in tuples]
-        colors = [b for (a, b) in 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)
         self.colors = np.array(colors)
 


=====================================
trollimage/tests/test_image.py
=====================================
@@ -21,8 +21,7 @@
 
 # You should have received a copy of the GNU General Public License
 # along with mpop.  If not, see <http://www.gnu.org/licenses/>.
-"""Module for testing the imageo.image module.
-"""
+"""Module for testing the image and xrimage modules."""
 import os
 import sys
 import random
@@ -58,18 +57,15 @@ class CustomScheduler(object):
 
 
 class TestEmptyImage(unittest.TestCase):
-    """Class for testing the mpop.imageo.image module
-    """
+    """Class for testing the mpop.imageo.image module."""
 
     def setUp(self):
-        """Setup the test.
-        """
+        """Set up the test case."""
         self.img = image.Image()
         self.modes = ["L", "LA", "RGB", "RGBA", "YCbCr", "YCbCrA", "P", "PA"]
 
     def test_shape(self):
-        """Shape of an empty image.
-        """
+        """Shape of an empty image."""
         oldmode = self.img.mode
         for mode in self.modes:
             self.img.convert(mode)
@@ -77,13 +73,11 @@ class TestEmptyImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_is_empty(self):
-        """Test if an image is empty.
-        """
+        """Test if an image is empty."""
         self.assertEqual(self.img.is_empty(), True)
 
     def test_clip(self):
-        """Clip an empty image.
-        """
+        """Clip an empty image."""
         oldmode = self.img.mode
         for mode in self.modes:
             self.img.convert(mode)
@@ -91,8 +85,7 @@ class TestEmptyImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_convert(self):
-        """Convert an empty image.
-        """
+        """Convert an empty image."""
         for mode1 in self.modes:
             for mode2 in self.modes:
                 self.img.convert(mode1)
@@ -108,8 +101,7 @@ class TestEmptyImage(unittest.TestCase):
         self.assertRaises(ValueError, self.img.convert, randstr)
 
     def test_stretch(self):
-        """Stretch an empty image
-        """
+        """Stretch an empty image."""
         oldmode = self.img.mode
         for mode in self.modes:
             self.img.convert(mode)
@@ -136,8 +128,7 @@ class TestEmptyImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_gamma(self):
-        """Gamma correction on an empty image.
-        """
+        """Gamma correction on an empty image."""
         oldmode = self.img.mode
         for mode in self.modes:
             self.img.convert(mode)
@@ -158,8 +149,7 @@ class TestEmptyImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_invert(self):
-        """Invert an empty image.
-        """
+        """Invert an empty image."""
         oldmode = self.img.mode
         for mode in self.modes:
             self.img.convert(mode)
@@ -174,8 +164,7 @@ class TestEmptyImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_pil_image(self):
-        """Return an empty PIL image.
-        """
+        """Return an empty PIL image."""
         oldmode = self.img.mode
         for mode in self.modes:
             self.img.convert(mode)
@@ -189,8 +178,7 @@ class TestEmptyImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_putalpha(self):
-        """Add an alpha channel to en empty image
-        """
+        """Add an alpha channel to en empty image."""
         # Putting alpha channel to an empty image should not do anything except
         # change the mode if necessary.
         oldmode = self.img.mode
@@ -212,8 +200,7 @@ class TestEmptyImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_save(self):
-        """Save an empty image.
-        """
+        """Save an empty image."""
         oldmode = self.img.mode
         for mode in self.modes:
             self.img.convert(mode)
@@ -222,8 +209,7 @@ class TestEmptyImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_replace_luminance(self):
-        """Replace luminance in an empty image.
-        """
+        """Replace luminance in an empty image."""
         oldmode = self.img.mode
         for mode in self.modes:
             self.img.convert(mode)
@@ -234,13 +220,11 @@ class TestEmptyImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_resize(self):
-        """Resize an empty image.
-        """
+        """Resize an empty image."""
         self.assertRaises(ValueError, self.img.resize, (10, 10))
 
     def test_merge(self):
-        """Merging of an empty image with another.
-        """
+        """Merging of an empty image with another."""
         newimg = image.Image()
         self.assertRaises(ValueError, self.img.merge, newimg)
         newimg = image.Image(np.array([[1, 2], [3, 4]]))
@@ -250,20 +234,16 @@ class TestEmptyImage(unittest.TestCase):
 
 
 class TestImageCreation(unittest.TestCase):
-    """Class for testing the mpop.imageo.image module
-    """
+    """Class for testing the mpop.imageo.image module."""
 
     def setUp(self):
-        """Setup the test.
-        """
+        """Set up the test case."""
         self.img = {}
         self.modes = ["L", "LA", "RGB", "RGBA", "YCbCr", "YCbCrA", "P", "PA"]
         self.modes_len = [1, 2, 3, 4, 3, 4, 1, 2]
 
     def test_creation(self):
-        """Creation of an image.
-        """
-
+        """Test creation of an image."""
         self.assertRaises(TypeError, image.Image,
                           channels=random.randint(1, 1000))
         self.assertRaises(TypeError, image.Image,
@@ -338,12 +318,10 @@ class TestImageCreation(unittest.TestCase):
 
 
 class TestRegularImage(unittest.TestCase):
-    """Class for testing the mpop.imageo.image module
-    """
+    """Class for testing the mpop.imageo.image module."""
 
     def setUp(self):
-        """Setup the test.
-        """
+        """Set up the test case."""
         one_channel = np.random.rand(random.randint(1, 10),
                                      random.randint(1, 10))
         self.rand_img = image.Image(channels=[one_channel] * 3,
@@ -370,8 +348,7 @@ class TestRegularImage(unittest.TestCase):
         os.chmod(self.tempdir, 0o444)
 
     def test_shape(self):
-        """Shape of an image.
-        """
+        """Shape of an image."""
         oldmode = self.img.mode
         for mode in self.modes:
             if mode == "P" or mode == "PA":
@@ -381,13 +358,11 @@ class TestRegularImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_is_empty(self):
-        """Test if an image is empty.
-        """
+        """Test if an image is empty."""
         self.assertEqual(self.img.is_empty(), False)
 
     def test_clip(self):
-        """Clip an image.
-        """
+        """Clip an image."""
         oldmode = self.img.mode
         for mode in self.modes:
             if mode == "P" or mode == "PA":
@@ -399,8 +374,7 @@ class TestRegularImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_convert(self):
-        """Convert an image.
-        """
+        """Convert an image."""
         i = 0
         for mode1 in self.modes:
             j = 0
@@ -437,8 +411,7 @@ class TestRegularImage(unittest.TestCase):
         self.assertRaises(ValueError, self.img.convert, randstr)
 
     def test_stretch(self):
-        """Stretch an image.
-        """
+        """Stretch an image."""
         oldmode = self.img.mode
 
         for mode in "L":
@@ -479,8 +452,7 @@ class TestRegularImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_gamma(self):
-        """Gamma correction on an image.
-        """
+        """Gamma correction on an image."""
         oldmode = self.img.mode
         for mode in self.modes:
             if mode == "P" or mode == "PA":
@@ -519,8 +491,7 @@ class TestRegularImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_invert(self):
-        """Invert an image.
-        """
+        """Invert an image."""
         oldmode = self.img.mode
         for mode in self.modes:
             if mode == "P" or mode == "PA":
@@ -543,11 +514,8 @@ class TestRegularImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_pil_image(self):
-        """Return an PIL image.
-        """
-
+        """Return an PIL image."""
         # FIXME: Should test on palette images
-
         oldmode = self.img.mode
         for mode in self.modes:
             if (mode == "YCbCr" or
@@ -561,8 +529,7 @@ class TestRegularImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_putalpha(self):
-        """Add an alpha channel.
-        """
+        """Add an alpha channel."""
         # Putting alpha channel to an image should not do anything except
         # change the mode if necessary.
         oldmode = self.img.mode
@@ -590,8 +557,7 @@ class TestRegularImage(unittest.TestCase):
     @unittest.skipIf(sys.platform.startswith('win'),
                      "Read-only tmp dir not working under Windows")
     def test_save(self):
-        """Save an image.
-        """
+        """Save an image."""
         oldmode = self.img.mode
         for mode in self.modes:
             if (mode == "YCbCr" or
@@ -614,8 +580,7 @@ class TestRegularImage(unittest.TestCase):
     @unittest.skipIf(sys.platform.startswith('win'),
                      "Read-only tmp dir not working under Windows")
     def test_save_jpeg(self):
-        """Save a jpeg image.
-        """
+        """Save a jpeg image."""
         oldmode = self.img.mode
         self.img.convert('L')
         self.img.save("test.jpg")
@@ -630,8 +595,7 @@ class TestRegularImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_replace_luminance(self):
-        """Replace luminance in an image.
-        """
+        """Replace luminance in an image."""
         oldmode = self.img.mode
         for mode in self.modes:
             if (mode == "P" or
@@ -651,8 +615,7 @@ class TestRegularImage(unittest.TestCase):
         self.img.convert(oldmode)
 
     def test_resize(self):
-        """Resize an image.
-        """
+        """Resize an image."""
         self.img.resize((6, 6))
         res = np.array([[0, 0, 0.5, 0.5, 0.5, 0.5],
                         [0, 0, 0.5, 0.5, 0.5, 0.5],
@@ -667,8 +630,7 @@ class TestRegularImage(unittest.TestCase):
         self.assertTrue(np.all(res == self.img.channels[0]))
 
     def test_merge(self):
-        """Merging of an image with another.
-        """
+        """Merging of an image with another."""
         newimg = image.Image()
         self.assertRaises(ValueError, self.img.merge, newimg)
         newimg = image.Image(np.array([[1, 2], [3, 4]]))
@@ -686,17 +648,16 @@ class TestRegularImage(unittest.TestCase):
                                EPSILON))
 
     def tearDown(self):
-        """Clean up the mess.
-        """
+        """Clean up the mess."""
         os.chmod(self.tempdir, 0o777)
         os.rmdir(self.tempdir)
 
 
 class TestFlatImage(unittest.TestCase):
-    """Test a flat image, ie an image where min == max.
-    """
+    """Test a flat image, ie an image where min == max."""
 
     def setUp(self):
+        """Set up the test case."""
         channel = np.ma.array([[0, 0.5, 0.5], [0.5, 0.25, 0.25]],
                               mask=[[1, 1, 1], [1, 1, 0]])
         self.img = image.Image(channels=[channel] * 3,
@@ -704,8 +665,7 @@ class TestFlatImage(unittest.TestCase):
         self.modes = ["L", "LA", "RGB", "RGBA", "YCbCr", "YCbCrA", "P", "PA"]
 
     def test_stretch(self):
-        """Stretch a flat image.
-        """
+        """Stretch a flat image."""
         self.img.stretch()
         self.assertTrue(self.img.channels[0].shape == (2, 3) and
                         np.ma.count_masked(self.img.channels[0]) == 5)
@@ -724,10 +684,10 @@ class TestFlatImage(unittest.TestCase):
 
 
 class TestNoDataImage(unittest.TestCase):
-    """Test an image filled with no data.
-    """
+    """Test an image filled with no data."""
 
     def setUp(self):
+        """Set up the test case."""
         channel = np.ma.array([[0, 0.5, 0.5], [0.5, 0.25, 0.25]],
                               mask=[[1, 1, 1], [1, 1, 1]])
         self.img = image.Image(channels=[channel] * 3,
@@ -735,8 +695,7 @@ class TestNoDataImage(unittest.TestCase):
         self.modes = ["L", "LA", "RGB", "RGBA", "YCbCr", "YCbCrA", "P", "PA"]
 
     def test_stretch(self):
-        """Stretch a no data image.
-        """
+        """Stretch a no data image."""
         self.img.stretch()
         self.assertTrue(self.img.channels[0].shape == (2, 3))
         self.img.stretch("crude")
@@ -752,16 +711,16 @@ class TestNoDataImage(unittest.TestCase):
 def random_string(length,
                   choices="abcdefghijklmnopqrstuvwxyz"
                           "ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
-    """Generates a random string with elements from *set* of the specified
-    *length*.
-    """
+    """Generate a random string with elements from *set* of the specified *length*."""
     return "".join([random.choice(choices)
                     for dummy in range(length)])
 
 
 class TestXRImage(unittest.TestCase):
+    """Test XRImage objects."""
 
     def test_init(self):
+        """Test object initialization."""
         import xarray as xr
         from trollimage import xrimage
         data = xr.DataArray([[0, 0.5, 0.5], [0.5, 0.25, 0.25]], dims=['y', 'x'])
@@ -793,9 +752,23 @@ class TestXRImage(unittest.TestCase):
         img = xrimage.XRImage(data)
         self.assertEqual(img.mode, 'YCbCrA')
 
+    def test_regression_double_format_save(self):
+        """Test that double format information isn't passed to save."""
+        import xarray as xr
+        from trollimage import xrimage
+
+        data = xr.DataArray(np.arange(75).reshape(5, 5, 3) / 74., dims=[
+            'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
+        with mock.patch.object(xrimage.XRImage, 'pil_save') as pil_save:
+            img = xrimage.XRImage(data)
+
+            img.save(filename='bla.png', fformat='png', format='png')
+            self.assertNotIn('format', pil_save.call_args_list[0][1])
+
     @unittest.skipIf(sys.platform.startswith('win'),
                      "'NamedTemporaryFile' not supported on Windows")
     def test_save(self):
+        """Test saving."""
         import xarray as xr
         import dask.array as da
         from dask.delayed import Delayed
@@ -1236,8 +1209,10 @@ class TestXRImage(unittest.TestCase):
         img = xrimage.XRImage(data)
         img.gamma(.5)
         self.assertTrue(np.allclose(img.data.values, arr ** 2))
+        self.assertDictEqual(img.data.attrs['enhancement_history'][0], {'gamma': 0.5})
 
         img.gamma([2., 2., 2.])
+        self.assertEqual(len(img.data.attrs['enhancement_history']), 2)
         self.assertTrue(np.allclose(img.data.values, arr))
 
     def test_crude_stretch(self):
@@ -1253,6 +1228,11 @@ class TestXRImage(unittest.TestCase):
         red = img.data.sel(bands='R')
         green = img.data.sel(bands='G')
         blue = img.data.sel(bands='B')
+        enhs = img.data.attrs['enhancement_history'][0]
+        scale_expected = np.array([0.01388889, 0.01388889, 0.01388889])
+        offset_expected = np.array([0., -0.01388889, -0.02777778])
+        np.testing.assert_allclose(enhs['scale'].values, scale_expected)
+        np.testing.assert_allclose(enhs['offset'].values, offset_expected)
         np.testing.assert_allclose(red, arr[:, :, 0] / 72.)
         np.testing.assert_allclose(green, (arr[:, :, 1] - 1.) / (73. - 1.))
         np.testing.assert_allclose(blue, (arr[:, :, 2] - 2.) / (74. - 2.))
@@ -1275,7 +1255,8 @@ class TestXRImage(unittest.TestCase):
         img = xrimage.XRImage(data)
 
         img.invert(True)
-
+        enhs = img.data.attrs['enhancement_history'][0]
+        self.assertDictEqual(enhs, {'scale': -1, 'offset': 1})
         self.assertTrue(np.allclose(img.data.values, 1 - arr))
 
         data = xr.DataArray(arr.copy(), dims=['y', 'x', 'bands'],
@@ -1299,6 +1280,9 @@ class TestXRImage(unittest.TestCase):
                             coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         img.stretch_linear()
+        enhs = img.data.attrs['enhancement_history'][0]
+        np.testing.assert_allclose(enhs['scale'].values, np.array([1.03815937, 1.03815937, 1.03815937]))
+        np.testing.assert_allclose(enhs['offset'].values, np.array([-0.00505051, -0.01907969, -0.03310887]), atol=1e-8)
         res = np.array([[[-0.005051, -0.005051, -0.005051],
                          [0.037037, 0.037037, 0.037037],
                          [0.079125, 0.079125, 0.079125],
@@ -1328,6 +1312,7 @@ class TestXRImage(unittest.TestCase):
         self.assertTrue(np.allclose(img.data.values, res, atol=1.e-6))
 
     def test_histogram_stretch(self):
+        """Test histogram stretching."""
         import xarray as xr
         from trollimage import xrimage
 
@@ -1336,6 +1321,8 @@ class TestXRImage(unittest.TestCase):
                             coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         img.stretch('histogram')
+        enhs = img.data.attrs['enhancement_history'][0]
+        self.assertDictEqual(enhs, {'hist_equalize': True})
         res = np.array([[[0., 0., 0.],
                          [0.04166667, 0.04166667, 0.04166667],
                          [0.08333333, 0.08333333, 0.08333333],
@@ -1369,6 +1356,7 @@ class TestXRImage(unittest.TestCase):
         self.assertTrue(np.allclose(img.data.values, res, atol=1.e-6))
 
     def test_logarithmic_stretch(self):
+        """Test logarithmic strecthing."""
         import xarray as xr
         from trollimage import xrimage
 
@@ -1377,6 +1365,8 @@ class TestXRImage(unittest.TestCase):
                             coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         img.stretch(stretch='logarithmic')
+        enhs = img.data.attrs['enhancement_history'][0]
+        self.assertDictEqual(enhs, {'log_factor': 100.0})
         res = np.array([[[0., 0., 0.],
                          [0.35484693, 0.35484693, 0.35484693],
                          [0.48307087, 0.48307087, 0.48307087],
@@ -1410,7 +1400,7 @@ class TestXRImage(unittest.TestCase):
         self.assertTrue(np.allclose(img.data.values, res, atol=1.e-6))
 
     def test_weber_fechner_stretch(self):
-        """S=2.3klog10I+C """
+        """Test applying S=2.3klog10I+C to the data."""
         import xarray as xr
         from trollimage import xrimage
 
@@ -1419,6 +1409,8 @@ class TestXRImage(unittest.TestCase):
                             coords={'bands': ['R', 'G', 'B']})
         img = xrimage.XRImage(data)
         img.stretch_weber_fechner(2.5, 0.2)
+        enhs = img.data.attrs['enhancement_history'][0]
+        self.assertDictEqual(enhs, {'weber_fechner': (2.5, 0.2)})
         res = np.array([[[-np.inf, -6.73656795, -5.0037],
                          [-3.99003723, -3.27083205, -2.71297317],
                          [-2.25716928, -1.87179258, -1.5379641],
@@ -1452,27 +1444,35 @@ class TestXRImage(unittest.TestCase):
         self.assertTrue(np.allclose(img.data.values, res, atol=1.e-6))
 
     def test_jpeg_save(self):
+        """Test saving to jpeg."""
         pass
 
     def test_gtiff_save(self):
+        """Test saving to geotiff."""
         pass
 
     def test_save_masked(self):
+        """Test saving masked data."""
         pass
 
     def test_LA_save(self):
+        """Test LA saving."""
         pass
 
     def test_L_save(self):
+        """Test L saving."""
         pass
 
     def test_P_save(self):
+        """Test P saving."""
         pass
 
     def test_PA_save(self):
+        """Test PA saving."""
         pass
 
     def test_convert_modes(self):
+        """Test modes convertions."""
         import dask
         import xarray as xr
         from trollimage import xrimage
@@ -1782,7 +1782,7 @@ class TestXRImage(unittest.TestCase):
         self.assertTupleEqual((2, 4), bw.colors.shape)
 
     def test_stack(self):
-
+        """Test stack."""
         import xarray as xr
         from trollimage import xrimage
 
@@ -1810,9 +1810,11 @@ class TestXRImage(unittest.TestCase):
         np.testing.assert_allclose(bkg.data, res.data, rtol=1e-05)
 
     def test_merge(self):
+        """Test merge."""
         pass
 
     def test_blend(self):
+        """Test blend."""
         import xarray as xr
         from trollimage import xrimage
 
@@ -1854,13 +1856,15 @@ class TestXRImage(unittest.TestCase):
             img1.blend(wrongimg)
 
     def test_replace_luminance(self):
+        """Test luminance replacement."""
         pass
 
     def test_putalpha(self):
+        """Test putalpha."""
         pass
 
     def test_show(self):
-        """Test that the show commands calls PIL.show"""
+        """Test that the show commands calls PIL.show."""
         import xarray as xr
         from trollimage import xrimage
 
@@ -1873,7 +1877,7 @@ class TestXRImage(unittest.TestCase):
 
 
 def suite():
-    """The suite for test_image."""
+    """Create the suite for test_image."""
     loader = unittest.TestLoader()
     mysuite = unittest.TestSuite()
     mysuite.addTest(loader.loadTestsFromTestCase(TestEmptyImage))


=====================================
trollimage/version.py
=====================================
@@ -23,9 +23,9 @@ def get_keywords():
     # setup.py/versioneer.py will grep for the variable names, so they must
     # each be defined on a line of their own. _version.py will just call
     # get_keywords().
-    git_refnames = " (tag: v1.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
 


=====================================
trollimage/xrimage.py
=====================================
@@ -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,7 +330,11 @@ class XRImage(object):
                                          photometric_map[mode.upper()])
 
             try:
-                crs = rasterio.crs.CRS(data.attrs['area'].proj_dict)
+                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']
                 transform = rasterio.transform.from_bounds(west, south,
@@ -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")
 



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

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/commit/b2240f1cb42c5a8ed6055257d2c41f044b4ab4b0
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/20190922/69897ab0/attachment-0001.html>


More information about the Pkg-grass-devel mailing list