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