Repository: dlr-eoc/ukis-pysat

Issue:
Image dataset update
When we do a transformation on the `Image` object, the underlying rasterio `dataset` should be updated accordingly. This does not happen at the moment and may be confusing and lead to problems when chaining several transformations. For example:

```python
img = Image(path="dummy.tif")
img.mask_img().warp()
```
This would create an incorrect output, because `mask_img()` updates `transform` and `arr`, but not the entire `dataset`. `Dataset` is however used by `warp()`. At the moment `warp()` would be applied on the original `dataset` and not on the transformed one from `mask_img()`.

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 7d28bc2..ba1146c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -20,7 +20,7 @@ Added
 *****
 - ``raster.Image()``: expanded test_arr to test AttributeError #31
 - ``raster.Image()``: optional dimorder for arr and according test #31
-- dn2toa tests and testfiles #17
+- `dn2toa` tests and testfiles #17
 - ``data.source()``: accept WKT string as AOI #26
 - ``data.source()``: check if an AOI string is a file or a WKT string #26
 
@@ -31,6 +31,7 @@ Fixed
 Changed
 *******
 - ``raster.Image()``: changed ``dn2toa(platform, metadata, wavelengths)`` to ``dn2toa(platform, mtl_file, wavelengths)`` #17
+- ``raster.Image()``: auto-update of in-memory `dataset` #35
 
 
 [0.3.0]  (2020-05-26)
diff --git a/docs/conf.py b/docs/conf.py
index a33e834..367a590 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -29,6 +29,7 @@ version = str(subprocess.check_output(["git", "describe", "--always"]).strip())
 # -- General configuration ---------------------------------------------------
 
 master_doc = 'index'
+autoclass_content = 'both'
 
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
diff --git a/ukis_pysat/raster.py b/ukis_pysat/raster.py
index 398e040..fa4d337 100644
--- a/ukis_pysat/raster.py
+++ b/ukis_pysat/raster.py
@@ -29,6 +29,12 @@ logger = logging.getLogger(__name__)
 
 class Image:
     def __init__(self, path=None, dataset=None, arr=None, dimorder="first"):
+        """
+        :param path: str, path to raster (default: None)
+        :param dataset: rasterio.io.DatasetReader (default: None)
+        :param arr: np.ndarray (default: None)
+        :param dimorder: order of channels or bands 'first' or 'last' (default: 'first)
+        """
         if path:
             if isinstance(path, str):
                 self.dataset = rasterio.open(path)
@@ -50,6 +56,7 @@ class Image:
 
     @property
     def arr(self):
+        """array property"""
         if self.dimorder == "first":
             return self.__arr
         elif self.dimorder == "last":
@@ -67,12 +74,13 @@ class Image:
         return windows.bounds(valid_data_window, windows.transform(valid_data_window, self.transform))
 
     def mask_image(self, bbox, crop=True, pad=False, **kwargs):
-        """
-        TODO https://github.com/mapbox/rasterio/issues/995
+        """Mask the area outside of the input shapes with no data.
+
         :param bbox: bounding box of type tuple or Shapely Polygon
         :param crop: bool, see rasterio.mask. Optional, (default: True)
         :param pad: pads image, should only be used when bbox.bounds extent img.bounds, optional (default: False)
         """
+        # TODO https://github.com/mapbox/rasterio/issues/995
         if pad:
             self.dataset = self._pad_to_bbox(bbox, **kwargs).open()
 
@@ -83,6 +91,9 @@ class Image:
         else:
             raise TypeError(f"bbox must be of type tuple or Shapely Polygon")
 
+        # update for further processing
+        self.dataset = self.__update_dataset().open()
+
     def _pad_to_bbox(self, bbox, mode="constant", constant_values=0):
         """Buffers array with biggest difference to bbox and adjusts affine transform matrix. Can be used to fill
         array with nodata values before masking in case bbox only partially overlaps dataset bounds.
@@ -90,7 +101,7 @@ class Image:
         :param bbox: bounding box of type tuple or Shapely Polygon
         :param mode: str, how to pad, see rasterio.pad. Optional (default: 'constant')
         :param constant_values: nodata value, padding should be filled with, optional (default: 0)
-        :return: open, buffered dataset in memory
+        :return: closed, buffered dataset in memory
         """
         if isinstance(bbox, polygon.Polygon):
             bbox = bbox.bounds
@@ -117,6 +128,13 @@ class Image:
 
         self.__arr = destination
 
+        return self.__update_dataset()
+
+    def __update_dataset(self):
+        """Update dataset without writing to file after it theoretically changed.
+
+        :return: closed dataset in memory
+        """
         mem_profile = self.dataset.meta
         mem_profile.update(
             {"height": self.__arr.shape[-2], "width": self.__arr.shape[-1], "transform": self.transform,}
@@ -171,6 +189,7 @@ class Image:
         self.__arr = destination
         self.transform = transform
         self.crs = dst_crs
+        self.dataset = self.__update_dataset().open()
 
     def dn2toa(self, platform, mtl_file=None, wavelengths=None):
         """This method converts digital numbers to top of atmosphere reflectance, like described here:


Test patch:
diff --git a/tests/test_raster.py b/tests/test_raster.py
index 0fbe9e1..ca6b7fb 100644
--- a/tests/test_raster.py
+++ b/tests/test_raster.py
@@ -85,13 +85,17 @@ class DataTest(unittest.TestCase):
         img.mask_image(box(11.9027457562112939, 51.4664152338322580, 11.9477435281016131, 51.5009522690838750,))
         self.assertEqual(
             img.dataset.bounds,
-            BoundingBox(left=11.896863892, bottom=51.446545369, right=11.9578595, top=51.515176657,),
+            BoundingBox(
+                left=11.902702941366716, bottom=51.46639813686387, right=11.947798368783504, top=51.50098327545026,
+            ),
         )
 
         img.mask_image((11.9027457562112939, 51.4664152338322580, 11.9477435281016131, 51.5009522690838750,))
         self.assertEqual(
             img.dataset.bounds,
-            BoundingBox(left=11.896863892, bottom=51.446545369, right=11.9578595, top=51.515176657,),
+            BoundingBox(
+                left=11.902702941366716, bottom=51.46639813686387, right=11.947798368783504, top=51.50098327545026,
+            ),
         )
 
         img.mask_image(
@@ -100,7 +104,7 @@ class DataTest(unittest.TestCase):
         self.assertEqual(
             img.dataset.bounds,
             BoundingBox(
-                left=11.897762207287187, bottom=51.4614574027801, right=11.952739102863033, top=51.50592400953403,
+                left=11.891923157920472, bottom=51.46639813686387, right=11.947798368783504, top=51.50098327545026
             ),
         )