RadGEEToolbox 1.6.9__py3-none-any.whl → 1.7.0__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.
@@ -161,9 +161,11 @@ class Sentinel2Collection:
161
161
  self._geometry_masked_collection = None
162
162
  self._geometry_masked_out_collection = None
163
163
  self._masked_clouds_collection = None
164
+ self._masked_shadows_collection = None
164
165
  self._masked_to_water_collection = None
165
166
  self._masked_water_collection = None
166
167
  self._median = None
168
+ self._monthly_median = None
167
169
  self._mean = None
168
170
  self._max = None
169
171
  self._min = None
@@ -478,11 +480,64 @@ class Sentinel2Collection:
478
480
  nbr_calc = image.expression(nbr_expression)
479
481
  nbr = nbr_calc.updateMask(nbr_calc.gte(threshold)).rename("nbr").copyProperties(image).set("threshold", threshold)
480
482
  return nbr
483
+
484
+ @staticmethod
485
+ def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True):
486
+ """
487
+ Calculates the anomaly of a singleband image compared to the mean of the singleband image.
488
+
489
+ This function computes the anomaly for each band in the input image by
490
+ subtracting the mean value of that band from a provided image.
491
+ The anomaly is a measure of how much the pixel values deviate from the
492
+ average conditions represented by the mean of the image.
493
+
494
+ Args:
495
+ image (ee.Image): An ee.Image for which the anomaly is to be calculated.
496
+ It is assumed that this image is a singleband image.
497
+ geometry (ee.Geometry): The geometry for image reduction to define the mean value to be used for anomaly calculation.
498
+ band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
499
+ anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
500
+ replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band in the output image. If True, the output image will contain only the anomaly band. If False, the output image will contain both the original band and the anomaly band. Default is True.
501
+
502
+ Returns:
503
+ ee.Image: An ee.Image where each band represents the anomaly (deviation from
504
+ the mean) for that band. The output image retains the same band name.
505
+ """
506
+ if band_name:
507
+ band_name = band_name
508
+ else:
509
+ band_name = ee.String(image.bandNames().get(0))
510
+
511
+ image_to_process = image.select([band_name])
512
+
513
+ # Calculate the mean image of the provided collection.
514
+ mean_image = image_to_process.reduceRegion(
515
+ reducer=ee.Reducer.mean(),
516
+ geometry=geometry,
517
+ scale=10,
518
+ maxPixels=1e13
519
+ ).toImage()
520
+
521
+ # Compute the anomaly by subtracting the mean image from the input image.
522
+ anomaly_image = image_to_process.subtract(mean_image)
523
+ if anomaly_band_name is None:
524
+ if band_name:
525
+ anomaly_image = anomaly_image.rename(band_name)
526
+ else:
527
+ # Preserve original properties from the input image.
528
+ anomaly_image = anomaly_image.rename(ee.String(image.bandNames().get(0)))
529
+ else:
530
+ anomaly_image = anomaly_image.rename(anomaly_band_name)
531
+ # return anomaly_image
532
+ if replace:
533
+ return anomaly_image.copyProperties(image)
534
+ else:
535
+ return image.addBands(anomaly_image, overwrite=True)
481
536
 
482
537
  @staticmethod
483
538
  def MaskCloudsS2(image):
