kuva-reader 1.0.4__tar.gz → 1.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ *.ipynb
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ .hypothesis/
49
+ .pytest_cache/
50
+
51
+ # Translations
52
+ *.mo
53
+ *.pot
54
+
55
+ # Django stuff:
56
+ *.log
57
+ local_settings.py
58
+ db.sqlite3
59
+
60
+ # Flask stuff:
61
+ instance/
62
+ .webassets-cache
63
+
64
+ # Torch stuff
65
+ lightning_logs/
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ target/
75
+
76
+ # Jupyter Notebook
77
+ .ipynb_checkpoints
78
+
79
+ # VSCode
80
+ .vscode
81
+
82
+ # pyenv
83
+ .python-version
84
+
85
+ # celery beat schedule file
86
+ celerybeat-schedule
87
+
88
+ # SageMath parsed files
89
+ *.sage.py
90
+
91
+ # Environments
92
+ .env
93
+ .venv
94
+ env/
95
+ venv/
96
+ ENV/
97
+ env.bak/
98
+ venv.bak/
99
+
100
+ # Spyder project settings
101
+ .spyderproject
102
+ .spyproject
103
+
104
+ # Rope project settings
105
+ .ropeproject
106
+
107
+ # mkdocs documentation
108
+ /site
109
+
110
+ # mypy
111
+ .mypy_cache/
112
+
113
+ # Direnv stuff
114
+ .direnv/
115
+ .envrc
116
+
117
+ # Poetry
118
+ # poetry.lock
119
+
120
+ # Supervisord
121
+ supervisord.log
122
+ supervisord.pid
123
+
124
+ # Debug folders
125
+ _debug/
126
+
127
+ # Kuva data
128
+ *.tif
129
+ *.tiff
130
+ *.npy
131
+ hyperfield*.json
132
+
133
+ # Do not ignore Kuva files in the test_data directory
134
+ !kuva-reader/tests/test_data/**
@@ -1,24 +1,16 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: kuva-reader
3
- Version: 1.0.4
3
+ Version: 1.1.6
4
4
  Summary: Manipulate the Kuva Space image and metadata formats
5
- License: MIT
6
- Author: Guillem Ballesteros
7
- Author-email: guillem@kuvaspace.com
8
- Requires-Python: >=3.10,<=3.13
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: kuva-geometry
15
- Requires-Dist: kuva-metadata
16
- Requires-Dist: numpy (>=1.26.4,<2.0.0)
17
- Requires-Dist: numpy-quaternion (>=2022.4.4,<2023.0.0)
18
- Requires-Dist: pint (>=0.22,<0.23)
19
- Requires-Dist: rasterio (>=1.4.1,<2.0.0)
20
- Requires-Dist: rioxarray (>=0.12.4,<0.13.0)
21
- Requires-Dist: xarray (>=2022.12.0,<2023.0.0)
5
+ Author-email: Guillem Ballesteros <guillem@kuvaspace.com>, Lennert Antson <lennert.antson@kuvaspace.com>, Arthur Vandenhoeke <arthur.vandenhoeke@kuvaspace.com>, Olli Eloranta <olli.eloranta@kuvaspace.com>
6
+ License-Expression: MIT
7
+ Requires-Python: <=3.13,>=3.10
8
+ Requires-Dist: kuva-geometry<2.0.0,>=1.0.1
9
+ Requires-Dist: kuva-metadata<2.0.0,>=1.1.1
10
+ Requires-Dist: numpy-quaternion>=2023.4.4
11
+ Requires-Dist: numpy>=1.26.4
12
+ Requires-Dist: pint<1.0.0,>=0.22
13
+ Requires-Dist: rasterio<2,>=1.4.3
22
14
  Description-Content-Type: text/markdown
23
15
 
24
16
  <div align="center">
@@ -36,7 +28,7 @@ The Kuva Space images are in GeoTIFF format. The products consist of an image or
36
28
  images along with its metadata to give all the necessary information to use the products.
37
29
  The metadata lives either in a Kuva Space database, or alternatively in a sidecar JSON file.
38
30
 
39
- This library allows the reading of the image GeoTIFFs into `xarray.Dataset` objects that
31
+ This library allows the reading of the image GeoTIFFs into `rasterio.DatasetReader` objects that
40
32
  allow convenient raster manipulations, along with their `kuva-metadata` metadata objects.
41
33
 
42
34
  # Installation
@@ -56,7 +48,7 @@ pip install kuva-reader
56
48
  This is a minimal example that allows you to read and print the image shape of a L2 product.
57
49
 
58
50
  The result product is in this case an L2A product (as seen from the folder name).
59
- The loaded product is stored in a `rioxarray` object, which contains extensive GIS functionalities [(examples for usage)](https://corteva.github.io/rioxarray/stable/examples/examples.html).
51
+ The loaded product is stored in a `rasterio.DatasetReader` object, which contains extensive GIS functionalities [(examples for usage)](https://rasterio.readthedocs.io/en/stable/api/rasterio.io.html#rasterio.io.DatasetReader).
60
52
 
61
53
  ```python
62
54
  from kuva_reader import read_product
@@ -130,4 +122,3 @@ The `kuva-reader` project software is under the [MIT license](https://github.com
130
122
  # Status of unit tests
131
123
 
132
124
  [![Unit tests for kuva-reader](https://github.com/KuvaSpace/kuva-data-processing/actions/workflows/test-kuva-reader.yml/badge.svg)](https://github.com/KuvaSpace/kuva-data-processing/actions/workflows/test-kuva-reader.yml)
133
-
@@ -13,7 +13,7 @@ The Kuva Space images are in GeoTIFF format. The products consist of an image or
13
13
  images along with its metadata to give all the necessary information to use the products.
14
14
  The metadata lives either in a Kuva Space database, or alternatively in a sidecar JSON file.
15
15
 
16
- This library allows the reading of the image GeoTIFFs into `xarray.Dataset` objects that
16
+ This library allows the reading of the image GeoTIFFs into `rasterio.DatasetReader` objects that
17
17
  allow convenient raster manipulations, along with their `kuva-metadata` metadata objects.
18
18
 
19
19
  # Installation
@@ -33,7 +33,7 @@ pip install kuva-reader
33
33
  This is a minimal example that allows you to read and print the image shape of a L2 product.
34
34
 
35
35
  The result product is in this case an L2A product (as seen from the folder name).
36
- The loaded product is stored in a `rioxarray` object, which contains extensive GIS functionalities [(examples for usage)](https://corteva.github.io/rioxarray/stable/examples/examples.html).
36
+ The loaded product is stored in a `rasterio.DatasetReader` object, which contains extensive GIS functionalities [(examples for usage)](https://rasterio.readthedocs.io/en/stable/api/rasterio.io.html#rasterio.io.DatasetReader).
37
37
 
38
38
  ```python
39
39
  from kuva_reader import read_product
@@ -17,17 +17,14 @@ Key Features
17
17
  Dependencies
18
18
  - kuva-metadata: A specialized library that handles the extraction and
19
19
  parsing of metadata associated with Kuva Space products.
20
- - xarray: Used for loading image data as arrays with extra functionality,
21
- including labeled coordinates and metadata, which is useful for analysis and
20
+ - rasterio: Used for loading image data as arrays with extra functionality,
21
+ including GIS specific functions and metadata, which are useful for analysis and
22
22
  visualization.
23
23
  """
24
24
 
25
- __version__ = "0.1.0"
25
+ __version__ = "1.1.2"
26
26
 
27
27
  from .reader.image import (
28
- image_to_dtype_range,
29
- image_to_original_range,
30
- image_to_uint16_range,
31
28
  image_footprint,
32
29
  )
33
30
  from .reader.level0 import Level0Product
@@ -40,9 +37,6 @@ __all__ = [
40
37
  "Level1ABProduct",
41
38
  "Level1CProduct",
42
39
  "Level2AProduct",
43
- "image_to_dtype_range",
44
- "image_to_original_range",
45
- "image_to_uint16_range",
46
40
  "image_footprint",
47
41
  "read_product",
48
42
  ]
@@ -0,0 +1,28 @@
1
+ """Utilities to process images related to product processing."""
2
+
3
+ import rasterio as rio
4
+ from shapely.geometry import box, Polygon
5
+ from rasterio.warp import transform_bounds
6
+
7
+
8
+ def image_footprint(image: rio.DatasetReader, crs: str = "") -> Polygon:
9
+ """Return a product footprint as a shapely polygon
10
+
11
+ Parameters
12
+ ----------
13
+ image
14
+ The product image
15
+ crs, optional
16
+ CRS to convert to, by default "", keeping the image's CRS
17
+
18
+ Returns
19
+ -------
20
+ A shapely polygon footprint
21
+ """
22
+ if crs:
23
+ # Transform the bounds to the new CRS using rasterio's built-in function
24
+ bounds = transform_bounds(image.crs, crs, *image.bounds)
25
+ footprint = box(*bounds)
26
+ else:
27
+ footprint = box(*image.bounds)
28
+ return footprint
@@ -2,15 +2,14 @@ from pathlib import Path
2
2
  from typing import cast
3
3
 
4
4
  import numpy as np
5
- import rioxarray as rx
6
- import xarray
5
+ import rasterio as rio
7
6
  from kuva_metadata import MetadataLevel0
8
7
  from pint import UnitRegistry
9
8
  from shapely import Polygon
10
9
 
11
- from kuva_reader import image_to_dtype_range, image_to_original_range, image_footprint
10
+ from kuva_reader import image_footprint
12
11
 
13
- from .product_base import ProductBase
12
+ from .product_base import NUM_THREADS, ProductBase
14
13
 
15
14
 
16
15
  class Level0Product(ProductBase[MetadataLevel0]):
@@ -39,13 +38,6 @@ class Level0Product(ProductBase[MetadataLevel0]):
39
38
  target_ureg, optional
40
39
  Pint Unit Registry to swap to. This is only relevant when parsing data from a
41
40
  JSON file, which by default uses the kuva-metadata ureg.
42
- as_physical_unit
43
- Whether to denormalize data from full data type range back to the physical
44
- units stored with the data, by default False
45
- target_dtype
46
- Target data type to normalize data to. This will first denormalize the data
47
- to its original range and then normalize to new data type range to keep a
48
- scale and offset, by default None
49
41
 
50
42
  Attributes
51
43
  ----------
@@ -53,10 +45,9 @@ class Level0Product(ProductBase[MetadataLevel0]):
53
45
  Path to the folder containing the images.
54
46
  metadata: MetadataLevel0
55
47
  The metadata associated with the images
56
- images: Dict[str, xarray.DataArray]
57
- The arrays with the actual data. This have the rioxarray extension activated on
58
- them so lots of GIS functionality are available on them. Imporantly, the GCPs
59
- can be retrieved like so: `ds.rio.get_gcps()`
48
+ images: Dict[str, rasterio.DatasetReader]
49
+ A dictionary that maps camera names to their respective Rasterio DatasetReader
50
+ objects.
60
51
  data_tags: Dict[str, Any]
61
52
  Tags stored along with the data. These can be used e.g. to check the physical
62
53
  units of pixels or normalisation factors.
@@ -67,58 +58,53 @@ class Level0Product(ProductBase[MetadataLevel0]):
67
58
  image_path: Path,
68
59
  metadata: MetadataLevel0 | None = None,
69
60
  target_ureg: UnitRegistry | None = None,
70
- as_physical_unit: bool = False,
71
- target_dtype: np.dtype | None = None,
72
61
  ) -> None:
73
62
  super().__init__(image_path, metadata, target_ureg)
74
63
 
75
- self.images = {
64
+ self._images = {
76
65
  camera: cast(
77
- xarray.DataArray,
78
- rx.open_rasterio(
66
+ rio.DatasetReader,
67
+ rio.open(
79
68
  self.image_path / (cube.camera.name + ".tif"),
69
+ num_threads=NUM_THREADS,
80
70
  ),
81
71
  )
82
72
  for camera, cube in self.metadata.image.data_cubes.items() # type: ignore
83
73
  }
84
- self.crs = self.images[list(self.images.keys())[0]].rio.crs
74
+ self.crs = self.images[list(self.images.keys())[0]].crs
85
75
 
86
76
  # Read tags for images and denormalize / renormalize if needed
87
- self.data_tags = {camera: img.attrs for camera, img in self.images.items()}
88
- if as_physical_unit or target_dtype:
89
- for camera, img in self.images.items():
90
- # Move from normalized full scale back to original data float values.
91
- # pop() since values not true anymore after denormalization.
92
- norm_img = image_to_original_range(
93
- img,
94
- self.data_tags[camera].pop("data_offset"),
95
- self.data_tags[camera].pop("data_scale"),
96
- )
97
- self.images[camera] = norm_img
98
-
99
- if target_dtype:
100
- # For algorithm needs, cast and normalize to a specific dtype range
101
- # NOTE: This may remove data precision e.g. uint16 -> uint8
102
- norm_img, offset, scale = image_to_dtype_range(img, target_dtype)
103
- self.data_tags[camera]["data_offset"] = offset
104
- self.data_tags[camera]["data_scale"] = scale
77
+ self.data_tags = {camera: src.tags() for camera, src in self.images.items()}
105
78
 
106
79
  def __repr__(self):
107
80
  """Pretty printing of the object with the most important info"""
108
81
  if self.images is not None and len(self.images):
82
+ image_shapes = []
83
+ for camera_name, image in self.images.items():
84
+ shape_str = f"({image.count}, {image.height}, {image.width})"
85
+ image_shapes.append(f"{camera_name.upper()} shape {shape_str}")
86
+
87
+ shapes_description = " and ".join(image_shapes)
88
+
109
89
  return (
110
- f"{self.__class__.__name__}"
111
- f"with VIS shape {self.images['vis'].shape} "
112
- f"and NIR shape {self.images['nir'].shape} "
113
- f"(CRS '{self.crs}'). Loaded from: '{self.image_path}'."
90
+ f"{self.__class__.__name__} "
91
+ f"with {shapes_description} and "
92
+ f"CRS: '{self.crs}'. Loaded from: '{self.image_path}'."
114
93
  )
115
94
  else:
116
95
  return f"{self.__class__.__name__} loaded from '{self.image_path}'."
117
96
 
118
- def __getitem__(self, camera: str) -> xarray.DataArray:
97
+ def __getitem__(self, camera: str) -> rio.DatasetReader:
119
98
  """Return the datarray for the chosen camera."""
120
99
  return self.images[camera]
121
100
 
101
+ @property
102
+ def images(self) -> dict[str, rio.DatasetReader]:
103
+ if self._images is None:
104
+ e_ = "Images have been released. Re-open the product to access it again."
105
+ raise RuntimeError(e_)
106
+ return self._images
107
+
122
108
  def keys(self) -> list[str]:
123
109
  """Easy access to the camera keys."""
124
110
  return list(self.images.keys())
@@ -192,7 +178,11 @@ class Level0Product(ProductBase[MetadataLevel0]):
192
178
  def read_frame(self, cube: str, band_id: int, frame_idx: int) -> np.ndarray:
193
179
  """Extract a specific frame from a cube and band."""
194
180
  frame_offset = self.calculate_frame_offset(cube, band_id, frame_idx)
195
- return self[cube][frame_offset, :, :].to_numpy()
181
+
182
+ # Rasterio index starts at 1
183
+ frame_offset += 1
184
+
185
+ return self[cube].read(frame_offset)
196
186
 
197
187
  def read_band(self, cube: str, band_id: int) -> np.ndarray:
198
188
  """Extract a specific band from a cube"""
@@ -201,7 +191,12 @@ class Level0Product(ProductBase[MetadataLevel0]):
201
191
  # Calculate the final frame offset for this band and frame
202
192
  band_offset_ll = band_offsets[band_id]
203
193
  band_offset_ul = band_offset_ll + band_n_frames[band_id]
204
- return self[cube][band_offset_ll:band_offset_ul, :, :].to_numpy()
194
+
195
+ # Rasterio index starts at 1
196
+ band_offset_ll += 1
197
+ band_offset_ul += 1
198
+
199
+ return self[cube].read(list(np.arange(band_offset_ll, band_offset_ul)))
205
200
 
206
201
  def read_data_units(self) -> np.ndarray:
207
202
  """Read unit of product and validate they match between cameras"""
@@ -213,7 +208,7 @@ class Level0Product(ProductBase[MetadataLevel0]):
213
208
  e_ = "Cameras have different physical units stored to them."
214
209
  raise ValueError(e_)
215
210
 
216
- def get_bad_pixel_mask(self, camera: str | None = None) -> xarray.Dataset:
211
+ def get_bad_pixel_mask(self, camera: str | None = None) -> rio.DatasetReader:
217
212
  """Get the bad pixel mask associated to each camera of the L0 product
218
213
 
219
214
  Returns
@@ -226,7 +221,7 @@ class Level0Product(ProductBase[MetadataLevel0]):
226
221
  bad_pixel_filename = f"{camera}_per_frame_bad_pixel_mask.tif"
227
222
  return self._read_array(self.image_path / bad_pixel_filename)
228
223
 
229
- def get_cloud_mask(self, camera: str | None = None) -> xarray.Dataset:
224
+ def get_cloud_mask(self, camera: str | None = None) -> rio.DatasetReader:
230
225
  """Get the cloud mask associated to the product.
231
226
 
232
227
  Returns
@@ -240,14 +235,17 @@ class Level0Product(ProductBase[MetadataLevel0]):
240
235
  return self._read_array(self.image_path / bad_pixel_filename)
241
236
 
242
237
  def release_memory(self):
243
- """Explicitely releases the memory of the `images` variable.
244
-
245
- NOTE: this function is implemented because of a memory leak inside the Rioxarray
246
- library that doesn't release memory properly. Only use it when the image data is
247
- not needed anymore.
238
+ """Explicitely closes the Rasterio DatasetReaders and releases the memory of
239
+ the `images` variable.
248
240
  """
249
- del self.images
250
- self.images = None
241
+ if self._images is not None:
242
+ for k in self._images.keys():
243
+ self._images[k].close()
244
+
245
+ del self._images
246
+ # We know that images are not None as long as somebody doesn't call
247
+ # this function beforehand....
248
+ self._images = None
251
249
 
252
250
 
253
251
  def generate_level_0_metafile():