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.
@@ -166,6 +166,10 @@ class Sentinel2Collection:
166
166
  self._masked_water_collection = None
167
167
  self._median = None
168
168
  self._monthly_median = None
169
+ self._monthly_mean = None
170
+ self._monthly_max = None
171
+ self._monthly_min = None
172
+ self._monthly_sum = None
169
173
  self._mean = None
170
174
  self._max = None
171
175
  self._min = None
@@ -482,7 +486,7 @@ class Sentinel2Collection:
482
486
  return nbr
483
487
 
484
488
  @staticmethod
485
- def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True):
489
+ def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=10):
486
490
  """
487
491
  Calculates the anomaly of a singleband image compared to the mean of the singleband image.
488
492
 
@@ -498,6 +502,7 @@ class Sentinel2Collection:
498
502
  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
503
  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
504
  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.
505
+ scale (int, optional): The scale in meters to use for the reduction operation. Default is 10 meters.
501
506
 
502
507
  Returns:
503
508
  ee.Image: An ee.Image where each band represents the anomaly (deviation from
@@ -514,12 +519,16 @@ class Sentinel2Collection:
514
519
  mean_image = image_to_process.reduceRegion(
515
520
  reducer=ee.Reducer.mean(),
516
521
  geometry=geometry,
517
- scale=10,
522
+ scale=scale,
518
523
  maxPixels=1e13
519
524
  ).toImage()
520
525
 
521
526
  # Compute the anomaly by subtracting the mean image from the input image.
522
- anomaly_image = image_to_process.subtract(mean_image)
527
+ if scale == 10:
528
+ anomaly_image = image_to_process.subtract(mean_image)
529
+ else:
530
+ anomaly_image = image_to_process.reproject(crs=image_to_process.projection(), scale=scale).subtract(mean_image)
531
+
523
532
  if anomaly_band_name is None:
524
533
  if band_name:
525
534
  anomaly_image = anomaly_image.rename(band_name)
@@ -530,7 +539,7 @@ class Sentinel2Collection:
530
539
  anomaly_image = anomaly_image.rename(anomaly_band_name)
531
540
  # return anomaly_image
532
541
  if replace:
533
- return anomaly_image.copyProperties(image)
542
+ return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
534
543
  else:
535
544
  return image.addBands(anomaly_image, overwrite=True)
536
545
 
@@ -808,11 +817,11 @@ class Sentinel2Collection:
808
817
 
809
818
  Args:
810
819
  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.
811
- geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
820
+ geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation.
812
821
  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.
813
- scale (int): integer scale of image resolution (meters) (defaults to 10)
814
- maxPixels (int): integer denoting maximum number of pixels for calculations
815
- output_type (str): 'ImageCollection' to return an ee.ImageCollection, 'Sentinel2Collection' to return a Sentinel2Collection object (defaults to 'ImageCollection')
822
+ scale (int): integer scale of image resolution (meters) (defaults to 30).
823
+ maxPixels (int): integer denoting maximum number of pixels for calculations.
824
+ output_type (str): 'ImageCollection' or 'ee.ImageCollection' to return an ee.ImageCollection, 'Sentinel2Collection' to return a Sentinel2Collection object, or 'DataFrame', 'Pandas', 'pd', 'dataframe', 'df' to return a pandas DataFrame (defaults to 'ImageCollection').
816
825
  area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
817
826
 
818
827
  Returns:
@@ -835,17 +844,45 @@ class Sentinel2Collection:
835
844
  # Storing the result in the instance variable to avoid redundant calculations
836
845
  self._PixelAreaSumCollection = AreaCollection
837
846
 
847
+ prop_names = band_name if isinstance(band_name, list) else [band_name]
848
+
838
849
  # If an export path is provided, the area data will be exported to a CSV file
839
850
  if area_data_export_path:
840
- Sentinel2Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=band_name, file_path=area_data_export_path+'.csv')
841
-
851
+ Sentinel2Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
842
852
  # Returning the result in the desired format based on output_type argument or raising an error for invalid input
843
- if output_type == 'ImageCollection':
853
+ if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
844
854
  return self._PixelAreaSumCollection
845
855
  elif output_type == 'Sentinel2Collection':
846
856
  return Sentinel2Collection(collection=self._PixelAreaSumCollection)
857
+ elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
858
+ return Sentinel2Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
847
859
  else:
848
- raise ValueError("output_type must be 'ImageCollection' or 'Sentinel2Collection'")
860
+ raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'Sentinel2Collection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
861
+
862
+ @staticmethod
863
+ def add_month_property_fn(image):
864
+ """
865
+ Adds a numeric 'month' property to the image based on its date.
866
+
867
+ Args:
868
+ image (ee.Image): Input image.
869
+
870
+ Returns:
871
+ ee.Image: Image with the 'month' property added.
872
+ """
873
+ return image.set('month', image.date().get('month'))
874
+
875
+ @property
876
+ def add_month_property(self):
877
+ """
878
+ Adds a numeric 'month' property to each image in the collection.
879
+
880
+ Returns:
881
+ Sentinel2Collection: A Sentinel2Collection image collection with the 'month' property added to each image.
882
+ """
883
+ col = self.collection.map(Sentinel2Collection.add_month_property_fn)
884
+ return Sentinel2Collection(collection=col)
885
+
849
886
 
850
887
  def combine(self, other):
851
888
  """
