RadGEEToolbox 1.7.2__py3-none-any.whl → 1.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- RadGEEToolbox/CollectionStitch.py +16 -3
- RadGEEToolbox/Export.py +249 -0
- RadGEEToolbox/GenericCollection.py +763 -42
- RadGEEToolbox/LandsatCollection.py +938 -111
- RadGEEToolbox/Sentinel1Collection.py +801 -39
- RadGEEToolbox/Sentinel2Collection.py +869 -75
- RadGEEToolbox/__init__.py +6 -4
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.4.dist-info}/METADATA +11 -7
- radgeetoolbox-1.7.4.dist-info/RECORD +14 -0
- radgeetoolbox-1.7.2.dist-info/RECORD +0 -13
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.4.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.4.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ee
|
|
2
2
|
import pandas as pd
|
|
3
3
|
import numpy as np
|
|
4
|
+
import warnings
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
# ---- Reflectance scaling for Sentinel-2 L2A (HARMONIZED) ----
|
|
@@ -68,7 +69,7 @@ class Sentinel2Collection:
|
|
|
68
69
|
... cloud_percentage_threshold=20,
|
|
69
70
|
... nodata_threshold=10,
|
|
70
71
|
... )
|
|
71
|
-
>>> mosaic_collection = image_collection.
|
|
72
|
+
>>> mosaic_collection = image_collection.mosaicByDate #mosaic images/tiles with same date
|
|
72
73
|
>>> cloud_masked = mosaic_collection.masked_clouds_collection #mask out clouds
|
|
73
74
|
>>> latest_image = cloud_masked.image_grab(-1) #grab latest image for viewing
|
|
74
75
|
>>> ndwi_collection = cloud_masked.ndwi #calculate ndwi for all images
|
|
@@ -170,6 +171,11 @@ class Sentinel2Collection:
|
|
|
170
171
|
self._monthly_max = None
|
|
171
172
|
self._monthly_min = None
|
|
172
173
|
self._monthly_sum = None
|
|
174
|
+
self._yearly_median = None
|
|
175
|
+
self._yearly_mean = None
|
|
176
|
+
self._yearly_max = None
|
|
177
|
+
self._yearly_min = None
|
|
178
|
+
self._yearly_sum = None
|
|
173
179
|
self._mean = None
|
|
174
180
|
self._max = None
|
|
175
181
|
self._min = None
|
|
@@ -191,6 +197,14 @@ class Sentinel2Collection:
|
|
|
191
197
|
self._PixelAreaSumCollection = None
|
|
192
198
|
self._Reflectance = None
|
|
193
199
|
|
|
200
|
+
def __call__(self):
|
|
201
|
+
"""
|
|
202
|
+
Allows the object to be called as a function, returning itself.
|
|
203
|
+
This enables property-like methods to be accessed with or without parentheses
|
|
204
|
+
(e.g., .mosaicByDate or .mosaicByDate()).
|
|
205
|
+
"""
|
|
206
|
+
return self
|
|
207
|
+
|
|
194
208
|
@staticmethod
|
|
195
209
|
def image_dater(image):
|
|
196
210
|
"""
|
|
@@ -223,7 +237,7 @@ class Sentinel2Collection:
|
|
|
223
237
|
water = (
|
|
224
238
|
ndwi_calc.updateMask(ndwi_calc.gte(threshold))
|
|
225
239
|
.rename("ndwi")
|
|
226
|
-
.copyProperties(image)
|
|
240
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
227
241
|
)
|
|
228
242
|
return water
|
|
229
243
|
|
|
@@ -245,7 +259,7 @@ class Sentinel2Collection:
|
|
|
245
259
|
water = (
|
|
246
260
|
mndwi_calc.updateMask(mndwi_calc.gte(threshold))
|
|
247
261
|
.rename("mndwi")
|
|
248
|
-
.copyProperties(image)
|
|
262
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
249
263
|
)
|
|
250
264
|
return water
|
|
251
265
|
|
|
@@ -267,7 +281,7 @@ class Sentinel2Collection:
|
|
|
267
281
|
vegetation = (
|
|
268
282
|
ndvi_calc.updateMask(ndvi_calc.gte(threshold))
|
|
269
283
|
.rename("ndvi")
|
|
270
|
-
.copyProperties(image)
|
|
284
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
271
285
|
) # subsets the image to just water pixels, 0.2 threshold for datasets
|
|
272
286
|
return vegetation
|
|
273
287
|
|
|
@@ -293,9 +307,9 @@ class Sentinel2Collection:
|
|
|
293
307
|
# If spacecraft is Landsat 5 TM, use the correct expression,
|
|
294
308
|
# otherwise treat as OLI and copy properties after renaming band to "albedo"
|
|
295
309
|
if snow_free == True:
|
|
296
|
-
albedo = image.expression(MSI_expression_snow_free).rename("albedo").copyProperties(image)
|
|
310
|
+
albedo = image.expression(MSI_expression_snow_free).rename("albedo").copyProperties(image).set("system:time_start", image.get("system:time_start"))
|
|
297
311
|
elif snow_free == False:
|
|
298
|
-
albedo = image.expression(MSI_expression_snow_included).rename("albedo").copyProperties(image)
|
|
312
|
+
albedo = image.expression(MSI_expression_snow_included).rename("albedo").copyProperties(image).set("system:time_start", image.get("system:time_start"))
|
|
299
313
|
else:
|
|
300
314
|
raise ValueError("snow_free argument must be True or False")
|
|
301
315
|
return albedo
|
|
@@ -316,7 +330,7 @@ class Sentinel2Collection:
|
|
|
316
330
|
halite = (
|
|
317
331
|
halite_index.updateMask(halite_index.gte(threshold))
|
|
318
332
|
.rename("halite")
|
|
319
|
-
.copyProperties(image)
|
|
333
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
320
334
|
)
|
|
321
335
|
return halite
|
|
322
336
|
|
|
@@ -336,7 +350,7 @@ class Sentinel2Collection:
|
|
|
336
350
|
gypsum = (
|
|
337
351
|
gypsum_index.updateMask(gypsum_index.gte(threshold))
|
|
338
352
|
.rename("gypsum")
|
|
339
|
-
.copyProperties(image)
|
|
353
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
340
354
|
)
|
|
341
355
|
return gypsum
|
|
342
356
|
|
|
@@ -355,6 +369,7 @@ class Sentinel2Collection:
|
|
|
355
369
|
NDTI = image.normalizedDifference(["B3", "B2"])
|
|
356
370
|
turbidity = (
|
|
357
371
|
NDTI.updateMask(NDTI.gte(threshold)).rename("ndti").copyProperties(image)
|
|
372
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
358
373
|
)
|
|
359
374
|
return turbidity
|
|
360
375
|
|
|
@@ -375,6 +390,7 @@ class Sentinel2Collection:
|
|
|
375
390
|
chl_index.updateMask(chl_index.gte(threshold))
|
|
376
391
|
.rename("2BDA")
|
|
377
392
|
.copyProperties(image)
|
|
393
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
378
394
|
)
|
|
379
395
|
return chlorophyll
|
|
380
396
|
|
|
@@ -390,7 +406,9 @@ class Sentinel2Collection:
|
|
|
390
406
|
ee.Image: NDSI ee.Image
|
|
391
407
|
"""
|
|
392
408
|
ndsi_calc = image.normalizedDifference(["B3", "B11"])
|
|
393
|
-
ndsi = ndsi_calc.updateMask(ndsi_calc.gte(threshold)).rename("ndsi")
|
|
409
|
+
ndsi = (ndsi_calc.updateMask(ndsi_calc.gte(threshold)).rename("ndsi")
|
|
410
|
+
.copyProperties(image)
|
|
411
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start")))
|
|
394
412
|
return ndsi
|
|
395
413
|
|
|
396
414
|
@staticmethod
|
|
@@ -411,7 +429,10 @@ class Sentinel2Collection:
|
|
|
411
429
|
"""
|
|
412
430
|
evi_expression = f'{gain_factor} * ((b("B8") - b("B4")) / (b("B8") + {c1} * b("B4") - {c2} * b("B2") + {l}))'
|
|
413
431
|
evi_calc = image.expression(evi_expression)
|
|
414
|
-
evi = evi_calc.updateMask(evi_calc.gte(threshold)).rename("evi")
|
|
432
|
+
evi = (evi_calc.updateMask(evi_calc.gte(threshold)).rename("evi")
|
|
433
|
+
.copyProperties(image)
|
|
434
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
435
|
+
)
|
|
415
436
|
return evi
|
|
416
437
|
|
|
417
438
|
@staticmethod
|
|
@@ -429,7 +450,9 @@ class Sentinel2Collection:
|
|
|
429
450
|
"""
|
|
430
451
|
savi_expression = f'((b("B8") - b("B4")) / (b("B8") + b("B4") + {l})) * (1 + {l})'
|
|
431
452
|
savi_calc = image.expression(savi_expression)
|
|
432
|
-
savi = savi_calc.updateMask(savi_calc.gte(threshold)).rename("savi")
|
|
453
|
+
savi = (savi_calc.updateMask(savi_calc.gte(threshold)).rename("savi")
|
|
454
|
+
.copyProperties(image)
|
|
455
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start")))
|
|
433
456
|
return savi
|
|
434
457
|
|
|
435
458
|
@staticmethod
|
|
@@ -447,7 +470,8 @@ class Sentinel2Collection:
|
|
|
447
470
|
"""
|
|
448
471
|
msavi_expression = '0.5 * (2 * b("B8") + 1 - ((2 * b("B8") + 1) ** 2 - 8 * (b("B8") - b("B4"))) ** 0.5)'
|
|
449
472
|
msavi_calc = image.expression(msavi_expression)
|
|
450
|
-
msavi = msavi_calc.updateMask(msavi_calc.gte(threshold)).rename("msavi").copyProperties(image)
|
|
473
|
+
msavi = (msavi_calc.updateMask(msavi_calc.gte(threshold)).rename("msavi").copyProperties(image)
|
|
474
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start")))
|
|
451
475
|
return msavi
|
|
452
476
|
|
|
453
477
|
@staticmethod
|
|
@@ -465,7 +489,10 @@ class Sentinel2Collection:
|
|
|
465
489
|
"""
|
|
466
490
|
ndmi_expression = '(b("B8") - b("B11")) / (b("B8") + b("B11"))'
|
|
467
491
|
ndmi_calc = image.expression(ndmi_expression)
|
|
468
|
-
ndmi = ndmi_calc.updateMask(ndmi_calc.gte(threshold)).rename("ndmi")
|
|
492
|
+
ndmi = (ndmi_calc.updateMask(ndmi_calc.gte(threshold)).rename("ndmi")
|
|
493
|
+
.copyProperties(image)
|
|
494
|
+
.set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
495
|
+
)
|
|
469
496
|
return ndmi
|
|
470
497
|
|
|
471
498
|
@staticmethod
|
|
@@ -482,7 +509,9 @@ class Sentinel2Collection:
|
|
|
482
509
|
"""
|
|
483
510
|
nbr_expression = '(b("B8") - b("B12")) / (b("B8") + b("B12"))'
|
|
484
511
|
nbr_calc = image.expression(nbr_expression)
|
|
485
|
-
nbr = nbr_calc.updateMask(nbr_calc.gte(threshold)).rename("nbr")
|
|
512
|
+
nbr = (nbr_calc.updateMask(nbr_calc.gte(threshold)).rename("nbr")
|
|
513
|
+
.copyProperties(image).set("threshold", threshold, "system:time_start", image.get("system:time_start"))
|
|
514
|
+
)
|
|
486
515
|
return nbr
|
|
487
516
|
|
|
488
517
|
@staticmethod
|
|
@@ -544,7 +573,7 @@ class Sentinel2Collection:
|
|
|
544
573
|
return image.addBands(anomaly_image, overwrite=True)
|
|
545
574
|
|
|
546
575
|
@staticmethod
|
|
547
|
-
def
|
|
576
|
+
def maskClouds(image):
|
|
548
577
|
"""
|
|
549
578
|
Function to mask clouds using SCL band data.
|
|
550
579
|
|
|
@@ -556,10 +585,17 @@ class Sentinel2Collection:
|
|
|
556
585
|
"""
|
|
557
586
|
SCL = image.select("SCL")
|
|
558
587
|
CloudMask = SCL.neq(9)
|
|
559
|
-
return image.updateMask(CloudMask).copyProperties(image)
|
|
588
|
+
return image.updateMask(CloudMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
560
589
|
|
|
561
590
|
@staticmethod
|
|
562
|
-
def
|
|
591
|
+
def MaskCloudsS2(image):
|
|
592
|
+
warnings.warn("MaskCloudsS2 is deprecated. Please use maskClouds instead.",
|
|
593
|
+
DeprecationWarning,
|
|
594
|
+
stacklevel=2)
|
|
595
|
+
return Sentinel2Collection.maskClouds(image)
|
|
596
|
+
|
|
597
|
+
@staticmethod
|
|
598
|
+
def maskShadows(image):
|
|
563
599
|
"""
|
|
564
600
|
Function to mask cloud shadows using SCL band data.
|
|
565
601
|
|
|
@@ -571,10 +607,17 @@ class Sentinel2Collection:
|
|
|
571
607
|
"""
|
|
572
608
|
SCL = image.select("SCL")
|
|
573
609
|
ShadowMask = SCL.neq(3)
|
|
574
|
-
return image.updateMask(ShadowMask).copyProperties(image)
|
|
610
|
+
return image.updateMask(ShadowMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
611
|
+
|
|
612
|
+
@staticmethod
|
|
613
|
+
def MaskShadowsS2(image):
|
|
614
|
+
warnings.warn("MaskShadowsS2 is deprecated. Please use maskShadows instead.",
|
|
615
|
+
DeprecationWarning,
|
|
616
|
+
stacklevel=2)
|
|
617
|
+
return Sentinel2Collection.maskShadows(image)
|
|
575
618
|
|
|
576
619
|
@staticmethod
|
|
577
|
-
def
|
|
620
|
+
def maskWater(image):
|
|
578
621
|
"""
|
|
579
622
|
Function to mask water pixels using SCL band data.
|
|
580
623
|
|
|
@@ -586,10 +629,17 @@ class Sentinel2Collection:
|
|
|
586
629
|
"""
|
|
587
630
|
SCL = image.select("SCL")
|
|
588
631
|
WaterMask = SCL.neq(6)
|
|
589
|
-
return image.updateMask(WaterMask).copyProperties(image)
|
|
632
|
+
return image.updateMask(WaterMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
633
|
+
|
|
634
|
+
@staticmethod
|
|
635
|
+
def MaskWaterS2(image):
|
|
636
|
+
warnings.warn("MaskWaterS2 is deprecated. Please use maskWater instead.",
|
|
637
|
+
DeprecationWarning,
|
|
638
|
+
stacklevel=2)
|
|
639
|
+
return Sentinel2Collection.maskWater(image)
|
|
590
640
|
|
|
591
641
|
@staticmethod
|
|
592
|
-
def
|
|
642
|
+
def maskWaterByNDWI(image, threshold):
|
|
593
643
|
"""
|
|
594
644
|
Function to mask water pixels (mask land and cloud pixels) for all bands based on NDWI and a set threshold where
|
|
595
645
|
all pixels less than NDWI threshold are masked out.
|
|
@@ -604,11 +654,18 @@ class Sentinel2Collection:
|
|
|
604
654
|
ndwi_calc = image.normalizedDifference(
|
|
605
655
|
["B3", "B8"]
|
|
606
656
|
) # green-NIR / green+NIR -- full NDWI image
|
|
607
|
-
water = image.updateMask(ndwi_calc.lt(threshold))
|
|
657
|
+
water = image.updateMask(ndwi_calc.lt(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
608
658
|
return water
|
|
659
|
+
|
|
660
|
+
@staticmethod
|
|
661
|
+
def MaskWaterS2ByNDWI(image, threshold):
|
|
662
|
+
warnings.warn("MaskWaterS2ByNDWI is deprecated. Please use maskWaterByNDWI instead.",
|
|
663
|
+
DeprecationWarning,
|
|
664
|
+
stacklevel=2)
|
|
665
|
+
return Sentinel2Collection.maskWaterByNDWI(image, threshold)
|
|
609
666
|
|
|
610
667
|
@staticmethod
|
|
611
|
-
def
|
|
668
|
+
def maskToWater(image):
|
|
612
669
|
"""
|
|
613
670
|
Function to mask to water pixels (mask land and cloud pixels) using SCL band data.
|
|
614
671
|
|
|
@@ -620,7 +677,14 @@ class Sentinel2Collection:
|
|
|
620
677
|
"""
|
|
621
678
|
SCL = image.select("SCL")
|
|
622
679
|
WaterMask = SCL.eq(6)
|
|
623
|
-
return image.updateMask(WaterMask).copyProperties(image)
|
|
680
|
+
return image.updateMask(WaterMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
681
|
+
|
|
682
|
+
@staticmethod
|
|
683
|
+
def MaskToWaterS2(image):
|
|
684
|
+
warnings.warn("MaskToWaterS2 is deprecated. Please use maskToWater instead.",
|
|
685
|
+
DeprecationWarning,
|
|
686
|
+
stacklevel=2)
|
|
687
|
+
return Sentinel2Collection.maskToWater(image)
|
|
624
688
|
|
|
625
689
|
@staticmethod
|
|
626
690
|
def halite_mask(image, threshold):
|
|
@@ -635,7 +699,7 @@ class Sentinel2Collection:
|
|
|
635
699
|
ee.Image: ee.Image where halite pixels are masked (image without halite pixels).
|
|
636
700
|
"""
|
|
637
701
|
halite_index = image.normalizedDifference(["B4", "B11"])
|
|
638
|
-
mask = image.updateMask(halite_index.lt(threshold)).copyProperties(image)
|
|
702
|
+
mask = image.updateMask(halite_index.lt(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
639
703
|
return mask
|
|
640
704
|
|
|
641
705
|
@staticmethod
|
|
@@ -659,6 +723,7 @@ class Sentinel2Collection:
|
|
|
659
723
|
.updateMask(gypsum_index.lt(gypsum_threshold))
|
|
660
724
|
.rename("carbonate_muds")
|
|
661
725
|
.copyProperties(image)
|
|
726
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
662
727
|
)
|
|
663
728
|
return mask
|
|
664
729
|
|
|
@@ -688,7 +753,7 @@ class Sentinel2Collection:
|
|
|
688
753
|
if add_band_to_original_image:
|
|
689
754
|
return image.addBands(band_to_mask_image.updateMask(mask).rename(band_to_mask), overwrite=True)
|
|
690
755
|
else:
|
|
691
|
-
return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image))
|
|
756
|
+
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
757
|
|
|
693
758
|
@staticmethod
|
|
694
759
|
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,10 +789,10 @@ class Sentinel2Collection:
|
|
|
724
789
|
mask = band_for_mask_image.gt(threshold)
|
|
725
790
|
else:
|
|
726
791
|
mask = band_for_mask_image.lt(threshold)
|
|
727
|
-
return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask)
|
|
792
|
+
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
793
|
|
|
729
794
|
@staticmethod
|
|
730
|
-
def
|
|
795
|
+
def maskToWaterByNDWI(image, threshold):
|
|
731
796
|
"""
|
|
732
797
|
Function to mask all bands to water pixels (mask land and cloud pixels) based on NDWI.
|
|
733
798
|
|
|
@@ -741,11 +806,18 @@ class Sentinel2Collection:
|
|
|
741
806
|
ndwi_calc = image.normalizedDifference(
|
|
742
807
|
["B3", "B8"]
|
|
743
808
|
) # green-NIR / green+NIR -- full NDWI image
|
|
744
|
-
water = image.updateMask(ndwi_calc.gte(threshold))
|
|
809
|
+
water = image.updateMask(ndwi_calc.gte(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
745
810
|
return water
|
|
811
|
+
|
|
812
|
+
@staticmethod
|
|
813
|
+
def MaskToWaterS2ByNDWI(image, threshold):
|
|
814
|
+
warnings.warn("MaskToWaterS2ByNDWI is deprecated. Please use maskToWaterByNDWI instead.",
|
|
815
|
+
DeprecationWarning,
|
|
816
|
+
stacklevel=2)
|
|
817
|
+
return Sentinel2Collection.maskToWaterByNDWI(image, threshold)
|
|
746
818
|
|
|
747
819
|
@staticmethod
|
|
748
|
-
def
|
|
820
|
+
def pixelAreaSum(
|
|
749
821
|
image, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12
|
|
750
822
|
):
|
|
751
823
|
"""
|
|
@@ -804,8 +876,17 @@ class Sentinel2Collection:
|
|
|
804
876
|
# Call to iterate the calculate_and_set_area function over the list of bands, starting with the original image
|
|
805
877
|
final_image = ee.Image(bands.iterate(calculate_and_set_area, image))
|
|
806
878
|
return final_image
|
|
879
|
+
|
|
880
|
+
@staticmethod
|
|
881
|
+
def PixelAreaSum(
|
|
882
|
+
image, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12
|
|
883
|
+
):
|
|
884
|
+
warnings.warn("PixelAreaSum is deprecated. Please use pixelAreaSum instead.",
|
|
885
|
+
DeprecationWarning,
|
|
886
|
+
stacklevel=2)
|
|
887
|
+
return Sentinel2Collection.pixelAreaSum(image, band_name, geometry, threshold, scale, maxPixels)
|
|
807
888
|
|
|
808
|
-
def
|
|
889
|
+
def pixelAreaSumCollection(
|
|
809
890
|
self, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
810
891
|
):
|
|
811
892
|
"""
|
|
@@ -832,7 +913,7 @@ class Sentinel2Collection:
|
|
|
832
913
|
collection = self.collection
|
|
833
914
|
# Area calculation for each image in the collection, using the PixelAreaSum function
|
|
834
915
|
AreaCollection = collection.map(
|
|
835
|
-
lambda image: Sentinel2Collection.
|
|
916
|
+
lambda image: Sentinel2Collection.pixelAreaSum(
|
|
836
917
|
image,
|
|
837
918
|
band_name=band_name,
|
|
838
919
|
geometry=geometry,
|
|
@@ -848,17 +929,25 @@ class Sentinel2Collection:
|
|
|
848
929
|
|
|
849
930
|
# If an export path is provided, the area data will be exported to a CSV file
|
|
850
931
|
if area_data_export_path:
|
|
851
|
-
Sentinel2Collection(collection=self._PixelAreaSumCollection).
|
|
932
|
+
Sentinel2Collection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
|
|
852
933
|
# Returning the result in the desired format based on output_type argument or raising an error for invalid input
|
|
853
934
|
if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
|
|
854
935
|
return self._PixelAreaSumCollection
|
|
855
936
|
elif output_type == 'Sentinel2Collection':
|
|
856
937
|
return Sentinel2Collection(collection=self._PixelAreaSumCollection)
|
|
857
938
|
elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
|
|
858
|
-
return Sentinel2Collection(collection=self._PixelAreaSumCollection).
|
|
939
|
+
return Sentinel2Collection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names)
|
|
859
940
|
else:
|
|
860
941
|
raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'Sentinel2Collection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
|
|
861
942
|
|
|
943
|
+
def PixelAreaSumCollection(
|
|
944
|
+
self, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
945
|
+
):
|
|
946
|
+
warnings.warn("PixelAreaSumCollection is deprecated. Please use pixelAreaSumCollection instead.",
|
|
947
|
+
DeprecationWarning,
|
|
948
|
+
stacklevel=2)
|
|
949
|
+
return self.pixelAreaSumCollection(band_name, geometry, threshold, scale, maxPixels, output_type, area_data_export_path)
|
|
950
|
+
|
|
862
951
|
@staticmethod
|
|
863
952
|
def add_month_property_fn(image):
|
|
864
953
|
"""
|
|
@@ -939,8 +1028,13 @@ class Sentinel2Collection:
|
|
|
939
1028
|
return Sentinel2Collection(collection=ee.ImageCollection(paired.map(_pair_two)))
|
|
940
1029
|
|
|
941
1030
|
# Preferred path: merge many singleband products into the parent
|
|
942
|
-
if not isinstance(collections, list) or len(collections) == 0:
|
|
943
|
-
|
|
1031
|
+
# if not isinstance(collections, list) or len(collections) == 0:
|
|
1032
|
+
# raise ValueError("Provide a non-empty list of Sentinel2Collection objects in `collections`.")
|
|
1033
|
+
if not isinstance(collections, list):
|
|
1034
|
+
collections = [collections]
|
|
1035
|
+
|
|
1036
|
+
if len(collections) == 0:
|
|
1037
|
+
raise ValueError("Provide a non-empty list of LandsatCollection objects in `collections`.")
|
|
944
1038
|
|
|
945
1039
|
result = self.collection
|
|
946
1040
|
for extra in collections:
|
|
@@ -997,7 +1091,7 @@ class Sentinel2Collection:
|
|
|
997
1091
|
self._dates = dates
|
|
998
1092
|
return self._dates
|
|
999
1093
|
|
|
1000
|
-
def
|
|
1094
|
+
def exportProperties(self, property_names, file_path=None):
|
|
1001
1095
|
"""
|
|
1002
1096
|
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.
|
|
1003
1097
|
|
|
@@ -1052,6 +1146,13 @@ class Sentinel2Collection:
|
|
|
1052
1146
|
print(f"Properties saved to {file_path}")
|
|
1053
1147
|
|
|
1054
1148
|
return df
|
|
1149
|
+
|
|
1150
|
+
def ExportProperties(self, property_names, file_path=None):
|
|
1151
|
+
warnings.warn(
|
|
1152
|
+
"The `ExportProperties` method is deprecated and will be removed in future versions. Please use the `exportProperties` method instead.",
|
|
1153
|
+
DeprecationWarning,
|
|
1154
|
+
stacklevel=2)
|
|
1155
|
+
return self.exportProperties(property_names, file_path)
|
|
1055
1156
|
|
|
1056
1157
|
def get_filtered_collection(self):
|
|
1057
1158
|
"""
|
|
@@ -1698,6 +1799,391 @@ class Sentinel2Collection:
|
|
|
1698
1799
|
pass
|
|
1699
1800
|
|
|
1700
1801
|
return self._monthly_median
|
|
1802
|
+
|
|
1803
|
+
def yearly_mean_collection(self, start_month=1, end_month=12):
|
|
1804
|
+
"""
|
|
1805
|
+
Creates a yearly mean composite from the collection, with optional monthly filtering.
|
|
1806
|
+
|
|
1807
|
+
This function computes the mean for each year within the collection's date range.
|
|
1808
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1809
|
+
to calculate the mean only using imagery from that specific season for each year.
|
|
1810
|
+
|
|
1811
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1812
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1813
|
+
|
|
1814
|
+
Args:
|
|
1815
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1816
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1817
|
+
|
|
1818
|
+
Returns:
|
|
1819
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly mean composites.
|
|
1820
|
+
"""
|
|
1821
|
+
if self._yearly_mean is None:
|
|
1822
|
+
|
|
1823
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1824
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1825
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1826
|
+
|
|
1827
|
+
start_year = start_date_full.get('year')
|
|
1828
|
+
end_year = end_date_full.get('year')
|
|
1829
|
+
|
|
1830
|
+
if start_month != 1 or end_month != 12:
|
|
1831
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1832
|
+
else:
|
|
1833
|
+
processing_collection = self.collection
|
|
1834
|
+
|
|
1835
|
+
# Capture projection from the first image to restore it after reduction
|
|
1836
|
+
target_proj = self.collection.first().projection()
|
|
1837
|
+
|
|
1838
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1839
|
+
|
|
1840
|
+
def create_yearly_composite(year):
|
|
1841
|
+
year = ee.Number(year)
|
|
1842
|
+
# Define the full calendar year range
|
|
1843
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1844
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1845
|
+
|
|
1846
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1847
|
+
|
|
1848
|
+
# Calculate stats
|
|
1849
|
+
image_count = yearly_subset.size()
|
|
1850
|
+
yearly_reduction = yearly_subset.mean()
|
|
1851
|
+
|
|
1852
|
+
# Define the timestamp for the composite.
|
|
1853
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1854
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1855
|
+
|
|
1856
|
+
return yearly_reduction.set({
|
|
1857
|
+
'system:time_start': composite_date.millis(),
|
|
1858
|
+
'year': year,
|
|
1859
|
+
'month': start_month,
|
|
1860
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1861
|
+
'image_count': image_count,
|
|
1862
|
+
'season_start': start_month,
|
|
1863
|
+
'season_end': end_month
|
|
1864
|
+
}).reproject(target_proj)
|
|
1865
|
+
|
|
1866
|
+
# Map the function over the years list
|
|
1867
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1868
|
+
|
|
1869
|
+
# Convert to Collection
|
|
1870
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1871
|
+
|
|
1872
|
+
# Filter out any composites that were created from zero images.
|
|
1873
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1874
|
+
|
|
1875
|
+
self._yearly_mean = Sentinel2Collection(collection=final_collection)
|
|
1876
|
+
else:
|
|
1877
|
+
pass
|
|
1878
|
+
return self._yearly_mean
|
|
1879
|
+
|
|
1880
|
+
def yearly_median_collection(self, start_month=1, end_month=12):
|
|
1881
|
+
"""
|
|
1882
|
+
Creates a yearly median composite from the collection, with optional monthly filtering.
|
|
1883
|
+
|
|
1884
|
+
This function computes the median for each year within the collection's date range.
|
|
1885
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1886
|
+
to calculate the median only using imagery from that specific season for each year.
|
|
1887
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1888
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1889
|
+
|
|
1890
|
+
Args:
|
|
1891
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1892
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1893
|
+
|
|
1894
|
+
Returns:
|
|
1895
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly median composites.
|
|
1896
|
+
"""
|
|
1897
|
+
if self._yearly_median is None:
|
|
1898
|
+
|
|
1899
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1900
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1901
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1902
|
+
|
|
1903
|
+
start_year = start_date_full.get('year')
|
|
1904
|
+
end_year = end_date_full.get('year')
|
|
1905
|
+
|
|
1906
|
+
if start_month != 1 or end_month != 12:
|
|
1907
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1908
|
+
else:
|
|
1909
|
+
processing_collection = self.collection
|
|
1910
|
+
|
|
1911
|
+
# Capture projection from the first image to restore it after reduction
|
|
1912
|
+
target_proj = self.collection.first().projection()
|
|
1913
|
+
|
|
1914
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1915
|
+
|
|
1916
|
+
def create_yearly_composite(year):
|
|
1917
|
+
year = ee.Number(year)
|
|
1918
|
+
# Define the full calendar year range
|
|
1919
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1920
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1921
|
+
|
|
1922
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
1923
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1924
|
+
|
|
1925
|
+
# Calculate stats
|
|
1926
|
+
image_count = yearly_subset.size()
|
|
1927
|
+
yearly_reduction = yearly_subset.median()
|
|
1928
|
+
|
|
1929
|
+
# Define the timestamp for the composite.
|
|
1930
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1931
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1932
|
+
|
|
1933
|
+
return yearly_reduction.set({
|
|
1934
|
+
'system:time_start': composite_date.millis(),
|
|
1935
|
+
'year': year,
|
|
1936
|
+
'month': start_month,
|
|
1937
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1938
|
+
'image_count': image_count,
|
|
1939
|
+
'season_start': start_month,
|
|
1940
|
+
'season_end': end_month
|
|
1941
|
+
}).reproject(target_proj)
|
|
1942
|
+
|
|
1943
|
+
# Map the function over the years list
|
|
1944
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1945
|
+
|
|
1946
|
+
# Convert to Collection
|
|
1947
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1948
|
+
|
|
1949
|
+
# Filter out any composites that were created from zero images.
|
|
1950
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1951
|
+
|
|
1952
|
+
self._yearly_median = Sentinel2Collection(collection=final_collection)
|
|
1953
|
+
else:
|
|
1954
|
+
pass
|
|
1955
|
+
return self._yearly_median
|
|
1956
|
+
|
|
1957
|
+
def yearly_max_collection(self, start_month=1, end_month=12):
|
|
1958
|
+
"""
|
|
1959
|
+
Creates a yearly max composite from the collection, with optional monthly filtering.
|
|
1960
|
+
|
|
1961
|
+
This function computes the max for each year within the collection's date range.
|
|
1962
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1963
|
+
to calculate the max only using imagery from that specific season for each year.
|
|
1964
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1965
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1966
|
+
|
|
1967
|
+
Args:
|
|
1968
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1969
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1970
|
+
|
|
1971
|
+
Returns:
|
|
1972
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly max composites.
|
|
1973
|
+
"""
|
|
1974
|
+
if self._yearly_max is None:
|
|
1975
|
+
|
|
1976
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1977
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1978
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1979
|
+
|
|
1980
|
+
start_year = start_date_full.get('year')
|
|
1981
|
+
end_year = end_date_full.get('year')
|
|
1982
|
+
|
|
1983
|
+
if start_month != 1 or end_month != 12:
|
|
1984
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1985
|
+
else:
|
|
1986
|
+
processing_collection = self.collection
|
|
1987
|
+
|
|
1988
|
+
# Capture projection from the first image to restore it after reduction
|
|
1989
|
+
target_proj = self.collection.first().projection()
|
|
1990
|
+
|
|
1991
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1992
|
+
|
|
1993
|
+
def create_yearly_composite(year):
|
|
1994
|
+
year = ee.Number(year)
|
|
1995
|
+
# Define the full calendar year range
|
|
1996
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1997
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1998
|
+
|
|
1999
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
2000
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
2001
|
+
|
|
2002
|
+
# Calculate stats
|
|
2003
|
+
image_count = yearly_subset.size()
|
|
2004
|
+
yearly_reduction = yearly_subset.max()
|
|
2005
|
+
|
|
2006
|
+
# Define the timestamp for the composite.
|
|
2007
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
2008
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
2009
|
+
|
|
2010
|
+
return yearly_reduction.set({
|
|
2011
|
+
'system:time_start': composite_date.millis(),
|
|
2012
|
+
'year': year,
|
|
2013
|
+
'month': start_month,
|
|
2014
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
2015
|
+
'image_count': image_count,
|
|
2016
|
+
'season_start': start_month,
|
|
2017
|
+
'season_end': end_month
|
|
2018
|
+
}).reproject(target_proj)
|
|
2019
|
+
|
|
2020
|
+
# Map the function over the years list
|
|
2021
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
2022
|
+
|
|
2023
|
+
# Convert to Collection
|
|
2024
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
2025
|
+
|
|
2026
|
+
# Filter out any composites that were created from zero images.
|
|
2027
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
2028
|
+
|
|
2029
|
+
self._yearly_max = Sentinel2Collection(collection=final_collection)
|
|
2030
|
+
else:
|
|
2031
|
+
pass
|
|
2032
|
+
return self._yearly_max
|
|
2033
|
+
|
|
2034
|
+
def yearly_min_collection(self, start_month=1, end_month=12):
|
|
2035
|
+
"""
|
|
2036
|
+
Creates a yearly min composite from the collection, with optional monthly filtering.
|
|
2037
|
+
|
|
2038
|
+
This function computes the min for each year within the collection's date range.
|
|
2039
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
2040
|
+
to calculate the min only using imagery from that specific season for each year.
|
|
2041
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
2042
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
2043
|
+
|
|
2044
|
+
Args:
|
|
2045
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
2046
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
2047
|
+
|
|
2048
|
+
Returns:
|
|
2049
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly min composites.
|
|
2050
|
+
"""
|
|
2051
|
+
if self._yearly_min is None:
|
|
2052
|
+
|
|
2053
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
2054
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
2055
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
2056
|
+
|
|
2057
|
+
start_year = start_date_full.get('year')
|
|
2058
|
+
end_year = end_date_full.get('year')
|
|
2059
|
+
|
|
2060
|
+
if start_month != 1 or end_month != 12:
|
|
2061
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
2062
|
+
else:
|
|
2063
|
+
processing_collection = self.collection
|
|
2064
|
+
|
|
2065
|
+
# Capture projection from the first image to restore it after reduction
|
|
2066
|
+
target_proj = self.collection.first().projection()
|
|
2067
|
+
|
|
2068
|
+
years = ee.List.sequence(start_year, end_year)
|
|
2069
|
+
|
|
2070
|
+
def create_yearly_composite(year):
|
|
2071
|
+
year = ee.Number(year)
|
|
2072
|
+
# Define the full calendar year range
|
|
2073
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
2074
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
2075
|
+
|
|
2076
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
2077
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
2078
|
+
|
|
2079
|
+
# Calculate stats
|
|
2080
|
+
image_count = yearly_subset.size()
|
|
2081
|
+
yearly_reduction = yearly_subset.min()
|
|
2082
|
+
|
|
2083
|
+
# Define the timestamp for the composite.
|
|
2084
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
2085
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
2086
|
+
|
|
2087
|
+
return yearly_reduction.set({
|
|
2088
|
+
'system:time_start': composite_date.millis(),
|
|
2089
|
+
'year': year,
|
|
2090
|
+
'month': start_month,
|
|
2091
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
2092
|
+
'image_count': image_count,
|
|
2093
|
+
'season_start': start_month,
|
|
2094
|
+
'season_end': end_month
|
|
2095
|
+
}).reproject(target_proj)
|
|
2096
|
+
|
|
2097
|
+
# Map the function over the years list
|
|
2098
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
2099
|
+
|
|
2100
|
+
# Convert to Collection
|
|
2101
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
2102
|
+
|
|
2103
|
+
# Filter out any composites that were created from zero images.
|
|
2104
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
2105
|
+
|
|
2106
|
+
self._yearly_min = Sentinel2Collection(collection=final_collection)
|
|
2107
|
+
else:
|
|
2108
|
+
pass
|
|
2109
|
+
return self._yearly_min
|
|
2110
|
+
|
|
2111
|
+
def yearly_sum_collection(self, start_month=1, end_month=12):
|
|
2112
|
+
"""
|
|
2113
|
+
Creates a yearly sum composite from the collection, with optional monthly filtering.
|
|
2114
|
+
|
|
2115
|
+
This function computes the sum for each year within the collection's date range.
|
|
2116
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
2117
|
+
to calculate the sum only using imagery from that specific season for each year.
|
|
2118
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
2119
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
2120
|
+
|
|
2121
|
+
Args:
|
|
2122
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
2123
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
2124
|
+
|
|
2125
|
+
Returns:
|
|
2126
|
+
Object: A new instance of the same class (e.g., Sentinel2Collection) containing the yearly sum composites.
|
|
2127
|
+
"""
|
|
2128
|
+
if self._yearly_sum is None:
|
|
2129
|
+
|
|
2130
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
2131
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
2132
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
2133
|
+
|
|
2134
|
+
start_year = start_date_full.get('year')
|
|
2135
|
+
end_year = end_date_full.get('year')
|
|
2136
|
+
|
|
2137
|
+
if start_month != 1 or end_month != 12:
|
|
2138
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
2139
|
+
else:
|
|
2140
|
+
processing_collection = self.collection
|
|
2141
|
+
|
|
2142
|
+
# Capture projection from the first image to restore it after reduction
|
|
2143
|
+
target_proj = self.collection.first().projection()
|
|
2144
|
+
|
|
2145
|
+
years = ee.List.sequence(start_year, end_year)
|
|
2146
|
+
|
|
2147
|
+
def create_yearly_composite(year):
|
|
2148
|
+
year = ee.Number(year)
|
|
2149
|
+
# Define the full calendar year range
|
|
2150
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
2151
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
2152
|
+
|
|
2153
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
2154
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
2155
|
+
|
|
2156
|
+
# Calculate stats
|
|
2157
|
+
image_count = yearly_subset.size()
|
|
2158
|
+
yearly_reduction = yearly_subset.sum()
|
|
2159
|
+
|
|
2160
|
+
# Define the timestamp for the composite.
|
|
2161
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
2162
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
2163
|
+
|
|
2164
|
+
return yearly_reduction.set({
|
|
2165
|
+
'system:time_start': composite_date.millis(),
|
|
2166
|
+
'year': year,
|
|
2167
|
+
'month': start_month,
|
|
2168
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
2169
|
+
'image_count': image_count,
|
|
2170
|
+
'season_start': start_month,
|
|
2171
|
+
'season_end': end_month
|
|
2172
|
+
}).reproject(target_proj)
|
|
2173
|
+
|
|
2174
|
+
# Map the function over the years list
|
|
2175
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
2176
|
+
|
|
2177
|
+
# Convert to Collection
|
|
2178
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
2179
|
+
|
|
2180
|
+
# Filter out any composites that were created from zero images.
|
|
2181
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
2182
|
+
|
|
2183
|
+
self._yearly_sum = Sentinel2Collection(collection=final_collection)
|
|
2184
|
+
else:
|
|
2185
|
+
pass
|
|
2186
|
+
return self._yearly_sum
|
|
1701
2187
|
|
|
1702
2188
|
@property
|
|
1703
2189
|
def ndwi(self):
|
|
@@ -2272,7 +2758,7 @@ class Sentinel2Collection:
|
|
|
2272
2758
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
2273
2759
|
"""
|
|
2274
2760
|
if self._masked_water_collection is None:
|
|
2275
|
-
col = self.collection.map(Sentinel2Collection.
|
|
2761
|
+
col = self.collection.map(Sentinel2Collection.maskWater)
|
|
2276
2762
|
self._masked_water_collection = Sentinel2Collection(collection=col)
|
|
2277
2763
|
return self._masked_water_collection
|
|
2278
2764
|
|
|
@@ -2284,7 +2770,7 @@ class Sentinel2Collection:
|
|
|
2284
2770
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
2285
2771
|
"""
|
|
2286
2772
|
col = self.collection.map(
|
|
2287
|
-
lambda image: Sentinel2Collection.
|
|
2773
|
+
lambda image: Sentinel2Collection.maskWaterByNDWI(
|
|
2288
2774
|
image, threshold=threshold
|
|
2289
2775
|
)
|
|
2290
2776
|
)
|
|
@@ -2299,7 +2785,7 @@ class Sentinel2Collection:
|
|
|
2299
2785
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
2300
2786
|
"""
|
|
2301
2787
|
if self._masked_to_water_collection is None:
|
|
2302
|
-
col = self.collection.map(Sentinel2Collection.
|
|
2788
|
+
col = self.collection.map(Sentinel2Collection.maskToWater)
|
|
2303
2789
|
self._masked_water_collection = Sentinel2Collection(collection=col)
|
|
2304
2790
|
return self._masked_water_collection
|
|
2305
2791
|
|
|
@@ -2311,7 +2797,7 @@ class Sentinel2Collection:
|
|
|
2311
2797
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
2312
2798
|
"""
|
|
2313
2799
|
col = self.collection.map(
|
|
2314
|
-
lambda image: Sentinel2Collection.
|
|
2800
|
+
lambda image: Sentinel2Collection.maskToWaterByNDWI(
|
|
2315
2801
|
image, threshold=threshold
|
|
2316
2802
|
)
|
|
2317
2803
|
)
|
|
@@ -2326,7 +2812,7 @@ class Sentinel2Collection:
|
|
|
2326
2812
|
Sentinel2Collection: masked Sentinel2Collection image collection.
|
|
2327
2813
|
"""
|
|
2328
2814
|
if self._masked_clouds_collection is None:
|
|
2329
|
-
col = self.collection.map(Sentinel2Collection.
|
|
2815
|
+
col = self.collection.map(Sentinel2Collection.maskClouds)
|
|
2330
2816
|
self._masked_clouds_collection = Sentinel2Collection(collection=col)
|
|
2331
2817
|
return self._masked_clouds_collection
|
|
2332
2818
|
|
|
@@ -2339,7 +2825,7 @@ class Sentinel2Collection:
|
|
|
2339
2825
|
Sentinel2Collection: Sentinel2Collection image collection
|
|
2340
2826
|
"""
|
|
2341
2827
|
if self._masked_shadows_collection is None:
|
|
2342
|
-
col = self.collection.map(Sentinel2Collection.
|
|
2828
|
+
col = self.collection.map(Sentinel2Collection.maskShadows)
|
|
2343
2829
|
self._masked_shadows_collection = Sentinel2Collection(collection=col)
|
|
2344
2830
|
return self._masked_shadows_collection
|
|
2345
2831
|
|
|
@@ -2354,20 +2840,15 @@ class Sentinel2Collection:
|
|
|
2354
2840
|
Sentinel2Collection: masked Sentinel2Collection image collection.
|
|
2355
2841
|
|
|
2356
2842
|
"""
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
mask = ee.Image.constant(1).clip(polygon)
|
|
2843
|
+
# Convert the polygon to a mask
|
|
2844
|
+
mask = ee.Image.constant(1).clip(polygon)
|
|
2360
2845
|
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
# Update the internal collection state
|
|
2365
|
-
self._geometry_masked_collection = Sentinel2Collection(
|
|
2366
|
-
collection=masked_collection
|
|
2367
|
-
)
|
|
2846
|
+
# Update the mask of each image in the collection
|
|
2847
|
+
masked_collection = self.collection.map(lambda img: img.updateMask(mask)\
|
|
2848
|
+
.copyProperties(img).set('system:time_start', img.get('system:time_start')))
|
|
2368
2849
|
|
|
2369
2850
|
# Return the updated object
|
|
2370
|
-
return
|
|
2851
|
+
return Sentinel2Collection(collection=masked_collection)
|
|
2371
2852
|
|
|
2372
2853
|
def mask_out_polygon(self, polygon):
|
|
2373
2854
|
"""
|
|
@@ -2380,23 +2861,17 @@ class Sentinel2Collection:
|
|
|
2380
2861
|
Sentinel2Collection: masked Sentinel2Collection image collection.
|
|
2381
2862
|
|
|
2382
2863
|
"""
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
full_mask = ee.Image.constant(1)
|
|
2386
|
-
|
|
2387
|
-
# Use paint to set pixels inside polygon as 0
|
|
2388
|
-
area = full_mask.paint(polygon, 0)
|
|
2864
|
+
# Convert the polygon to a mask
|
|
2865
|
+
full_mask = ee.Image.constant(1)
|
|
2389
2866
|
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
# Update the internal collection state
|
|
2394
|
-
self._geometry_masked_out_collection = Sentinel2Collection(
|
|
2395
|
-
collection=masked_collection
|
|
2396
|
-
)
|
|
2867
|
+
# Use paint to set pixels inside polygon as 0
|
|
2868
|
+
area = full_mask.paint(polygon, 0)
|
|
2397
2869
|
|
|
2870
|
+
# Update the mask of each image in the collection
|
|
2871
|
+
masked_collection = self.collection.map(lambda img: img.updateMask(area)\
|
|
2872
|
+
.copyProperties(img).set('system:time_start', img.get('system:time_start')))
|
|
2398
2873
|
# Return the updated object
|
|
2399
|
-
return
|
|
2874
|
+
return Sentinel2Collection(collection=masked_collection)
|
|
2400
2875
|
|
|
2401
2876
|
def mask_halite(self, threshold):
|
|
2402
2877
|
"""
|
|
@@ -2465,20 +2940,28 @@ class Sentinel2Collection:
|
|
|
2465
2940
|
if classify_above_threshold:
|
|
2466
2941
|
if mask_zeros is True:
|
|
2467
2942
|
col = self.collection.map(
|
|
2468
|
-
lambda image: image.select(band_name).gte(threshold)
|
|
2943
|
+
lambda image: image.select(band_name).gte(threshold)
|
|
2944
|
+
.rename(band_name).selfMask().copyProperties(image)
|
|
2945
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
2469
2946
|
)
|
|
2470
2947
|
else:
|
|
2471
2948
|
col = self.collection.map(
|
|
2472
|
-
lambda image: image.select(band_name).gte(threshold)
|
|
2949
|
+
lambda image: image.select(band_name).gte(threshold)
|
|
2950
|
+
.rename(band_name).copyProperties(image)
|
|
2951
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
2473
2952
|
)
|
|
2474
2953
|
else:
|
|
2475
2954
|
if mask_zeros is True:
|
|
2476
2955
|
col = self.collection.map(
|
|
2477
|
-
lambda image: image.select(band_name).lte(threshold)
|
|
2956
|
+
lambda image: image.select(band_name).lte(threshold)
|
|
2957
|
+
.rename(band_name).selfMask().copyProperties(image)
|
|
2958
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
2478
2959
|
)
|
|
2479
2960
|
else:
|
|
2480
2961
|
col = self.collection.map(
|
|
2481
|
-
lambda image: image.select(band_name).lte(threshold)
|
|
2962
|
+
lambda image: image.select(band_name).lte(threshold)
|
|
2963
|
+
.rename(band_name).copyProperties(image)
|
|
2964
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
2482
2965
|
)
|
|
2483
2966
|
|
|
2484
2967
|
return Sentinel2Collection(collection=col)
|
|
@@ -2519,6 +3002,239 @@ class Sentinel2Collection:
|
|
|
2519
3002
|
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
3003
|
return Sentinel2Collection(collection=col)
|
|
2521
3004
|
|
|
3005
|
+
def mann_kendall_trend(self, target_band=None, join_method='system:time_start', geometry=None):
|
|
3006
|
+
"""
|
|
3007
|
+
Calculates the Mann-Kendall S-value, Variance, Z-Score, and Confidence Level for each pixel in the image collection, in addition to calculating
|
|
3008
|
+
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'.
|
|
3009
|
+
|
|
3010
|
+
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.
|
|
3011
|
+
Note that this function is computationally intensive and may take a long time to run for large image collections or high-resolution images.
|
|
3012
|
+
|
|
3013
|
+
The 's_statistic' band represents the Mann-Kendall S-value, which is a measure of the strength and direction of the trend.
|
|
3014
|
+
The 'variance' band represents the variance of the S-value, which is a measure of the variability of the S-value.
|
|
3015
|
+
The 'z_score' band represents the Z-Score, which is a measure of the significance of the trend.
|
|
3016
|
+
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).
|
|
3017
|
+
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.
|
|
3018
|
+
|
|
3019
|
+
Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
|
|
3020
|
+
You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
|
|
3021
|
+
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.
|
|
3022
|
+
|
|
3023
|
+
Args:
|
|
3024
|
+
image_collection (Sentinel2Collection or ee.ImageCollection): The input image collection for which the Mann-Kendall and Sen's slope trend statistics will be calculated.
|
|
3025
|
+
target_band (str): The band name to be used for the output anomaly image. e.g. 'ndvi'
|
|
3026
|
+
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'.
|
|
3027
|
+
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.
|
|
3028
|
+
|
|
3029
|
+
Returns:
|
|
3030
|
+
ee.Image: An image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
|
|
3031
|
+
"""
|
|
3032
|
+
########## PART 1 - S-VALUE CALCULATION ##########
|
|
3033
|
+
##### https://vsp.pnnl.gov/help/vsample/design_trend_mann_kendall.htm #####
|
|
3034
|
+
image_collection = self
|
|
3035
|
+
if isinstance(image_collection, Sentinel2Collection):
|
|
3036
|
+
image_collection = image_collection.collection
|
|
3037
|
+
elif isinstance(image_collection, ee.ImageCollection):
|
|
3038
|
+
pass
|
|
3039
|
+
else:
|
|
3040
|
+
raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid Sentinel2Collection or ee.ImageCollection object.')
|
|
3041
|
+
|
|
3042
|
+
if target_band is None:
|
|
3043
|
+
raise ValueError('The `target_band` parameter must be specified.')
|
|
3044
|
+
if not isinstance(target_band, str):
|
|
3045
|
+
raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
|
|
3046
|
+
|
|
3047
|
+
if geometry is not None and not isinstance(geometry, ee.Geometry):
|
|
3048
|
+
raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
|
|
3049
|
+
|
|
3050
|
+
native_projection = image_collection.first().select(target_band).projection()
|
|
3051
|
+
|
|
3052
|
+
# define the join, which will join all images newer than the current image
|
|
3053
|
+
# use system:time_start if the image does not have a Date_Filter property
|
|
3054
|
+
if join_method == 'system:time_start':
|
|
3055
|
+
# get all images where the leftField value is less than (before) the rightField value
|
|
3056
|
+
time_filter = ee.Filter.lessThan(leftField='system:time_start',
|
|
3057
|
+
rightField='system:time_start')
|
|
3058
|
+
elif join_method == 'Date_Filter':
|
|
3059
|
+
# get all images where the leftField value is less than (before) the rightField value
|
|
3060
|
+
time_filter = ee.Filter.lessThan(leftField='Date_Filter',
|
|
3061
|
+
rightField='Date_Filter')
|
|
3062
|
+
else:
|
|
3063
|
+
raise ValueError(f'The chosen `join_method`: {join_method} does not match the options of "system:time_start" or "Date_Filter".')
|
|
3064
|
+
|
|
3065
|
+
# for any matches during a join, set image as a property key called 'future_image'
|
|
3066
|
+
join = ee.Join.saveAll(matchesKey='future_image')
|
|
3067
|
+
|
|
3068
|
+
# apply the join on the input collection
|
|
3069
|
+
# joining all images newer than the current image with the current image
|
|
3070
|
+
joined_collection = ee.ImageCollection(join.apply(primary=image_collection,
|
|
3071
|
+
secondary=image_collection, condition=time_filter))
|
|
3072
|
+
|
|
3073
|
+
# defining a collection to calculate the partial S value for each match in the join
|
|
3074
|
+
# e.g. t4-t1, t3-t1, t2-1 if there are 4 images
|
|
3075
|
+
def calculate_partial_s(current_image):
|
|
3076
|
+
# select the target band for arithmetic
|
|
3077
|
+
current_val = current_image.select(target_band)
|
|
3078
|
+
# get the joined images from the current image properties and cast the joined images as a list
|
|
3079
|
+
future_image_list = ee.List(current_image.get('future_image'))
|
|
3080
|
+
# convert the joined list to an image collection
|
|
3081
|
+
future_image_collection = ee.ImageCollection(future_image_list)
|
|
3082
|
+
|
|
3083
|
+
# define a function that will calculate the difference between the joined images and the current image,
|
|
3084
|
+
# then calculate the partial S sign based on the value of the difference calculation
|
|
3085
|
+
def get_sign(future_image):
|
|
3086
|
+
# select the target band for arithmetic from the future image
|
|
3087
|
+
future_val = future_image.select(target_band)
|
|
3088
|
+
# calculate the difference, i.e. t2-t1
|
|
3089
|
+
difference = future_val.subtract(current_val)
|
|
3090
|
+
# determine the sign of the difference value (1 if diff > 0, 0 if 0, and -1 if diff < 0)
|
|
3091
|
+
# use .unmask(0) to set any masked pixels as 0 to avoid
|
|
3092
|
+
|
|
3093
|
+
sign = difference.signum().unmask(0)
|
|
3094
|
+
|
|
3095
|
+
return sign
|
|
3096
|
+
|
|
3097
|
+
# map the get_sign() function along the future image col
|
|
3098
|
+
# then sum the values for each pixel to get the partial S value
|
|
3099
|
+
return future_image_collection.map(get_sign).sum()
|
|
3100
|
+
|
|
3101
|
+
# calculate the partial s value for each image in the joined/input image collection
|
|
3102
|
+
partial_s_col = joined_collection.map(calculate_partial_s)
|
|
3103
|
+
|
|
3104
|
+
# convert the image collection to an image of s_statistic values per pixel
|
|
3105
|
+
# where the s_statistic is the sum of partial s values
|
|
3106
|
+
# renaming the band as 's_statistic' for later usage
|
|
3107
|
+
final_s_image = partial_s_col.sum().rename('s_statistic').setDefaultProjection(native_projection)
|
|
3108
|
+
|
|
3109
|
+
|
|
3110
|
+
########## PART 2 - VARIANCE and Z-SCORE ##########
|
|
3111
|
+
# to calculate variance we need to know how many pixels were involved in the partial_s calculations per pixel
|
|
3112
|
+
# we do this by using count() and turn the value to a float for later arithmetic
|
|
3113
|
+
n = image_collection.select(target_band).count().toFloat()
|
|
3114
|
+
|
|
3115
|
+
##### VARIANCE CALCULATION #####
|
|
3116
|
+
# as we are using floating point values with high precision, it is HIGHLY
|
|
3117
|
+
# unlikely that there will be multiple pixel values with the same value.
|
|
3118
|
+
# Thus, we opt to use the simplified variance calculation approach as the
|
|
3119
|
+
# impacts to the output value are negligible and the processing benefits are HUGE
|
|
3120
|
+
# variance = (n * (n - 1) * (2n + 5)) / 18
|
|
3121
|
+
var_s = n.multiply(n.subtract(1))\
|
|
3122
|
+
.multiply(n.multiply(2).add(5))\
|
|
3123
|
+
.divide(18).rename('variance')
|
|
3124
|
+
|
|
3125
|
+
z_score = ee.Image().expression(
|
|
3126
|
+
"""
|
|
3127
|
+
(s > 0) ? (s - 1) / sqrt(var) :
|
|
3128
|
+
(s < 0) ? (s + 1) / sqrt(var) :
|
|
3129
|
+
0
|
|
3130
|
+
""",
|
|
3131
|
+
{'s': final_s_image, 'var': var_s}
|
|
3132
|
+
).rename('z_score')
|
|
3133
|
+
|
|
3134
|
+
confidence = z_score.abs().divide(ee.Number(2).sqrt()).erf().rename('confidence')
|
|
3135
|
+
|
|
3136
|
+
stat_bands = ee.Image([var_s, z_score, confidence])
|
|
3137
|
+
|
|
3138
|
+
mk_stats_image = final_s_image.addBands(stat_bands)
|
|
3139
|
+
|
|
3140
|
+
########## PART 3 - Sen's Slope ##########
|
|
3141
|
+
def add_year_band(image):
|
|
3142
|
+
if join_method == 'Date_Filter':
|
|
3143
|
+
# Get the string 'YYYY-MM-DD'
|
|
3144
|
+
date_string = image.get('Date_Filter')
|
|
3145
|
+
# Parse it into an ee.Date object (handles the conversion to time math)
|
|
3146
|
+
date = ee.Date.parse('YYYY-MM-dd', date_string)
|
|
3147
|
+
else:
|
|
3148
|
+
# Standard way: assumes system:time_start exists
|
|
3149
|
+
date = image.date()
|
|
3150
|
+
years = date.difference(ee.Date('1970-01-01'), 'year')
|
|
3151
|
+
return image.addBands(ee.Image(years).float().rename('year'))
|
|
3152
|
+
|
|
3153
|
+
slope_input = image_collection.map(add_year_band).select(['year', target_band])
|
|
3154
|
+
|
|
3155
|
+
sens_slope = slope_input.reduce(ee.Reducer.sensSlope())
|
|
3156
|
+
|
|
3157
|
+
slope_band = sens_slope.select('slope')
|
|
3158
|
+
|
|
3159
|
+
# add a mask to the final image to remove pixels with less than min_observations
|
|
3160
|
+
# mainly an effort to mask pixels outside of the boundary of the input image collection
|
|
3161
|
+
min_observations = 1
|
|
3162
|
+
valid_mask = n.gte(min_observations)
|
|
3163
|
+
|
|
3164
|
+
final_image = mk_stats_image.addBands(slope_band).updateMask(valid_mask)
|
|
3165
|
+
|
|
3166
|
+
if geometry is not None:
|
|
3167
|
+
mask = ee.Image(1).clip(geometry)
|
|
3168
|
+
final_image = final_image.updateMask(mask)
|
|
3169
|
+
|
|
3170
|
+
return final_image.setDefaultProjection(native_projection)
|
|
3171
|
+
|
|
3172
|
+
def sens_slope_trend(self, target_band=None, join_method='system:time_start', geometry=None):
|
|
3173
|
+
"""
|
|
3174
|
+
Calculates Sen's Slope (trend magnitude) for the collection.
|
|
3175
|
+
This is a lighter-weight alternative to the full `mann_kendall_trend` function if only
|
|
3176
|
+
the direction and magnitude of the trend are needed.
|
|
3177
|
+
|
|
3178
|
+
Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
|
|
3179
|
+
You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
|
|
3180
|
+
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.
|
|
3181
|
+
|
|
3182
|
+
Args:
|
|
3183
|
+
target_band (str): The name of the band to analyze. Defaults to 'ndvi'.
|
|
3184
|
+
join_method (str): Property to use for time sorting ('system:time_start' or 'Date_Filter').
|
|
3185
|
+
geometry (ee.Geometry, optional): Geometry to mask the final output.
|
|
3186
|
+
|
|
3187
|
+
Returns:
|
|
3188
|
+
ee.Image: An image containing the 'slope' band.
|
|
3189
|
+
"""
|
|
3190
|
+
image_collection = self
|
|
3191
|
+
if isinstance(image_collection, Sentinel2Collection):
|
|
3192
|
+
image_collection = image_collection.collection
|
|
3193
|
+
elif isinstance(image_collection, ee.ImageCollection):
|
|
3194
|
+
pass
|
|
3195
|
+
else:
|
|
3196
|
+
raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid Sentinel2Collection or ee.ImageCollection object.')
|
|
3197
|
+
|
|
3198
|
+
if target_band is None:
|
|
3199
|
+
raise ValueError('The `target_band` parameter must be specified.')
|
|
3200
|
+
if not isinstance(target_band, str):
|
|
3201
|
+
raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
|
|
3202
|
+
|
|
3203
|
+
if geometry is not None and not isinstance(geometry, ee.Geometry):
|
|
3204
|
+
raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
|
|
3205
|
+
|
|
3206
|
+
native_projection = image_collection.first().select(target_band).projection()
|
|
3207
|
+
|
|
3208
|
+
# Add Year Band (Time X-Axis)
|
|
3209
|
+
def add_year_band(image):
|
|
3210
|
+
# Handle user-defined date strings vs system time
|
|
3211
|
+
if join_method == 'Date_Filter':
|
|
3212
|
+
date_string = image.get('Date_Filter')
|
|
3213
|
+
date = ee.Date.parse('YYYY-MM-dd', date_string)
|
|
3214
|
+
else:
|
|
3215
|
+
date = image.date()
|
|
3216
|
+
|
|
3217
|
+
# Convert to fractional years relative to epoch
|
|
3218
|
+
years = date.difference(ee.Date('1970-01-01'), 'year')
|
|
3219
|
+
return image.addBands(ee.Image(years).float().rename('year'))
|
|
3220
|
+
|
|
3221
|
+
# Prepare Collection: Select ONLY [Year, Target]
|
|
3222
|
+
# sensSlope expects Band 0 = Independent (X), Band 1 = Dependent (Y)
|
|
3223
|
+
slope_input = self.collection.map(add_year_band).select(['year', target_band])
|
|
3224
|
+
|
|
3225
|
+
# Run the Native Reducer
|
|
3226
|
+
sens_result = slope_input.reduce(ee.Reducer.sensSlope())
|
|
3227
|
+
|
|
3228
|
+
# Extract and Mask
|
|
3229
|
+
slope_band = sens_result.select('slope')
|
|
3230
|
+
|
|
3231
|
+
if geometry is not None:
|
|
3232
|
+
mask = ee.Image(1).clip(geometry)
|
|
3233
|
+
slope_band = slope_band.updateMask(mask)
|
|
3234
|
+
|
|
3235
|
+
return slope_band.setDefaultProjection(native_projection)
|
|
3236
|
+
|
|
3237
|
+
|
|
2522
3238
|
def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
|
|
2523
3239
|
"""
|
|
2524
3240
|
Masks select pixels of a selected band from an image based on another specified band and threshold (optional).
|
|
@@ -2601,7 +3317,8 @@ class Sentinel2Collection:
|
|
|
2601
3317
|
)
|
|
2602
3318
|
|
|
2603
3319
|
# guarantee single band + keep properties
|
|
2604
|
-
out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())
|
|
3320
|
+
out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())\
|
|
3321
|
+
.set('system:time_start', prim.get('system:time_start'))
|
|
2605
3322
|
out = out.set('Date_Filter', prim.get('Date_Filter'))
|
|
2606
3323
|
return ee.Image(out) # <-- return as Image
|
|
2607
3324
|
|
|
@@ -2659,7 +3376,7 @@ class Sentinel2Collection:
|
|
|
2659
3376
|
new_col = self.collection.filter(ee.Filter.eq("Date_Filter", img_date))
|
|
2660
3377
|
return new_col.first()
|
|
2661
3378
|
|
|
2662
|
-
def
|
|
3379
|
+
def collectionStitch(self, img_col2):
|
|
2663
3380
|
"""
|
|
2664
3381
|
Function to mosaic two Sentinel2Collection objects which share image dates.
|
|
2665
3382
|
Mosaics are only formed for dates where both image collections have images.
|
|
@@ -2713,8 +3430,15 @@ class Sentinel2Collection:
|
|
|
2713
3430
|
# Return a Sentinel2Collection instance
|
|
2714
3431
|
return Sentinel2Collection(collection=new_col)
|
|
2715
3432
|
|
|
3433
|
+
def CollectionStitch(self, img_col2):
|
|
3434
|
+
warnings.warn(
|
|
3435
|
+
"The `CollectionStitch` method is deprecated and will be removed in future versions. Please use the `collectionStitch` method instead.",
|
|
3436
|
+
DeprecationWarning,
|
|
3437
|
+
stacklevel=2)
|
|
3438
|
+
return self.collectionStitch(img_col2)
|
|
3439
|
+
|
|
2716
3440
|
@property
|
|
2717
|
-
def
|
|
3441
|
+
def mosaicByDateDepr(self):
|
|
2718
3442
|
"""
|
|
2719
3443
|
Property attribute function to mosaic collection images that share the same date. The properties CLOUD_PIXEL_PERCENTAGE and NODATA_PIXEL_PERCENTAGE
|
|
2720
3444
|
for each image are used to calculate an overall mean, which replaces the CLOUD_PIXEL_PERCENTAGE and NODATA_PIXEL_PERCENTAGE for each mosaiced image.
|
|
@@ -2780,6 +3504,76 @@ class Sentinel2Collection:
|
|
|
2780
3504
|
self._MosaicByDate = col
|
|
2781
3505
|
|
|
2782
3506
|
return self._MosaicByDate
|
|
3507
|
+
|
|
3508
|
+
@property
|
|
3509
|
+
def mosaicByDate(self):
|
|
3510
|
+
"""
|
|
3511
|
+
Property attribute function to mosaic collection images that share the same date.
|
|
3512
|
+
|
|
3513
|
+
The property CLOUD_COVER for each image is used to calculate an overall mean,
|
|
3514
|
+
which replaces the CLOUD_COVER property for each mosaiced image.
|
|
3515
|
+
Server-side friendly.
|
|
3516
|
+
|
|
3517
|
+
NOTE: if images are removed from the collection from cloud filtering, you may have mosaics composed of only one image.
|
|
3518
|
+
|
|
3519
|
+
Returns:
|
|
3520
|
+
LandsatCollection: LandsatCollection image collection with mosaiced imagery and mean CLOUD_COVER as a property
|
|
3521
|
+
"""
|
|
3522
|
+
if self._MosaicByDate is None:
|
|
3523
|
+
distinct_dates = self.collection.distinct("Date_Filter")
|
|
3524
|
+
|
|
3525
|
+
# Define a join to link images by Date_Filter
|
|
3526
|
+
filter_date = ee.Filter.equals(leftField="Date_Filter", rightField="Date_Filter")
|
|
3527
|
+
join = ee.Join.saveAll(matchesKey="date_matches")
|
|
3528
|
+
|
|
3529
|
+
# Apply the join
|
|
3530
|
+
# Primary: Distinct dates collection
|
|
3531
|
+
# Secondary: The full original collection
|
|
3532
|
+
joined_col = ee.ImageCollection(join.apply(distinct_dates, self.collection, filter_date))
|
|
3533
|
+
|
|
3534
|
+
# Define the mosaicking function
|
|
3535
|
+
def _mosaic_day(img):
|
|
3536
|
+
# Recover the list of images for this day
|
|
3537
|
+
daily_list = ee.List(img.get("date_matches"))
|
|
3538
|
+
daily_col = ee.ImageCollection.fromImages(daily_list)
|
|
3539
|
+
|
|
3540
|
+
# Create the mosaic
|
|
3541
|
+
mosaic = daily_col.mosaic().setDefaultProjection(img.projection())
|
|
3542
|
+
|
|
3543
|
+
# Calculate means for Sentinel-2 specific props
|
|
3544
|
+
cloud_pct = daily_col.aggregate_mean("CLOUDY_PIXEL_PERCENTAGE")
|
|
3545
|
+
nodata_pct = daily_col.aggregate_mean("NODATA_PIXEL_PERCENTAGE")
|
|
3546
|
+
|
|
3547
|
+
# Properties to preserve from the representative image
|
|
3548
|
+
props_of_interest = [
|
|
3549
|
+
"SPACECRAFT_NAME",
|
|
3550
|
+
"SENSING_ORBIT_NUMBER",
|
|
3551
|
+
"SENSING_ORBIT_DIRECTION",
|
|
3552
|
+
"MISSION_ID",
|
|
3553
|
+
"PLATFORM_IDENTIFIER",
|
|
3554
|
+
"system:time_start"
|
|
3555
|
+
]
|
|
3556
|
+
|
|
3557
|
+
# Return mosaic with properties set
|
|
3558
|
+
return mosaic.copyProperties(img, props_of_interest).set({
|
|
3559
|
+
"CLOUDY_PIXEL_PERCENTAGE": cloud_pct,
|
|
3560
|
+
"NODATA_PIXEL_PERCENTAGE": nodata_pct
|
|
3561
|
+
})
|
|
3562
|
+
|
|
3563
|
+
# 5. Map the function and wrap the result
|
|
3564
|
+
mosaiced_col = joined_col.map(_mosaic_day)
|
|
3565
|
+
self._MosaicByDate = Sentinel2Collection(collection=mosaiced_col)
|
|
3566
|
+
|
|
3567
|
+
# Convert the list of mosaics to an ImageCollection
|
|
3568
|
+
return self._MosaicByDate
|
|
3569
|
+
|
|
3570
|
+
@property
|
|
3571
|
+
def MosaicByDate(self):
|
|
3572
|
+
warnings.warn(
|
|
3573
|
+
"The `MosaicByDate` property is deprecated and will be removed in future versions. Please use the `mosaicByDate` property instead.",
|
|
3574
|
+
DeprecationWarning,
|
|
3575
|
+
stacklevel=2)
|
|
3576
|
+
return self.mosaicByDate
|
|
2783
3577
|
|
|
2784
3578
|
@staticmethod
|
|
2785
3579
|
def ee_to_df(
|