484
539
  """
485
- Function to map clouds using SCL band data.
540
+ Function to mask clouds using SCL band data.
486
541
 
487
542
  Args:
488
543
  image (ee.Image): input image
@@ -493,6 +548,21 @@ class Sentinel2Collection:
493
548
  SCL = image.select("SCL")
494
549
  CloudMask = SCL.neq(9)
495
550
  return image.updateMask(CloudMask).copyProperties(image)
551
+
552
+ @staticmethod
553
+ def MaskShadowsS2(image):
554
+ """
555
+ Function to mask cloud shadows using SCL band data.
556
+
557
+ Args:
558
+ image (ee.Image): input image
559
+
560
+ Returns:
561
+ ee.Image: output ee.Image with cloud shadows masked
562
+ """
563
+ SCL = image.select("SCL")
564
+ ShadowMask = SCL.neq(3)
565
+ return image.updateMask(ShadowMask).copyProperties(image)
496
566
 
497
567
  @staticmethod
498
568
  def MaskWaterS2(image):
@@ -582,6 +652,70 @@ class Sentinel2Collection:
582
652
  .copyProperties(image)
583
653
  )
584
654
  return mask
655
+
656
+ @staticmethod
657
+ def mask_via_band_fn(image, band_to_mask, band_for_mask, threshold, mask_above=False, add_band_to_original_image=False):
658
+ """
659
+ Masks pixels of interest from a specified band of a target image, based on a specified reference band and threshold.
660
+ Designed for single image input which contains both the target and reference band.
661
+ Example use case is masking vegetation from image when targeting land pixels. Can specify whether to mask pixels above or below the threshold.
662
+
663
+ Args:
664
+ image (ee.Image): input ee.Image
665
+ band_to_mask (str): name of the band which will be masked (target image)
666
+ band_for_mask (str): name of the band to use for the mask (band you want to remove/mask from target image)
667
+ threshold (float): value where pixels less or more than threshold (depending on `mask_above` argument) will be masked
668
+ mask_above (bool): if True, masks pixels above the threshold; if False, masks pixels below the threshold
669
+
670
+ Returns:
671
+ ee.Image: masked ee.Image
672
+ """
673
+
674
+ band_to_mask_image = image.select(band_to_mask)
675
+ band_for_mask_image = image.select(band_for_mask)
676
+
677
+ mask = band_for_mask_image.lte(threshold) if mask_above else band_for_mask_image.gte(threshold)
678
+
679
+ if add_band_to_original_image:
680
+ return image.addBands(band_to_mask_image.updateMask(mask).rename(band_to_mask), overwrite=True)
681
+ else:
682
+ return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image))
683
+
684
+ @staticmethod
685
+ 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):
686
+ """
687
+ Masks pixels of interest from a specified band of a target image, based on a specified reference band and threshold.
688
+ Designed for the case where the target and reference bands are in separate images.
689
+ Example use case is masking vegetation from image when targeting land pixels. Can specify whether to mask pixels above or below the threshold.
690
+
691
+ Args:
692
+ image_to_mask (ee.Image): image which will be masked (target image). If multiband, only the first band will be masked.
693
+ image_for_mask (ee.Image): image to use for the mask (image you want to remove/mask from target image). If multiband, only the first band will be used for the masked.
694
+ threshold (float): value where pixels less or more than threshold (depending on `mask_above` argument) will be masked
695
+ band_name_to_mask (str, optional): name of the band in image_to_mask to be masked. If None, the first band will be used.
696
+ band_name_for_mask (str, optional): name of the band in image_for_mask to be used for masking. If None, the first band will be used.
697
+ mask_above (bool): if True, masks pixels above the threshold; if False, masks pixels below the threshold.
698
+
699
+ Returns:
700
+ ee.Image: masked ee.Image
701
+ """
702
+ if band_name_to_mask is None:
703
+ band_to_mask = ee.String(image_to_mask.bandNames().get(0))
704
+ else:
705
+ band_to_mask = ee.String(band_name_to_mask)
706
+
707
+ if band_name_for_mask is None:
708
+ band_for_mask = ee.String(image_for_mask.bandNames().get(0))
709
+ else:
710
+ band_for_mask = ee.String(band_name_for_mask)
711
+
712
+ band_to_mask_image = image_to_mask.select(band_to_mask)
713
+ band_for_mask_image = image_for_mask.select(band_for_mask)
714
+ if mask_above:
715
+ mask = band_for_mask_image.gt(threshold)
716
+ else:
717
+ mask = band_for_mask_image.lt(threshold)
718
+ return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask)
585
719
 
586
720
  @staticmethod
587
721
  def MaskToWaterS2ByNDWI(image, threshold):
@@ -713,12 +847,12 @@ class Sentinel2Collection:
713
847
  else:
714
848
  raise ValueError("output_type must be 'ImageCollection' or 'Sentinel2Collection'")
715
849
 
716
- def merge(self, other):
850
+ def combine(self, other):
717
851
  """
