Repository: dlr-eoc/ukis-pysat

Issue:
Image write compression and dtype
The [Image.write_to_file()](https://github.com/dlr-eoc/ukis-pysat/blob/master/ukis_pysat/raster.py#L341) method could be expanded to support optional image compression (e.g. Packbits or LZW). This is necessary to reduce the storage space for example when saving binary masks to disk. 

To achieve this we basically would have to update the profile with "compress" and optionally also "nbits" attributes. Beyond this, the "tiled" attribute could be useful to support.

Also this method defines as default dtype uint16. It would be more reasonable to set the default dtype to the dtype of the Image array in my opinion. This would allow us to stay consistent between image loading and writing.

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ba1146c..9bbb7ca 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -31,6 +31,7 @@ Fixed
 Changed
 *******
 - ``raster.Image()``: changed ``dn2toa(platform, metadata, wavelengths)`` to ``dn2toa(platform, mtl_file, wavelengths)`` #17
+- ``raster.Image()``: explicit dtype when writing, optional compression #32
 - ``raster.Image()``: auto-update of in-memory `dataset` #35
 
 
diff --git a/ukis_pysat/raster.py b/ukis_pysat/raster.py
index fa4d337..97fd77c 100644
--- a/ukis_pysat/raster.py
+++ b/ukis_pysat/raster.py
@@ -9,6 +9,7 @@ try:
     import rasterio
     import rasterio.mask
     from rasterio import windows
+    from rasterio.dtypes import get_minimum_dtype
     from rasterio.io import MemoryFile
     from rasterio.plot import reshape_as_image
     from rasterio.warp import calculate_default_transform, reproject
@@ -360,13 +361,18 @@ class Image:
         self.da_arr = da.from_array(self.__arr, chunks=chunk_size)
         return self.da_arr
 
-    def write_to_file(self, path_to_file, dtype=rasterio.uint16, driver="GTiff"):
+    def write_to_file(self, path_to_file, dtype, driver="GTiff", compress=None):
         """
         Write a dataset to file.
         :param path_to_file: str, path to new file
-        :param dtype: datatype, optional (default: rasterio.uint16)
+        :param dtype: datatype, like np.uint16, 'float32' or 'min' to use the minimum type to represent values
+
         :param driver: str, optional (default: 'GTiff')
+        :param compress: compression, e.g. 'lzw' (default: None)
         """
+        if dtype == 'min':
+            dtype = get_minimum_dtype(self.__arr)
+
         profile = self.dataset.meta
         profile.update(
             {
@@ -379,6 +385,9 @@ class Image:
             }
         )
 
+        if compress:
+            profile.update({"compress": compress})
+
         with rasterio.open(path_to_file, "w", **profile) as dst:
             dst.write(self.__arr.astype(dtype))
 


Test patch:
diff --git a/tests/test_raster.py b/tests/test_raster.py
index ca6b7fb..42ee9d3 100644
--- a/tests/test_raster.py
+++ b/tests/test_raster.py
@@ -5,8 +5,8 @@ import dask.array
 import numpy as np
 from rasterio import windows
 from rasterio.coords import BoundingBox
-from shapely.geometry import box
 from rasterio.transform import from_bounds
+from shapely.geometry import box
 
 from ukis_pysat.members import Platform
 from ukis_pysat.raster import Image
@@ -187,13 +187,21 @@ class DataTest(unittest.TestCase):
         self.assertIsInstance(img.to_dask_array(chunk_size=(1, 10, 10)), dask.array.core.Array)
 
     def test_write_to_file(self):
-        img.write_to_file(r"result.tif")
+        img.write_to_file(r"result.tif", np.uint16)
         img2 = Image("result.tif")
         self.assertTrue(np.array_equal(img2.arr, img.arr))
 
         img2.close()
         os.remove(r"result.tif")
 
+        img.write_to_file(r"result.tif", "min", compress="lzw")
+        img2 = Image("result.tif")
+        self.assertEqual(img2.arr.dtype, "uint8")
+        self.assertEqual(img2.dataset.profile["compress"], "lzw")
+
+        img2.close()
+        os.remove(r"result.tif")
+
 
 if __name__ == "__main__":
     unittest.main()