@@ -974,6 +1011,8 @@ class Sentinel2Collection:
974
1011
  # Ensure property_names is a list for consistent processing
975
1012
  if isinstance(property_names, str):
976
1013
  property_names = [property_names]
1014
+ elif isinstance(property_names, list):
1015
+ property_names = property_names
977
1016
 
978
1017
  # Ensure properties are included without duplication, including 'Date_Filter'
979
1018
  all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
@@ -1209,6 +1248,338 @@ class Sentinel2Collection:
1209
1248
  pass
1210
1249
 
1211
1250
  return self._monthly_median
1251
+
1252
+ @property
1253
+ def monthly_mean_collection(self):
1254
+ """Creates a monthly mean composite from a Sentinel2Collection image collection.
1255
+
1256
+ This function computes the mean for each
1257
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1258
+ temporal extent of the input collection.
1259
+
1260
+ The resulting images have a 'system:time_start' property set to the
1261
+ first day of each month and an 'image_count' property indicating how
1262
+ many images were used in the composite. Months with no images are
1263
+ automatically excluded from the final collection.
1264
+
1265
+ 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.
1266
+
1267
+ Returns:
1268
+ Sentinel2Collection: A new Sentinel2Collection object with monthly mean composites.
1269
+ """
1270
+ if self._monthly_mean is None:
1271
+ collection = self.collection
1272
+ target_proj = collection.first().projection()
1273
+ # Get the start and end dates of the entire collection.
1274
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1275
+ original_start_date = ee.Date(date_range.get('min'))
1276
+ end_date = ee.Date(date_range.get('max'))
1277
+
1278
+ start_year = original_start_date.get('year')
1279
+ start_month = original_start_date.get('month')
1280
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1281
+
1282
+ # Calculate the total number of months in the date range.
1283
+ # The .round() is important for ensuring we get an integer.
1284
+ num_months = end_date.difference(start_date, 'month').round()
1285
+
1286
+ # Generate a list of starting dates for each month.
1287
+ # This uses a sequence and advances the start date by 'i' months.
1288
+ def get_month_start(i):
1289
+ return start_date.advance(i, 'month')
1290
+
1291
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1292
+
1293
+ # Define a function to map over the list of month start dates.
1294
+ def create_monthly_composite(date):
1295
+ # Cast the input to an ee.Date object.
1296
+ start_of_month = ee.Date(date)
1297
+ # The end date is exclusive, so we advance by 1 month.
1298
+ end_of_month = start_of_month.advance(1, 'month')
1299
+
1300
+ # Filter the original collection to get images for the current month.
1301
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1302
+
1303
+ # Count the number of images in the monthly subset.
1304
+ image_count = monthly_subset.size()
1305
+
1306
+ # Compute the mean. This is robust to outliers like clouds.
1307
+ monthly_mean = monthly_subset.mean()
1308
+
1309
+ # Set essential properties on the resulting composite image.
1310
+ # The timestamp is crucial for time-series analysis and charting.
1311
+ # The image_count is useful metadata for quality assessment.
1312
+ return monthly_mean.set({
1313
+ 'system:time_start': start_of_month.millis(),
1314
+ 'month': start_of_month.get('month'),
1315
+ 'year': start_of_month.get('year'),
1316
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1317
+ 'image_count': image_count
1318
+ }).reproject(target_proj)
1319
+
1320
+ # Map the composite function over the list of month start dates.
1321
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1322
+
1323
+ # Convert the list of images into an ee.ImageCollection.
1324
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1325
+
1326
+ # Filter out any composites that were created from zero images.
1327
+ # This prevents empty/masked images from being in the final collection.
1328
+ final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1329
+ self._monthly_mean = final_collection
1330
+ else:
1331
+ pass
1332
+
1333
+ return self._monthly_mean
1334
+
1335
+ @property
1336
+ def monthly_sum_collection(self):
1337
+ """Creates a monthly sum composite from a Sentinel2Collection image collection.
1338
+
1339
+ This function computes the sum for each
1340
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1341
+ temporal extent of the input collection.
1342
+
1343
+ The resulting images have a 'system:time_start' property set to the
1344
+ first day of each month and an 'image_count' property indicating how
1345
+ many images were used in the composite. Months with no images are
1346
+ automatically excluded from the final collection.
1347
+
1348
+ 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.
1349
+
1350
+ Returns:
1351
+ Sentinel2Collection: A new Sentinel2Collection object with monthly sum composites.
1352
+ """
1353
+ if self._monthly_sum is None:
1354
+ collection = self.collection
1355
+ target_proj = collection.first().projection()
1356
+ # Get the start and end dates of the entire collection.
1357
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1358
+ original_start_date = ee.Date(date_range.get('min'))
1359
+ end_date = ee.Date(date_range.get('max'))
1360
+
1361
+ start_year = original_start_date.get('year')
1362
+ start_month = original_start_date.get('month')
1363
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1364
+
1365
+ # Calculate the total number of months in the date range.
1366
+ # The .round() is important for ensuring we get an integer.
1367
+ num_months = end_date.difference(start_date, 'month').round()
1368
+
1369
+ # Generate a list of starting dates for each month.
1370
+ # This uses a sequence and advances the start date by 'i' months.
1371
+ def get_month_start(i):
1372
+ return start_date.advance(i, 'month')
1373
+
1374
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1375
+
1376
+ # Define a function to map over the list of month start dates.
1377
+ def create_monthly_composite(date):
1378
+ # Cast the input to an ee.Date object.
1379
+ start_of_month = ee.Date(date)
1380
+ # The end date is exclusive, so we advance by 1 month.
1381
+ end_of_month = start_of_month.advance(1, 'month')
1382
+
1383
+ # Filter the original collection to get images for the current month.
1384
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1385
+
1386
+ # Count the number of images in the monthly subset.
1387
+ image_count = monthly_subset.size()
1388
+
1389
+ # Compute the sum. This is robust to outliers like clouds.
1390
+ monthly_sum = monthly_subset.sum()
1391
+
1392
+ # Set essential properties on the resulting composite image.
1393
+ # The timestamp is crucial for time-series analysis and charting.
1394
+ # The image_count is useful metadata for quality assessment.
1395
+ return monthly_sum.set({
1396
+ 'system:time_start': start_of_month.millis(),
1397
+ 'month': start_of_month.get('month'),
1398
+ 'year': start_of_month.get('year'),
1399
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1400
+ 'image_count': image_count
1401
+ }).reproject(target_proj)
1402
+
1403
+ # Map the composite function over the list of month start dates.
1404
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1405
+
1406
+ # Convert the list of images into an ee.ImageCollection.
1407
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1408
+
1409
+ # Filter out any composites that were created from zero images.
1410
+ # This prevents empty/masked images from being in the final collection.
1411
+ final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1412
+ self._monthly_sum = final_collection
1413
+ else:
1414
+ pass
1415
+
1416
+ return self._monthly_sum
1417
+
1418
+ @property
1419
+ def monthly_max_collection(self):
1420
+ """Creates a monthly max composite from a Sentinel2Collection image collection.
1421
+
1422
+ This function computes the max for each
1423
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1424
+ temporal extent of the input collection.
1425
+
1426
+ The resulting images have a 'system:time_start' property set to the
1427
+ first day of each month and an 'image_count' property indicating how
1428
+ many images were used in the composite. Months with no images are
1429
+ automatically excluded from the final collection.
1430
+
1431
+ 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.
1432
+
1433
+ Returns:
1434
+ Sentinel2Collection: A new Sentinel2Collection object with monthly max composites.
1435
+ """
1436
+ if self._monthly_max is None:
1437
+ collection = self.collection
1438
+ target_proj = collection.first().projection()
1439
+ # Get the start and end dates of the entire collection.
1440
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1441
+ original_start_date = ee.Date(date_range.get('min'))
1442
+ end_date = ee.Date(date_range.get('max'))
1443
+
1444
+ start_year = original_start_date.get('year')
1445
+ start_month = original_start_date.get('month')
1446
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1447
+
1448
+ # Calculate the total number of months in the date range.
1449
+ # The .round() is important for ensuring we get an integer.
1450
+ num_months = end_date.difference(start_date, 'month').round()
1451
+
1452
+ # Generate a list of starting dates for each month.
1453
+ # This uses a sequence and advances the start date by 'i' months.
1454
+ def get_month_start(i):
1455
+ return start_date.advance(i, 'month')
1456
+
1457
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1458
+
1459
+ # Define a function to map over the list of month start dates.
1460
+ def create_monthly_composite(date):
1461
+ # Cast the input to an ee.Date object.
1462
+ start_of_month = ee.Date(date)
1463
+ # The end date is exclusive, so we advance by 1 month.
1464
+ end_of_month = start_of_month.advance(1, 'month')
1465
+
1466
+ # Filter the original collection to get images for the current month.
1467
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1468
+
1469
+ # Count the number of images in the monthly subset.
1470
+ image_count = monthly_subset.size()
1471
+
1472
+ # Compute the max. This is robust to outliers like clouds.
1473
+ monthly_max = monthly_subset.max()
1474
+
1475
+ # Set essential properties on the resulting composite image.
1476
+ # The timestamp is crucial for time-series analysis and charting.
1477
+ # The image_count is useful metadata for quality assessment.
1478
+ return monthly_max.set({
1479
+ 'system:time_start': start_of_month.millis(),
1480
+ 'month': start_of_month.get('month'),
1481
+ 'year': start_of_month.get('year'),
1482
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1483
+ 'image_count': image_count
1484
+ }).reproject(target_proj)
1485
+
1486
+ # Map the composite function over the list of month start dates.
1487
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1488
+
1489
+ # Convert the list of images into an ee.ImageCollection.
1490
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1491
+
1492
+ # Filter out any composites that were created from zero images.
1493
+ # This prevents empty/masked images from being in the final collection.
1494
+ final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1495
+ self._monthly_max = final_collection
1496
+ else:
1497
+ pass
1498
+
1499
+ return self._monthly_max
1500
+
1501
+ @property
1502
+ def monthly_min_collection(self):
1503
+ """Creates a monthly min composite from a Sentinel2Collection image collection.
1504
+
1505
+ This function computes the min for each
1506
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1507
+ temporal extent of the input collection.
1508
+
1509
+ The resulting images have a 'system:time_start' property set to the
1510
+ first day of each month and an 'image_count' property indicating how
1511
+ many images were used in the composite. Months with no images are
1512
+ automatically excluded from the final collection.
1513
+
1514
+ 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.
1515
+
1516
+ Returns:
1517
+ Sentinel2Collection: A new Sentinel2Collection object with monthly min composites.
1518
+ """
1519
+ if self._monthly_min is None:
1520
+ collection = self.collection
1521
+ target_proj = collection.first().projection()
1522
+ # Get the start and end dates of the entire collection.
1523
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1524
+ original_start_date = ee.Date(date_range.get('min'))
1525
+ end_date = ee.Date(date_range.get('max'))
1526
+
1527
+ start_year = original_start_date.get('year')
1528
+ start_month = original_start_date.get('month')
1529
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1530
+
1531
+ # Calculate the total number of months in the date range.
1532
+ # The .round() is important for ensuring we get an integer.
1533
+ num_months = end_date.difference(start_date, 'month').round()
1534
+
1535
+ # Generate a list of starting dates for each month.
1536
+ # This uses a sequence and advances the start date by 'i' months.
1537
+ def get_month_start(i):
1538
+ return start_date.advance(i, 'month')
1539
+
1540
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1541
+
1542
+ # Define a function to map over the list of month start dates.
1543
+ def create_monthly_composite(date):
1544
+ # Cast the input to an ee.Date object.
1545
+ start_of_month = ee.Date(date)
1546
+ # The end date is exclusive, so we advance by 1 month.
1547
+ end_of_month = start_of_month.advance(1, 'month')
1548
+
1549
+ # Filter the original collection to get images for the current month.
1550
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1551
+
1552
+ # Count the number of images in the monthly subset.
1553
+ image_count = monthly_subset.size()
1554
+
1555
+ # Compute the min. This is robust to outliers like clouds.
1556
+ monthly_min = monthly_subset.min()
1557
+
1558
+ # Set essential properties on the resulting composite image.
1559
+ # The timestamp is crucial for time-series analysis and charting.
1560
+ # The image_count is useful metadata for quality assessment.
1561
+ return monthly_min.set({
1562
+ 'system:time_start': start_of_month.millis(),
1563
+ 'month': start_of_month.get('month'),
1564
+ 'year': start_of_month.get('year'),
1565
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1566
+ 'image_count': image_count
1567
+ }).reproject(target_proj)
1568
+
1569
+ # Map the composite function over the list of month start dates.
1570
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1571
+
1572
+ # Convert the list of images into an ee.ImageCollection.
1573
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1574
+
1575
+ # Filter out any composites that were created from zero images.
1576
+ # This prevents empty/masked images from being in the final collection.
1577
+ final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1578
+ self._monthly_min = final_collection
1579
+ else:
1580
+ pass
1581
+
1582
+ return self._monthly_min
1212
1583
 