718
- Merges the current Sentinel2Collection with another Sentinel2Collection, where images/bands with the same date are combined to a single multiband image.
852
+ Combines the current Sentinel2Collection with another Sentinel2Collection, using the `combine` method.
719
853
 
720
854
  Args:
721
- other (Sentinel2Collection): Another Sentinel2Collection to merge with current collection.
855
+ other (Sentinel2Collection): Another Sentinel2Collection to combine with current collection.
722
856
 
723
857
  Returns:
724
858
  Sentinel2Collection: A new Sentinel2Collection containing images from both collections.
@@ -726,11 +860,77 @@ class Sentinel2Collection:
726
860
  # Checking if 'other' is an instance of Sentinel2Collection
727
861
  if not isinstance(other, Sentinel2Collection):
728
862
  raise ValueError("The 'other' parameter must be an instance of Sentinel2Collection.")
729
-
863
+
730
864
  # Merging the collections using the .combine() method
731
865
  merged_collection = self.collection.combine(other.collection)
732
866
  return Sentinel2Collection(collection=merged_collection)
733
867
 
868
+ def merge(self, collections=None, multiband_collection=None, date_key='Date_Filter'):
869
+ """
870
+ Merge many singleband Sentinel2Collection products into the parent collection,
871
+ or merge a single multiband collection with parent collection,
872
+ pairing images by exact Date_Filter and returning one multiband image per date.
873
+
874
+ NOTE: if you want to merge two multiband collections, use the `combine` method instead.
875
+
876
+ Args:
877
+ collections (list): List of singleband collections to merge with parent collection, effectively adds one band per collection to each image in parent
878
+ multiband_collection (Sentinel2Collection, optional): A multiband collection to merge with parent. Specifying a collection here will override `collections`.
879
+ date_key (str): image property key for exact pairing (default 'Date_Filter')
880
+
881
+ Returns:
882
+ Sentinel2Collection: parent with extra single bands attached (one image per date)
883
+ """
884
+
885
+ if collections is None and multiband_collection is not None:
886
+ # Exact-date inner-join merge of two collections (adds ALL bands from 'other').
887
+ join = ee.Join.inner()
888
+ flt = ee.Filter.equals(leftField=date_key, rightField=date_key)
889
+ paired = join.apply(self.collection, multiband_collection.collection, flt)
890
+
891
+ def _pair_two(f):
892
+ f = ee.Feature(f)
893
+ a = ee.Image(f.get('primary'))
894
+ b = ee.Image(f.get('secondary'))
895
+ # Overwrite on name collision
896
+ merged = a.addBands(b, None, True)
897
+ # Keep parent props + date key
898
+ merged = merged.copyProperties(a, a.propertyNames())
899
+ merged = merged.set(date_key, a.get(date_key))
900
+ return ee.Image(merged)
901
+
902
+ return Sentinel2Collection(collection=ee.ImageCollection(paired.map(_pair_two)))
903
+
904
+ # Preferred path: merge many singleband products into the parent
905
+ if not isinstance(collections, list) or len(collections) == 0:
906
+ raise ValueError("Provide a non-empty list of Sentinel2Collection objects in `collections`.")
907
+
908
+ result = self.collection
909
+ for extra in collections:
910
+ if not isinstance(extra, Sentinel2Collection):
911
+ raise ValueError("All items in `collections` must be Sentinel2Collection objects.")
912
+
913
+ join = ee.Join.inner()
914
+ flt = ee.Filter.equals(leftField=date_key, rightField=date_key)
915
+ paired = join.apply(result, extra.collection, flt)
916
+
917
+ def _attach_one(f):
918
+ f = ee.Feature(f)
919
+ parent = ee.Image(f.get('primary'))
920
+ sb = ee.Image(f.get('secondary'))
921
+ # Assume singleband product; grab its first band name server-side
922
+ bname = ee.String(sb.bandNames().get(0))
923
+ # Add the single band; overwrite if the name already exists in parent
924
+ merged = parent.addBands(sb.select([bname]).rename([bname]), None, True)
925
+ # Preserve parent props + date key
926
+ merged = merged.copyProperties(parent, parent.propertyNames())
927
+ merged = merged.set(date_key, parent.get(date_key))
928
+ return ee.Image(merged)
929
+
930
+ result = ee.ImageCollection(paired.map(_attach_one))
931
+
932
+ return Sentinel2Collection(collection=result)
933
+
734
934
  @property
