kuva-reader 1.0.3__py3-none-any.whl → 1.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 CHANGED
@@ -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
- - 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
25
  __version__ = "0.1.0"
26
26
 
27
27
  from .reader.image import (
28
- image_to_dtype_range,
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
- "image_to_dtype_range",
43
- "image_to_original_range",
44
- "image_to_uint16_range",
40
+ "image_footprint",
45
41
  "read_product",
46
42
  ]
@@ -1,176 +1,28 @@
1
1
  """Utilities to process images related to product processing."""
2
2
 
3
- from typing import cast, overload
3
+ import rasterio as rio
4
+ from shapely.geometry import box, Polygon
5
+ from rasterio.warp import transform_bounds
4
6
 
5
- import numpy as np
6
- import xarray
7
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]
8
+ def image_footprint(image: rio.DatasetReader, crs: str = "") -> Polygon:
9
+ """Return a product footprint as a shapely polygon
39
10
 
40
11
  Parameters
41
12
  ----------
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`
13
+ image
14
+ The product image
15
+ crs, optional
16
+ CRS to convert to, by default "", keeping the image's CRS
50
17
 
51
18
  Returns
52
19
  -------
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
20
+ A shapely polygon footprint
60
21
  """
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_)
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)
73
26
  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
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 rioxarray as rx
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 image_to_dtype_range, image_to_original_range
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, 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()`
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
- xarray.DataArray,
77
- rx.open_rasterio(
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: 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
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 {len(self.images)} frames of shape {self.images[0].shape} "
110
- f"and CRS '{self.images[0].rio.crs}'. "
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) -> xarray.DataArray:
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
- return self[cube][frame_offset, :, :].to_numpy()
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
- return self[cube][band_offset_ll:band_offset_ul, :, :].to_numpy()
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) -> xarray.Dataset:
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) -> xarray.Dataset:
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 releases the memory of the `images` variable.
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 rioxarray as rx
5
- import xarray
4
+ import rasterio as rio
6
5
  from kuva_metadata import MetadataLevel1AB, MetadataLevel1C
7
6
  from pint import UnitRegistry
8
- from xarray import Dataset
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: xarray.DataArray
38
- The arrays with the actual data. This have the rioxarray extension activated on
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
- Dataset,
56
- rx.open_rasterio(self.image_path / "L1B.tif"),
54
+ rio.DatasetReader,
55
+ rio.open(self.image_path / "L1B.tif"),
57
56
  )
58
- self.data_tags = self.image.attrs
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
- ) -> xarray.Dataset:
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 releases the memory of the `image` variable.
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: xarray.DataArray
142
- The arrays with the actual data. This have the rioxarray extension activated on
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
- Dataset,
160
- rx.open_rasterio(self.image_path / "L1C.tif"),
174
+ rio.DatasetReader,
175
+ rio.open(self.image_path / "L1C.tif"),
161
176
  )
162
- self.data_tags = self.image.attrs
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 {self.image.shape} "
172
- f"and wavelengths {self.wavelengths} (CRS: '{self.image.rio.crs}'). "
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 releases the memory of the `image` variable.
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 rioxarray as rx
4
+ import rasterio as rio
5
5
  from kuva_metadata import MetadataLevel2A
6
6
  from pint import UnitRegistry
7
- from xarray import Dataset
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: 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()`
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
- Dataset,
52
- rx.open_rasterio(self.image_path / "L2A.tif"),
51
+ rio.DatasetReader,
52
+ rio.open(self.image_path / "L2A.tif"),
53
53
  )
54
- self.data_tags = self.image.attrs
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 {self.image.shape} "
64
- f"and wavelengths {self.wavelengths} (CRS: '{self.image.rio.crs}'). "
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 releases the memory of the `image` variable.
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 rioxarray as rx
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) -> Dataset:
68
+ def _read_array(array_path: Path) -> rio.DatasetReader:
70
69
  if array_path.exists():
