RadGEEToolbox 1.6.3__py3-none-any.whl → 1.6.5__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.
- RadGEEToolbox/CollectionStitch.py +20 -10
- RadGEEToolbox/GetPalette.py +109 -15
- RadGEEToolbox/LandsatCollection.py +924 -359
- RadGEEToolbox/Sentinel1Collection.py +568 -292
- RadGEEToolbox/Sentinel2Collection.py +685 -258
- RadGEEToolbox/VisParams.py +83 -83
- RadGEEToolbox/__init__.py +2 -2
- {radgeetoolbox-1.6.3.dist-info → radgeetoolbox-1.6.5.dist-info}/METADATA +26 -7
- radgeetoolbox-1.6.5.dist-info/RECORD +12 -0
- radgeetoolbox-1.6.3.dist-info/RECORD +0 -12
- {radgeetoolbox-1.6.3.dist-info → radgeetoolbox-1.6.5.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.6.3.dist-info → radgeetoolbox-1.6.5.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.6.3.dist-info → radgeetoolbox-1.6.5.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import ee
|
|
2
2
|
import pandas as pd
|
|
3
3
|
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ---- Reflectance scaling for Sentinel-2 L2A (HARMONIZED) ----
|
|
7
|
+
_S2_SR_BANDS = ["B1","B2","B3","B4","B5","B6","B7","B8","B8A","B9","B10","B11","B12"]
|
|
8
|
+
_S2_SCALE = 0.0001 # offset 0.0
|
|
9
|
+
|
|
10
|
+
def _scale_s2_sr(img):
|
|
11
|
+
"""
|
|
12
|
+
Convert S2 L2A DN values to reflectance values for bands B1 through B12 (overwrites bands).
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
img (ee.Image): Input Sentinel-2 image without scaled bands.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
ee.Image: Image with scaled reflectance bands.
|
|
19
|
+
"""
|
|
20
|
+
img = ee.Image(img)
|
|
21
|
+
already = ee.String(img.get('rgt:scaled')).eq('sentinel2_sr')
|
|
22
|
+
scaled = img.select(_S2_SR_BANDS).multiply(_S2_SCALE)
|
|
23
|
+
scaled = img.addBands(scaled, None, True).set('rgt:scaled','sentinel2_sr')
|
|
24
|
+
return ee.Image(ee.Algorithms.If(already, img, scaled))
|
|
25
|
+
|
|
4
26
|
class Sentinel2Collection:
|
|
5
27
|
"""
|
|
6
28
|
Represents a user-defined collection of ESA Sentinel-2 MSI surface reflectance satellite images at 10 m/px from Google Earth Engine (GEE).
|
|
7
29
|
|
|
8
30
|
This class enables simplified definition, filtering, masking, and processing of multispectral Sentinel-2 imagery.
|
|
9
|
-
It supports multiple spatial and temporal filters, caching for efficient computation, and direct computation of
|
|
31
|
+
It supports multiple spatial and temporal filters, caching for efficient computation, and direct computation of
|
|
10
32
|
key spectral indices like NDWI, NDVI, halite index, and more. It also includes utilities for cloud masking,
|
|
11
33
|
mosaicking, zonal statistics, and transect analysis.
|
|
12
34
|
|
|
@@ -17,15 +39,16 @@ class Sentinel2Collection:
|
|
|
17
39
|
Args:
|
|
18
40
|
start_date (str): Start date in 'YYYY-MM-DD' format. Required unless `collection` is provided.
|
|
19
41
|
end_date (str): End date in 'YYYY-MM-DD' format. Required unless `collection` is provided.
|
|
20
|
-
tile (str or list): MGRS tile(s) of Sentinel image. Required unless `boundary`, `relative_orbit_number`, or `collection` is provided. The user is allowed to provide multiple tiles as list (note tile specifications will override boundary or orbits). See https://hls.gsfc.nasa.gov/products-description/tiling-system/
|
|
42
|
+
tile (str or list): MGRS tile(s) of Sentinel image. Required unless `boundary`, `relative_orbit_number`, or `collection` is provided. The user is allowed to provide multiple tiles as list (note tile specifications will override boundary or orbits). See https://hls.gsfc.nasa.gov/products-description/tiling-system/
|
|
21
43
|
cloud_percentage_threshold (int, optional): Max allowed cloud cover percentage. Defaults to 100.
|
|
22
44
|
nodata_threshold (int, optional): Integer percentage threshold where only imagery with nodata pixels encompassing a % less than the threshold will be provided (defaults to 100)
|
|
23
|
-
boundary (ee.Geometry, optional): A geometry for filtering to images that intersect with the boundary shape. Overrides `tile` if provided.
|
|
24
|
-
relative_orbit_number (int or list, optional): Relative orbit number(s) to filter collection. Provide multiple values as list
|
|
45
|
+
boundary (ee.Geometry, optional): A geometry for filtering to images that intersect with the boundary shape. Overrides `tile` if provided.
|
|
46
|
+
relative_orbit_number (int or list, optional): Relative orbit number(s) to filter collection. Provide multiple values as list
|
|
25
47
|
collection (ee.ImageCollection, optional): A pre-filtered Sentinel-2 ee.ImageCollection object to be converted to a Sentinel2Collection object. Overrides all other filters.
|
|
48
|
+
scale_bands (bool, optional): If True, all SR bands will be scaled from DN values to reflectance values. Defaults to False.
|
|
26
49
|
|
|
27
50
|
Attributes:
|
|
28
|
-
collection (ee.ImageCollection): The filtered or user-supplied image collection converted to an ee.ImageCollection object.
|
|
51
|
+
collection (ee.ImageCollection): The filtered or user-supplied image collection converted to an ee.ImageCollection object.
|
|
29
52
|
|
|
30
53
|
Raises:
|
|
31
54
|
ValueError: Raised if required filter parameters are missing, or if both `collection` and other filters are provided.
|
|
@@ -33,14 +56,14 @@ class Sentinel2Collection:
|
|
|
33
56
|
Note:
|
|
34
57
|
See full usage examples in the documentation or notebooks:
|
|
35
58
|
https://github.com/radwinskis/RadGEEToolbox/tree/main/Example%20Notebooks
|
|
36
|
-
|
|
59
|
+
|
|
37
60
|
Examples:
|
|
38
61
|
>>> from RadGEEToolbox import Sentinel2Collection
|
|
39
62
|
>>> import ee
|
|
40
63
|
>>> ee.Initialize()
|
|
41
64
|
>>> image_collection = Sentinel2Collection(
|
|
42
|
-
... start_date='2023-06-01',
|
|
43
|
-
... end_date='2023-06-30',
|
|
65
|
+
... start_date='2023-06-01',
|
|
66
|
+
... end_date='2023-06-30',
|
|
44
67
|
... tile=['12TUL', '12TUM', '12TUN'],
|
|
45
68
|
... cloud_percentage_threshold=20,
|
|
46
69
|
... nodata_threshold=10,
|
|
@@ -51,11 +74,31 @@ class Sentinel2Collection:
|
|
|
51
74
|
>>> ndwi_collection = cloud_masked.ndwi #calculate ndwi for all images
|
|
52
75
|
"""
|
|
53
76
|
|
|
54
|
-
def __init__(
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
start_date=None,
|
|
80
|
+
end_date=None,
|
|
81
|
+
tile=None,
|
|
82
|
+
cloud_percentage_threshold=None,
|
|
83
|
+
nodata_threshold=None,
|
|
84
|
+
boundary=None,
|
|
85
|
+
relative_orbit_number=None,
|
|
86
|
+
collection=None,
|
|
87
|
+
scale_bands=False,
|
|
88
|
+
):
|
|
55
89
|
if collection is None and (start_date is None or end_date is None):
|
|
56
|
-
raise ValueError(
|
|
57
|
-
|
|
58
|
-
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"Either provide all required fields (start_date, end_date, tile, cloud_percentage_threshold, nodata_threshold) or provide a collection."
|
|
92
|
+
)
|
|
93
|
+
if (
|
|
94
|
+
tile is None
|
|
95
|
+
and boundary is None
|
|
96
|
+
and relative_orbit_number is None
|
|
97
|
+
and collection is None
|
|
98
|
+
):
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"Provide either tile, boundary/geometry, or relative orbit number specifications to filter the image collection"
|
|
101
|
+
)
|
|
59
102
|
if collection is None:
|
|
60
103
|
self.start_date = start_date
|
|
61
104
|
self.end_date = end_date
|
|
@@ -96,17 +139,19 @@ class Sentinel2Collection:
|
|
|
96
139
|
self.collection = self.get_orbit_and_boundary_filtered_collection()
|
|
97
140
|
else:
|
|
98
141
|
self.collection = collection
|
|
142
|
+
if scale_bands:
|
|
143
|
+
self.collection = self.collection.map(_scale_s2_sr)
|
|
99
144
|
|
|
100
145
|
self._dates_list = None
|
|
101
146
|
self._dates = None
|
|
102
147
|
self.ndwi_threshold = -1
|
|
148
|
+
self.mndwi_threshold = -1
|
|
103
149
|
self.ndvi_threshold = -1
|
|
104
150
|
self.halite_threshold = -1
|
|
105
151
|
self.gypsum_threshold = -1
|
|
106
152
|
self.turbidity_threshold = -1
|
|
107
153
|
self.chlorophyll_threshold = 0.5
|
|
108
|
-
|
|
109
|
-
|
|
154
|
+
|
|
110
155
|
self._geometry_masked_collection = None
|
|
111
156
|
self._geometry_masked_out_collection = None
|
|
112
157
|
self._masked_clouds_collection = None
|
|
@@ -117,6 +162,7 @@ class Sentinel2Collection:
|
|
|
117
162
|
self._max = None
|
|
118
163
|
self._min = None
|
|
119
164
|
self._ndwi = None
|
|
165
|
+
self._mndwi = None
|
|
120
166
|
self._ndvi = None
|
|
121
167
|
self._halite = None
|
|
122
168
|
self._gypsum = None
|
|
@@ -124,36 +170,64 @@ class Sentinel2Collection:
|
|
|
124
170
|
self._chlorophyll = None
|
|
125
171
|
self._MosaicByDate = None
|
|
126
172
|
self._PixelAreaSumCollection = None
|
|
173
|
+
self._Reflectance = None
|
|
127
174
|
|
|
128
175
|
@staticmethod
|
|
129
176
|
def image_dater(image):
|
|
130
177
|
"""
|
|
131
178
|
Adds date to image properties as 'Date_Filter'.
|
|
132
179
|
|
|
133
|
-
Args:
|
|
180
|
+
Args:
|
|
134
181
|
image (ee.Image): Input image
|
|
135
182
|
|
|
136
|
-
Returns:
|
|
183
|
+
Returns:
|
|
137
184
|
ee.Image: Image with date in properties.
|
|
138
185
|
"""
|
|
139
|
-
date = ee.Number(image.date().format(
|
|
140
|
-
return image.set({
|
|
141
|
-
|
|
142
|
-
|
|
186
|
+
date = ee.Number(image.date().format("YYYY-MM-dd"))
|
|
187
|
+
return image.set({"Date_Filter": date})
|
|
188
|
+
|
|
143
189
|
@staticmethod
|
|
144
190
|
def sentinel_ndwi_fn(image, threshold):
|
|
145
191
|
"""
|
|
146
192
|
Calculates ndwi from GREEN and NIR bands (McFeeters, 1996 - https://doi.org/10.1080/01431169608948714) for Sentinel2 imagery and masks image based on threshold.
|
|
147
193
|
|
|
148
|
-
Args:
|
|
194
|
+
Args:
|
|
149
195
|
image (ee.Image): input image
|
|
150
196
|
threshold (float): value between -1 and 1 where pixels less than threshold will be masked.
|
|
151
197
|
|
|
152
198
|
Returns:
|
|
153
199
|
ee.Image: ndwi ee.Image
|
|
154
200
|
"""
|
|
155
|
-
ndwi_calc = image.normalizedDifference(
|
|
156
|
-
|
|
201
|
+
ndwi_calc = image.normalizedDifference(
|
|
202
|
+
["B3", "B8"]
|
|
203
|
+
) # green-NIR / green+NIR -- full NDWI image
|
|
204
|
+
water = (
|
|
205
|
+
ndwi_calc.updateMask(ndwi_calc.gte(threshold))
|
|
206
|
+
.rename("ndwi")
|
|
207
|
+
.copyProperties(image)
|
|
208
|
+
)
|
|
209
|
+
return water
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def sentinel_mndwi_fn(image, threshold):
|
|
213
|
+
"""
|
|
214
|
+
Calculates mndwi from GREEN and SWIR bands for Sentinel-2 imagery and masks image based on threshold.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
image (ee.Image): input image
|
|
218
|
+
threshold (float): value between -1 and 1 where pixels less than threshold will be masked.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
ee.Image: mndwi ee.Image
|
|
222
|
+
"""
|
|
223
|
+
mndwi_calc = image.normalizedDifference(
|
|
224
|
+
["B3", "B11"]
|
|
225
|
+
) # green-SWIR / green+SWIR -- full MNDWI image
|
|
226
|
+
water = (
|
|
227
|
+
mndwi_calc.updateMask(mndwi_calc.gte(threshold))
|
|
228
|
+
.rename("mndwi")
|
|
229
|
+
.copyProperties(image)
|
|
230
|
+
)
|
|
157
231
|
return water
|
|
158
232
|
|
|
159
233
|
@staticmethod
|
|
@@ -168,8 +242,14 @@ class Sentinel2Collection:
|
|
|
168
242
|
Returns:
|
|
169
243
|
ee.Image: ndvi ee.Image
|
|
170
244
|
"""
|
|
171
|
-
ndvi_calc = image.normalizedDifference(
|
|
172
|
-
|
|
245
|
+
ndvi_calc = image.normalizedDifference(
|
|
246
|
+
["B8", "B4"]
|
|
247
|
+
) # NIR-RED/NIR+RED -- full NDVI image
|
|
248
|
+
vegetation = (
|
|
249
|
+
ndvi_calc.updateMask(ndvi_calc.gte(threshold))
|
|
250
|
+
.rename("ndvi")
|
|
251
|
+
.copyProperties(image)
|
|
252
|
+
) # subsets the image to just water pixels, 0.2 threshold for datasets
|
|
173
253
|
return vegetation
|
|
174
254
|
|
|
175
255
|
@staticmethod
|
|
@@ -184,8 +264,12 @@ class Sentinel2Collection:
|
|
|
184
264
|
Returns:
|
|
185
265
|
ee.Image: halite ee.Image
|
|
186
266
|
"""
|
|
187
|
-
halite_index = image.normalizedDifference([
|
|
188
|
-
halite =
|
|
267
|
+
halite_index = image.normalizedDifference(["B4", "B11"])
|
|
268
|
+
halite = (
|
|
269
|
+
halite_index.updateMask(halite_index.gte(threshold))
|
|
270
|
+
.rename("halite")
|
|
271
|
+
.copyProperties(image)
|
|
272
|
+
)
|
|
189
273
|
return halite
|
|
190
274
|
|
|
191
275
|
@staticmethod
|
|
@@ -200,10 +284,14 @@ class Sentinel2Collection:
|
|
|
200
284
|
Returns:
|
|
201
285
|
ee.Image: gypsum ee.Image
|
|
202
286
|
"""
|
|
203
|
-
gypsum_index = image.normalizedDifference([
|
|
204
|
-
gypsum =
|
|
287
|
+
gypsum_index = image.normalizedDifference(["B11", "B12"])
|
|
288
|
+
gypsum = (
|
|
289
|
+
gypsum_index.updateMask(gypsum_index.gte(threshold))
|
|
290
|
+
.rename("gypsum")
|
|
291
|
+
.copyProperties(image)
|
|
292
|
+
)
|
|
205
293
|
return gypsum
|
|
206
|
-
|
|
294
|
+
|
|
207
295
|
@staticmethod
|
|
208
296
|
def sentinel_turbidity_fn(image, threshold):
|
|
209
297
|
"""
|
|
@@ -216,10 +304,12 @@ class Sentinel2Collection:
|
|
|
216
304
|
Returns:
|
|
217
305
|
ee.Image: turbidity ee.Image
|
|
218
306
|
"""
|
|
219
|
-
NDTI = image.normalizedDifference([
|
|
220
|
-
turbidity =
|
|
307
|
+
NDTI = image.normalizedDifference(["B3", "B2"])
|
|
308
|
+
turbidity = (
|
|
309
|
+
NDTI.updateMask(NDTI.gte(threshold)).rename("ndti").copyProperties(image)
|
|
310
|
+
)
|
|
221
311
|
return turbidity
|
|
222
|
-
|
|
312
|
+
|
|
223
313
|
@staticmethod
|
|
224
314
|
def sentinel_chlorophyll_fn(image, threshold):
|
|
225
315
|
"""
|
|
@@ -232,10 +322,14 @@ class Sentinel2Collection:
|
|
|
232
322
|
Returns:
|
|
233
323
|
ee.Image: chlorophyll-a ee.Image
|
|
234
324
|
"""
|
|
235
|
-
chl_index = image.normalizedDifference([
|
|
236
|
-
chlorophyll =
|
|
325
|
+
chl_index = image.normalizedDifference(["B5", "B4"])
|
|
326
|
+
chlorophyll = (
|
|
327
|
+
chl_index.updateMask(chl_index.gte(threshold))
|
|
328
|
+
.rename("2BDA")
|
|
329
|
+
.copyProperties(image)
|
|
330
|
+
)
|
|
237
331
|
return chlorophyll
|
|
238
|
-
|
|
332
|
+
|
|
239
333
|
@staticmethod
|
|
240
334
|
def MaskCloudsS2(image):
|
|
241
335
|
"""
|
|
@@ -247,10 +341,10 @@ class Sentinel2Collection:
|
|
|
247
341
|
Returns:
|
|
248
342
|
ee.Image: output ee.Image with clouds masked
|
|
249
343
|
"""
|
|
250
|
-
SCL = image.select(
|
|
344
|
+
SCL = image.select("SCL")
|
|
251
345
|
CloudMask = SCL.neq(9)
|
|
252
346
|
return image.updateMask(CloudMask).copyProperties(image)
|
|
253
|
-
|
|
347
|
+
|
|
254
348
|
@staticmethod
|
|
255
349
|
def MaskWaterS2(image):
|
|
256
350
|
"""
|
|
@@ -262,27 +356,29 @@ class Sentinel2Collection:
|
|
|
262
356
|
Returns:
|
|
263
357
|
ee.Image: output ee.Image with water pixels masked
|
|
264
358
|
"""
|
|
265
|
-
SCL = image.select(
|
|
359
|
+
SCL = image.select("SCL")
|
|
266
360
|
WaterMask = SCL.neq(6)
|
|
267
361
|
return image.updateMask(WaterMask).copyProperties(image)
|
|
268
|
-
|
|
362
|
+
|
|
269
363
|
@staticmethod
|
|
270
364
|
def MaskWaterS2ByNDWI(image, threshold):
|
|
271
365
|
"""
|
|
272
366
|
Function to mask water pixels (mask land and cloud pixels) for all bands based on NDWI and a set threshold where
|
|
273
367
|
all pixels less than NDWI threshold are masked out.
|
|
274
368
|
|
|
275
|
-
Args:
|
|
369
|
+
Args:
|
|
276
370
|
image (ee.Image): input image
|
|
277
371
|
threshold (float): value between -1 and 1 where pixels less than threshold will be masked.
|
|
278
372
|
|
|
279
373
|
Returns:
|
|
280
374
|
ee.Image: ee.Image
|
|
281
375
|
"""
|
|
282
|
-
ndwi_calc = image.normalizedDifference(
|
|
283
|
-
|
|
376
|
+
ndwi_calc = image.normalizedDifference(
|
|
377
|
+
["B3", "B8"]
|
|
378
|
+
) # green-NIR / green+NIR -- full NDWI image
|
|
379
|
+
water = image.updateMask(ndwi_calc.lt(threshold))
|
|
284
380
|
return water
|
|
285
|
-
|
|
381
|
+
|
|
286
382
|
@staticmethod
|
|
287
383
|
def MaskToWaterS2(image):
|
|
288
384
|
"""
|
|
@@ -294,10 +390,10 @@ class Sentinel2Collection:
|
|
|
294
390
|
Returns:
|
|
295
391
|
ee.Image: output ee.Image with all but water pixels masked
|
|
296
392
|
"""
|
|
297
|
-
SCL = image.select(
|
|
393
|
+
SCL = image.select("SCL")
|
|
298
394
|
WaterMask = SCL.eq(6)
|
|
299
395
|
return image.updateMask(WaterMask).copyProperties(image)
|
|
300
|
-
|
|
396
|
+
|
|
301
397
|
@staticmethod
|
|
302
398
|
def halite_mask(image, threshold):
|
|
303
399
|
"""
|
|
@@ -306,54 +402,63 @@ class Sentinel2Collection:
|
|
|
306
402
|
Args:
|
|
307
403
|
image (ee.Image): input image
|
|
308
404
|
threshold (float): value between -1 and 1 where pixels less than threshold will be masked..
|
|
309
|
-
|
|
405
|
+
|
|
310
406
|
Returns:
|
|
311
407
|
ee.Image: ee.Image where halite pixels are masked (image without halite pixels).
|
|
312
408
|
"""
|
|
313
|
-
halite_index = image.normalizedDifference([
|
|
409
|
+
halite_index = image.normalizedDifference(["B4", "B11"])
|
|
314
410
|
mask = image.updateMask(halite_index.lt(threshold)).copyProperties(image)
|
|
315
|
-
return mask
|
|
316
|
-
|
|
411
|
+
return mask
|
|
412
|
+
|
|
317
413
|
@staticmethod
|
|
318
414
|
def gypsum_and_halite_mask(image, halite_threshold, gypsum_threshold):
|
|
319
415
|
"""
|
|
320
|
-
Function to mask both gypsum and halite pixels. Must specify threshold for isolating halite and gypsum pixels.
|
|
416
|
+
Function to mask both gypsum and halite pixels. Must specify threshold for isolating halite and gypsum pixels.
|
|
321
417
|
|
|
322
418
|
Args:
|
|
323
419
|
image (ee.Image): input image
|
|
324
420
|
halite_threshold: integer threshold for halite where pixels less than threshold are masked.
|
|
325
421
|
gypsum_threshold: integer threshold for gypsum where pixels less than threshold are masked.
|
|
326
|
-
|
|
422
|
+
|
|
327
423
|
Returns:
|
|
328
424
|
ee.Image: ee.Image where gypsum and halite pixels are masked (image without halite or gypsum pixels).
|
|
329
425
|
"""
|
|
330
|
-
halite_index = image.normalizedDifference([
|
|
331
|
-
gypsum_index = image.normalizedDifference([
|
|
332
|
-
|
|
333
|
-
mask =
|
|
426
|
+
halite_index = image.normalizedDifference(["B4", "B11"])
|
|
427
|
+
gypsum_index = image.normalizedDifference(["B11", "B12"])
|
|
428
|
+
|
|
429
|
+
mask = (
|
|
430
|
+
gypsum_index.updateMask(halite_index.lt(halite_threshold))
|
|
431
|
+
.updateMask(gypsum_index.lt(gypsum_threshold))
|
|
432
|
+
.rename("carbonate_muds")
|
|
433
|
+
.copyProperties(image)
|
|
434
|
+
)
|
|
334
435
|
return mask
|
|
335
|
-
|
|
436
|
+
|
|
336
437
|
@staticmethod
|
|
337
438
|
def MaskToWaterS2ByNDWI(image, threshold):
|
|
338
439
|
"""
|
|
339
440
|
Function to mask all bands to water pixels (mask land and cloud pixels) based on NDWI.
|
|
340
441
|
|
|
341
|
-
Args:
|
|
442
|
+
Args:
|
|
342
443
|
image (ee.Image): input image
|
|
343
444
|
threshold (float): value between -1 and 1 where pixels less than threshold will be masked.
|
|
344
445
|
|
|
345
446
|
Returns:
|
|
346
447
|
ee.Image: ee.Image image
|
|
347
448
|
"""
|
|
348
|
-
ndwi_calc = image.normalizedDifference(
|
|
349
|
-
|
|
449
|
+
ndwi_calc = image.normalizedDifference(
|
|
450
|
+
["B3", "B8"]
|
|
451
|
+
) # green-NIR / green+NIR -- full NDWI image
|
|
452
|
+
water = image.updateMask(ndwi_calc.gte(threshold))
|
|
350
453
|
return water
|
|
351
|
-
|
|
454
|
+
|
|
352
455
|
@staticmethod
|
|
353
|
-
def PixelAreaSum(
|
|
456
|
+
def PixelAreaSum(
|
|
457
|
+
image, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12
|
|
458
|
+
):
|
|
354
459
|
"""
|
|
355
460
|
Calculates the summation of area for pixels of interest (above a specific threshold) within a geometry and store the value as image property (matching name of chosen band).
|
|
356
|
-
The resulting value has units of square meters.
|
|
461
|
+
The resulting value has units of square meters.
|
|
357
462
|
|
|
358
463
|
Args:
|
|
359
464
|
image (ee.Image): input ee.Image
|
|
@@ -362,26 +467,36 @@ class Sentinel2Collection:
|
|
|
362
467
|
threshold: integer threshold to specify masking of pixels below threshold (defaults to -1).
|
|
363
468
|
scale: integer scale of image resolution (meters) (defaults to 10).
|
|
364
469
|
maxPixels: integer denoting maximum number of pixels for calculations.
|
|
365
|
-
|
|
470
|
+
|
|
366
471
|
Returns:
|
|
367
472
|
ee.Image: Image with area calculation stored as property matching name of band.
|
|
368
473
|
"""
|
|
369
474
|
area_image = ee.Image.pixelArea()
|
|
370
475
|
mask = image.select(band_name).gte(threshold)
|
|
371
476
|
final = image.addBands(area_image)
|
|
372
|
-
stats =
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
477
|
+
stats = (
|
|
478
|
+
final.select("area")
|
|
479
|
+
.updateMask(mask)
|
|
480
|
+
.rename(band_name)
|
|
481
|
+
.reduceRegion(
|
|
482
|
+
reducer=ee.Reducer.sum(),
|
|
483
|
+
geometry=geometry,
|
|
484
|
+
scale=scale,
|
|
485
|
+
maxPixels=maxPixels,
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
return image.set(
|
|
489
|
+
band_name, stats.get(band_name)
|
|
490
|
+
) # calculates and returns summed pixel area as image property titled the same as the band name of the band used for calculation
|
|
491
|
+
|
|
492
|
+
def PixelAreaSumCollection(
|
|
493
|
+
self, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12
|
|
494
|
+
):
|
|
380
495
|
"""
|
|
381
|
-
Calculates the summation of area for pixels of interest (above a specific threshold)
|
|
496
|
+
Calculates the summation of area for pixels of interest (above a specific threshold)
|
|
382
497
|
within a geometry and store the value as image property (matching name of chosen band) for an entire
|
|
383
498
|
image collection.
|
|
384
|
-
The resulting value has units of square meters.
|
|
499
|
+
The resulting value has units of square meters.
|
|
385
500
|
|
|
386
501
|
Args:
|
|
387
502
|
band_name: name of band (string) for calculating area.
|
|
@@ -389,13 +504,22 @@ class Sentinel2Collection:
|
|
|
389
504
|
threshold: integer threshold to specify masking of pixels below threshold (defaults to -1).
|
|
390
505
|
scale: integer scale of image resolution (meters) (defaults to 10).
|
|
391
506
|
maxPixels: integer denoting maximum number of pixels for calculations.
|
|
392
|
-
|
|
507
|
+
|
|
393
508
|
Returns:
|
|
394
509
|
ee.Image: Image with area calculation stored as property matching name of band.
|
|
395
510
|
"""
|
|
396
511
|
if self._PixelAreaSumCollection is None:
|
|
397
512
|
collection = self.collection
|
|
398
|
-
AreaCollection = collection.map(
|
|
513
|
+
AreaCollection = collection.map(
|
|
514
|
+
lambda image: Sentinel2Collection.PixelAreaSum(
|
|
515
|
+
image,
|
|
516
|
+
band_name=band_name,
|
|
517
|
+
geometry=geometry,
|
|
518
|
+
threshold=threshold,
|
|
519
|
+
scale=scale,
|
|
520
|
+
maxPixels=maxPixels,
|
|
521
|
+
)
|
|
522
|
+
)
|
|
399
523
|
self._PixelAreaSumCollection = AreaCollection
|
|
400
524
|
return self._PixelAreaSumCollection
|
|
401
525
|
|
|
@@ -408,7 +532,7 @@ class Sentinel2Collection:
|
|
|
408
532
|
ee.List: Server-side ee.List of dates.
|
|
409
533
|
"""
|
|
410
534
|
if self._dates_list is None:
|
|
411
|
-
dates = self.collection.aggregate_array(
|
|
535
|
+
dates = self.collection.aggregate_array("Date_Filter")
|
|
412
536
|
self._dates_list = dates
|
|
413
537
|
return self._dates_list
|
|
414
538
|
|
|
@@ -421,7 +545,7 @@ class Sentinel2Collection:
|
|
|
421
545
|
list: list of date strings.
|
|
422
546
|
"""
|
|
423
547
|
if self._dates_list is None:
|
|
424
|
-
dates = self.collection.aggregate_array(
|
|
548
|
+
dates = self.collection.aggregate_array("Date_Filter")
|
|
425
549
|
self._dates_list = dates
|
|
426
550
|
if self._dates is None:
|
|
427
551
|
dates = self._dates_list.getInfo()
|
|
@@ -436,10 +560,20 @@ class Sentinel2Collection:
|
|
|
436
560
|
ee.ImageCollection: Image collection objects
|
|
437
561
|
"""
|
|
438
562
|
sentinel2 = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
|
|
439
|
-
filtered_collection =
|
|
440
|
-
|
|
563
|
+
filtered_collection = (
|
|
564
|
+
sentinel2.filterDate(self.start_date, self.end_date)
|
|
565
|
+
.filter(ee.Filter.inList("MGRS_TILE", self.tile))
|
|
566
|
+
.filter(ee.Filter.lte("NODATA_PIXEL_PERCENTAGE", self.nodata_threshold))
|
|
567
|
+
.filter(
|
|
568
|
+
ee.Filter.lte(
|
|
569
|
+
"CLOUDY_PIXEL_PERCENTAGE", self.cloud_percentage_threshold
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
.map(Sentinel2Collection.image_dater)
|
|
573
|
+
.sort("Date_Filter")
|
|
574
|
+
)
|
|
441
575
|
return filtered_collection
|
|
442
|
-
|
|
576
|
+
|
|
443
577
|
def get_boundary_filtered_collection(self):
|
|
444
578
|
"""
|
|
445
579
|
Function to filter image collection using a geometry/boundary rather than list of tiles (based on Sentinel2Collection class arguments).
|
|
@@ -449,10 +583,20 @@ class Sentinel2Collection:
|
|
|
449
583
|
|
|
450
584
|
"""
|
|
451
585
|
sentinel2 = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
|
|
452
|
-
filtered_collection =
|
|
453
|
-
|
|
586
|
+
filtered_collection = (
|
|
587
|
+
sentinel2.filterDate(self.start_date, self.end_date)
|
|
588
|
+
.filterBounds(self.boundary)
|
|
589
|
+
.filter(ee.Filter.lte("NODATA_PIXEL_PERCENTAGE", self.nodata_threshold))
|
|
590
|
+
.filter(
|
|
591
|
+
ee.Filter.lte(
|
|
592
|
+
"CLOUDY_PIXEL_PERCENTAGE", self.cloud_percentage_threshold
|
|
593
|
+
)
|
|
594
|
+
)
|
|
595
|
+
.map(Sentinel2Collection.image_dater)
|
|
596
|
+
.sort("Date_Filter")
|
|
597
|
+
)
|
|
454
598
|
return filtered_collection
|
|
455
|
-
|
|
599
|
+
|
|
456
600
|
def get_orbit_filtered_collection(self):
|
|
457
601
|
"""
|
|
458
602
|
Function to filter image collection a list of relative orbit numbers rather than list of tiles (based on Sentinel2Collection class arguments).
|
|
@@ -461,10 +605,22 @@ class Sentinel2Collection:
|
|
|
461
605
|
ee.ImageCollection: Image collection objects
|
|
462
606
|
"""
|
|
463
607
|
sentinel2 = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
|
|
464
|
-
filtered_collection =
|
|
465
|
-
|
|
608
|
+
filtered_collection = (
|
|
609
|
+
sentinel2.filterDate(self.start_date, self.end_date)
|
|
610
|
+
.filter(
|
|
611
|
+
ee.Filter.inList("SENSING_ORBIT_NUMBER", self.relative_orbit_number)
|
|
612
|
+
)
|
|
613
|
+
.filter(ee.Filter.lte("NODATA_PIXEL_PERCENTAGE", self.nodata_threshold))
|
|
614
|
+
.filter(
|
|
615
|
+
ee.Filter.lte(
|
|
616
|
+
"CLOUDY_PIXEL_PERCENTAGE", self.cloud_percentage_threshold
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
.map(Sentinel2Collection.image_dater)
|
|
620
|
+
.sort("Date_Filter")
|
|
621
|
+
)
|
|
466
622
|
return filtered_collection
|
|
467
|
-
|
|
623
|
+
|
|
468
624
|
def get_orbit_and_boundary_filtered_collection(self):
|
|
469
625
|
"""
|
|
470
626
|
Function to filter image collection a list of relative orbit numbers and geometry/boundary rather than list of tiles (based on Sentinel2Collection class arguments).
|
|
@@ -473,10 +629,36 @@ class Sentinel2Collection:
|
|
|
473
629
|
ee.ImageCollection: Image collection objects
|
|
474
630
|
"""
|
|
475
631
|
sentinel2 = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
|
|
476
|
-
filtered_collection =
|
|
477
|
-
|
|
632
|
+
filtered_collection = (
|
|
633
|
+
sentinel2.filterDate(self.start_date, self.end_date)
|
|
634
|
+
.filter(
|
|
635
|
+
ee.Filter.inList("SENSING_ORBIT_NUMBER", self.relative_orbit_number)
|
|
636
|
+
)
|
|
637
|
+
.filterBounds(self.boundary)
|
|
638
|
+
.filter(ee.Filter.lte("NODATA_PIXEL_PERCENTAGE", self.nodata_threshold))
|
|
639
|
+
.filter(
|
|
640
|
+
ee.Filter.lte(
|
|
641
|
+
"CLOUDY_PIXEL_PERCENTAGE", self.cloud_percentage_threshold
|
|
642
|
+
)
|
|
643
|
+
)
|
|
644
|
+
.map(Sentinel2Collection.image_dater)
|
|
645
|
+
.sort("Date_Filter")
|
|
646
|
+
)
|
|
478
647
|
return filtered_collection
|
|
479
648
|
|
|
649
|
+
@property
|
|
650
|
+
def scale_to_reflectance(self):
|
|
651
|
+
"""
|
|
652
|
+
Scales each band in the Sentinel-2 collection from DN values to surface reflectance values.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Sentinel2Collection: A new Sentinel2Collection object with bands scaled to reflectance.
|
|
656
|
+
"""
|
|
657
|
+
if self._Reflectance is None:
|
|
658
|
+
self._Reflectance = self.collection.map(_scale_s2_sr)
|
|
659
|
+
return Sentinel2Collection(collection=self._Reflectance)
|
|
660
|
+
|
|
661
|
+
|
|
480
662
|
@property
|
|
481
663
|
def median(self):
|
|
482
664
|
"""
|
|
@@ -489,7 +671,7 @@ class Sentinel2Collection:
|
|
|
489
671
|
col = self.collection.median()
|
|
490
672
|
self._median = col
|
|
491
673
|
return self._median
|
|
492
|
-
|
|
674
|
+
|
|
493
675
|
@property
|
|
494
676
|
def mean(self):
|
|
495
677
|
"""
|
|
@@ -503,7 +685,7 @@ class Sentinel2Collection:
|
|
|
503
685
|
col = self.collection.mean()
|
|
504
686
|
self._mean = col
|
|
505
687
|
return self._mean
|
|
506
|
-
|
|
688
|
+
|
|
507
689
|
@property
|
|
508
690
|
def max(self):
|
|
509
691
|
"""
|
|
@@ -516,12 +698,12 @@ class Sentinel2Collection:
|
|
|
516
698
|
col = self.collection.max()
|
|
517
699
|
self._max = col
|
|
518
700
|
return self._max
|
|
519
|
-
|
|
701
|
+
|
|
520
702
|
@property
|
|
521
703
|
def min(self):
|
|
522
704
|
"""
|
|
523
705
|
Calculates min image from image collection. Results are calculated once per class object then cached for future use.
|
|
524
|
-
|
|
706
|
+
|
|
525
707
|
Returns:
|
|
526
708
|
ee.Image: min image from entire collection.
|
|
527
709
|
"""
|
|
@@ -533,9 +715,9 @@ class Sentinel2Collection:
|
|
|
533
715
|
@property
|
|
534
716
|
def ndwi(self):
|
|
535
717
|
"""
|
|
536
|
-
Property attribute to calculate and access the NDWI (Normalized Difference Water Index) imagery of the Sentinel2Collection.
|
|
537
|
-
This property initiates the calculation of NDWI using a default threshold of -1 (or a previously set threshold of self.ndwi_threshold)
|
|
538
|
-
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
718
|
+
Property attribute to calculate and access the NDWI (Normalized Difference Water Index) imagery of the Sentinel2Collection.
|
|
719
|
+
This property initiates the calculation of NDWI using a default threshold of -1 (or a previously set threshold of self.ndwi_threshold)
|
|
720
|
+
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
539
721
|
on subsequent accesses.
|
|
540
722
|
|
|
541
723
|
Returns:
|
|
@@ -544,6 +726,21 @@ class Sentinel2Collection:
|
|
|
544
726
|
if self._ndwi is None:
|
|
545
727
|
self._ndwi = self.ndwi_collection(self.ndwi_threshold)
|
|
546
728
|
return self._ndwi
|
|
729
|
+
|
|
730
|
+
@property
|
|
731
|
+
def mndwi(self):
|
|
732
|
+
"""
|
|
733
|
+
Property attribute to calculate and access the MNDWI (Modified Normalized Difference Water Index) imagery of the Sentinel2Collection.
|
|
734
|
+
This property initiates the calculation of MNDWI using a default threshold of -1 (or a previously set threshold of self.mndwi_threshold)
|
|
735
|
+
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
736
|
+
on subsequent accesses.
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
Sentinel2Collection: A Sentinel2Collection image collection.
|
|
740
|
+
"""
|
|
741
|
+
if self._mndwi is None:
|
|
742
|
+
self._mndwi = self.mndwi_collection(self.mndwi_threshold)
|
|
743
|
+
return self._mndwi
|
|
547
744
|
|
|
548
745
|
def ndwi_collection(self, threshold):
|
|
549
746
|
"""
|
|
@@ -558,19 +755,47 @@ class Sentinel2Collection:
|
|
|
558
755
|
first_image = self.collection.first()
|
|
559
756
|
available_bands = first_image.bandNames()
|
|
560
757
|
|
|
561
|
-
if available_bands.contains(
|
|
758
|
+
if available_bands.contains("B3") and available_bands.contains("B8"):
|
|
562
759
|
pass
|
|
563
760
|
else:
|
|
564
761
|
raise ValueError("Insufficient Bands for ndwi calculation")
|
|
565
|
-
col =
|
|
762
|
+
col = self.collection.map(
|
|
763
|
+
lambda image: Sentinel2Collection.sentinel_ndwi_fn(
|
|
764
|
+
image, threshold=threshold
|
|
765
|
+
)
|
|
766
|
+
)
|
|
566
767
|
return Sentinel2Collection(collection=col)
|
|
567
|
-
|
|
768
|
+
|
|
769
|
+
def mndwi_collection(self, threshold):
|
|
770
|
+
"""
|
|
771
|
+
Calculates mndwi and return collection as class object. Masks collection based on threshold which defaults to -1.
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
threshold: specify threshold for MNDWI function (values less than threshold are masked).
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
Sentinel2Collection: Sentinel2Collection image collection.
|
|
778
|
+
"""
|
|
779
|
+
first_image = self.collection.first()
|
|
780
|
+
available_bands = first_image.bandNames()
|
|
781
|
+
|
|
782
|
+
if available_bands.contains("B3") and available_bands.contains("B11"):
|
|
783
|
+
pass
|
|
784
|
+
else:
|
|
785
|
+
raise ValueError("Insufficient Bands for mndwi calculation")
|
|
786
|
+
col = self.collection.map(
|
|
787
|
+
lambda image: Sentinel2Collection.sentinel_mndwi_fn(
|
|
788
|
+
image, threshold=threshold
|
|
789
|
+
)
|
|
790
|
+
)
|
|
791
|
+
return Sentinel2Collection(collection=col)
|
|
792
|
+
|
|
568
793
|
@property
|
|
569
794
|
def ndvi(self):
|
|
570
795
|
"""
|
|
571
|
-
Property attribute to calculate and access the NDVI (Normalized Difference Vegetation Index) imagery of the Sentinel2Collection.
|
|
572
|
-
This property initiates the calculation of NDVI using a default threshold of -1 (or a previously set threshold of self.ndvi_threshold)
|
|
573
|
-
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
796
|
+
Property attribute to calculate and access the NDVI (Normalized Difference Vegetation Index) imagery of the Sentinel2Collection.
|
|
797
|
+
This property initiates the calculation of NDVI using a default threshold of -1 (or a previously set threshold of self.ndvi_threshold)
|
|
798
|
+
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
574
799
|
on subsequent accesses.
|
|
575
800
|
|
|
576
801
|
Returns:
|
|
@@ -579,7 +804,7 @@ class Sentinel2Collection:
|
|
|
579
804
|
if self._ndvi is None:
|
|
580
805
|
self._ndvi = self.ndvi_collection(self.ndvi_threshold)
|
|
581
806
|
return self._ndvi
|
|
582
|
-
|
|
807
|
+
|
|
583
808
|
def ndvi_collection(self, threshold):
|
|
584
809
|
"""
|
|
585
810
|
Calculates ndvi and return collection as class object. Masks collection based on threshold which defaults to -1.
|
|
@@ -592,19 +817,23 @@ class Sentinel2Collection:
|
|
|
592
817
|
"""
|
|
593
818
|
first_image = self.collection.first()
|
|
594
819
|
available_bands = first_image.bandNames()
|
|
595
|
-
if available_bands.contains(
|
|
820
|
+
if available_bands.contains("B4") and available_bands.contains("B8"):
|
|
596
821
|
pass
|
|
597
822
|
else:
|
|
598
823
|
raise ValueError("Insufficient Bands for ndvi calculation")
|
|
599
|
-
col = self.collection.map(
|
|
824
|
+
col = self.collection.map(
|
|
825
|
+
lambda image: Sentinel2Collection.sentinel_ndvi_fn(
|
|
826
|
+
image, threshold=threshold
|
|
827
|
+
)
|
|
828
|
+
)
|
|
600
829
|
return Sentinel2Collection(collection=col)
|
|
601
830
|
|
|
602
831
|
@property
|
|
603
832
|
def halite(self):
|
|
604
833
|
"""
|
|
605
|
-
Property attribute to calculate and access the halite index (see Radwin & Bowen, 2021) imagery of the Sentinel2Collection.
|
|
606
|
-
This property initiates the calculation of halite using a default threshold of -1 (or a previously set threshold of self.halite_threshold)
|
|
607
|
-
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
834
|
+
Property attribute to calculate and access the halite index (see Radwin & Bowen, 2021) imagery of the Sentinel2Collection.
|
|
835
|
+
This property initiates the calculation of halite using a default threshold of -1 (or a previously set threshold of self.halite_threshold)
|
|
836
|
+
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
608
837
|
on subsequent accesses.
|
|
609
838
|
|
|
610
839
|
Returns:
|
|
@@ -626,19 +855,23 @@ class Sentinel2Collection:
|
|
|
626
855
|
"""
|
|
627
856
|
first_image = self.collection.first()
|
|
628
857
|
available_bands = first_image.bandNames()
|
|
629
|
-
if available_bands.contains(
|
|
858
|
+
if available_bands.contains("B4") and available_bands.contains("B11"):
|
|
630
859
|
pass
|
|
631
860
|
else:
|
|
632
861
|
raise ValueError("Insufficient Bands for halite calculation")
|
|
633
|
-
col = self.collection.map(
|
|
862
|
+
col = self.collection.map(
|
|
863
|
+
lambda image: Sentinel2Collection.sentinel_halite_fn(
|
|
864
|
+
image, threshold=threshold
|
|
865
|
+
)
|
|
866
|
+
)
|
|
634
867
|
return Sentinel2Collection(collection=col)
|
|
635
868
|
|
|
636
869
|
@property
|
|
637
870
|
def gypsum(self):
|
|
638
871
|
"""
|
|
639
|
-
Property attribute to calculate and access the gypsum/sulfate index (see Radwin & Bowen, 2021) imagery of the Sentinel2Collection.
|
|
640
|
-
This property initiates the calculation of gypsum using a default threshold of -1 (or a previously set threshold of self.gypsum_threshold)
|
|
641
|
-
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
872
|
+
Property attribute to calculate and access the gypsum/sulfate index (see Radwin & Bowen, 2021) imagery of the Sentinel2Collection.
|
|
873
|
+
This property initiates the calculation of gypsum using a default threshold of -1 (or a previously set threshold of self.gypsum_threshold)
|
|
874
|
+
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
642
875
|
on subsequent accesses.
|
|
643
876
|
|
|
644
877
|
Returns:
|
|
@@ -660,19 +893,23 @@ class Sentinel2Collection:
|
|
|
660
893
|
"""
|
|
661
894
|
first_image = self.collection.first()
|
|
662
895
|
available_bands = first_image.bandNames()
|
|
663
|
-
if available_bands.contains(
|
|
896
|
+
if available_bands.contains("B11") and available_bands.contains("B12"):
|
|
664
897
|
pass
|
|
665
898
|
else:
|
|
666
899
|
raise ValueError("Insufficient Bands for gypsum calculation")
|
|
667
|
-
col = self.collection.map(
|
|
900
|
+
col = self.collection.map(
|
|
901
|
+
lambda image: Sentinel2Collection.sentinel_gypsum_fn(
|
|
902
|
+
image, threshold=threshold
|
|
903
|
+
)
|
|
904
|
+
)
|
|
668
905
|
return Sentinel2Collection(collection=col)
|
|
669
|
-
|
|
906
|
+
|
|
670
907
|
@property
|
|
671
908
|
def turbidity(self):
|
|
672
909
|
"""
|
|
673
|
-
Property attribute to calculate and access the turbidity (NDTI) imagery of the Sentinel2Collection.
|
|
674
|
-
This property initiates the calculation of turbidity using a default threshold of -1 (or a previously set threshold of self.turbidity_threshold)
|
|
675
|
-
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
910
|
+
Property attribute to calculate and access the turbidity (NDTI) imagery of the Sentinel2Collection.
|
|
911
|
+
This property initiates the calculation of turbidity using a default threshold of -1 (or a previously set threshold of self.turbidity_threshold)
|
|
912
|
+
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
676
913
|
on subsequent accesses.
|
|
677
914
|
|
|
678
915
|
Returns:
|
|
@@ -694,19 +931,23 @@ class Sentinel2Collection:
|
|
|
694
931
|
"""
|
|
695
932
|
first_image = self.collection.first()
|
|
696
933
|
available_bands = first_image.bandNames()
|
|
697
|
-
if available_bands.contains(
|
|
934
|
+
if available_bands.contains("B3") and available_bands.contains("B2"):
|
|
698
935
|
pass
|
|
699
936
|
else:
|
|
700
937
|
raise ValueError("Insufficient Bands for turbidity calculation")
|
|
701
|
-
col = self.collection.map(
|
|
938
|
+
col = self.collection.map(
|
|
939
|
+
lambda image: Sentinel2Collection.sentinel_turbidity_fn(
|
|
940
|
+
image, threshold=threshold
|
|
941
|
+
)
|
|
942
|
+
)
|
|
702
943
|
return Sentinel2Collection(collection=col)
|
|
703
|
-
|
|
944
|
+
|
|
704
945
|
@property
|
|
705
946
|
def chlorophyll(self):
|
|
706
947
|
"""
|
|
707
|
-
Property attribute to calculate and access the chlorophyll (NDTI) imagery of the Sentinel2Collection.
|
|
708
|
-
This property initiates the calculation of chlorophyll using a default threshold of -1 (or a previously set threshold of self.chlorophyll_threshold)
|
|
709
|
-
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
948
|
+
Property attribute to calculate and access the chlorophyll (NDTI) imagery of the Sentinel2Collection.
|
|
949
|
+
This property initiates the calculation of chlorophyll using a default threshold of -1 (or a previously set threshold of self.chlorophyll_threshold)
|
|
950
|
+
and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
|
|
710
951
|
on subsequent accesses.
|
|
711
952
|
|
|
712
953
|
Returns:
|
|
@@ -728,11 +969,15 @@ class Sentinel2Collection:
|
|
|
728
969
|
"""
|
|
729
970
|
first_image = self.collection.first()
|
|
730
971
|
available_bands = first_image.bandNames()
|
|
731
|
-
if available_bands.contains(
|
|
972
|
+
if available_bands.contains("B5") and available_bands.contains("B4"):
|
|
732
973
|
pass
|
|
733
974
|
else:
|
|
734
975
|
raise ValueError("Insufficient Bands for chlorophyll calculation")
|
|
735
|
-
col = self.collection.map(
|
|
976
|
+
col = self.collection.map(
|
|
977
|
+
lambda image: Sentinel2Collection.sentinel_chlorophyll_fn(
|
|
978
|
+
image, threshold=threshold
|
|
979
|
+
)
|
|
980
|
+
)
|
|
736
981
|
return Sentinel2Collection(collection=col)
|
|
737
982
|
|
|
738
983
|
@property
|
|
@@ -747,7 +992,7 @@ class Sentinel2Collection:
|
|
|
747
992
|
col = self.collection.map(Sentinel2Collection.MaskWaterS2)
|
|
748
993
|
self._masked_water_collection = Sentinel2Collection(collection=col)
|
|
749
994
|
return self._masked_water_collection
|
|
750
|
-
|
|
995
|
+
|
|
751
996
|
def masked_water_collection_NDWI(self, threshold):
|
|
752
997
|
"""
|
|
753
998
|
Function to mask water by using NDWI and return collection as class object.
|
|
@@ -755,9 +1000,13 @@ class Sentinel2Collection:
|
|
|
755
1000
|
Returns:
|
|
756
1001
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
757
1002
|
"""
|
|
758
|
-
col = self.collection.map(
|
|
1003
|
+
col = self.collection.map(
|
|
1004
|
+
lambda image: Sentinel2Collection.MaskWaterS2ByNDWI(
|
|
1005
|
+
image, threshold=threshold
|
|
1006
|
+
)
|
|
1007
|
+
)
|
|
759
1008
|
return Sentinel2Collection(collection=col)
|
|
760
|
-
|
|
1009
|
+
|
|
761
1010
|
@property
|
|
762
1011
|
def masked_to_water_collection(self):
|
|
763
1012
|
"""
|
|
@@ -770,7 +1019,7 @@ class Sentinel2Collection:
|
|
|
770
1019
|
col = self.collection.map(Sentinel2Collection.MaskToWaterS2)
|
|
771
1020
|
self._masked_water_collection = Sentinel2Collection(collection=col)
|
|
772
1021
|
return self._masked_water_collection
|
|
773
|
-
|
|
1022
|
+
|
|
774
1023
|
def masked_to_water_collection_NDWI(self, threshold):
|
|
775
1024
|
"""
|
|
776
1025
|
Function to mask to water pixels by using NDWI and return collection as class object
|
|
@@ -778,9 +1027,13 @@ class Sentinel2Collection:
|
|
|
778
1027
|
Returns:
|
|
779
1028
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
780
1029
|
"""
|
|
781
|
-
col = self.collection.map(
|
|
1030
|
+
col = self.collection.map(
|
|
1031
|
+
lambda image: Sentinel2Collection.MaskToWaterS2ByNDWI(
|
|
1032
|
+
image, threshold=threshold
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
782
1035
|
return Sentinel2Collection(collection=col)
|
|
783
|
-
|
|
1036
|
+
|
|
784
1037
|
@property
|
|
785
1038
|
def masked_clouds_collection(self):
|
|
786
1039
|
"""
|
|
@@ -793,7 +1046,7 @@ class Sentinel2Collection:
|
|
|
793
1046
|
col = self.collection.map(Sentinel2Collection.MaskCloudsS2)
|
|
794
1047
|
self._masked_clouds_collection = Sentinel2Collection(collection=col)
|
|
795
1048
|
return self._masked_clouds_collection
|
|
796
|
-
|
|
1049
|
+
|
|
797
1050
|
def mask_to_polygon(self, polygon):
|
|
798
1051
|
"""
|
|
799
1052
|
Function to mask Sentinel2Collection image collection by a polygon (ee.Geometry), where pixels outside the polygon are masked out.
|
|
@@ -803,21 +1056,23 @@ class Sentinel2Collection:
|
|
|
803
1056
|
|
|
804
1057
|
Returns:
|
|
805
1058
|
Sentinel2Collection: masked Sentinel2Collection image collection.
|
|
806
|
-
|
|
1059
|
+
|
|
807
1060
|
"""
|
|
808
1061
|
if self._geometry_masked_collection is None:
|
|
809
1062
|
# Convert the polygon to a mask
|
|
810
1063
|
mask = ee.Image.constant(1).clip(polygon)
|
|
811
|
-
|
|
1064
|
+
|
|
812
1065
|
# Update the mask of each image in the collection
|
|
813
1066
|
masked_collection = self.collection.map(lambda img: img.updateMask(mask))
|
|
814
|
-
|
|
1067
|
+
|
|
815
1068
|
# Update the internal collection state
|
|
816
|
-
self._geometry_masked_collection = Sentinel2Collection(
|
|
817
|
-
|
|
1069
|
+
self._geometry_masked_collection = Sentinel2Collection(
|
|
1070
|
+
collection=masked_collection
|
|
1071
|
+
)
|
|
1072
|
+
|
|
818
1073
|
# Return the updated object
|
|
819
1074
|
return self._geometry_masked_collection
|
|
820
|
-
|
|
1075
|
+
|
|
821
1076
|
def mask_out_polygon(self, polygon):
|
|
822
1077
|
"""
|
|
823
1078
|
Function to mask Sentinel2Collection image collection by a polygon (ee.Geometry), where pixels inside the polygon are masked out.
|
|
@@ -827,7 +1082,7 @@ class Sentinel2Collection:
|
|
|
827
1082
|
|
|
828
1083
|
Returns:
|
|
829
1084
|
Sentinel2Collection: masked Sentinel2Collection image collection.
|
|
830
|
-
|
|
1085
|
+
|
|
831
1086
|
"""
|
|
832
1087
|
if self._geometry_masked_out_collection is None:
|
|
833
1088
|
# Convert the polygon to a mask
|
|
@@ -835,32 +1090,36 @@ class Sentinel2Collection:
|
|
|
835
1090
|
|
|
836
1091
|
# Use paint to set pixels inside polygon as 0
|
|
837
1092
|
area = full_mask.paint(polygon, 0)
|
|
838
|
-
|
|
1093
|
+
|
|
839
1094
|
# Update the mask of each image in the collection
|
|
840
1095
|
masked_collection = self.collection.map(lambda img: img.updateMask(area))
|
|
841
|
-
|
|
1096
|
+
|
|
842
1097
|
# Update the internal collection state
|
|
843
|
-
self._geometry_masked_out_collection = Sentinel2Collection(
|
|
844
|
-
|
|
1098
|
+
self._geometry_masked_out_collection = Sentinel2Collection(
|
|
1099
|
+
collection=masked_collection
|
|
1100
|
+
)
|
|
1101
|
+
|
|
845
1102
|
# Return the updated object
|
|
846
1103
|
return self._geometry_masked_out_collection
|
|
847
|
-
|
|
1104
|
+
|
|
848
1105
|
def mask_halite(self, threshold):
|
|
849
1106
|
"""
|
|
850
|
-
Function to mask halite and return collection as class object.
|
|
1107
|
+
Function to mask halite and return collection as class object.
|
|
851
1108
|
|
|
852
1109
|
Args:
|
|
853
1110
|
threshold: specify threshold for gypsum function (values less than threshold are masked).
|
|
854
|
-
|
|
1111
|
+
|
|
855
1112
|
Returns:
|
|
856
1113
|
Sentinel2Collection: Sentinel2Collection image collection
|
|
857
1114
|
"""
|
|
858
|
-
col = self.collection.map(
|
|
1115
|
+
col = self.collection.map(
|
|
1116
|
+
lambda image: Sentinel2Collection.halite_mask(image, threshold=threshold)
|
|
1117
|
+
)
|
|
859
1118
|
return Sentinel2Collection(collection=col)
|
|
860
|
-
|
|
1119
|
+
|
|
861
1120
|
def mask_halite_and_gypsum(self, halite_threshold, gypsum_threshold):
|
|
862
1121
|
"""
|
|
863
|
-
Function to mask halite and gypsum and return collection as class object.
|
|
1122
|
+
Function to mask halite and gypsum and return collection as class object.
|
|
864
1123
|
|
|
865
1124
|
Args:
|
|
866
1125
|
halite_threshold: specify threshold for halite function (values less than threshold are masked).
|
|
@@ -869,7 +1128,44 @@ class Sentinel2Collection:
|
|
|
869
1128
|
Returns:
|
|
870
1129
|
Sentinel2Collection: Sentinel2Collection image collection
|
|
871
1130
|
"""
|
|
872
|
-
col = self.collection.map(
|
|
1131
|
+
col = self.collection.map(
|
|
1132
|
+
lambda image: Sentinel2Collection.gypsum_and_halite_mask(
|
|
1133
|
+
image,
|
|
1134
|
+
halite_threshold=halite_threshold,
|
|
1135
|
+
gypsum_threshold=gypsum_threshold,
|
|
1136
|
+
)
|
|
1137
|
+
)
|
|
1138
|
+
return Sentinel2Collection(collection=col)
|
|
1139
|
+
|
|
1140
|
+
def binary_mask(self, threshold=None, band_name=None):
|
|
1141
|
+
"""
|
|
1142
|
+
Creates a binary mask (value of 1 for pixels above set threshold and value of 0 for all other pixels) of the Sentinel2Collection image collection based on a specified band.
|
|
1143
|
+
If a singleband image is provided, the band name is automatically determined.
|
|
1144
|
+
If multiple bands are available, the user must specify the band name to use for masking.
|
|
1145
|
+
|
|
1146
|
+
Args:
|
|
1147
|
+
band_name (str, optional): The name of the band to use for masking. Defaults to None.
|
|
1148
|
+
|
|
1149
|
+
Returns:
|
|
1150
|
+
Sentinel2Collection: Sentinel2Collection singleband image collection with binary masks applied.
|
|
1151
|
+
"""
|
|
1152
|
+
if self.collection.size().eq(0).getInfo():
|
|
1153
|
+
raise ValueError("The collection is empty. Cannot create a binary mask.")
|
|
1154
|
+
if band_name is None:
|
|
1155
|
+
first_image = self.collection.first()
|
|
1156
|
+
band_names = first_image.bandNames()
|
|
1157
|
+
if band_names.size().getInfo() == 0:
|
|
1158
|
+
raise ValueError("No bands available in the collection.")
|
|
1159
|
+
if band_names.size().getInfo() > 1:
|
|
1160
|
+
raise ValueError("Multiple bands available, please specify a band name.")
|
|
1161
|
+
else:
|
|
1162
|
+
band_name = band_names.get(0).getInfo()
|
|
1163
|
+
if threshold is None:
|
|
1164
|
+
raise ValueError("Threshold must be specified for binary masking.")
|
|
1165
|
+
|
|
1166
|
+
col = self.collection.map(
|
|
1167
|
+
lambda image: image.select(band_name).gte(threshold).rename(band_name)
|
|
1168
|
+
)
|
|
873
1169
|
return Sentinel2Collection(collection=col)
|
|
874
1170
|
|
|
875
1171
|
def image_grab(self, img_selector):
|
|
@@ -878,7 +1174,7 @@ class Sentinel2Collection:
|
|
|
878
1174
|
|
|
879
1175
|
Args:
|
|
880
1176
|
img_selector: index of image in the collection for which user seeks to select/"grab".
|
|
881
|
-
|
|
1177
|
+
|
|
882
1178
|
Returns:
|
|
883
1179
|
ee.Image: ee.Image of selected image.
|
|
884
1180
|
"""
|
|
@@ -897,7 +1193,7 @@ class Sentinel2Collection:
|
|
|
897
1193
|
Args:
|
|
898
1194
|
img_col: ee.ImageCollection with same dates as another Sentinel2Collection image collection object.
|
|
899
1195
|
img_selector: index of image in list of dates for which user seeks to "select".
|
|
900
|
-
|
|
1196
|
+
|
|
901
1197
|
Returns:
|
|
902
1198
|
ee.Image: ee.Image of selected image.
|
|
903
1199
|
"""
|
|
@@ -908,7 +1204,7 @@ class Sentinel2Collection:
|
|
|
908
1204
|
image = ee.Image(image_list.get(img_selector))
|
|
909
1205
|
|
|
910
1206
|
return image
|
|
911
|
-
|
|
1207
|
+
|
|
912
1208
|
def image_pick(self, img_date):
|
|
913
1209
|
"""
|
|
914
1210
|
Function to select ("grab") image of a specific date in format of 'YYYY-MM-DD'.
|
|
@@ -920,13 +1216,13 @@ class Sentinel2Collection:
|
|
|
920
1216
|
Returns:
|
|
921
1217
|
ee.Image: ee.Image of selected image.
|
|
922
1218
|
"""
|
|
923
|
-
new_col = self.collection.filter(ee.Filter.eq(
|
|
1219
|
+
new_col = self.collection.filter(ee.Filter.eq("Date_Filter", img_date))
|
|
924
1220
|
return new_col.first()
|
|
925
|
-
|
|
1221
|
+
|
|
926
1222
|
def CollectionStitch(self, img_col2):
|
|
927
1223
|
"""
|
|
928
|
-
Function to mosaic two Sentinel2Collection objects which share image dates.
|
|
929
|
-
Mosaics are only formed for dates where both image collections have images.
|
|
1224
|
+
Function to mosaic two Sentinel2Collection objects which share image dates.
|
|
1225
|
+
Mosaics are only formed for dates where both image collections have images.
|
|
930
1226
|
Image properties are copied from the primary collection.
|
|
931
1227
|
Server-side friendly.
|
|
932
1228
|
|
|
@@ -936,26 +1232,38 @@ class Sentinel2Collection:
|
|
|
936
1232
|
Returns:
|
|
937
1233
|
Sentinel2Collection: Sentinel2Collection image collection
|
|
938
1234
|
"""
|
|
939
|
-
dates_list =
|
|
1235
|
+
dates_list = (
|
|
1236
|
+
ee.List(self.dates_list).cat(ee.List(img_col2.dates_list)).distinct()
|
|
1237
|
+
)
|
|
940
1238
|
filtered_dates1 = self.dates_list
|
|
941
1239
|
filtered_dates2 = img_col2.dates_list
|
|
942
1240
|
|
|
943
|
-
filtered_col2 = img_col2.collection.filter(
|
|
944
|
-
|
|
1241
|
+
filtered_col2 = img_col2.collection.filter(
|
|
1242
|
+
ee.Filter.inList("Date_Filter", filtered_dates1)
|
|
1243
|
+
)
|
|
1244
|
+
filtered_col1 = self.collection.filter(
|
|
1245
|
+
ee.Filter.inList(
|
|
1246
|
+
"Date_Filter", filtered_col2.aggregate_array("Date_Filter")
|
|
1247
|
+
)
|
|
1248
|
+
)
|
|
945
1249
|
|
|
946
1250
|
# Create a function that will be mapped over filtered_col1
|
|
947
1251
|
def mosaic_images(img):
|
|
948
1252
|
# Get the date of the image
|
|
949
|
-
date = img.get(
|
|
950
|
-
|
|
1253
|
+
date = img.get("Date_Filter")
|
|
1254
|
+
|
|
951
1255
|
# Get the corresponding image from filtered_col2
|
|
952
|
-
img2 = filtered_col2.filter(ee.Filter.equals(
|
|
1256
|
+
img2 = filtered_col2.filter(ee.Filter.equals("Date_Filter", date)).first()
|
|
953
1257
|
|
|
954
1258
|
# Create a mosaic of the two images
|
|
955
1259
|
mosaic = ee.ImageCollection.fromImages([img, img2]).mosaic()
|
|
956
1260
|
|
|
957
1261
|
# Copy properties from the first image and set the time properties
|
|
958
|
-
mosaic =
|
|
1262
|
+
mosaic = (
|
|
1263
|
+
mosaic.copyProperties(img)
|
|
1264
|
+
.set("Date_Filter", date)
|
|
1265
|
+
.set("system:time_start", img.get("system:time_start"))
|
|
1266
|
+
)
|
|
959
1267
|
|
|
960
1268
|
return mosaic
|
|
961
1269
|
|
|
@@ -964,48 +1272,62 @@ class Sentinel2Collection:
|
|
|
964
1272
|
|
|
965
1273
|
# Return a Sentinel2Collection instance
|
|
966
1274
|
return Sentinel2Collection(collection=new_col)
|
|
967
|
-
|
|
1275
|
+
|
|
968
1276
|
@property
|
|
969
1277
|
def MosaicByDate(self):
|
|
970
1278
|
"""
|
|
971
|
-
Property attribute function to mosaic collection images that share the same date. The properties CLOUD_PIXEL_PERCENTAGE and NODATA_PIXEL_PERCENTAGE
|
|
972
|
-
for each image are used to calculate an overall mean, which replaces the CLOUD_PIXEL_PERCENTAGE and NODATA_PIXEL_PERCENTAGE for each mosaiced image.
|
|
973
|
-
Server-side friendly.
|
|
974
|
-
|
|
1279
|
+
Property attribute function to mosaic collection images that share the same date. The properties CLOUD_PIXEL_PERCENTAGE and NODATA_PIXEL_PERCENTAGE
|
|
1280
|
+
for each image are used to calculate an overall mean, which replaces the CLOUD_PIXEL_PERCENTAGE and NODATA_PIXEL_PERCENTAGE for each mosaiced image.
|
|
1281
|
+
Server-side friendly.
|
|
1282
|
+
|
|
975
1283
|
NOTE: if images are removed from the collection from cloud filtering, you may have mosaics composed of only one image.
|
|
976
1284
|
|
|
977
1285
|
Returns:
|
|
978
|
-
Sentinel2Collection: Sentinel2Collection image collection
|
|
1286
|
+
Sentinel2Collection: Sentinel2Collection image collection
|
|
979
1287
|
"""
|
|
980
1288
|
if self._MosaicByDate is None:
|
|
981
1289
|
input_collection = self.collection
|
|
1290
|
+
|
|
982
1291
|
# Function to mosaic images of the same date and accumulate them
|
|
983
1292
|
def mosaic_and_accumulate(date, list_accumulator):
|
|
984
1293
|
# date = ee.Date(date)
|
|
985
1294
|
list_accumulator = ee.List(list_accumulator)
|
|
986
|
-
date_filter = ee.Filter.eq(
|
|
1295
|
+
date_filter = ee.Filter.eq("Date_Filter", date)
|
|
987
1296
|
date_collection = input_collection.filter(date_filter)
|
|
988
1297
|
image_list = date_collection.toList(date_collection.size())
|
|
989
1298
|
first_image = ee.Image(image_list.get(0))
|
|
990
|
-
|
|
1299
|
+
|
|
991
1300
|
# Create mosaic
|
|
992
|
-
mosaic = date_collection.mosaic().set(
|
|
1301
|
+
mosaic = date_collection.mosaic().set("Date_Filter", date)
|
|
993
1302
|
|
|
994
1303
|
# Calculate cumulative cloud and no data percentages
|
|
995
|
-
cloud_percentage = date_collection.aggregate_mean(
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1304
|
+
cloud_percentage = date_collection.aggregate_mean(
|
|
1305
|
+
"CLOUDY_PIXEL_PERCENTAGE"
|
|
1306
|
+
)
|
|
1307
|
+
no_data_percentage = date_collection.aggregate_mean(
|
|
1308
|
+
"NODATA_PIXEL_PERCENTAGE"
|
|
1309
|
+
)
|
|
999
1310
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1311
|
+
props_of_interest = [
|
|
1312
|
+
"SPACECRAFT_NAME",
|
|
1313
|
+
"SENSING_ORBIT_NUMBER",
|
|
1314
|
+
"SENSING_ORBIT_DIRECTION",
|
|
1315
|
+
"MISSION_ID",
|
|
1316
|
+
"PLATFORM_IDENTIFIER",
|
|
1317
|
+
"system:time_start",
|
|
1318
|
+
]
|
|
1319
|
+
|
|
1320
|
+
mosaic = mosaic.copyProperties(first_image, props_of_interest).set(
|
|
1321
|
+
{
|
|
1322
|
+
"CLOUDY_PIXEL_PERCENTAGE": cloud_percentage,
|
|
1323
|
+
"NODATA_PIXEL_PERCENTAGE": no_data_percentage,
|
|
1324
|
+
}
|
|
1325
|
+
)
|
|
1004
1326
|
|
|
1005
1327
|
return list_accumulator.add(mosaic)
|
|
1006
1328
|
|
|
1007
1329
|
# Get distinct dates
|
|
1008
|
-
distinct_dates = input_collection.aggregate_array(
|
|
1330
|
+
distinct_dates = input_collection.aggregate_array("Date_Filter").distinct()
|
|
1009
1331
|
|
|
1010
1332
|
# Initialize an empty list as the accumulator
|
|
1011
1333
|
initial = ee.List([])
|
|
@@ -1020,7 +1342,9 @@ class Sentinel2Collection:
|
|
|
1020
1342
|
return self._MosaicByDate
|
|
1021
1343
|
|
|
1022
1344
|
@staticmethod
|
|
1023
|
-
def ee_to_df(
|
|
1345
|
+
def ee_to_df(
|
|
1346
|
+
ee_object, columns=None, remove_geom=True, sort_columns=False, **kwargs
|
|
1347
|
+
):
|
|
1024
1348
|
"""Converts an ee.FeatureCollection to pandas dataframe. Adapted from the geemap package (https://geemap.org/common/#geemap.common.ee_to_df)
|
|
1025
1349
|
|
|
1026
1350
|
Args:
|
|
@@ -1070,8 +1394,19 @@ class Sentinel2Collection:
|
|
|
1070
1394
|
raise Exception(e)
|
|
1071
1395
|
|
|
1072
1396
|
@staticmethod
|
|
1073
|
-
def extract_transect(
|
|
1074
|
-
|
|
1397
|
+
def extract_transect(
|
|
1398
|
+
image,
|
|
1399
|
+
line,
|
|
1400
|
+
reducer="mean",
|
|
1401
|
+
n_segments=100,
|
|
1402
|
+
dist_interval=None,
|
|
1403
|
+
scale=None,
|
|
1404
|
+
crs=None,
|
|
1405
|
+
crsTransform=None,
|
|
1406
|
+
tileScale=1.0,
|
|
1407
|
+
to_pandas=False,
|
|
1408
|
+
**kwargs,
|
|
1409
|
+
):
|
|
1075
1410
|
"""Extracts transect from an image. Adapted from the geemap package (https://geemap.org/common/#geemap.common.extract_transect). Exists as an alternative to RadGEEToolbox 'transect' function.
|
|
1076
1411
|
|
|
1077
1412
|
Args:
|
|
@@ -1135,9 +1470,17 @@ class Sentinel2Collection:
|
|
|
1135
1470
|
|
|
1136
1471
|
except Exception as e:
|
|
1137
1472
|
raise Exception(e)
|
|
1138
|
-
|
|
1473
|
+
|
|
1139
1474
|
@staticmethod
|
|
1140
|
-
def transect(
|
|
1475
|
+
def transect(
|
|
1476
|
+
image,
|
|
1477
|
+
lines,
|
|
1478
|
+
line_names,
|
|
1479
|
+
reducer="mean",
|
|
1480
|
+
n_segments=None,
|
|
1481
|
+
dist_interval=10,
|
|
1482
|
+
to_pandas=True,
|
|
1483
|
+
):
|
|
1141
1484
|
"""Computes and stores the values along a transect for each line in a list of lines. Builds off of the extract_transect function from the geemap package
|
|
1142
1485
|
where checks are ran to ensure that the reducer column is present in the transect data. If the reducer column is not present, a column of NaNs is created.
|
|
1143
1486
|
An ee reducer is used to aggregate the values along the transect, depending on the number of segments or distance interval specified. Defaults to 'mean' reducer.
|
|
@@ -1154,46 +1497,71 @@ class Sentinel2Collection:
|
|
|
1154
1497
|
Returns:
|
|
1155
1498
|
pd.DataFrame or ee.FeatureCollection: organized list of values along the transect(s)
|
|
1156
1499
|
"""
|
|
1157
|
-
#Create empty dataframe
|
|
1500
|
+
# Create empty dataframe
|
|
1158
1501
|
transects_df = pd.DataFrame()
|
|
1159
1502
|
|
|
1160
|
-
#Check if line is a list of lines or a single line - if single line, convert to list
|
|
1503
|
+
# Check if line is a list of lines or a single line - if single line, convert to list
|
|
1161
1504
|
if isinstance(lines, list):
|
|
1162
1505
|
pass
|
|
1163
1506
|
else:
|
|
1164
1507
|
lines = [lines]
|
|
1165
|
-
|
|
1508
|
+
|
|
1166
1509
|
for i, line in enumerate(lines):
|
|
1167
1510
|
if n_segments is None:
|
|
1168
|
-
transect_data = Sentinel2Collection.extract_transect(
|
|
1511
|
+
transect_data = Sentinel2Collection.extract_transect(
|
|
1512
|
+
image=image,
|
|
1513
|
+
line=line,
|
|
1514
|
+
reducer=reducer,
|
|
1515
|
+
dist_interval=dist_interval,
|
|
1516
|
+
to_pandas=to_pandas,
|
|
1517
|
+
)
|
|
1169
1518
|
if reducer in transect_data.columns:
|
|
1170
1519
|
# Extract the 'mean' column and rename it
|
|
1171
|
-
mean_column = transect_data[[
|
|
1520
|
+
mean_column = transect_data[["mean"]]
|
|
1172
1521
|
else:
|
|
1173
1522
|
# Handle the case where 'mean' column is not present
|
|
1174
|
-
print(
|
|
1523
|
+
print(
|
|
1524
|
+
f"{reducer} column not found in transect data for line {line_names[i]}"
|
|
1525
|
+
)
|
|
1175
1526
|
# Create a column of NaNs with the same length as the longest column in transects_df
|
|
1176
1527
|
max_length = max(transects_df.shape[0], transect_data.shape[0])
|
|
1177
1528
|
mean_column = pd.Series([np.nan] * max_length)
|
|
1178
1529
|
else:
|
|
1179
|
-
transect_data = Sentinel2Collection.extract_transect(
|
|
1530
|
+
transect_data = Sentinel2Collection.extract_transect(
|
|
1531
|
+
image=image,
|
|
1532
|
+
line=line,
|
|
1533
|
+
reducer=reducer,
|
|
1534
|
+
n_segments=n_segments,
|
|
1535
|
+
to_pandas=to_pandas,
|
|
1536
|
+
)
|
|
1180
1537
|
if reducer in transect_data.columns:
|
|
1181
1538
|
# Extract the 'mean' column and rename it
|
|
1182
|
-
mean_column = transect_data[[
|
|
1539
|
+
mean_column = transect_data[["mean"]]
|
|
1183
1540
|
else:
|
|
1184
1541
|
# Handle the case where 'mean' column is not present
|
|
1185
|
-
print(
|
|
1542
|
+
print(
|
|
1543
|
+
f"{reducer} column not found in transect data for line {line_names[i]}"
|
|
1544
|
+
)
|
|
1186
1545
|
# Create a column of NaNs with the same length as the longest column in transects_df
|
|
1187
1546
|
max_length = max(transects_df.shape[0], transect_data.shape[0])
|
|
1188
1547
|
mean_column = pd.Series([np.nan] * max_length)
|
|
1189
|
-
|
|
1548
|
+
|
|
1190
1549
|
transects_df = pd.concat([transects_df, mean_column], axis=1)
|
|
1191
1550
|
|
|
1192
1551
|
transects_df.columns = line_names
|
|
1193
|
-
|
|
1552
|
+
|
|
1194
1553
|
return transects_df
|
|
1195
|
-
|
|
1196
|
-
def transect_iterator(
|
|
1554
|
+
|
|
1555
|
+
def transect_iterator(
|
|
1556
|
+
self,
|
|
1557
|
+
lines,
|
|
1558
|
+
line_names,
|
|
1559
|
+
save_folder_path,
|
|
1560
|
+
reducer="mean",
|
|
1561
|
+
n_segments=None,
|
|
1562
|
+
dist_interval=10,
|
|
1563
|
+
to_pandas=True,
|
|
1564
|
+
):
|
|
1197
1565
|
"""Computes and stores the values along a transect for each line in a list of lines for each image in a Sentinel2Collection image collection, then saves the data for each image to a csv file. Builds off of the extract_transect function from the geemap package
|
|
1198
1566
|
where checks are ran to ensure that the reducer column is present in the transect data. If the reducer column is not present, a column of NaNs is created.
|
|
1199
1567
|
An ee reducer is used to aggregate the values along the transect, depending on the number of segments or distance interval specified. Defaults to 'mean' reducer.
|
|
@@ -1214,21 +1582,37 @@ class Sentinel2Collection:
|
|
|
1214
1582
|
Returns:
|
|
1215
1583
|
csv file: file for each image with an organized list of values along the transect(s)
|
|
1216
1584
|
"""
|
|
1217
|
-
image_collection = self
|
|
1585
|
+
image_collection = self # .collection
|
|
1218
1586
|
image_collection_dates = self.dates
|
|
1219
1587
|
for i, date in enumerate(image_collection_dates):
|
|
1220
1588
|
try:
|
|
1221
1589
|
print(f"Processing image {i+1}/{len(image_collection_dates)}: {date}")
|
|
1222
1590
|
image = image_collection.image_grab(i)
|
|
1223
|
-
transects_df = Sentinel2Collection.transect(
|
|
1591
|
+
transects_df = Sentinel2Collection.transect(
|
|
1592
|
+
image,
|
|
1593
|
+
lines,
|
|
1594
|
+
line_names,
|
|
1595
|
+
reducer=reducer,
|
|
1596
|
+
n_segments=n_segments,
|
|
1597
|
+
dist_interval=dist_interval,
|
|
1598
|
+
to_pandas=to_pandas,
|
|
1599
|
+
)
|
|
1224
1600
|
image_id = date
|
|
1225
|
-
transects_df.to_csv(f
|
|
1226
|
-
print(f
|
|
1601
|
+
transects_df.to_csv(f"{save_folder_path}{image_id}_transects.csv")
|
|
1602
|
+
print(f"{image_id}_transects saved to csv")
|
|
1227
1603
|
except Exception as e:
|
|
1228
1604
|
print(f"An error occurred while processing image {i+1}: {e}")
|
|
1229
1605
|
|
|
1230
1606
|
@staticmethod
|
|
1231
|
-
def extract_zonal_stats_from_buffer(
|
|
1607
|
+
def extract_zonal_stats_from_buffer(
|
|
1608
|
+
image,
|
|
1609
|
+
coordinates,
|
|
1610
|
+
buffer_size=1,
|
|
1611
|
+
reducer_type="mean",
|
|
1612
|
+
scale=10,
|
|
1613
|
+
tileScale=1,
|
|
1614
|
+
coordinate_names=None,
|
|
1615
|
+
):
|
|
1232
1616
|
"""
|
|
1233
1617
|
Function to extract spatial statistics from an image for a list of coordinates, providing individual statistics for each location.
|
|
1234
1618
|
A radial buffer is applied around each coordinate to extract the statistics, which defaults to 1 meter.
|
|
@@ -1250,15 +1634,26 @@ class Sentinel2Collection:
|
|
|
1250
1634
|
# Check if coordinates is a single tuple and convert it to a list of tuples if necessary
|
|
1251
1635
|
if isinstance(coordinates, tuple) and len(coordinates) == 2:
|
|
1252
1636
|
coordinates = [coordinates]
|
|
1253
|
-
elif not (
|
|
1254
|
-
|
|
1255
|
-
|
|
1637
|
+
elif not (
|
|
1638
|
+
isinstance(coordinates, list)
|
|
1639
|
+
and all(
|
|
1640
|
+
isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates
|
|
1641
|
+
)
|
|
1642
|
+
):
|
|
1643
|
+
raise ValueError(
|
|
1644
|
+
"Coordinates must be a list of tuples with two elements each (latitude, longitude)."
|
|
1645
|
+
)
|
|
1646
|
+
|
|
1256
1647
|
# Check if coordinate_names is a list of strings
|
|
1257
1648
|
if coordinate_names is not None:
|
|
1258
|
-
if not isinstance(coordinate_names, list) or not all(
|
|
1649
|
+
if not isinstance(coordinate_names, list) or not all(
|
|
1650
|
+
isinstance(name, str) for name in coordinate_names
|
|
1651
|
+
):
|
|
1259
1652
|
raise ValueError("coordinate_names must be a list of strings.")
|
|
1260
1653
|
if len(coordinate_names) != len(coordinates):
|
|
1261
|
-
raise ValueError(
|
|
1654
|
+
raise ValueError(
|
|
1655
|
+
"coordinate_names must have the same length as the coordinates list."
|
|
1656
|
+
)
|
|
1262
1657
|
else:
|
|
1263
1658
|
coordinate_names = [f"Location {i+1}" for i in range(len(coordinates))]
|
|
1264
1659
|
|
|
@@ -1270,75 +1665,97 @@ class Sentinel2Collection:
|
|
|
1270
1665
|
# image = ee.Image(check_singleband(image))
|
|
1271
1666
|
image = ee.Image(check_singleband(image))
|
|
1272
1667
|
|
|
1273
|
-
#Convert coordinates to ee.Geometry.Point, buffer them, and add label/name to feature
|
|
1274
|
-
points = [
|
|
1668
|
+
# Convert coordinates to ee.Geometry.Point, buffer them, and add label/name to feature
|
|
1669
|
+
points = [
|
|
1670
|
+
ee.Feature(
|
|
1671
|
+
ee.Geometry.Point([coord[0], coord[1]]).buffer(buffer_size),
|
|
1672
|
+
{"name": str(coordinate_names[i])},
|
|
1673
|
+
)
|
|
1674
|
+
for i, coord in enumerate(coordinates)
|
|
1675
|
+
]
|
|
1275
1676
|
# Create a feature collection from the buffered points
|
|
1276
1677
|
features = ee.FeatureCollection(points)
|
|
1277
1678
|
# Reduce the image to the buffered points - handle different reducer types
|
|
1278
|
-
if reducer_type ==
|
|
1679
|
+
if reducer_type == "mean":
|
|
1279
1680
|
img_stats = image.reduceRegions(
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1681
|
+
collection=features,
|
|
1682
|
+
reducer=ee.Reducer.mean(),
|
|
1683
|
+
scale=scale,
|
|
1684
|
+
tileScale=tileScale,
|
|
1685
|
+
)
|
|
1284
1686
|
mean_values = img_stats.getInfo()
|
|
1285
1687
|
means = []
|
|
1286
1688
|
names = []
|
|
1287
|
-
for feature in mean_values[
|
|
1288
|
-
names.append(feature[
|
|
1289
|
-
means.append(feature[
|
|
1689
|
+
for feature in mean_values["features"]:
|
|
1690
|
+
names.append(feature["properties"]["name"])
|
|
1691
|
+
means.append(feature["properties"]["mean"])
|
|
1290
1692
|
organized_values = pd.DataFrame([means], columns=names)
|
|
1291
|
-
elif reducer_type ==
|
|
1693
|
+
elif reducer_type == "median":
|
|
1292
1694
|
img_stats = image.reduceRegions(
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1695
|
+
collection=features,
|
|
1696
|
+
reducer=ee.Reducer.median(),
|
|
1697
|
+
scale=scale,
|
|
1698
|
+
tileScale=tileScale,
|
|
1699
|
+
)
|
|
1297
1700
|
median_values = img_stats.getInfo()
|
|
1298
1701
|
medians = []
|
|
1299
1702
|
names = []
|
|
1300
|
-
for feature in median_values[
|
|
1301
|
-
names.append(feature[
|
|
1302
|
-
medians.append(feature[
|
|
1703
|
+
for feature in median_values["features"]:
|
|
1704
|
+
names.append(feature["properties"]["name"])
|
|
1705
|
+
medians.append(feature["properties"]["median"])
|
|
1303
1706
|
organized_values = pd.DataFrame([medians], columns=names)
|
|
1304
|
-
elif reducer_type ==
|
|
1707
|
+
elif reducer_type == "min":
|
|
1305
1708
|
img_stats = image.reduceRegions(
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1709
|
+
collection=features,
|
|
1710
|
+
reducer=ee.Reducer.min(),
|
|
1711
|
+
scale=scale,
|
|
1712
|
+
tileScale=tileScale,
|
|
1713
|
+
)
|
|
1310
1714
|
min_values = img_stats.getInfo()
|
|
1311
1715
|
mins = []
|
|
1312
1716
|
names = []
|
|
1313
|
-
for feature in min_values[
|
|
1314
|
-
names.append(feature[
|
|
1315
|
-
mins.append(feature[
|
|
1717
|
+
for feature in min_values["features"]:
|
|
1718
|
+
names.append(feature["properties"]["name"])
|
|
1719
|
+
mins.append(feature["properties"]["min"])
|
|
1316
1720
|
organized_values = pd.DataFrame([mins], columns=names)
|
|
1317
|
-
elif reducer_type ==
|
|
1721
|
+
elif reducer_type == "max":
|
|
1318
1722
|
img_stats = image.reduceRegions(
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1723
|
+
collection=features,
|
|
1724
|
+
reducer=ee.Reducer.max(),
|
|
1725
|
+
scale=scale,
|
|
1726
|
+
tileScale=tileScale,
|
|
1727
|
+
)
|
|
1323
1728
|
max_values = img_stats.getInfo()
|
|
1324
1729
|
maxs = []
|
|
1325
1730
|
names = []
|
|
1326
|
-
for feature in max_values[
|
|
1327
|
-
names.append(feature[
|
|
1328
|
-
maxs.append(feature[
|
|
1731
|
+
for feature in max_values["features"]:
|
|
1732
|
+
names.append(feature["properties"]["name"])
|
|
1733
|
+
maxs.append(feature["properties"]["max"])
|
|
1329
1734
|
organized_values = pd.DataFrame([maxs], columns=names)
|
|
1330
1735
|
else:
|
|
1331
|
-
raise ValueError(
|
|
1736
|
+
raise ValueError(
|
|
1737
|
+
"reducer_type must be one of 'mean', 'median', 'min', or 'max'."
|
|
1738
|
+
)
|
|
1332
1739
|
return organized_values
|
|
1333
1740
|
|
|
1334
|
-
def iterate_zonal_stats(
|
|
1741
|
+
def iterate_zonal_stats(
|
|
1742
|
+
self,
|
|
1743
|
+
coordinates,
|
|
1744
|
+
buffer_size=1,
|
|
1745
|
+
reducer_type="mean",
|
|
1746
|
+
scale=10,
|
|
1747
|
+
tileScale=1,
|
|
1748
|
+
coordinate_names=None,
|
|
1749
|
+
file_path=None,
|
|
1750
|
+
dates=None,
|
|
1751
|
+
):
|
|
1335
1752
|
"""
|
|
1336
1753
|
Function to iterate over a collection of images and extract spatial statistics for a list of coordinates (defaults to mean). Individual statistics are provided for each location.
|
|
1337
1754
|
A radial buffer is applied around each coordinate to extract the statistics, which defaults to 1 meter.
|
|
1338
1755
|
The function returns a pandas DataFrame with the statistics for each coordinate and date, or optionally exports the data to a table in .csv format.
|
|
1339
1756
|
|
|
1340
1757
|
NOTE: The input RadGEEToolbox object must be a collection of singleband images, otherwise the resulting values will all be zero!
|
|
1341
|
-
|
|
1758
|
+
|
|
1342
1759
|
Args:
|
|
1343
1760
|
coordinates (list): Single tuple or a list of tuples with the coordinates as decimal degrees in the format of (longitude, latitude) for which to extract the statistics. NOTE the format needs to be [(x1, y1), (x2, y2), ...].
|
|
1344
1761
|
buffer_size (int, optional): The radial buffer size in meters around the coordinates. Defaults to 1.
|
|
@@ -1354,22 +1771,32 @@ class Sentinel2Collection:
|
|
|
1354
1771
|
.csv file: Optionally exports the data to a table in .csv format. If file_path is None, the function returns the DataFrame - otherwise the function will only export the csv file.
|
|
1355
1772
|
"""
|
|
1356
1773
|
img_collection = self
|
|
1357
|
-
#Create empty DataFrame to accumulate results
|
|
1774
|
+
# Create empty DataFrame to accumulate results
|
|
1358
1775
|
accumulated_df = pd.DataFrame()
|
|
1359
|
-
#Check if dates is None, if not use the dates provided
|
|
1776
|
+
# Check if dates is None, if not use the dates provided
|
|
1360
1777
|
if dates is None:
|
|
1361
1778
|
dates = img_collection.dates
|
|
1362
1779
|
else:
|
|
1363
1780
|
dates = dates
|
|
1364
|
-
#Iterate over the dates and extract the zonal statistics for each date
|
|
1781
|
+
# Iterate over the dates and extract the zonal statistics for each date
|
|
1365
1782
|
for date in dates:
|
|
1366
|
-
image = img_collection.collection.filter(
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
single_df.
|
|
1783
|
+
image = img_collection.collection.filter(
|
|
1784
|
+
ee.Filter.eq("Date_Filter", date)
|
|
1785
|
+
).first()
|
|
1786
|
+
single_df = Sentinel2Collection.extract_zonal_stats_from_buffer(
|
|
1787
|
+
image,
|
|
1788
|
+
coordinates,
|
|
1789
|
+
buffer_size=buffer_size,
|
|
1790
|
+
reducer_type=reducer_type,
|
|
1791
|
+
scale=scale,
|
|
1792
|
+
tileScale=tileScale,
|
|
1793
|
+
coordinate_names=coordinate_names,
|
|
1794
|
+
)
|
|
1795
|
+
single_df["Date"] = date
|
|
1796
|
+
single_df.set_index("Date", inplace=True)
|
|
1370
1797
|
accumulated_df = pd.concat([accumulated_df, single_df])
|
|
1371
|
-
#Return the DataFrame or export the data to a .csv file
|
|
1798
|
+
# Return the DataFrame or export the data to a .csv file
|
|
1372
1799
|
if file_path is None:
|
|
1373
1800
|
return accumulated_df
|
|
1374
1801
|
else:
|
|
1375
|
-
return accumulated_df.to_csv(f
|
|
1802
|
+
return accumulated_df.to_csv(f"{file_path}.csv")
|