satcube 0.1.17__py3-none-any.whl → 0.1.18__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.
Potentially problematic release.
This version of satcube might be problematic. Click here for more details.
- satcube/__init__.py +2 -4
- satcube/align.py +85 -44
- satcube/archive_cloud_detection.py +23 -0
- satcube/archive_dataclass.py +39 -0
- satcube/archive_main.py +453 -0
- satcube/archive_utils.py +1087 -0
- satcube/{cloud_detection.py → cloud.py} +100 -95
- satcube/composite.py +85 -0
- satcube/download.py +2 -5
- satcube/gapfill.py +216 -0
- satcube/objects.py +208 -36
- satcube/smooth.py +46 -0
- {satcube-0.1.17.dist-info → satcube-0.1.18.dist-info}/METADATA +1 -1
- satcube-0.1.18.dist-info/RECORD +17 -0
- satcube-0.1.17.dist-info/RECORD +0 -10
- {satcube-0.1.17.dist-info → satcube-0.1.18.dist-info}/LICENSE +0 -0
- {satcube-0.1.17.dist-info → satcube-0.1.18.dist-info}/WHEEL +0 -0
satcube/archive_main.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Optional, Tuple, Union
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import fastcubo
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from satcube.archive_dataclass import Sensor
|
|
10
|
+
from satcube.archive_utils import (aligned_s2, cloudmasking_s2, display_images,
|
|
11
|
+
gapfilling_s2, intermediate_process, interpolate_s2,
|
|
12
|
+
metadata_s2, monthly_composites_s2, smooth_s2, super_s2)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SatCube:
|
|
16
|
+
"""Satellite cube class to create datacubes from a specific sensor."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
sensor: Sensor,
|
|
21
|
+
output_dir: str,
|
|
22
|
+
max_workers: int,
|
|
23
|
+
coordinates: Tuple[float, float],
|
|
24
|
+
device: Union[str, torch.device],
|
|
25
|
+
):
|
|
26
|
+
"""Create a new instance of the Satellite Cube class.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
coordinates (Tuple[float, float]): The coordinates of the
|
|
30
|
+
location to download the data.
|
|
31
|
+
sensor (Sensor): The sensor object with all the information
|
|
32
|
+
to download and preprocess the data.
|
|
33
|
+
output_dir (str): The output directory to save the data.
|
|
34
|
+
max_workers (int): The maximum number of workers to use in the
|
|
35
|
+
download process.
|
|
36
|
+
device (Union[str, torch.device]): The device to use in the
|
|
37
|
+
cloud removal process.
|
|
38
|
+
"""
|
|
39
|
+
self.device = device
|
|
40
|
+
self.sensor = sensor
|
|
41
|
+
self.output_dir = pathlib.Path(output_dir)
|
|
42
|
+
self.max_workers = max_workers
|
|
43
|
+
self.lon, self.lat = coordinates
|
|
44
|
+
|
|
45
|
+
# If the output directory does not exist, create it
|
|
46
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
def metadata_s2(
|
|
49
|
+
self,
|
|
50
|
+
out_csv: Optional[pathlib.Path] = None,
|
|
51
|
+
quiet: Optional[pathlib.Path] = False,
|
|
52
|
+
force: Optional[bool] = False,
|
|
53
|
+
) -> pd.DataFrame:
|
|
54
|
+
"""Create a pd.DataFrame with the images to download and their
|
|
55
|
+
cloud cover percentage. The table is saved in a CSV file.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
force (Optional[bool], optional): If True, the query
|
|
59
|
+
process is done again. Defaults to False.
|
|
60
|
+
out_csv (Optional[pathlib.Path], optional): The path to the
|
|
61
|
+
CSV file with the query table. Defaults to None.
|
|
62
|
+
quiet (Optional[bool], optional): If True, no message is
|
|
63
|
+
displayed. Defaults to False.
|
|
64
|
+
force (Optional[bool], optional): If True, the query process
|
|
65
|
+
is done again. Defaults to False.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
pd.DataFrame: The table with the images to download.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
if out_csv is None:
|
|
72
|
+
out_csv: pathlib.Path = self.output_dir / "s2_01_gee_query.csv"
|
|
73
|
+
|
|
74
|
+
if not out_csv.exists() or force:
|
|
75
|
+
query_table: pd.DataFrame = metadata_s2(
|
|
76
|
+
lon=self.lon,
|
|
77
|
+
lat=self.lat,
|
|
78
|
+
range_date=(self.sensor.start_date, self.sensor.end_date),
|
|
79
|
+
edge_size=self.sensor.edge_size,
|
|
80
|
+
quiet=quiet
|
|
81
|
+
)
|
|
82
|
+
query_table.to_csv(out_csv, index=False)
|
|
83
|
+
else:
|
|
84
|
+
query_table = pd.read_csv(out_csv)
|
|
85
|
+
|
|
86
|
+
return query_table
|
|
87
|
+
|
|
88
|
+
def download_s2_image(
|
|
89
|
+
self,
|
|
90
|
+
table: pd.DataFrame,
|
|
91
|
+
out_folder: Optional[pathlib.Path] = None,
|
|
92
|
+
quiet: Optional[bool] = False,
|
|
93
|
+
force: Optional[bool] = False,
|
|
94
|
+
) -> pathlib.Path:
|
|
95
|
+
"""Download the images from the query table.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
table (pd.DataFrame): The table with the images to download.
|
|
99
|
+
out_csv (Optional[pathlib.Path], optional): The path to the
|
|
100
|
+
CSV file with the query table. Defaults to None.
|
|
101
|
+
quiet (Optional[bool], optional): If True, the download
|
|
102
|
+
process is not displayed. Defaults to False.
|
|
103
|
+
force (Optional[bool], optional): If True, the download
|
|
104
|
+
process is done again. Defaults to False.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
pathlib.Path: The path to the folder with the downloaded images.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# Create the output directory if it does not exist
|
|
111
|
+
if out_folder is None:
|
|
112
|
+
output_path: pathlib.Path = self.output_dir / "s2_01_raw"
|
|
113
|
+
|
|
114
|
+
# Download the selected images
|
|
115
|
+
if not output_path.exists() or force:
|
|
116
|
+
if not quiet:
|
|
117
|
+
print(f"Saving the images in the directory {output_path}")
|
|
118
|
+
|
|
119
|
+
fastcubo.getPixels(
|
|
120
|
+
table=table, nworkers=self.max_workers, output_path=output_path
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Add folder path
|
|
124
|
+
table["folder"] = output_path
|
|
125
|
+
|
|
126
|
+
return table
|
|
127
|
+
|
|
128
|
+
def cloudmasking_s2(
|
|
129
|
+
self,
|
|
130
|
+
table: pd.DataFrame,
|
|
131
|
+
out_folder: Optional[pathlib.Path] = None,
|
|
132
|
+
quiet: Optional[bool] = False,
|
|
133
|
+
force: Optional[bool] = False,
|
|
134
|
+
) -> pathlib.Path:
|
|
135
|
+
"""Remove the clouds from the data.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
table (pd.DataFrame): The table with the images to remove
|
|
139
|
+
the clouds.
|
|
140
|
+
out_csv (Optional[pathlib.Path], optional): The path to the
|
|
141
|
+
CSV file with the query table. Defaults to None.
|
|
142
|
+
quiet (Optional[bool], optional): If True, the messages
|
|
143
|
+
are not displayed. Defaults to False.
|
|
144
|
+
force (Optional[bool], optional): If True, the cloud removal
|
|
145
|
+
is done again. Defaults to False.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
pathlib.Path: The path to the folder with the
|
|
149
|
+
data without clouds.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
if out_folder is None:
|
|
153
|
+
out_folder: pathlib = self.output_dir / "s2_02_nocloud"
|
|
154
|
+
|
|
155
|
+
# Apply the cloud removal
|
|
156
|
+
out_table = intermediate_process(
|
|
157
|
+
table=table,
|
|
158
|
+
out_folder=out_folder,
|
|
159
|
+
process_function=cloudmasking_s2,
|
|
160
|
+
process_function_args=dict(
|
|
161
|
+
device=self.device,
|
|
162
|
+
sensor=self.sensor,
|
|
163
|
+
quiet=quiet
|
|
164
|
+
),
|
|
165
|
+
force=force,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Change the folder path
|
|
169
|
+
out_table["folder"] = out_folder
|
|
170
|
+
|
|
171
|
+
# Sort by cloud cover
|
|
172
|
+
out_table = out_table.sort_values(by="cloud_cover", ascending=False)
|
|
173
|
+
out_table.reset_index(drop=True, inplace=True)
|
|
174
|
+
|
|
175
|
+
return out_table
|
|
176
|
+
|
|
177
|
+
def gapfilling_s2(
|
|
178
|
+
self,
|
|
179
|
+
table: pd.DataFrame,
|
|
180
|
+
method: Optional[str] = "linear",
|
|
181
|
+
out_folder: Optional[pathlib.Path] = None,
|
|
182
|
+
quiet: Optional[bool] = False,
|
|
183
|
+
force: Optional[bool] = False,
|
|
184
|
+
) -> pathlib.Path:
|
|
185
|
+
"""Fill the gaps in the data.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
force (Optional[bool], optional): If True, the gap filling
|
|
189
|
+
is done again. Defaults to False.
|
|
190
|
+
histogram_match_error (float, optional): If the error in the
|
|
191
|
+
histogram matching is greater than this value, the image
|
|
192
|
+
is not filled and therefore, it is removed. Defaults
|
|
193
|
+
to 0.10.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
pathlib.Path: The path to the folder with the
|
|
197
|
+
data without gaps.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
if out_folder is None:
|
|
201
|
+
out_folder: pathlib = self.output_dir / "s2_03_nogaps"
|
|
202
|
+
|
|
203
|
+
# Apply the cloud removal
|
|
204
|
+
out_table = intermediate_process(
|
|
205
|
+
table=table,
|
|
206
|
+
out_folder=out_folder,
|
|
207
|
+
process_function=gapfilling_s2,
|
|
208
|
+
process_function_args={"method": method, "quiet": quiet},
|
|
209
|
+
force=force,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Change the folder path
|
|
213
|
+
out_table["folder"] = out_folder
|
|
214
|
+
|
|
215
|
+
# Sort by the matching error
|
|
216
|
+
out_table = out_table.sort_values(by="match_error", ascending=False)
|
|
217
|
+
|
|
218
|
+
return out_table
|
|
219
|
+
|
|
220
|
+
def align_s2(
|
|
221
|
+
self,
|
|
222
|
+
table: pd.DataFrame,
|
|
223
|
+
out_folder: Optional[pathlib.Path] = None,
|
|
224
|
+
quiet: Optional[bool] = False,
|
|
225
|
+
force: Optional[bool] = False,
|
|
226
|
+
) -> pathlib.Path:
|
|
227
|
+
"""Align all the images in the data cube.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
table (pd.DataFrame): The table with the images to align.
|
|
231
|
+
force (Optional[bool], optional): If True, the alignment
|
|
232
|
+
is done again. Defaults to False.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
pathlib.Path: The path to the folder with the
|
|
236
|
+
aligned images.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
if out_folder is None:
|
|
240
|
+
out_folder: pathlib = self.output_dir / "s2_04_aligned"
|
|
241
|
+
|
|
242
|
+
# Apply the cloud removal
|
|
243
|
+
out_table = intermediate_process(
|
|
244
|
+
table=table,
|
|
245
|
+
out_folder=out_folder,
|
|
246
|
+
process_function=aligned_s2,
|
|
247
|
+
process_function_args={"quiet": quiet},
|
|
248
|
+
force=force,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Change the folder path
|
|
252
|
+
out_table["folder"] = out_folder
|
|
253
|
+
|
|
254
|
+
return out_table
|
|
255
|
+
|
|
256
|
+
def monthly_composites_s2(
|
|
257
|
+
self,
|
|
258
|
+
table: Optional[pd.DataFrame],
|
|
259
|
+
out_folder: Optional[pathlib.Path] = None,
|
|
260
|
+
agg_method: Optional[str] = "median",
|
|
261
|
+
date_range: Tuple[str, str] = ("2016-01-01", datetime.now().strftime("%Y-%m-%d")),
|
|
262
|
+
quiet: Optional[bool] = False,
|
|
263
|
+
force: Optional[bool] = False,
|
|
264
|
+
) -> pathlib.Path:
|
|
265
|
+
"""Smooth the data considering the temporal dimension.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
force (Optional[bool], optional): If True, the interpolation
|
|
269
|
+
is done again. Defaults to False.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
xr.Dataset: The interpolated data.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
if out_folder is None:
|
|
276
|
+
out_folder: pathlib = self.output_dir / "s2_05_monthlycomposites"
|
|
277
|
+
|
|
278
|
+
# Prepare the composites
|
|
279
|
+
out_table = intermediate_process(
|
|
280
|
+
table=table,
|
|
281
|
+
out_folder=out_folder,
|
|
282
|
+
process_function=monthly_composites_s2,
|
|
283
|
+
process_function_args=dict(
|
|
284
|
+
agg_method=agg_method,
|
|
285
|
+
date_range=date_range,
|
|
286
|
+
quiet=quiet
|
|
287
|
+
),
|
|
288
|
+
force=force
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Change the folder path
|
|
292
|
+
out_table["folder"] = out_folder
|
|
293
|
+
|
|
294
|
+
return out_table
|
|
295
|
+
|
|
296
|
+
def interpolate_s2(
|
|
297
|
+
self,
|
|
298
|
+
table: pd.DataFrame,
|
|
299
|
+
out_folder: Optional[pathlib.Path] = None,
|
|
300
|
+
quiet: Optional[bool] = False,
|
|
301
|
+
force: Optional[bool] = False,
|
|
302
|
+
) -> pathlib.Path:
|
|
303
|
+
"""Interpolate the data.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
force (Optional[bool], optional): If True, the interpolation
|
|
307
|
+
is done again. Defaults to False.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
xr.Dataset: The interpolated data.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
if out_folder is None:
|
|
314
|
+
out_folder: pathlib = self.output_dir / "s2_06_interpolation"
|
|
315
|
+
|
|
316
|
+
# Apply the cloud removal
|
|
317
|
+
out_table = intermediate_process(
|
|
318
|
+
table=table,
|
|
319
|
+
out_folder=out_folder,
|
|
320
|
+
process_function=interpolate_s2,
|
|
321
|
+
process_function_args=dict(
|
|
322
|
+
quiet=quiet
|
|
323
|
+
),
|
|
324
|
+
force=force,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Change the folder path
|
|
328
|
+
out_table["folder"] = out_folder
|
|
329
|
+
|
|
330
|
+
return out_table
|
|
331
|
+
|
|
332
|
+
def smooth_s2(
|
|
333
|
+
self,
|
|
334
|
+
table: pd.DataFrame,
|
|
335
|
+
out_folder: Optional[pathlib.Path] = None,
|
|
336
|
+
smooth_w: Optional[int] = 7,
|
|
337
|
+
smooth_p: Optional[int] = 1,
|
|
338
|
+
device: Union[str, torch.device, None] = None,
|
|
339
|
+
quiet: Optional[bool] = False,
|
|
340
|
+
force: Optional[bool] = False,
|
|
341
|
+
) -> pd.DataFrame:
|
|
342
|
+
"""Interpolate the data.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
force (Optional[bool], optional): If True, the interpolation
|
|
346
|
+
is done again. Defaults to False.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
xr.Dataset: The interpolated data.
|
|
350
|
+
"""
|
|
351
|
+
if out_folder is None:
|
|
352
|
+
out_folder: pathlib = self.output_dir / "s2_07_smoothed"
|
|
353
|
+
|
|
354
|
+
# Apply the cloud removal
|
|
355
|
+
out_table = intermediate_process(
|
|
356
|
+
table=table,
|
|
357
|
+
out_folder=out_folder,
|
|
358
|
+
process_function=smooth_s2,
|
|
359
|
+
process_function_args=dict(
|
|
360
|
+
quiet=quiet,
|
|
361
|
+
smooth_w=smooth_w,
|
|
362
|
+
smooth_p=smooth_p,
|
|
363
|
+
device=device if device is not None else self.device
|
|
364
|
+
),
|
|
365
|
+
force=force,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Change the folder path
|
|
369
|
+
out_table["folder"] = out_folder
|
|
370
|
+
|
|
371
|
+
return out_table
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def super_s2(
|
|
375
|
+
self,
|
|
376
|
+
table: pd.DataFrame,
|
|
377
|
+
out_folder: Optional[pathlib.Path] = None,
|
|
378
|
+
quiet: Optional[bool] = False,
|
|
379
|
+
force: Optional[bool] = False,
|
|
380
|
+
) -> pd.DataFrame:
|
|
381
|
+
"""Superresolution to the Sentinel-2 image cube.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
table (pd.DataFrame): The table with the images to
|
|
385
|
+
superresolve.
|
|
386
|
+
out_folder (Optional[pathlib.Path], optional): The path to the
|
|
387
|
+
CSV file with the query table. Defaults to None.
|
|
388
|
+
quiet (Optional[bool], optional): If True, the messages
|
|
389
|
+
are not displayed. Defaults to False.
|
|
390
|
+
force (Optional[bool], optional): If True, the superresolution
|
|
391
|
+
is done again. Defaults to False.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
pd.DataFrame: The table with the superresolved images.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
if out_folder is None:
|
|
398
|
+
out_folder: pathlib.Path = self.output_dir / "s2_08_superresolution"
|
|
399
|
+
|
|
400
|
+
# Apply the Superresolution process
|
|
401
|
+
out_table = intermediate_process(
|
|
402
|
+
table=table,
|
|
403
|
+
out_folder=out_folder,
|
|
404
|
+
process_function=super_s2,
|
|
405
|
+
process_function_args=dict(
|
|
406
|
+
device=self.device,
|
|
407
|
+
sensor=self.sensor,
|
|
408
|
+
quiet=quiet
|
|
409
|
+
),
|
|
410
|
+
force=force,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Change the folder path
|
|
414
|
+
out_table["folder"] = out_folder
|
|
415
|
+
|
|
416
|
+
return out_table
|
|
417
|
+
|
|
418
|
+
def display_images(
|
|
419
|
+
self,
|
|
420
|
+
table: pd.DataFrame,
|
|
421
|
+
out_folder: Optional[pathlib.Path] = None,
|
|
422
|
+
bands: Optional[list[str]] = [2, 1, 0],
|
|
423
|
+
ratio: Optional[int] = 3000,
|
|
424
|
+
) -> pathlib.Path:
|
|
425
|
+
""" Display the images in the table.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
table (pd.DataFrame): The table with the images to display.
|
|
429
|
+
out_folder (Optional[pathlib.Path], optional): The path to the
|
|
430
|
+
CSV file with the query table. Defaults to None.
|
|
431
|
+
bands (Optional[list[str]], optional): The bands to display.
|
|
432
|
+
Defaults to [2, 1, 0].
|
|
433
|
+
ratio (Optional[int], optional): The ratio to divide the
|
|
434
|
+
image. Defaults to 3000. The larger the number, the
|
|
435
|
+
darker the image.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
pathlib.Path: The path to the folder with the
|
|
439
|
+
displayed images.
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
# Create the output folder
|
|
443
|
+
out_folder = (
|
|
444
|
+
self.output_dir / ("z_" + table["folder"].iloc[0].name + "_png")
|
|
445
|
+
)
|
|
446
|
+
out_folder.mkdir(exist_ok=True, parents=True)
|
|
447
|
+
|
|
448
|
+
return display_images(
|
|
449
|
+
table=table,
|
|
450
|
+
out_folder=out_folder,
|
|
451
|
+
bands=bands,
|
|
452
|
+
ratio=ratio,
|
|
453
|
+
)
|