735
935
  def dates_list(self):
736
936
  """
@@ -934,6 +1134,82 @@ class Sentinel2Collection:
934
1134
  self._median = col
935
1135
  return self._median
936
1136
 
1137
+ @property
1138
+ def monthly_median_collection(self):
1139
+ """Creates a monthly median composite from a Sentinel2Collection image collection.
1140
+
1141
+ This function computes the median for each
1142
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1143
+ temporal extent of the input collection.
1144
+
1145
+ The resulting images have a 'system:time_start' property set to the
1146
+ first day of each month and an 'image_count' property indicating how
1147
+ many images were used in the composite. Months with no images are
1148
+ automatically excluded from the final collection.
1149
+
1150
+ Returns:
1151
+ Sentinel2Collection: A new Sentinel2Collection object with monthly median composites.
1152
+ """
1153
+ if self._monthly_median is None:
1154
+ collection = self.collection
1155
+ # Get the start and end dates of the entire collection.
1156
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1157
+ start_date = ee.Date(date_range.get('min'))
1158
+ end_date = ee.Date(date_range.get('max'))
1159
+
1160
+ # Calculate the total number of months in the date range.
1161
+ # The .round() is important for ensuring we get an integer.
1162
+ num_months = end_date.difference(start_date, 'month').round()
1163
+
1164
+ # Generate a list of starting dates for each month.
1165
+ # This uses a sequence and advances the start date by 'i' months.
1166
+ def get_month_start(i):
1167
+ return start_date.advance(i, 'month')
1168
+
1169
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1170
+
1171
+ # Define a function to map over the list of month start dates.
1172
+ def create_monthly_composite(date):
1173
+ # Cast the input to an ee.Date object.
1174
+ start_of_month = ee.Date(date)
1175
+ # The end date is exclusive, so we advance by 1 month.
1176
+ end_of_month = start_of_month.advance(1, 'month')
1177
+
1178
+ # Filter the original collection to get images for the current month.
1179
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1180
+
1181
+ # Count the number of images in the monthly subset.
1182
+ image_count = monthly_subset.size()
1183
+
1184
+ # Compute the median. This is robust to outliers like clouds.
1185
+ monthly_median = monthly_subset.median()
1186
+
1187
+ # Set essential properties on the resulting composite image.
1188
+ # The timestamp is crucial for time-series analysis and charting.
1189
+ # The image_count is useful metadata for quality assessment.
1190
+ return monthly_median.set({
1191
+ 'system:time_start': start_of_month.millis(),
1192
+ 'month': start_of_month.get('month'),
1193
+ 'year': start_of_month.get('year'),
1194
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1195
+ 'image_count': image_count
1196
+ })
1197
+
1198
+ # Map the composite function over the list of month start dates.
1199
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1200
+
1201
+ # Convert the list of images into an ee.ImageCollection.
1202
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1203
+
1204
+ # Filter out any composites that were created from zero images.
1205
+ # This prevents empty/masked images from being in the final collection.
1206
+ final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1207
+ self._monthly_median = final_collection
1208
+ else:
1209
+ pass
1210
+
1211
+ return self._monthly_median
1212
+
937
1213
  @property
938
1214
  def mean(self):
939
1215
  """
@@ -973,6 +1249,84 @@ class Sentinel2Collection:
973
1249
  col = self.collection.min()
974
1250
  self._min = col
975
1251
  return self._min
