mapchete-eo 2026.2.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.
- mapchete_eo/__init__.py +1 -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 +163 -0
- mapchete_eo/base.py +653 -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 +227 -0
- mapchete_eo/cli/s2_brdf.py +77 -0
- mapchete_eo/cli/s2_cat_results.py +130 -0
- mapchete_eo/cli/s2_find_broken_products.py +77 -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 +82 -0
- mapchete_eo/eostac.py +30 -0
- mapchete_eo/exceptions.py +87 -0
- mapchete_eo/image_operations/__init__.py +12 -0
- mapchete_eo/image_operations/blend_functions.py +579 -0
- mapchete_eo/image_operations/color_correction.py +136 -0
- mapchete_eo/image_operations/compositing.py +266 -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 +496 -0
- mapchete_eo/io/items.py +162 -0
- mapchete_eo/io/levelled_cubes.py +259 -0
- mapchete_eo/io/path.py +155 -0
- mapchete_eo/io/products.py +423 -0
- mapchete_eo/io/profiles.py +45 -0
- mapchete_eo/platforms/sentinel2/__init__.py +17 -0
- mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -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 +241 -0
- mapchete_eo/platforms/sentinel2/driver.py +43 -0
- mapchete_eo/platforms/sentinel2/masks.py +329 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/base.py +56 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/default_path_mapper.py +135 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/s2metadata.py +639 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py +57 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py +108 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py +171 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py +217 -0
- mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +50 -0
- mapchete_eo/platforms/sentinel2/processing_baseline.py +163 -0
- mapchete_eo/platforms/sentinel2/product.py +747 -0
- mapchete_eo/platforms/sentinel2/source.py +114 -0
- mapchete_eo/platforms/sentinel2/types.py +114 -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 +239 -0
- mapchete_eo/product.py +323 -0
- mapchete_eo/protocols.py +61 -0
- mapchete_eo/search/__init__.py +14 -0
- mapchete_eo/search/base.py +285 -0
- mapchete_eo/search/config.py +113 -0
- mapchete_eo/search/s2_mgrs.py +313 -0
- mapchete_eo/search/stac_search.py +278 -0
- mapchete_eo/search/stac_static.py +197 -0
- mapchete_eo/search/utm_search.py +251 -0
- mapchete_eo/settings.py +25 -0
- mapchete_eo/sort.py +60 -0
- mapchete_eo/source.py +109 -0
- mapchete_eo/time.py +62 -0
- mapchete_eo/types.py +76 -0
- mapchete_eo-2026.2.0.dist-info/METADATA +91 -0
- mapchete_eo-2026.2.0.dist-info/RECORD +89 -0
- mapchete_eo-2026.2.0.dist-info/WHEEL +4 -0
- mapchete_eo-2026.2.0.dist-info/entry_points.txt +11 -0
- mapchete_eo-2026.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pystac
|
|
8
|
+
from mapchete.cli.options import opt_debug
|
|
9
|
+
from mapchete.io import copy
|
|
10
|
+
from mapchete.io.raster import read_raster_no_crs
|
|
11
|
+
from mapchete.path import MPath
|
|
12
|
+
from tqdm import tqdm
|
|
13
|
+
|
|
14
|
+
from mapchete_eo.array.color import outlier_pixels
|
|
15
|
+
from mapchete_eo.cli import options_arguments
|
|
16
|
+
from mapchete_eo.exceptions import AssetKeyError
|
|
17
|
+
from mapchete_eo.platforms.sentinel2.product import asset_mpath
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Report:
|
|
24
|
+
item: pystac.Item
|
|
25
|
+
missing_asset_entries: List[str]
|
|
26
|
+
missing_assets: List[MPath]
|
|
27
|
+
color_artefacts: bool = False
|
|
28
|
+
|
|
29
|
+
def product_broken(self) -> bool:
|
|
30
|
+
return any(
|
|
31
|
+
[
|
|
32
|
+
bool(self.missing_asset_entries),
|
|
33
|
+
bool(self.missing_assets),
|
|
34
|
+
bool(self.color_artefacts),
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@click.command()
|
|
40
|
+
@options_arguments.arg_stac_items
|
|
41
|
+
@options_arguments.opt_assets
|
|
42
|
+
@opt_debug
|
|
43
|
+
def s2_verify(
|
|
44
|
+
stac_items: List[MPath],
|
|
45
|
+
assets: List[str] = [],
|
|
46
|
+
asset_exists_check: bool = True,
|
|
47
|
+
**_,
|
|
48
|
+
):
|
|
49
|
+
"""Verify Sentinel-2 products."""
|
|
50
|
+
assets = assets or []
|
|
51
|
+
for item_path in tqdm(stac_items):
|
|
52
|
+
report = verify_item(
|
|
53
|
+
pystac.Item.from_file(item_path),
|
|
54
|
+
assets=assets,
|
|
55
|
+
asset_exists_check=asset_exists_check,
|
|
56
|
+
)
|
|
57
|
+
for asset in report.missing_asset_entries:
|
|
58
|
+
tqdm.write(f"[ERROR] {report.item.id} has no asset named '{asset}")
|
|
59
|
+
for path in report.missing_assets:
|
|
60
|
+
tqdm.write(
|
|
61
|
+
f"[ERROR] {report.item.id} asset '{asset}' with path {str(path)} does not exist"
|
|
62
|
+
)
|
|
63
|
+
if report.color_artefacts:
|
|
64
|
+
tqdm.write(
|
|
65
|
+
f"[ERROR] {report.item.id} thumbnail ({report.item.assets['thumbnail'].href}) indicates that there are some color artefacts"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def verify_item(
|
|
70
|
+
item: pystac.Item,
|
|
71
|
+
assets: List[str],
|
|
72
|
+
asset_exists_check: bool = False,
|
|
73
|
+
check_thumbnail: bool = True,
|
|
74
|
+
thumbnail_dir: Optional[MPath] = None,
|
|
75
|
+
):
|
|
76
|
+
missing_asset_entries = []
|
|
77
|
+
missing_assets = []
|
|
78
|
+
color_artefacts = False
|
|
79
|
+
for asset in assets:
|
|
80
|
+
logger.debug("verify asset %s is available", asset)
|
|
81
|
+
if asset not in item.assets:
|
|
82
|
+
missing_asset_entries.append(asset)
|
|
83
|
+
if asset_exists_check:
|
|
84
|
+
try:
|
|
85
|
+
path = asset_mpath(item=item, asset=asset)
|
|
86
|
+
logger.debug("check if asset %s (%s) exists", asset, str(path))
|
|
87
|
+
if not path.exists():
|
|
88
|
+
missing_assets.append(path)
|
|
89
|
+
except AssetKeyError:
|
|
90
|
+
missing_asset_entries.append(asset)
|
|
91
|
+
if check_thumbnail:
|
|
92
|
+
thumbnail_href = MPath.from_inp(item.assets["thumbnail"].href)
|
|
93
|
+
logger.debug("check thumbnail %s for artefacts ...", thumbnail_href)
|
|
94
|
+
if thumbnail_dir:
|
|
95
|
+
thumbnail_path = thumbnail_dir / item.id + ".jpg"
|
|
96
|
+
copy(thumbnail_href, thumbnail_path)
|
|
97
|
+
else:
|
|
98
|
+
thumbnail_path = thumbnail_href
|
|
99
|
+
color_artefacts = outlier_pixels_detected(read_raster_no_crs(thumbnail_href))
|
|
100
|
+
return Report(
|
|
101
|
+
item,
|
|
102
|
+
missing_asset_entries=missing_asset_entries,
|
|
103
|
+
missing_assets=missing_assets,
|
|
104
|
+
color_artefacts=color_artefacts,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def outlier_pixels_detected(
|
|
109
|
+
arr: np.ndarray,
|
|
110
|
+
axis: int = 0,
|
|
111
|
+
range_threshold: int = 100,
|
|
112
|
+
allowed_error_percentage: float = 1,
|
|
113
|
+
) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
Checks whether number of outlier pixels is larger than allowed.
|
|
116
|
+
|
|
117
|
+
An outlier pixel is a pixel, where the value range between bands exceeds
|
|
118
|
+
the range_threshold.
|
|
119
|
+
"""
|
|
120
|
+
_, width, height = arr.shape
|
|
121
|
+
pixels = width * height
|
|
122
|
+
outliers = outlier_pixels(arr, axis=axis, range_threshold=range_threshold).sum()
|
|
123
|
+
outlier_percent = outliers / pixels * 100
|
|
124
|
+
logger.debug(
|
|
125
|
+
"%s (%s %%) suspicious pixels detected",
|
|
126
|
+
outliers,
|
|
127
|
+
round(outlier_percent, 2),
|
|
128
|
+
)
|
|
129
|
+
return outlier_percent > allowed_error_percentage
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from mapchete.cli.options import opt_bounds, opt_debug
|
|
6
|
+
from mapchete.path import MPath
|
|
7
|
+
from mapchete.types import Bounds
|
|
8
|
+
from rasterio.profiles import Profile
|
|
9
|
+
|
|
10
|
+
from mapchete_eo.cli import options_arguments
|
|
11
|
+
from mapchete_eo.platforms.sentinel2 import S2Metadata
|
|
12
|
+
from mapchete_eo.platforms.sentinel2.source import Sentinel2Source
|
|
13
|
+
from mapchete_eo.platforms.sentinel2.types import Resolution
|
|
14
|
+
from mapchete_eo.types import TimeRange
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command()
|
|
18
|
+
@options_arguments.arg_dst_path
|
|
19
|
+
@opt_bounds
|
|
20
|
+
@options_arguments.opt_mgrs_tile
|
|
21
|
+
@options_arguments.opt_start_time
|
|
22
|
+
@options_arguments.opt_end_time
|
|
23
|
+
@options_arguments.opt_source
|
|
24
|
+
@options_arguments.opt_name
|
|
25
|
+
@options_arguments.opt_description
|
|
26
|
+
@options_arguments.opt_assets
|
|
27
|
+
@options_arguments.opt_assets_dst_resolution
|
|
28
|
+
@options_arguments.opt_assets_dst_rio_profile
|
|
29
|
+
@options_arguments.opt_copy_metadata
|
|
30
|
+
@options_arguments.opt_overwrite
|
|
31
|
+
@opt_debug
|
|
32
|
+
def static_catalog(
|
|
33
|
+
dst_path: MPath,
|
|
34
|
+
start_time: datetime,
|
|
35
|
+
end_time: datetime,
|
|
36
|
+
bounds: Optional[Bounds] = None,
|
|
37
|
+
mgrs_tile: Optional[str] = None,
|
|
38
|
+
source: Sentinel2Source = Sentinel2Source(collection="EarthSearch"),
|
|
39
|
+
name: Optional[str] = None,
|
|
40
|
+
description: Optional[str] = None,
|
|
41
|
+
assets: Optional[List[str]] = None,
|
|
42
|
+
assets_dst_resolution: Resolution = Resolution.original,
|
|
43
|
+
assets_dst_rio_profile: Optional[Profile] = None,
|
|
44
|
+
copy_metadata: bool = False,
|
|
45
|
+
overwrite: bool = False,
|
|
46
|
+
**__,
|
|
47
|
+
):
|
|
48
|
+
"""Write a static STAC catalog for selected area."""
|
|
49
|
+
if any([start_time is None, end_time is None]): # pragma: no cover
|
|
50
|
+
raise click.ClickException("--start-time and --end-time are mandatory")
|
|
51
|
+
if all([bounds is None, mgrs_tile is None]): # pragma: no cover
|
|
52
|
+
raise click.ClickException("--bounds or --mgrs-tile are required")
|
|
53
|
+
catalog = source.get_catalog()
|
|
54
|
+
if hasattr(catalog, "write_static_catalog"):
|
|
55
|
+
with options_arguments.TqdmUpTo(
|
|
56
|
+
unit="products", unit_scale=True, miniters=1, disable=opt_debug
|
|
57
|
+
) as progress:
|
|
58
|
+
catalog_json = catalog.write_static_catalog(
|
|
59
|
+
dst_path,
|
|
60
|
+
name=name,
|
|
61
|
+
bounds=bounds,
|
|
62
|
+
time=TimeRange(
|
|
63
|
+
start=start_time,
|
|
64
|
+
end=end_time,
|
|
65
|
+
),
|
|
66
|
+
search_kwargs=dict(mgrs_tile=mgrs_tile),
|
|
67
|
+
description=description,
|
|
68
|
+
assets=assets,
|
|
69
|
+
assets_dst_resolution=assets_dst_resolution.value,
|
|
70
|
+
assets_convert_profile=assets_dst_rio_profile,
|
|
71
|
+
copy_metadata=copy_metadata,
|
|
72
|
+
metadata_parser_classes=(S2Metadata,),
|
|
73
|
+
overwrite=overwrite,
|
|
74
|
+
progress_callback=progress.update_to,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
click.echo(f"Catalog successfully written to {catalog_json}")
|
|
78
|
+
|
|
79
|
+
else:
|
|
80
|
+
raise AttributeError(
|
|
81
|
+
f"catalog {catalog} does not support writing a static version"
|
|
82
|
+
)
|
mapchete_eo/eostac.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Driver class for EOSTAC static STAC catalogs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from mapchete_eo import base
|
|
6
|
+
|
|
7
|
+
METADATA: dict = {
|
|
8
|
+
"driver_name": "EOSTAC",
|
|
9
|
+
"data_type": None,
|
|
10
|
+
"mode": "r",
|
|
11
|
+
"file_extensions": [],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InputTile(base.EODataCube):
|
|
16
|
+
"""
|
|
17
|
+
Target Tile representation of input data.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
tile : ``Tile``
|
|
22
|
+
kwargs : keyword arguments
|
|
23
|
+
driver specific parameters
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InputData(base.InputData):
|
|
28
|
+
"""In case this driver is used when being a readonly input to another process."""
|
|
29
|
+
|
|
30
|
+
input_tile_cls = InputTile
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Custom exceptions."""
|
|
2
|
+
|
|
3
|
+
from mapchete.errors import MapcheteNodataTile
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EmptyFootprintException(Exception):
|
|
7
|
+
"""Raised when footprint is empty."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EmptySliceException(Exception):
|
|
11
|
+
"""Raised when slice is empty."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EmptyProductException(EmptySliceException):
|
|
15
|
+
"""Raised when product is empty."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EmptyStackException(MapcheteNodataTile):
|
|
19
|
+
"""Raised when whole stack is empty."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EmptyFileException(Exception):
|
|
23
|
+
"""Raised when no bytes are downloaded."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IncompleteDownloadException(Exception):
|
|
27
|
+
""" "Raised when the file is not downloaded completely."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InvalidMapcheteEOCollectionError(Exception):
|
|
31
|
+
""" "Raised for unsupported collections of Mapchete EO package."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EmptyCatalogueResponse(Exception):
|
|
35
|
+
"""Raised when catalogue response is empty."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CorruptedGTiffError(Exception):
|
|
39
|
+
"""Raised when GTiff validation fails."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BRDFError(Exception):
|
|
43
|
+
"""Raised when BRDF grid cannot be calculated."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AssetError(Exception):
|
|
47
|
+
"""Generic Exception class for Assets."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AssetMissing(AssetError, FileNotFoundError):
|
|
51
|
+
"""Raised when a product asset should be there but isn't."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AssetEmpty(AssetError):
|
|
55
|
+
"""Raised when a product asset should contain data but is empty."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AssetKeyError(AssetError, KeyError):
|
|
59
|
+
"""Raised when an asset name cannot be found in item."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PreprocessingNotFinished(Exception):
|
|
63
|
+
"""Raised when preprocessing tasks have not been fully executed."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AllMasked(Exception):
|
|
67
|
+
"""Raised when an array is fully masked."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class NoSourceProducts(MapcheteNodataTile, ValueError):
|
|
71
|
+
"""Raised when no products are available."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CorruptedProduct(Exception):
|
|
75
|
+
"""Raised when product is damaged and cannot be read."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CorruptedProductMetadata(CorruptedProduct):
|
|
79
|
+
"""Raised when EOProduct cannot be parsed due to a metadata issue."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CorruptedSlice(Exception):
|
|
83
|
+
"""Raised when all products in a slice are damaged and cannot be read."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ItemGeometryError(Exception):
|
|
87
|
+
"""Raised when STAC item geometry cannot be resolved."""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from mapchete_eo.image_operations.color_correction import color_correct
|
|
2
|
+
from mapchete_eo.image_operations.dtype_scale import dtype_scale
|
|
3
|
+
from mapchete_eo.image_operations.fillnodata import FillSelectionMethod, fillnodata
|
|
4
|
+
from mapchete_eo.image_operations.linear_normalization import linear_normalization
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"color_correct",
|
|
8
|
+
"dtype_scale",
|
|
9
|
+
"fillnodata",
|
|
10
|
+
"FillSelectionMethod",
|
|
11
|
+
"linear_normalization",
|
|
12
|
+
]
|