mapchete-eo 2025.7.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.
- mapchete_eo-2025.7.0/.gitignore +13 -0
- mapchete_eo-2025.7.0/LICENSE +21 -0
- mapchete_eo-2025.7.0/PKG-INFO +38 -0
- mapchete_eo-2025.7.0/README.md +2 -0
- mapchete_eo-2025.7.0/mapchete_eo/__init__.py +1 -0
- mapchete_eo-2025.7.0/mapchete_eo/archives/__init__.py +0 -0
- mapchete_eo-2025.7.0/mapchete_eo/archives/base.py +65 -0
- mapchete_eo-2025.7.0/mapchete_eo/array/__init__.py +0 -0
- mapchete_eo-2025.7.0/mapchete_eo/array/buffer.py +16 -0
- mapchete_eo-2025.7.0/mapchete_eo/array/color.py +29 -0
- mapchete_eo-2025.7.0/mapchete_eo/array/convert.py +157 -0
- mapchete_eo-2025.7.0/mapchete_eo/base.py +528 -0
- mapchete_eo-2025.7.0/mapchete_eo/blacklist.txt +175 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/__init__.py +30 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/bounds.py +22 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/options_arguments.py +243 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/s2_brdf.py +77 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/s2_cat_results.py +146 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/s2_find_broken_products.py +93 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/s2_mask.py +71 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/s2_mgrs.py +45 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/s2_rgb.py +114 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/s2_verify.py +129 -0
- mapchete_eo-2025.7.0/mapchete_eo/cli/static_catalog.py +123 -0
- mapchete_eo-2025.7.0/mapchete_eo/eostac.py +30 -0
- mapchete_eo-2025.7.0/mapchete_eo/exceptions.py +87 -0
- mapchete_eo-2025.7.0/mapchete_eo/geometry.py +271 -0
- mapchete_eo-2025.7.0/mapchete_eo/image_operations/__init__.py +12 -0
- mapchete_eo-2025.7.0/mapchete_eo/image_operations/color_correction.py +136 -0
- mapchete_eo-2025.7.0/mapchete_eo/image_operations/compositing.py +247 -0
- mapchete_eo-2025.7.0/mapchete_eo/image_operations/dtype_scale.py +43 -0
- mapchete_eo-2025.7.0/mapchete_eo/image_operations/fillnodata.py +130 -0
- mapchete_eo-2025.7.0/mapchete_eo/image_operations/filters.py +319 -0
- mapchete_eo-2025.7.0/mapchete_eo/image_operations/linear_normalization.py +81 -0
- mapchete_eo-2025.7.0/mapchete_eo/image_operations/sigmoidal.py +114 -0
- mapchete_eo-2025.7.0/mapchete_eo/io/__init__.py +37 -0
- mapchete_eo-2025.7.0/mapchete_eo/io/assets.py +492 -0
- mapchete_eo-2025.7.0/mapchete_eo/io/items.py +147 -0
- mapchete_eo-2025.7.0/mapchete_eo/io/levelled_cubes.py +228 -0
- mapchete_eo-2025.7.0/mapchete_eo/io/path.py +144 -0
- mapchete_eo-2025.7.0/mapchete_eo/io/products.py +413 -0
- mapchete_eo-2025.7.0/mapchete_eo/io/profiles.py +45 -0
- mapchete_eo-2025.7.0/mapchete_eo/known_catalogs.py +42 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/__init__.py +17 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/archives.py +190 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/config.py +181 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/driver.py +78 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/masks.py +325 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/product.py +669 -0
- mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/types.py +109 -0
- mapchete_eo-2025.7.0/mapchete_eo/processes/__init__.py +0 -0
- mapchete_eo-2025.7.0/mapchete_eo/processes/config.py +51 -0
- mapchete_eo-2025.7.0/mapchete_eo/processes/dtype_scale.py +112 -0
- mapchete_eo-2025.7.0/mapchete_eo/processes/eo_to_xarray.py +19 -0
- mapchete_eo-2025.7.0/mapchete_eo/processes/merge_rasters.py +235 -0
- mapchete_eo-2025.7.0/mapchete_eo/product.py +278 -0
- mapchete_eo-2025.7.0/mapchete_eo/protocols.py +56 -0
- mapchete_eo-2025.7.0/mapchete_eo/search/__init__.py +14 -0
- mapchete_eo-2025.7.0/mapchete_eo/search/base.py +222 -0
- mapchete_eo-2025.7.0/mapchete_eo/search/config.py +42 -0
- mapchete_eo-2025.7.0/mapchete_eo/search/s2_mgrs.py +314 -0
- mapchete_eo-2025.7.0/mapchete_eo/search/stac_search.py +251 -0
- mapchete_eo-2025.7.0/mapchete_eo/search/stac_static.py +236 -0
- mapchete_eo-2025.7.0/mapchete_eo/search/utm_search.py +251 -0
- mapchete_eo-2025.7.0/mapchete_eo/settings.py +24 -0
- mapchete_eo-2025.7.0/mapchete_eo/sort.py +48 -0
- mapchete_eo-2025.7.0/mapchete_eo/time.py +53 -0
- mapchete_eo-2025.7.0/mapchete_eo/types.py +73 -0
- mapchete_eo-2025.7.0/pyproject.toml +74 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 - 2025 EOX IT Services
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mapchete-eo
|
|
3
|
+
Version: 2025.7.0
|
|
4
|
+
Summary: mapchete EO data reader
|
|
5
|
+
Project-URL: Homepage, https://gitlab.eox.at/maps/mapchete_eo
|
|
6
|
+
Author-email: Joachim Ungar <joachim.ungar@eox.at>, Petr Sevcik <petr.sevcik@eox.at>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
16
|
+
Requires-Dist: blend-modes
|
|
17
|
+
Requires-Dist: click
|
|
18
|
+
Requires-Dist: croniter
|
|
19
|
+
Requires-Dist: lxml
|
|
20
|
+
Requires-Dist: mapchete[complete]>=2025.6.0
|
|
21
|
+
Requires-Dist: opencv-python
|
|
22
|
+
Requires-Dist: pillow
|
|
23
|
+
Requires-Dist: pydantic
|
|
24
|
+
Requires-Dist: pystac-client>=0.7.5
|
|
25
|
+
Requires-Dist: pystac[urllib3]>=1.12.2
|
|
26
|
+
Requires-Dist: retry
|
|
27
|
+
Requires-Dist: rtree
|
|
28
|
+
Requires-Dist: scipy
|
|
29
|
+
Requires-Dist: tqdm
|
|
30
|
+
Requires-Dist: xarray
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest-coverage; extra == 'test'
|
|
33
|
+
Requires-Dist: pytest-lazy-fixture; extra == 'test'
|
|
34
|
+
Requires-Dist: pytest<8; extra == 'test'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# Mapchete EO driver
|
|
38
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2025.7.0"
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Callable, Dict, Generator, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from mapchete.io.vector import IndexedFeatures
|
|
6
|
+
from mapchete.types import Bounds
|
|
7
|
+
from pystac import Item
|
|
8
|
+
from shapely.errors import GEOSException
|
|
9
|
+
from shapely.geometry.base import BaseGeometry
|
|
10
|
+
|
|
11
|
+
from mapchete_eo.exceptions import ItemGeometryError
|
|
12
|
+
from mapchete_eo.search.base import CatalogSearcher
|
|
13
|
+
from mapchete_eo.types import TimeRange
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Archive(ABC):
|
|
19
|
+
"""
|
|
20
|
+
An archive combines a Catalog and a Storage.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
time: Union[TimeRange, List[TimeRange]]
|
|
24
|
+
area: BaseGeometry
|
|
25
|
+
catalog: CatalogSearcher
|
|
26
|
+
search_kwargs: Dict[str, Any]
|
|
27
|
+
_items: Optional[IndexedFeatures] = None
|
|
28
|
+
item_modifier_funcs: Optional[List[Callable[[Item], Item]]] = None
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
time: Union[TimeRange, List[TimeRange]],
|
|
33
|
+
bounds: Optional[Bounds] = None,
|
|
34
|
+
area: Optional[BaseGeometry] = None,
|
|
35
|
+
search_kwargs: Optional[Dict[str, Any]] = None,
|
|
36
|
+
catalog: Optional[CatalogSearcher] = None,
|
|
37
|
+
):
|
|
38
|
+
if bounds is None and area is None:
|
|
39
|
+
raise ValueError("either bounds or area have to be provided")
|
|
40
|
+
elif area is None:
|
|
41
|
+
area = Bounds.from_inp(bounds).geometry
|
|
42
|
+
self.time = time
|
|
43
|
+
self.area = area
|
|
44
|
+
self.search_kwargs = search_kwargs or {}
|
|
45
|
+
if catalog:
|
|
46
|
+
self.catalog = catalog
|
|
47
|
+
|
|
48
|
+
def get_catalog_config(self):
|
|
49
|
+
return self.catalog.config_cls(**self.search_kwargs)
|
|
50
|
+
|
|
51
|
+
def apply_item_modifier_funcs(self, item: Item) -> Item:
|
|
52
|
+
try:
|
|
53
|
+
for modifier in self.item_modifier_funcs or []:
|
|
54
|
+
item = modifier(item)
|
|
55
|
+
except GEOSException as exc:
|
|
56
|
+
raise ItemGeometryError(
|
|
57
|
+
f"item {item.get_self_href()} geometry could not be resolved: {str(exc)}"
|
|
58
|
+
)
|
|
59
|
+
return item
|
|
60
|
+
|
|
61
|
+
def items(self) -> Generator[Item, None, None]:
|
|
62
|
+
for item in self.catalog.search(
|
|
63
|
+
time=self.time, area=self.area, search_kwargs=self.search_kwargs
|
|
64
|
+
):
|
|
65
|
+
yield self.apply_item_modifier_funcs(item)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import DTypeLike
|
|
5
|
+
from scipy.ndimage import binary_dilation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def buffer_array(
|
|
9
|
+
array: np.ndarray, buffer: int = 0, out_array_dtype: Optional[DTypeLike] = None
|
|
10
|
+
) -> np.ndarray:
|
|
11
|
+
if out_array_dtype is None:
|
|
12
|
+
out_array_dtype = array.dtype
|
|
13
|
+
if buffer == 0:
|
|
14
|
+
return array.astype(out_array_dtype, copy=False)
|
|
15
|
+
|
|
16
|
+
return binary_dilation(array, iterations=buffer).astype(out_array_dtype, copy=False)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numpy.ma as ma
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def color_array(shape: tuple, hex_color: str):
|
|
6
|
+
colors = hex_to_rgb(hex_color)
|
|
7
|
+
return ma.masked_array(
|
|
8
|
+
[np.full(shape, color, dtype=np.uint8) for color in colors],
|
|
9
|
+
mask=ma.zeros((len(colors), *shape)),
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def hex_to_rgb(hex_color):
|
|
14
|
+
"""
|
|
15
|
+
Convert hex color to tuple of RGB(A) colors.
|
|
16
|
+
|
|
17
|
+
e.g. "#FFFFFF" --> (255, 255, 255) or "#00FF00FF" --> (0, 255, 0, 255)
|
|
18
|
+
"""
|
|
19
|
+
channels = iter(hex_color.lstrip("#"))
|
|
20
|
+
return tuple(int("".join(channel), 16) for channel in zip(channels, channels))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def outlier_pixels(
|
|
24
|
+
arr: np.ndarray,
|
|
25
|
+
axis: int = 0,
|
|
26
|
+
range_threshold: int = 100,
|
|
27
|
+
) -> np.ndarray:
|
|
28
|
+
"""Detect outlier pixels containing extreme colors."""
|
|
29
|
+
return arr.max(axis=axis) - arr.min(axis=axis) >= range_threshold
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from typing import List, Optional, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.ma as ma
|
|
5
|
+
import xarray as xr
|
|
6
|
+
from mapchete.types import NodataVal
|
|
7
|
+
|
|
8
|
+
# dtypes from https://numpy.org/doc/stable/user/basics.types.html
|
|
9
|
+
_NUMPY_FLOAT_DTYPES = [
|
|
10
|
+
np.half,
|
|
11
|
+
np.float16,
|
|
12
|
+
np.single,
|
|
13
|
+
np.double,
|
|
14
|
+
np.longdouble,
|
|
15
|
+
np.csingle,
|
|
16
|
+
np.cdouble,
|
|
17
|
+
np.clongdouble,
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def to_masked_array(
|
|
22
|
+
xarr: Union[xr.Dataset, xr.DataArray], copy: bool = False
|
|
23
|
+
) -> ma.MaskedArray:
|
|
24
|
+
"""Convert xr.DataArray to ma.MaskedArray."""
|
|
25
|
+
if isinstance(xarr, xr.Dataset):
|
|
26
|
+
xarr = xarr.to_array()
|
|
27
|
+
|
|
28
|
+
fill_value = xarr.attrs.get("_FillValue")
|
|
29
|
+
if fill_value is None:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"Cannot create masked_array because DataArray fill value is None"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if xarr.dtype in _NUMPY_FLOAT_DTYPES:
|
|
35
|
+
return ma.masked_values(xarr, fill_value, copy=copy, shrink=False)
|
|
36
|
+
else:
|
|
37
|
+
out = ma.masked_equal(xarr, fill_value, copy=copy)
|
|
38
|
+
# in case of a shrinked mask we have to expand it to the full array shape
|
|
39
|
+
if not isinstance(out.mask, np.ndarray):
|
|
40
|
+
out.mask = np.full(out.mask.shape, out.mask, dtype=bool)
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def to_dataarray(
|
|
45
|
+
masked_arr: ma.MaskedArray,
|
|
46
|
+
nodataval: NodataVal = None,
|
|
47
|
+
name: Optional[str] = None,
|
|
48
|
+
band_names: Optional[List[str]] = None,
|
|
49
|
+
band_axis_name: str = "bands",
|
|
50
|
+
x_axis_name: str = "x",
|
|
51
|
+
y_axis_name: str = "y",
|
|
52
|
+
attrs: Optional[dict] = None,
|
|
53
|
+
) -> xr.DataArray:
|
|
54
|
+
"""
|
|
55
|
+
Convert ma.MaskedArray to xr.DataArray.
|
|
56
|
+
|
|
57
|
+
Depending on whether the array is 2D or 3D, the axes will be named accordingly.
|
|
58
|
+
|
|
59
|
+
A 2-dimensional array indicates that we only have a spatial x- and y-axis. A
|
|
60
|
+
3rd dimension will be interpreted as bands.
|
|
61
|
+
"""
|
|
62
|
+
# nodata handling is weird.
|
|
63
|
+
#
|
|
64
|
+
# xr.DataArray cannot hold a masked_array but will turn it into
|
|
65
|
+
# a usual NumPy array, replacing the masked values with np.nan.
|
|
66
|
+
# However, this also seems to change the dtype to float32 which
|
|
67
|
+
# is not desirable.
|
|
68
|
+
nodataval = masked_arr.fill_value if nodataval is None else nodataval
|
|
69
|
+
attrs = attrs or dict()
|
|
70
|
+
|
|
71
|
+
if masked_arr.ndim == 2:
|
|
72
|
+
dims = [x_axis_name, y_axis_name]
|
|
73
|
+
coords = None
|
|
74
|
+
elif masked_arr.ndim == 3:
|
|
75
|
+
bands_count = masked_arr.shape[0]
|
|
76
|
+
band_names = band_names or [f"{band_axis_name}-{i}" for i in range(bands_count)]
|
|
77
|
+
dims = [band_axis_name, x_axis_name, y_axis_name]
|
|
78
|
+
coords = {band_axis_name: band_names}
|
|
79
|
+
else: # pragma: no cover
|
|
80
|
+
raise TypeError("only a 2D or 3D ma.MaskedArray is allowed.")
|
|
81
|
+
|
|
82
|
+
return xr.DataArray(
|
|
83
|
+
data=masked_arr.filled(nodataval),
|
|
84
|
+
dims=dims,
|
|
85
|
+
name=name,
|
|
86
|
+
attrs=dict(attrs, _FillValue=nodataval),
|
|
87
|
+
coords=coords,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def to_dataset(
|
|
92
|
+
masked_arr: ma.MaskedArray,
|
|
93
|
+
nodataval: NodataVal = None,
|
|
94
|
+
slice_names: Optional[List[str]] = None,
|
|
95
|
+
band_names: Optional[List[str]] = None,
|
|
96
|
+
slices_attrs: Optional[List[Union[dict, None]]] = None,
|
|
97
|
+
slice_axis_name: str = "time",
|
|
98
|
+
band_axis_name: str = "bands",
|
|
99
|
+
x_axis_name: str = "x",
|
|
100
|
+
y_axis_name: str = "y",
|
|
101
|
+
attrs: Optional[dict] = None,
|
|
102
|
+
):
|
|
103
|
+
"""Convert a 3D or 4D ma.MaskedArray to an xarray.Dataset."""
|
|
104
|
+
attrs = attrs or dict()
|
|
105
|
+
nodataval = masked_arr.fill_value if nodataval is None else nodataval
|
|
106
|
+
|
|
107
|
+
if masked_arr.ndim == 3:
|
|
108
|
+
bands = masked_arr.shape[0]
|
|
109
|
+
band_names = band_names or [f"{band_axis_name}-{i}" for i in range(bands)]
|
|
110
|
+
raise NotImplementedError()
|
|
111
|
+
elif masked_arr.ndim == 4:
|
|
112
|
+
slices, bands = masked_arr.shape[:2]
|
|
113
|
+
band_names = band_names or [f"{band_axis_name}-{i}" for i in range(bands)]
|
|
114
|
+
slice_names = slice_names or [f"{slice_axis_name}-{i}" for i in range(slices)]
|
|
115
|
+
slices_attrs = (
|
|
116
|
+
[None for _ in range(slices)] if slices_attrs is None else slices_attrs
|
|
117
|
+
)
|
|
118
|
+
coords = {slice_axis_name: slice_names}
|
|
119
|
+
return xr.Dataset(
|
|
120
|
+
data_vars={
|
|
121
|
+
# every slice gets its own xarray Dataset
|
|
122
|
+
slice_name: to_dataarray(
|
|
123
|
+
slice_array,
|
|
124
|
+
nodataval=nodataval,
|
|
125
|
+
band_names=band_names,
|
|
126
|
+
name=slice_name,
|
|
127
|
+
attrs=slice_attrs,
|
|
128
|
+
band_axis_name=band_axis_name,
|
|
129
|
+
x_axis_name=x_axis_name,
|
|
130
|
+
y_axis_name=y_axis_name,
|
|
131
|
+
)
|
|
132
|
+
for slice_name, slice_attrs, slice_array in zip(
|
|
133
|
+
slice_names,
|
|
134
|
+
slices_attrs,
|
|
135
|
+
masked_arr,
|
|
136
|
+
)
|
|
137
|
+
},
|
|
138
|
+
coords=coords,
|
|
139
|
+
attrs=dict(attrs, _FillValue=nodataval),
|
|
140
|
+
).transpose(slice_axis_name, band_axis_name, x_axis_name, y_axis_name)
|
|
141
|
+
|
|
142
|
+
else: # pragma: no cover
|
|
143
|
+
raise TypeError("only a 3D or 4D ma.MaskedArray is allowed.")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def to_bands_mask(arr: np.ndarray, bands: int = 1) -> np.ndarray:
|
|
147
|
+
"""Expands a 2D mask to a full band mask."""
|
|
148
|
+
if arr.ndim != 2:
|
|
149
|
+
raise TypeError("input array has to have exactly 2 dimensions.")
|
|
150
|
+
return np.repeat(
|
|
151
|
+
np.expand_dims(
|
|
152
|
+
arr,
|
|
153
|
+
axis=0,
|
|
154
|
+
),
|
|
155
|
+
bands,
|
|
156
|
+
axis=0,
|
|
157
|
+
)
|