1213
1584
  @property
1214
1585
  def mean(self):
@@ -1868,6 +2239,30 @@ class Sentinel2Collection:
1868
2239
  )
1869
2240
  return Sentinel2Collection(collection=col)
1870
2241
 
2242
+ def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
2243
+ """
2244
+ Removes duplicate images that share the same date, keeping only the first one encountered.
2245
+ Useful for handling duplicate Sentinel-2A/2B acquisitions or overlapping path/rows.
2246
+
2247
+ Args:
2248
+ sort_by (str): Property to sort by before filtering distinct dates.
2249
+ Defaults to 'system:time_start'.
2250
+ Recommended to use 'CLOUDY_PIXEL_PERCENTAGE' (ascending=True) to keep the clearest image.
2251
+ ascending (bool): Sort order. Defaults to True.
2252
+
2253
+ Returns:
2254
+ Sentinel2Collection: A new Sentinel2Collection object with distinct dates.
2255
+ """
2256
+ if sort_by not in ['system:time_start', 'CLOUDY_PIXEL_PERCENTAGE']:
2257
+ raise ValueError(f"The provided `sort_by` argument is invalid: {sort_by}. The `sort_by` argument must be either 'system:time_start' or 'CLOUDY_PIXEL_PERCENTAGE'.")
2258
+ # Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
2259
+ sorted_col = self.collection.sort(sort_by, ascending)
2260
+
2261
+ # distinct() retains the first image for each unique value of the specified property
2262
+ distinct_col = sorted_col.distinct('Date_Filter')
2263
+
2264
+ return Sentinel2Collection(collection=distinct_col)
2265
+
1871
2266
  @property
