RadGEEToolbox 1.7.0__py3-none-any.whl → 1.7.2__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.
@@ -213,6 +213,11 @@ class Sentinel1Collection:
213
213
  self._mean = None
214
214
  self._max = None
215
215
  self._min = None
216
+ self._monthly_median = None
217
+ self._monthly_mean = None
218
+ self._monthly_max = None
219
+ self._monthly_min = None
220
+ self._monthly_sum = None
216
221
  self._MosaicByDate = None
217
222
  self._PixelAreaSumCollection = None
218
223
  self._speckle_filter = None
@@ -307,11 +312,11 @@ class Sentinel1Collection:
307
312
 
308
313
  Args:
309
314
  band_name (string or list of strings): name of band(s) (string) for calculating area. If providing multiple band names, pass as a list of strings.
310
- geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
315
+ geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation.
311
316
  threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1). If providing multiple band names, the same threshold will be applied to all bands. Best practice in this case is to mask the bands prior to passing to this function and leave threshold at default of -1.
312
- scale (int): integer scale of image resolution (meters) (defaults to 10)
313
- maxPixels (int): integer denoting maximum number of pixels for calculations
314
- output_type (str): 'ImageCollection' to return an ee.ImageCollection, 'Sentinel1Collection' to return a Sentinel1Collection object (defaults to 'ImageCollection')
317
+ scale (int): integer scale of image resolution (meters) (defaults to 30).
318
+ maxPixels (int): integer denoting maximum number of pixels for calculations.
319
+ output_type (str): 'ImageCollection' or 'ee.ImageCollection' to return an ee.ImageCollection, 'Sentinel1Collection' to return a Sentinel1Collection object, or 'DataFrame', 'Pandas', 'pd', 'dataframe', 'df' to return a pandas DataFrame (defaults to 'ImageCollection').
315
320
  area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
316
321
 
317
322
  Returns:
@@ -334,17 +339,45 @@ class Sentinel1Collection:
334
339
  # Storing the result in the instance variable to avoid redundant calculations
335
340
  self._PixelAreaSumCollection = AreaCollection
336
341
 
342
+ prop_names = band_name if isinstance(band_name, list) else [band_name]
343
+
337
344
  # If an export path is provided, the area data will be exported to a CSV file
338
345
  if area_data_export_path:
339
- Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=band_name, file_path=area_data_export_path+'.csv')
340
-
346
+ Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
341
347
  # Returning the result in the desired format based on output_type argument or raising an error for invalid input
342
- if output_type == 'ImageCollection':
348
+ if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
343
349
  return self._PixelAreaSumCollection
344
350
  elif output_type == 'Sentinel1Collection':
345
351
  return Sentinel1Collection(collection=self._PixelAreaSumCollection)
352
+ elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
353
+ return Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
346
354
  else:
347
- raise ValueError("output_type must be 'ImageCollection' or 'Sentinel1Collection'")
355
+ raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'Sentinel1Collection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
356
+
357
+ @staticmethod
358
+ def add_month_property_fn(image):
359
+ """
360
+ Adds a numeric 'month' property to the image based on its date.
361
+
362
+ Args:
363
+ image (ee.Image): Input image.
364
+
365
+ Returns:
366
+ ee.Image: Image with the 'month' property added.
367
+ """
368
+ return image.set('month', image.date().get('month'))
369
+
370
+ @property
371
+ def add_month_property(self):
372
+ """
373
+ Adds a numeric 'month' property to each image in the collection.
374
+
375
+ Returns:
376
+ Sentinel1Collection: A Sentinel1Collection image collection with the 'month' property added to each image.
377
+ """
378
+ col = self.collection.map(Sentinel1Collection.add_month_property_fn)
379
+ return Sentinel1Collection(collection=col)
380
+
348
381
 
349
382
  def combine(self, other):
