kuva-reader 1.0.3__tar.gz → 1.1.0__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.
Potentially problematic release.
This version of kuva-reader might be problematic. Click here for more details.
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/PKG-INFO +20 -8
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/README.md +19 -5
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/__init__.py +4 -8
- kuva_reader-1.1.0/kuva_reader/reader/image.py +28 -0
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/reader/level0.py +43 -50
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/reader/level1.py +50 -30
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/reader/level2.py +21 -16
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/reader/product_base.py +6 -7
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/reader/read.py +1 -1
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/pyproject.toml +1 -13
- kuva_reader-1.0.3/kuva_reader/reader/image.py +0 -176
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/py.typed +0 -0
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/reader/__init__.py +0 -0
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/reader/py.typed +0 -0
- {kuva_reader-1.0.3 → kuva_reader-1.1.0}/kuva_reader/reader/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: kuva-reader
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Manipulate the Kuva Space image and metadata formats
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Guillem Ballesteros
|
|
@@ -17,8 +17,6 @@ Requires-Dist: numpy (>=1.26.4,<2.0.0)
|
|
|
17
17
|
Requires-Dist: numpy-quaternion (>=2022.4.4,<2023.0.0)
|
|
18
18
|
Requires-Dist: pint (>=0.22,<0.23)
|
|
19
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)
|
|
22
20
|
Description-Content-Type: text/markdown
|
|
23
21
|
|
|
24
22
|
<div align="center">
|
|
@@ -36,7 +34,7 @@ The Kuva Space images are in GeoTIFF format. The products consist of an image or
|
|
|
36
34
|
images along with its metadata to give all the necessary information to use the products.
|
|
37
35
|
The metadata lives either in a Kuva Space database, or alternatively in a sidecar JSON file.
|
|
38
36
|
|
|
39
|
-
This library allows the reading of the image GeoTIFFs into `
|
|
37
|
+
This library allows the reading of the image GeoTIFFs into `rasterio.DatasetReader` objects that
|
|
40
38
|
allow convenient raster manipulations, along with their `kuva-metadata` metadata objects.
|
|
41
39
|
|
|
42
40
|
# Installation
|
|
@@ -56,13 +54,13 @@ pip install kuva-reader
|
|
|
56
54
|
This is a minimal example that allows you to read and print the image shape of a L2 product.
|
|
57
55
|
|
|
58
56
|
The result product is in this case an L2A product (as seen from the folder name).
|
|
59
|
-
The loaded product is stored in a `
|
|
57
|
+
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
58
|
|
|
61
59
|
```python
|
|
62
60
|
from kuva_reader import read_product
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
print(
|
|
62
|
+
product = read_product("my_data_folder/hyperfield1a_L2A_20250105T092548")
|
|
63
|
+
print(product) # Will show some main information such as image shape and CRS
|
|
66
64
|
```
|
|
67
65
|
|
|
68
66
|
This assumes a mostly untouched folder after distributing. Otherwise, you may need to
|
|
@@ -75,7 +73,21 @@ l2a_product = Level2AProduct("your/l2a/folder")
|
|
|
75
73
|
```
|
|
76
74
|
|
|
77
75
|
The actual raster image is stored and can be analysed in `product.image`, while metadata
|
|
78
|
-
information of the product is in `product.metadata`.
|
|
76
|
+
information of the product is in `product.metadata`.
|
|
77
|
+
|
|
78
|
+
## Other tips
|
|
79
|
+
|
|
80
|
+
The product object attributes and methods allow the retrieval of other interesting information as well:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from kuva_reader import read_product
|
|
84
|
+
|
|
85
|
+
product = read_product("your/product/folder")
|
|
86
|
+
product.footprint(crs="EPSG:4326") # Footprint with option to transform CRS
|
|
87
|
+
product.image.shape # The image attribute contains all the image data
|
|
88
|
+
product.wavelengths # Wavelengths corresponding to image bands
|
|
89
|
+
product.crs # CRS
|
|
90
|
+
```
|
|
79
91
|
|
|
80
92
|
## Processing levels
|
|
81
93
|
|
|
@@ -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 `
|
|
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,13 +33,13 @@ 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 `
|
|
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
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
print(
|
|
41
|
+
product = read_product("my_data_folder/hyperfield1a_L2A_20250105T092548")
|
|
42
|
+
print(product) # Will show some main information such as image shape and CRS
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
This assumes a mostly untouched folder after distributing. Otherwise, you may need to
|
|
@@ -52,7 +52,21 @@ l2a_product = Level2AProduct("your/l2a/folder")
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
The actual raster image is stored and can be analysed in `product.image`, while metadata
|
|
55
|
-
information of the product is in `product.metadata`.
|
|
55
|
+
information of the product is in `product.metadata`.
|
|
56
|
+
|
|
57
|
+
## Other tips
|
|
58
|
+
|
|
59
|
+
The product object attributes and methods allow the retrieval of other interesting information as well:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from kuva_reader import read_product
|
|
63
|
+
|
|
64
|
+
product = read_product("your/product/folder")
|
|
65
|
+
product.footprint(crs="EPSG:4326") # Footprint with option to transform CRS
|
|
66
|
+
product.image.shape # The image attribute contains all the image data
|
|
67
|
+
product.wavelengths # Wavelengths corresponding to image bands
|
|
68
|
+
product.crs # CRS
|
|
69
|
+
```
|
|
56
70
|
|
|
57
71
|
## Processing levels
|
|
58
72
|
|
|
@@ -17,17 +17,15 @@ 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
|
-
-
|
|
21
|
-
including
|
|
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
25
|
__version__ = "0.1.0"
|
|
26
26
|
|
|
27
27
|
from .reader.image import (
|
|
28
|
-
|
|
29
|
-
image_to_original_range,
|
|
30
|
-
image_to_uint16_range,
|
|
28
|
+
image_footprint,
|
|
31
29
|
)
|
|
32
30
|
from .reader.level0 import Level0Product
|
|
33
31
|
from .reader.level1 import Level1ABProduct, Level1CProduct
|
|
@@ -39,8 +37,6 @@ __all__ = [
|
|
|
39
37
|
"Level1ABProduct",
|
|
40
38
|
"Level1CProduct",
|
|
41
39
|
"Level2AProduct",
|
|
42
|
-
"
|
|
43
|
-
"image_to_original_range",
|
|
44
|
-
"image_to_uint16_range",
|
|
40
|
+
"image_footprint",
|
|
45
41
|
"read_product",
|
|
46
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,12 +2,12 @@ from pathlib import Path
|
|
|
2
2
|
from typing import cast
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
|
-
import
|
|
6
|
-
import xarray
|
|
5
|
+
import rasterio as rio
|
|
7
6
|
from kuva_metadata import MetadataLevel0
|
|
8
7
|
from pint import UnitRegistry
|
|
8
|
+
from shapely import Polygon
|
|
9
9
|
|
|
10
|
-
from kuva_reader import
|
|
10
|
+
from kuva_reader import image_footprint
|
|
11
11
|
|
|
12
12
|
from .product_base import ProductBase
|
|
13
13
|
|
|
@@ -38,13 +38,6 @@ class Level0Product(ProductBase[MetadataLevel0]):
|
|
|
38
38
|
target_ureg, optional
|
|
39
39
|
Pint Unit Registry to swap to. This is only relevant when parsing data from a
|
|
40
40
|
JSON file, which by default uses the kuva-metadata ureg.
|
|
41
|
-
as_physical_unit
|
|
42
|
-
Whether to denormalize data from full data type range back to the physical
|
|
43
|
-
units stored with the data, by default False
|
|
44
|
-
target_dtype
|
|
45
|
-
Target data type to normalize data to. This will first denormalize the data
|
|
46
|
-
to its original range and then normalize to new data type range to keep a
|
|
47
|
-
scale and offset, by default None
|
|
48
41
|
|
|
49
42
|
Attributes
|
|
50
43
|
----------
|
|
@@ -52,10 +45,9 @@ class Level0Product(ProductBase[MetadataLevel0]):
|
|
|
52
45
|
Path to the folder containing the images.
|
|
53
46
|
metadata: MetadataLevel0
|
|
54
47
|
The metadata associated with the images
|
|
55
|
-
images: Dict[str,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
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.
|
|
59
51
|
data_tags: Dict[str, Any]
|
|
60
52
|
Tags stored along with the data. These can be used e.g. to check the physical
|
|
61
53
|
units of pixels or normalisation factors.
|
|
@@ -66,54 +58,42 @@ class Level0Product(ProductBase[MetadataLevel0]):
|
|
|
66
58
|
image_path: Path,
|
|
67
59
|
metadata: MetadataLevel0 | None = None,
|
|
68
60
|
target_ureg: UnitRegistry | None = None,
|
|
69
|
-
as_physical_unit: bool = False,
|
|
70
|
-
target_dtype: np.dtype | None = None,
|
|
71
61
|
) -> None:
|
|
72
62
|
super().__init__(image_path, metadata, target_ureg)
|
|
73
63
|
|
|
74
64
|
self.images = {
|
|
75
65
|
camera: cast(
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
rio.DatasetReader,
|
|
67
|
+
rio.open(
|
|
78
68
|
self.image_path / (cube.camera.name + ".tif"),
|
|
79
69
|
),
|
|
80
70
|
)
|
|
81
71
|
for camera, cube in self.metadata.image.data_cubes.items() # type: ignore
|
|
82
72
|
}
|
|
73
|
+
self.crs = self.images[list(self.images.keys())[0]].crs
|
|
83
74
|
|
|
84
75
|
# Read tags for images and denormalize / renormalize if needed
|
|
85
|
-
self.data_tags = {camera:
|
|
86
|
-
if as_physical_unit or target_dtype:
|
|
87
|
-
for camera, img in self.images.items():
|
|
88
|
-
# Move from normalized full scale back to original data float values.
|
|
89
|
-
# pop() since values not true anymore after denormalization.
|
|
90
|
-
norm_img = image_to_original_range(
|
|
91
|
-
img,
|
|
92
|
-
self.data_tags[camera].pop("data_offset"),
|
|
93
|
-
self.data_tags[camera].pop("data_scale"),
|
|
94
|
-
)
|
|
95
|
-
self.images[camera] = norm_img
|
|
96
|
-
|
|
97
|
-
if target_dtype:
|
|
98
|
-
# For algorithm needs, cast and normalize to a specific dtype range
|
|
99
|
-
# NOTE: This may remove data precision e.g. uint16 -> uint8
|
|
100
|
-
norm_img, offset, scale = image_to_dtype_range(img, target_dtype)
|
|
101
|
-
self.data_tags[camera]["data_offset"] = offset
|
|
102
|
-
self.data_tags[camera]["data_scale"] = scale
|
|
76
|
+
self.data_tags = {camera: src.tags() for camera, src in self.images.items()}
|
|
103
77
|
|
|
104
78
|
def __repr__(self):
|
|
105
79
|
"""Pretty printing of the object with the most important info"""
|
|
106
80
|
if self.images is not None and len(self.images):
|
|
81
|
+
image_shapes = []
|
|
82
|
+
for camera_name, image in self.images.items():
|
|
83
|
+
shape_str = f"({image.count}, {image.height}, {image.width})"
|
|
84
|
+
image_shapes.append(f"{camera_name.upper()} shape {shape_str}")
|
|
85
|
+
|
|
86
|
+
shapes_description = " and ".join(image_shapes)
|
|
87
|
+
|
|
107
88
|
return (
|
|
108
|
-
f"{self.__class__.__name__}"
|
|
109
|
-
f"with {
|
|
110
|
-
f"
|
|
111
|
-
f"Loaded from: '{self.image_path}'."
|
|
89
|
+
f"{self.__class__.__name__} "
|
|
90
|
+
f"with {shapes_description} and "
|
|
91
|
+
f"CRS: '{self.crs}'. Loaded from: '{self.image_path}'."
|
|
112
92
|
)
|
|
113
93
|
else:
|
|
114
94
|
return f"{self.__class__.__name__} loaded from '{self.image_path}'."
|
|
115
95
|
|
|
116
|
-
def __getitem__(self, camera: str) ->
|
|
96
|
+
def __getitem__(self, camera: str) -> rio.DatasetReader:
|
|
117
97
|
"""Return the datarray for the chosen camera."""
|
|
118
98
|
return self.images[camera]
|
|
119
99
|
|
|
@@ -121,6 +101,10 @@ class Level0Product(ProductBase[MetadataLevel0]):
|
|
|
121
101
|
"""Easy access to the camera keys."""
|
|
122
102
|
return list(self.images.keys())
|
|
123
103
|
|
|
104
|
+
def footprint(self, crs="") -> Polygon:
|
|
105
|
+
"""The product footprint as a Shapely polygon."""
|
|
106
|
+
return image_footprint(self.images["vis"], crs)
|
|
107
|
+
|
|
124
108
|
def _get_data_from_sidecar(
|
|
125
109
|
self, sidecar_path: Path, target_ureg: UnitRegistry | None = None
|
|
126
110
|
) -> MetadataLevel0:
|
|
@@ -186,7 +170,11 @@ class Level0Product(ProductBase[MetadataLevel0]):
|
|
|
186
170
|
def read_frame(self, cube: str, band_id: int, frame_idx: int) -> np.ndarray:
|
|
187
171
|
"""Extract a specific frame from a cube and band."""
|
|
188
172
|
frame_offset = self.calculate_frame_offset(cube, band_id, frame_idx)
|
|
189
|
-
|
|
173
|
+
|
|
174
|
+
# Rasterio index starts at 1
|
|
175
|
+
frame_offset += 1
|
|
176
|
+
|
|
177
|
+
return self[cube].read(frame_offset)
|
|
190
178
|
|
|
191
179
|
def read_band(self, cube: str, band_id: int) -> np.ndarray:
|
|
192
180
|
"""Extract a specific band from a cube"""
|
|
@@ -195,7 +183,12 @@ class Level0Product(ProductBase[MetadataLevel0]):
|
|
|
195
183
|
# Calculate the final frame offset for this band and frame
|
|
196
184
|
band_offset_ll = band_offsets[band_id]
|
|
197
185
|
band_offset_ul = band_offset_ll + band_n_frames[band_id]
|
|
198
|
-
|
|
186
|
+
|
|
187
|
+
# Rasterio index starts at 1
|
|
188
|
+
band_offset_ll += 1
|
|
189
|
+
band_offset_ul += 1
|
|
190
|
+
|
|
191
|
+
return self[cube].read(list(np.arange(band_offset_ll, band_offset_ul)))
|
|
199
192
|
|
|
200
193
|
def read_data_units(self) -> np.ndarray:
|
|
201
194
|
"""Read unit of product and validate they match between cameras"""
|
|
@@ -207,7 +200,7 @@ class Level0Product(ProductBase[MetadataLevel0]):
|
|
|
207
200
|
e_ = "Cameras have different physical units stored to them."
|
|
208
201
|
raise ValueError(e_)
|
|
209
202
|
|
|
210
|
-
def get_bad_pixel_mask(self, camera: str | None = None) ->
|
|
203
|
+
def get_bad_pixel_mask(self, camera: str | None = None) -> rio.DatasetReader:
|
|
211
204
|
"""Get the bad pixel mask associated to each camera of the L0 product
|
|
212
205
|
|
|
213
206
|
Returns
|
|
@@ -220,7 +213,7 @@ class Level0Product(ProductBase[MetadataLevel0]):
|
|
|
220
213
|
bad_pixel_filename = f"{camera}_per_frame_bad_pixel_mask.tif"
|
|
221
214
|
return self._read_array(self.image_path / bad_pixel_filename)
|
|
222
215
|
|
|
223
|
-
def get_cloud_mask(self, camera: str | None = None) ->
|
|
216
|
+
def get_cloud_mask(self, camera: str | None = None) -> rio.DatasetReader:
|
|
224
217
|
"""Get the cloud mask associated to the product.
|
|
225
218
|
|
|
226
219
|
Returns
|
|
@@ -234,12 +227,12 @@ class Level0Product(ProductBase[MetadataLevel0]):
|
|
|
234
227
|
return self._read_array(self.image_path / bad_pixel_filename)
|
|
235
228
|
|
|
236
229
|
def release_memory(self):
|
|
237
|
-
"""Explicitely
|
|
238
|
-
|
|
239
|
-
NOTE: this function is implemented because of a memory leak inside the Rioxarray
|
|
240
|
-
library that doesn't release memory properly. Only use it when the image data is
|
|
241
|
-
not needed anymore.
|
|
230
|
+
"""Explicitely closes the Rasterio DatasetReaders and releases the memory of
|
|
231
|
+
the `images` variable.
|
|
242
232
|
"""
|
|
233
|
+
for k in self.images.keys():
|
|
234
|
+
self.images[k].close()
|
|
235
|
+
|
|
243
236
|
del self.images
|
|
244
237
|
self.images = None
|
|
245
238
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import cast
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import xarray
|
|
4
|
+
import rasterio as rio
|
|
6
5
|
from kuva_metadata import MetadataLevel1AB, MetadataLevel1C
|
|
7
6
|
from pint import UnitRegistry
|
|
8
|
-
from
|
|
7
|
+
from shapely import Polygon
|
|
8
|
+
|
|
9
|
+
from kuva_reader import image_footprint
|
|
9
10
|
|
|
10
11
|
from .product_base import ProductBase
|
|
11
12
|
|
|
@@ -34,10 +35,8 @@ class Level1ABProduct(ProductBase[MetadataLevel1AB]):
|
|
|
34
35
|
Path to the folder containing the image.
|
|
35
36
|
metadata: MetadataLevel1AB
|
|
36
37
|
The metadata associated with the images
|
|
37
|
-
image:
|
|
38
|
-
The
|
|
39
|
-
them so lots of GIS functionality are available on them. For example, the GCPs
|
|
40
|
-
if any could be retrieved like so: `ds.rio.get_gcps()`
|
|
38
|
+
image: rasterio.DatasetReader
|
|
39
|
+
The Rasterio DatasetReader to open the image and other metadata with.
|
|
41
40
|
data_tags: dict
|
|
42
41
|
Tags saved along with the product. The tag "data_unit" shows what the unit of
|
|
43
42
|
the product actually is.
|
|
@@ -52,10 +51,31 @@ class Level1ABProduct(ProductBase[MetadataLevel1AB]):
|
|
|
52
51
|
super().__init__(image_path, metadata, target_ureg)
|
|
53
52
|
|
|
54
53
|
self.image = cast(
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
rio.DatasetReader,
|
|
55
|
+
rio.open(self.image_path / "L1B.tif"),
|
|
57
56
|
)
|
|
58
|
-
|
|
57
|
+
|
|
58
|
+
self.data_tags = self.image.tags()
|
|
59
|
+
self.wavelengths = [
|
|
60
|
+
b.wavelength.to("nm").magnitude for b in self.metadata.image.bands
|
|
61
|
+
]
|
|
62
|
+
self.crs = self.image.crs
|
|
63
|
+
|
|
64
|
+
def __repr__(self):
|
|
65
|
+
"""Pretty printing of the object with the most important info"""
|
|
66
|
+
if self.image is not None:
|
|
67
|
+
shape_str = f"({self.image.count}, {self.image.height}, {self.image.width})"
|
|
68
|
+
return (
|
|
69
|
+
f"{self.__class__.__name__} with shape {shape_str} "
|
|
70
|
+
f"and wavelengths {self.wavelengths} (CRS: '{self.crs}'). "
|
|
71
|
+
f"Loaded from: '{self.image_path}'."
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
return f"{self.__class__.__name__} loaded from '{self.image_path}'"
|
|
75
|
+
|
|
76
|
+
def footprint(self, crs="") -> Polygon:
|
|
77
|
+
"""The product footprint as a Shapely polygon."""
|
|
78
|
+
return image_footprint(self.image, crs)
|
|
59
79
|
|
|
60
80
|
def _get_data_from_sidecar(
|
|
61
81
|
self, sidecar_path: Path, target_ureg: UnitRegistry | None = None
|
|
@@ -89,7 +109,7 @@ class Level1ABProduct(ProductBase[MetadataLevel1AB]):
|
|
|
89
109
|
|
|
90
110
|
def get_bad_pixel_mask(
|
|
91
111
|
self, camera: str | None = None, per_band: bool = False
|
|
92
|
-
) ->
|
|
112
|
+
) -> rio.DatasetReader:
|
|
93
113
|
"""Get the bad pixel mask associated to each camera of the L0 product
|
|
94
114
|
Returns
|
|
95
115
|
-------
|
|
@@ -107,11 +127,8 @@ class Level1ABProduct(ProductBase[MetadataLevel1AB]):
|
|
|
107
127
|
return self._read_array(self.image_path / bad_pixel_filename)
|
|
108
128
|
|
|
109
129
|
def release_memory(self):
|
|
110
|
-
"""Explicitely
|
|
111
|
-
|
|
112
|
-
NOTE: this function is implemented because of a memory leak inside the Rioxarray
|
|
113
|
-
library that doesn't release memory properly. Only use it when the image data is
|
|
114
|
-
not needed anymore.
|
|
130
|
+
"""Explicitely closes the Rasterio DatasetReader and releases the memory of
|
|
131
|
+
the `image` variable.
|
|
115
132
|
"""
|
|
116
133
|
del self.image
|
|
117
134
|
self.image = None
|
|
@@ -138,10 +155,8 @@ class Level1CProduct(ProductBase[MetadataLevel1C]):
|
|
|
138
155
|
Path to the folder containing the image.
|
|
139
156
|
metadata: MetadataLevel1C
|
|
140
157
|
The metadata associated with the images
|
|
141
|
-
image:
|
|
142
|
-
The
|
|
143
|
-
them so lots of GIS functionality are available on them. For example, the GCPs
|
|
144
|
-
if any could be retrieved like so: `ds.rio.get_gcps()`
|
|
158
|
+
image: rio.DatasetReader
|
|
159
|
+
The Rasterio DatasetReader to open the image and other metadata with.
|
|
145
160
|
data_tags: dict
|
|
146
161
|
Tags saved along with the product. The tag "data_unit" shows what the unit of
|
|
147
162
|
the product actually is.
|
|
@@ -156,25 +171,32 @@ class Level1CProduct(ProductBase[MetadataLevel1C]):
|
|
|
156
171
|
super().__init__(image_path, metadata, target_ureg)
|
|
157
172
|
|
|
158
173
|
self.image = cast(
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
rio.DatasetReader,
|
|
175
|
+
rio.open(self.image_path / "L1C.tif"),
|
|
161
176
|
)
|
|
162
|
-
self.data_tags = self.image.
|
|
177
|
+
self.data_tags = self.image.tags()
|
|
178
|
+
|
|
163
179
|
self.wavelengths = [
|
|
164
180
|
b.wavelength.to("nm").magnitude for b in self.metadata.image.bands
|
|
165
181
|
]
|
|
182
|
+
self.crs = self.image.crs
|
|
166
183
|
|
|
167
184
|
def __repr__(self):
|
|
168
185
|
"""Pretty printing of the object with the most important info"""
|
|
169
186
|
if self.image is not None:
|
|
187
|
+
shape_str = f"({self.image.count}, {self.image.height}, {self.image.width})"
|
|
170
188
|
return (
|
|
171
|
-
f"{self.__class__.__name__} with shape {
|
|
172
|
-
f"and wavelengths {self.wavelengths} (CRS: '{self.
|
|
189
|
+
f"{self.__class__.__name__} with shape {shape_str} "
|
|
190
|
+
f"and wavelengths {self.wavelengths} (CRS: '{self.crs}'). "
|
|
173
191
|
f"Loaded from: '{self.image_path}'."
|
|
174
192
|
)
|
|
175
193
|
else:
|
|
176
194
|
return f"{self.__class__.__name__} loaded from '{self.image_path}'"
|
|
177
195
|
|
|
196
|
+
def footprint(self, crs="") -> Polygon:
|
|
197
|
+
"""The product footprint as a Shapely polygon."""
|
|
198
|
+
return image_footprint(self.image, crs)
|
|
199
|
+
|
|
178
200
|
def _get_data_from_sidecar(
|
|
179
201
|
self, sidecar_path: Path, target_ureg: UnitRegistry | None = None
|
|
180
202
|
) -> MetadataLevel1C:
|
|
@@ -206,12 +228,10 @@ class Level1CProduct(ProductBase[MetadataLevel1C]):
|
|
|
206
228
|
return metadata
|
|
207
229
|
|
|
208
230
|
def release_memory(self):
|
|
209
|
-
"""Explicitely
|
|
210
|
-
|
|
211
|
-
NOTE: this function is implemented because of a memory leak inside the Rioxarray
|
|
212
|
-
library that doesn't release memory properly. Only use it when the image data is
|
|
213
|
-
not needed anymore.
|
|
231
|
+
"""Explicitely closes the Rasterio DatasetReader and releases the memory of
|
|
232
|
+
the `image` variable.
|
|
214
233
|
"""
|
|
234
|
+
self.image.close()
|
|
215
235
|
del self.image
|
|
216
236
|
self.image = None
|
|
217
237
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import cast
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import rasterio as rio
|
|
5
5
|
from kuva_metadata import MetadataLevel2A
|
|
6
6
|
from pint import UnitRegistry
|
|
7
|
-
from
|
|
7
|
+
from shapely import Polygon
|
|
8
|
+
|
|
9
|
+
from kuva_reader import image_footprint
|
|
8
10
|
|
|
9
11
|
from .product_base import ProductBase
|
|
10
12
|
|
|
@@ -30,10 +32,8 @@ class Level2AProduct(ProductBase[MetadataLevel2A]):
|
|
|
30
32
|
Path to the folder containing the image.
|
|
31
33
|
metadata: MetadataLevel2A
|
|
32
34
|
The metadata associated with the images
|
|
33
|
-
image:
|
|
34
|
-
The
|
|
35
|
-
them so lots of GIS functionality are available on them. For example, the GCPs
|
|
36
|
-
if any could be retrieved like so: `ds.rio.get_gcps()`
|
|
35
|
+
image: rasterio.DatasetReader
|
|
36
|
+
The Rasterio DatasetReader to open the image and other metadata with.
|
|
37
37
|
data_tags: dict
|
|
38
38
|
Tags saved along with the product. The tag "data_unit" shows what the unit of
|
|
39
39
|
the product actually is.
|
|
@@ -48,25 +48,32 @@ class Level2AProduct(ProductBase[MetadataLevel2A]):
|
|
|
48
48
|
super().__init__(image_path, metadata, target_ureg)
|
|
49
49
|
|
|
50
50
|
self.image = cast(
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
rio.DatasetReader,
|
|
52
|
+
rio.open(self.image_path / "L2A.tif"),
|
|
53
53
|
)
|
|
54
|
-
self.data_tags = self.image.
|
|
54
|
+
self.data_tags = self.image.tags()
|
|
55
|
+
|
|
55
56
|
self.wavelengths = [
|
|
56
57
|
b.wavelength.to("nm").magnitude for b in self.metadata.image.bands
|
|
57
58
|
]
|
|
59
|
+
self.crs = self.image.crs
|
|
58
60
|
|
|
59
61
|
def __repr__(self):
|
|
60
62
|
"""Pretty printing of the object with the most important info"""
|
|
61
63
|
if self.image is not None:
|
|
64
|
+
shape_str = f"({self.image.count}, {self.image.height}, {self.image.width})"
|
|
62
65
|
return (
|
|
63
|
-
f"{self.__class__.__name__} with shape {
|
|
64
|
-
f"and wavelengths {self.wavelengths} (CRS: '{self.
|
|
66
|
+
f"{self.__class__.__name__} with shape {shape_str} "
|
|
67
|
+
f"and wavelengths {self.wavelengths} (CRS: '{self.crs}'). "
|
|
65
68
|
f"Loaded from: '{self.image_path}'."
|
|
66
69
|
)
|
|
67
70
|
else:
|
|
68
71
|
return f"{self.__class__.__name__} loaded from '{self.image_path}'"
|
|
69
72
|
|
|
73
|
+
def footprint(self, crs="") -> Polygon:
|
|
74
|
+
"""The product footprint as a Shapely polygon."""
|
|
75
|
+
return image_footprint(self.image, crs)
|
|
76
|
+
|
|
70
77
|
def _get_data_from_sidecar(
|
|
71
78
|
self, sidecar_path: Path, target_ureg: UnitRegistry | None = None
|
|
72
79
|
) -> MetadataLevel2A:
|
|
@@ -98,12 +105,10 @@ class Level2AProduct(ProductBase[MetadataLevel2A]):
|
|
|
98
105
|
return metadata
|
|
99
106
|
|
|
100
107
|
def release_memory(self):
|
|
101
|
-
"""Explicitely
|
|
102
|
-
|
|
103
|
-
NOTE: this function is implemented because of a memory leak inside the Rioxarray
|
|
104
|
-
library that doesn't release memory properly. Only use it when the image data is
|
|
105
|
-
not needed anymore.
|
|
108
|
+
"""Explicitely closes the Rasterio DatasetReader and releases the memory of
|
|
109
|
+
the `image` variable.
|
|
106
110
|
"""
|
|
111
|
+
self.image.close()
|
|
107
112
|
del self.image
|
|
108
113
|
self.image = None
|
|
109
114
|
|
|
@@ -2,11 +2,10 @@ from abc import ABCMeta, abstractmethod
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import Generic, TypeVar, cast
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import rasterio as rio
|
|
6
6
|
from kuva_metadata.sections_common import MetadataBase
|
|
7
7
|
from pint import UnitRegistry
|
|
8
8
|
from pydantic import BaseModel
|
|
9
|
-
from xarray import Dataset
|
|
10
9
|
|
|
11
10
|
TMetadata = TypeVar("TMetadata", bound=BaseModel)
|
|
12
11
|
|
|
@@ -66,17 +65,17 @@ class ProductBase(Generic[TMetadata], metaclass=ABCMeta):
|
|
|
66
65
|
pass
|
|
67
66
|
|
|
68
67
|
@staticmethod
|
|
69
|
-
def _read_array(array_path: Path) ->
|
|
68
|
+
def _read_array(array_path: Path) -> rio.DatasetReader:
|
|
70
69
|
if array_path.exists():
|
|
71
70
|
return cast(
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
rio.DatasetReader,
|
|
72
|
+
rio.open(array_path),
|
|
74
73
|
)
|
|
75
74
|
else:
|
|
76
75
|
e_ = f"Product does not contain the array to be read at '{array_path}'"
|
|
77
76
|
raise ValueError(e_)
|
|
78
77
|
|
|
79
|
-
def get_bad_pixel_mask(self, camera: str | None = None) ->
|
|
78
|
+
def get_bad_pixel_mask(self, camera: str | None = None) -> rio.DatasetReader:
|
|
80
79
|
"""Get the bad pixel mask associated to the product.
|
|
81
80
|
|
|
82
81
|
Parameters
|
|
@@ -94,7 +93,7 @@ class ProductBase(Generic[TMetadata], metaclass=ABCMeta):
|
|
|
94
93
|
raise ValueError(e_)
|
|
95
94
|
return self._read_array(self.image_path / "bad_pixel_mask_aggregated.tif")
|
|
96
95
|
|
|
97
|
-
def get_cloud_mask(self, camera: str | None = None) ->
|
|
96
|
+
def get_cloud_mask(self, camera: str | None = None) -> rio.DatasetReader:
|
|
98
97
|
"""Get the cloud mask associated to the product.
|
|
99
98
|
|
|
100
99
|
Parameters
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "kuva-reader"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Manipulate the Kuva Space image and metadata formats"
|
|
9
9
|
authors = ["Guillem Ballesteros <guillem@kuvaspace.com>" , "Lennert Antson <lennert.antson@kuvaspace.com>", "Arthur Vandenhoeke <arthur.vandenhoeke@kuvaspace.com>", "Olli Eloranta <olli.eloranta@kuvaspace.com>"]
|
|
10
10
|
readme = "README.md"
|
|
@@ -28,21 +28,9 @@ numpy = "^1.26.4"
|
|
|
28
28
|
numpy-quaternion = "^2022.4.4"
|
|
29
29
|
pint = "^0.22"
|
|
30
30
|
rasterio = "^1.4.1"
|
|
31
|
-
xarray = "^2022.12.0"
|
|
32
|
-
rioxarray = "^0.12.4"
|
|
33
31
|
kuva-geometry = "*"
|
|
34
32
|
kuva-metadata = "*"
|
|
35
33
|
|
|
36
|
-
# Temporarily can replace pypi version with relative dep if doing local development
|
|
37
|
-
# [tool.poetry.dependencies.kuva-geometry]
|
|
38
|
-
# path = "../kuva-geometry"
|
|
39
|
-
# develop = true
|
|
40
|
-
|
|
41
|
-
# [tool.poetry.dependencies.kuva-metadata]
|
|
42
|
-
# path = "../kuva-metadata"
|
|
43
|
-
# develop = true
|
|
44
|
-
|
|
45
|
-
|
|
46
34
|
[tool.ruff.lint]
|
|
47
35
|
select = [ "E", "F", "A", "DTZ", "NPY", "I", "ISC", "B003", "B004", "B015", "PTH", "D100", "D101", "D102", "D103", "D104", "D105", "D200", "W191", "W291", "W293", "N801", "N804", "N805", "T100", "S105", "S106", "S108", "S604", "S602", "S609", "UP003", "UP005", "UP006", "UP007", "UP008", "UP032", "UP035", "RUF001", "RUF200", "RUF013", "C901", "COM818", "RSE102", "EM101",]
|
|
48
36
|
exclude = [ ".direnv", ".eggs", ".git", ".mypy_cache", ".nox", ".pytype", ".ruff_cache", ".tox", ".venv", "__pypackages__", "_build", "build", "dist", "venv", "__pycache__",]
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
"""Utilities to process images related to product processing."""
|
|
2
|
-
|
|
3
|
-
from typing import cast, overload
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
import xarray
|
|
7
|
-
|
|
8
|
-
# Helper type for image processing purposes. The same operations work both for EO
|
|
9
|
-
# DataArrays and Numpy arrays.
|
|
10
|
-
ImageArray_ = np.ndarray | xarray.DataArray
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@overload
|
|
14
|
-
def image_to_dtype_range(
|
|
15
|
-
img: np.ndarray,
|
|
16
|
-
dtype: np.dtype,
|
|
17
|
-
offset: float | None = None,
|
|
18
|
-
scale: float | None = None,
|
|
19
|
-
) -> tuple[xarray.DataArray, float, float]: ...
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@overload
|
|
23
|
-
def image_to_dtype_range(
|
|
24
|
-
img: xarray.DataArray,
|
|
25
|
-
dtype: np.dtype,
|
|
26
|
-
offset: float | None = None,
|
|
27
|
-
scale: float | None = None,
|
|
28
|
-
) -> tuple[xarray.DataArray, float, float]: ...
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def image_to_dtype_range(
|
|
32
|
-
img: ImageArray_,
|
|
33
|
-
dtype: np.dtype,
|
|
34
|
-
offset: float | None = None,
|
|
35
|
-
scale: float | None = None,
|
|
36
|
-
) -> tuple[ImageArray_, float, float]:
|
|
37
|
-
"""Normalize an image to the bounds of whatever numpy datatype. E.g. np.uint16
|
|
38
|
-
results in a np.uint16 image with values between entire range [0, 65535]
|
|
39
|
-
|
|
40
|
-
Parameters
|
|
41
|
-
----------
|
|
42
|
-
img
|
|
43
|
-
Image to normalize
|
|
44
|
-
dtype
|
|
45
|
-
Target data type, only integer subtypes currently sensible and are supported
|
|
46
|
-
offset, optional
|
|
47
|
-
Offset if that was already precomputed. If not, it will be calculated from `arr`
|
|
48
|
-
scale, optional
|
|
49
|
-
Scale if that was already precomputed. If not, it will be calculated from `arr`
|
|
50
|
-
|
|
51
|
-
Returns
|
|
52
|
-
-------
|
|
53
|
-
The normalized image along casted to given data type, along with the offset and
|
|
54
|
-
scale used to normalize it
|
|
55
|
-
|
|
56
|
-
Raises
|
|
57
|
-
------
|
|
58
|
-
ValueError
|
|
59
|
-
Unsupported data type
|
|
60
|
-
"""
|
|
61
|
-
if np.issubdtype(dtype, np.integer):
|
|
62
|
-
type_info = np.iinfo(dtype)
|
|
63
|
-
else:
|
|
64
|
-
e_ = f"Unsupported dtype {dtype} for normalization"
|
|
65
|
-
raise ValueError(e_)
|
|
66
|
-
|
|
67
|
-
dtype_min = type_info.min
|
|
68
|
-
dtype_max = type_info.max
|
|
69
|
-
|
|
70
|
-
if offset is None or scale is None:
|
|
71
|
-
offset_ = cast(float, np.min(img))
|
|
72
|
-
scale_ = cast(float, np.max(img) - offset_)
|
|
73
|
-
else:
|
|
74
|
-
offset_ = offset
|
|
75
|
-
scale_ = scale
|
|
76
|
-
|
|
77
|
-
normed_to_0_1 = (img - offset_) / scale_
|
|
78
|
-
|
|
79
|
-
normalized_image = normed_to_0_1 * (dtype_max - dtype_min) + dtype_min
|
|
80
|
-
normalized_image = normalized_image.astype(dtype)
|
|
81
|
-
|
|
82
|
-
return normalized_image, offset_, scale_
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@overload
|
|
86
|
-
def image_to_uint16_range(img: np.ndarray) -> tuple[np.ndarray, float, float]: ...
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@overload
|
|
90
|
-
def image_to_uint16_range(
|
|
91
|
-
img: xarray.DataArray,
|
|
92
|
-
) -> tuple[xarray.DataArray, float, float]: ...
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def image_to_uint16_range(img: ImageArray_) -> tuple[ImageArray_, float, float]:
|
|
96
|
-
"""Normalise image to bounds of uint16, see above function for details
|
|
97
|
-
|
|
98
|
-
Parameters
|
|
99
|
-
----------
|
|
100
|
-
img
|
|
101
|
-
Image to normalize
|
|
102
|
-
|
|
103
|
-
Returns
|
|
104
|
-
-------
|
|
105
|
-
The normalized image along casted to given data type, along with the offset and
|
|
106
|
-
scale used to normalize it
|
|
107
|
-
"""
|
|
108
|
-
return image_to_dtype_range(img, np.dtype(np.uint16))
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@overload
|
|
112
|
-
def image_to_original_range(
|
|
113
|
-
img: np.ndarray,
|
|
114
|
-
offset: float,
|
|
115
|
-
scale: float,
|
|
116
|
-
dtype: np.dtype | None = None,
|
|
117
|
-
) -> xarray.DataArray: ...
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@overload
|
|
121
|
-
def image_to_original_range(
|
|
122
|
-
img: xarray.DataArray,
|
|
123
|
-
offset: float,
|
|
124
|
-
scale: float,
|
|
125
|
-
dtype: np.dtype | None = None,
|
|
126
|
-
) -> xarray.DataArray: ...
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def image_to_original_range(
|
|
130
|
-
img: ImageArray_,
|
|
131
|
-
offset: float,
|
|
132
|
-
scale: float,
|
|
133
|
-
dtype: np.dtype | None = None,
|
|
134
|
-
) -> ImageArray_:
|
|
135
|
-
"""Revert normalisation applied to an image. The image 'arr' must have the same
|
|
136
|
-
data type as the result from normalization, or it must be given separately
|
|
137
|
-
|
|
138
|
-
Parameters
|
|
139
|
-
----------
|
|
140
|
-
arr
|
|
141
|
-
Image to revert back to original values
|
|
142
|
-
offset
|
|
143
|
-
Offset that was applied to the image
|
|
144
|
-
scale
|
|
145
|
-
Scale that was applied to the image
|
|
146
|
-
dtype, optional
|
|
147
|
-
The data type that the image was casted to during normalization, by default None
|
|
148
|
-
where the data type of `arr` will be assumed to be correct.
|
|
149
|
-
|
|
150
|
-
Returns
|
|
151
|
-
-------
|
|
152
|
-
Image that is back in original range of values before normalization
|
|
153
|
-
|
|
154
|
-
Raises
|
|
155
|
-
------
|
|
156
|
-
ValueError
|
|
157
|
-
Unsupported data type
|
|
158
|
-
"""
|
|
159
|
-
if not dtype:
|
|
160
|
-
dtype = img.dtype
|
|
161
|
-
|
|
162
|
-
# Check real bounds from numpy data types
|
|
163
|
-
if np.issubdtype(dtype, np.integer) and isinstance(dtype, np.dtype):
|
|
164
|
-
type_info = np.iinfo(dtype)
|
|
165
|
-
else:
|
|
166
|
-
e_ = f"Unsupported dtype {dtype} for normalization"
|
|
167
|
-
raise ValueError(e_)
|
|
168
|
-
|
|
169
|
-
dtype_min = type_info.min
|
|
170
|
-
dtype_max = type_info.max
|
|
171
|
-
|
|
172
|
-
# Reverse the normalization
|
|
173
|
-
denormed_to_0_1 = (img - dtype_min) / (dtype_max - dtype_min)
|
|
174
|
-
original_image = denormed_to_0_1 * scale + offset
|
|
175
|
-
|
|
176
|
-
return original_image
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|