RadGEEToolbox 1.6.7__py3-none-any.whl → 1.6.8__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.
@@ -236,60 +236,91 @@ class Sentinel1Collection:
236
236
 
237
237
  @staticmethod
238
238
  def PixelAreaSum(
239
- image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
239
+ image, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12
240
240
  ):
241
241
  """
242
- Function to calculate the summation of area for pixels of interest (above a specific threshold) in a geometry
243
- and store the value as image property (matching name of chosen band).
242
+ Calculates the summation of area for pixels of interest (above a specific threshold) in a geometry
243
+ and store the value as image property (matching name of chosen band). If multiple band names are provided in a list,
244
+ the function will calculate area for each band in the list and store each as a separate property.
245
+
246
+ NOTE: The resulting value has units of square meters.
244
247
 
245
248
  Args:
246
249
  image (ee.Image): input ee.Image
247
- band_name (string): name of band (string) for calculating area
250
+ 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.
248
251
  geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
249
- threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1)
250
- scale (int): integer scale of image resolution (meters) (defaults to 30)
252
+ 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.
253
+ scale (int): integer scale of image resolution (meters) (defaults to 10)
251
254
  maxPixels (int): integer denoting maximum number of pixels for calculations
252
255
 
253
256
  Returns:
254
- ee.Image: ee.Image with area calculation stored as property matching name of band
257
+ ee.Image: ee.Image with area calculation in square meters stored as property matching name of band
255
258
  """
259
+ # Ensure band_name is a server-side ee.List for consistent processing. Wrap band_name in a list if it's a single string.
260
+ bands = ee.List(band_name) if isinstance(band_name, list) else ee.List([band_name])
261
+ # Create an image representing the area of each pixel in square meters
256
262
  area_image = ee.Image.pixelArea()
257
- mask = image.select(band_name).gte(threshold)
258
- final = image.addBands(area_image)
259
- stats = (
260
- final.select("area")
261
- .updateMask(mask)
262
- .rename(band_name)
263
- .reduceRegion(
264
- reducer=ee.Reducer.sum(),
265
- geometry=geometry,
266
- scale=scale,
267
- maxPixels=maxPixels,
263
+
264
+ # Function to iterate over each band and calculate area, storing the result as a property on the image
265
+ def calculate_and_set_area(band, img_accumulator):
266
+ # Explcitly cast inputs to expected types
267
+ img_accumulator = ee.Image(img_accumulator)
268
+ band = ee.String(band)
269
+
270
+ # Create a mask from the input image for the current band
271
+ mask = img_accumulator.select(band).gte(threshold)
272
+ # Combine the original image with the area image
273
+ final = img_accumulator.addBands(area_image)
274
+
275
+ # Calculation of area for a given band, utilizing other inputs
276
+ stats = (
277
+ final.select("area").updateMask(mask)
278
+ .rename(band) # renames 'area' to band name like 'ndwi'
279
+ .reduceRegion(
280
+ reducer=ee.Reducer.sum(),
281
+ geometry=geometry,
282
+ scale=scale,
283
+ maxPixels=maxPixels,
284
+ )
268
285
  )
269
- )
270
- return image.set(band_name, stats.get(band_name))
286
+ # Retrieving the area value from the stats dictionary with stats.get(band), as the band name is now the key
287
+ reduced_area = stats.get(band)
288
+ # Checking whether the calculated area is valid and replaces with 0 if not. This avoids breaking the loop for erroneous images.
289
+ area_value = ee.Algorithms.If(reduced_area, reduced_area, 0)
290
+
291
+ # Set the property on the image, named after the band
292
+ return img_accumulator.set(band, area_value)
293
+
294
+ # Call to iterate the calculate_and_set_area function over the list of bands, starting with the original image
295
+ final_image = ee.Image(bands.iterate(calculate_and_set_area, image))
296
+ return final_image
271
297
 
272
298
  def PixelAreaSumCollection(
273
- self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
299
+ self, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
274
300
  ):
275
301
  """
276
- Function to calculate the summation of area for pixels of interest (above a specific threshold)
277
- within a geometry and store the value as image property (matching name of chosen band) for an entire
278
- image collection.
279
- The resulting value has units of square meters.
302
+ Calculates the geodesic summation of area for pixels of interest (above a specific threshold)
303
+ within a geometry and stores the value as an image property (matching name of chosen band) for an entire
304
+ image collection. Optionally exports the area data to a CSV file.
305
+
306
+ NOTE: The resulting value has units of square meters.
280
307
 
281
308
  Args:
282
- band_name (str): name of band (string) for calculating area.
283
- geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation.
284
- threshold (int): integer threshold to specify masking of pixels below threshold (defaults to -1).
285
- scale (int): integer scale of image resolution (meters) (defaults to 30).
286
- maxPixels (int): integer denoting maximum number of pixels for calculations.
309
+ 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
311
+ 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')
315
+ area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
287
316
 
288
317
  Returns:
289
- ee.Image: Image with area calculation stored as property matching name of band.
318
+ ee.ImageCollection or Sentinel1Collection: Image collection of images with area calculation (square meters) stored as property matching name of band. Type of output depends on output_type argument.
290
319
  """
320
+ # If the area calculation has not been computed for this Sentinel1Collection instance, the area will be calculated for the provided bands
291
321
  if self._PixelAreaSumCollection is None:
292
322
  collection = self.collection
323
+ # Area calculation for each image in the collection, using the PixelAreaSum function
293
324
  AreaCollection = collection.map(
294
325
  lambda image: Sentinel1Collection.PixelAreaSum(
295
326
  image,
@@ -300,8 +331,38 @@ class Sentinel1Collection:
300
331
  maxPixels=maxPixels,
301
332
  )
302
333
  )
334
+ # Storing the result in the instance variable to avoid redundant calculations
303
335
  self._PixelAreaSumCollection = AreaCollection
304
- return self._PixelAreaSumCollection
336
+
337
+ # If an export path is provided, the area data will be exported to a CSV file
338
+ if area_data_export_path:
339
+ Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=band_name, file_path=area_data_export_path+'.csv')
340
+
341
+ # Returning the result in the desired format based on output_type argument or raising an error for invalid input
342
+ if output_type == 'ImageCollection':
343
+ return self._PixelAreaSumCollection
344
+ elif output_type == 'Sentinel1Collection':
345
+ return Sentinel1Collection(collection=self._PixelAreaSumCollection)
346
+ else:
347
+ raise ValueError("output_type must be 'ImageCollection' or 'Sentinel1Collection'")
348
+
349
+ def merge(self, other):
350
+ """
351
+ Merges the current Sentinel1Collection with another Sentinel1Collection, where images/bands with the same date are combined to a single multiband image.
352
+
353
+ Args:
354
+ other (Sentinel1Collection): Another Sentinel1Collection to merge with current collection.
355
+
356
+ Returns:
357
+ Sentinel1Collection: A new Sentinel1Collection containing images from both collections.
358
+ """
359
+ # Checking if 'other' is an instance of Sentinel1Collection
360
+ if not isinstance(other, Sentinel1Collection):
361
+ raise ValueError("The 'other' parameter must be an instance of Sentinel1Collection.")
362
+
363
+ # Merging the collections using the .combine() method
364
+ merged_collection = self.collection.combine(other.collection)
365
+ return Sentinel1Collection(collection=merged_collection)
305
366
 