350
383
  """
@@ -719,6 +752,64 @@ class Sentinel1Collection:
719
752
  dB_collection = collection.map(conversion)
720
753
  self._DbFromSigma0 = dB_collection
721
754
  return Sentinel1Collection(collection=self._DbFromSigma0)
755
+
756
+ @staticmethod
757
+ def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=10):
758
+ """
759
+ Calculates the anomaly of a singleband image compared to the mean of the singleband image.
760
+
761
+ This function computes the anomaly for each band in the input image by
762
+ subtracting the mean value of that band from a provided image.
763
+ The anomaly is a measure of how much the pixel values deviate from the
764
+ average conditions represented by the mean of the image.
765
+
766
+ Args:
767
+ image (ee.Image): An ee.Image for which the anomaly is to be calculated.
768
+ It is assumed that this image is a singleband image.
769
+ geometry (ee.Geometry): The geometry for image reduction to define the mean value to be used for anomaly calculation.
770
+ 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.
771
+ 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.
772
+ 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.
773
+ scale (int, optional): The scale (in meters) to use for the image reduction. Default is 10.
774
+
775
+ Returns:
776
+ ee.Image: An ee.Image where each band represents the anomaly (deviation from
777
+ the mean) for that band. The output image retains the same band name.
778
+ """
779
+ if band_name:
780
+ band_name = band_name
781
+ else:
782
+ band_name = ee.String(image.bandNames().get(0))
783
+
784
+ image_to_process = image.select([band_name])
785
+
786
+ # Calculate the mean image of the provided collection.
787
+ mean_image = image_to_process.reduceRegion(
788
+ reducer=ee.Reducer.mean(),
789
+ geometry=geometry,
790
+ scale=scale,
791
+ maxPixels=1e13
792
+ ).toImage()
793
+
794
+ # Compute the anomaly by subtracting the mean image from the input image.
795
+ if scale == 10:
796
+ anomaly_image = image_to_process.subtract(mean_image)
797
+ else:
798
+ anomaly_image = image_to_process.reproject(crs=image_to_process.projection(), scale=scale).subtract(mean_image)
799
+
800
+ if anomaly_band_name is None:
801
+ if band_name:
802
+ anomaly_image = anomaly_image.rename(band_name)
803
+ else:
804
+ # Preserve original properties from the input image.
805
+ anomaly_image = anomaly_image.rename(ee.String(image.bandNames().get(0)))
806
+ else:
807
+ anomaly_image = anomaly_image.rename(anomaly_band_name)
808
+ # return anomaly_image
809
+ if replace:
810
+ return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
811
+ else:
812
+ return image.addBands(anomaly_image, overwrite=True)
722
813
 
723
814
  @property
724
815
  def dates_list(self):
@@ -763,6 +854,8 @@ class Sentinel1Collection:
763
854
  # Ensure property_names is a list for consistent processing
764
855
  if isinstance(property_names, str):
765
856
  property_names = [property_names]
857
+ elif isinstance(property_names, list):
858
+ property_names = property_names
766
859
 
767
860
  # Ensure properties are included without duplication, including 'Date_Filter'
768
861
  all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
@@ -887,6 +980,27 @@ class Sentinel1Collection:
887
980
  .select(self.bands)
888
981
  )
889
982
  return filtered_collection
983
+
984
+ def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
985
+ """
986
+ Removes duplicate images that share the same date, keeping only the first one encountered.
987
+ Useful for handling duplicate Sentinel-1A/1B acquisitions or overlapping tiles.
988
+
989
+ Args:
990
+ sort_by (str): Property to sort by before filtering distinct dates.
991
+ Defaults to 'system:time_start'. Take care to provide a property that exists in all images if using a custom property.
992
+ ascending (bool): Sort order. Defaults to True.
993
+
994
+ Returns:
995
+ Sentinel1Collection: A new Sentinel1Collection object with distinct dates.
996
+ """
997
+ # Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
998
+ sorted_col = self.collection.sort(sort_by, ascending)
999
+
1000
+ # distinct() retains the first image for each unique value of the specified property
1001
+ distinct_col = sorted_col.distinct('Date_Filter')
1002
+
1003
+ return Sentinel1Collection(collection=distinct_col)
890
1004
 
891
1005
  @property
892
1006
  def median(self):
@@ -940,6 +1054,450 @@ class Sentinel1Collection:
940
1054
  col = self.collection.min()
941
1055
  self._min = col
942
1056
  return self._min
1057
+
1058
+ @property
1059
+ def monthly_median_collection(self):
1060
+ """Creates a monthly median composite from a Sentinel1Collection image collection.
1061
+
1062
+ This function computes the median for each
1063
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1064
+ temporal extent of the input collection.
1065
+
1066
+ The resulting images have a 'system:time_start' property set to the
1067
+ first day of each month and an 'image_count' property indicating how
1068
+ many images were used in the composite. Months with no images are
1069
+ automatically excluded from the final collection.
1070
+
1071
+ Returns:
1072
+ Sentinel1Collection: A new Sentinel1Collection object with monthly median composites.
1073
+ """
1074
+ if self._monthly_median is None:
1075
+ collection = self.collection
1076
+ # Get the start and end dates of the entire collection.
1077
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1078
+ start_date = ee.Date(date_range.get('min'))
1079
+ end_date = ee.Date(date_range.get('max'))
1080
+
1081
+ # Calculate the total number of months in the date range.
1082
+ # The .round() is important for ensuring we get an integer.
1083
+ num_months = end_date.difference(start_date, 'month').round()
1084
+
1085
+ # Generate a list of starting dates for each month.
1086
+ # This uses a sequence and advances the start date by 'i' months.
1087
+ def get_month_start(i):
1088
+ return start_date.advance(i, 'month')
1089
+
1090
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1091
+
1092
+ # Define a function to map over the list of month start dates.
1093
+ def create_monthly_composite(date):
1094
+ # Cast the input to an ee.Date object.
1095
+ start_of_month = ee.Date(date)
1096
+ # The end date is exclusive, so we advance by 1 month.
1097
+ end_of_month = start_of_month.advance(1, 'month')
1098
+
1099
+ # Filter the original collection to get images for the current month.
1100
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1101
+
1102
+ # Count the number of images in the monthly subset.
1103
+ image_count = monthly_subset.size()
1104
+
1105
+ # Compute the median. This is robust to outliers like clouds.
1106
+ monthly_median = monthly_subset.median()
1107
+
1108
+ # Set essential properties on the resulting composite image.
1109
+ # The timestamp is crucial for time-series analysis and charting.
1110
+ # The image_count is useful metadata for quality assessment.
1111
+ return monthly_median.set({
1112
+ 'system:time_start': start_of_month.millis(),
1113
+ 'month': start_of_month.get('month'),
1114
+ 'year': start_of_month.get('year'),
1115
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1116
+ 'image_count': image_count
1117
+ })
1118
+
1119
+ # Map the composite function over the list of month start dates.
1120
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1121
+
1122
+ # Convert the list of images into an ee.ImageCollection.
1123
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1124
+
1125
+ # Filter out any composites that were created from zero images.
1126
+ # This prevents empty/masked images from being in the final collection.
1127
+ final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1128
+ self._monthly_median = final_collection
1129
+ else:
1130
+ pass
1131
+
1132
+ return self._monthly_median
1133
+
1134
+ @property
1135
+ def monthly_mean_collection(self):
1136
+ """Creates a monthly mean composite from a Sentinel1Collection image collection.
1137
+
1138
+ This function computes the mean for each
1139
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1140
+ temporal extent of the input collection.
1141
+
1142
+ The resulting images have a 'system:time_start' property set to the
1143
+ first day of each month and an 'image_count' property indicating how
1144
+ many images were used in the composite. Months with no images are
1145
+ automatically excluded from the final collection.
1146
+
1147
+ 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.
1148
+
1149
+ Returns:
1150
+ Sentinel1Collection: A new Sentinel1Collection object with monthly mean composites.
1151
+ """
1152
+ if self._monthly_mean is None:
1153
+ collection = self.collection
1154
+ target_proj = collection.first().projection()
1155
+ # Get the start and end dates of the entire collection.
1156
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1157
+ original_start_date = ee.Date(date_range.get('min'))
1158
+ end_date = ee.Date(date_range.get('max'))
1159
+
1160
+ start_year = original_start_date.get('year')
1161
+ start_month = original_start_date.get('month')
1162
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1163
+
1164
+ # Calculate the total number of months in the date range.
1165
+ # The .round() is important for ensuring we get an integer.
1166
+ num_months = end_date.difference(start_date, 'month').round()
1167
+
1168
+ # Generate a list of starting dates for each month.
1169
+ # This uses a sequence and advances the start date by 'i' months.
1170
+ def get_month_start(i):
1171
+ return start_date.advance(i, 'month')
1172
+
1173
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1174
+
1175
+ # Define a function to map over the list of month start dates.
1176
+ def create_monthly_composite(date):
1177
+ # Cast the input to an ee.Date object.
1178
+ start_of_month = ee.Date(date)
1179
+ # The end date is exclusive, so we advance by 1 month.
1180
+ end_of_month = start_of_month.advance(1, 'month')
1181
+
1182
+ # Filter the original collection to get images for the current month.
1183
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1184
+
1185
+ # Count the number of images in the monthly subset.
1186
+ image_count = monthly_subset.size()
1187
+
1188
+ # Compute the mean. This is robust to outliers like clouds.
1189
+ monthly_mean = monthly_subset.mean()
1190
+
1191
+ # Set essential properties on the resulting composite image.
1192
+ # The timestamp is crucial for time-series analysis and charting.
1193
+ # The image_count is useful metadata for quality assessment.
1194
+ return monthly_mean.set({
1195
+ 'system:time_start': start_of_month.millis(),
1196
+ 'month': start_of_month.get('month'),
1197
+ 'year': start_of_month.get('year'),
1198
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1199
+ 'image_count': image_count
1200
+ }).reproject(target_proj)
1201
+
1202
+ # Map the composite function over the list of month start dates.
1203
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1204
+
1205
+ # Convert the list of images into an ee.ImageCollection.
1206
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1207
+
1208
+ # Filter out any composites that were created from zero images.
1209
+ # This prevents empty/masked images from being in the final collection.
1210
+ final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1211
+ self._monthly_mean = final_collection
1212
+ else:
1213
+ pass
1214
+
1215
+ return self._monthly_mean
1216
+
1217
+ @property
1218
+ def monthly_sum_collection(self):
1219
+ """Creates a monthly sum composite from a Sentinel1Collection image collection.
1220
+
1221
+ This function computes the sum for each
1222
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1223
+ temporal extent of the input collection.
1224
+
1225
+ The resulting images have a 'system:time_start' property set to the
1226
+ first day of each month and an 'image_count' property indicating how
1227
+ many images were used in the composite. Months with no images are
1228
+ automatically excluded from the final collection.
1229
+
1230
+ 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.
1231
+
1232
+ Returns:
1233
+ Sentinel1Collection: A new Sentinel1Collection object with monthly sum composites.
1234
+ """
1235
+ if self._monthly_sum is None:
1236
+ collection = self.collection
1237
+ target_proj = collection.first().projection()
1238
+ # Get the start and end dates of the entire collection.
1239
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1240
+ original_start_date = ee.Date(date_range.get('min'))
1241
+ end_date = ee.Date(date_range.get('max'))
1242
+
1243
+ start_year = original_start_date.get('year')
1244
+ start_month = original_start_date.get('month')
1245
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1246
+
1247
+ # Calculate the total number of months in the date range.
1248
+ # The .round() is important for ensuring we get an integer.
1249
+ num_months = end_date.difference(start_date, 'month').round()
1250
+
1251
+ # Generate a list of starting dates for each month.
1252
+ # This uses a sequence and advances the start date by 'i' months.
1253
+ def get_month_start(i):
1254
+ return start_date.advance(i, 'month')
1255
+
1256
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1257
+
1258
+ # Define a function to map over the list of month start dates.
1259
+ def create_monthly_composite(date):
1260
+ # Cast the input to an ee.Date object.
1261
+ start_of_month = ee.Date(date)
1262
+ # The end date is exclusive, so we advance by 1 month.
1263
+ end_of_month = start_of_month.advance(1, 'month')
1264
+
1265
+ # Filter the original collection to get images for the current month.
1266
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1267
+
1268
+ # Count the number of images in the monthly subset.
1269
+ image_count = monthly_subset.size()
1270
+
1271
+ # Compute the sum. This is robust to outliers like clouds.
1272
+ monthly_sum = monthly_subset.sum()
1273
+
1274
+ # Set essential properties on the resulting composite image.
1275
+ # The timestamp is crucial for time-series analysis and charting.
1276
+ # The image_count is useful metadata for quality assessment.
1277
+ return monthly_sum.set({
1278
+ 'system:time_start': start_of_month.millis(),
1279
+ 'month': start_of_month.get('month'),
1280
+ 'year': start_of_month.get('year'),
1281
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1282
+ 'image_count': image_count
1283
+ }).reproject(target_proj)
1284
+
1285
+ # Map the composite function over the list of month start dates.
1286
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1287
+
1288
+ # Convert the list of images into an ee.ImageCollection.
1289
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1290
+
1291
+ # Filter out any composites that were created from zero images.
1292
+ # This prevents empty/masked images from being in the final collection.
1293
+ final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1294
+ self._monthly_sum = final_collection
1295
+ else:
1296
+ pass
1297
+
1298
+ return self._monthly_sum
1299
+
1300
+ @property
1301
+ def monthly_max_collection(self):
1302
+ """Creates a monthly max composite from a Sentinel1Collection image collection.
1303
+
1304
+ This function computes the max for each
1305
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1306
+ temporal extent of the input collection.
1307
+
1308
+ The resulting images have a 'system:time_start' property set to the
1309
+ first day of each month and an 'image_count' property indicating how
1310
+ many images were used in the composite. Months with no images are
1311
+ automatically excluded from the final collection.
1312
+
1313
+ 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.
1314
+
1315
+ Returns:
1316
+ Sentinel1Collection: A new Sentinel1Collection object with monthly max composites.
1317
+ """
1318
+ if self._monthly_max is None:
1319
+ collection = self.collection
1320
+ target_proj = collection.first().projection()
1321
+ # Get the start and end dates of the entire collection.
1322
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1323
+ original_start_date = ee.Date(date_range.get('min'))
1324
+ end_date = ee.Date(date_range.get('max'))
1325
+
1326
+ start_year = original_start_date.get('year')
1327
+ start_month = original_start_date.get('month')
1328
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1329
+
1330
+ # Calculate the total number of months in the date range.
1331
+ # The .round() is important for ensuring we get an integer.
1332
+ num_months = end_date.difference(start_date, 'month').round()
1333
+
1334
+ # Generate a list of starting dates for each month.
1335
+ # This uses a sequence and advances the start date by 'i' months.
1336
+ def get_month_start(i):
1337
+ return start_date.advance(i, 'month')
1338
+
1339
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1340
+
1341
+ # Define a function to map over the list of month start dates.
1342
+ def create_monthly_composite(date):
1343
+ # Cast the input to an ee.Date object.
1344
+ start_of_month = ee.Date(date)
1345
+ # The end date is exclusive, so we advance by 1 month.
1346
+ end_of_month = start_of_month.advance(1, 'month')
1347
+
1348
+ # Filter the original collection to get images for the current month.
1349
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1350
+
1351
+ # Count the number of images in the monthly subset.
1352
+ image_count = monthly_subset.size()
1353
+
1354
+ # Compute the max. This is robust to outliers like clouds.
1355
+ monthly_max = monthly_subset.max()
1356
+
1357
+ # Set essential properties on the resulting composite image.
1358
+ # The timestamp is crucial for time-series analysis and charting.
1359
+ # The image_count is useful metadata for quality assessment.
1360
+ return monthly_max.set({
1361
+ 'system:time_start': start_of_month.millis(),
1362
+ 'month': start_of_month.get('month'),
1363
+ 'year': start_of_month.get('year'),
1364
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1365
+ 'image_count': image_count
1366
+ }).reproject(target_proj)
1367
+
1368
+ # Map the composite function over the list of month start dates.
1369
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1370
+
1371
+ # Convert the list of images into an ee.ImageCollection.
1372
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1373
+
1374
+ # Filter out any composites that were created from zero images.
1375
+ # This prevents empty/masked images from being in the final collection.
1376
+ final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1377
+ self._monthly_max = final_collection
1378
+ else:
1379
+ pass
1380
+
1381
+ return self._monthly_max
1382
+
1383
+ @property
1384
+ def monthly_min_collection(self):
1385
+ """Creates a monthly min composite from a Sentinel1Collection image collection.
1386
+
1387
+ This function computes the min for each
1388
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1389
+ temporal extent of the input collection.
1390
+
1391
+ The resulting images have a 'system:time_start' property set to the
1392
+ first day of each month and an 'image_count' property indicating how
1393
+ many images were used in the composite. Months with no images are
1394
+ automatically excluded from the final collection.
1395
+
1396
+ 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.
1397
+
1398
+ Returns:
1399
+ Sentinel1Collection: A new Sentinel1Collection object with monthly min composites.
1400
+ """
1401
+ if self._monthly_min is None:
1402
+ collection = self.collection
1403
+ target_proj = collection.first().projection()
1404
+ # Get the start and end dates of the entire collection.
1405
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1406
+ original_start_date = ee.Date(date_range.get('min'))
1407
+ end_date = ee.Date(date_range.get('max'))
1408
+
1409
+ start_year = original_start_date.get('year')
1410
+ start_month = original_start_date.get('month')
1411
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1412
+
1413
+ # Calculate the total number of months in the date range.
1414
+ # The .round() is important for ensuring we get an integer.
1415
+ num_months = end_date.difference(start_date, 'month').round()
1416
+
1417
+ # Generate a list of starting dates for each month.
1418
+ # This uses a sequence and advances the start date by 'i' months.
1419
+ def get_month_start(i):
1420
+ return start_date.advance(i, 'month')
1421
+
1422
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1423
+
1424
+ # Define a function to map over the list of month start dates.
1425
+ def create_monthly_composite(date):
1426
+ # Cast the input to an ee.Date object.
1427
+ start_of_month = ee.Date(date)
1428
+ # The end date is exclusive, so we advance by 1 month.
1429
+ end_of_month = start_of_month.advance(1, 'month')
1430
+
1431
+ # Filter the original collection to get images for the current month.
1432
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1433
+
1434
+ # Count the number of images in the monthly subset.
1435
+ image_count = monthly_subset.size()
1436
+
1437
+ # Compute the min. This is robust to outliers like clouds.
1438
+ monthly_min = monthly_subset.min()
1439
+
1440
+ # Set essential properties on the resulting composite image.
1441
+ # The timestamp is crucial for time-series analysis and charting.
1442
+ # The image_count is useful metadata for quality assessment.
1443
+ return monthly_min.set({
1444
+ 'system:time_start': start_of_month.millis(),
1445
+ 'month': start_of_month.get('month'),
1446
+ 'year': start_of_month.get('year'),
1447
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1448
+ 'image_count': image_count
1449
+ }).reproject(target_proj)
1450
+
1451
+ # Map the composite function over the list of month start dates.
1452
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1453
+
1454
+ # Convert the list of images into an ee.ImageCollection.
1455
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1456
+
1457
+ # Filter out any composites that were created from zero images.
1458
+ # This prevents empty/masked images from being in the final collection.
1459
+ final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1460
+ self._monthly_min = final_collection
1461
+ else:
1462
+ pass
1463
+
1464
+ return self._monthly_min
1465
+
1466
+ def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=10):
1467
+ """
1468
+ Calculates the anomaly of each image in a collection compared to the mean of each image.
1469
+
1470
+ This function computes the anomaly for each band in the input image by
1471
+ subtracting the mean value of that band from a provided ImageCollection.
1472
+ The anomaly is a measure of how much the pixel values deviate from the
1473
+ average conditions represented by the collection.
1474
+
1475
+ Args:
1476
+ geometry (ee.Geometry): The geometry for image reduction to define the mean value to be used for anomaly calculation.
1477
+ 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.
1478
+ 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.
1479
+ 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.
1480
+ scale (int, optional): The scale (in meters) to use for the image reduction. Default is 10 meters.
1481
+
1482
+ Returns:
1483
+ Sentinel1Collection: A Sentinel1Collection where each image represents the anomaly (deviation from
1484
+ the mean) for the chosen band. The output images retain the same band name.
1485
+ """
1486
+ if self.collection.size().eq(0).getInfo():
1487
+ raise ValueError("The collection is empty.")
1488
+ if band_name is None:
1489
+ first_image = self.collection.first()
1490
+ band_names = first_image.bandNames()
1491
+ if band_names.size().getInfo() == 0:
1492
+ raise ValueError("No bands available in the collection.")
1493
+ elif band_names.size().getInfo() > 1:
1494
+ band_name = band_names.get(0).getInfo()
1495
+ 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.")
1496
+ else:
1497
+ band_name = band_names.get(0).getInfo()
1498
+
1499
+ col = self.collection.map(lambda image: Sentinel1Collection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
1500
+ return Sentinel1Collection(collection=col)
943
1501
 
944
1502
  def binary_mask(self, threshold=None, band_name=None):
945
1503
  """
@@ -1734,24 +2292,30 @@ class Sentinel1Collection:
1734
2292
  ValueError: If input parameters are invalid.
1735
2293
  TypeError: If geometries input type is unsupported.
1736
2294
  """
2295
+ # Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
1737
2296
  img_collection_obj = self
2297
+
2298
+ # If a specific band is requested, select only that band
1738
2299
  if band:
1739
2300
  img_collection_obj = Sentinel1Collection(collection=img_collection_obj.collection.select(band))
1740
2301
  else:
2302
+ # If no band is specified, default to using the first band of the first image in the collection
1741
2303
  first_image = img_collection_obj.image_grab(0)
1742
2304
  first_band = first_image.bandNames().get(0)
1743
2305
  img_collection_obj = Sentinel1Collection(collection=img_collection_obj.collection.select([first_band]))
1744
- # Filter collection by dates if provided
2306
+
2307
+ # If a list of dates is provided, filter the collection to include only images matching those dates
1745
2308
  if dates:
1746
2309
  img_collection_obj = Sentinel1Collection(
1747
2310
  collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
1748
2311
  )
1749
2312
 
1750
- # Initialize variables
2313
+ # Initialize variables to hold the standardized feature collection and coordinates
1751
2314
  features = None
1752
2315
  validated_coordinates = []
1753
2316
 
1754
- # Function to standardize feature names if no names are provided
2317
+ # Define a helper function to ensure every feature has a standardized 'geo_name' property
2318
+ # This handles features that might have different existing name properties or none at all
1755
2319
  def set_standard_name(feature):
1756
2320
  has_geo_name = feature.get('geo_name')
1757
2321
  has_name = feature.get('name')
@@ -1762,33 +2326,38 @@ class Sentinel1Collection:
1762
2326
  ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
1763
2327
  return feature.set({'geo_name': new_name})
1764
2328
 
2329
+ # Handle input: FeatureCollection or single Feature
1765
2330
  if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
1766
2331
  features = ee.FeatureCollection(geometries)
1767
2332
  if geometry_names:
1768
2333
  print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
1769
2334
 
2335
+ # Handle input: Single ee.Geometry
1770
2336
  elif isinstance(geometries, ee.Geometry):
1771
2337
  name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
1772
2338
  features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
1773
2339
 
2340
+ # Handle input: List (could be coordinates or ee.Geometry objects)
1774
2341
  elif isinstance(geometries, list):
1775
2342
  if not geometries: # Handle empty list case
1776
2343
  raise ValueError("'geometries' list cannot be empty.")
1777
2344
 
1778
- # Case: List of coordinates
2345
+ # Case: List of tuples (coordinates)
1779
2346
  if all(isinstance(i, tuple) for i in geometries):
1780
2347
  validated_coordinates = geometries
2348
+ # Generate default names if none provided
1781
2349
  if geometry_names is None:
1782
2350
  geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
1783
2351
  elif len(geometry_names) != len(validated_coordinates):
1784
2352
  raise ValueError("geometry_names must have the same length as the coordinates list.")
2353
+ # Create features with buffers around the coordinates
1785
2354
  points = [
1786
2355
  ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
1787
2356
  for coord, name in zip(validated_coordinates, geometry_names)
1788
2357
  ]
1789
2358
  features = ee.FeatureCollection(points)
1790
2359
 
1791
- # Case: List of Geometries
2360
+ # Case: List of ee.Geometry objects
1792
2361
  elif all(isinstance(i, ee.Geometry) for i in geometries):
1793
2362
  if geometry_names is None:
1794
2363
  geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
@@ -1803,6 +2372,7 @@ class Sentinel1Collection:
1803
2372
  else:
1804
2373
  raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
1805
2374
 
2375
+ # Handle input: Single tuple (coordinate)
1806
2376
  elif isinstance(geometries, tuple) and len(geometries) == 2:
1807
2377
  name = geometry_names[0] if geometry_names else 'Location_1'
1808
2378
  features = ee.FeatureCollection([
@@ -1811,39 +2381,48 @@ class Sentinel1Collection:
1811
2381
  else:
1812
2382
  raise TypeError("Unsupported type for 'geometries'.")
1813
2383
 
2384
+ # Apply the naming standardization to the created FeatureCollection
1814
2385
  features = features.map(set_standard_name)
1815
2386
 
2387
+ # Dynamically retrieve the Earth Engine reducer based on the string name provided
1816
2388
  try:
1817
2389
  reducer = getattr(ee.Reducer, reducer_type)()
1818
2390
  except AttributeError:
1819
2391
  raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
1820
2392
 
2393
+ # Define the function to map over the image collection
1821
2394
  def calculate_stats_for_image(image):
1822
2395
  image_date = image.get('Date_Filter')
2396
+ # Calculate statistics for all geometries in 'features' for this specific image
1823
2397
  stats_fc = image.reduceRegions(
1824
2398
  collection=features, reducer=reducer, scale=scale, tileScale=tileScale
1825
2399
  )
1826
2400
 
2401
+ # Helper to ensure the result has the reducer property, even if masked
2402
+ # If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
1827
2403
  def guarantee_reducer_property(f):
1828
2404
  has_property = f.propertyNames().contains(reducer_type)
1829
2405
  return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
2406
+
2407
+ # Apply the guarantee check
1830
2408
  fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
1831
2409
 
2410
+ # Attach the image date to every feature in the result so we know which image it came from
1832
2411
  return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
1833
2412
 
2413
+ # Map the calculation over the image collection and flatten the resulting FeatureCollections into one
1834
2414
  results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
2415
+
2416
+ # Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
1835
2417
  df = Sentinel1Collection.ee_to_df(results_fc, remove_geom=True)
1836
2418
 
1837
- # Checking for issues
2419
+ # Check for empty results or missing columns
1838
2420
  if df.empty:
1839
- # print("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
1840
- # return df
1841
2421
  raise ValueError("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
1842
2422
  if reducer_type not in df.columns:
1843
2423
  print(f"Warning: Reducer '{reducer_type}' not found in results.")
1844
- # return df
1845
2424
 
1846
- # Get the number of rows before dropping nulls for a helpful message
2425
+ # Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
1847
2426
  initial_rows = len(df)
1848
2427
  df.dropna(subset=[reducer_type], inplace=True)
1849
2428
  df = df[df[reducer_type] != -9999]
@@ -1851,9 +2430,18 @@ class Sentinel1Collection:
1851
2430
  if dropped_rows > 0:
1852
2431
  print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
1853
2432
 
1854
- # Reshape DataFrame to have dates as index and geometry names as columns
2433
+ # Pivot the DataFrame so that each row represents a date and each column represents a geometry location
1855
2434
  pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
2435
+ # Rename the column headers (geometry names) to include the reducer type
2436
+ pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
2437
+ # Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
1856
2438
  pivot_df.index.name = 'Date'
2439
+ # Remove the name of the columns axis (which defaults to 'geo_name') so it doesn't appear as a confusing label in the final output
2440
+ pivot_df.columns.name = None
2441
+ # Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
2442
+ pivot_df = pivot_df.reset_index(drop=False)
2443
+
2444
+ # If a file path is provided, save the resulting DataFrame to CSV
1857
2445
  if file_path:
1858
2446
  # Check if file_path ends with .csv and remove it if so for consistency
1859
2447
  if file_path.endswith('.csv'):