kuva-reader 0.1.0__py3-none-any.whl
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/__init__.py +44 -0
- kuva_reader/py.typed +0 -0
- kuva_reader/reader/__init__.py +0 -0
- kuva_reader/reader/image.py +176 -0
- kuva_reader/reader/level0.py +238 -0
- kuva_reader/reader/level1.py +178 -0
- kuva_reader/reader/level2.py +100 -0
- kuva_reader/reader/product_base.py +129 -0
- kuva_reader/reader/py.typed +0 -0
- kuva_reader/reader/utils.py +51 -0
- kuva_reader-0.1.0.dist-info/METADATA +24 -0
- kuva_reader-0.1.0.dist-info/RECORD +14 -0
- kuva_reader-0.1.0.dist-info/WHEEL +4 -0
- kuva_reader-0.1.0.dist-info/entry_points.txt +4 -0
kuva_reader/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kuva Reader provides functionality for opening and accessing Kuva Space Earth
|
|
3
|
+
Observation (EO) products. The module handles the reading and parsing of image
|
|
4
|
+
data, as well as extracting and structuring the associated metadata to
|
|
5
|
+
facilitate further analysis or visualization.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
- **Open EO Products**: Load satellite images and corresponding metadata from
|
|
10
|
+
various data formats.
|
|
11
|
+
- **Access Metadata**: Retrieve information such as acquisition time, satellite
|
|
12
|
+
name, sensor type, geospatial coordinates, and any custom metadata embedded
|
|
13
|
+
within the product.
|
|
14
|
+
- **Image Handling**: Manage the loading of image data for efficient use in
|
|
15
|
+
analytical processes.
|
|
16
|
+
|
|
17
|
+
## Dependencies
|
|
18
|
+
- **kuva-metadata**: A specialized library that handles the extraction and
|
|
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
|
|
22
|
+
visualization.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
from .reader.image import (
|
|
28
|
+
image_to_dtype_range,
|
|
29
|
+
image_to_original_range,
|
|
30
|
+
image_to_uint16_range,
|
|
31
|
+
)
|
|
32
|
+
from .reader.level0 import Level0Product
|
|
33
|
+
from .reader.level1 import Level1ABProduct, Level1CProduct
|
|
34
|
+
from .reader.level2 import Level2AProduct
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"Level0Product",
|
|
38
|
+
"Level1ABProduct",
|
|
39
|
+
"Level1CProduct",
|
|
40
|
+
"Level2AProduct",
|
|
41
|
+
"image_to_dtype_range",
|
|
42
|
+
"image_to_original_range",
|
|
43
|
+
"image_to_uint16_range",
|
|
44
|
+
]
|
kuva_reader/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import rioxarray as rx
|
|
6
|
+
import xarray
|
|
7
|
+
from kuva_metadata import MetadataLevel0
|
|
8
|
+
from pint import UnitRegistry
|
|
9
|
+
|
|
10
|
+
from kuva_reader import image_to_dtype_range, image_to_original_range
|
|
11
|
+
|
|
12
|
+
from .product_base import ProductBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Level0Product(ProductBase[MetadataLevel0]):
|
|
16
|
+
"""
|
|
17
|
+
Level 0 products contain the raw data acquired from the sensor. They
|
|
18
|
+
consist of one roughly georeferenced geotiff per camera and the associated
|
|
19
|
+
metadata. Changes to them are only performed at the metadata level to avoid
|
|
20
|
+
deteriorating them.
|
|
21
|
+
|
|
22
|
+
At this processing level frames are not aligned, a natural consequence of
|
|
23
|
+
satellite motion, and are therefore not very useful for any activity that
|
|
24
|
+
require working with more than one band simultaneously. In that case you
|
|
25
|
+
should look into using L1 products.
|
|
26
|
+
|
|
27
|
+
The data in the image files is lazy loaded to make things snappier for end
|
|
28
|
+
users but may lead to surprising behaviour if you are not aware of it
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
image_path
|
|
34
|
+
Path to the folder containing the L0 product images
|
|
35
|
+
metadata, optional
|
|
36
|
+
Metadata if already read e.g. from a database. By default None, meaning
|
|
37
|
+
automatic fetching from metadata sidecar file
|
|
38
|
+
target_ureg, optional
|
|
39
|
+
Pint Unit Registry to swap to. This is only relevant when parsing data from a
|
|
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
|
+
|
|
49
|
+
Attributes
|
|
50
|
+
----------
|
|
51
|
+
image_path: Path
|
|
52
|
+
Path to the folder containing the images.
|
|
53
|
+
metadata: MetadataLevel0
|
|
54
|
+
The metadata associated with the images
|
|
55
|
+
images: Dict[str, xarray.DataArray]
|
|
56
|
+
The arrays with the actual data. This have the rioxarray extension activated on
|
|
57
|
+
them so lots of GIS functionality are available on them. Imporantly, the GCPs
|
|
58
|
+
can be retrieved like so: `ds.rio.get_gcps()`
|
|
59
|
+
data_tags: Dict[str, Any]
|
|
60
|
+
Tags stored along with the data. These can be used e.g. to check the physical
|
|
61
|
+
units of pixels or normalisation factors.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
image_path: Path,
|
|
67
|
+
metadata: MetadataLevel0 | None = None,
|
|
68
|
+
target_ureg: UnitRegistry | None = None,
|
|
69
|
+
as_physical_unit: bool = False,
|
|
70
|
+
target_dtype: np.dtype | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
super().__init__(image_path, metadata, target_ureg)
|
|
73
|
+
|
|
74
|
+
self.images = {
|
|
75
|
+
camera: cast(
|
|
76
|
+
xarray.DataArray,
|
|
77
|
+
rx.open_rasterio(
|
|
78
|
+
self.image_path / (cube.camera.name + ".tif"),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
for camera, cube in self.metadata.image.data_cubes.items() # type: ignore
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Read tags for images and denormalize / renormalize if needed
|
|
85
|
+
self.data_tags = {camera: img.attrs for camera, img in self.images.items()}
|
|
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
|
|
103
|
+
|
|
104
|
+
def __getitem__(self, camera: str) -> xarray.DataArray:
|
|
105
|
+
"""Return the datarray for the chosen camera."""
|
|
106
|
+
return self.images[camera]
|
|
107
|
+
|
|
108
|
+
def keys(self) -> list[str]:
|
|
109
|
+
"""Easy access to the camera keys."""
|
|
110
|
+
return list(self.images.keys())
|
|
111
|
+
|
|
112
|
+
def _get_data_from_sidecar(
|
|
113
|
+
self, sidecar_path: Path, target_ureg: UnitRegistry | None = None
|
|
114
|
+
) -> MetadataLevel0:
|
|
115
|
+
"""Read product metadata from the sidecar file attached with the product
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
sidecar_path
|
|
120
|
+
Path to sidecar JSON
|
|
121
|
+
target_ureg, optional
|
|
122
|
+
Unit registry to change to when validating JSON, by default None
|
|
123
|
+
(kuva-metadata ureg)
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
The metadata object
|
|
128
|
+
"""
|
|
129
|
+
with (sidecar_path).open("r") as fh:
|
|
130
|
+
if target_ureg is None:
|
|
131
|
+
metadata = MetadataLevel0.model_validate_json(
|
|
132
|
+
fh.read(),
|
|
133
|
+
context={
|
|
134
|
+
"image_path": sidecar_path.parent,
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
# The Image subclass in MetadataLevel0 has an alignment graph that
|
|
139
|
+
# requires a specific context. Swapping UnitRegistries will also require
|
|
140
|
+
# serialization, requiring the extra graph path context parameter.
|
|
141
|
+
metadata = cast(
|
|
142
|
+
MetadataLevel0,
|
|
143
|
+
MetadataLevel0.model_validate_json_with_ureg(
|
|
144
|
+
fh.read(),
|
|
145
|
+
target_ureg,
|
|
146
|
+
context={
|
|
147
|
+
"image_path": sidecar_path.parent,
|
|
148
|
+
"graph_json_file_name": f"{sidecar_path.stem}_graph.json",
|
|
149
|
+
},
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return metadata
|
|
154
|
+
|
|
155
|
+
def _calculate_band_offsets_and_frames(self, cube: str):
|
|
156
|
+
bands_info = self.metadata.image.data_cubes[cube].bands
|
|
157
|
+
|
|
158
|
+
band_n_frames = [band.n_frames for band in bands_info]
|
|
159
|
+
band_offsets = np.cumsum(band_n_frames)
|
|
160
|
+
|
|
161
|
+
# The first offset ie 0 is missing and the last is not an offset just the
|
|
162
|
+
# length. Fix it.
|
|
163
|
+
band_offsets = band_offsets[:-1].tolist()
|
|
164
|
+
band_offsets.insert(0, 0)
|
|
165
|
+
return band_offsets, band_n_frames
|
|
166
|
+
|
|
167
|
+
def calculate_frame_offset(self, cube: str, band_id: int, frame_idx: int) -> int:
|
|
168
|
+
"""Find the offset at which a frame lives within a cube."""
|
|
169
|
+
band_offsets, _ = self._calculate_band_offsets_and_frames(cube)
|
|
170
|
+
frame_offset = band_offsets[band_id] + frame_idx
|
|
171
|
+
|
|
172
|
+
return frame_offset
|
|
173
|
+
|
|
174
|
+
def read_frame(self, cube: str, band_id: int, frame_idx: int) -> np.ndarray:
|
|
175
|
+
"""Extract a specific frame from a cube and band."""
|
|
176
|
+
frame_offset = self.calculate_frame_offset(cube, band_id, frame_idx)
|
|
177
|
+
return self[cube][frame_offset, :, :].to_numpy()
|
|
178
|
+
|
|
179
|
+
def read_band(self, cube: str, band_id: int) -> np.ndarray:
|
|
180
|
+
"""Extract a specific band from a cube"""
|
|
181
|
+
band_offsets, band_n_frames = self._calculate_band_offsets_and_frames(cube)
|
|
182
|
+
|
|
183
|
+
# Calculate the final frame offset for this band and frame
|
|
184
|
+
band_offset_ll = band_offsets[band_id]
|
|
185
|
+
band_offset_ul = band_offset_ll + band_n_frames[band_id]
|
|
186
|
+
return self[cube][band_offset_ll:band_offset_ul, :, :].to_numpy()
|
|
187
|
+
|
|
188
|
+
def read_data_units(self) -> np.ndarray:
|
|
189
|
+
"""Read unit of product and validate they match between cameras"""
|
|
190
|
+
units = [tags.get("data_unit") for tags in self.data_tags.values()]
|
|
191
|
+
if all(product_unit == units[0] for product_unit in units):
|
|
192
|
+
return units[0]
|
|
193
|
+
else:
|
|
194
|
+
# TODO: We should try conversion though
|
|
195
|
+
e_ = "Cameras have different physical units stored to them."
|
|
196
|
+
raise ValueError(e_)
|
|
197
|
+
|
|
198
|
+
def get_bad_pixel_mask(self, camera: str | None = None) -> xarray.Dataset:
|
|
199
|
+
"""Get the bad pixel mask associated to each camera of the L0 product
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
The bad pixel masks of the cameras
|
|
204
|
+
"""
|
|
205
|
+
if camera is None:
|
|
206
|
+
e_ = "The `camera` argument must be given for L0 product bad pixel masks."
|
|
207
|
+
raise ValueError(e_)
|
|
208
|
+
bad_pixel_filename = f"{camera}_per_frame_cloud_mask.tif"
|
|
209
|
+
return self._read_array(self.image_path / bad_pixel_filename)
|
|
210
|
+
|
|
211
|
+
def get_cloud_mask(self, camera: str | None = None) -> xarray.Dataset:
|
|
212
|
+
"""Get the cloud mask associated to the product.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
The cloud mask
|
|
217
|
+
"""
|
|
218
|
+
if camera is None:
|
|
219
|
+
e_ = "The `camera` argument must be given for L0 product cloud masks."
|
|
220
|
+
raise ValueError(e_)
|
|
221
|
+
bad_pixel_filename = f"{camera}_per_frame_cloud_mask.tif"
|
|
222
|
+
return self._read_array(self.image_path / bad_pixel_filename)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def generate_level_0_metafile():
|
|
226
|
+
"""Example function for reading a product and generating a metadata file from the
|
|
227
|
+
sidecar metadata objects.
|
|
228
|
+
"""
|
|
229
|
+
import argparse
|
|
230
|
+
|
|
231
|
+
parser = argparse.ArgumentParser()
|
|
232
|
+
parser.add_argument("image_path")
|
|
233
|
+
args = parser.parse_args()
|
|
234
|
+
|
|
235
|
+
image_path = Path(args.image_path)
|
|
236
|
+
|
|
237
|
+
product = Level0Product(image_path)
|
|
238
|
+
product.generate_metadata_file()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
import rioxarray as rx
|
|
5
|
+
from kuva_metadata import MetadataLevel1AB, MetadataLevel1C
|
|
6
|
+
from pint import UnitRegistry
|
|
7
|
+
from xarray import Dataset
|
|
8
|
+
|
|
9
|
+
from .product_base import ProductBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Level1ABProduct(ProductBase[MetadataLevel1AB]):
|
|
13
|
+
"""
|
|
14
|
+
Level 1AB products combine multiple L0 products into a band aligned product.
|
|
15
|
+
|
|
16
|
+
Changes to them are only performed at the metadata level where results may be
|
|
17
|
+
cached for further use.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
image_path
|
|
22
|
+
Path to the folder containing the L1A or L1B product
|
|
23
|
+
metadata, optional
|
|
24
|
+
Metadata if already read e.g. from a database. By default None, meaning
|
|
25
|
+
automatic fetching from metadata sidecar file
|
|
26
|
+
target_ureg, optional
|
|
27
|
+
Pint Unit Registry to swap to. This is only relevant when parsing data from a
|
|
28
|
+
JSON file, which by default uses the kuva-metadata ureg.
|
|
29
|
+
|
|
30
|
+
Attributes
|
|
31
|
+
----------
|
|
32
|
+
image_path: Path
|
|
33
|
+
Path to the folder containing the image.
|
|
34
|
+
metadata: MetadataLevel1AB
|
|
35
|
+
The metadata associated with the images
|
|
36
|
+
image: xarray.DataArray
|
|
37
|
+
The arrays with the actual data. This have the rioxarray extension activated on
|
|
38
|
+
them so lots of GIS functionality are available on them. For example, the GCPs
|
|
39
|
+
if any could be retrieved like so: `ds.rio.get_gcps()`
|
|
40
|
+
data_tags: dict
|
|
41
|
+
Tags saved along with the product. The tag "data_unit" shows what the unit of
|
|
42
|
+
the product actually is.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
image_path: Path,
|
|
48
|
+
metadata: MetadataLevel1AB | None = None,
|
|
49
|
+
target_ureg: UnitRegistry | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(image_path, metadata, target_ureg)
|
|
52
|
+
|
|
53
|
+
self.image = cast(
|
|
54
|
+
Dataset,
|
|
55
|
+
rx.open_rasterio(self.image_path / "L1B.tif"),
|
|
56
|
+
)
|
|
57
|
+
self.data_tags = self.image.attrs
|
|
58
|
+
|
|
59
|
+
def _get_data_from_sidecar(
|
|
60
|
+
self, sidecar_path: Path, target_ureg: UnitRegistry | None = None
|
|
61
|
+
) -> MetadataLevel1AB:
|
|
62
|
+
"""Read product metadata from the sidecar file attached with the product
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
sidecar_path
|
|
67
|
+
Path to sidecar JSON
|
|
68
|
+
target_ureg, optional
|
|
69
|
+
Unit registry to change to when validating JSON, by default None
|
|
70
|
+
(kuva-metadata ureg)
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
The metadata object
|
|
75
|
+
"""
|
|
76
|
+
with (sidecar_path).open("r") as fh:
|
|
77
|
+
if target_ureg is None:
|
|
78
|
+
metadata = MetadataLevel1AB.model_validate_json(fh.read())
|
|
79
|
+
else:
|
|
80
|
+
metadata = cast(
|
|
81
|
+
MetadataLevel1AB,
|
|
82
|
+
MetadataLevel1AB.model_validate_json_with_ureg(
|
|
83
|
+
fh.read(), target_ureg
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return metadata
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Level1CProduct(ProductBase[MetadataLevel1C]):
|
|
91
|
+
"""
|
|
92
|
+
Level 1C products are georeferenced and orthorectified L1AB products.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
image_path
|
|
97
|
+
Path to the folder containing the L1C product
|
|
98
|
+
metadata, optional
|
|
99
|
+
Metadata if already read e.g. from a database. By default None, meaning
|
|
100
|
+
automatic fetching from metadata sidecar file
|
|
101
|
+
target_ureg, optional
|
|
102
|
+
Pint Unit Registry to swap to. This is only relevant when parsing data from a
|
|
103
|
+
JSON file, which by default uses the kuva-metadata ureg.
|
|
104
|
+
|
|
105
|
+
Attributes
|
|
106
|
+
----------
|
|
107
|
+
image_path: Path
|
|
108
|
+
Path to the folder containing the image.
|
|
109
|
+
metadata: MetadataLevel1C
|
|
110
|
+
The metadata associated with the images
|
|
111
|
+
image: xarray.DataArray
|
|
112
|
+
The arrays with the actual data. This have the rioxarray extension activated on
|
|
113
|
+
them so lots of GIS functionality are available on them. For example, the GCPs
|
|
114
|
+
if any could be retrieved like so: `ds.rio.get_gcps()`
|
|
115
|
+
data_tags: dict
|
|
116
|
+
Tags saved along with the product. The tag "data_unit" shows what the unit of
|
|
117
|
+
the product actually is.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
image_path: Path,
|
|
123
|
+
metadata: MetadataLevel1C | None = None,
|
|
124
|
+
target_ureg: UnitRegistry | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
super().__init__(image_path, metadata, target_ureg)
|
|
127
|
+
|
|
128
|
+
self.image = cast(
|
|
129
|
+
Dataset,
|
|
130
|
+
rx.open_rasterio(self.image_path / "L1C.tif"),
|
|
131
|
+
)
|
|
132
|
+
self.data_tags = self.image.attrs
|
|
133
|
+
|
|
134
|
+
def _get_data_from_sidecar(
|
|
135
|
+
self, sidecar_path: Path, target_ureg: UnitRegistry | None = None
|
|
136
|
+
) -> MetadataLevel1C:
|
|
137
|
+
"""Read product metadata from the sidecar file attached with the product
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
sidecar_path
|
|
142
|
+
Path to sidecar JSON
|
|
143
|
+
target_ureg, optional
|
|
144
|
+
Unit registry to change to when validating JSON, by default None
|
|
145
|
+
(kuva-metadata ureg)
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
The metadata object
|
|
150
|
+
"""
|
|
151
|
+
with (sidecar_path).open("r") as fh:
|
|
152
|
+
if target_ureg is None:
|
|
153
|
+
metadata = MetadataLevel1C.model_validate_json(fh.read())
|
|
154
|
+
else:
|
|
155
|
+
metadata = cast(
|
|
156
|
+
MetadataLevel1C,
|
|
157
|
+
MetadataLevel1C.model_validate_json_with_ureg(
|
|
158
|
+
fh.read(), target_ureg
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return metadata
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def generate_level_1_metafile():
|
|
166
|
+
"""Example function for reading a product and generating a metadata file from the
|
|
167
|
+
sidecar metadata objects.
|
|
168
|
+
"""
|
|
169
|
+
import argparse
|
|
170
|
+
|
|
171
|
+
parser = argparse.ArgumentParser()
|
|
172
|
+
parser.add_argument("image_path")
|
|
173
|
+
args = parser.parse_args()
|
|
174
|
+
|
|
175
|
+
image_path = Path(args.image_path)
|
|
176
|
+
|
|
177
|
+
product = Level1ABProduct(image_path)
|
|
178
|
+
product.generate_metadata_file()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
import rioxarray as rx
|
|
5
|
+
from kuva_metadata import MetadataLevel2A
|
|
6
|
+
from pint import UnitRegistry
|
|
7
|
+
from xarray import Dataset
|
|
8
|
+
|
|
9
|
+
from .product_base import ProductBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Level2AProduct(ProductBase[MetadataLevel2A]):
|
|
13
|
+
"""
|
|
14
|
+
Level 2A products contain the atmospherically corrected BOA reflectance values.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
image_path
|
|
19
|
+
Path to the folder containing the L2A product
|
|
20
|
+
metadata, optional
|
|
21
|
+
Metadata if already read e.g. from a database. By default None, meaning
|
|
22
|
+
automatic fetching from metadata sidecar file
|
|
23
|
+
target_ureg, optional
|
|
24
|
+
Pint Unit Registry to swap to. This is only relevant when parsing data from a
|
|
25
|
+
JSON file, which by default uses the kuva-metadata ureg.
|
|
26
|
+
|
|
27
|
+
Attributes
|
|
28
|
+
----------
|
|
29
|
+
image_path: Path
|
|
30
|
+
Path to the folder containing the image.
|
|
31
|
+
metadata: MetadataLevel2A
|
|
32
|
+
The metadata associated with the images
|
|
33
|
+
image: xarray.DataArray
|
|
34
|
+
The arrays with the actual data. This have the rioxarray extension activated on
|
|
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()`
|
|
37
|
+
data_tags: dict
|
|
38
|
+
Tags saved along with the product. The tag "data_unit" shows what the unit of
|
|
39
|
+
the product actually is.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
image_path: Path,
|
|
45
|
+
metadata: MetadataLevel2A | None = None,
|
|
46
|
+
target_ureg: UnitRegistry | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
super().__init__(image_path, metadata, target_ureg)
|
|
49
|
+
|
|
50
|
+
self.image = cast(
|
|
51
|
+
Dataset,
|
|
52
|
+
rx.open_rasterio(self.image_path / "L2A.tif"),
|
|
53
|
+
)
|
|
54
|
+
self.data_tags = self.image.attrs
|
|
55
|
+
|
|
56
|
+
def _get_data_from_sidecar(
|
|
57
|
+
self, sidecar_path: Path, target_ureg: UnitRegistry | None = None
|
|
58
|
+
) -> MetadataLevel2A:
|
|
59
|
+
"""Read product metadata from the sidecar file attached with the product
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
sidecar_path
|
|
64
|
+
Path to sidecar JSON
|
|
65
|
+
target_ureg, optional
|
|
66
|
+
Unit registry to change to when validating JSON, by default None
|
|
67
|
+
(kuva-metadata ureg)
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
The metadata object
|
|
72
|
+
"""
|
|
73
|
+
with (sidecar_path).open("r") as fh:
|
|
74
|
+
if target_ureg is None:
|
|
75
|
+
metadata = MetadataLevel2A.model_validate_json(fh.read())
|
|
76
|
+
else:
|
|
77
|
+
metadata = cast(
|
|
78
|
+
MetadataLevel2A,
|
|
79
|
+
MetadataLevel2A.model_validate_json_with_ureg(
|
|
80
|
+
fh.read(), target_ureg
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return metadata
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def generate_level_2_metafile():
|
|
88
|
+
"""Example function for reading a product and generating a metadata file from the
|
|
89
|
+
sidecar metadata objects.
|
|
90
|
+
"""
|
|
91
|
+
import argparse
|
|
92
|
+
|
|
93
|
+
parser = argparse.ArgumentParser()
|
|
94
|
+
parser.add_argument("image_path")
|
|
95
|
+
args = parser.parse_args()
|
|
96
|
+
|
|
97
|
+
image_path = Path(args.image_path)
|
|
98
|
+
|
|
99
|
+
product = Level2AProduct(image_path)
|
|
100
|
+
product.generate_metadata_file()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from abc import ABCMeta, abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Generic, TypeVar, cast
|
|
4
|
+
|
|
5
|
+
import rioxarray as rx
|
|
6
|
+
from kuva_metadata.sections_common import MetadataBase
|
|
7
|
+
from pint import UnitRegistry
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from xarray import Dataset
|
|
10
|
+
|
|
11
|
+
TMetadata = TypeVar("TMetadata", bound=BaseModel)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProductBase(Generic[TMetadata], metaclass=ABCMeta):
|
|
15
|
+
"""Base class for all Kuva product levels containing the image and all metadata
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
image_path
|
|
20
|
+
Local path to the stored image
|
|
21
|
+
metadata, optional
|
|
22
|
+
Metadata if already read e.g. from a database. By default None, meaning
|
|
23
|
+
automatic fetching from metadata sidecar file
|
|
24
|
+
target_ureg, optional
|
|
25
|
+
Pint Unit Registry to swap to. This is only relevant when parsing data from a
|
|
26
|
+
JSON file, which by default uses the kuva-metadata ureg.
|
|
27
|
+
|
|
28
|
+
Raises
|
|
29
|
+
------
|
|
30
|
+
ValueError
|
|
31
|
+
Providing Kuva image as something else than a folder
|
|
32
|
+
Exception
|
|
33
|
+
Any errors coming from the reading of the sidecar object
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
image_path: Path,
|
|
39
|
+
metadata: MetadataBase | None = None,
|
|
40
|
+
target_ureg: UnitRegistry | None = None,
|
|
41
|
+
):
|
|
42
|
+
self.image_path = Path(image_path)
|
|
43
|
+
|
|
44
|
+
if not self.image_path.exists():
|
|
45
|
+
e_ = f"Image path does not exist: {self.image_path}"
|
|
46
|
+
raise ValueError(e_)
|
|
47
|
+
|
|
48
|
+
if not self.image_path.is_dir():
|
|
49
|
+
e_ = "Kuva images are folders."
|
|
50
|
+
raise ValueError(e_)
|
|
51
|
+
|
|
52
|
+
if metadata is None:
|
|
53
|
+
sidecar_path = self.image_path / f"{self.image_path.name}.json"
|
|
54
|
+
try:
|
|
55
|
+
self.metadata = self._get_data_from_sidecar(sidecar_path, target_ureg)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
e_ = f"Metadata could not be read from the sidecar: {sidecar_path}."
|
|
58
|
+
raise Exception(e_).with_traceback(e.__traceback__)
|
|
59
|
+
else:
|
|
60
|
+
self.metadata = metadata
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def _get_data_from_sidecar(
|
|
64
|
+
self, sidecar_path: Path, target_ureg: UnitRegistry | None = None
|
|
65
|
+
) -> TMetadata:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _read_array(array_path: Path) -> Dataset:
|
|
70
|
+
if array_path.exists():
|
|
71
|
+
return cast(
|
|
72
|
+
Dataset,
|
|
73
|
+
rx.open_rasterio(array_path),
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
e_ = f"Product does not contain the array to be read at '{array_path}'"
|
|
77
|
+
raise ValueError(e_)
|
|
78
|
+
|
|
79
|
+
def get_bad_pixel_mask(self, camera: str | None = None) -> Dataset:
|
|
80
|
+
"""Get the bad pixel mask associated to the product.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
camera
|
|
85
|
+
The camera to fetch the mask for. Only valid for L0 products, and is ignored
|
|
86
|
+
in any other level.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
The bad pixel mask
|
|
91
|
+
"""
|
|
92
|
+
if camera is not None:
|
|
93
|
+
e_ = "Parameter `camera` is not supported in this product level."
|
|
94
|
+
raise ValueError(e_)
|
|
95
|
+
return self._read_array(self.image_path / "bad_pixel_mask_aggregated.tif")
|
|
96
|
+
|
|
97
|
+
def get_cloud_mask(self, camera: str | None = None) -> Dataset:
|
|
98
|
+
"""Get the cloud mask associated to the product.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
camera
|
|
103
|
+
The camera to fetch the mask for. Only valid for L0 products, and is ignored
|
|
104
|
+
in any other level.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
The cloud mask
|
|
109
|
+
"""
|
|
110
|
+
if camera is not None:
|
|
111
|
+
e_ = "Parameter `camera` is not supported in this product level."
|
|
112
|
+
raise ValueError(e_)
|
|
113
|
+
return self._read_array(self.image_path / "cloud_mask.tif")
|
|
114
|
+
|
|
115
|
+
def generate_metadata_file(self) -> None:
|
|
116
|
+
"""Write the sidecar files next to the product."""
|
|
117
|
+
metadata_file_name = self.image_path.name + ".json"
|
|
118
|
+
graph_json_file_name = self.image_path.name + "_graph.json"
|
|
119
|
+
|
|
120
|
+
with (self.image_path / metadata_file_name).open("w") as fh:
|
|
121
|
+
fh.write(
|
|
122
|
+
self.metadata.model_dump_json(
|
|
123
|
+
indent=2,
|
|
124
|
+
context={
|
|
125
|
+
"image_path": self.image_path,
|
|
126
|
+
"graph_json_file_name": graph_json_file_name,
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import rasterio
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def db_conn_str():
|
|
8
|
+
"Prepare a connection string to connect to the DB"
|
|
9
|
+
test_db_params = {
|
|
10
|
+
"POSTGRES_USER": "postgres",
|
|
11
|
+
"POSTGRES_PASSWORD": "postgres",
|
|
12
|
+
"POSTGRES_HOST": "localhost",
|
|
13
|
+
"POSTGRES_DB": "hyperfield",
|
|
14
|
+
"POSTGRES_PORT": "5432",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def query_param(param):
|
|
18
|
+
return os.environ[param] if param in os.environ else test_db_params[param]
|
|
19
|
+
|
|
20
|
+
username = query_param("POSTGRES_USER")
|
|
21
|
+
password = query_param("POSTGRES_PASSWORD")
|
|
22
|
+
host = query_param("POSTGRES_HOST")
|
|
23
|
+
name = query_param("POSTGRES_DB")
|
|
24
|
+
port = query_param("POSTGRES_PORT")
|
|
25
|
+
|
|
26
|
+
conn_str = f"postgres://{username}:{password}@{host}:{port}/{name}?sslmode=disable"
|
|
27
|
+
|
|
28
|
+
return conn_str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def retrieve_folder_product_id(image_path: Path, product_level: str) -> str:
|
|
32
|
+
tif_files = Path(image_path).glob("*.tif")
|
|
33
|
+
|
|
34
|
+
potential_ids = set()
|
|
35
|
+
for tif in tif_files:
|
|
36
|
+
ds = rasterio.open(tif)
|
|
37
|
+
tags = ds.tags()
|
|
38
|
+
|
|
39
|
+
if "_KUVA_PRODUCT_LEVEL" in tags and "_KUVA_PRODUCT_ID" in tags:
|
|
40
|
+
if tags["_KUVA_PRODUCT_LEVEL"] == product_level:
|
|
41
|
+
# This are files of interest
|
|
42
|
+
potential_ids.add(tags["_KUVA_PRODUCT_ID"])
|
|
43
|
+
|
|
44
|
+
if len(potential_ids) == 0:
|
|
45
|
+
raise ValueError(f"The folder contains no KUVA L{product_level} products.")
|
|
46
|
+
elif len(potential_ids) > 1:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"The folder contains more than one KUVA L{product_level} product."
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
return list(potential_ids)[0]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: kuva-reader
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: dask (>=2023.12.1,<2024.0.0)
|
|
16
|
+
Requires-Dist: kuva-geometry
|
|
17
|
+
Requires-Dist: kuva-metadata
|
|
18
|
+
Requires-Dist: numpy (>=1.26.4,<2.0.0)
|
|
19
|
+
Requires-Dist: numpy-quaternion (>=2022.4.4,<2023.0.0)
|
|
20
|
+
Requires-Dist: pint (>=0.22,<0.23)
|
|
21
|
+
Requires-Dist: psycopg (>=3.2.3,<4.0.0)
|
|
22
|
+
Requires-Dist: rasterio (>=1.4.1,<2.0.0)
|
|
23
|
+
Requires-Dist: rioxarray (>=0.12.4,<0.13.0)
|
|
24
|
+
Requires-Dist: xarray (>=2022.12.0,<2023.0.0)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
kuva_reader/__init__.py,sha256=ZfbIywT9FhYS3hqEMVeg0U2E8F5B-be6KTfyzO9Sz2Q,1478
|
|
2
|
+
kuva_reader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
kuva_reader/reader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
kuva_reader/reader/image.py,sha256=Ep54Tzila3hKF2I_u-54xnFUf8WqP2fiKAJnbyZxasA,4647
|
|
5
|
+
kuva_reader/reader/level0.py,sha256=8tNS2ok8EdOWEURnFbWGGYGtLecBc3J9xAWHTO_IpQA,9474
|
|
6
|
+
kuva_reader/reader/level1.py,sha256=bqEo6NlVSC2I6lrf9is6YgGsa_uNZMX21zXxtC6ewNc,5772
|
|
7
|
+
kuva_reader/reader/level2.py,sha256=k-vIvT84jvgyTBb_PmbaMMXY1wrl5v-MWqL7hV0tj1Q,3129
|
|
8
|
+
kuva_reader/reader/product_base.py,sha256=_O96DACi2bWEXhNJVuIU328RJ1y1gs7_rMyyEaMo4aA,4294
|
|
9
|
+
kuva_reader/reader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
kuva_reader/reader/utils.py,sha256=oZ1G43nm8lCxzfbdqBs0KhKaXWe_uf-78uBXWvmZigs,1566
|
|
11
|
+
kuva_reader-0.1.0.dist-info/METADATA,sha256=Ru1joPFSJVhZ8dUKPYQv9D6VxT7LV0HD-36RuSBXT0c,930
|
|
12
|
+
kuva_reader-0.1.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
13
|
+
kuva_reader-0.1.0.dist-info/entry_points.txt,sha256=YJysY9EChfOb1W_IEht2oE5Z8Y9jA0J6-kofaPv-NdI,149
|
|
14
|
+
kuva_reader-0.1.0.dist-info/RECORD,,
|