306
367
  @staticmethod
307
368
  def multilook_fn(image, looks):
@@ -620,6 +681,60 @@ class Sentinel1Collection:
620
681
  dates = self._dates_list.getInfo()
621
682
  self._dates = dates
622
683
  return self._dates
684
+
685
+ def ExportProperties(self, property_names, file_path=None):
686
+ """
687
+ Fetches and returns specified properties from each image in the collection as a list, and returns a pandas DataFrame and optionally saves the results to a csv file.
688
+
689
+ Args:
690
+ property_names (list or str): A property name or list of property names to retrieve. The 'Date_Filter' property is always included to provide temporal context.
691
+ file_path (str, optional): If provided, the function will save the resulting DataFrame to a CSV file at this path. Defaults to None.
692
+
693
+ Returns:
694
+ pd.DataFrame: A pandas DataFrame containing the requested properties for each image, sorted chronologically by 'Date_Filter'.
695
+ """
696
+ # Ensure property_names is a list for consistent processing
697
+ if isinstance(property_names, str):
698
+ property_names = [property_names]
699
+
700
+ # Ensure properties are included without duplication, including 'Date_Filter'
701
+ all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
702
+
703
+ # Defining the helper function to create features with specified properties
704
+ def create_feature_with_properties(image):
705
+ """A function to map over the collection and store the image properties as an ee.Feature.
706
+ Args:
707
+ image (ee.Image): An image from the collection.
708
+ Returns:
709
+ ee.Feature: A feature containing the specified properties from the image.
710
+ """
711
+ properties = image.toDictionary(all_properties_to_fetch)
712
+ return ee.Feature(None, properties)
713
+
714
+ # Map the feature creation function over the server-side collection.
715
+ # The result is an ee.FeatureCollection where each feature holds the properties of one image.
716
+ mapped_collection = self.collection.map(create_feature_with_properties)
717
+ # Explicitly cast to ee.FeatureCollection for clarity
718
+ feature_collection = ee.FeatureCollection(mapped_collection)
719
+
720
+ # Use the existing ee_to_df static method. This performs the single .getInfo() call
721
+ # and converts the structured result directly to a pandas DataFrame.
722
+ df = Sentinel1Collection.ee_to_df(feature_collection, columns=all_properties_to_fetch)
723
+
724
+ # Sort by date for a clean, chronological output.
725
+ if 'Date_Filter' in df.columns:
726
+ df = df.sort_values(by='Date_Filter').reset_index(drop=True)
727
+
728
+ # Check condition for saving to CSV
729
+ if file_path:
730
+ # Check whether file_path ends with .csv, if not, append it
731
+ if not file_path.lower().endswith('.csv'):
732
+ file_path += '.csv'
733
+ # Save DataFrame to CSV
734
+ df.to_csv(file_path, index=True)
735
+ print(f"Properties saved to {file_path}")
736
+
737
+ return df
623
738
 
624
739
  def get_filtered_collection(self):
625
740
  """
@@ -759,6 +874,37 @@ class Sentinel1Collection:
759
874
  self._min = col
760
875
  return self._min
761
876
 
877
+ def binary_mask(self, threshold=None, band_name=None):
878
+ """
879
+ Creates a binary mask (value of 1 for pixels above set threshold and value of 0 for all other pixels) of the Sentinel1Collection image collection based on a specified band.
880
+ If a singleband image is provided, the band name is automatically determined.
881
+ If multiple bands are available, the user must specify the band name to use for masking.
882
+
883
+ Args:
884
+ band_name (str, optional): The name of the band to use for masking. Defaults to None.
885
+
886
+ Returns:
887
+ Sentinel1Collection: Sentinel1Collection singleband image collection with binary masks applied.
888
+ """
889
+ if self.collection.size().eq(0).getInfo():
890
+ raise ValueError("The collection is empty. Cannot create a binary mask.")
891
+ if band_name is None:
892
+ first_image = self.collection.first()
893
+ band_names = first_image.bandNames()
894
+ if band_names.size().getInfo() == 0:
895
+ raise ValueError("No bands available in the collection.")
896
+ if band_names.size().getInfo() > 1:
897
+ raise ValueError("Multiple bands available, please specify a band name.")
898
+ else:
899
+ band_name = band_names.get(0).getInfo()
900
+ if threshold is None:
901
+ raise ValueError("Threshold must be specified for binary masking.")
902
+
903
+ col = self.collection.map(
904
+ lambda image: image.select(band_name).gte(threshold).rename(band_name)
905
+ )
906
+ return Sentinel1Collection(collection=col)
907
+
762
908
  def mask_to_polygon(self, polygon):
763
909
  """