1252
+
1253
+ @property
1254
+ def monthly_median_collection(self):
1255
+ """Creates a monthly median composite from a Sentinel2Collection image collection.
1256
+
1257
+ This function computes the median for each
1258
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1259
+ temporal extent of the input collection.
1260
+
1261
+ The resulting images have a 'system:time_start' property set to the
1262
+ first day of each month and an 'image_count' property indicating how
1263
+ many images were used in the composite. Months with no images are
1264
+ automatically excluded from the final collection.
1265
+
1266
+ NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
1267
+
1268
+ Returns:
1269
+ Sentinel2Collection: A new Sentinel2Collection object with monthly median composites.
1270
+ """
1271
+ if self._monthly_median is None:
1272
+ collection = self.collection
1273
+ # Get the start and end dates of the entire collection.
1274
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1275
+ start_date = ee.Date(date_range.get('min'))
1276
+ end_date = ee.Date(date_range.get('max'))
1277
+
1278
+ # Calculate the total number of months in the date range.
1279
+ # The .round() is important for ensuring we get an integer.
1280
+ num_months = end_date.difference(start_date, 'month').round()
1281
+
1282
+ # Generate a list of starting dates for each month.
1283
+ # This uses a sequence and advances the start date by 'i' months.
1284
+ def get_month_start(i):
1285
+ return start_date.advance(i, 'month')
1286
+
1287
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1288
+
1289
+ # Define a function to map over the list of month start dates.
1290
+ def create_monthly_composite(date):
1291
+ # Cast the input to an ee.Date object.
1292
+ start_of_month = ee.Date(date)
1293
+ # The end date is exclusive, so we advance by 1 month.
1294
+ end_of_month = start_of_month.advance(1, 'month')
1295
+
1296
+ # Filter the original collection to get images for the current month.
1297
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1298
+
1299
+ # Count the number of images in the monthly subset.
1300
+ image_count = monthly_subset.size()
1301
+
1302
+ # Compute the median. This is robust to outliers like clouds.
1303
+ monthly_median = monthly_subset.median()
1304
+
1305
+ # Set essential properties on the resulting composite image.
1306
+ # The timestamp is crucial for time-series analysis and charting.
1307
+ # The image_count is useful metadata for quality assessment.
1308
+ return monthly_median.set({
1309
+ 'system:time_start': start_of_month.millis(),
1310
+ 'month': start_of_month.get('month'),
1311
+ 'year': start_of_month.get('year'),
1312
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1313
+ 'image_count': image_count
1314
+ })
1315
+
1316
+ # Map the composite function over the list of month start dates.
1317
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1318
+
1319
+ # Convert the list of images into an ee.ImageCollection.
1320
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1321
+
1322
+ # Filter out any composites that were created from zero images.
1323
+ # This prevents empty/masked images from being in the final collection.
1324
+ final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1325
+ self._monthly_median = final_collection
1326
+ else:
1327
+ pass
1328
+
1329
+ return self._monthly_median
976
1330
 
977
1331
  @property
978
1332
  def ndwi(self):
@@ -1327,7 +1681,7 @@ class Sentinel2Collection:
1327
1681
  The calculation is performed only once when the property is first accessed, and the cached result is returned on subsequent accesses.
1328
1682
 
1329
1683
  Returns:
1330
- LandsatCollection: A LandsatCollection image collection
1684
+ Sentinel2Collection: A Sentinel2Collection image collection
1331
1685
  """
1332
1686
  if self._albedo is None:
1333
1687
  self._albedo = self.albedo_collection(snow_free=True)
@@ -1344,7 +1698,7 @@ class Sentinel2Collection:
1344
1698
  snow_free (bool): If True, applies a snow mask to the albedo calculation. Defaults to True.
1345
1699
 
1346
1700
  Returns:
1347
- LandsatCollection: A LandsatCollection image collection
1701
+ Sentinel2Collection: A Sentinel2Collection image collection
1348
1702
  """
1349
1703
  first_image = self.collection.first()
1350
1704
  available_bands = first_image.bandNames()
@@ -1581,6 +1935,19 @@ class Sentinel2Collection:
1581
1935
  self._masked_clouds_collection = Sentinel2Collection(collection=col)
1582
1936
  return self._masked_clouds_collection
1583
1937
 
1938
+ @property
1939
+ def masked_shadows_collection(self):
1940
+ """
1941
+ Property attribute to mask shadows and return collection as class object.
1942
+
1943
+ Returns:
1944
+ Sentinel2Collection: Sentinel2Collection image collection
1945
+ """
1946
+ if self._masked_shadows_collection is None:
1947
+ col = self.collection.map(Sentinel2Collection.MaskShadowsS2)
1948
+ self._masked_shadows_collection = Sentinel2Collection(collection=col)
1949
+ return self._masked_shadows_collection
1950
+
1584
1951
  def mask_to_polygon(self, polygon):
