RadGEEToolbox 1.6.4__py3-none-any.whl → 1.6.6__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.
@@ -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
+ is_scaled = ee.Algorithms.IsEqual(img.get('rgt:scaled'), 'sentinel2_sr')
22
+ scaled = img.select(_S2_SR_BANDS).multiply(_S2_SCALE)
23
+ out = img.addBands(scaled, None, True).set('rgt:scaled', 'sentinel2_sr')
24
+ return ee.Image(ee.Algorithms.If(is_scaled, img, out))
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__(self, start_date=None, end_date=None, tile=None, cloud_percentage_threshold=None, nodata_threshold=None, boundary=None, relative_orbit_number=None, collection=None):
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("Either provide all required fields (start_date, end_date, tile, cloud_percentage_threshold, nodata_threshold) or provide a collection.")
57
- if tile is None and boundary is None and relative_orbit_number is None and collection is None:
58
- raise ValueError("Provide either tile, boundary/geometry, or relative orbit number specifications to filter the image collection")
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('YYYY-MM-dd'))
140
- return image.set({'Date_Filter': date})
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(['B3', 'B8']) #green-NIR / green+NIR -- full NDWI image
156
- water = ndwi_calc.updateMask(ndwi_calc.gte(threshold)).rename('ndwi').copyProperties(image)
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(['B8', 'B4']) #NIR-RED/NIR+RED -- full NDVI image
172
- vegetation = ndvi_calc.updateMask(ndvi_calc.gte(threshold)).rename('ndvi').copyProperties(image) # subsets the image to just water pixels, 0.2 threshold for datasets
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(['B4', 'B11'])
188
- halite = halite_index.updateMask(halite_index.gte(threshold)).rename('halite').copyProperties(image)
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(['B11', 'B12'])
204
- gypsum = gypsum_index.updateMask(gypsum_index.gte(threshold)).rename('gypsum').copyProperties(image)
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(['B3', 'B2'])
220
- turbidity = NDTI.updateMask(NDTI.gte(threshold)).rename('ndti').copyProperties(image)
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(['B5', 'B4'])
236
- chlorophyll = chl_index.updateMask(chl_index.gte(threshold)).rename('2BDA').copyProperties(image)
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('SCL')
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('SCL')
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(['B3', 'B8']) #green-NIR / green+NIR -- full NDWI image
283
- water = image.updateMask(ndwi_calc.lt(threshold))
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('SCL')
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(['B4', 'B11'])
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(['B4', 'B11'])
331
- gypsum_index = image.normalizedDifference(['B11', 'B12'])
332
-
333
- mask = gypsum_index.updateMask(halite_index.lt(halite_threshold)).updateMask(gypsum_index.lt(gypsum_threshold)).rename('carbonate_muds').copyProperties(image)
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(['B3', 'B8']) #green-NIR / green+NIR -- full NDWI image
349
- water = image.updateMask(ndwi_calc.gte(threshold))
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(image, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12):
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 = final.select('area').updateMask(mask).rename(band_name).reduceRegion(
373
- reducer = ee.Reducer.sum(),
374
- geometry= geometry,
375
- scale=scale,
376
- maxPixels = maxPixels)
377
- return image.set(band_name, stats.get(band_name)) #calculates and returns summed pixel area as image property titled the same as the band name of the band used for calculation
378
-
379
- def PixelAreaSumCollection(self, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12):
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(lambda image: Sentinel2Collection.PixelAreaSum(image, band_name=band_name, geometry=geometry, threshold=threshold, scale=scale, maxPixels=maxPixels))
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('Date_Filter')
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('Date_Filter')
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 = sentinel2.filterDate(self.start_date, self.end_date).filter(ee.Filter.inList('MGRS_TILE', self.tile)).filter(ee.Filter.lte('NODATA_PIXEL_PERCENTAGE', self.nodata_threshold)) \
440
- .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', self.cloud_percentage_threshold)).map(Sentinel2Collection.image_dater).sort('Date_Filter')
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 = sentinel2.filterDate(self.start_date, self.end_date).filterBounds(self.boundary).filter(ee.Filter.lte('NODATA_PIXEL_PERCENTAGE', self.nodata_threshold)) \
453
- .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', self.cloud_percentage_threshold)).map(Sentinel2Collection.image_dater).sort('Date_Filter')
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 = sentinel2.filterDate(self.start_date, self.end_date).filter(ee.Filter.inList('SENSING_ORBIT_NUMBER', self.relative_orbit_number)).filter(ee.Filter.lte('NODATA_PIXEL_PERCENTAGE', self.nodata_threshold)) \
465
- .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', self.cloud_percentage_threshold)).map(Sentinel2Collection.image_dater).sort('Date_Filter')
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 = sentinel2.filterDate(self.start_date, self.end_date).filter(ee.Filter.inList('SENSING_ORBIT_NUMBER', self.relative_orbit_number)).filterBounds(self.boundary).filter(ee.Filter.lte('NODATA_PIXEL_PERCENTAGE', self.nodata_threshold)) \
477
- .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', self.cloud_percentage_threshold)).map(Sentinel2Collection.image_dater).sort('Date_Filter')
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('B3') and available_bands.contains('B8'):
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 = self.collection.map(lambda image: Sentinel2Collection.sentinel_ndwi_fn(image, threshold=threshold))
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('B4') and available_bands.contains('B8'):
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(lambda image: Sentinel2Collection.sentinel_ndvi_fn(image, threshold=threshold))
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('B4') and available_bands.contains('B11'):
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(lambda image: Sentinel2Collection.sentinel_halite_fn(image, threshold=threshold))
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('B11') and available_bands.contains('B12'):
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(lambda image: Sentinel2Collection.sentinel_gypsum_fn(image, threshold=threshold))
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('B3') and available_bands.contains('B2'):
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(lambda image: Sentinel2Collection.sentinel_turbidity_fn(image, threshold=threshold))
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('B5') and available_bands.contains('B4'):
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(lambda image: Sentinel2Collection.sentinel_chlorophyll_fn(image, threshold=threshold))
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(lambda image: Sentinel2Collection.MaskWaterS2ByNDWI(image, threshold=threshold))
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(lambda image: Sentinel2Collection.MaskToWaterS2ByNDWI(image, threshold=threshold))
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(collection=masked_collection)
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(collection=masked_collection)
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(lambda image: Sentinel2Collection.halite_mask(image, threshold=threshold))
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(lambda image: Sentinel2Collection.gypsum_and_halite_mask(image, halite_threshold=halite_threshold, gypsum_threshold=gypsum_threshold))
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('Date_Filter', img_date))
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 = ee.List(self.dates_list).cat(ee.List(img_col2.dates_list)).distinct()
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(ee.Filter.inList('Date_Filter', filtered_dates1))
944
- filtered_col1 = self.collection.filter(ee.Filter.inList('Date_Filter', filtered_col2.aggregate_array('Date_Filter')))
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('Date_Filter')
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('Date_Filter', date)).first()
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 = mosaic.copyProperties(img).set('Date_Filter', date).set('system:time_start', img.get('system:time_start'))
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('Date_Filter', date)
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('Date_Filter', date)
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('CLOUDY_PIXEL_PERCENTAGE')
996
- no_data_percentage = date_collection.aggregate_mean('NODATA_PIXEL_PERCENTAGE')
997
-
998
- props_of_interest = ['SPACECRAFT_NAME', 'SENSING_ORBIT_NUMBER', 'SENSING_ORBIT_DIRECTION', 'MISSION_ID', 'PLATFORM_IDENTIFIER', 'system:time_start']
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
- mosaic = mosaic.copyProperties(first_image, props_of_interest).set({
1001
- 'CLOUDY_PIXEL_PERCENTAGE': cloud_percentage,
1002
- 'NODATA_PIXEL_PERCENTAGE': no_data_percentage
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('Date_Filter').distinct()
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(ee_object, columns=None, remove_geom=True, sort_columns=False, **kwargs):
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(image, line, reducer="mean", n_segments=100, dist_interval=None, scale=None, crs=None, crsTransform=None, tileScale=1.0, to_pandas=False, **kwargs):
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(image, lines, line_names, reducer='mean', n_segments=None, dist_interval=10, to_pandas=True):
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(image=image, line=line, reducer=reducer, dist_interval=dist_interval, to_pandas=to_pandas)
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[['mean']]
1520
+ mean_column = transect_data[["mean"]]
1172
1521
  else:
1173
1522
  # Handle the case where 'mean' column is not present
1174
- print(f"{reducer} column not found in transect data for line {line_names[i]}")
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(image=image, line=line, reducer=reducer, n_segments=n_segments, to_pandas=to_pandas)
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[['mean']]
1539
+ mean_column = transect_data[["mean"]]
1183
1540
  else:
1184
1541
  # Handle the case where 'mean' column is not present
1185
- print(f"{reducer} column not found in transect data for line {line_names[i]}")
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(self, lines, line_names, save_folder_path, reducer='mean', n_segments=None, dist_interval=10, to_pandas=True):
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 #.collection
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(image, lines, line_names, reducer=reducer, n_segments=n_segments, dist_interval=dist_interval, to_pandas=to_pandas)
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'{save_folder_path}{image_id}_transects.csv')
1226
- print(f'{image_id}_transects saved to csv')
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(image, coordinates, buffer_size=1, reducer_type='mean', scale=10, tileScale=1, coordinate_names=None):
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 (isinstance(coordinates, list) and all(isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates)):
1254
- raise ValueError("Coordinates must be a list of tuples with two elements each (latitude, longitude).")
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(isinstance(name, str) for name in coordinate_names):
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("coordinate_names must have the same length as the coordinates list.")
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 = [ee.Feature(ee.Geometry.Point([coord[0], coord[1]]).buffer(buffer_size), {'name': str(coordinate_names[i])}) for i, coord in enumerate(coordinates)]
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 == 'mean':
1679
+ if reducer_type == "mean":
1279
1680
  img_stats = image.reduceRegions(
1280
- collection=features,
1281
- reducer=ee.Reducer.mean(),
1282
- scale=scale,
1283
- tileScale=tileScale)
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['features']:
1288
- names.append(feature['properties']['name'])
1289
- means.append(feature['properties']['mean'])
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 == 'median':
1693
+ elif reducer_type == "median":
1292
1694
  img_stats = image.reduceRegions(
1293
- collection=features,
1294
- reducer=ee.Reducer.median(),
1295
- scale=scale,
1296
- tileScale=tileScale)
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['features']:
1301
- names.append(feature['properties']['name'])
1302
- medians.append(feature['properties']['median'])
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 == 'min':
1707
+ elif reducer_type == "min":
1305
1708
  img_stats = image.reduceRegions(
1306
- collection=features,
1307
- reducer=ee.Reducer.min(),
1308
- scale=scale,
1309
- tileScale=tileScale)
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['features']:
1314
- names.append(feature['properties']['name'])
1315
- mins.append(feature['properties']['min'])
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 == 'max':
1721
+ elif reducer_type == "max":
1318
1722
  img_stats = image.reduceRegions(
1319
- collection=features,
1320
- reducer=ee.Reducer.max(),
1321
- scale=scale,
1322
- tileScale=tileScale)
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['features']:
1327
- names.append(feature['properties']['name'])
1328
- maxs.append(feature['properties']['max'])
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("reducer_type must be one of 'mean', 'median', 'min', or 'max'.")
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(self, coordinates, buffer_size=1, reducer_type='mean', scale=10, tileScale=1, coordinate_names=None, file_path=None, dates=None):
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(ee.Filter.eq('Date_Filter', date)).first()
1367
- single_df = Sentinel2Collection.extract_zonal_stats_from_buffer(image, coordinates, buffer_size=buffer_size, reducer_type=reducer_type, scale=scale, tileScale=tileScale, coordinate_names=coordinate_names)
1368
- single_df['Date'] = date
1369
- single_df.set_index('Date', inplace=True)
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'{file_path}.csv')
1802
+ return accumulated_df.to_csv(f"{file_path}.csv")