764
910
  Function to mask Sentinel1Collection image collection by a polygon (ee.Geometry), where pixels outside the polygon are masked out.
@@ -1046,7 +1192,8 @@ class Sentinel1Collection:
1046
1192
  to_pandas=False,
1047
1193
  **kwargs,
1048
1194
  ):
1049
- """Extracts transect from an image. Adapted from the geemap package (https://geemap.org/common/#geemap.common.extract_transect). Exists as an alternative to RadGEEToolbox 'transect' function.
1195
+ """
1196
+ Extracts transect from an image. Adapted from the geemap package (https://geemap.org/common/#geemap.common.extract_transect).
1050
1197
 
1051
1198
  Args:
1052
1199
  image (ee.Image): The image to extract transect from.
@@ -1120,7 +1267,8 @@ class Sentinel1Collection:
1120
1267
  dist_interval=10,
1121
1268
  to_pandas=True,
1122
1269
  ):
1123
- """Computes and stores the values along a transect for each line in a list of lines. Builds off of the extract_transect function from the geemap package
1270
+ """
1271
+ Computes and stores the values along a transect for each line in a list of lines. Builds off of the extract_transect function from the geemap package
1124
1272
  where checks are ran to ensure that the reducer column is present in the transect data. If the reducer column is not present, a column of NaNs is created.
1125
1273
  An ee reducer is used to aggregate the values along the transect, depending on the number of segments or distance interval specified. Defaults to 'mean' reducer.
1126
1274
 
@@ -1195,52 +1343,201 @@ class Sentinel1Collection:
1195
1343
  self,
1196
1344
  lines,
1197
1345
  line_names,
1198
- save_folder_path,
1199
1346
  reducer="mean",
1347
+ dist_interval= 10,
1200
1348
  n_segments=None,
1201
- dist_interval=10,
1202
- to_pandas=True,
1349
+ scale=10,
1350
+ processing_mode='aggregated',
1351
+ save_folder_path=None,
1352
+ sampling_method='line',
1353
+ point_buffer_radius=5
1203
1354
  ):
1204
- """Computes and stores the values along a transect for each line in a list of lines for each image in a Sentinel1Collection image collection, then saves the data for each image to a csv file. Builds off of the extract_transect function from the geemap package
1205
- where checks are ran to ensure that the reducer column is present in the transect data. If the reducer column is not present, a column of NaNs is created.
1206
- An ee reducer is used to aggregate the values along the transect, depending on the number of segments or distance interval specified. Defaults to 'mean' reducer.
1207
- Naming conventions for the csv files follows as: "image-date_line-name.csv"
1355
+ """
1356
+ Computes and returns pixel values along transects for each image in a collection.
1208
1357
 
1209
- Args:
1210
- lines (list): List of ee.Geometry.LineString objects.
1211
- line_names (list of strings): List of line string names.
1212
- save_folder_path (str): The path to the folder where the csv files will be saved.
1213
- reducer (str): The ee reducer to use. Defaults to 'mean'.
1214
- n_segments (int): The number of segments that the LineString will be split into. Defaults to None.
1215
- dist_interval (float): The distance interval used for splitting the LineString. If specified, the n_segments parameter will be ignored. Defaults to 10.
1216
- to_pandas (bool): Whether to convert the result to a pandas dataframe. Defaults to True.
1358
+ This iterative function generates time-series data along one or more lines, and
1359
+ supports two different geometric sampling methods ('line' and 'buffered_point')
1360
+ for maximum flexibility and performance.
1217
1361
 