1872
2267
  def masked_water_collection(self):
1873
2268
  """
@@ -2068,26 +2463,27 @@ class Sentinel2Collection:
2068
2463
  raise ValueError("Threshold must be specified for binary masking.")
2069
2464
 
2070
2465
  if classify_above_threshold:
2071
- if mask_zeros:
2466
+ if mask_zeros is True:
2072
2467
  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)
2468
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).selfMask().copyProperties(image)
2074
2469
  )
2075
2470
  else:
2076
2471
  col = self.collection.map(
2077
2472
  lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
2078
2473
  )
2079
2474
  else:
2080
- if mask_zeros:
2475
+ if mask_zeros is True:
2081
2476
  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)
2477
+ lambda image: image.select(band_name).lte(threshold).rename(band_name).selfMask().copyProperties(image)
2083
2478
  )
2084
2479
  else:
2085
2480
  col = self.collection.map(
2086
2481
  lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
2087
2482
  )
2483
+
2088
2484
  return Sentinel2Collection(collection=col)
2089
2485
 
2090
- def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
2486
+ def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=10):
2091
2487
  """
2092
2488
  Calculates the anomaly of each image in a collection compared to the mean of each image.
2093
2489
 
@@ -2101,6 +2497,7 @@ class Sentinel2Collection:
2101
2497
  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
