RadGEEToolbox 1.6.7__py3-none-any.whl → 1.6.9__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/LandsatCollection.py +1066 -207
- RadGEEToolbox/Sentinel1Collection.py +566 -210
- RadGEEToolbox/Sentinel2Collection.py +953 -217
- RadGEEToolbox/VisParams.py +173 -85
- RadGEEToolbox/__init__.py +1 -1
- {radgeetoolbox-1.6.7.dist-info → radgeetoolbox-1.6.9.dist-info}/METADATA +32 -9
- radgeetoolbox-1.6.9.dist-info/RECORD +12 -0
- radgeetoolbox-1.6.7.dist-info/RECORD +0 -12
- {radgeetoolbox-1.6.7.dist-info → radgeetoolbox-1.6.9.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.6.7.dist-info → radgeetoolbox-1.6.9.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.6.7.dist-info → radgeetoolbox-1.6.9.dist-info}/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ import numpy as np
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
# ---- Reflectance scaling for Sentinel-2 L2A (HARMONIZED) ----
|
|
7
|
-
_S2_SR_BANDS = ["B1","B2","B3","B4","B5","B6","B7","B8","B8A","B9","
|
|
7
|
+
_S2_SR_BANDS = ["B1","B2","B3","B4","B5","B6","B7","B8","B8A","B9","B11","B12"]
|
|
8
8
|
_S2_SCALE = 0.0001 # offset 0.0
|
|
9
9
|
|
|
10
10
|
def _scale_s2_sr(img):
|
|
@@ -151,6 +151,12 @@ class Sentinel2Collection:
|
|
|
151
151
|
self.gypsum_threshold = -1
|
|
152
152
|
self.turbidity_threshold = -1
|
|
153
153
|
self.chlorophyll_threshold = 0.5
|
|
154
|
+
self.ndsi_threshold = -1
|
|
155
|
+
self.evi_threshold = -1
|
|
156
|
+
self.savi_threshold = -1.5
|
|
157
|
+
self.msavi_threshold = -1
|
|
158
|
+
self.ndmi_threshold = -1
|
|
159
|
+
self.nbr_threshold = -1
|
|
154
160
|
|
|
155
161
|
self._geometry_masked_collection = None
|
|
156
162
|
self._geometry_masked_out_collection = None
|
|
@@ -164,10 +170,17 @@ class Sentinel2Collection:
|
|
|
164
170
|
self._ndwi = None
|
|
165
171
|
self._mndwi = None
|
|
166
172
|
self._ndvi = None
|
|
173
|
+
self._ndsi = None
|
|
174
|
+
self._evi = None
|
|
175
|
+
self._savi = None
|
|
176
|
+
self._msavi = None
|
|
177
|
+
self._ndmi = None
|
|
178
|
+
self._nbr = None
|
|
167
179
|
self._halite = None
|
|
168
180
|
self._gypsum = None
|
|
169
181
|
self._turbidity = None
|
|
170
182
|
self._chlorophyll = None
|
|
183
|
+
self._albedo = None
|
|
171
184
|
self._MosaicByDate = None
|
|
172
185
|
self._PixelAreaSumCollection = None
|
|
173
186
|
self._Reflectance = None
|
|
@@ -252,6 +265,35 @@ class Sentinel2Collection:
|
|
|
252
265
|
) # subsets the image to just water pixels, 0.2 threshold for datasets
|
|
253
266
|
return vegetation
|
|
254
267
|
|
|
268
|
+
@staticmethod
|
|
269
|
+
def sentinel_broadband_albedo_fn(image, snow_free=True):
|
|
270
|
+
"""
|
|
271
|
+
Calculates broadband albedo for Sentinel-2 images, based on Li et al., 2018
|
|
272
|
+
(https://doi.org/10.1016/j.rse.2018.08.025) for snow-free or snow-included conditions.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
image (ee.Image): input ee.Image
|
|
276
|
+
snow_free (bool): If True, calculates albedo for snow-free conditions. If False, calculates for snow-included conditions.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
ee.Image: broadband albedo ee.Image
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
ValueError: if snow_free argument is not True or False
|
|
283
|
+
"""
|
|
284
|
+
# Conversion using Li et al., 2018 as reference
|
|
285
|
+
MSI_expression_snow_free = '0.2688*b("B2") + 0.0362*b("B3") + 0.1501*b("B4") + 0.3045*b("B8A") + 0.1644*b("B11") + 0.0356*b("B12") - 0.0049'
|
|
286
|
+
MSI_expression_snow_included = '-0.1992*b("B2") + 2.3002*b("B3") + -1.9121*b("B4") + 0.6715*b("B8A") - 2.2728*b("B11") + 1.9341*b("B12") - 0.0001'
|
|
287
|
+
# If spacecraft is Landsat 5 TM, use the correct expression,
|
|
288
|
+
# otherwise treat as OLI and copy properties after renaming band to "albedo"
|
|
289
|
+
if snow_free == True:
|
|
290
|
+
albedo = image.expression(MSI_expression_snow_free).rename("albedo").copyProperties(image)
|
|
291
|
+
elif snow_free == False:
|
|
292
|
+
albedo = image.expression(MSI_expression_snow_included).rename("albedo").copyProperties(image)
|
|
293
|
+
else:
|
|
294
|
+
raise ValueError("snow_free argument must be True or False")
|
|
295
|
+
return albedo
|
|
296
|
+
|
|
255
297
|
@staticmethod
|
|
256
298
|
def sentinel_halite_fn(image, threshold):
|
|
257
299
|
"""
|
|
@@ -329,6 +371,113 @@ class Sentinel2Collection:
|
|
|
329
371
|
.copyProperties(image)
|
|
330
372
|
)
|
|
331
373
|
return chlorophyll
|
|
374
|
+
|
|
375
|
+
@staticmethod
|
|
376
|
+
def sentinel_ndsi_fn(image, threshold):
|
|
377
|
+
"""
|
|
378
|
+
Calculates the Normalized Difference Snow Index (NDSI) for Sentinel2 images. Masks image based on threshold.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
image (ee.Image): input ee.Image
|
|
382
|
+
threshold (float): value between -1 and 1 where NDSI pixels greater than threshold will be masked.
|
|
383
|
+
Returns:
|
|
384
|
+
ee.Image: NDSI ee.Image
|
|
385
|
+
"""
|
|
386
|
+
ndsi_calc = image.normalizedDifference(["B3", "B11"])
|
|
387
|
+
ndsi = ndsi_calc.updateMask(ndsi_calc.gte(threshold)).rename("ndsi").copyProperties(image).set("threshold", threshold)
|
|
388
|
+
return ndsi
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def sentinel_evi_fn(image, threshold, gain_factor=2.5, l=1, c1=6, c2=7.5):
|
|
392
|
+
"""
|
|
393
|
+
Calculates the Enhanced Vegetation Index (EVI) for Sentinel-2 images. Masks image based on threshold.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
image (ee.Image): input ee.Image
|
|
397
|
+
threshold (float): value between -1 and 1 where EVI pixels less than threshold will be masked.
|
|
398
|
+
gain_factor (float, optional): Gain factor, typically set to 2.5. Defaults to 2.5.
|
|
399
|
+
l (float, optional): Canopy background adjustment factor, typically set to 1. Defaults to 1.
|
|
400
|
+
c1 (float, optional): Coefficient for the aerosol resistance term, typically set to 6. Defaults to 6.
|
|
401
|
+
c2 (float, optional): Coefficient for the aerosol resistance term, typically set to 7.5. Defaults to 7.5.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
ee.Image: EVI ee.Image
|
|
405
|
+
"""
|
|
406
|
+
evi_expression = f'{gain_factor} * ((b("B8") - b("B4")) / (b("B8") + {c1} * b("B4") - {c2} * b("B2") + {l}))'
|
|
407
|
+
evi_calc = image.expression(evi_expression)
|
|
408
|
+
evi = evi_calc.updateMask(evi_calc.gte(threshold)).rename("evi").copyProperties(image).set("threshold", threshold)
|
|
409
|
+
return evi
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def sentinel_savi_fn(image, threshold, l=0.5):
|
|
413
|
+
"""
|
|
414
|
+
Calculates the Soil-Adjusted Vegetation Index (SAVI) for Sentinel2 images. Masks image based on threshold.
|
|
415
|
+
See Huete, 1988 - https://doi.org/10.1016/0034-4257(88)90106-X
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
image (ee.Image): input ee.Image
|
|
419
|
+
threshold (float): value between -1 and 1 where SAVI pixels less than threshold will be masked.
|
|
420
|
+
l (float, optional): Soil brightness correction factor, typically set to 0.5. Defaults to 0.5.
|
|
421
|
+
Returns:
|
|
422
|
+
ee.Image: SAVI ee.Image
|
|
423
|
+
"""
|
|
424
|
+
savi_expression = f'((b("B8") - b("B4")) / (b("B8") + b("B4") + {l})) * (1 + {l})'
|
|
425
|
+
savi_calc = image.expression(savi_expression)
|
|
426
|
+
savi = savi_calc.updateMask(savi_calc.gte(threshold)).rename("savi").copyProperties(image).set("threshold", threshold)
|
|
427
|
+
return savi
|
|
428
|
+
|
|
429
|
+
@staticmethod
|
|
430
|
+
def sentinel_msavi_fn(image, threshold):
|
|
431
|
+
"""
|
|
432
|
+
Calculates the Modified Soil-Adjusted Vegetation Index (MSAVI) for Sentinel-2 images. Masks image based on threshold.
|
|
433
|
+
See Qi et al., 1994 - https://doi.org/10.1016/0034-4257(94)90134-1
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
image (ee.Image): input ee.Image
|
|
437
|
+
threshold (float): value between -1 and 1 where MSAVI pixels less than threshold will be masked
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
ee.Image: MSAVI ee.Image
|
|
441
|
+
"""
|
|
442
|
+
msavi_expression = '0.5 * (2 * b("B8") + 1 - ((2 * b("B8") + 1) ** 2 - 8 * (b("B8") - b("B4"))) ** 0.5)'
|
|
443
|
+
msavi_calc = image.expression(msavi_expression)
|
|
444
|
+
msavi = msavi_calc.updateMask(msavi_calc.gte(threshold)).rename("msavi").copyProperties(image).set("threshold", threshold)
|
|
445
|
+
return msavi
|
|
446
|
+
|
|
447
|
+
@staticmethod
|
|
448
|
+
def sentinel_ndmi_fn(image, threshold):
|
|
449
|
+
"""
|
|
450
|
+
Calculates the Normalized Difference Moisture Index (NDMI) for Sentinel-2 images. Masks image based on threshold.
|
|
451
|
+
See Wilson & Sader, 2002 - https://doi.org/10.1016/S0034-4257(02)00074-7
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
image (ee.Image): input ee.Image
|
|
455
|
+
threshold (float): value between -1 and 1 where NDMI pixels less than threshold will be masked
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
ee.Image: NDMI ee.Image
|
|
459
|
+
"""
|
|
460
|
+
ndmi_expression = '(b("B8") - b("B11")) / (b("B8") + b("B11"))'
|
|
461
|
+
ndmi_calc = image.expression(ndmi_expression)
|
|
462
|
+
ndmi = ndmi_calc.updateMask(ndmi_calc.gte(threshold)).rename("ndmi").copyProperties(image).set("threshold", threshold)
|
|
463
|
+
return ndmi
|
|
464
|
+
|
|
465
|
+
@staticmethod
|
|
466
|
+
def sentinel_nbr_fn(image, threshold):
|
|
467
|
+
"""
|
|
468
|
+
Calculates the Normalized Burn Ratio (NBR) for Sentinel-2 images. Masks image based on threshold.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
image (ee.Image): input ee.Image
|
|
472
|
+
threshold (float): value between -1 and 1 where NBR pixels less than threshold will be masked.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
ee.Image: NBR ee.Image
|
|
476
|
+
"""
|
|
477
|
+
nbr_expression = '(b("B8") - b("B12")) / (b("B8") + b("B12"))'
|
|
478
|
+
nbr_calc = image.expression(nbr_expression)
|
|
479
|
+
nbr = nbr_calc.updateMask(nbr_calc.gte(threshold)).rename("nbr").copyProperties(image).set("threshold", threshold)
|
|
480
|
+
return nbr
|
|
332
481
|
|
|
333
482
|
@staticmethod
|
|
334
483
|
def MaskCloudsS2(image):
|
|
@@ -457,59 +606,88 @@ class Sentinel2Collection:
|
|
|
457
606
|
image, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12
|
|
458
607
|
):
|
|
459
608
|
"""
|
|
460
|
-
Calculates the summation of area for pixels of interest (above a specific threshold)
|
|
461
|
-
|
|
609
|
+
Calculates the summation of area for pixels of interest (above a specific threshold) in a geometry
|
|
610
|
+
and store the value as image property (matching name of chosen band). If multiple band names are provided in a list,
|
|
611
|
+
the function will calculate area for each band in the list and store each as a separate property.
|
|
612
|
+
|
|
613
|
+
NOTE: The resulting value has units of square meters.
|
|
462
614
|
|
|
463
615
|
Args:
|
|
464
616
|
image (ee.Image): input ee.Image
|
|
465
|
-
band_name: name of band (string) for calculating area.
|
|
466
|
-
geometry: ee.Geometry object denoting area to clip to for area calculation
|
|
467
|
-
threshold: integer threshold to specify masking of pixels below threshold (defaults to -1).
|
|
468
|
-
scale: integer scale of image resolution (meters) (defaults to 10)
|
|
469
|
-
maxPixels: integer denoting maximum number of pixels for calculations
|
|
617
|
+
band_name (string or list of strings): name of band(s) (string) for calculating area. If providing multiple band names, pass as a list of strings.
|
|
618
|
+
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
|
|
619
|
+
threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1). If providing multiple band names, the same threshold will be applied to all bands. Best practice in this case is to mask the bands prior to passing to this function and leave threshold at default of -1.
|
|
620
|
+
scale (int): integer scale of image resolution (meters) (defaults to 10)
|
|
621
|
+
maxPixels (int): integer denoting maximum number of pixels for calculations
|
|
470
622
|
|
|
471
623
|
Returns:
|
|
472
|
-
ee.Image: Image with area calculation stored as property matching name of band
|
|
624
|
+
ee.Image: ee.Image with area calculation in square meters stored as property matching name of band
|
|
473
625
|
"""
|
|
626
|
+
# Ensure band_name is a server-side ee.List for consistent processing. Wrap band_name in a list if it's a single string.
|
|
627
|
+
bands = ee.List(band_name) if isinstance(band_name, list) else ee.List([band_name])
|
|
628
|
+
# Create an image representing the area of each pixel in square meters
|
|
474
629
|
area_image = ee.Image.pixelArea()
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
.
|
|
480
|
-
.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
630
|
+
|
|
631
|
+
# Function to iterate over each band and calculate area, storing the result as a property on the image
|
|
632
|
+
def calculate_and_set_area(band, img_accumulator):
|
|
633
|
+
# Explcitly cast inputs to expected types
|
|
634
|
+
img_accumulator = ee.Image(img_accumulator)
|
|
635
|
+
band = ee.String(band)
|
|
636
|
+
|
|
637
|
+
# Create a mask from the input image for the current band
|
|
638
|
+
mask = img_accumulator.select(band).gte(threshold)
|
|
639
|
+
# Combine the original image with the area image
|
|
640
|
+
final = img_accumulator.addBands(area_image)
|
|
641
|
+
|
|
642
|
+
# Calculation of area for a given band, utilizing other inputs
|
|
643
|
+
stats = (
|
|
644
|
+
final.select("area").updateMask(mask)
|
|
645
|
+
.rename(band) # renames 'area' to band name like 'ndwi'
|
|
646
|
+
.reduceRegion(
|
|
647
|
+
reducer=ee.Reducer.sum(),
|
|
648
|
+
geometry=geometry,
|
|
649
|
+
scale=scale,
|
|
650
|
+
maxPixels=maxPixels,
|
|
651
|
+
)
|
|
486
652
|
)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
653
|
+
# Retrieving the area value from the stats dictionary with stats.get(band), as the band name is now the key
|
|
654
|
+
reduced_area = stats.get(band)
|
|
655
|
+
# Checking whether the calculated area is valid and replaces with 0 if not. This avoids breaking the loop for erroneous images.
|
|
656
|
+
area_value = ee.Algorithms.If(reduced_area, reduced_area, 0)
|
|
657
|
+
|
|
658
|
+
# Set the property on the image, named after the band
|
|
659
|
+
return img_accumulator.set(band, area_value)
|
|
660
|
+
|
|
661
|
+
# Call to iterate the calculate_and_set_area function over the list of bands, starting with the original image
|
|
662
|
+
final_image = ee.Image(bands.iterate(calculate_and_set_area, image))
|
|
663
|
+
return final_image
|
|
491
664
|
|
|
492
665
|
def PixelAreaSumCollection(
|
|
493
|
-
self, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12
|
|
666
|
+
self, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
494
667
|
):
|
|
495
668
|
"""
|
|
496
|
-
Calculates the summation of area for pixels of interest (above a specific threshold)
|
|
497
|
-
within a geometry and
|
|
498
|
-
image collection.
|
|
499
|
-
|
|
669
|
+
Calculates the geodesic summation of area for pixels of interest (above a specific threshold)
|
|
670
|
+
within a geometry and stores the value as an image property (matching name of chosen band) for an entire
|
|
671
|
+
image collection. Optionally exports the area data to a CSV file.
|
|
672
|
+
|
|
673
|
+
NOTE: The resulting value has units of square meters.
|
|
500
674
|
|
|
501
675
|
Args:
|
|
502
|
-
band_name: name of band (string) for calculating area.
|
|
503
|
-
geometry: ee.Geometry object denoting area to clip to for area calculation
|
|
504
|
-
threshold: integer threshold to specify masking of pixels below threshold (defaults to -1).
|
|
505
|
-
scale: integer scale of image resolution (meters) (defaults to 10)
|
|
506
|
-
maxPixels: integer denoting maximum number of pixels for calculations
|
|
676
|
+
band_name (string or list of strings): name of band(s) (string) for calculating area. If providing multiple band names, pass as a list of strings.
|
|
677
|
+
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
|
|
678
|
+
threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1). If providing multiple band names, the same threshold will be applied to all bands. Best practice in this case is to mask the bands prior to passing to this function and leave threshold at default of -1.
|
|
679
|
+
scale (int): integer scale of image resolution (meters) (defaults to 10)
|
|
680
|
+
maxPixels (int): integer denoting maximum number of pixels for calculations
|
|
681
|
+
output_type (str): 'ImageCollection' to return an ee.ImageCollection, 'Sentinel2Collection' to return a Sentinel2Collection object (defaults to 'ImageCollection')
|
|
682
|
+
area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
|
|
507
683
|
|
|
508
684
|
Returns:
|
|
509
|
-
ee.
|
|
685
|
+
ee.ImageCollection or Sentinel2Collection: Image collection of images with area calculation (square meters) stored as property matching name of band. Type of output depends on output_type argument.
|
|
510
686
|
"""
|
|
687
|
+
# If the area calculation has not been computed for this Sentinel2Collection instance, the area will be calculated for the provided bands
|
|
511
688
|
if self._PixelAreaSumCollection is None:
|
|
512
689
|
collection = self.collection
|
|
690
|
+
# Area calculation for each image in the collection, using the PixelAreaSum function
|
|
513
691
|
AreaCollection = collection.map(
|
|
514
692
|
lambda image: Sentinel2Collection.PixelAreaSum(
|
|
515
693
|
image,
|
|
@@ -520,8 +698,38 @@ class Sentinel2Collection:
|
|
|
520
698
|
maxPixels=maxPixels,
|
|
521
699
|
)
|
|
522
700
|
)
|
|
701
|
+
# Storing the result in the instance variable to avoid redundant calculations
|
|
523
702
|
self._PixelAreaSumCollection = AreaCollection
|
|
524
|
-
|
|
703
|
+
|
|
704
|
+
# If an export path is provided, the area data will be exported to a CSV file
|
|
705
|
+
if area_data_export_path:
|
|
706
|
+
Sentinel2Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=band_name, file_path=area_data_export_path+'.csv')
|
|
707
|
+
|
|
708
|
+
# Returning the result in the desired format based on output_type argument or raising an error for invalid input
|
|
709
|
+
if output_type == 'ImageCollection':
|
|
710
|
+
return self._PixelAreaSumCollection
|
|
711
|
+
elif output_type == 'Sentinel2Collection':
|
|
712
|
+
return Sentinel2Collection(collection=self._PixelAreaSumCollection)
|
|
713
|
+
else:
|
|
714
|
+
raise ValueError("output_type must be 'ImageCollection' or 'Sentinel2Collection'")
|
|
715
|
+
|
|
716
|
+
def merge(self, other):
|
|
717
|
+
"""
|
|
718
|
+
Merges the current Sentinel2Collection with another Sentinel2Collection, where images/bands with the same date are combined to a single multiband image.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
other (Sentinel2Collection): Another Sentinel2Collection to merge with current collection.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
Sentinel2Collection: A new Sentinel2Collection containing images from both collections.
|
|
725
|
+
"""
|
|
726
|
+
# Checking if 'other' is an instance of Sentinel2Collection
|
|
727
|
+
if not isinstance(other, Sentinel2Collection):
|
|
728
|
+
raise ValueError("The 'other' parameter must be an instance of Sentinel2Collection.")
|
|
729
|
+
|
|
730
|
+
# Merging the collections using the .combine() method
|
|
731
|
+
merged_collection = self.collection.combine(other.collection)
|
|
732
|
+
return Sentinel2Collection(collection=merged_collection)
|
|
525
733
|
|
|
526
734
|
@property
|
|
527
735
|
def dates_list(self):
|
|
@@ -551,6 +759,60 @@ class Sentinel2Collection:
|
|
|
551
759
|
dates = self._dates_list.getInfo()
|
|
552
760
|
self._dates = dates
|
|
553
761
|
return self._dates
|
|
762
|
+
|
|
763
|
+
def ExportProperties(self, property_names, file_path=None):
|
|
764
|
+
"""
|
|
765
|
+
Fetches and returns specified properties from each image in the collection as a list, and returns a pandas DataFrame and optionally saves the results to a csv file.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
property_names (list or str): A property name or list of property names to retrieve. The 'Date_Filter' property is always included to provide temporal context.
|
|
769
|
+
file_path (str, optional): If provided, the function will save the resulting DataFrame to a CSV file at this path. Defaults to None.
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
pd.DataFrame: A pandas DataFrame containing the requested properties for each image, sorted chronologically by 'Date_Filter'.
|
|
773
|
+
"""
|
|
774
|
+
# Ensure property_names is a list for consistent processing
|
|
775
|
+
if isinstance(property_names, str):
|
|
776
|
+
property_names = [property_names]
|
|
777
|
+
|
|
778
|
+
# Ensure properties are included without duplication, including 'Date_Filter'
|
|
779
|
+
all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
|
|
780
|
+
|
|
781
|
+
# Defining the helper function to create features with specified properties
|
|
782
|
+
def create_feature_with_properties(image):
|
|
783
|
+
"""A function to map over the collection and store the image properties as an ee.Feature.
|
|
784
|
+
Args:
|
|
785
|
+
image (ee.Image): An image from the collection.
|
|
786
|
+
Returns:
|
|
787
|
+
ee.Feature: A feature containing the specified properties from the image.
|
|
788
|
+
"""
|
|
789
|
+
properties = image.toDictionary(all_properties_to_fetch)
|
|
790
|
+
return ee.Feature(None, properties)
|
|
791
|
+
|
|
792
|
+
# Map the feature creation function over the server-side collection.
|
|
793
|
+
# The result is an ee.FeatureCollection where each feature holds the properties of one image.
|
|
794
|
+
mapped_collection = self.collection.map(create_feature_with_properties)
|
|
795
|
+
# Explicitly cast to ee.FeatureCollection for clarity
|
|
796
|
+
feature_collection = ee.FeatureCollection(mapped_collection)
|
|
797
|
+
|
|
798
|
+
# Use the existing ee_to_df static method. This performs the single .getInfo() call
|
|
799
|
+
# and converts the structured result directly to a pandas DataFrame.
|
|
800
|
+
df = Sentinel2Collection.ee_to_df(feature_collection, columns=all_properties_to_fetch)
|
|
801
|
+
|
|
802
|
+
# Sort by date for a clean, chronological output.
|
|
803
|
+
if 'Date_Filter' in df.columns:
|
|
804
|
+
df = df.sort_values(by='Date_Filter').reset_index(drop=True)
|
|
805
|
+
|
|
806
|
+
# Check condition for saving to CSV
|
|
807
|
+
if file_path:
|
|
808
|
+
# Check whether file_path ends with .csv, if not, append it
|
|
809
|
+
if not file_path.lower().endswith('.csv'):
|
|
810
|
+
file_path += '.csv'
|
|
811
|
+
# Save DataFrame to CSV
|
|
812
|
+
df.to_csv(file_path, index=True)
|
|
813
|
+
print(f"Properties saved to {file_path}")
|
|
814
|
+
|
|
815
|
+
return df
|
|
554
816
|
|
|
555
817
|
def get_filtered_collection(self):
|
|
556
818
|
"""
|
|
@@ -827,6 +1089,278 @@ class Sentinel2Collection:
|
|
|
827
1089
|
)
|
|
828
1090
|
)
|
|
829
1091
|
return Sentinel2Collection(collection=col)
|
|
1092
|
+
|
|
1093
|
+
@property
|
|
1094
|
+
def evi(self):
|
|
1095
|
+
"""
|
|
1096
|
+
Property attribute to calculate and access the EVI (Enhanced Vegetation Index) imagery of the Sentinel2Collection.
|
|
1097
|
+
This property initiates the calculation of EVI and caches the result. The calculation is performed only once when
|
|
1098
|
+
the property is first accessed, and the cached result is returned on subsequent accesses.
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1102
|
+
"""
|
|
1103
|
+
if self._evi is None:
|
|
1104
|
+
self._evi = self.evi_collection(self.evi_threshold)
|
|
1105
|
+
return self._evi
|
|
1106
|
+
|
|
1107
|
+
def evi_collection(self, threshold):
|
|
1108
|
+
"""
|
|
1109
|
+
Function to calculate the EVI (Enhanced Vegetation Index) and return collection as class object, allows specifying threshold(s) for masking.
|
|
1110
|
+
This function can be called as a method but is called by default when using the evi property attribute.
|
|
1111
|
+
|
|
1112
|
+
Args:
|
|
1113
|
+
threshold (float): specify threshold for EVI function (values less than threshold are masked)
|
|
1114
|
+
|
|
1115
|
+
Returns:
|
|
1116
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1117
|
+
"""
|
|
1118
|
+
first_image = self.collection.first()
|
|
1119
|
+
available_bands = first_image.bandNames()
|
|
1120
|
+
if available_bands.contains("B4") and available_bands.contains("B8") and available_bands.contains("B2"):
|
|
1121
|
+
pass
|
|
1122
|
+
else:
|
|
1123
|
+
raise ValueError("Insufficient Bands for evi calculation")
|
|
1124
|
+
col = self.collection.map(
|
|
1125
|
+
lambda image: Sentinel2Collection.sentinel_evi_fn(
|
|
1126
|
+
image, threshold=threshold
|
|
1127
|
+
)
|
|
1128
|
+
)
|
|
1129
|
+
return Sentinel2Collection(collection=col)
|
|
1130
|
+
|
|
1131
|
+
@property
|
|
1132
|
+
def savi(self):
|
|
1133
|
+
"""
|
|
1134
|
+
Property attribute to calculate and access the SAVI (Soil Adjusted Vegetation Index) imagery of the Sentinel2Collection.
|
|
1135
|
+
This property initiates the calculation of SAVI and caches the result. The calculation is performed only once when the
|
|
1136
|
+
property is first accessed, and the cached result is returned on subsequent accesses.
|
|
1137
|
+
|
|
1138
|
+
Returns:
|
|
1139
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1140
|
+
"""
|
|
1141
|
+
if self._savi is None:
|
|
1142
|
+
self._savi = self.savi_collection(self.savi_threshold)
|
|
1143
|
+
return self._savi
|
|
1144
|
+
|
|
1145
|
+
def savi_collection(self, threshold, l=0.5):
|
|
1146
|
+
"""
|
|
1147
|
+
Function to calculate the SAVI (Soil Adjusted Vegetation Index) and return collection as class object, allows specifying threshold(s) for masking.
|
|
1148
|
+
This function can be called as a method but is called by default when using the savi property attribute.
|
|
1149
|
+
|
|
1150
|
+
Args:
|
|
1151
|
+
threshold (float): specify threshold for SAVI function (values less than threshold are masked)
|
|
1152
|
+
l (float, optional): Soil brightness correction factor, typically set to 0.5 for intermediate vegetation cover. Defaults to 0.5.
|
|
1153
|
+
|
|
1154
|
+
Returns:
|
|
1155
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1156
|
+
"""
|
|
1157
|
+
first_image = self.collection.first()
|
|
1158
|
+
available_bands = first_image.bandNames()
|
|
1159
|
+
if available_bands.contains("B4") and available_bands.contains("B8"):
|
|
1160
|
+
pass
|
|
1161
|
+
else:
|
|
1162
|
+
raise ValueError("Insufficient Bands for savi calculation")
|
|
1163
|
+
col = self.collection.map(
|
|
1164
|
+
lambda image: Sentinel2Collection.sentinel_savi_fn(
|
|
1165
|
+
image, threshold=threshold, l=l
|
|
1166
|
+
)
|
|
1167
|
+
)
|
|
1168
|
+
return Sentinel2Collection(collection=col)
|
|
1169
|
+
|
|
1170
|
+
@property
|
|
1171
|
+
def msavi(self):
|
|
1172
|
+
"""
|
|
1173
|
+
Property attribute to calculate and access the MSAVI (Modified Soil Adjusted Vegetation Index) imagery of the Sentinel2Collection.
|
|
1174
|
+
This property initiates the calculation of MSAVI and caches the result. The calculation is performed only once when the property
|
|
1175
|
+
is first accessed, and the cached result is returned on subsequent accesses.
|
|
1176
|
+
|
|
1177
|
+
Returns:
|
|
1178
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1179
|
+
"""
|
|
1180
|
+
if self._msavi is None:
|
|
1181
|
+
self._msavi = self.msavi_collection(self.msavi_threshold)
|
|
1182
|
+
return self._msavi
|
|
1183
|
+
|
|
1184
|
+
def msavi_collection(self, threshold):
|
|
1185
|
+
"""
|
|
1186
|
+
Function to calculate the MSAVI (Modified Soil Adjusted Vegetation Index) and return collection as class object, allows specifying threshold(s) for masking.
|
|
1187
|
+
This function can be called as a method but is called by default when using the msavi property attribute.
|
|
1188
|
+
|
|
1189
|
+
Args:
|
|
1190
|
+
threshold (float): specify threshold for MSAVI function (values less than threshold are masked)
|
|
1191
|
+
|
|
1192
|
+
Returns:
|
|
1193
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1194
|
+
"""
|
|
1195
|
+
first_image = self.collection.first()
|
|
1196
|
+
available_bands = first_image.bandNames()
|
|
1197
|
+
if available_bands.contains("B4") and available_bands.contains("B8"):
|
|
1198
|
+
pass
|
|
1199
|
+
else:
|
|
1200
|
+
raise ValueError("Insufficient Bands for msavi calculation")
|
|
1201
|
+
col = self.collection.map(
|
|
1202
|
+
lambda image: Sentinel2Collection.sentinel_msavi_fn(
|
|
1203
|
+
image, threshold=threshold
|
|
1204
|
+
)
|
|
1205
|
+
)
|
|
1206
|
+
return Sentinel2Collection(collection=col)
|
|
1207
|
+
|
|
1208
|
+
@property
|
|
1209
|
+
def ndmi(self):
|
|
1210
|
+
"""
|
|
1211
|
+
Property attribute to calculate and access the NDMI (Normalized Difference Moisture Index) imagery of the Sentinel2Collection.
|
|
1212
|
+
This property initiates the calculation of NDMI and caches the result. The calculation is performed only once when the property
|
|
1213
|
+
is first accessed, and the cached result is returned on subsequent accesses.
|
|
1214
|
+
|
|
1215
|
+
Returns:
|
|
1216
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1217
|
+
"""
|
|
1218
|
+
if self._ndmi is None:
|
|
1219
|
+
self._ndmi = self.ndmi_collection(self.ndmi_threshold)
|
|
1220
|
+
return self._ndmi
|
|
1221
|
+
|
|
1222
|
+
def ndmi_collection(self, threshold):
|
|
1223
|
+
"""
|
|
1224
|
+
Function to calculate the NDMI (Normalized Difference Moisture Index) and return collection as class object, allows specifying threshold(s) for masking.
|
|
1225
|
+
This function can be called as a method but is called by default when using the ndmi property attribute.
|
|
1226
|
+
|
|
1227
|
+
Args:
|
|
1228
|
+
threshold (float): specify threshold for NDMI function (values less than threshold are masked)
|
|
1229
|
+
Returns:
|
|
1230
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1231
|
+
"""
|
|
1232
|
+
first_image = self.collection.first()
|
|
1233
|
+
available_bands = first_image.bandNames()
|
|
1234
|
+
if available_bands.contains("B8") and available_bands.contains("B11"):
|
|
1235
|
+
pass
|
|
1236
|
+
else:
|
|
1237
|
+
raise ValueError("Insufficient Bands for ndmi calculation")
|
|
1238
|
+
col = self.collection.map(
|
|
1239
|
+
lambda image: Sentinel2Collection.sentinel_ndmi_fn(
|
|
1240
|
+
image, threshold=threshold
|
|
1241
|
+
)
|
|
1242
|
+
)
|
|
1243
|
+
return Sentinel2Collection(collection=col)
|
|
1244
|
+
|
|
1245
|
+
@property
|
|
1246
|
+
def nbr(self):
|
|
1247
|
+
"""
|
|
1248
|
+
Property attribute to calculate and access the NBR (Normalized Burn Ratio) imagery of the Sentinel2Collection.
|
|
1249
|
+
This property initiates the calculation of NBR and caches the result. The calculation is performed only once
|
|
1250
|
+
when the property is first accessed, and the cached result is returned on subsequent accesses.
|
|
1251
|
+
|
|
1252
|
+
Returns:
|
|
1253
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1254
|
+
"""
|
|
1255
|
+
if self._nbr is None:
|
|
1256
|
+
self._nbr = self.nbr_collection(self.nbr_threshold)
|
|
1257
|
+
return self._nbr
|
|
1258
|
+
|
|
1259
|
+
def nbr_collection(self, threshold):
|
|
1260
|
+
"""
|
|
1261
|
+
Function to calculate the NBR (Normalized Burn Ratio) and return collection as class object, allows specifying threshold(s) for masking.
|
|
1262
|
+
This function can be called as a method but is called by default when using the nbr property attribute.
|
|
1263
|
+
|
|
1264
|
+
Args:
|
|
1265
|
+
threshold (float): specify threshold for NBR function (values less than threshold are masked)
|
|
1266
|
+
|
|
1267
|
+
Returns:
|
|
1268
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1269
|
+
"""
|
|
1270
|
+
first_image = self.collection.first()
|
|
1271
|
+
available_bands = first_image.bandNames()
|
|
1272
|
+
if available_bands.contains("B8") and available_bands.contains("B12"):
|
|
1273
|
+
pass
|
|
1274
|
+
else:
|
|
1275
|
+
raise ValueError("Insufficient Bands for nbr calculation")
|
|
1276
|
+
col = self.collection.map(
|
|
1277
|
+
lambda image: Sentinel2Collection.sentinel_nbr_fn(
|
|
1278
|
+
image, threshold=threshold
|
|
1279
|
+
)
|
|
1280
|
+
)
|
|
1281
|
+
return Sentinel2Collection(collection=col)
|
|
1282
|
+
|
|
1283
|
+
@property
|
|
1284
|
+
def ndsi(self):
|
|
1285
|
+
"""
|
|
1286
|
+
Property attribute to calculate and access the NDSI (Normalized Difference Snow Index) imagery of the Sentinel2Collection.
|
|
1287
|
+
This property initiates the calculation of NDSI and caches the result. The calculation is performed only once when the
|
|
1288
|
+
property is first accessed, and the cached result is returned on subsequent accesses.
|
|
1289
|
+
|
|
1290
|
+
Returns:
|
|
1291
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1292
|
+
"""
|
|
1293
|
+
if self._ndsi is None:
|
|
1294
|
+
self._ndsi = self.ndsi_collection(self.ndsi_threshold)
|
|
1295
|
+
return self._ndsi
|
|
1296
|
+
|
|
1297
|
+
def ndsi_collection(self, threshold):
|
|
1298
|
+
"""
|
|
1299
|
+
Function to calculate the NDSI (Normalized Difference Snow Index) and return collection as class object, allows specifying threshold(s) for masking.
|
|
1300
|
+
This function can be called as a method but is called
|
|
1301
|
+
by default when using the ndsi property attribute.
|
|
1302
|
+
|
|
1303
|
+
Args:
|
|
1304
|
+
threshold (float): specify threshold for NDSI function (values less than threshold are masked)
|
|
1305
|
+
|
|
1306
|
+
Returns:
|
|
1307
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1308
|
+
"""
|
|
1309
|
+
first_image = self.collection.first()
|
|
1310
|
+
available_bands = first_image.bandNames()
|
|
1311
|
+
if available_bands.contains("B3") and available_bands.contains("B11"):
|
|
1312
|
+
pass
|
|
1313
|
+
else:
|
|
1314
|
+
raise ValueError("Insufficient Bands for ndsi calculation")
|
|
1315
|
+
col = self.collection.map(
|
|
1316
|
+
lambda image: Sentinel2Collection.sentinel_ndsi_fn(
|
|
1317
|
+
image, threshold=threshold
|
|
1318
|
+
)
|
|
1319
|
+
)
|
|
1320
|
+
return Sentinel2Collection(collection=col)
|
|
1321
|
+
|
|
1322
|
+
@property
|
|
1323
|
+
def albedo(self):
|
|
1324
|
+
"""
|
|
1325
|
+
Property attribute to calculate albedo imagery for snow-free conditions, based on Li et al., 2018 (https://doi.org/10.1016/j.rse.2018.08.025).
|
|
1326
|
+
Use `albedo_collection(snow_free=False)` for images with snow present. This property initiates the calculation of albedo and caches the result.
|
|
1327
|
+
The calculation is performed only once when the property is first accessed, and the cached result is returned on subsequent accesses.
|
|
1328
|
+
|
|
1329
|
+
Returns:
|
|
1330
|
+
LandsatCollection: A LandsatCollection image collection
|
|
1331
|
+
"""
|
|
1332
|
+
if self._albedo is None:
|
|
1333
|
+
self._albedo = self.albedo_collection(snow_free=True)
|
|
1334
|
+
return self._albedo
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def albedo_collection(self, snow_free=True):
|
|
1338
|
+
"""
|
|
1339
|
+
Calculates albedo for snow or snow-free conditions and returns collection as class object, allows specifying threshold(s) for masking.
|
|
1340
|
+
This function can be called as a method but is called by default when using the ndwi property attribute.
|
|
1341
|
+
Albedo calculation based on Li et al., 2018 (https://doi.org/10.1016/j.rse.2018.08.025).
|
|
1342
|
+
|
|
1343
|
+
Args:
|
|
1344
|
+
snow_free (bool): If True, applies a snow mask to the albedo calculation. Defaults to True.
|
|
1345
|
+
|
|
1346
|
+
Returns:
|
|
1347
|
+
LandsatCollection: A LandsatCollection image collection
|
|
1348
|
+
"""
|
|
1349
|
+
first_image = self.collection.first()
|
|
1350
|
+
available_bands = first_image.bandNames()
|
|
1351
|
+
if (
|
|
1352
|
+
available_bands.contains("B2")
|
|
1353
|
+
and available_bands.contains("B3")
|
|
1354
|
+
and available_bands.contains("B4")
|
|
1355
|
+
and available_bands.contains("B8A")
|
|
1356
|
+
and available_bands.contains("B11")
|
|
1357
|
+
and available_bands.contains("B12")
|
|
1358
|
+
):
|
|
1359
|
+
pass
|
|
1360
|
+
else:
|
|
1361
|
+
raise ValueError("Insufficient Bands for albedo calculation")
|
|
1362
|
+
col = self.collection.map(lambda image:Sentinel2Collection.sentinel_broadband_albedo_fn(image, snow_free=snow_free))
|
|
1363
|
+
return Sentinel2Collection(collection=col)
|
|
830
1364
|
|
|
831
1365
|
@property
|
|
832
1366
|
def halite(self):
|
|
@@ -1407,7 +1941,8 @@ class Sentinel2Collection:
|
|
|
1407
1941
|
to_pandas=False,
|
|
1408
1942
|
**kwargs,
|
|
1409
1943
|
):
|
|
1410
|
-
"""
|
|
1944
|
+
"""
|
|
1945
|
+
Extracts transect from an image. Adapted from the geemap package (https://geemap.org/common/#geemap.common.extract_transect).
|
|
1411
1946
|
|
|
1412
1947
|
Args:
|
|
1413
1948
|
image (ee.Image): The image to extract transect from.
|
|
@@ -1481,9 +2016,10 @@ class Sentinel2Collection:
|
|
|
1481
2016
|
dist_interval=10,
|
|
1482
2017
|
to_pandas=True,
|
|
1483
2018
|
):
|
|
1484
|
-
"""
|
|
1485
|
-
|
|
1486
|
-
|
|
2019
|
+
"""
|
|
2020
|
+
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
|
|
2021
|
+
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.
|
|
2022
|
+
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.
|
|
1487
2023
|
|
|
1488
2024
|
Args:
|
|
1489
2025
|
image (ee.Image): ee.Image object to use for calculating transect values.
|
|
@@ -1556,52 +2092,201 @@ class Sentinel2Collection:
|
|
|
1556
2092
|
self,
|
|
1557
2093
|
lines,
|
|
1558
2094
|
line_names,
|
|
1559
|
-
save_folder_path,
|
|
1560
2095
|
reducer="mean",
|
|
2096
|
+
dist_interval= 10,
|
|
1561
2097
|
n_segments=None,
|
|
1562
|
-
|
|
1563
|
-
|
|
2098
|
+
scale=10,
|
|
2099
|
+
processing_mode='aggregated',
|
|
2100
|
+
save_folder_path=None,
|
|
2101
|
+
sampling_method='line',
|
|
2102
|
+
point_buffer_radius=5
|
|
1564
2103
|
):
|
|
1565
|
-
"""
|
|
1566
|
-
|
|
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.
|
|
1568
|
-
Naming conventions for the csv files follows as: "image-date_transects.csv"
|
|
2104
|
+
"""
|
|
2105
|
+
Computes and returns pixel values along transects for each image in a collection.
|
|
1569
2106
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
save_folder_path (str): The path to the folder where the csv files will be saved.
|
|
1574
|
-
reducer (str): The ee reducer to use. Defaults to 'mean'.
|
|
1575
|
-
n_segments (int): The number of segments that the LineString will be split into. Defaults to None.
|
|
1576
|
-
dist_interval (float): The distance interval in meters used for splitting the LineString. If specified, the n_segments parameter will be ignored. Defaults to 10.
|
|
1577
|
-
to_pandas (bool): Whether to convert the result to a pandas dataframe. Defaults to True.
|
|
2107
|
+
This iterative function generates time-series data along one or more lines, and
|
|
2108
|
+
supports two different geometric sampling methods ('line' and 'buffered_point')
|
|
2109
|
+
for maximum flexibility and performance.
|
|
1578
2110
|
|
|
1579
|
-
|
|
1580
|
-
|
|
2111
|
+
There are two processing modes available, aggregated and iterative:
|
|
2112
|
+
- 'aggregated' (default; suggested): Fast, server-side processing. Fetches all results
|
|
2113
|
+
in a single request. Highly recommended. Returns a dictionary of pandas DataFrames.
|
|
2114
|
+
- 'iterative': Slower, client-side loop that processes one image at a time.
|
|
2115
|
+
Kept for backward compatibility (effectively depreciated). Returns None and saves individual CSVs.
|
|
2116
|
+
This method is not recommended unless absolutely necessary, as it is less efficient and may be subject to client-side timeouts.
|
|
2117
|
+
|
|
2118
|
+
Args:
|
|
2119
|
+
lines (list): A list of one or more ee.Geometry.LineString objects that
|
|
2120
|
+
define the transects.
|
|
2121
|
+
line_names (list): A list of string names for each transect. The length
|
|
2122
|
+
of this list must match the length of the `lines` list.
|
|
2123
|
+
reducer (str, optional): The name of the ee.Reducer to apply at each
|
|
2124
|
+
transect point (e.g., 'mean', 'median', 'first'). Defaults to 'mean'.
|
|
2125
|
+
dist_interval (float, optional): The distance interval in meters for
|
|
2126
|
+
sampling points along each transect. Will be overridden if `n_segments` is provided.
|
|
2127
|
+
Defaults to 10. Recommended to increase this value when using the
|
|
2128
|
+
'line' processing method, or else you may get blank rows.
|
|
2129
|
+
n_segments (int, optional): The number of equal-length segments to split
|
|
2130
|
+
each transect line into for sampling. This parameter overrides `dist_interval`.
|
|
2131
|
+
Defaults to None.
|
|
2132
|
+
scale (int, optional): The nominal scale in meters for the reduction,
|
|
2133
|
+
which should typically match the pixel resolution of the imagery.
|
|
2134
|
+
Defaults to 10.
|
|
2135
|
+
processing_mode (str, optional): The method for processing the collection.
|
|
2136
|
+
- 'aggregated' (default): Fast, server-side processing. Fetches all
|
|
2137
|
+
results in a single request. Highly recommended. Returns a dictionary
|
|
2138
|
+
of pandas DataFrames.
|
|
2139
|
+
- 'iterative': Slower, client-side loop that processes one image at a
|
|
2140
|
+
time. Kept for backward compatibility. Returns None and saves
|
|
2141
|
+
individual CSVs.
|
|
2142
|
+
save_folder_path (str, optional): If provided, the function will save the
|
|
2143
|
+
resulting transect data to CSV files. The behavior depends on the
|
|
2144
|
+
`processing_mode`:
|
|
2145
|
+
- In 'aggregated' mode, one CSV is saved for each transect,
|
|
2146
|
+
containing all dates. (e.g., 'MyTransect_transects.csv').
|
|
2147
|
+
- In 'iterative' mode, one CSV is saved for each date,
|
|
2148
|
+
containing all transects. (e.g., '2022-06-15_transects.csv').
|
|
2149
|
+
sampling_method (str, optional): The geometric method used for sampling.
|
|
2150
|
+
- 'line' (default): Reduces all pixels intersecting each small line
|
|
2151
|
+
segment. This can be unreliable and produce blank rows if
|
|
2152
|
+
`dist_interval` is too small relative to the `scale`.
|
|
2153
|
+
- 'buffered_point': Reduces all pixels within a buffer around the
|
|
2154
|
+
midpoint of each line segment. This method is more robust and
|
|
2155
|
+
reliably avoids blank rows, but may not reduce all pixels along a line segment.
|
|
2156
|
+
point_buffer_radius (int, optional): The radius in meters for the buffer
|
|
2157
|
+
when `sampling_method` is 'buffered_point'. Defaults to 5.
|
|
1581
2158
|
|
|
1582
2159
|
Returns:
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
2160
|
+
dict or None:
|
|
2161
|
+
- If `processing_mode` is 'aggregated', returns a dictionary where each
|
|
2162
|
+
key is a transect name and each value is a pandas DataFrame. In the
|
|
2163
|
+
DataFrame, the index is the distance along the transect and each
|
|
2164
|
+
column represents an image date. Optionally saves CSV files if
|
|
2165
|
+
`save_folder_path` is provided.
|
|
2166
|
+
- If `processing_mode` is 'iterative', returns None as it saves
|
|
2167
|
+
files directly.
|
|
2168
|
+
|
|
2169
|
+
Raises:
|
|
2170
|
+
ValueError: If `lines` and `line_names` have different lengths, or if
|
|
2171
|
+
an unknown reducer or processing mode is specified.
|
|
2172
|
+
"""
|
|
2173
|
+
# Validating inputs
|
|
2174
|
+
if len(lines) != len(line_names):
|
|
2175
|
+
raise ValueError("'lines' and 'line_names' must have the same number of elements.")
|
|
2176
|
+
### Current, server-side processing method ###
|
|
2177
|
+
if processing_mode == 'aggregated':
|
|
2178
|
+
# Validating reducer type
|
|
1588
2179
|
try:
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
2180
|
+
ee_reducer = getattr(ee.Reducer, reducer)()
|
|
2181
|
+
except AttributeError:
|
|
2182
|
+
raise ValueError(f"Unknown reducer: '{reducer}'.")
|
|
2183
|
+
### Function to extract transects for a single image
|
|
2184
|
+
def get_transects_for_image(image):
|
|
2185
|
+
image_date = image.get('Date_Filter')
|
|
2186
|
+
# Initialize an empty list to hold all transect FeatureCollections
|
|
2187
|
+
all_transects_for_image = ee.List([])
|
|
2188
|
+
# Looping through each line and processing
|
|
2189
|
+
for i, line in enumerate(lines):
|
|
2190
|
+
# Index line and name
|
|
2191
|
+
line_name = line_names[i]
|
|
2192
|
+
# Determine maxError based on image projection, used for geometry operations
|
|
2193
|
+
maxError = image.projection().nominalScale().divide(5)
|
|
2194
|
+
# Calculate effective distance interval
|
|
2195
|
+
length = line.length(maxError) # using maxError here ensures consistency with cutLines
|
|
2196
|
+
# Determine effective distance interval based on n_segments or dist_interval
|
|
2197
|
+
effective_dist_interval = ee.Algorithms.If(
|
|
2198
|
+
n_segments,
|
|
2199
|
+
length.divide(n_segments),
|
|
2200
|
+
dist_interval or 30 # Defaults to 30 if both are None
|
|
2201
|
+
)
|
|
2202
|
+
# Generate distances along the line(s) for segmentation
|
|
2203
|
+
distances = ee.List.sequence(0, length, effective_dist_interval)
|
|
2204
|
+
# Segmenting the line into smaller lines at the specified distances
|
|
2205
|
+
cut_lines_geoms = line.cutLines(distances, maxError).geometries()
|
|
2206
|
+
# Function to create features with distance attributes
|
|
2207
|
+
# Adjusted to ensure consistent return types
|
|
2208
|
+
def set_dist_attr(l):
|
|
2209
|
+
# l is a list: [geometry, distance]
|
|
2210
|
+
# Extracting geometry portion of line
|
|
2211
|
+
geom_segment = ee.Geometry(ee.List(l).get(0))
|
|
2212
|
+
# Extracting distance value for attribute
|
|
2213
|
+
distance = ee.Number(ee.List(l).get(1))
|
|
2214
|
+
### Determine final geometry based on sampling method
|
|
2215
|
+
# If the sampling method is 'buffered_point',
|
|
2216
|
+
# create a buffered point feature at the centroid of each segment,
|
|
2217
|
+
# otherwise create a line feature
|
|
2218
|
+
final_feature = ee.Algorithms.If(
|
|
2219
|
+
ee.String(sampling_method).equals('buffered_point'),
|
|
2220
|
+
# True Case: Create the buffered point feature
|
|
2221
|
+
ee.Feature(
|
|
2222
|
+
geom_segment.centroid(maxError).buffer(point_buffer_radius),
|
|
2223
|
+
{'distance': distance}
|
|
2224
|
+
),
|
|
2225
|
+
# False Case: Create the line segment feature
|
|
2226
|
+
ee.Feature(geom_segment, {'distance': distance})
|
|
2227
|
+
)
|
|
2228
|
+
# Return either the line segment feature or the buffered point feature
|
|
2229
|
+
return final_feature
|
|
2230
|
+
# Creating a FeatureCollection of the cut lines with distance attributes
|
|
2231
|
+
# Using map to apply the set_dist_attr function to each cut line geometry
|
|
2232
|
+
line_features = ee.FeatureCollection(cut_lines_geoms.zip(distances).map(set_dist_attr))
|
|
2233
|
+
# Reducing the image over the line features to get transect values
|
|
2234
|
+
transect_fc = image.reduceRegions(
|
|
2235
|
+
collection=line_features, reducer=ee_reducer, scale=scale
|
|
2236
|
+
)
|
|
2237
|
+
# Adding image date and line name properties to each feature
|
|
2238
|
+
def set_props(feature):
|
|
2239
|
+
return feature.set({'image_date': image_date, 'transect_name': line_name})
|
|
2240
|
+
# Append to the list of all transects for this image
|
|
2241
|
+
all_transects_for_image = all_transects_for_image.add(transect_fc.map(set_props))
|
|
2242
|
+
# Combine all transect FeatureCollections into a single FeatureCollection and flatten
|
|
2243
|
+
# Flatten is used to merge the list of FeatureCollections into one
|
|
2244
|
+
return ee.FeatureCollection(all_transects_for_image).flatten()
|
|
2245
|
+
# Map the function over the entire image collection and flatten the results
|
|
2246
|
+
results_fc = ee.FeatureCollection(self.collection.map(get_transects_for_image)).flatten()
|
|
2247
|
+
# Convert the results to a pandas DataFrame
|
|
2248
|
+
df = Sentinel2Collection.ee_to_df(results_fc, remove_geom=True)
|
|
2249
|
+
# Check if the DataFrame is empty
|
|
2250
|
+
if df.empty:
|
|
2251
|
+
print("Warning: No transect data was generated.")
|
|
2252
|
+
return {}
|
|
2253
|
+
# Initialize dictionary to hold output DataFrames for each transect
|
|
2254
|
+
output_dfs = {}
|
|
2255
|
+
# Loop through each unique transect name and create a pivot table
|
|
2256
|
+
for name in sorted(df['transect_name'].unique()):
|
|
2257
|
+
transect_df = df[df['transect_name'] == name]
|
|
2258
|
+
pivot_df = transect_df.pivot(index='distance', columns='image_date', values=reducer)
|
|
2259
|
+
pivot_df.columns.name = 'Date'
|
|
2260
|
+
output_dfs[name] = pivot_df
|
|
2261
|
+
# Optionally save each transect DataFrame to CSV
|
|
2262
|
+
if save_folder_path:
|
|
2263
|
+
for transect_name, transect_df in output_dfs.items():
|
|
2264
|
+
safe_filename = "".join(x for x in transect_name if x.isalnum() or x in "._-")
|
|
2265
|
+
file_path = f"{save_folder_path}{safe_filename}_transects.csv"
|
|
2266
|
+
transect_df.to_csv(file_path)
|
|
2267
|
+
print(f"Saved transect data to {file_path}")
|
|
2268
|
+
|
|
2269
|
+
return output_dfs
|
|
2270
|
+
|
|
2271
|
+
### old, depreciated iterative client-side processing method ###
|
|
2272
|
+
elif processing_mode == 'iterative':
|
|
2273
|
+
if not save_folder_path:
|
|
2274
|
+
raise ValueError("`save_folder_path` is required for 'iterative' processing mode.")
|
|
2275
|
+
|
|
2276
|
+
image_collection_dates = self.dates
|
|
2277
|
+
for i, date in enumerate(image_collection_dates):
|
|
2278
|
+
try:
|
|
2279
|
+
print(f"Processing image {i+1}/{len(image_collection_dates)}: {date}")
|
|
2280
|
+
image = self.image_grab(i)
|
|
2281
|
+
transects_df = Sentinel2Collection.transect(
|
|
2282
|
+
image, lines, line_names, reducer, n_segments, dist_interval, to_pandas=True
|
|
2283
|
+
)
|
|
2284
|
+
transects_df.to_csv(f"{save_folder_path}{date}_transects.csv")
|
|
2285
|
+
print(f"{date}_transects saved to csv")
|
|
2286
|
+
except Exception as e:
|
|
2287
|
+
print(f"An error occurred while processing image {i+1}: {e}")
|
|
2288
|
+
else:
|
|
2289
|
+
raise ValueError("`processing_mode` must be 'iterative' or 'aggregated'.")
|
|
1605
2290
|
|
|
1606
2291
|
@staticmethod
|
|
1607
2292
|
def extract_zonal_stats_from_buffer(
|
|
@@ -1614,37 +2299,35 @@ class Sentinel2Collection:
|
|
|
1614
2299
|
coordinate_names=None,
|
|
1615
2300
|
):
|
|
1616
2301
|
"""
|
|
1617
|
-
Function to extract spatial statistics from an image for a list of coordinates, providing individual statistics for each location.
|
|
2302
|
+
Function to extract spatial statistics from an image for a list or single set of (long, lat) coordinates, providing individual statistics for each location.
|
|
1618
2303
|
A radial buffer is applied around each coordinate to extract the statistics, which defaults to 1 meter.
|
|
1619
2304
|
The function returns a pandas DataFrame with the statistics for each coordinate.
|
|
1620
2305
|
|
|
2306
|
+
NOTE: Be sure the coordinates are provided as longitude, latitude (x, y) tuples!
|
|
2307
|
+
|
|
1621
2308
|
Args:
|
|
1622
|
-
image (ee.Image): The image from which to extract
|
|
1623
|
-
coordinates (list
|
|
1624
|
-
buffer_size (int, optional): The radial buffer size
|
|
1625
|
-
reducer_type (str, optional): The
|
|
1626
|
-
scale (int, optional): The scale
|
|
1627
|
-
tileScale (int, optional): The tile scale
|
|
1628
|
-
coordinate_names (list, optional): A list of
|
|
2309
|
+
image (ee.Image): The image from which to extract statistics. Should be single-band.
|
|
2310
|
+
coordinates (list or tuple): A single (lon, lat) tuple or a list of (lon, lat) tuples.
|
|
2311
|
+
buffer_size (int, optional): The radial buffer size in meters. Defaults to 1.
|
|
2312
|
+
reducer_type (str, optional): The ee.Reducer to use ('mean', 'median', 'min', etc.). Defaults to 'mean'.
|
|
2313
|
+
scale (int, optional): The scale in meters for the reduction. Defaults to 10.
|
|
2314
|
+
tileScale (int, optional): The tile scale factor. Defaults to 1.
|
|
2315
|
+
coordinate_names (list, optional): A list of names for the coordinates.
|
|
1629
2316
|
|
|
1630
2317
|
Returns:
|
|
1631
|
-
pd.DataFrame: A pandas DataFrame with the
|
|
2318
|
+
pd.DataFrame: A pandas DataFrame with the image's 'Date_Filter' as the index and a
|
|
2319
|
+
column for each coordinate location.
|
|
1632
2320
|
"""
|
|
1633
|
-
|
|
1634
|
-
# Check if coordinates is a single tuple and convert it to a list of tuples if necessary
|
|
1635
2321
|
if isinstance(coordinates, tuple) and len(coordinates) == 2:
|
|
1636
2322
|
coordinates = [coordinates]
|
|
1637
2323
|
elif not (
|
|
1638
2324
|
isinstance(coordinates, list)
|
|
1639
|
-
and all(
|
|
1640
|
-
isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates
|
|
1641
|
-
)
|
|
2325
|
+
and all(isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates)
|
|
1642
2326
|
):
|
|
1643
2327
|
raise ValueError(
|
|
1644
|
-
"Coordinates must be a list of tuples with two elements each (
|
|
2328
|
+
"Coordinates must be a list of tuples with two elements each (longitude, latitude)."
|
|
1645
2329
|
)
|
|
1646
2330
|
|
|
1647
|
-
# Check if coordinate_names is a list of strings
|
|
1648
2331
|
if coordinate_names is not None:
|
|
1649
2332
|
if not isinstance(coordinate_names, list) or not all(
|
|
1650
2333
|
isinstance(name, str) for name in coordinate_names
|
|
@@ -1657,146 +2340,199 @@ class Sentinel2Collection:
|
|
|
1657
2340
|
else:
|
|
1658
2341
|
coordinate_names = [f"Location {i+1}" for i in range(len(coordinates))]
|
|
1659
2342
|
|
|
1660
|
-
|
|
1661
|
-
def check_singleband(image):
|
|
1662
|
-
band_count = image.bandNames().size()
|
|
1663
|
-
return ee.Algorithms.If(band_count.eq(1), image, ee.Image.constant(0))
|
|
2343
|
+
image_date = image.get('Date_Filter')
|
|
1664
2344
|
|
|
1665
|
-
# image = ee.Image(check_singleband(image))
|
|
1666
|
-
image = ee.Image(check_singleband(image))
|
|
1667
|
-
|
|
1668
|
-
# Convert coordinates to ee.Geometry.Point, buffer them, and add label/name to feature
|
|
1669
2345
|
points = [
|
|
1670
2346
|
ee.Feature(
|
|
1671
|
-
ee.Geometry.Point(
|
|
1672
|
-
{"
|
|
2347
|
+
ee.Geometry.Point(coord).buffer(buffer_size),
|
|
2348
|
+
{"location_name": str(name)},
|
|
1673
2349
|
)
|
|
1674
|
-
for
|
|
2350
|
+
for coord, name in zip(coordinates, coordinate_names)
|
|
1675
2351
|
]
|
|
1676
|
-
# Create a feature collection from the buffered points
|
|
1677
2352
|
features = ee.FeatureCollection(points)
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
elif reducer_type == "min":
|
|
1708
|
-
img_stats = image.reduceRegions(
|
|
1709
|
-
collection=features,
|
|
1710
|
-
reducer=ee.Reducer.min(),
|
|
1711
|
-
scale=scale,
|
|
1712
|
-
tileScale=tileScale,
|
|
1713
|
-
)
|
|
1714
|
-
min_values = img_stats.getInfo()
|
|
1715
|
-
mins = []
|
|
1716
|
-
names = []
|
|
1717
|
-
for feature in min_values["features"]:
|
|
1718
|
-
names.append(feature["properties"]["name"])
|
|
1719
|
-
mins.append(feature["properties"]["min"])
|
|
1720
|
-
organized_values = pd.DataFrame([mins], columns=names)
|
|
1721
|
-
elif reducer_type == "max":
|
|
1722
|
-
img_stats = image.reduceRegions(
|
|
1723
|
-
collection=features,
|
|
1724
|
-
reducer=ee.Reducer.max(),
|
|
1725
|
-
scale=scale,
|
|
1726
|
-
tileScale=tileScale,
|
|
1727
|
-
)
|
|
1728
|
-
max_values = img_stats.getInfo()
|
|
1729
|
-
maxs = []
|
|
1730
|
-
names = []
|
|
1731
|
-
for feature in max_values["features"]:
|
|
1732
|
-
names.append(feature["properties"]["name"])
|
|
1733
|
-
maxs.append(feature["properties"]["max"])
|
|
1734
|
-
organized_values = pd.DataFrame([maxs], columns=names)
|
|
1735
|
-
else:
|
|
1736
|
-
raise ValueError(
|
|
1737
|
-
"reducer_type must be one of 'mean', 'median', 'min', or 'max'."
|
|
1738
|
-
)
|
|
1739
|
-
return organized_values
|
|
2353
|
+
|
|
2354
|
+
try:
|
|
2355
|
+
reducer = getattr(ee.Reducer, reducer_type)()
|
|
2356
|
+
except AttributeError:
|
|
2357
|
+
raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
|
|
2358
|
+
|
|
2359
|
+
stats_fc = image.reduceRegions(
|
|
2360
|
+
collection=features,
|
|
2361
|
+
reducer=reducer,
|
|
2362
|
+
scale=scale,
|
|
2363
|
+
tileScale=tileScale,
|
|
2364
|
+
)
|
|
2365
|
+
|
|
2366
|
+
df = Sentinel2Collection.ee_to_df(stats_fc, remove_geom=True)
|
|
2367
|
+
|
|
2368
|
+
if df.empty:
|
|
2369
|
+
print("Warning: No results returned. The points may not intersect the image.")
|
|
2370
|
+
empty_df = pd.DataFrame(columns=coordinate_names)
|
|
2371
|
+
empty_df.index.name = 'Date'
|
|
2372
|
+
return empty_df
|
|
2373
|
+
|
|
2374
|
+
if reducer_type not in df.columns:
|
|
2375
|
+
print(f"Warning: Reducer type '{reducer_type}' not found in results. Returning raw data.")
|
|
2376
|
+
return df
|
|
2377
|
+
|
|
2378
|
+
pivot_df = df.pivot(columns='location_name', values=reducer_type)
|
|
2379
|
+
pivot_df['Date'] = image_date.getInfo() # .getInfo() is needed here as it's a server object
|
|
2380
|
+
pivot_df = pivot_df.set_index('Date')
|
|
2381
|
+
return pivot_df
|
|
1740
2382
|
|
|
1741
2383
|
def iterate_zonal_stats(
|
|
1742
2384
|
self,
|
|
1743
|
-
|
|
1744
|
-
buffer_size=1,
|
|
2385
|
+
geometries,
|
|
1745
2386
|
reducer_type="mean",
|
|
1746
2387
|
scale=10,
|
|
2388
|
+
geometry_names=None,
|
|
2389
|
+
buffer_size=1,
|
|
1747
2390
|
tileScale=1,
|
|
1748
|
-
coordinate_names=None,
|
|
1749
|
-
file_path=None,
|
|
1750
2391
|
dates=None,
|
|
2392
|
+
file_path=None
|
|
1751
2393
|
):
|
|
1752
2394
|
"""
|
|
1753
|
-
|
|
1754
|
-
|
|
2395
|
+
Iterates over a collection of images and extracts spatial statistics (defaults to mean) for a given list of geometries or coordinates. Individual statistics are calculated for each geometry or coordinate provided.
|
|
2396
|
+
When coordinates are provided, a radial buffer is applied around each coordinate to extract the statistics, where the size of the buffer is determined by the buffer_size argument (defaults to 1 meter).
|
|
1755
2397
|
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.
|
|
1756
2398
|
|
|
1757
|
-
NOTE: The input RadGEEToolbox object must be a collection of singleband images, otherwise the resulting values will all be zero!
|
|
1758
|
-
|
|
1759
2399
|
Args:
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
Returns:
|
|
1770
|
-
pd.DataFrame: A pandas DataFrame with
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
for date in dates:
|
|
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,
|
|
2400
|
+
geometries (ee.Geometry, ee.Feature, ee.FeatureCollection, list, or tuple): Input geometries for which to extract statistics. Can be a single ee.Geometry, an ee.Feature, an ee.FeatureCollection, a list of (lon, lat) tuples, or a list of ee.Geometry objects. Be careful to NOT provide coordinates as (lat, lon)!
|
|
2401
|
+
reducer_type (str, optional): The ee.Reducer to use, e.g., 'mean', 'median', 'max', 'sum'. Defaults to 'mean'. Any ee.Reducer method can be used.
|
|
2402
|
+
scale (int, optional): Pixel scale in meters for the reduction. Defaults to 10.
|
|
2403
|
+
geometry_names (list, optional): A list of string names for the geometries. If provided, must match the number of geometries. Defaults to None.
|
|
2404
|
+
buffer_size (int, optional): Radial buffer in meters around coordinates. Defaults to 1.
|
|
2405
|
+
tileScale (int, optional): A scaling factor to reduce aggregation tile size. Defaults to 1.
|
|
2406
|
+
dates (list, optional): A list of date strings ('YYYY-MM-DD') for filtering the collection, such that only images from these dates are included for zonal statistic retrieval. Defaults to None, which uses all dates in the collection.
|
|
2407
|
+
file_path (str, optional): File path to save the output CSV.
|
|
2408
|
+
|
|
2409
|
+
Returns:
|
|
2410
|
+
pd.DataFrame or None: A pandas DataFrame with dates as the index and coordinate names
|
|
2411
|
+
as columns. Returns None if using 'iterative' mode with file_path.
|
|
2412
|
+
|
|
2413
|
+
Raises:
|
|
2414
|
+
ValueError: If input parameters are invalid.
|
|
2415
|
+
TypeError: If geometries input type is unsupported.
|
|
2416
|
+
"""
|
|
2417
|
+
img_collection_obj = self
|
|
2418
|
+
# Filter collection by dates if provided
|
|
2419
|
+
if dates:
|
|
2420
|
+
img_collection_obj = Sentinel2Collection(
|
|
2421
|
+
collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
|
|
1794
2422
|
)
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2423
|
+
|
|
2424
|
+
# Initialize variables
|
|
2425
|
+
features = None
|
|
2426
|
+
validated_coordinates = []
|
|
2427
|
+
|
|
2428
|
+
# Function to standardize feature names if no names are provided
|
|
2429
|
+
def set_standard_name(feature):
|
|
2430
|
+
has_geo_name = feature.get('geo_name')
|
|
2431
|
+
has_name = feature.get('name')
|
|
2432
|
+
has_index = feature.get('system:index')
|
|
2433
|
+
new_name = ee.Algorithms.If(
|
|
2434
|
+
has_geo_name, has_geo_name,
|
|
2435
|
+
ee.Algorithms.If(has_name, has_name,
|
|
2436
|
+
ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
|
|
2437
|
+
return feature.set({'geo_name': new_name})
|
|
2438
|
+
|
|
2439
|
+
if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
|
|
2440
|
+
features = ee.FeatureCollection(geometries)
|
|
2441
|
+
if geometry_names:
|
|
2442
|
+
print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
|
|
2443
|
+
|
|
2444
|
+
elif isinstance(geometries, ee.Geometry):
|
|
2445
|
+
name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
|
|
2446
|
+
features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
|
|
2447
|
+
|
|
2448
|
+
elif isinstance(geometries, list):
|
|
2449
|
+
if not geometries: # Handle empty list case
|
|
2450
|
+
raise ValueError("'geometries' list cannot be empty.")
|
|
2451
|
+
|
|
2452
|
+
# Case: List of coordinates
|
|
2453
|
+
if all(isinstance(i, tuple) for i in geometries):
|
|
2454
|
+
validated_coordinates = geometries
|
|
2455
|
+
if geometry_names is None:
|
|
2456
|
+
geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
|
|
2457
|
+
elif len(geometry_names) != len(validated_coordinates):
|
|
2458
|
+
raise ValueError("geometry_names must have the same length as the coordinates list.")
|
|
2459
|
+
points = [
|
|
2460
|
+
ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
|
|
2461
|
+
for coord, name in zip(validated_coordinates, geometry_names)
|
|
2462
|
+
]
|
|
2463
|
+
features = ee.FeatureCollection(points)
|
|
2464
|
+
|
|
2465
|
+
# Case: List of Geometries
|
|
2466
|
+
elif all(isinstance(i, ee.Geometry) for i in geometries):
|
|
2467
|
+
if geometry_names is None:
|
|
2468
|
+
geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
|
|
2469
|
+
elif len(geometry_names) != len(geometries):
|
|
2470
|
+
raise ValueError("geometry_names must have the same length as the geometries list.")
|
|
2471
|
+
geom_features = [
|
|
2472
|
+
ee.Feature(geom).set({'geo_name': str(name)})
|
|
2473
|
+
for geom, name in zip(geometries, geometry_names)
|
|
2474
|
+
]
|
|
2475
|
+
features = ee.FeatureCollection(geom_features)
|
|
2476
|
+
|
|
2477
|
+
else:
|
|
2478
|
+
raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
|
|
2479
|
+
|
|
2480
|
+
elif isinstance(geometries, tuple) and len(geometries) == 2:
|
|
2481
|
+
name = geometry_names[0] if geometry_names else 'Location_1'
|
|
2482
|
+
features = ee.FeatureCollection([
|
|
2483
|
+
ee.Feature(ee.Geometry.Point(geometries).buffer(buffer_size), {'geo_name': name})
|
|
2484
|
+
])
|
|
1801
2485
|
else:
|
|
1802
|
-
|
|
2486
|
+
raise TypeError("Unsupported type for 'geometries'.")
|
|
2487
|
+
|
|
2488
|
+
features = features.map(set_standard_name)
|
|
2489
|
+
|
|
2490
|
+
try:
|
|
2491
|
+
reducer = getattr(ee.Reducer, reducer_type)()
|
|
2492
|
+
except AttributeError:
|
|
2493
|
+
raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
|
|
2494
|
+
|
|
2495
|
+
def calculate_stats_for_image(image):
|
|
2496
|
+
image_date = image.get('Date_Filter')
|
|
2497
|
+
stats_fc = image.reduceRegions(
|
|
2498
|
+
collection=features, reducer=reducer, scale=scale, tileScale=tileScale
|
|
2499
|
+
)
|
|
2500
|
+
|
|
2501
|
+
def guarantee_reducer_property(f):
|
|
2502
|
+
has_property = f.propertyNames().contains(reducer_type)
|
|
2503
|
+
return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
|
|
2504
|
+
fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
|
|
2505
|
+
|
|
2506
|
+
return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
|
|
2507
|
+
|
|
2508
|
+
results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
|
|
2509
|
+
df = Sentinel2Collection.ee_to_df(results_fc, remove_geom=True)
|
|
2510
|
+
|
|
2511
|
+
# Checking for issues
|
|
2512
|
+
if df.empty:
|
|
2513
|
+
# print("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
|
|
2514
|
+
# return df
|
|
2515
|
+
raise ValueError("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
|
|
2516
|
+
if reducer_type not in df.columns:
|
|
2517
|
+
print(f"Warning: Reducer '{reducer_type}' not found in results.")
|
|
2518
|
+
# return df
|
|
2519
|
+
|
|
2520
|
+
# Get the number of rows before dropping nulls for a helpful message
|
|
2521
|
+
initial_rows = len(df)
|
|
2522
|
+
df.dropna(subset=[reducer_type], inplace=True)
|
|
2523
|
+
df = df[df[reducer_type] != -9999]
|
|
2524
|
+
dropped_rows = initial_rows - len(df)
|
|
2525
|
+
if dropped_rows > 0:
|
|
2526
|
+
print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
|
|
2527
|
+
|
|
2528
|
+
# Reshape DataFrame to have dates as index and geometry names as columns
|
|
2529
|
+
pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
|
|
2530
|
+
pivot_df.index.name = 'Date'
|
|
2531
|
+
if file_path:
|
|
2532
|
+
# Check if file_path ends with .csv and remove it if so for consistency
|
|
2533
|
+
if file_path.endswith('.csv'):
|
|
2534
|
+
file_path = file_path[:-4]
|
|
2535
|
+
pivot_df.to_csv(f"{file_path}.csv")
|
|
2536
|
+
print(f"Zonal stats saved to {file_path}.csv")
|
|
2537
|
+
return
|
|
2538
|
+
return pivot_df
|