71
70
  return cast(
72
- Dataset,
73
- rx.open_rasterio(array_path),
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) -> Dataset:
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) -> Dataset:
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
@@ -25,7 +25,7 @@ def read_product(
25
25
 
26
26
  product_map = {
27
27
  "L0": Level0Product,
28
- "L1AB": Level1ABProduct,
28
+ "L1B": Level1ABProduct,
29
29
  "L1C": Level1CProduct,
30
30
  "L2A": Level2AProduct,
31
31
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kuva-reader
3
- Version: 1.0.3
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 `xarray.Dataset` objects that
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 `rioxarray` object, which contains extensive GIS functionalities [(examples for usage)](https://corteva.github.io/rioxarray/stable/examples/examples.html).
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
- l2a_product = read_product("my_data_folder/hyperfield1a_L2A_20250105T092548")
65
- print(l2a_product) # Will show some main information such as image shape and CRS
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
 
@@ -0,0 +1,15 @@
1
+ kuva_reader/__init__.py,sha256=y0ViXqCoRSWvGPXEUFP9TOohcJIS24-WIarkZrDgcKs,1389
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=rqzs5C7nzRArFfVepydRUECd3u55McTZlKINg76nMX4,785
5
+ kuva_reader/reader/level0.py,sha256=u61sKRXmx4uljlnqPfuLdtdEGfkw5NdtiR9N6XfUsrA,9352
6
+ kuva_reader/reader/level1.py,sha256=HRvhSasDEzB03qJi6wrQiv-r9kSdWITuWIf-2IbYqX4,8355
7
+ kuva_reader/reader/level2.py,sha256=kGd-6qTZXiNWuYwwLf_-UWS0Oeng8iTpeQFXtAfyPXA,4106
8
+ kuva_reader/reader/product_base.py,sha256=4NpZQk3FvSPsN2kKde8lHJA_0Wwy6DQBMfOh4ItlO00,4299
9
+ kuva_reader/reader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ kuva_reader/reader/read.py,sha256=b5hGFbeJ-7sg-iilSJn29Hcqj4e7kiWT2S0BH2JwcDE,1761
11
+ kuva_reader/reader/utils.py,sha256=oZ1G43nm8lCxzfbdqBs0KhKaXWe_uf-78uBXWvmZigs,1566
12
+ kuva_reader-1.1.0.dist-info/METADATA,sha256=5nnnCGRiKQbC_Q0U6RZgx6hbX2MxZ5dXcOnjv6R8etY,5007
13
+ kuva_reader-1.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
14
+ kuva_reader-1.1.0.dist-info/entry_points.txt,sha256=YJysY9EChfOb1W_IEht2oE5Z8Y9jA0J6-kofaPv-NdI,149
15
+ kuva_reader-1.1.0.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- kuva_reader/__init__.py,sha256=qqsgtVNCkqXFuXOXf5v51ZT_Dcipj1ty0mGUw8fU3sU,1509
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=-ONYaBH9YFHGaFvJDiDRlmXUWTYPQyQt0XZeuv5sEL4,10370
6
- kuva_reader/reader/level1.py,sha256=V1gNjz9y185nCJkYpQVcXrmAOQVMaFwIPeIhQJBzaI4,7815
7
- kuva_reader/reader/level2.py,sha256=lyEbVSLkBxz32ZIEnJih4ln-F_d8h6FTOu1Npt7VHg0,4091
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/read.py,sha256=l2x-vlOLP8AUMzqSjHLaT7xt0YJtYUjiYWm6btV-rqo,1762
11
- kuva_reader/reader/utils.py,sha256=oZ1G43nm8lCxzfbdqBs0KhKaXWe_uf-78uBXWvmZigs,1566
12
- kuva_reader-1.0.3.dist-info/METADATA,sha256=XDxxOJM_4pUzFthDkE8vLYlLTbXtSCRzkOvCCTxYECs,4612
13
- kuva_reader-1.0.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
14
- kuva_reader-1.0.3.dist-info/entry_points.txt,sha256=YJysY9EChfOb1W_IEht2oE5Z8Y9jA0J6-kofaPv-NdI,149
15
- kuva_reader-1.0.3.dist-info/RECORD,,