1218
- Raises:
1219
- Exception: If the program fails to compute.
1362
+ There are two processing modes available, aggregated and iterative:
1363
+ - 'aggregated' (default; suggested): Fast, server-side processing. Fetches all results
1364
+ in a single request. Highly recommended. Returns a dictionary of pandas DataFrames.
1365
+ - 'iterative': Slower, client-side loop that processes one image at a time.
1366
+ Kept for backward compatibility (effectively depreciated). Returns None and saves individual CSVs.
1367
+ This method is not recommended unless absolutely necessary, as it is less efficient and may be subject to client-side timeouts.
1368
+
1369
+ Args:
1370
+ lines (list): A list of one or more ee.Geometry.LineString objects that
1371
+ define the transects.
1372
+ line_names (list): A list of string names for each transect. The length
1373
+ of this list must match the length of the `lines` list.
1374
+ reducer (str, optional): The name of the ee.Reducer to apply at each
1375
+ transect point (e.g., 'mean', 'median', 'first'). Defaults to 'mean'.
1376
+ dist_interval (float, optional): The distance interval in meters for
1377
+ sampling points along each transect. Will be overridden if `n_segments` is provided.
1378
+ Defaults to 10. Recommended to increase this value when using the
1379
+ 'line' processing method, or else you may get blank rows.
1380
+ n_segments (int, optional): The number of equal-length segments to split
1381
+ each transect line into for sampling. This parameter overrides `dist_interval`.
1382
+ Defaults to None.
1383
+ scale (int, optional): The nominal scale in meters for the reduction,
1384
+ which should typically match the pixel resolution of the imagery.
1385
+ Defaults to 10.
1386
+ processing_mode (str, optional): The method for processing the collection.
1387
+ - 'aggregated' (default): Fast, server-side processing. Fetches all
1388
+ results in a single request. Highly recommended. Returns a dictionary
1389
+ of pandas DataFrames.
1390
+ - 'iterative': Slower, client-side loop that processes one image at a
1391
+ time. Kept for backward compatibility. Returns None and saves
1392
+ individual CSVs.
1393
+ save_folder_path (str, optional): If provided, the function will save the
1394
+ resulting transect data to CSV files. The behavior depends on the
1395
+ `processing_mode`:
1396
+ - In 'aggregated' mode, one CSV is saved for each transect,
1397
+ containing all dates. (e.g., 'MyTransect_transects.csv').
1398
+ - In 'iterative' mode, one CSV is saved for each date,
1399
+ containing all transects. (e.g., '2022-06-15_transects.csv').
1400
+ sampling_method (str, optional): The geometric method used for sampling.
1401
+ - 'line' (default): Reduces all pixels intersecting each small line
1402
+ segment. This can be unreliable and produce blank rows if
1403
+ `dist_interval` is too small relative to the `scale`.
1404
+ - 'buffered_point': Reduces all pixels within a buffer around the
1405
+ midpoint of each line segment. This method is more robust and
1406
+ reliably avoids blank rows, but may not reduce all pixels along a line segment.
1407
+ point_buffer_radius (int, optional): The radius in meters for the buffer
1408
+ when `sampling_method` is 'buffered_point'. Defaults to 5.
1220
1409
 
1221
1410
  Returns:
1222
- csv file: file for each image with an organized list of values along the transect(s)
1411
+ dict or None:
1412
+ - If `processing_mode` is 'aggregated', returns a dictionary where each
1413
+ key is a transect name and each value is a pandas DataFrame. In the
1414
+ DataFrame, the index is the distance along the transect and each
1415
+ column represents an image date. Optionally saves CSV files if
1416
+ `save_folder_path` is provided.
1417
+ - If `processing_mode` is 'iterative', returns None as it saves
1418
+ files directly.
1419
+
1420
+ Raises:
1421
+ ValueError: If `lines` and `line_names` have different lengths, or if
1422
+ an unknown reducer or processing mode is specified.
1223
1423
  """
1224
- image_collection = self
1225
- image_collection_dates = self.dates
1226
- for i, date in enumerate(image_collection_dates):
1424
+ # Validating inputs
1425
+ if len(lines) != len(line_names):
1426
+ raise ValueError("'lines' and 'line_names' must have the same number of elements.")
1427
+ ### Current, server-side processing method ###
1428
+ if processing_mode == 'aggregated':
1429
+ # Validating reducer type
1227
1430
  try:
1228
- print(f"Processing image {i+1}/{len(image_collection_dates)}: {date}")
1229
- image = image_collection.image_grab(i)
1230
- transects_df = Sentinel1Collection.transect(
1231
- image,
1232
- lines,
1233
- line_names,
1234
- reducer=reducer,
1235
- n_segments=n_segments,
1236
- dist_interval=dist_interval,
1237
- to_pandas=to_pandas,
1238
- )
1239
- image_id = date
1240
- transects_df.to_csv(f"{save_folder_path}{image_id}_transects.csv")
1241
- print(f"{image_id}_transects saved to csv")
1242
- except Exception as e:
1243
- print(f"An error occurred while processing image {i+1}: {e}")
1431
+ ee_reducer = getattr(ee.Reducer, reducer)()
1432
+ except AttributeError:
1433
+ raise ValueError(f"Unknown reducer: '{reducer}'.")
1434
+ ### Function to extract transects for a single image
1435
+ def get_transects_for_image(image):
1436
+ image_date = image.get('Date_Filter')
1437
+ # Initialize an empty list to hold all transect FeatureCollections
1438
+ all_transects_for_image = ee.List([])
1439
+ # Looping through each line and processing
1440
+ for i, line in enumerate(lines):
1441
+ # Index line and name
1442
+ line_name = line_names[i]
1443
+ # Determine maxError based on image projection, used for geometry operations
1444
+ maxError = image.projection().nominalScale().divide(5)
1445
+ # Calculate effective distance interval
1446
+ length = line.length(maxError) # using maxError here ensures consistency with cutLines
1447
+ # Determine effective distance interval based on n_segments or dist_interval
1448
+ effective_dist_interval = ee.Algorithms.If(
1449
+ n_segments,
1450
+ length.divide(n_segments),
1451
+ dist_interval or 30 # Defaults to 30 if both are None
1452
+ )
1453
+ # Generate distances along the line(s) for segmentation
1454
+ distances = ee.List.sequence(0, length, effective_dist_interval)
1455
+ # Segmenting the line into smaller lines at the specified distances
1456
+ cut_lines_geoms = line.cutLines(distances, maxError).geometries()
1457
+ # Function to create features with distance attributes
1458
+ # Adjusted to ensure consistent return types
1459
+ def set_dist_attr(l):
1460
+ # l is a list: [geometry, distance]
1461
+ # Extracting geometry portion of line
1462
+ geom_segment = ee.Geometry(ee.List(l).get(0))
1463
+ # Extracting distance value for attribute
1464
+ distance = ee.Number(ee.List(l).get(1))
1465
+ ### Determine final geometry based on sampling method
1466
+ # If the sampling method is 'buffered_point',
1467
+ # create a buffered point feature at the centroid of each segment,
1468
+ # otherwise create a line feature
1469
+ final_feature = ee.Algorithms.If(
1470
+ ee.String(sampling_method).equals('buffered_point'),
1471
+ # True Case: Create the buffered point feature
1472
+ ee.Feature(
1473
+ geom_segment.centroid(maxError).buffer(point_buffer_radius),
1474
+ {'distance': distance}
1475
+ ),
1476
+ # False Case: Create the line segment feature
1477
+ ee.Feature(geom_segment, {'distance': distance})
1478
+ )
1479
+ # Return either the line segment feature or the buffered point feature
1480
+ return final_feature
1481
+ # Creating a FeatureCollection of the cut lines with distance attributes
1482
+ # Using map to apply the set_dist_attr function to each cut line geometry
1483
+ line_features = ee.FeatureCollection(cut_lines_geoms.zip(distances).map(set_dist_attr))
1484
+ # Reducing the image over the line features to get transect values
1485
+ transect_fc = image.reduceRegions(
1486
+ collection=line_features, reducer=ee_reducer, scale=scale
1487
+ )
1488
+ # Adding image date and line name properties to each feature
1489
+ def set_props(feature):
1490
+ return feature.set({'image_date': image_date, 'transect_name': line_name})
1491
+ # Append to the list of all transects for this image
1492
+ all_transects_for_image = all_transects_for_image.add(transect_fc.map(set_props))
1493
+ # Combine all transect FeatureCollections into a single FeatureCollection and flatten
1494
+ # Flatten is used to merge the list of FeatureCollections into one
1495
+ return ee.FeatureCollection(all_transects_for_image).flatten()
1496
+ # Map the function over the entire image collection and flatten the results
1497
+ results_fc = ee.FeatureCollection(self.collection.map(get_transects_for_image)).flatten()
1498
+ # Convert the results to a pandas DataFrame
1499
+ df = Sentinel1Collection.ee_to_df(results_fc, remove_geom=True)
1500
+ # Check if the DataFrame is empty
1501
+ if df.empty:
1502
+ print("Warning: No transect data was generated.")
1503
+ return {}
1504
+ # Initialize dictionary to hold output DataFrames for each transect
1505
+ output_dfs = {}
1506
+ # Loop through each unique transect name and create a pivot table
1507
+ for name in sorted(df['transect_name'].unique()):
1508
+ transect_df = df[df['transect_name'] == name]
1509
+ pivot_df = transect_df.pivot(index='distance', columns='image_date', values=reducer)
1510
+ pivot_df.columns.name = 'Date'
1511
+ output_dfs[name] = pivot_df
1512
+ # Optionally save each transect DataFrame to CSV
1513
+ if save_folder_path:
1514
+ for transect_name, transect_df in output_dfs.items():
1515
+ safe_filename = "".join(x for x in transect_name if x.isalnum() or x in "._-")
1516
+ file_path = f"{save_folder_path}{safe_filename}_transects.csv"
1517
+ transect_df.to_csv(file_path)
1518
+ print(f"Saved transect data to {file_path}")
1519
+
1520
+ return output_dfs
1521
+
1522
+ ### old, depreciated iterative client-side processing method ###
1523
+ elif processing_mode == 'iterative':
1524
+ if not save_folder_path:
1525
+ raise ValueError("`save_folder_path` is required for 'iterative' processing mode.")
1526
+
1527
+ image_collection_dates = self.dates
1528
+ for i, date in enumerate(image_collection_dates):
1529
+ try:
1530
+ print(f"Processing image {i+1}/{len(image_collection_dates)}: {date}")
1531
+ image = self.image_grab(i)
1532
+ transects_df = Sentinel1Collection.transect(
1533
+ image, lines, line_names, reducer, n_segments, dist_interval, to_pandas=True
1534
+ )
1535
+ transects_df.to_csv(f"{save_folder_path}{date}_transects.csv")
1536
+ print(f"{date}_transects saved to csv")
1537
+ except Exception as e:
1538
+ print(f"An error occurred while processing image {i+1}: {e}")
1539
+ else:
1540
+ raise ValueError("`processing_mode` must be 'iterative' or 'aggregated'.")
1244
1541
 
1245
1542
  @staticmethod
1246
1543
  def extract_zonal_stats_from_buffer(
@@ -1248,42 +1545,40 @@ class Sentinel1Collection:
1248
1545
  coordinates,
1249
1546
  buffer_size=1,
1250
1547
  reducer_type="mean",
1251
- scale=40,
1548
+ scale=10,
1252
1549
  tileScale=1,
1253
1550
  coordinate_names=None,
1254
1551
  ):
1255
1552
  """
1256
- Function to extract spatial statistics from an image for a list of coordinates, providing individual statistics for each location.
1553
+ Function to extract spatial statistics from an image for a list or single set of (long, lat) coordinates, providing individual statistics for each location.
1257
1554
  A radial buffer is applied around each coordinate to extract the statistics, which defaults to 1 meter.
1258
1555
  The function returns a pandas DataFrame with the statistics for each coordinate.
1259
1556
 
1557
+ NOTE: Be sure the coordinates are provided as longitude, latitude (x, y) tuples!
1558
+
1260
1559
  Args:
1261
- image (ee.Image): The image from which to extract the statistics. Must be a singleband image or else resulting values will all be zero!
1262
- coordinates (list): Single tuple or list of tuples with the decimal degrees coordinates in the format of (longitude, latitude) for which to extract the statistics. NOTE the format needs to be [(x1, y1), (x2, y2), ...].
1263
- buffer_size (int, optional): The radial buffer size around the coordinates in meters. Defaults to 1.
1264
- reducer_type (str, optional): The reducer type to use. Defaults to 'mean'. Options are 'mean', 'median', 'min', and 'max'.
1265
- scale (int, optional): The scale (pixel size) to use in meters. Defaults to 40.
1266
- tileScale (int, optional): The tile scale to use. Defaults to 1.
1267
- coordinate_names (list, optional): A list of strings with the names of the coordinates. Defaults to None.
1560
+ image (ee.Image): The image from which to extract statistics. Should be single-band.
1561
+ coordinates (list or tuple): A single (lon, lat) tuple or a list of (lon, lat) tuples.
1562
+ buffer_size (int, optional): The radial buffer size in meters. Defaults to 1.
1563
+ reducer_type (str, optional): The ee.Reducer to use ('mean', 'median', 'min', etc.). Defaults to 'mean'.
1564
+ scale (int, optional): The scale in meters for the reduction. Defaults to 10.
1565
+ tileScale (int, optional): The tile scale factor. Defaults to 1.
1566
+ coordinate_names (list, optional): A list of names for the coordinates.
1268
1567
 
1269
1568
  Returns:
1270
- pd.DataFrame: A pandas DataFrame with the statistics for each coordinate, each column name corresponds to the name of the coordinate feature (which may be blank if no names are supplied).
1569
+ pd.DataFrame: A pandas DataFrame with the image's 'Date_Filter' as the index and a
1570
+ column for each coordinate location.
1271
1571
  """
1272
-
1273
- # Check if coordinates is a single tuple and convert it to a list of tuples if necessary
1274
1572
  if isinstance(coordinates, tuple) and len(coordinates) == 2:
1275
1573
  coordinates = [coordinates]
1276
1574
  elif not (
1277
1575
  isinstance(coordinates, list)
1278
- and all(
1279
- isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates
1280
- )
1576
+ and all(isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates)
1281
1577
  ):
1282
1578
  raise ValueError(
1283
- "Coordinates must be a list of tuples with two elements each (latitude, longitude)."
1579
+ "Coordinates must be a list of tuples with two elements each (longitude, latitude)."
1284
1580
  )
1285
1581
 
1286
- # Check if coordinate_names is a list of strings
1287
1582
  if coordinate_names is not None:
1288
1583
  if not isinstance(coordinate_names, list) or not all(
1289
1584
  isinstance(name, str) for name in coordinate_names
@@ -1296,146 +1591,184 @@ class Sentinel1Collection:
1296
1591
  else:
1297
1592
  coordinate_names = [f"Location {i+1}" for i in range(len(coordinates))]
1298
1593
 
1299
- # Check if the image is a singleband image
1300
- def check_singleband(image):
1301
- band_count = image.bandNames().size()
1302
- return ee.Algorithms.If(band_count.eq(1), image, ee.Image.constant(0))
1594
+ image_date = image.get('Date_Filter')
1303
1595
 
1304
- # Check if the image is a singleband image
1305
- image = ee.Image(check_singleband(image))
1306
-
1307
- # Convert coordinates to ee.Geometry.Point, buffer them, and add label/name to feature
1308
1596
  points = [
1309
1597
  ee.Feature(
1310
- ee.Geometry.Point([coord[0], coord[1]]).buffer(buffer_size),
1311
- {"name": str(coordinate_names[i])},
1598
+ ee.Geometry.Point(coord).buffer(buffer_size),
1599
+ {"location_name": str(name)},
1312
1600
  )
1313
- for i, coord in enumerate(coordinates)
1601
+ for coord, name in zip(coordinates, coordinate_names)
1314
1602
  ]
1315
- # Create a feature collection from the buffered points
1316
1603
  features = ee.FeatureCollection(points)
1317
- # Reduce the image to the buffered points - handle different reducer types
1318
- if reducer_type == "mean":
1319
- img_stats = image.reduceRegions(
1320
- collection=features,
1321
- reducer=ee.Reducer.mean(),
1322
- scale=scale,
1323
- tileScale=tileScale,
1324
- )
1325
- mean_values = img_stats.getInfo()
1326
- means = []
1327
- names = []
1328
- for feature in mean_values["features"]:
1329
- names.append(feature["properties"]["name"])
1330
- means.append(feature["properties"]["mean"])
1331
- organized_values = pd.DataFrame([means], columns=names)
1332
- elif reducer_type == "median":
1333
- img_stats = image.reduceRegions(
1334
- collection=features,
1335
- reducer=ee.Reducer.median(),
1336
- scale=scale,
1337
- tileScale=tileScale,
1338
- )
1339
- median_values = img_stats.getInfo()
1340
- medians = []
1341
- names = []
1342
- for feature in median_values["features"]:
1343
- names.append(feature["properties"]["name"])
1344
- medians.append(feature["properties"]["median"])
1345
- organized_values = pd.DataFrame([medians], columns=names)
1346
- elif reducer_type == "min":
1347
- img_stats = image.reduceRegions(
1348
- collection=features,
1349
- reducer=ee.Reducer.min(),
1350
- scale=scale,
1351
- tileScale=tileScale,
1352
- )
1353
- min_values = img_stats.getInfo()
1354
- mins = []
1355
- names = []
1356
- for feature in min_values["features"]:
1357
- names.append(feature["properties"]["name"])
1358
- mins.append(feature["properties"]["min"])
1359
- organized_values = pd.DataFrame([mins], columns=names)
1360
- elif reducer_type == "max":
1361
- img_stats = image.reduceRegions(
1362
- collection=features,
1363
- reducer=ee.Reducer.max(),
1364
- scale=scale,
1365
- tileScale=tileScale,
1366
- )
1367
- max_values = img_stats.getInfo()
1368
- maxs = []
1369
- names = []
1370
- for feature in max_values["features"]:
1371
- names.append(feature["properties"]["name"])
1372
- maxs.append(feature["properties"]["max"])
1373
- organized_values = pd.DataFrame([maxs], columns=names)
1374
- else:
1375
- raise ValueError(
1376
- "reducer_type must be one of 'mean', 'median', 'min', or 'max'."
1377
- )
1378
- return organized_values
1604
+
1605
+ try:
1606
+ reducer = getattr(ee.Reducer, reducer_type)()
1607
+ except AttributeError:
1608
+ raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
1609
+
1610
+ stats_fc = image.reduceRegions(
1611
+ collection=features,
1612
+ reducer=reducer,
1613
+ scale=scale,
1614
+ tileScale=tileScale,
1615
+ )
1616
+
1617
+ df = Sentinel1Collection.ee_to_df(stats_fc, remove_geom=True)
1618
+
1619
+ if df.empty:
1620
+ print("Warning: No results returned. The points may not intersect the image.")
1621
+ empty_df = pd.DataFrame(columns=coordinate_names)
1622
+ empty_df.index.name = 'Date'
1623
+ return empty_df
1624
+
1625
+ if reducer_type not in df.columns:
1626
+ print(f"Warning: Reducer type '{reducer_type}' not found in results. Returning raw data.")
1627
+ return df
1628
+
1629
+ pivot_df = df.pivot(columns='location_name', values=reducer_type)
1630
+ pivot_df['Date'] = image_date.getInfo() # .getInfo() is needed here as it's a server object
1631
+ pivot_df = pivot_df.set_index('Date')
1632
+ return pivot_df
1379
1633
 
1380
1634
  def iterate_zonal_stats(
1381
1635
  self,
1382
- coordinates,
1383
- buffer_size=1,
1636
+ geometries,
1384
1637
  reducer_type="mean",
1385
- scale=40,
1638
+ scale=10,
1639
+ geometry_names=None,
1640
+ buffer_size=1,
1386
1641
  tileScale=1,
1387
- coordinate_names=None,
1388
- file_path=None,
1389
1642
  dates=None,
1643
+ file_path=None
1390
1644
  ):
1391
1645
  """
