mapchete-eo 2025.7.0__py2.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.
- mapchete_eo/__init__.py +1 -0
- mapchete_eo/archives/__init__.py +0 -0
- mapchete_eo/archives/base.py +65 -0
- mapchete_eo/array/__init__.py +0 -0
- mapchete_eo/array/buffer.py +16 -0
- mapchete_eo/array/color.py +29 -0
- mapchete_eo/array/convert.py +157 -0
- mapchete_eo/base.py +528 -0
- mapchete_eo/blacklist.txt +175 -0
- mapchete_eo/cli/__init__.py +30 -0
- mapchete_eo/cli/bounds.py +22 -0
- mapchete_eo/cli/options_arguments.py +243 -0
- mapchete_eo/cli/s2_brdf.py +77 -0
- mapchete_eo/cli/s2_cat_results.py +146 -0
- mapchete_eo/cli/s2_find_broken_products.py +93 -0
- mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
- mapchete_eo/cli/s2_mask.py +71 -0
- mapchete_eo/cli/s2_mgrs.py +45 -0
- mapchete_eo/cli/s2_rgb.py +114 -0
- mapchete_eo/cli/s2_verify.py +129 -0
- mapchete_eo/cli/static_catalog.py +123 -0
- mapchete_eo/eostac.py +30 -0
- mapchete_eo/exceptions.py +87 -0
- mapchete_eo/geometry.py +271 -0
- mapchete_eo/image_operations/__init__.py +12 -0
- mapchete_eo/image_operations/color_correction.py +136 -0
- mapchete_eo/image_operations/compositing.py +247 -0
- mapchete_eo/image_operations/dtype_scale.py +43 -0
- mapchete_eo/image_operations/fillnodata.py +130 -0
- mapchete_eo/image_operations/filters.py +319 -0
- mapchete_eo/image_operations/linear_normalization.py +81 -0
- mapchete_eo/image_operations/sigmoidal.py +114 -0
- mapchete_eo/io/__init__.py +37 -0
- mapchete_eo/io/assets.py +492 -0
- mapchete_eo/io/items.py +147 -0
- mapchete_eo/io/levelled_cubes.py +228 -0
- mapchete_eo/io/path.py +144 -0
- mapchete_eo/io/products.py +413 -0
- mapchete_eo/io/profiles.py +45 -0
- mapchete_eo/known_catalogs.py +42 -0
- mapchete_eo/platforms/sentinel2/__init__.py +17 -0
- mapchete_eo/platforms/sentinel2/archives.py +190 -0
- mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
- mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
- mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
- mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
- mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
- mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
- mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
- mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
- mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
- mapchete_eo/platforms/sentinel2/config.py +181 -0
- mapchete_eo/platforms/sentinel2/driver.py +78 -0
- mapchete_eo/platforms/sentinel2/masks.py +325 -0
- mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
- mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
- mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
- mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
- mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
- mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
- mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
- mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
- mapchete_eo/platforms/sentinel2/product.py +669 -0
- mapchete_eo/platforms/sentinel2/types.py +109 -0
- mapchete_eo/processes/__init__.py +0 -0
- mapchete_eo/processes/config.py +51 -0
- mapchete_eo/processes/dtype_scale.py +112 -0
- mapchete_eo/processes/eo_to_xarray.py +19 -0
- mapchete_eo/processes/merge_rasters.py +235 -0
- mapchete_eo/product.py +278 -0
- mapchete_eo/protocols.py +56 -0
- mapchete_eo/search/__init__.py +14 -0
- mapchete_eo/search/base.py +222 -0
- mapchete_eo/search/config.py +42 -0
- mapchete_eo/search/s2_mgrs.py +314 -0
- mapchete_eo/search/stac_search.py +251 -0
- mapchete_eo/search/stac_static.py +236 -0
- mapchete_eo/search/utm_search.py +251 -0
- mapchete_eo/settings.py +24 -0
- mapchete_eo/sort.py +48 -0
- mapchete_eo/time.py +53 -0
- mapchete_eo/types.py +73 -0
- mapchete_eo-2025.7.0.dist-info/METADATA +38 -0
- mapchete_eo-2025.7.0.dist-info/RECORD +87 -0
- mapchete_eo-2025.7.0.dist-info/WHEEL +5 -0
- mapchete_eo-2025.7.0.dist-info/entry_points.txt +11 -0
- mapchete_eo-2025.7.0.dist-info/licenses/LICENSE +21 -0
mapchete_eo/__init__.py
ADDED
|
@@ -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
|
+
)
|