1585
1952
  """
1586
1953
  Function to mask Sentinel2Collection image collection by a polygon (ee.Geometry), where pixels outside the polygon are masked out.
@@ -1671,14 +2038,17 @@ class Sentinel2Collection:
1671
2038
  )
1672
2039
  return Sentinel2Collection(collection=col)
1673
2040
 
1674
- def binary_mask(self, threshold=None, band_name=None):
2041
+ def binary_mask(self, threshold=None, band_name=None, classify_above_threshold=True, mask_zeros=False):
1675
2042
  """
1676
- Creates a binary mask (value of 1 for pixels above set threshold and value of 0 for all other pixels) of the Sentinel2Collection image collection based on a specified band.
2043
+ Function to create a binary mask (value of 1 for pixels above set threshold and value of 0 for all other pixels) of the Sentinel2Collection image collection based on a specified band.
1677
2044
  If a singleband image is provided, the band name is automatically determined.
1678
2045
  If multiple bands are available, the user must specify the band name to use for masking.
1679
2046
 
1680
2047
  Args:
2048
+ threshold (float, optional): The threshold value for creating the binary mask. Defaults to None.
1681
2049
  band_name (str, optional): The name of the band to use for masking. Defaults to None.
2050
+ classifiy_above_threshold (bool, optional): If True, pixels above the threshold are classified as 1. If False, pixels below the threshold are classified as 1. Defaults to True.
2051
+ mask_zeros (bool, optional): If True, pixels with a value of 0 after the binary mask are masked out in the output binary mask. Useful for classifications. Defaults to False.
1682
2052
 
1683
2053
  Returns:
1684
2054
  Sentinel2Collection: Sentinel2Collection singleband image collection with binary masks applied.
@@ -1697,11 +2067,150 @@ class Sentinel2Collection:
1697
2067
  if threshold is None:
1698
2068
  raise ValueError("Threshold must be specified for binary masking.")
1699
2069
 
2070
+ if classify_above_threshold:
2071
+ if mask_zeros:
2072
+ col = self.collection.map(
2073
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
2074
+ )
2075
+ else:
2076
+ col = self.collection.map(
2077
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
2078
+ )
2079
+ else:
2080
+ if mask_zeros:
2081
+ col = self.collection.map(
2082
+ lambda image: image.select(band_name).lte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
2083
+ )
2084
+ else:
2085
+ col = self.collection.map(
2086
+ lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
2087
+ )
2088
+ return Sentinel2Collection(collection=col)
2089
+
2090
+ def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
2091
+ """
2092
+ Calculates the anomaly of each image in a collection compared to the mean of each image.
2093
+
2094
+ This function computes the anomaly for each band in the input image by
2095
+ subtracting the mean value of that band from a provided ImageCollection.
2096
+ The anomaly is a measure of how much the pixel values deviate from the
2097
+ average conditions represented by the collection.
2098
+
2099
+ Args:
2100
+ geometry (ee.Geometry): The geometry for image reduction to define the mean value to be used for anomaly calculation.
2101
+ band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
2102
+ anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
2103
+ replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band. If True, the output image will only contain the anomaly band. If False, the output image will retain all original bands and add the anomaly band. Default is True.
2104
+
2105
+ Returns:
2106
+ Sentinel2Collection: A Sentinel2Collection where each image represents the anomaly (deviation from
2107
+ the mean) for the chosen band. The output images retain the same band name.
2108
+ """
2109
+ if self.collection.size().eq(0).getInfo():
2110
+ raise ValueError("The collection is empty.")
2111
+ if band_name is None:
2112
+ first_image = self.collection.first()
2113
+ band_names = first_image.bandNames()
2114
+ if band_names.size().getInfo() == 0:
2115
+ raise ValueError("No bands available in the collection.")
2116
+ elif band_names.size().getInfo() > 1:
2117
+ band_name = band_names.get(0).getInfo()
2118
+ print("Multiple bands available, will be using the first band in the collection for anomaly calculation. Please specify a band name if you wish to use a different band.")
2119
+ else:
2120
+ band_name = band_names.get(0).getInfo()
2121
+
2122
+ col = self.collection.map(lambda image: Sentinel2Collection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace))
2123
+ return Sentinel2Collection(collection=col)
2124
+
2125
+ def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
2126
+ """
2127
+ Masks select pixels of a selected band from an image based on another specified band and threshold (optional).
2128
+ Example use case is masking vegetation from image when targeting land pixels. Can specify whether to mask pixels above or below the threshold.
2129
+
2130
+ Args:
2131
+ band_to_mask (str): name of the band which will be masked (target image)
2132
+ band_for_mask (str): name of the band to use for the mask (band you want to remove/mask from target image)
2133
+ threshold (float): value between -1 and 1 where pixels less than threshold will be masked; defaults to -1 assuming input band is already classified (masked to pixels of interest).
2134
+ mask_above (bool): if True, masks pixels above the threshold; if False, masks pixels below the threshold
2135
+
2136
+ Returns:
2137
+ Sentinel2Collection: A new Sentinel2Collection with the specified band masked to pixels excluding from `band_for_mask`.
2138
+ """
2139
+ if self.collection.size().eq(0).getInfo():
2140
+ raise ValueError("The collection is empty.")
2141
+
1700
2142
  col = self.collection.map(
1701
- lambda image: image.select(band_name).gte(threshold).rename(band_name)
2143
+ lambda image: Sentinel2Collection.mask_via_band_fn(
2144
+ image,
2145
+ band_to_mask=band_to_mask,
2146
+ band_for_mask=band_for_mask,
2147
+ threshold=threshold,
2148
+ mask_above=mask_above,
2149
+ add_band_to_original_image=add_band_to_original_image
2150
+ )
1702
2151
  )