1392
- Function to iterate over a collection of images and extract spatial statistics for a list of coordinates (defaults to mean). Individual statistics are provided for each location.
1393
- A radial buffer is applied around each coordinate to extract the statistics, which defaults to 1 meter.
1646
+ Iterates over a collection of images and extracts spatial statistics (defaults to mean) for a given list of geometries or coordinates. Individual statistics are calculated for each geometry or coordinate provided.
1647
+ 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).
1394
1648
  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.
1395
1649
 
1396
- NOTE: The input RadGEEToolbox class object but be a collection of singleband images or else resulting values will all be zero!
1397
-
1398
1650
  Args:
1399
- coordinates (list): Single tuple or a list of tuples with the coordinates as decimal degrees in the format of (longitude, latitude) for which to extract the statistics. NOTE the format needs to be [(x1, y1), (x2, y2), ...].
1400
- buffer_size (int, optional): The radial buffer size in meters around the coordinates. Defaults to 1.
1401
- reducer_type (str, optional): The reducer type to use. Defaults to 'mean'. Options are 'mean', 'median', 'min', and 'max'.
1402
- scale (int, optional): The scale (pixel size) to use in meters. Defaults to 40.
1403
- tileScale (int, optional): The tile scale to use. Defaults to 1.
1404
- coordinate_names (list, optional): A list of strings with the names of the coordinates. Defaults to None.
1405
- file_path (str, optional): The file path to export the data to. Defaults to None. Ensure ".csv" is NOT included in the file name path.
1406
- dates (list, optional): A list of dates for which to extract the statistics. Defaults to None.
1651
+ 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)!
1652
+ 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.
1653
+ scale (int, optional): Pixel scale in meters for the reduction. Defaults to 10.
1654
+ geometry_names (list, optional): A list of string names for the geometries. If provided, must match the number of geometries. Defaults to None.
1655
+ buffer_size (int, optional): Radial buffer in meters around coordinates. Defaults to 1.
1656
+ tileScale (int, optional): A scaling factor to reduce aggregation tile size. Defaults to 1.
1657
+ dates (list, optional): A list of date strings ('YYYY-MM-DD') for filtering the collection, such that only images from these dates are included for zonal statistic retrieval. Defaults to None, which uses all dates in the collection.
1658
+ file_path (str, optional): File path to save the output CSV.
1407
1659
 
1408
1660
  Returns:
