RadGEEToolbox 1.7.2__py3-none-any.whl → 1.7.3__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/Export.py +233 -0
- RadGEEToolbox/GenericCollection.py +637 -12
- RadGEEToolbox/LandsatCollection.py +692 -64
- RadGEEToolbox/Sentinel1Collection.py +627 -7
- RadGEEToolbox/Sentinel2Collection.py +670 -27
- RadGEEToolbox/__init__.py +3 -1
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.3.dist-info}/METADATA +11 -7
- radgeetoolbox-1.7.3.dist-info/RECORD +14 -0
- radgeetoolbox-1.7.2.dist-info/RECORD +0 -13
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.3.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.3.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.3.dist-info}/top_level.txt +0 -0
|
@@ -170,6 +170,11 @@ class Sentinel2Collection:
|
|
|
170
170
|
self._monthly_max = None
|
|
171
171
|
self._monthly_min = None
|
|
172
172
|
self._monthly_sum = None
|
|
173
|
+
self._yearly_median = None
|
|
174
|
+
self._yearly_mean = None
|
|
175
|
+
self._yearly_max = None
|
|
176
|
+
self._yearly_min = None
|
|
177
|
+
self._yearly_sum = None
|
|
173
178
|
self._mean = None
|
|
174
179
|
self._max = None
|
|
175
180
|
self._min = None
|
|
@@ -223,7 +228,7 @@ class Sentinel2Collection:
|
|
|
223
228
|
water = (
|
|
224
229
|
ndwi_calc.updateMask(ndwi_calc.gte(threshold))
|
|
225
230
|
.rename("ndwi")
|
|
226
|
-
.copyProperties(image)
|
|
231
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
227
232
|
)
|
|
228
233
|
return water
|
|
229
234
|
|
|
@@ -245,7 +250,7 @@ class Sentinel2Collection:
|
|
|
245
250
|
water = (
|
|
246
251
|
mndwi_calc.updateMask(mndwi_calc.gte(threshold))
|
|
247
252
|
.rename("mndwi")
|
|
248
|
-
.copyProperties(image)
|
|
253
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
249
254
|
)
|
|
250
255
|
return water
|
|
251
256
|
|
|
@@ -267,7 +272,7 @@ class Sentinel2Collection:
|
|
|
267
272
|
vegetation = (
|
|
268
273
|
ndvi_calc.updateMask(ndvi_calc.gte(threshold))
|
|
269
274
|
.rename("ndvi")
|
|
270
|
-
.copyProperties(image)
|
|
275
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
271
276
|
) # subsets the image to just water pixels, 0.2 threshold for datasets
|
|
272
277
|
return vegetation
|
|
273
278
|
|
|
@@ -293,9 +298,9 @@ class Sentinel2Collection:
|
|
|
293
298
|
# If spacecraft is Landsat 5 TM, use the correct expression,
|
|
294
299
|
# otherwise treat as OLI and copy properties after renaming band to "albedo"
|
|
295
300
|
if snow_free == True:
|
|
296
|
-
albedo = image.expression(MSI_expression_snow_free).rename("albedo").copyProperties(image)
|
|
301
|
+
albedo = image.expression(MSI_expression_snow_free).rename("albedo").copyProperties(image).set("system:time_start", image.get("system:time_start"))
|
|
297
302
|
elif snow_free == False:
|
|
298
|
-
albedo = image.expression(MSI_expression_snow_included).rename("albedo").copyProperties(image)
|
|
303
|
+
albedo = image.expression(MSI_expression_snow_included).rename("albedo").copyProperties(image).set("system:time_start", image.get("system:time_start"))
|
|
299
304
|
else:
|
|
300
305
|
raise ValueError("snow_free argument must be True or False")
|
|
301
306
|
return albedo
|
|
@@ -316,7 +321,7 @@ class Sentinel2Collection:
|
|
|
316
321
|
halite = (
|
|
317
322
|
halite_index.updateMask(halite_index.gte(threshold))
|
|
318
323
|
.rename("halite")
|
|
319
|
-
.copyProperties(image)
|
|
324
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
320
325
|
)
|
|
321
326
|
return halite
|
|
322
327
|
|
|
@@ -336,7 +341,7 @@ class Sentinel2Collection:
|
|
|
336
341
|
gypsum = (
|
|
337
342
|
gypsum_index.updateMask(gypsum_index.gte(threshold))
|
|
338
343
|
.rename("gypsum")
|
|
339
|
-
.copyProperties(image)
|
|
344
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
340
345
|
)
|
|
341
346
|
return gypsum
|
|
342
347
|
|
|
@@ -355,6 +360,7 @@ class Sentinel2Collection:
|
|
|
355
360
|
NDTI = image.normalizedDifference(["B3", "B2"])
|
|
356
361
|
turbidity = (
|
|
357
362
|
NDTI.updateMask(NDTI.gte(threshold)).rename("ndti").copyProperties(image)
|
|
363
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
358
364
|
)
|
|
359
365
|
return turbidity
|
|
360
366
|
|
|
@@ -375,6 +381,7 @@ class Sentinel2Collection:
|
|
|
375
381
|
chl_index.updateMask(chl_index.gte(threshold))
|
|
376
382
|
.rename("2BDA")
|
|
377
383
|
.copyProperties(image)
|
|
384
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
378
385
|
)
|
|
379
386
|
return chlorophyll
|
|
380
387
|
|
|
@@ -390,7 +397,9 @@ class Sentinel2Collection:
|
|
|
390
397
|
ee.Image: NDSI ee.Image
|
|
391
398
|
"""
|
|
392
399
|
ndsi_calc = image.normalizedDifference(["B3", "B11"])
|
|
393
|
-
ndsi = ndsi_calc.updateMask(ndsi_calc.gte(threshold)).rename("ndsi")
|
|
400
|
+
ndsi = (ndsi_calc.updateMask(ndsi_calc.gte(threshold)).rename("ndsi")
|
|
401
|
+
.copyProperties(image)
|
|
402
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start")))
|
|
394
403
|
return ndsi
|
|
395
404
|
|
|
396
405
|
@staticmethod
|
|
@@ -411,7 +420,10 @@ class Sentinel2Collection:
|
|
|
411
420
|
"""
|
|
412
421
|
evi_expression = f'{gain_factor} * ((b("B8") - b("B4")) / (b("B8") + {c1} * b("B4") - {c2} * b("B2") + {l}))'
|
|
413
422
|
evi_calc = image.expression(evi_expression)
|
|
414
|
-
evi = evi_calc.updateMask(evi_calc.gte(threshold)).rename("evi")
|
|
423
|
+
evi = (evi_calc.updateMask(evi_calc.gte(threshold)).rename("evi")
|
|
424
|
+
.copyProperties(image)
|
|
425
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
426
|
+
)
|
|
415
427
|
return evi
|
|
416
428
|
|
|
417
429
|
@staticmethod
|
|
@@ -429,7 +441,9 @@ class Sentinel2Collection:
|
|
|
429
441
|
"""
|
|
430
442
|
savi_expression = f'((b("B8") - b("B4")) / (b("B8") + b("B4") + {l})) * (1 + {l})'
|
|
431
443
|
savi_calc = image.expression(savi_expression)
|
|
432
|
-
savi = savi_calc.updateMask(savi_calc.gte(threshold)).rename("savi")
|
|
444
|
+
savi = (savi_calc.updateMask(savi_calc.gte(threshold)).rename("savi")
|
|
445
|
+
.copyProperties(image)
|
|
446
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start")))
|
|
433
447
|
return savi
|
|
434
448
|
|
|
435
449
|
@staticmethod
|
|
@@ -447,7 +461,8 @@ class Sentinel2Collection:
|
|
|
447
461
|
"""
|
|
448
462
|
msavi_expression = '0.5 * (2 * b("B8") + 1 - ((2 * b("B8") + 1) ** 2 - 8 * (b("B8") - b("B4"))) ** 0.5)'
|
|
449
463
|
msavi_calc = image.expression(msavi_expression)
|
|
450
|
-
msavi = msavi_calc.updateMask(msavi_calc.gte(threshold)).rename("msavi").copyProperties(image)
|
|
464
|
+
msavi = (msavi_calc.updateMask(msavi_calc.gte(threshold)).rename("msavi").copyProperties(image)
|
|
465
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start")))
|
|
451
466
|
return msavi
|
|
452
467
|
|
|
453
468
|
@staticmethod
|
|
@@ -465,7 +480,10 @@ class Sentinel2Collection:
|
|
|
465
480
|
"""
|
|
466
481
|
ndmi_expression = '(b("B8") - b("B11")) / (b("B8") + b("B11"))'
|
|
467
482
|
ndmi_calc = image.expression(ndmi_expression)
|
|
468
|
-
ndmi = ndmi_calc.updateMask(ndmi_calc.gte(threshold)).rename("ndmi")
|
|
483
|
+
ndmi = (ndmi_calc.updateMask(ndmi_calc.gte(threshold)).rename("ndmi")
|
|
484
|
+
.copyProperties(image)
|
|
485
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
486
|
+
)
|
|
469
487
|
return ndmi
|
|
470
488
|
|
|
471
489
|
@staticmethod
|
|
@@ -482,7 +500,9 @@ class Sentinel2Collection:
|
|
|
482
500
|
"""
|
|
483
501
|
nbr_expression = '(b("B8") - b("B12")) / (b("B8") + b("B12"))'
|
|
484
502
|
nbr_calc = image.expression(nbr_expression)
|
|
485
|
-
nbr = nbr_calc.updateMask(nbr_calc.gte(threshold)).rename("nbr")
|
|
503
|
+
nbr = (nbr_calc.updateMask(nbr_calc.gte(threshold)).rename("nbr")
|
|
504
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
505
|
+
)
|
|
486
506
|
return nbr
|
|
487
507
|
|
|
488
508
|
@staticmethod
|
|
@@ -556,7 +576,7 @@ class Sentinel2Collection:
|
|
|
556
576
|
"""
|
|
557
577
|
SCL = image.select("SCL")
|
|
558
578
|
CloudMask = SCL.neq(9)
|
|
559
|
-
return image.updateMask(CloudMask).copyProperties(image)
|
|
579
|
+
return image.updateMask(CloudMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
560
580
|
|
|
561
581
|
@staticmethod
|
|
562
582
|
def MaskShadowsS2(image):
|
|
@@ -571,7 +591,7 @@ class Sentinel2Collection:
|
|
|
571
591
|
"""
|
|
572
592
|
SCL = image.select("SCL")
|
|
573
593
|
ShadowMask = SCL.neq(3)
|
|
574
|
-
return image.updateMask(ShadowMask).copyProperties(image)
|
|
594
|
+
return image.updateMask(ShadowMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
575
595
|
|
|
576
596
|
@staticmethod
|
|
577
597
|
def MaskWaterS2(image):
|
|
@@ -586,7 +606,7 @@ class Sentinel2Collection:
|
|
|
586
606
|
"""
|
|
587
607
|
SCL = image.select("SCL")
|
|
588
608
|
WaterMask = SCL.neq(6)
|
|
589
|
-
return image.updateMask(WaterMask).copyProperties(image)
|
|
609
|
+
return image.updateMask(WaterMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
590
610
|
|
|
591
611
|
@staticmethod
|
|
592
612
|
def MaskWaterS2ByNDWI(image, threshold):
|
|
@@ -604,7 +624,7 @@ class Sentinel2Collection:
|
|
|
604
624
|
ndwi_calc = image.normalizedDifference(
|
|
605
625
|
["B3", "B8"]
|
|
606
626
|
) # green-NIR / green+NIR -- full NDWI image
|
|
607
|
-
water = image.updateMask(ndwi_calc.lt(threshold))
|
|
627
|
+
water = image.updateMask(ndwi_calc.lt(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
608
628
|
return water
|
|
609
629
|
|
|
610
630
|
@staticmethod
|
|
@@ -620,7 +640,7 @@ class Sentinel2Collection:
|
|
|
620
640
|
"""
|
|
621
641
|
SCL = image.select("SCL")
|
|
622
642
|
WaterMask = SCL.eq(6)
|
|
623
|
-
return image.updateMask(WaterMask).copyProperties(image)
|
|
643
|
+
return image.updateMask(WaterMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
624
644
|
|
|
625
645
|
@staticmethod
|
|
626
646
|
def halite_mask(image, threshold):
|
|
@@ -635,7 +655,7 @@ class Sentinel2Collection:
|
|
|
635
655
|
ee.Image: ee.Image where halite pixels are masked (image without halite pixels).
|
|
636
656
|
"""
|
|
637
657
|
halite_index = image.normalizedDifference(["B4", "B11"])
|
|
638
|
-
mask = image.updateMask(halite_index.lt(threshold)).copyProperties(image)
|
|
658
|
+
mask = image.updateMask(halite_index.lt(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
639
659
|
return mask
|
|
640
660
|
|
|
641
661
|
@staticmethod
|
|
@@ -659,6 +679,7 @@ class Sentinel2Collection:
|
|
|
659
679
|
.updateMask(gypsum_index.lt(gypsum_threshold))
|
|
660
680
|
.rename("carbonate_muds")
|
|
661
681
|
.copyProperties(image)
|
|
682
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
662
683
|
)
|
|
663
684
|
return mask
|
|
664
685
|
|
|
@@ -688,7 +709,7 @@ class Sentinel2Collection:
|
|
|
688
709
|
if add_band_to_original_image:
|
|
689
710
|
return image.addBands(band_to_mask_image.updateMask(mask).rename(band_to_mask), overwrite=True)
|
|
690
711
|
else:
|
|
691
|
-
return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image))
|
|
712
|
+
return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image).set('system:time_start', image.get('system:time_start')))
|
|
692
713
|
|
|
693
714
|
@staticmethod
|
|
694
715
|
def mask_via_singleband_image_fn(image_to_mask, image_for_mask, threshold, band_name_to_mask=None, band_name_for_mask=None, mask_above=True):
|
|
@@ -724,7 +745,7 @@ class Sentinel2Collection:
|
|
|
724
745
|
mask = band_for_mask_image.gt(threshold)
|
|
725
746
|
else:
|
|
726
747
|
mask = band_for_mask_image.lt(threshold)
|
|
727
|
-
return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask)
|
|
748
|
+
return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask).set('system:time_start', image_to_mask.get('system:time_start'))
|
|
728
749
|
|
|
729
750
|
@staticmethod
|
|
730
751
|
def MaskToWaterS2ByNDWI(image, threshold):
|
|
@@ -741,7 +762,7 @@ class Sentinel2Collection:
|
|
|
741
762
|
ndwi_calc = image.normalizedDifference(
|
|
742
763
|
["B3", "B8"]
|
|
743
764
|
) # green-NIR / green+NIR -- full NDWI image
|
|
744
|
-
water = image.updateMask(ndwi_calc.gte(threshold))
|
|
765
|
+
water = image.updateMask(ndwi_calc.gte(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
745
766
|
return water
|
|
746
767
|
|
|
747
768
|
@staticmethod
|
|
@@ -1698,6 +1719,391 @@ class Sentinel2Collection:
|
|
|
1698
1719
|
pass
|
|
1699
1720
|
|
|
1700
1721
|
return self._monthly_median
|
|
1722
|
+
|
|
1723
|
+
def yearly_mean_collection(self, start_month=1, end_month=12):
|
|
1724
|
+
"""
|
|
1725
|
+
Creates a yearly mean composite from the collection, with optional monthly filtering.
|
|
1726
|
+
|
|
1727
|
+
This function computes the mean for each year within the collection's date range.
|
|
1728
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1729
|
+
to calculate the mean only using imagery from that specific season for each year.
|
|
1730
|
+
|
|
1731
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1732
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1733
|
+
|
|
1734
|
+
Args:
|
|
1735
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1736
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1737
|
+
|
|
1738
|
+
Returns:
|
|
1739
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly mean composites.
|
|
1740
|
+
"""
|
|
1741
|
+
if self._yearly_mean is None:
|
|
1742
|
+
|
|
1743
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1744
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1745
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1746
|
+
|
|
1747
|
+
start_year = start_date_full.get('year')
|
|
1748
|
+
end_year = end_date_full.get('year')
|
|
1749
|
+
|
|
1750
|
+
if start_month != 1 or end_month != 12:
|
|
1751
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1752
|
+
else:
|
|
1753
|
+
processing_collection = self.collection
|
|
1754
|
+
|
|
1755
|
+
# Capture projection from the first image to restore it after reduction
|
|
1756
|
+
target_proj = self.collection.first().projection()
|
|
1757
|
+
|
|
1758
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1759
|
+
|
|
1760
|
+
def create_yearly_composite(year):
|
|
1761
|
+
year = ee.Number(year)
|
|
1762
|
+
# Define the full calendar year range
|
|
1763
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1764
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1765
|
+
|
|
1766
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1767
|
+
|
|
1768
|
+
# Calculate stats
|
|
1769
|
+
image_count = yearly_subset.size()
|
|
1770
|
+
yearly_reduction = yearly_subset.mean()
|
|
1771
|
+
|
|
1772
|
+
# Define the timestamp for the composite.
|
|
1773
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1774
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1775
|
+
|
|
1776
|
+
return yearly_reduction.set({
|
|
1777
|
+
'system:time_start': composite_date.millis(),
|
|
1778
|
+
'year': year,
|
|
1779
|
+
'month': start_month,
|
|
1780
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1781
|
+
'image_count': image_count,
|
|
1782
|
+
'season_start': start_month,
|
|
1783
|
+
'season_end': end_month
|
|
1784
|
+
}).reproject(target_proj)
|
|
1785
|
+
|
|
1786
|
+
# Map the function over the years list
|
|
1787
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1788
|
+
|
|
1789
|
+
# Convert to Collection
|
|
1790
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1791
|
+
|
|
1792
|
+
# Filter out any composites that were created from zero images.
|
|
1793
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1794
|
+
|
|
1795
|
+
self._yearly_mean = Sentinel2Collection(collection=final_collection)
|
|
1796
|
+
else:
|
|
1797
|
+
pass
|
|
1798
|
+
return self._yearly_mean
|
|
1799
|
+
|
|
1800
|
+
def yearly_median_collection(self, start_month=1, end_month=12):
|
|
1801
|
+
"""
|
|
1802
|
+
Creates a yearly median composite from the collection, with optional monthly filtering.
|
|
1803
|
+
|
|
1804
|
+
This function computes the median for each year within the collection's date range.
|
|
1805
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1806
|
+
to calculate the median only using imagery from that specific season for each year.
|
|
1807
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1808
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1809
|
+
|
|
1810
|
+
Args:
|
|
1811
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1812
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1813
|
+
|
|
1814
|
+
Returns:
|
|
1815
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly median composites.
|
|
1816
|
+
"""
|
|
1817
|
+
if self._yearly_median is None:
|
|
1818
|
+
|
|
1819
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1820
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1821
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1822
|
+
|
|
1823
|
+
start_year = start_date_full.get('year')
|
|
1824
|
+
end_year = end_date_full.get('year')
|
|
1825
|
+
|
|
1826
|
+
if start_month != 1 or end_month != 12:
|
|
1827
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1828
|
+
else:
|
|
1829
|
+
processing_collection = self.collection
|
|
1830
|
+
|
|
1831
|
+
# Capture projection from the first image to restore it after reduction
|
|
1832
|
+
target_proj = self.collection.first().projection()
|
|
1833
|
+
|
|
1834
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1835
|
+
|
|
1836
|
+
def create_yearly_composite(year):
|
|
1837
|
+
year = ee.Number(year)
|
|
1838
|
+
# Define the full calendar year range
|
|
1839
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1840
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1841
|
+
|
|
1842
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
1843
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1844
|
+
|
|
1845
|
+
# Calculate stats
|
|
1846
|
+
image_count = yearly_subset.size()
|
|
1847
|
+
yearly_reduction = yearly_subset.median()
|
|
1848
|
+
|
|
1849
|
+
# Define the timestamp for the composite.
|
|
1850
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1851
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1852
|
+
|
|
1853
|
+
return yearly_reduction.set({
|
|
1854
|
+
'system:time_start': composite_date.millis(),
|
|
1855
|
+
'year': year,
|
|
1856
|
+
'month': start_month,
|
|
1857
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1858
|
+
'image_count': image_count,
|
|
1859
|
+
'season_start': start_month,
|
|
1860
|
+
'season_end': end_month
|
|
1861
|
+
}).reproject(target_proj)
|
|
1862
|
+
|
|
1863
|
+
# Map the function over the years list
|
|
1864
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1865
|
+
|
|
1866
|
+
# Convert to Collection
|
|
1867
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1868
|
+
|
|
1869
|
+
# Filter out any composites that were created from zero images.
|
|
1870
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1871
|
+
|
|
1872
|
+
self._yearly_median = Sentinel2Collection(collection=final_collection)
|
|
1873
|
+
else:
|
|
1874
|
+
pass
|
|
1875
|
+
return self._yearly_median
|
|
1876
|
+
|
|
1877
|
+
def yearly_max_collection(self, start_month=1, end_month=12):
|
|
1878
|
+
"""
|
|
1879
|
+
Creates a yearly max composite from the collection, with optional monthly filtering.
|
|
1880
|
+
|
|
1881
|
+
This function computes the max for each year within the collection's date range.
|
|
1882
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1883
|
+
to calculate the max only using imagery from that specific season for each year.
|
|
1884
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1885
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1886
|
+
|
|
1887
|
+
Args:
|
|
1888
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1889
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1890
|
+
|
|
1891
|
+
Returns:
|
|
1892
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly max composites.
|
|
1893
|
+
"""
|
|
1894
|
+
if self._yearly_max is None:
|
|
1895
|
+
|
|
1896
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1897
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1898
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1899
|
+
|
|
1900
|
+
start_year = start_date_full.get('year')
|
|
1901
|
+
end_year = end_date_full.get('year')
|
|
1902
|
+
|
|
1903
|
+
if start_month != 1 or end_month != 12:
|
|
1904
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1905
|
+
else:
|
|
1906
|
+
processing_collection = self.collection
|
|
1907
|
+
|
|
1908
|
+
# Capture projection from the first image to restore it after reduction
|
|
1909
|
+
target_proj = self.collection.first().projection()
|
|
1910
|
+
|
|
1911
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1912
|
+
|
|
1913
|
+
def create_yearly_composite(year):
|
|
1914
|
+
year = ee.Number(year)
|
|
1915
|
+
# Define the full calendar year range
|
|
1916
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1917
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1918
|
+
|
|
1919
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
1920
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1921
|
+
|
|
1922
|
+
# Calculate stats
|
|
1923
|
+
image_count = yearly_subset.size()
|
|
1924
|
+
yearly_reduction = yearly_subset.max()
|
|
1925
|
+
|
|
1926
|
+
# Define the timestamp for the composite.
|
|
1927
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1928
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1929
|
+
|
|
1930
|
+
return yearly_reduction.set({
|
|
1931
|
+
'system:time_start': composite_date.millis(),
|
|
1932
|
+
'year': year,
|
|
1933
|
+
'month': start_month,
|
|
1934
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1935
|
+
'image_count': image_count,
|
|
1936
|
+
'season_start': start_month,
|
|
1937
|
+
'season_end': end_month
|
|
1938
|
+
}).reproject(target_proj)
|
|
1939
|
+
|
|
1940
|
+
# Map the function over the years list
|
|
1941
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1942
|
+
|
|
1943
|
+
# Convert to Collection
|
|
1944
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1945
|
+
|
|
1946
|
+
# Filter out any composites that were created from zero images.
|
|
1947
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1948
|
+
|
|
1949
|
+
self._yearly_max = Sentinel2Collection(collection=final_collection)
|
|
1950
|
+
else:
|
|
1951
|
+
pass
|
|
1952
|
+
return self._yearly_max
|
|
1953
|
+
|
|
1954
|
+
def yearly_min_collection(self, start_month=1, end_month=12):
|
|
1955
|
+
"""
|
|
1956
|
+
Creates a yearly min composite from the collection, with optional monthly filtering.
|
|
1957
|
+
|
|
1958
|
+
This function computes the min for each year within the collection's date range.
|
|
1959
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1960
|
+
to calculate the min only using imagery from that specific season for each year.
|
|
1961
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1962
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1963
|
+
|
|
1964
|
+
Args:
|
|
1965
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1966
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1967
|
+
|
|
1968
|
+
Returns:
|
|
1969
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly min composites.
|
|
1970
|
+
"""
|
|
1971
|
+
if self._yearly_min is None:
|
|
1972
|
+
|
|
1973
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1974
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1975
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1976
|
+
|
|
1977
|
+
start_year = start_date_full.get('year')
|
|
1978
|
+
end_year = end_date_full.get('year')
|
|
1979
|
+
|
|
1980
|
+
if start_month != 1 or end_month != 12:
|
|
1981
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1982
|
+
else:
|
|
1983
|
+
processing_collection = self.collection
|
|
1984
|
+
|
|
1985
|
+
# Capture projection from the first image to restore it after reduction
|
|
1986
|
+
target_proj = self.collection.first().projection()
|
|
1987
|
+
|
|
1988
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1989
|
+
|
|
1990
|
+
def create_yearly_composite(year):
|
|
1991
|
+
year = ee.Number(year)
|
|
1992
|
+
# Define the full calendar year range
|
|
1993
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1994
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1995
|
+
|
|
1996
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
1997
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1998
|
+
|
|
1999
|
+
# Calculate stats
|
|
2000
|
+
image_count = yearly_subset.size()
|
|
2001
|
+
yearly_reduction = yearly_subset.min()
|
|
2002
|
+
|
|
2003
|
+
# Define the timestamp for the composite.
|
|
2004
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
2005
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
2006
|
+
|
|
2007
|
+
return yearly_reduction.set({
|
|
2008
|
+
'system:time_start': composite_date.millis(),
|
|
2009
|
+
'year': year,
|
|
2010
|
+
'month': start_month,
|
|
2011
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
2012
|
+
'image_count': image_count,
|
|
2013
|
+
'season_start': start_month,
|
|
2014
|
+
'season_end': end_month
|
|
2015
|
+
}).reproject(target_proj)
|
|
2016
|
+
|
|
2017
|
+
# Map the function over the years list
|
|
2018
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
2019
|
+
|
|
2020
|
+
# Convert to Collection
|
|
2021
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
2022
|
+
|
|
2023
|
+
# Filter out any composites that were created from zero images.
|
|
2024
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
2025
|
+
|
|
2026
|
+
self._yearly_min = Sentinel2Collection(collection=final_collection)
|
|
2027
|
+
else:
|
|
2028
|
+
pass
|
|
2029
|
+
return self._yearly_min
|
|
2030
|
+
|
|
2031
|
+
def yearly_sum_collection(self, start_month=1, end_month=12):
|
|
2032
|
+
"""
|
|
2033
|
+
Creates a yearly sum composite from the collection, with optional monthly filtering.
|
|
2034
|
+
|
|
2035
|
+
This function computes the sum for each year within the collection's date range.
|
|
2036
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
2037
|
+
to calculate the sum only using imagery from that specific season for each year.
|
|
2038
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
2039
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
2040
|
+
|
|
2041
|
+
Args:
|
|
2042
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
2043
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
2044
|
+
|
|
2045
|
+
Returns:
|
|
2046
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly sum composites.
|
|
2047
|
+
"""
|
|
2048
|
+
if self._yearly_sum is None:
|
|
2049
|
+
|
|
2050
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
2051
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
2052
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
2053
|
+
|
|
2054
|
+
start_year = start_date_full.get('year')
|
|
2055
|
+
end_year = end_date_full.get('year')
|
|
2056
|
+
|
|
2057
|
+
if start_month != 1 or end_month != 12:
|
|
2058
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
2059
|
+
else:
|
|
2060
|
+
processing_collection = self.collection
|
|
2061
|
+
|
|
2062
|
+
# Capture projection from the first image to restore it after reduction
|
|
2063
|
+
target_proj = self.collection.first().projection()
|
|
2064
|
+
|
|
2065
|
+
years = ee.List.sequence(start_year, end_year)
|
|
2066
|
+
|
|
2067
|
+
def create_yearly_composite(year):
|
|
2068
|
+
year = ee.Number(year)
|
|
2069
|
+
# Define the full calendar year range
|
|
2070
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
2071
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
2072
|
+
|
|
2073
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
2074
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
2075
|
+
|
|
2076
|
+
# Calculate stats
|
|
2077
|
+
image_count = yearly_subset.size()
|
|
2078
|
+
yearly_reduction = yearly_subset.sum()
|
|
2079
|
+
|
|
2080
|
+
# Define the timestamp for the composite.
|
|
2081
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
2082
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
2083
|
+
|
|
2084
|
+
return yearly_reduction.set({
|
|
2085
|
+
'system:time_start': composite_date.millis(),
|
|
2086
|
+
'year': year,
|
|
2087
|
+
'month': start_month,
|
|
2088
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
2089
|
+
'image_count': image_count,
|
|
2090
|
+
'season_start': start_month,
|
|
2091
|
+
'season_end': end_month
|
|
2092
|
+
}).reproject(target_proj)
|
|
2093
|
+
|
|
2094
|
+
# Map the function over the years list
|
|
2095
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
2096
|
+
|
|
2097
|
+
# Convert to Collection
|
|
2098
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
2099
|
+
|
|
2100
|
+
# Filter out any composites that were created from zero images.
|
|
2101
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
2102
|
+
|
|
2103
|
+
self._yearly_sum = Sentinel2Collection(collection=final_collection)
|
|
2104
|
+
else:
|
|
2105
|
+
pass
|
|
2106
|
+
return self._yearly_sum
|
|
1701
2107
|
|
|
1702
2108
|
@property
|
|
1703
2109
|
def ndwi(self):
|
|
@@ -2465,20 +2871,28 @@ class Sentinel2Collection:
|
|
|
2465
2871
|
if classify_above_threshold:
|
|
2466
2872
|
if mask_zeros is True:
|
|
2467
2873
|
col = self.collection.map(
|
|
2468
|
-
lambda image: image.select(band_name).gte(threshold)
|
|
2874
|
+
lambda image: image.select(band_name).gte(threshold)
|
|
2875
|
+
.rename(band_name).selfMask().copyProperties(image)
|
|
2876
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
2469
2877
|
)
|
|
2470
2878
|
else:
|
|
2471
2879
|
col = self.collection.map(
|
|
2472
|
-
lambda image: image.select(band_name).gte(threshold)
|
|
2880
|
+
lambda image: image.select(band_name).gte(threshold)
|
|
2881
|
+
.rename(band_name).copyProperties(image)
|
|
2882
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
2473
2883
|
)
|
|
2474
2884
|
else:
|
|
2475
2885
|
if mask_zeros is True:
|
|
2476
2886
|
col = self.collection.map(
|
|
2477
|
-
lambda image: image.select(band_name).lte(threshold)
|
|
2887
|
+
lambda image: image.select(band_name).lte(threshold)
|
|
2888
|
+
.rename(band_name).selfMask().copyProperties(image)
|
|
2889
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
2478
2890
|
)
|
|
2479
2891
|
else:
|
|
2480
2892
|
col = self.collection.map(
|
|
2481
|
-
lambda image: image.select(band_name).lte(threshold)
|
|
2893
|
+
lambda image: image.select(band_name).lte(threshold)
|
|
2894
|
+
.rename(band_name).copyProperties(image)
|
|
2895
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
2482
2896
|
)
|
|
2483
2897
|
|
|
2484
2898
|
return Sentinel2Collection(collection=col)
|
|
@@ -2519,6 +2933,234 @@ class Sentinel2Collection:
|
|
|
2519
2933
|
col = self.collection.map(lambda image: Sentinel2Collection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
|
|
2520
2934
|
return Sentinel2Collection(collection=col)
|
|
2521
2935
|
|
|
2936
|
+
def mann_kendall_trend(self, target_band=None, join_method='system:time_start', geometry=None):
|
|
2937
|
+
"""
|
|
2938
|
+
Calculates the Mann-Kendall S-value, Variance, Z-Score, and Confidence Level for each pixel in the image collection, in addition to calculating
|
|
2939
|
+
the Sen's slope for each pixel in the image collection. The output is an image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
|
|
2940
|
+
|
|
2941
|
+
This function can be used to identify trends in the image collection over time, such as increasing or decreasing values in the target band, and can be used to assess the significance of these trends.
|
|
2942
|
+
Note that this function is computationally intensive and may take a long time to run for large image collections or high-resolution images.
|
|
2943
|
+
|
|
2944
|
+
The 's_statistic' band represents the Mann-Kendall S-value, which is a measure of the strength and direction of the trend.
|
|
2945
|
+
The 'variance' band represents the variance of the S-value, which is a measure of the variability of the S-value.
|
|
2946
|
+
The 'z_score' band represents the Z-Score, which is a measure of the significance of the trend.
|
|
2947
|
+
The 'confidence' band represents the confidence level of the trend based on the z_score, which is a probabilistic measure of the confidence in the trend (percentage).
|
|
2948
|
+
The 'slope' band represents the Sen's slope, which is a measure of the rate of change in the target band over time. This value can be small as multispectral indices commonly range from -1 to 1, so a slope may have values of <0.2 for most cases.
|
|
2949
|
+
|
|
2950
|
+
Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
|
|
2951
|
+
You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
|
|
2952
|
+
The `geometry` parameter is optional and defaults to None, which means that the trend statistics will be calculated over the entire footprint of the image collection.
|
|
2953
|
+
|
|
2954
|
+
Args:
|
|
2955
|
+
image_collection (Sentinel2Collection or ee.ImageCollection): The input image collection for which the Mann-Kendall and Sen's slope trend statistics will be calculated.
|
|
2956
|
+
target_band (str): The band name to be used for the output anomaly image. e.g. 'ndvi'
|
|
2957
|
+
join_method (str, optional): The method used to join images in the collection. Options are 'system:time_start' or 'Date_Filter'. Default is 'system:time_start'.
|
|
2958
|
+
geometry (ee.Geometry, optional): An ee.Geometry object to limit the area over which the trend statistics are calculated and mask the output image. Default is None.
|
|
2959
|
+
|
|
2960
|
+
Returns:
|
|
2961
|
+
ee.Image: An image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
|
|
2962
|
+
"""
|
|
2963
|
+
########## PART 1 - S-VALUE CALCULATION ##########
|
|
2964
|
+
##### https://vsp.pnnl.gov/help/vsample/design_trend_mann_kendall.htm #####
|
|
2965
|
+
image_collection = self
|
|
2966
|
+
if isinstance(image_collection, Sentinel2Collection):
|
|
2967
|
+
image_collection = image_collection.collection
|
|
2968
|
+
elif isinstance(image_collection, ee.ImageCollection):
|
|
2969
|
+
pass
|
|
2970
|
+
else:
|
|
2971
|
+
raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid Sentinel2Collection or ee.ImageCollection object.')
|
|
2972
|
+
|
|
2973
|
+
if target_band is None:
|
|
2974
|
+
raise ValueError('The `target_band` parameter must be specified.')
|
|
2975
|
+
if not isinstance(target_band, str):
|
|
2976
|
+
raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
|
|
2977
|
+
|
|
2978
|
+
if geometry is not None and not isinstance(geometry, ee.Geometry):
|
|
2979
|
+
raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
|
|
2980
|
+
# define the join, which will join all images newer than the current image
|
|
2981
|
+
# use system:time_start if the image does not have a Date_Filter property
|
|
2982
|
+
if join_method == 'system:time_start':
|
|
2983
|
+
# get all images where the leftField value is less than (before) the rightField value
|
|
2984
|
+
time_filter = ee.Filter.lessThan(leftField='system:time_start',
|
|
2985
|
+
rightField='system:time_start')
|
|
2986
|
+
elif join_method == 'Date_Filter':
|
|
2987
|
+
# get all images where the leftField value is less than (before) the rightField value
|
|
2988
|
+
time_filter = ee.Filter.lessThan(leftField='Date_Filter',
|
|
2989
|
+
rightField='Date_Filter')
|
|
2990
|
+
else:
|
|
2991
|
+
raise ValueError(f'The chosen `join_method`: {join_method} does not match the options of "system:time_start" or "Date_Filter".')
|
|
2992
|
+
|
|
2993
|
+
# for any matches during a join, set image as a property key called 'future_image'
|
|
2994
|
+
join = ee.Join.saveAll(matchesKey='future_image')
|
|
2995
|
+
|
|
2996
|
+
# apply the join on the input collection
|
|
2997
|
+
# joining all images newer than the current image with the current image
|
|
2998
|
+
joined_collection = ee.ImageCollection(join.apply(primary=image_collection,
|
|
2999
|
+
secondary=image_collection, condition=time_filter))
|
|
3000
|
+
|
|
3001
|
+
# defining a collection to calculate the partial S value for each match in the join
|
|
3002
|
+
# e.g. t4-t1, t3-t1, t2-1 if there are 4 images
|
|
3003
|
+
def calculate_partial_s(current_image):
|
|
3004
|
+
# select the target band for arithmetic
|
|
3005
|
+
current_val = current_image.select(target_band)
|
|
3006
|
+
# get the joined images from the current image properties and cast the joined images as a list
|
|
3007
|
+
future_image_list = ee.List(current_image.get('future_image'))
|
|
3008
|
+
# convert the joined list to an image collection
|
|
3009
|
+
future_image_collection = ee.ImageCollection(future_image_list)
|
|
3010
|
+
|
|
3011
|
+
# define a function that will calculate the difference between the joined images and the current image,
|
|
3012
|
+
# then calculate the partial S sign based on the value of the difference calculation
|
|
3013
|
+
def get_sign(future_image):
|
|
3014
|
+
# select the target band for arithmetic from the future image
|
|
3015
|
+
future_val = future_image.select(target_band)
|
|
3016
|
+
# calculate the difference, i.e. t2-t1
|
|
3017
|
+
difference = future_val.subtract(current_val)
|
|
3018
|
+
# determine the sign of the difference value (1 if diff > 0, 0 if 0, and -1 if diff < 0)
|
|
3019
|
+
# use .unmask(0) to set any masked pixels as 0 to avoid
|
|
3020
|
+
|
|
3021
|
+
sign = difference.signum().unmask(0)
|
|
3022
|
+
|
|
3023
|
+
return sign
|
|
3024
|
+
|
|
3025
|
+
# map the get_sign() function along the future image col
|
|
3026
|
+
# then sum the values for each pixel to get the partial S value
|
|
3027
|
+
return future_image_collection.map(get_sign).sum()
|
|
3028
|
+
|
|
3029
|
+
# calculate the partial s value for each image in the joined/input image collection
|
|
3030
|
+
partial_s_col = joined_collection.map(calculate_partial_s)
|
|
3031
|
+
|
|
3032
|
+
# convert the image collection to an image of s_statistic values per pixel
|
|
3033
|
+
# where the s_statistic is the sum of partial s values
|
|
3034
|
+
# renaming the band as 's_statistic' for later usage
|
|
3035
|
+
final_s_image = partial_s_col.sum().rename('s_statistic')
|
|
3036
|
+
|
|
3037
|
+
|
|
3038
|
+
########## PART 2 - VARIANCE and Z-SCORE ##########
|
|
3039
|
+
# to calculate variance we need to know how many pixels were involved in the partial_s calculations per pixel
|
|
3040
|
+
# we do this by using count() and turn the value to a float for later arithmetic
|
|
3041
|
+
n = image_collection.select(target_band).count().toFloat()
|
|
3042
|
+
|
|
3043
|
+
##### VARIANCE CALCULATION #####
|
|
3044
|
+
# as we are using floating point values with high precision, it is HIGHLY
|
|
3045
|
+
# unlikely that there will be multiple pixel values with the same value.
|
|
3046
|
+
# Thus, we opt to use the simplified variance calculation approach as the
|
|
3047
|
+
# impacts to the output value are negligible and the processing benefits are HUGE
|
|
3048
|
+
# variance = (n * (n - 1) * (2n + 5)) / 18
|
|
3049
|
+
var_s = n.multiply(n.subtract(1))\
|
|
3050
|
+
.multiply(n.multiply(2).add(5))\
|
|
3051
|
+
.divide(18).rename('variance')
|
|
3052
|
+
|
|
3053
|
+
z_score = ee.Image().expression(
|
|
3054
|
+
"""
|
|
3055
|
+
(s > 0) ? (s - 1) / sqrt(var) :
|
|
3056
|
+
(s < 0) ? (s + 1) / sqrt(var) :
|
|
3057
|
+
0
|
|
3058
|
+
""",
|
|
3059
|
+
{'s': final_s_image, 'var': var_s}
|
|
3060
|
+
).rename('z_score')
|
|
3061
|
+
|
|
3062
|
+
confidence = z_score.abs().divide(ee.Number(2).sqrt()).erf().rename('confidence')
|
|
3063
|
+
|
|
3064
|
+
stat_bands = ee.Image([var_s, z_score, confidence])
|
|
3065
|
+
|
|
3066
|
+
mk_stats_image = final_s_image.addBands(stat_bands)
|
|
3067
|
+
|
|
3068
|
+
########## PART 3 - Sen's Slope ##########
|
|
3069
|
+
def add_year_band(image):
|
|
3070
|
+
if join_method == 'Date_Filter':
|
|
3071
|
+
# Get the string 'YYYY-MM-DD'
|
|
3072
|
+
date_string = image.get('Date_Filter')
|
|
3073
|
+
# Parse it into an ee.Date object (handles the conversion to time math)
|
|
3074
|
+
date = ee.Date.parse('YYYY-MM-dd', date_string)
|
|
3075
|
+
else:
|
|
3076
|
+
# Standard way: assumes system:time_start exists
|
|
3077
|
+
date = image.date()
|
|
3078
|
+
years = date.difference(ee.Date('1970-01-01'), 'year')
|
|
3079
|
+
return image.addBands(ee.Image(years).float().rename('year'))
|
|
3080
|
+
|
|
3081
|
+
slope_input = image_collection.map(add_year_band).select(['year', target_band])
|
|
3082
|
+
|
|
3083
|
+
sens_slope = slope_input.reduce(ee.Reducer.sensSlope())
|
|
3084
|
+
|
|
3085
|
+
slope_band = sens_slope.select('slope')
|
|
3086
|
+
|
|
3087
|
+
# add a mask to the final image to remove pixels with less than min_observations
|
|
3088
|
+
# mainly an effort to mask pixels outside of the boundary of the input image collection
|
|
3089
|
+
min_observations = 1
|
|
3090
|
+
valid_mask = n.gte(min_observations)
|
|
3091
|
+
|
|
3092
|
+
final_image = mk_stats_image.addBands(slope_band).updateMask(valid_mask)
|
|
3093
|
+
|
|
3094
|
+
if geometry is not None:
|
|
3095
|
+
mask = ee.Image(1).clip(geometry)
|
|
3096
|
+
final_image = final_image.updateMask(mask)
|
|
3097
|
+
|
|
3098
|
+
return final_image
|
|
3099
|
+
|
|
3100
|
+
def sens_slope_trend(self, target_band=None, join_method='system:time_start', geometry=None):
|
|
3101
|
+
"""
|
|
3102
|
+
Calculates Sen's Slope (trend magnitude) for the collection.
|
|
3103
|
+
This is a lighter-weight alternative to the full `mann_kendall_trend` function if only
|
|
3104
|
+
the direction and magnitude of the trend are needed.
|
|
3105
|
+
|
|
3106
|
+
Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
|
|
3107
|
+
You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
|
|
3108
|
+
The `geometry` parameter is optional and defaults to None, which means that the trend statistics will be calculated over the entire footprint of the image collection.
|
|
3109
|
+
|
|
3110
|
+
Args:
|
|
3111
|
+
target_band (str): The name of the band to analyze. Defaults to 'ndvi'.
|
|
3112
|
+
join_method (str): Property to use for time sorting ('system:time_start' or 'Date_Filter').
|
|
3113
|
+
geometry (ee.Geometry, optional): Geometry to mask the final output.
|
|
3114
|
+
|
|
3115
|
+
Returns:
|
|
3116
|
+
ee.Image: An image containing the 'slope' band.
|
|
3117
|
+
"""
|
|
3118
|
+
image_collection = self
|
|
3119
|
+
if isinstance(image_collection, Sentinel2Collection):
|
|
3120
|
+
image_collection = image_collection.collection
|
|
3121
|
+
elif isinstance(image_collection, ee.ImageCollection):
|
|
3122
|
+
pass
|
|
3123
|
+
else:
|
|
3124
|
+
raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid Sentinel2Collection or ee.ImageCollection object.')
|
|
3125
|
+
|
|
3126
|
+
if target_band is None:
|
|
3127
|
+
raise ValueError('The `target_band` parameter must be specified.')
|
|
3128
|
+
if not isinstance(target_band, str):
|
|
3129
|
+
raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
|
|
3130
|
+
|
|
3131
|
+
if geometry is not None and not isinstance(geometry, ee.Geometry):
|
|
3132
|
+
raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
|
|
3133
|
+
|
|
3134
|
+
# Add Year Band (Time X-Axis)
|
|
3135
|
+
def add_year_band(image):
|
|
3136
|
+
# Handle user-defined date strings vs system time
|
|
3137
|
+
if join_method == 'Date_Filter':
|
|
3138
|
+
date_string = image.get('Date_Filter')
|
|
3139
|
+
date = ee.Date.parse('YYYY-MM-dd', date_string)
|
|
3140
|
+
else:
|
|
3141
|
+
date = image.date()
|
|
3142
|
+
|
|
3143
|
+
# Convert to fractional years relative to epoch
|
|
3144
|
+
years = date.difference(ee.Date('1970-01-01'), 'year')
|
|
3145
|
+
return image.addBands(ee.Image(years).float().rename('year'))
|
|
3146
|
+
|
|
3147
|
+
# Prepare Collection: Select ONLY [Year, Target]
|
|
3148
|
+
# sensSlope expects Band 0 = Independent (X), Band 1 = Dependent (Y)
|
|
3149
|
+
slope_input = self.collection.map(add_year_band).select(['year', target_band])
|
|
3150
|
+
|
|
3151
|
+
# Run the Native Reducer
|
|
3152
|
+
sens_result = slope_input.reduce(ee.Reducer.sensSlope())
|
|
3153
|
+
|
|
3154
|
+
# Extract and Mask
|
|
3155
|
+
slope_band = sens_result.select('slope')
|
|
3156
|
+
|
|
3157
|
+
if geometry is not None:
|
|
3158
|
+
mask = ee.Image(1).clip(geometry)
|
|
3159
|
+
slope_band = slope_band.updateMask(mask)
|
|
3160
|
+
|
|
3161
|
+
return slope_band
|
|
3162
|
+
|
|
3163
|
+
|
|
2522
3164
|
def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
|
|
2523
3165
|
"""
|
|
2524
3166
|
Masks select pixels of a selected band from an image based on another specified band and threshold (optional).
|
|
@@ -2601,7 +3243,8 @@ class Sentinel2Collection:
|
|
|
2601
3243
|
)
|
|
2602
3244
|
|
|
2603
3245
|
# guarantee single band + keep properties
|
|
2604
|
-
out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())
|
|
3246
|
+
out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())\
|
|
3247
|
+
.set('system:time_start', prim.get('system:time_start'))
|
|
2605
3248
|
out = out.set('Date_Filter', prim.get('Date_Filter'))
|
|
2606
3249
|
return ee.Image(out) # <-- return as Image
|
|
2607
3250
|
|