2498
  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
2499
  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.
2500
+ scale (int, optional): The scale (in meters) to use for the image reduction. Default is 10 meters.
2104
2501
 
2105
2502
  Returns:
2106
2503
  Sentinel2Collection: A Sentinel2Collection where each image represents the anomaly (deviation from
@@ -2119,7 +2516,7 @@ class Sentinel2Collection:
2119
2516
  else:
2120
2517
  band_name = band_names.get(0).getInfo()
2121
2518
 
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))
2519
+ col = self.collection.map(lambda image: Sentinel2Collection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
2123
2520
  return Sentinel2Collection(collection=col)
2124
2521
 
2125
2522
  def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
@@ -2892,8 +3289,9 @@ class Sentinel2Collection:
2892
3289
  def iterate_zonal_stats(
2893
3290
  self,
2894
3291
  geometries,
3292
+ band=None,
2895
3293
  reducer_type="mean",
2896
- scale=10,
3294
+ scale=30,
2897
3295
  geometry_names=None,
2898
3296
  buffer_size=1,
2899
3297
  tileScale=1,
@@ -2905,10 +3303,13 @@ class Sentinel2Collection:
2905
3303
  When coordinates are provided, a radial buffer is applied around each coordinate to extract the statistics, where the size of the buffer is determined by the buffer_size argument (defaults to 1 meter).
2906
3304
  The function returns a pandas DataFrame with the statistics for each coordinate and date, or optionally exports the data to a table in .csv format.
2907
3305
 
3306
+ NOTE: The expected input is a singleband image. If a multiband image is provided, the first band will be used for zonal statistic extraction, unless `band` is specified.
3307
+
2908
3308
  Args:
2909
3309
  geometries (ee.Geometry, ee.Feature, ee.FeatureCollection, list, or tuple): Input geometries for which to extract statistics. Can be a single ee.Geometry, an ee.Feature, an ee.FeatureCollection, a list of (lon, lat) tuples, or a list of ee.Geometry objects. Be careful to NOT provide coordinates as (lat, lon)!
3310
+ band (str, optional): The name of the band to use for statistics. If None, the first band is used. Defaults to None.
2910
3311
  reducer_type (str, optional): The ee.Reducer to use, e.g., 'mean', 'median', 'max', 'sum'. Defaults to 'mean'. Any ee.Reducer method can be used.
2911
- scale (int, optional): Pixel scale in meters for the reduction. Defaults to 10.
3312
+ scale (int, optional): Pixel scale in meters for the reduction. Defaults to 30.
2912
3313
  geometry_names (list, optional): A list of string names for the geometries. If provided, must match the number of geometries. Defaults to None.
2913
3314
  buffer_size (int, optional): Radial buffer in meters around coordinates. Defaults to 1.
2914
3315
  tileScale (int, optional): A scaling factor to reduce aggregation tile size. Defaults to 1.
@@ -2923,18 +3324,30 @@ class Sentinel2Collection:
2923
3324
  ValueError: If input parameters are invalid.
2924
3325
  TypeError: If geometries input type is unsupported.
2925
3326
  """
3327
+ # Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
2926
3328
  img_collection_obj = self
2927
- # Filter collection by dates if provided
3329
+
3330
+ # If a specific band is requested, select only that band
3331
+ if band:
3332
+ img_collection_obj = Sentinel2Collection(collection=img_collection_obj.collection.select(band))
3333
+ else:
3334
+ # If no band is specified, default to using the first band of the first image in the collection
3335
+ first_image = img_collection_obj.image_grab(0)
3336
+ first_band = first_image.bandNames().get(0)
3337
+ img_collection_obj = Sentinel2Collection(collection=img_collection_obj.collection.select([first_band]))
3338
+
3339
+ # If a list of dates is provided, filter the collection to include only images matching those dates
2928
3340
  if dates:
2929
3341
  img_collection_obj = Sentinel2Collection(
2930
3342
  collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
2931
3343
  )
2932
3344
 
2933
- # Initialize variables
3345
+ # Initialize variables to hold the standardized feature collection and coordinates
2934
3346
  features = None
2935
3347
  validated_coordinates = []
2936
3348
 
2937
- # Function to standardize feature names if no names are provided
3349
+ # Define a helper function to ensure every feature has a standardized 'geo_name' property
3350
+ # This handles features that might have different existing name properties or none at all
2938
3351
  def set_standard_name(feature):
2939
3352
  has_geo_name = feature.get('geo_name')
2940
3353
  has_name = feature.get('name')
@@ -2945,33 +3358,38 @@ class Sentinel2Collection:
2945
3358
  ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
2946
3359
  return feature.set({'geo_name': new_name})
2947
3360
 
3361
+ # Handle input: FeatureCollection or single Feature
2948
3362
  if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
2949
3363
  features = ee.FeatureCollection(geometries)
2950
3364
  if geometry_names:
2951
3365
  print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
2952
3366
 
3367
+ # Handle input: Single ee.Geometry
2953
3368
  elif isinstance(geometries, ee.Geometry):
2954
3369
  name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
2955
3370
  features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
2956
3371
 
3372
+ # Handle input: List (could be coordinates or ee.Geometry objects)
2957
3373
  elif isinstance(geometries, list):
2958
3374
  if not geometries: # Handle empty list case
2959
3375
  raise ValueError("'geometries' list cannot be empty.")
2960
3376
 
2961
- # Case: List of coordinates
3377
+ # Case: List of tuples (coordinates)
2962
3378
  if all(isinstance(i, tuple) for i in geometries):
2963
3379
  validated_coordinates = geometries
3380
+ # Generate default names if none provided
2964
3381
  if geometry_names is None:
2965
3382
  geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
2966
3383
  elif len(geometry_names) != len(validated_coordinates):
2967
3384
  raise ValueError("geometry_names must have the same length as the coordinates list.")
3385
+ # Create features with buffers around the coordinates
2968
3386
  points = [
2969
3387
  ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
2970
3388
  for coord, name in zip(validated_coordinates, geometry_names)
2971
3389
  ]
2972
3390
  features = ee.FeatureCollection(points)
2973
3391
 
2974
- # Case: List of Geometries
3392
+ # Case: List of ee.Geometry objects
2975
3393
  elif all(isinstance(i, ee.Geometry) for i in geometries):
2976
3394
  if geometry_names is None:
2977
3395
  geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
@@ -2986,6 +3404,7 @@ class Sentinel2Collection:
2986
3404
  else:
2987
3405
  raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
2988
3406
 
3407
+ # Handle input: Single tuple (coordinate)
2989
3408
  elif isinstance(geometries, tuple) and len(geometries) == 2:
2990
3409
  name = geometry_names[0] if geometry_names else 'Location_1'
2991
3410
  features = ee.FeatureCollection([
@@ -2994,39 +3413,48 @@ class Sentinel2Collection:
2994
3413
  else:
2995
3414
  raise TypeError("Unsupported type for 'geometries'.")
2996
3415
 
3416
+ # Apply the naming standardization to the created FeatureCollection
2997
3417
  features = features.map(set_standard_name)
2998
3418
 
3419
+ # Dynamically retrieve the Earth Engine reducer based on the string name provided
2999
3420
  try:
3000
3421
  reducer = getattr(ee.Reducer, reducer_type)()
3001
3422
  except AttributeError:
3002
3423
  raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
3003
3424
 
3425
+ # Define the function to map over the image collection
3004
3426
  def calculate_stats_for_image(image):
3005
3427
  image_date = image.get('Date_Filter')
3428
+ # Calculate statistics for all geometries in 'features' for this specific image
3006
3429
  stats_fc = image.reduceRegions(
3007
3430
  collection=features, reducer=reducer, scale=scale, tileScale=tileScale
3008
3431
  )
3009
3432
 
3433
+ # Helper to ensure the result has the reducer property, even if masked
3434
+ # If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
3010
3435
  def guarantee_reducer_property(f):
3011
3436
  has_property = f.propertyNames().contains(reducer_type)
3012
3437
  return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
3438
+
3439
+ # Apply the guarantee check
3013
3440
  fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
3014
3441
 
3442
+ # Attach the image date to every feature in the result so we know which image it came from
3015
3443
  return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
3016
3444
 
3445
+ # Map the calculation over the image collection and flatten the resulting FeatureCollections into one
3017
3446
  results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
3447
+
3448
+ # Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
3018
3449
  df = Sentinel2Collection.ee_to_df(results_fc, remove_geom=True)
3019
3450
 
3020
- # Checking for issues
3451
+ # Check for empty results or missing columns
3021
3452
  if df.empty:
3022
- # 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.")
3023
- # return df
3024
3453
  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.")
3025
3454
  if reducer_type not in df.columns:
3026
3455
  print(f"Warning: Reducer '{reducer_type}' not found in results.")
3027
- # return df
3028
3456
 
3029
- # Get the number of rows before dropping nulls for a helpful message
3457
+ # Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
3030
3458
  initial_rows = len(df)
3031
3459
  df.dropna(subset=[reducer_type], inplace=True)
3032
3460
  df = df[df[reducer_type] != -9999]
@@ -3034,9 +3462,18 @@ class Sentinel2Collection:
3034
3462
  if dropped_rows > 0:
3035
3463
  print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
3036
3464
 
3037
- # Reshape DataFrame to have dates as index and geometry names as columns
3465
+ # Pivot the DataFrame so that each row represents a date and each column represents a geometry location
3038
3466
  pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
3467
+ # Rename the column headers (geometry names) to include the reducer type
3468
+ pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
3469
+ # Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
3039
3470
  pivot_df.index.name = 'Date'
3471
+ # 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
3472
+ pivot_df.columns.name = None
3473
+ # Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
3474
+ pivot_df = pivot_df.reset_index(drop=False)
3475
+
3476
+ # If a file path is provided, save the resulting DataFrame to CSV
3040
3477
  if file_path:
3041
3478
  # Check if file_path ends with .csv and remove it if so for consistency
3042
3479
  if file_path.endswith('.csv'):