1409
- pd.DataFrame: A pandas DataFrame with the statistics for each coordinate and date, each row corresponds to a date and each column to a coordinate.
1410
- .csv file: Optionally exports the data to a table in .csv format. If file_path is None, the function returns the DataFrame - otherwise the function will only export the csv file.
1411
- """
1412
- img_collection = self
1413
- # Create empty DataFrame to accumulate results
1414
- accumulated_df = pd.DataFrame()
1415
- # Check if dates is None, if not use the dates provided
1416
- if dates is None:
1417
- dates = img_collection.dates
1418
- else:
1419
- dates = dates
1420
- # Iterate over the dates and extract the zonal statistics for each date
1421
- for date in dates:
1422
- image = img_collection.collection.filter(
1423
- ee.Filter.eq("Date_Filter", date)
1424
- ).first()
1425
- single_df = Sentinel1Collection.extract_zonal_stats_from_buffer(
1426
- image,
1427
- coordinates,
1428
- buffer_size=buffer_size,
1429
- reducer_type=reducer_type,
1430
- scale=scale,
1431
- tileScale=tileScale,
1432
- coordinate_names=coordinate_names,
1661
+ pd.DataFrame or None: A pandas DataFrame with dates as the index and coordinate names
1662
+ as columns. Returns None if using 'iterative' mode with file_path.
1663
+
1664
+ Raises:
1665
+ ValueError: If input parameters are invalid.
1666
+ TypeError: If geometries input type is unsupported.
1667
+ """
1668
+ img_collection_obj = self
1669
+ # Filter collection by dates if provided
1670
+ if dates:
1671
+ img_collection_obj = Sentinel1Collection(
1672
+ collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
1433
1673
  )
1434
- single_df["Date"] = date
1435
- single_df.set_index("Date", inplace=True)
1436
- accumulated_df = pd.concat([accumulated_df, single_df])
1437
- # Return the DataFrame or export the data to a .csv file
1438
- if file_path is None:
1439
- return accumulated_df
1674
+
1675
+ # Initialize variables
1676
+ features = None
1677
+ validated_coordinates = []
1678
+
1679
+ # Function to standardize feature names if no names are provided
1680
+ def set_standard_name(feature):
1681
+ has_geo_name = feature.get('geo_name')
1682
+ has_name = feature.get('name')
1683
+ has_index = feature.get('system:index')
1684
+ new_name = ee.Algorithms.If(
1685
+ has_geo_name, has_geo_name,
1686
+ ee.Algorithms.If(has_name, has_name,
1687
+ ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
1688
+ return feature.set({'geo_name': new_name})
1689
+
1690
+ if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
1691
+ features = ee.FeatureCollection(geometries)
1692
+ if geometry_names:
1693
+ print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
1694
+
1695
+ elif isinstance(geometries, ee.Geometry):
1696
+ name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
1697
+ features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
1698
+
1699
+ elif isinstance(geometries, list):
1700
+ if not geometries: # Handle empty list case
1701
+ raise ValueError("'geometries' list cannot be empty.")
1702
+
1703
+ # Case: List of coordinates
1704
+ if all(isinstance(i, tuple) for i in geometries):
1705
+ validated_coordinates = geometries
1706
+ if geometry_names is None:
1707
+ geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
1708
+ elif len(geometry_names) != len(validated_coordinates):
1709
+ raise ValueError("geometry_names must have the same length as the coordinates list.")
1710
+ points = [
1711
+ ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
1712
+ for coord, name in zip(validated_coordinates, geometry_names)
1713
+ ]
1714
+ features = ee.FeatureCollection(points)
1715
+
1716
+ # Case: List of Geometries
1717
+ elif all(isinstance(i, ee.Geometry) for i in geometries):
1718
+ if geometry_names is None:
1719
+ geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
1720
+ elif len(geometry_names) != len(geometries):
1721
+ raise ValueError("geometry_names must have the same length as the geometries list.")
1722
+ geom_features = [
1723
+ ee.Feature(geom).set({'geo_name': str(name)})
1724
+ for geom, name in zip(geometries, geometry_names)
1725
+ ]
1726
+ features = ee.FeatureCollection(geom_features)
1727
+
1728
+ else:
1729
+ raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
1730
+
1731
+ elif isinstance(geometries, tuple) and len(geometries) == 2:
1732
+ name = geometry_names[0] if geometry_names else 'Location_1'
1733
+ features = ee.FeatureCollection([
1734
+ ee.Feature(ee.Geometry.Point(geometries).buffer(buffer_size), {'geo_name': name})
1735
+ ])
1440
1736
  else:
1441
- return accumulated_df.to_csv(f"{file_path}.csv")
1737
+ raise TypeError("Unsupported type for 'geometries'.")
1738
+
1739
+ features = features.map(set_standard_name)
1740
+
1741
+ try:
1742
+ reducer = getattr(ee.Reducer, reducer_type)()
1743
+ except AttributeError:
1744
+ raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
1745
+
1746
+ def calculate_stats_for_image(image):
1747
+ image_date = image.get('Date_Filter')
1748
+ stats_fc = image.reduceRegions(
1749
+ collection=features, reducer=reducer, scale=scale, tileScale=tileScale
1750
+ )
1751
+ return stats_fc.map(lambda f: f.set('image_date', image_date))
1752
+
1753
+ results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
1754
+ df = Sentinel1Collection.ee_to_df(results_fc, remove_geom=True)
1755
+
1756
+ # Checking for issues
1757
+ if df.empty:
1758
+ 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.")
1759
+ return df
1760
+ if reducer_type not in df.columns:
1761
+ print(f"Warning: Reducer '{reducer_type}' not found in results.")
1762
+ return df
1763
+
1764
+ # Reshape DataFrame to have dates as index and geometry names as columns
1765
+ pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
1766
+ pivot_df.index.name = 'Date'
1767
+ if file_path:
1768
+ # Check if file_path ends with .csv and remove it if so for consistency
1769
+ if file_path.endswith('.csv'):
1770
+ file_path = file_path[:-4]
1771
+ pivot_df.to_csv(f"{file_path}.csv")
1772
+ print(f"Zonal stats saved to {file_path}.csv")
1773
+ return
1774
+ return pivot_df