1703
2152
  return Sentinel2Collection(collection=col)
1704
2153
 
2154
+ def mask_via_singleband_image(self, image_collection_for_mask, band_name_to_mask, band_name_for_mask, threshold=-1, mask_above=False, add_band_to_original_image=False):
2155
+ """
2156
+ Masks select pixels of a selected band from an image collection based on another specified singleband image collection and threshold (optional).
2157
+ Example use case is masking vegetation from image when targeting land pixels. Can specify whether to mask pixels above or below the threshold.
2158
+ This function pairs images from the two collections based on an exact match of the 'Date_Filter' property.
2159
+
2160
+ Args:
2161
+ image_collection_for_mask (Sentinel2Collection): Sentinel2Collection image collection to use for masking (source of pixels that will be used to mask the parent image collection)
2162
+ band_name_to_mask (str): name of the band which will be masked (target image)
2163
+ band_name_for_mask (str): name of the band to use for the mask (band which contains pixels the user wants to remove/mask from target image)
2164
+ threshold (float): threshold value where pixels less (or more, depending on `mask_above`) than threshold will be masked; defaults to -1.
2165
+ mask_above (bool): if True, masks pixels above the threshold; if False, masks pixels below the threshold
2166
+ add_band_to_original_image (bool): if True, adds the band used for masking to the original image as an additional band; if False, only the masked band is retained in the output image.
2167
+
2168
+ Returns:
2169
+ Sentinel2Collection: A new Sentinel2Collection with the specified band masked to pixels excluding from `band_for_mask`.
2170
+ """
2171
+
2172
+ if self.collection.size().eq(0).getInfo():
2173
+ raise ValueError("The collection is empty.")
2174
+ if not isinstance(image_collection_for_mask, Sentinel2Collection):
2175
+ raise ValueError("image_collection_for_mask must be a Sentinel2Collection object.")
2176
+ size1 = self.collection.size().getInfo()
2177
+ size2 = image_collection_for_mask.collection.size().getInfo()
2178
+ if size1 != size2:
2179
+ raise ValueError(f"Warning: Collections have different sizes ({size1} vs {size2}). Please ensure both collections have the same number of images and matching dates.")
2180
+ if size1 == 0 or size2 == 0:
2181
+ raise ValueError("Warning: One of the input collections is empty.")
2182
+
2183
+ # Pair by exact Date_Filter property
2184
+ primary = self.collection.select([band_name_to_mask])
2185
+ secondary = image_collection_for_mask.collection.select([band_name_for_mask])
2186
+ join = ee.Join.inner()
2187
+ flt = ee.Filter.equals(leftField='Date_Filter', rightField='Date_Filter')
2188
+ paired = join.apply(primary, secondary, flt)
2189
+
2190
+ def _map_pair(f):
2191
+ f = ee.Feature(f) # <-- treat as Feature
2192
+ prim = ee.Image(f.get('primary')) # <-- get the primary Image
2193
+ sec = ee.Image(f.get('secondary')) # <-- get the secondary Image
2194
+
2195
+ merged = prim.addBands(sec.select([band_name_for_mask]))
2196
+
2197
+ out = Sentinel2Collection.mask_via_band_fn(
2198
+ merged,
2199
+ band_to_mask=band_name_to_mask,
2200
+ band_for_mask=band_name_for_mask,
2201
+ threshold=threshold,
2202
+ mask_above=mask_above,
2203
+ add_band_to_original_image=add_band_to_original_image
2204
+ )
2205
+
2206
+ # guarantee single band + keep properties
2207
+ out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())
2208
+ out = out.set('Date_Filter', prim.get('Date_Filter'))
2209
+ return ee.Image(out) # <-- return as Image
2210
+
2211
+ col = ee.ImageCollection(paired.map(_map_pair))
2212
+ return Sentinel2Collection(collection=col)
2213
+
1705
2214
  def image_grab(self, img_selector):
