s2mosaic 0.1.3__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.
- s2mosaic-0.1.3/LICENSE +21 -0
- s2mosaic-0.1.3/PKG-INFO +138 -0
- s2mosaic-0.1.3/README.md +115 -0
- s2mosaic-0.1.3/s2mosaic/S2_grid/sentinel_2_index.gpkg +0 -0
- s2mosaic-0.1.3/s2mosaic/__init__.py +7 -0
- s2mosaic-0.1.3/s2mosaic/__version__.py +1 -0
- s2mosaic-0.1.3/s2mosaic/download.py +517 -0
- s2mosaic-0.1.3/s2mosaic.egg-info/PKG-INFO +138 -0
- s2mosaic-0.1.3/s2mosaic.egg-info/SOURCES.txt +12 -0
- s2mosaic-0.1.3/s2mosaic.egg-info/dependency_links.txt +1 -0
- s2mosaic-0.1.3/s2mosaic.egg-info/requires.txt +4 -0
- s2mosaic-0.1.3/s2mosaic.egg-info/top_level.txt +1 -0
- s2mosaic-0.1.3/setup.cfg +4 -0
- s2mosaic-0.1.3/setup.py +36 -0
s2mosaic-0.1.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 DPIRD-DMA
|
|
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.
|
s2mosaic-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: s2mosaic
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Python library for making cloud-free Sentinel-2 mosaics
|
|
5
|
+
Home-page: https://github.com/DPIRD-DMA/S2Mosaic
|
|
6
|
+
Author: Nick Wright
|
|
7
|
+
Author-email: nicholas.wright@dpird.wa.gov.au
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Requires-Python: >=3.7
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: planetary_computer
|
|
20
|
+
Requires-Dist: pystac_client
|
|
21
|
+
Requires-Dist: geopandas
|
|
22
|
+
Requires-Dist: omnicloudmask
|
|
23
|
+
|
|
24
|
+
## S2Mosaic 🛰️🌍
|
|
25
|
+
|
|
26
|
+
S2Mosaic is a Python package for creating cloud-free mosaics from Sentinel-2 satellite imagery. It allows users to generate composite images for specified grid areas and time ranges, with various options for scene selection and mosaic creation.
|
|
27
|
+
|
|
28
|
+
## Features 🌟
|
|
29
|
+
|
|
30
|
+
- Create Sentinel-2 mosaics for specific grid areas and time ranges.
|
|
31
|
+
- Flexible scene selection methods: by valid data percentage, oldest, or newest scenes.
|
|
32
|
+
- Multiple mosaic creation methods: mean or first valid pixel.
|
|
33
|
+
- Support for different spectral bands, including visual (RGB) composites.
|
|
34
|
+
- State-of-the-art cloud masking using the OmniCloudMask library.
|
|
35
|
+
- Export mosaics as GeoTIFF files or return as NumPy arrays.
|
|
36
|
+
|
|
37
|
+
## Note 📝
|
|
38
|
+
|
|
39
|
+
We use OmniCloudMask for state-of-the-art cloud and cloud shadow masking. OCM will run significantly faster if an available NVIDIA GPU is present.
|
|
40
|
+
|
|
41
|
+
## Try in Colab
|
|
42
|
+
|
|
43
|
+
[![Colab_Button]][Link]
|
|
44
|
+
|
|
45
|
+
[Link]: https://colab.research.google.com/drive/1-vdAAnpzp_VCotTV07cbSC9iQFiD7DcH?usp=sharing 'Try S2Mosaic In Colab'
|
|
46
|
+
|
|
47
|
+
[Colab_Button]: https://img.shields.io/badge/Try%20in%20Colab-grey?style=for-the-badge&logo=google-colab
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Installation 🛠️
|
|
52
|
+
|
|
53
|
+
You can install S2Mosaic using pip:
|
|
54
|
+
```
|
|
55
|
+
pip install s2mosaic
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage Example 1 🚀
|
|
59
|
+
|
|
60
|
+
Here's a basic example of how to use S2Mosaic:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from s2mosaic import mosaic
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
|
|
66
|
+
# Create a mosaic for a specific grid area and time range
|
|
67
|
+
result = mosaic(
|
|
68
|
+
grid_id="50HMH", # Sentinel-2 scene grid ID
|
|
69
|
+
start_year=2022,
|
|
70
|
+
start_month=1,
|
|
71
|
+
start_day=1,
|
|
72
|
+
duration_months=2, # Duration to collect data from
|
|
73
|
+
output_dir=Path("output"), # Output directory for mosaic TIFF files
|
|
74
|
+
sort_method="valid_data", # Method to sort potential scenes before download
|
|
75
|
+
mosaic_method="mean", # Approach used to combine scenes
|
|
76
|
+
required_bands=['visual'], # Required Sentinel-2 bands
|
|
77
|
+
no_data_threshold=0.001 # Threshold for early stopping
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
print(f"Mosaic saved to: {result}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This example creates a mosaic for the grid area "50HMH" for the first two months of 2022, using the visual (TCI) product. The scenes are sorted by valid data percentage, and the mosaic is created using the mean of valid pixels. The process stops iterating through scenes once the no_data_threshold is reached.
|
|
84
|
+
|
|
85
|
+
## Usage Example 2 🔬
|
|
86
|
+
|
|
87
|
+
Here's another example of how to use S2Mosaic:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from s2mosaic import mosaic
|
|
91
|
+
|
|
92
|
+
# Create a mosaic for a specific grid area and time range
|
|
93
|
+
array, rio_profile = mosaic(
|
|
94
|
+
grid_id="50HMH",
|
|
95
|
+
start_year=2022,
|
|
96
|
+
start_month=1,
|
|
97
|
+
start_day=1,
|
|
98
|
+
duration_months=2,
|
|
99
|
+
sort_method="valid_data",
|
|
100
|
+
mosaic_method="mean",
|
|
101
|
+
required_bands=["B04", "B03", "B02", "B08"],
|
|
102
|
+
no_data_threshold=0.001
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print(f"Mosaic array shape: {array.shape}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Similar to the example above but with 16-bit red, green, blue, and NIR bands returned as a NumPy array and rasterio profile.
|
|
109
|
+
|
|
110
|
+
## Advanced Usage 🧠
|
|
111
|
+
|
|
112
|
+
S2Mosaic provides several options for customizing the mosaic creation process:
|
|
113
|
+
|
|
114
|
+
- `sort_method`: Choose between "valid_data", "oldest", or "newest" to determine scene selection priority.
|
|
115
|
+
- `mosaic_method`: Use "mean" for an average of valid pixels or "first" to use the first valid pixel.
|
|
116
|
+
- `required_bands`: Specify which spectral bands to include in the mosaic. Use ["visual"] for an RGB composite.
|
|
117
|
+
- `no_data_threshold`: Set the threshold for considering a pixel as no-data. Set to None to process all scenes.
|
|
118
|
+
- `ocm_batch_size`: Set the batch size for OmniCloudMask inference (default: 6).
|
|
119
|
+
- `ocm_inference_dtype`: Set the data type for OmniCloudMask inference (default: "bf16").
|
|
120
|
+
|
|
121
|
+
For more detailed information on these options and additional functionality, please refer to the function docstring in the source code.
|
|
122
|
+
|
|
123
|
+
## Performance Tips 🚀
|
|
124
|
+
- `ocm_batch_size`: If using a GPU, setting this above the default value (1) will speed up cloud masking. In most cases, a value of 4 works well. If you encounter CUDA errors, try using a lower number.
|
|
125
|
+
- `ocm_inference_dtype`: if the device supports it 'bf16' tends to be the fastest option, failing this try 'fp16' then 'fp32'.
|
|
126
|
+
- `sort_method`: Using "valid_data" as the sort method tends to be the fastest option if no_data_threshold is not None.
|
|
127
|
+
|
|
128
|
+
## Contributing 🤝
|
|
129
|
+
|
|
130
|
+
Contributions to S2Mosaic are welcome! Please feel free to submit pull requests, create issues, or suggest improvements. 🙌
|
|
131
|
+
|
|
132
|
+
## License 📄
|
|
133
|
+
|
|
134
|
+
This project is licensed under the MIT License. ⚖️
|
|
135
|
+
|
|
136
|
+
## Acknowledgments 🙏
|
|
137
|
+
|
|
138
|
+
This package uses the Planetary Computer STAC API and the OmniCloudMask library for cloud masking.
|
s2mosaic-0.1.3/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
## S2Mosaic 🛰️🌍
|
|
2
|
+
|
|
3
|
+
S2Mosaic is a Python package for creating cloud-free mosaics from Sentinel-2 satellite imagery. It allows users to generate composite images for specified grid areas and time ranges, with various options for scene selection and mosaic creation.
|
|
4
|
+
|
|
5
|
+
## Features 🌟
|
|
6
|
+
|
|
7
|
+
- Create Sentinel-2 mosaics for specific grid areas and time ranges.
|
|
8
|
+
- Flexible scene selection methods: by valid data percentage, oldest, or newest scenes.
|
|
9
|
+
- Multiple mosaic creation methods: mean or first valid pixel.
|
|
10
|
+
- Support for different spectral bands, including visual (RGB) composites.
|
|
11
|
+
- State-of-the-art cloud masking using the OmniCloudMask library.
|
|
12
|
+
- Export mosaics as GeoTIFF files or return as NumPy arrays.
|
|
13
|
+
|
|
14
|
+
## Note 📝
|
|
15
|
+
|
|
16
|
+
We use OmniCloudMask for state-of-the-art cloud and cloud shadow masking. OCM will run significantly faster if an available NVIDIA GPU is present.
|
|
17
|
+
|
|
18
|
+
## Try in Colab
|
|
19
|
+
|
|
20
|
+
[![Colab_Button]][Link]
|
|
21
|
+
|
|
22
|
+
[Link]: https://colab.research.google.com/drive/1-vdAAnpzp_VCotTV07cbSC9iQFiD7DcH?usp=sharing 'Try S2Mosaic In Colab'
|
|
23
|
+
|
|
24
|
+
[Colab_Button]: https://img.shields.io/badge/Try%20in%20Colab-grey?style=for-the-badge&logo=google-colab
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Installation 🛠️
|
|
29
|
+
|
|
30
|
+
You can install S2Mosaic using pip:
|
|
31
|
+
```
|
|
32
|
+
pip install s2mosaic
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage Example 1 🚀
|
|
36
|
+
|
|
37
|
+
Here's a basic example of how to use S2Mosaic:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from s2mosaic import mosaic
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
# Create a mosaic for a specific grid area and time range
|
|
44
|
+
result = mosaic(
|
|
45
|
+
grid_id="50HMH", # Sentinel-2 scene grid ID
|
|
46
|
+
start_year=2022,
|
|
47
|
+
start_month=1,
|
|
48
|
+
start_day=1,
|
|
49
|
+
duration_months=2, # Duration to collect data from
|
|
50
|
+
output_dir=Path("output"), # Output directory for mosaic TIFF files
|
|
51
|
+
sort_method="valid_data", # Method to sort potential scenes before download
|
|
52
|
+
mosaic_method="mean", # Approach used to combine scenes
|
|
53
|
+
required_bands=['visual'], # Required Sentinel-2 bands
|
|
54
|
+
no_data_threshold=0.001 # Threshold for early stopping
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
print(f"Mosaic saved to: {result}")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This example creates a mosaic for the grid area "50HMH" for the first two months of 2022, using the visual (TCI) product. The scenes are sorted by valid data percentage, and the mosaic is created using the mean of valid pixels. The process stops iterating through scenes once the no_data_threshold is reached.
|
|
61
|
+
|
|
62
|
+
## Usage Example 2 🔬
|
|
63
|
+
|
|
64
|
+
Here's another example of how to use S2Mosaic:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from s2mosaic import mosaic
|
|
68
|
+
|
|
69
|
+
# Create a mosaic for a specific grid area and time range
|
|
70
|
+
array, rio_profile = mosaic(
|
|
71
|
+
grid_id="50HMH",
|
|
72
|
+
start_year=2022,
|
|
73
|
+
start_month=1,
|
|
74
|
+
start_day=1,
|
|
75
|
+
duration_months=2,
|
|
76
|
+
sort_method="valid_data",
|
|
77
|
+
mosaic_method="mean",
|
|
78
|
+
required_bands=["B04", "B03", "B02", "B08"],
|
|
79
|
+
no_data_threshold=0.001
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
print(f"Mosaic array shape: {array.shape}")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Similar to the example above but with 16-bit red, green, blue, and NIR bands returned as a NumPy array and rasterio profile.
|
|
86
|
+
|
|
87
|
+
## Advanced Usage 🧠
|
|
88
|
+
|
|
89
|
+
S2Mosaic provides several options for customizing the mosaic creation process:
|
|
90
|
+
|
|
91
|
+
- `sort_method`: Choose between "valid_data", "oldest", or "newest" to determine scene selection priority.
|
|
92
|
+
- `mosaic_method`: Use "mean" for an average of valid pixels or "first" to use the first valid pixel.
|
|
93
|
+
- `required_bands`: Specify which spectral bands to include in the mosaic. Use ["visual"] for an RGB composite.
|
|
94
|
+
- `no_data_threshold`: Set the threshold for considering a pixel as no-data. Set to None to process all scenes.
|
|
95
|
+
- `ocm_batch_size`: Set the batch size for OmniCloudMask inference (default: 6).
|
|
96
|
+
- `ocm_inference_dtype`: Set the data type for OmniCloudMask inference (default: "bf16").
|
|
97
|
+
|
|
98
|
+
For more detailed information on these options and additional functionality, please refer to the function docstring in the source code.
|
|
99
|
+
|
|
100
|
+
## Performance Tips 🚀
|
|
101
|
+
- `ocm_batch_size`: If using a GPU, setting this above the default value (1) will speed up cloud masking. In most cases, a value of 4 works well. If you encounter CUDA errors, try using a lower number.
|
|
102
|
+
- `ocm_inference_dtype`: if the device supports it 'bf16' tends to be the fastest option, failing this try 'fp16' then 'fp32'.
|
|
103
|
+
- `sort_method`: Using "valid_data" as the sort method tends to be the fastest option if no_data_threshold is not None.
|
|
104
|
+
|
|
105
|
+
## Contributing 🤝
|
|
106
|
+
|
|
107
|
+
Contributions to S2Mosaic are welcome! Please feel free to submit pull requests, create issues, or suggest improvements. 🙌
|
|
108
|
+
|
|
109
|
+
## License 📄
|
|
110
|
+
|
|
111
|
+
This project is licensed under the MIT License. ⚖️
|
|
112
|
+
|
|
113
|
+
## Acknowledgments 🙏
|
|
114
|
+
|
|
115
|
+
This package uses the Planetary Computer STAC API and the OmniCloudMask library for cloud masking.
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.3"
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict, List, Tuple, Union, Optional, overload
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from datetime import date
|
|
5
|
+
from dateutil.relativedelta import relativedelta
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from functools import partial
|
|
8
|
+
import pkg_resources
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import planetary_computer
|
|
13
|
+
import pystac_client
|
|
14
|
+
import rasterio as rio
|
|
15
|
+
import shapely
|
|
16
|
+
from pandas import DataFrame
|
|
17
|
+
from pystac.item_collection import ItemCollection
|
|
18
|
+
import pystac
|
|
19
|
+
from tqdm.auto import tqdm
|
|
20
|
+
from omnicloudmask import predict_from_array
|
|
21
|
+
import geopandas as gpd
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_band(
|
|
25
|
+
href: str, attempt: int = 0, res: int = 10
|
|
26
|
+
) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|
27
|
+
try:
|
|
28
|
+
singed_href = planetary_computer.sign(href)
|
|
29
|
+
spatial_ratio = res / 10
|
|
30
|
+
if "TCI_10m" in href:
|
|
31
|
+
band_indexes = [1, 2, 3]
|
|
32
|
+
else:
|
|
33
|
+
band_indexes = [1]
|
|
34
|
+
with rio.open(singed_href) as src:
|
|
35
|
+
array = src.read(
|
|
36
|
+
band_indexes,
|
|
37
|
+
out_shape=(
|
|
38
|
+
len(band_indexes),
|
|
39
|
+
int(10980 / spatial_ratio),
|
|
40
|
+
int(10980 / spatial_ratio),
|
|
41
|
+
),
|
|
42
|
+
).astype(np.uint16)
|
|
43
|
+
result = array, src.profile.copy()
|
|
44
|
+
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(e)
|
|
49
|
+
print(f"Failed to open {href}")
|
|
50
|
+
if attempt < 3:
|
|
51
|
+
print(f"Trying again {attempt+1}")
|
|
52
|
+
return get_band(href, attempt + 1)
|
|
53
|
+
else:
|
|
54
|
+
raise Exception(f"Failed to open {href}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ocm_cloud_mask(
|
|
58
|
+
item: pystac.Item,
|
|
59
|
+
batch_size: int = 6,
|
|
60
|
+
inference_dtype: str = "bf16",
|
|
61
|
+
) -> np.ndarray:
|
|
62
|
+
# download RG+NIR bands at 20m resolution for cloud masking
|
|
63
|
+
required_bands = ["B04", "B03", "B8A"]
|
|
64
|
+
get_band_20m = partial(get_band, res=20)
|
|
65
|
+
|
|
66
|
+
hrefs = [item.assets[band].href for band in required_bands]
|
|
67
|
+
|
|
68
|
+
with ThreadPoolExecutor(max_workers=len(required_bands)) as executor:
|
|
69
|
+
bands_and_profiles = list(executor.map(get_band_20m, hrefs))
|
|
70
|
+
|
|
71
|
+
# Separate bands and profiles
|
|
72
|
+
bands, profiles = zip(*bands_and_profiles)
|
|
73
|
+
mask = predict_from_array(
|
|
74
|
+
input_array=np.vstack(bands),
|
|
75
|
+
batch_size=batch_size,
|
|
76
|
+
inference_dtype=inference_dtype,
|
|
77
|
+
)[0]
|
|
78
|
+
# interpolate mask back to 10m
|
|
79
|
+
return mask.repeat(2, axis=0).repeat(2, axis=1) == 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def format_progress(current, total, no_data_pct):
|
|
83
|
+
return f"Scenes: {current}/{total} | No data: {no_data_pct:.2f}%"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def download_bands_pool(
|
|
87
|
+
sorted_scenes: pd.DataFrame,
|
|
88
|
+
required_bands: List[str],
|
|
89
|
+
no_data_threshold: Union[float, None],
|
|
90
|
+
mosaic_method: str = "mean",
|
|
91
|
+
ocm_batch_size: int = 6,
|
|
92
|
+
ocm_inference_dtype: str = "bf16",
|
|
93
|
+
) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|
94
|
+
s2_scene_size = 10980
|
|
95
|
+
pixel_count = s2_scene_size * s2_scene_size
|
|
96
|
+
if "visual" in required_bands:
|
|
97
|
+
mosaic = np.zeros((3, s2_scene_size, s2_scene_size)).astype(np.float32)
|
|
98
|
+
else:
|
|
99
|
+
mosaic = np.zeros((len(required_bands), s2_scene_size, s2_scene_size)).astype(
|
|
100
|
+
np.float32
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
good_pixel_tracker = np.zeros((s2_scene_size, s2_scene_size))
|
|
104
|
+
|
|
105
|
+
pbar = tqdm(
|
|
106
|
+
total=len(sorted_scenes),
|
|
107
|
+
desc=format_progress(0, len(sorted_scenes), 100.0),
|
|
108
|
+
leave=False,
|
|
109
|
+
bar_format="{desc}",
|
|
110
|
+
)
|
|
111
|
+
get_bands_partial = partial(get_band, res=10)
|
|
112
|
+
|
|
113
|
+
for index, item in enumerate(sorted_scenes["item"].tolist()):
|
|
114
|
+
hrefs = [item.assets[band].href for band in required_bands]
|
|
115
|
+
|
|
116
|
+
with ThreadPoolExecutor(max_workers=len(required_bands)) as executor:
|
|
117
|
+
bands_and_profiles = list(executor.map(get_bands_partial, hrefs))
|
|
118
|
+
|
|
119
|
+
bands, profiles = zip(*bands_and_profiles)
|
|
120
|
+
|
|
121
|
+
if "visual" in required_bands:
|
|
122
|
+
full_scene = np.array(bands[0])
|
|
123
|
+
else:
|
|
124
|
+
full_scene = np.vstack(bands)
|
|
125
|
+
|
|
126
|
+
good_pixels = full_scene.sum(axis=0) > 0
|
|
127
|
+
|
|
128
|
+
clear_pixels = ocm_cloud_mask(
|
|
129
|
+
item=item,
|
|
130
|
+
batch_size=ocm_batch_size,
|
|
131
|
+
inference_dtype=ocm_inference_dtype,
|
|
132
|
+
)
|
|
133
|
+
combo_mask = clear_pixels * good_pixels
|
|
134
|
+
good_pixel_tracker += combo_mask
|
|
135
|
+
|
|
136
|
+
full_scene[:, ~combo_mask] = 0
|
|
137
|
+
|
|
138
|
+
if mosaic_method == "mean":
|
|
139
|
+
mosaic += full_scene
|
|
140
|
+
elif mosaic_method == "first":
|
|
141
|
+
new_valid_pixels = combo_mask & (mosaic == 0)
|
|
142
|
+
mosaic[new_valid_pixels] = full_scene[new_valid_pixels]
|
|
143
|
+
|
|
144
|
+
else:
|
|
145
|
+
raise Exception("Invalid mosaic method, must be mean or first")
|
|
146
|
+
|
|
147
|
+
no_data_sum = (good_pixel_tracker == 0).sum()
|
|
148
|
+
no_data_pct = (no_data_sum / pixel_count) * 100
|
|
149
|
+
pbar.set_description(
|
|
150
|
+
format_progress(index + 1, len(sorted_scenes), no_data_pct)
|
|
151
|
+
)
|
|
152
|
+
# if using first or last method, stop if all pixels are filled
|
|
153
|
+
if mosaic_method != "mean":
|
|
154
|
+
if no_data_sum == 0:
|
|
155
|
+
break
|
|
156
|
+
# if no_data_threshold is set, stop if threshold is reached
|
|
157
|
+
if no_data_threshold is not None:
|
|
158
|
+
if no_data_sum < (pixel_count) * no_data_threshold:
|
|
159
|
+
break
|
|
160
|
+
pbar.update(1)
|
|
161
|
+
|
|
162
|
+
remaining_scenes = pbar.total - pbar.n
|
|
163
|
+
pbar.update(remaining_scenes)
|
|
164
|
+
pbar.refresh()
|
|
165
|
+
pbar.close()
|
|
166
|
+
|
|
167
|
+
if mosaic_method == "mean":
|
|
168
|
+
mosaic = mosaic / (good_pixel_tracker + 0.000001)
|
|
169
|
+
if "visual" in required_bands:
|
|
170
|
+
mosaic = np.clip(mosaic, 0, 255).astype(np.uint8)
|
|
171
|
+
else:
|
|
172
|
+
mosaic = np.clip(mosaic, 0, 65535).astype(np.int16)
|
|
173
|
+
|
|
174
|
+
return mosaic, profiles[-1]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def add_item_info(items: ItemCollection) -> DataFrame:
|
|
178
|
+
"""Split items by orbit and sort by no_data"""
|
|
179
|
+
|
|
180
|
+
items_list = []
|
|
181
|
+
for item in items:
|
|
182
|
+
nodata = item.properties["s2:nodata_pixel_percentage"]
|
|
183
|
+
data_pct = 100 - nodata
|
|
184
|
+
|
|
185
|
+
cloud = item.properties["s2:high_proba_clouds_percentage"]
|
|
186
|
+
shadow = item.properties["s2:cloud_shadow_percentage"]
|
|
187
|
+
good_data_pct = data_pct * (1 - (cloud + shadow) / 100)
|
|
188
|
+
capture_date = item.datetime
|
|
189
|
+
|
|
190
|
+
items_list.append(
|
|
191
|
+
{
|
|
192
|
+
"item": item,
|
|
193
|
+
"orbit": item.properties["sat:relative_orbit"],
|
|
194
|
+
"good_data_pct": good_data_pct,
|
|
195
|
+
"datetime": capture_date,
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
items_df = pd.DataFrame(items_list)
|
|
200
|
+
return items_df
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def export_tif(
|
|
204
|
+
array: np.ndarray,
|
|
205
|
+
profile: Dict[str, Any],
|
|
206
|
+
export_path: Path,
|
|
207
|
+
required_bands: List[str],
|
|
208
|
+
) -> None:
|
|
209
|
+
profile.update(count=array.shape[0], dtype=array.dtype, nodata=0, compress="lzw")
|
|
210
|
+
with rio.open(export_path, "w", **profile) as dst:
|
|
211
|
+
dst.write(array)
|
|
212
|
+
dst.descriptions = required_bands
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def search_for_items(
|
|
216
|
+
bounds, grid_id: str, start_date: date, end_date: date
|
|
217
|
+
) -> ItemCollection:
|
|
218
|
+
query = {
|
|
219
|
+
"collections": ["sentinel-2-l2a"],
|
|
220
|
+
"intersects": shapely.to_geojson(bounds),
|
|
221
|
+
"datetime": f"{start_date.isoformat()}Z/{end_date.isoformat()}Z",
|
|
222
|
+
"query": {"s2:mgrs_tile": {"eq": grid_id}},
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
catalog = pystac_client.Client.open(
|
|
226
|
+
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
|
227
|
+
)
|
|
228
|
+
return catalog.search(**query).item_collection()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def sort_items(items: DataFrame, sort_method: str) -> DataFrame:
|
|
232
|
+
# Sort the dataframe by selected method then by orbit
|
|
233
|
+
if sort_method == "valid_data":
|
|
234
|
+
items_sorted = items.sort_values("good_data_pct", ascending=False)
|
|
235
|
+
orbits = items_sorted["orbit"].unique()
|
|
236
|
+
orbit_groups = {
|
|
237
|
+
orbit: items_sorted[items_sorted["orbit"] == orbit] for orbit in orbits
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
result = []
|
|
241
|
+
|
|
242
|
+
while any(len(group) > 0 for group in orbit_groups.values()):
|
|
243
|
+
for orbit in orbits:
|
|
244
|
+
if len(orbit_groups[orbit]) > 0:
|
|
245
|
+
result.append(orbit_groups[orbit].iloc[0])
|
|
246
|
+
orbit_groups[orbit] = orbit_groups[orbit].iloc[1:]
|
|
247
|
+
|
|
248
|
+
items_sorted = pd.DataFrame(result).reset_index(drop=True)
|
|
249
|
+
|
|
250
|
+
elif sort_method == "oldest":
|
|
251
|
+
items_sorted = items.sort_values("datetime", ascending=True).reset_index(
|
|
252
|
+
drop=True
|
|
253
|
+
)
|
|
254
|
+
elif sort_method == "newest":
|
|
255
|
+
items_sorted = items.sort_values("datetime", ascending=False).reset_index(
|
|
256
|
+
drop=True
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
raise Exception("Invalid sort method, must be valid_data, oldest or newest")
|
|
260
|
+
|
|
261
|
+
return items_sorted
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_extent_from_grid_id(grid_id: str) -> shapely.geometry.polygon.Polygon:
|
|
265
|
+
S2_grid_file = Path(
|
|
266
|
+
pkg_resources.resource_filename("s2mosaic", "S2_grid/sentinel_2_index.gpkg")
|
|
267
|
+
)
|
|
268
|
+
assert S2_grid_file.exists()
|
|
269
|
+
S2_grid_gdf = gpd.read_file(S2_grid_file)
|
|
270
|
+
try:
|
|
271
|
+
S2_grid_gdf = S2_grid_gdf[S2_grid_gdf["Name"] == grid_id]
|
|
272
|
+
if len(S2_grid_gdf) != 1:
|
|
273
|
+
raise Exception(f"Grid {grid_id} not found")
|
|
274
|
+
return S2_grid_gdf.iloc[0].geometry.buffer(-0.05)
|
|
275
|
+
except Exception:
|
|
276
|
+
raise Exception(f"Grid {grid_id} not found")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def define_dates(
|
|
280
|
+
start_year: int,
|
|
281
|
+
start_month: int,
|
|
282
|
+
start_day: int,
|
|
283
|
+
duration_years: int,
|
|
284
|
+
duration_months: int,
|
|
285
|
+
duration_days: int,
|
|
286
|
+
) -> Tuple[date, date]:
|
|
287
|
+
start_date = datetime(start_year, start_month, start_day)
|
|
288
|
+
end_date = start_date + relativedelta(
|
|
289
|
+
years=duration_years, months=duration_months, days=duration_days
|
|
290
|
+
)
|
|
291
|
+
return start_date, end_date
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
SORT_VALID_DATA = "valid_data"
|
|
295
|
+
SORT_OLDEST = "oldest"
|
|
296
|
+
SORT_NEWEST = "newest"
|
|
297
|
+
MOSAIC_MEAN = "mean"
|
|
298
|
+
MOSAIC_FIRST = "first"
|
|
299
|
+
|
|
300
|
+
VALID_SORT_METHODS = {SORT_VALID_DATA, SORT_OLDEST, SORT_NEWEST}
|
|
301
|
+
VALID_MOSAIC_METHODS = {MOSAIC_MEAN, MOSAIC_FIRST}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def validate_inputs(
|
|
305
|
+
sort_method: str,
|
|
306
|
+
mosaic_method: str,
|
|
307
|
+
no_data_threshold: Union[float, None],
|
|
308
|
+
required_bands: List[str],
|
|
309
|
+
) -> None:
|
|
310
|
+
if sort_method not in VALID_SORT_METHODS:
|
|
311
|
+
raise ValueError(
|
|
312
|
+
f"Invalid sort method: {sort_method}. Must be one of {VALID_SORT_METHODS}"
|
|
313
|
+
)
|
|
314
|
+
if mosaic_method not in VALID_MOSAIC_METHODS:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"Invalid mosaic method: {mosaic_method}. Must be one of {VALID_MOSAIC_METHODS}"
|
|
317
|
+
)
|
|
318
|
+
if no_data_threshold is not None:
|
|
319
|
+
if not (0.0 <= no_data_threshold <= 1.0):
|
|
320
|
+
raise ValueError(
|
|
321
|
+
f"No data threshold must be between 0 and 1 or None, got {no_data_threshold}"
|
|
322
|
+
)
|
|
323
|
+
valid_bands = [
|
|
324
|
+
"AOT",
|
|
325
|
+
"SCL",
|
|
326
|
+
"WVP",
|
|
327
|
+
"visual",
|
|
328
|
+
"B01",
|
|
329
|
+
"B02",
|
|
330
|
+
"B03",
|
|
331
|
+
"B04",
|
|
332
|
+
"B05",
|
|
333
|
+
"B06",
|
|
334
|
+
"B07",
|
|
335
|
+
"B08",
|
|
336
|
+
"B8A",
|
|
337
|
+
"B09",
|
|
338
|
+
"B11",
|
|
339
|
+
"B12",
|
|
340
|
+
]
|
|
341
|
+
for band in required_bands:
|
|
342
|
+
if band not in valid_bands:
|
|
343
|
+
raise ValueError(f"Invalid band: {band}, must be one of {valid_bands}")
|
|
344
|
+
if "visual" in required_bands and len(required_bands) > 1:
|
|
345
|
+
raise ValueError("Cannot use visual band with other bands, must be used alone")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def get_output_path(
|
|
349
|
+
output_dir: Union[Path, str],
|
|
350
|
+
grid_id: str,
|
|
351
|
+
start_date: date,
|
|
352
|
+
end_date: date,
|
|
353
|
+
sort_method: str,
|
|
354
|
+
mosaic_method: str,
|
|
355
|
+
required_bands: List[str],
|
|
356
|
+
) -> Path:
|
|
357
|
+
output_dir = Path(output_dir)
|
|
358
|
+
output_dir.mkdir(exist_ok=True, parents=True)
|
|
359
|
+
bands_str = "_".join(required_bands)
|
|
360
|
+
export_path = output_dir / (
|
|
361
|
+
f"{grid_id}_{start_date.strftime('%Y-%m-%d')}_to_{end_date.strftime('%Y-%m-%d')}_{sort_method}_{mosaic_method}_{bands_str}.tif"
|
|
362
|
+
)
|
|
363
|
+
return export_path
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@overload
|
|
367
|
+
def mosaic(
|
|
368
|
+
grid_id: str,
|
|
369
|
+
start_year: int,
|
|
370
|
+
start_month: int = 1,
|
|
371
|
+
start_day: int = 1,
|
|
372
|
+
output_dir: None = None,
|
|
373
|
+
sort_method: str = "valid_data",
|
|
374
|
+
mosaic_method: str = "mean",
|
|
375
|
+
duration_years: int = 0,
|
|
376
|
+
duration_months: int = 0,
|
|
377
|
+
duration_days: int = 0,
|
|
378
|
+
required_bands: List[str] = ["B04", "B03", "B02", "B08"],
|
|
379
|
+
no_data_threshold: Optional[float] = 0.01,
|
|
380
|
+
overwrite: bool = True,
|
|
381
|
+
ocm_batch_size: int = 1,
|
|
382
|
+
ocm_inference_dtype: str = "bf16",
|
|
383
|
+
) -> Tuple[np.ndarray, Dict[str, Any]]: ...
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@overload
|
|
387
|
+
def mosaic(
|
|
388
|
+
grid_id: str,
|
|
389
|
+
start_year: int,
|
|
390
|
+
start_month: int = 1,
|
|
391
|
+
start_day: int = 1,
|
|
392
|
+
output_dir: Union[str, Path] = ...,
|
|
393
|
+
sort_method: str = "valid_data",
|
|
394
|
+
mosaic_method: str = "mean",
|
|
395
|
+
duration_years: int = 0,
|
|
396
|
+
duration_months: int = 0,
|
|
397
|
+
duration_days: int = 0,
|
|
398
|
+
required_bands: List[str] = ["B04", "B03", "B02", "B08"],
|
|
399
|
+
no_data_threshold: Optional[float] = 0.01,
|
|
400
|
+
overwrite: bool = True,
|
|
401
|
+
ocm_batch_size: int = 1,
|
|
402
|
+
ocm_inference_dtype: str = "bf16",
|
|
403
|
+
) -> Path: ...
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def mosaic(
|
|
407
|
+
grid_id: str,
|
|
408
|
+
start_year: int,
|
|
409
|
+
start_month: int = 1,
|
|
410
|
+
start_day: int = 1,
|
|
411
|
+
output_dir: Optional[Union[Path, str]] = None,
|
|
412
|
+
sort_method: str = "valid_data",
|
|
413
|
+
mosaic_method: str = "mean",
|
|
414
|
+
duration_years: int = 0,
|
|
415
|
+
duration_months: int = 0,
|
|
416
|
+
duration_days: int = 0,
|
|
417
|
+
required_bands: List[str] = ["B04", "B03", "B02", "B08"],
|
|
418
|
+
no_data_threshold: Union[float, None] = 0.01,
|
|
419
|
+
overwrite: bool = True,
|
|
420
|
+
ocm_batch_size: int = 1,
|
|
421
|
+
ocm_inference_dtype: str = "bf16",
|
|
422
|
+
) -> Union[Tuple[np.ndarray, Dict[str, Any]], Path]:
|
|
423
|
+
"""
|
|
424
|
+
Create a Sentinel-2 mosaic for a specified grid and time range.
|
|
425
|
+
|
|
426
|
+
This function generates a mosaic from Sentinel-2 satellite imagery based on the provided
|
|
427
|
+
grid ID and time range. It can either return the mosaic data and metadata or save it as
|
|
428
|
+
a GeoTIFF file.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
grid_id (str): The ID of the grid area for which to create the mosaic (e.g., "50HMH").
|
|
432
|
+
start_year (int): The start year of the time range.
|
|
433
|
+
start_month (int, optional): The start month of the time range. Defaults to 1 (January).
|
|
434
|
+
start_day (int, optional): The start day of the time range. Defaults to 1.
|
|
435
|
+
output_dir (Optional[Union[Path, str]], optional): Directory to save the output GeoTIFF.
|
|
436
|
+
If None, the mosaic is not saved to disk and is returned instead. Defaults to None.
|
|
437
|
+
sort_method (str, optional): Method to sort scenes. Options are "valid_data", "oldest", or "newest". Defaults to "valid_data".
|
|
438
|
+
mosaic_method (str, optional): Method to create the mosaic. Options are "mean" or "first". Defaults to "mean".
|
|
439
|
+
duration_years (int, optional): Duration in years to add to the start date. Defaults to 0.
|
|
440
|
+
duration_months (int, optional): Duration in months to add to the start date. Defaults to 0.
|
|
441
|
+
duration_days (int, optional): Duration in days to add to the start date. Defaults to 0.
|
|
442
|
+
required_bands (List[str], optional): List of required spectral bands.
|
|
443
|
+
Defaults to ["B04", "B03", "B02", "B08"] (Red, Green, Blue, NIR).
|
|
444
|
+
no_data_threshold (float, optional): Threshold for no data values. Defaults to 0.01.
|
|
445
|
+
overwrite (bool, optional): Whether to overwrite existing output files. Defaults to True.
|
|
446
|
+
ocm_batch_size (int, optional): Batch size for OCM inference. Defaults to 1.
|
|
447
|
+
ocm_inference_dtype (str, optional): Data type for OCM inference. Defaults to "bf16".
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Union[Tuple[np.ndarray, Dict[str, Any]], Path]: If output_dir is None, returns a tuple
|
|
451
|
+
containing the mosaic array and metadata dictionary. If output_dir is provided,
|
|
452
|
+
returns the path to the saved GeoTIFF file.
|
|
453
|
+
|
|
454
|
+
Raises:
|
|
455
|
+
Exception: If no scenes are found for the specified grid ID and time range.
|
|
456
|
+
|
|
457
|
+
Note:
|
|
458
|
+
- The function uses the STAC API to search for Sentinel-2 scenes.
|
|
459
|
+
- If 'visual' is included in required_bands, it will be replaced with 'Red', 'Green', 'Blue' in the output.
|
|
460
|
+
- The time range for scene selection is inclusive of the start date and exclusive of the end date.
|
|
461
|
+
"""
|
|
462
|
+
bounds = get_extent_from_grid_id(grid_id)
|
|
463
|
+
|
|
464
|
+
validate_inputs(sort_method, mosaic_method, no_data_threshold, required_bands)
|
|
465
|
+
|
|
466
|
+
start_date, end_date = define_dates(
|
|
467
|
+
start_year,
|
|
468
|
+
start_month,
|
|
469
|
+
start_day,
|
|
470
|
+
duration_years,
|
|
471
|
+
duration_months,
|
|
472
|
+
duration_days,
|
|
473
|
+
)
|
|
474
|
+
if output_dir:
|
|
475
|
+
export_path = get_output_path(
|
|
476
|
+
grid_id=grid_id,
|
|
477
|
+
start_date=start_date,
|
|
478
|
+
end_date=end_date,
|
|
479
|
+
sort_method=sort_method,
|
|
480
|
+
mosaic_method=mosaic_method,
|
|
481
|
+
required_bands=required_bands,
|
|
482
|
+
output_dir=output_dir,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if output_dir:
|
|
486
|
+
if export_path.exists() and not overwrite:
|
|
487
|
+
return export_path
|
|
488
|
+
|
|
489
|
+
items = search_for_items(
|
|
490
|
+
bounds=bounds, grid_id=grid_id, start_date=start_date, end_date=end_date
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
if len(items) == 0:
|
|
494
|
+
raise Exception(
|
|
495
|
+
f"No scenes found for {grid_id} between {start_date.strftime('%Y-%m-%d')} and {end_date.strftime('%Y-%m-%d')}"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
items_with_orbits = add_item_info(items)
|
|
499
|
+
|
|
500
|
+
sorted_items = sort_items(items=items_with_orbits, sort_method=sort_method)
|
|
501
|
+
|
|
502
|
+
mosaic, profile = download_bands_pool(
|
|
503
|
+
sorted_scenes=sorted_items,
|
|
504
|
+
required_bands=required_bands,
|
|
505
|
+
no_data_threshold=no_data_threshold,
|
|
506
|
+
mosaic_method=mosaic_method,
|
|
507
|
+
ocm_batch_size=ocm_batch_size,
|
|
508
|
+
ocm_inference_dtype=ocm_inference_dtype,
|
|
509
|
+
)
|
|
510
|
+
if "visual" in required_bands:
|
|
511
|
+
required_bands = ["Red", "Green", "Blue"]
|
|
512
|
+
|
|
513
|
+
if output_dir:
|
|
514
|
+
export_tif(mosaic, profile, export_path, required_bands)
|
|
515
|
+
return export_path
|
|
516
|
+
|
|
517
|
+
return mosaic, profile
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: s2mosaic
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Python library for making cloud-free Sentinel-2 mosaics
|
|
5
|
+
Home-page: https://github.com/DPIRD-DMA/S2Mosaic
|
|
6
|
+
Author: Nick Wright
|
|
7
|
+
Author-email: nicholas.wright@dpird.wa.gov.au
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Requires-Python: >=3.7
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: planetary_computer
|
|
20
|
+
Requires-Dist: pystac_client
|
|
21
|
+
Requires-Dist: geopandas
|
|
22
|
+
Requires-Dist: omnicloudmask
|
|
23
|
+
|
|
24
|
+
## S2Mosaic 🛰️🌍
|
|
25
|
+
|
|
26
|
+
S2Mosaic is a Python package for creating cloud-free mosaics from Sentinel-2 satellite imagery. It allows users to generate composite images for specified grid areas and time ranges, with various options for scene selection and mosaic creation.
|
|
27
|
+
|
|
28
|
+
## Features 🌟
|
|
29
|
+
|
|
30
|
+
- Create Sentinel-2 mosaics for specific grid areas and time ranges.
|
|
31
|
+
- Flexible scene selection methods: by valid data percentage, oldest, or newest scenes.
|
|
32
|
+
- Multiple mosaic creation methods: mean or first valid pixel.
|
|
33
|
+
- Support for different spectral bands, including visual (RGB) composites.
|
|
34
|
+
- State-of-the-art cloud masking using the OmniCloudMask library.
|
|
35
|
+
- Export mosaics as GeoTIFF files or return as NumPy arrays.
|
|
36
|
+
|
|
37
|
+
## Note 📝
|
|
38
|
+
|
|
39
|
+
We use OmniCloudMask for state-of-the-art cloud and cloud shadow masking. OCM will run significantly faster if an available NVIDIA GPU is present.
|
|
40
|
+
|
|
41
|
+
## Try in Colab
|
|
42
|
+
|
|
43
|
+
[![Colab_Button]][Link]
|
|
44
|
+
|
|
45
|
+
[Link]: https://colab.research.google.com/drive/1-vdAAnpzp_VCotTV07cbSC9iQFiD7DcH?usp=sharing 'Try S2Mosaic In Colab'
|
|
46
|
+
|
|
47
|
+
[Colab_Button]: https://img.shields.io/badge/Try%20in%20Colab-grey?style=for-the-badge&logo=google-colab
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Installation 🛠️
|
|
52
|
+
|
|
53
|
+
You can install S2Mosaic using pip:
|
|
54
|
+
```
|
|
55
|
+
pip install s2mosaic
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage Example 1 🚀
|
|
59
|
+
|
|
60
|
+
Here's a basic example of how to use S2Mosaic:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from s2mosaic import mosaic
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
|
|
66
|
+
# Create a mosaic for a specific grid area and time range
|
|
67
|
+
result = mosaic(
|
|
68
|
+
grid_id="50HMH", # Sentinel-2 scene grid ID
|
|
69
|
+
start_year=2022,
|
|
70
|
+
start_month=1,
|
|
71
|
+
start_day=1,
|
|
72
|
+
duration_months=2, # Duration to collect data from
|
|
73
|
+
output_dir=Path("output"), # Output directory for mosaic TIFF files
|
|
74
|
+
sort_method="valid_data", # Method to sort potential scenes before download
|
|
75
|
+
mosaic_method="mean", # Approach used to combine scenes
|
|
76
|
+
required_bands=['visual'], # Required Sentinel-2 bands
|
|
77
|
+
no_data_threshold=0.001 # Threshold for early stopping
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
print(f"Mosaic saved to: {result}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This example creates a mosaic for the grid area "50HMH" for the first two months of 2022, using the visual (TCI) product. The scenes are sorted by valid data percentage, and the mosaic is created using the mean of valid pixels. The process stops iterating through scenes once the no_data_threshold is reached.
|
|
84
|
+
|
|
85
|
+
## Usage Example 2 🔬
|
|
86
|
+
|
|
87
|
+
Here's another example of how to use S2Mosaic:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from s2mosaic import mosaic
|
|
91
|
+
|
|
92
|
+
# Create a mosaic for a specific grid area and time range
|
|
93
|
+
array, rio_profile = mosaic(
|
|
94
|
+
grid_id="50HMH",
|
|
95
|
+
start_year=2022,
|
|
96
|
+
start_month=1,
|
|
97
|
+
start_day=1,
|
|
98
|
+
duration_months=2,
|
|
99
|
+
sort_method="valid_data",
|
|
100
|
+
mosaic_method="mean",
|
|
101
|
+
required_bands=["B04", "B03", "B02", "B08"],
|
|
102
|
+
no_data_threshold=0.001
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print(f"Mosaic array shape: {array.shape}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Similar to the example above but with 16-bit red, green, blue, and NIR bands returned as a NumPy array and rasterio profile.
|
|
109
|
+
|
|
110
|
+
## Advanced Usage 🧠
|
|
111
|
+
|
|
112
|
+
S2Mosaic provides several options for customizing the mosaic creation process:
|
|
113
|
+
|
|
114
|
+
- `sort_method`: Choose between "valid_data", "oldest", or "newest" to determine scene selection priority.
|
|
115
|
+
- `mosaic_method`: Use "mean" for an average of valid pixels or "first" to use the first valid pixel.
|
|
116
|
+
- `required_bands`: Specify which spectral bands to include in the mosaic. Use ["visual"] for an RGB composite.
|
|
117
|
+
- `no_data_threshold`: Set the threshold for considering a pixel as no-data. Set to None to process all scenes.
|
|
118
|
+
- `ocm_batch_size`: Set the batch size for OmniCloudMask inference (default: 6).
|
|
119
|
+
- `ocm_inference_dtype`: Set the data type for OmniCloudMask inference (default: "bf16").
|
|
120
|
+
|
|
121
|
+
For more detailed information on these options and additional functionality, please refer to the function docstring in the source code.
|
|
122
|
+
|
|
123
|
+
## Performance Tips 🚀
|
|
124
|
+
- `ocm_batch_size`: If using a GPU, setting this above the default value (1) will speed up cloud masking. In most cases, a value of 4 works well. If you encounter CUDA errors, try using a lower number.
|
|
125
|
+
- `ocm_inference_dtype`: if the device supports it 'bf16' tends to be the fastest option, failing this try 'fp16' then 'fp32'.
|
|
126
|
+
- `sort_method`: Using "valid_data" as the sort method tends to be the fastest option if no_data_threshold is not None.
|
|
127
|
+
|
|
128
|
+
## Contributing 🤝
|
|
129
|
+
|
|
130
|
+
Contributions to S2Mosaic are welcome! Please feel free to submit pull requests, create issues, or suggest improvements. 🙌
|
|
131
|
+
|
|
132
|
+
## License 📄
|
|
133
|
+
|
|
134
|
+
This project is licensed under the MIT License. ⚖️
|
|
135
|
+
|
|
136
|
+
## Acknowledgments 🙏
|
|
137
|
+
|
|
138
|
+
This package uses the Planetary Computer STAC API and the OmniCloudMask library for cloud masking.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
s2mosaic/__init__.py
|
|
5
|
+
s2mosaic/__version__.py
|
|
6
|
+
s2mosaic/download.py
|
|
7
|
+
s2mosaic.egg-info/PKG-INFO
|
|
8
|
+
s2mosaic.egg-info/SOURCES.txt
|
|
9
|
+
s2mosaic.egg-info/dependency_links.txt
|
|
10
|
+
s2mosaic.egg-info/requires.txt
|
|
11
|
+
s2mosaic.egg-info/top_level.txt
|
|
12
|
+
s2mosaic/S2_grid/sentinel_2_index.gpkg
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
s2mosaic
|
s2mosaic-0.1.3/setup.cfg
ADDED
s2mosaic-0.1.3/setup.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
version = {}
|
|
5
|
+
with open(os.path.join("s2mosaic", "__version__.py")) as fp:
|
|
6
|
+
exec(fp.read(), version)
|
|
7
|
+
|
|
8
|
+
setup(
|
|
9
|
+
name="s2mosaic",
|
|
10
|
+
version=version["__version__"],
|
|
11
|
+
description="""Python library for making cloud-free Sentinel-2 mosaics""",
|
|
12
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
author="Nick Wright",
|
|
15
|
+
author_email="nicholas.wright@dpird.wa.gov.au",
|
|
16
|
+
url="https://github.com/DPIRD-DMA/S2Mosaic",
|
|
17
|
+
python_requires=">=3.7",
|
|
18
|
+
packages=find_packages(),
|
|
19
|
+
install_requires=[
|
|
20
|
+
"planetary_computer",
|
|
21
|
+
"pystac_client",
|
|
22
|
+
"geopandas",
|
|
23
|
+
"omnicloudmask",
|
|
24
|
+
],
|
|
25
|
+
classifiers=[
|
|
26
|
+
"Development Status :: 3 - Alpha",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Programming Language :: Python :: 3.7",
|
|
30
|
+
"Programming Language :: Python :: 3.8",
|
|
31
|
+
"Programming Language :: Python :: 3.9",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
],
|
|
35
|
+
package_data={"s2mosaic": ["S2_grid/sentinel_2_index.gpkg"]},
|
|
36
|
+
)
|