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.
@@ -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.MosaicByDate #mosaic images/tiles with same date
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").copyProperties(image).set("threshold", threshold)
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").copyProperties(image).set("threshold", threshold)
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").copyProperties(image).set("threshold", threshold)
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).set("threshold", threshold)
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").copyProperties(image).set("threshold", threshold)
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").copyProperties(image).set("threshold", threshold)
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 MaskCloudsS2(image):
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 MaskShadowsS2(image):
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 MaskWaterS2(image):
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 MaskWaterS2ByNDWI(image, threshold):
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 MaskToWaterS2(image):
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 MaskToWaterS2ByNDWI(image, threshold):
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 PixelAreaSum(
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 PixelAreaSumCollection(
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.PixelAreaSum(
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).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
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).ExportProperties(property_names=prop_names)
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
- raise ValueError("Provide a non-empty list of Sentinel2Collection objects in `collections`.")
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 ExportProperties(self, property_names, file_path=None):
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.MaskWaterS2)
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.MaskWaterS2ByNDWI(
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.MaskToWaterS2)
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.MaskToWaterS2ByNDWI(
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.MaskCloudsS2)
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.MaskShadowsS2)
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
- if self._geometry_masked_collection is None:
2358
- # Convert the polygon to a mask
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
- # Update the mask of each image in the collection
2362
- masked_collection = self.collection.map(lambda img: img.updateMask(mask))
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 self._geometry_masked_collection
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
- if self._geometry_masked_out_collection is None:
2384
- # Convert the polygon to a mask
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
- # Update the mask of each image in the collection
2391
- masked_collection = self.collection.map(lambda img: img.updateMask(area))
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 self._geometry_masked_out_collection
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).rename(band_name).selfMask().copyProperties(image)
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).rename(band_name).copyProperties(image)
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).rename(band_name).selfMask().copyProperties(image)
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).rename(band_name).copyProperties(image)
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 CollectionStitch(self, img_col2):
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 MosaicByDate(self):
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(