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.
Files changed (87) hide show
  1. mapchete_eo/__init__.py +1 -0
  2. mapchete_eo/archives/__init__.py +0 -0
  3. mapchete_eo/archives/base.py +65 -0
  4. mapchete_eo/array/__init__.py +0 -0
  5. mapchete_eo/array/buffer.py +16 -0
  6. mapchete_eo/array/color.py +29 -0
  7. mapchete_eo/array/convert.py +157 -0
  8. mapchete_eo/base.py +528 -0
  9. mapchete_eo/blacklist.txt +175 -0
  10. mapchete_eo/cli/__init__.py +30 -0
  11. mapchete_eo/cli/bounds.py +22 -0
  12. mapchete_eo/cli/options_arguments.py +243 -0
  13. mapchete_eo/cli/s2_brdf.py +77 -0
  14. mapchete_eo/cli/s2_cat_results.py +146 -0
  15. mapchete_eo/cli/s2_find_broken_products.py +93 -0
  16. mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
  17. mapchete_eo/cli/s2_mask.py +71 -0
  18. mapchete_eo/cli/s2_mgrs.py +45 -0
  19. mapchete_eo/cli/s2_rgb.py +114 -0
  20. mapchete_eo/cli/s2_verify.py +129 -0
  21. mapchete_eo/cli/static_catalog.py +123 -0
  22. mapchete_eo/eostac.py +30 -0
  23. mapchete_eo/exceptions.py +87 -0
  24. mapchete_eo/geometry.py +271 -0
  25. mapchete_eo/image_operations/__init__.py +12 -0
  26. mapchete_eo/image_operations/color_correction.py +136 -0
  27. mapchete_eo/image_operations/compositing.py +247 -0
  28. mapchete_eo/image_operations/dtype_scale.py +43 -0
  29. mapchete_eo/image_operations/fillnodata.py +130 -0
  30. mapchete_eo/image_operations/filters.py +319 -0
  31. mapchete_eo/image_operations/linear_normalization.py +81 -0
  32. mapchete_eo/image_operations/sigmoidal.py +114 -0
  33. mapchete_eo/io/__init__.py +37 -0
  34. mapchete_eo/io/assets.py +492 -0
  35. mapchete_eo/io/items.py +147 -0
  36. mapchete_eo/io/levelled_cubes.py +228 -0
  37. mapchete_eo/io/path.py +144 -0
  38. mapchete_eo/io/products.py +413 -0
  39. mapchete_eo/io/profiles.py +45 -0
  40. mapchete_eo/known_catalogs.py +42 -0
  41. mapchete_eo/platforms/sentinel2/__init__.py +17 -0
  42. mapchete_eo/platforms/sentinel2/archives.py +190 -0
  43. mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
  44. mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
  45. mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
  46. mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
  47. mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
  48. mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
  49. mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
  50. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
  51. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
  52. mapchete_eo/platforms/sentinel2/config.py +181 -0
  53. mapchete_eo/platforms/sentinel2/driver.py +78 -0
  54. mapchete_eo/platforms/sentinel2/masks.py +325 -0
  55. mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
  56. mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
  57. mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
  58. mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
  59. mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
  60. mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
  61. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
  62. mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
  63. mapchete_eo/platforms/sentinel2/product.py +669 -0
  64. mapchete_eo/platforms/sentinel2/types.py +109 -0
  65. mapchete_eo/processes/__init__.py +0 -0
  66. mapchete_eo/processes/config.py +51 -0
  67. mapchete_eo/processes/dtype_scale.py +112 -0
  68. mapchete_eo/processes/eo_to_xarray.py +19 -0
  69. mapchete_eo/processes/merge_rasters.py +235 -0
  70. mapchete_eo/product.py +278 -0
  71. mapchete_eo/protocols.py +56 -0
  72. mapchete_eo/search/__init__.py +14 -0
  73. mapchete_eo/search/base.py +222 -0
  74. mapchete_eo/search/config.py +42 -0
  75. mapchete_eo/search/s2_mgrs.py +314 -0
  76. mapchete_eo/search/stac_search.py +251 -0
  77. mapchete_eo/search/stac_static.py +236 -0
  78. mapchete_eo/search/utm_search.py +251 -0
  79. mapchete_eo/settings.py +24 -0
  80. mapchete_eo/sort.py +48 -0
  81. mapchete_eo/time.py +53 -0
  82. mapchete_eo/types.py +73 -0
  83. mapchete_eo-2025.7.0.dist-info/METADATA +38 -0
  84. mapchete_eo-2025.7.0.dist-info/RECORD +87 -0
  85. mapchete_eo-2025.7.0.dist-info/WHEEL +5 -0
  86. mapchete_eo-2025.7.0.dist-info/entry_points.txt +11 -0
  87. mapchete_eo-2025.7.0.dist-info/licenses/LICENSE +21 -0
@@ -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
+ )