1706
2215
  """
1707
2216
  Function to select ("grab") an image by index from the collection. Easy way to get latest image or browse imagery one-by-one.
@@ -2536,3 +3045,59 @@ class Sentinel2Collection:
2536
3045
  print(f"Zonal stats saved to {file_path}.csv")
2537
3046
  return
2538
3047
  return pivot_df
3048
+
3049
+ def export_to_asset_collection(
3050
+ self,
3051
+ asset_collection_path,
3052
+ region,
3053
+ scale,
3054
+ dates=None,
3055
+ filename_prefix="",
3056
+ crs=None,
3057
+ max_pixels=int(1e13),
3058
+ description_prefix="export"
3059
+ ):
3060
+ """
3061
+ Exports an image collection to a Google Earth Engine asset collection. The asset collection will be created if it does not already exist,
3062
+ and each image exported will be named according to the provided filename prefix and date.
3063
+
3064
+ Args:
3065
+ asset_collection_path (str): The path to the asset collection.
3066
+ region (ee.Geometry): The region to export.
3067
+ scale (int): The scale of the export.
3068
+ dates (list, optional): The dates to export. Defaults to None.
3069
+ filename_prefix (str, optional): The filename prefix. Defaults to "", i.e. blank.
3070
+ crs (str, optional): The coordinate reference system. Defaults to None, which will use the image's CRS.
3071
+ max_pixels (int, optional): The maximum number of pixels. Defaults to int(1e13).
3072
+ description_prefix (str, optional): The description prefix. Defaults to "export".
3073
+
3074
+ Returns:
3075
+ None: (queues export tasks)
3076
+ """
3077
+ ic = self.collection
3078
+ if dates is None:
3079
+ dates = self.dates
3080
+ try:
3081
+ ee.data.createAsset({'type': 'ImageCollection'}, asset_collection_path)
3082
+ except Exception:
3083
+ pass
3084
+
3085
+ for date_str in dates:
3086
+ img = ee.Image(ic.filter(ee.Filter.eq('Date_Filter', date_str)).first())
3087
+ asset_id = asset_collection_path + "/" + filename_prefix + date_str
3088
+ desc = description_prefix + "_" + filename_prefix + date_str
3089
+
3090
+ params = {
3091
+ 'image': img,
3092
+ 'description': desc,
3093
+ 'assetId': asset_id,
3094
+ 'region': region,
3095
+ 'scale': scale,
3096
+ 'maxPixels': max_pixels
3097
+ }
3098
+ if crs:
3099
+ params['crs'] = crs
3100
+
3101
+ ee.batch.Export.image.toAsset(**params).start()
3102
+
3103
+ print("Queued", len(dates), "export tasks to", asset_collection_path)