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 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.
@@ -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,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.
@@ -0,0 +1,7 @@
1
+ from .__version__ import __version__
2
+ from .download import mosaic
3
+
4
+
5
+ __all__ = [
6
+ "mosaic",
7
+ ]
@@ -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,4 @@
1
+ planetary_computer
2
+ pystac_client
3
+ geopandas
4
+ omnicloudmask
@@ -0,0 +1 @@
1
+ s2mosaic
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )