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