RadGEEToolbox 1.7.4__py3-none-any.whl → 1.7.6__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.
@@ -2491,7 +2491,8 @@ class Sentinel1Collection:
2491
2491
  "resolution_meters",
2492
2492
  "transmitterReceiverPolarisation",
2493
2493
  "system:time_start",
2494
- "crs"
2494
+ "crs",
2495
+ "Date_Filter"
2495
2496
  ]
2496
2497
 
2497
2498
  # Return mosaic with properties set
@@ -2731,200 +2732,197 @@ class Sentinel1Collection:
2731
2732
  lines,
2732
2733
  line_names,
2733
2734
  reducer="mean",
2734
- dist_interval= 10,
2735
+ dist_interval=30,
2735
2736
  n_segments=None,
2736
2737
  scale=10,
2737
2738
  processing_mode='aggregated',
2738
2739
  save_folder_path=None,
2739
2740
  sampling_method='line',
2740
- point_buffer_radius=5
2741
+ point_buffer_radius=15,
2742
+ batch_size=10
2741
2743
  ):
2742
2744
  """
2743
- Computes and returns pixel values along transects for each image in a collection.
2744
-
2745
- This iterative function generates time-series data along one or more lines, and
2746
- supports two different geometric sampling methods ('line' and 'buffered_point')
2747
- for maximum flexibility and performance.
2748
-
2749
- There are two processing modes available, aggregated and iterative:
2750
- - 'aggregated' (default; suggested): Fast, server-side processing. Fetches all results
2751
- in a single request. Highly recommended. Returns a dictionary of pandas DataFrames.
2752
- - 'iterative': Slower, client-side loop that processes one image at a time.
2753
- Kept for backward compatibility (effectively depreciated). Returns None and saves individual CSVs.
2754
- This method is not recommended unless absolutely necessary, as it is less efficient and may be subject to client-side timeouts.
2755
-
2745
+ Computes and returns pixel values along transects. Provide a list of ee.Geometry.LineString objects and corresponding names, and the function will compute the specified reducer value
2746
+ at regular intervals along each line for all images in the collection. Use `dist_interval` or `n_segments` to control sampling resolution. The user can choose between 'aggregated' mode (returns a dictionary of DataFrames) or 'iterative' mode (saves individual CSVs for each transect).
2747
+ Alter `sampling_method` to sample directly along the line or via buffered points along the line. Buffered points can help capture more representative pixel values in heterogeneous landscapes, and the buffer radius can be adjusted via `point_buffer_radius`.
2748
+
2756
2749
  Args:
2757
- lines (list): A list of one or more ee.Geometry.LineString objects that
2758
- define the transects.
2759
- line_names (list): A list of string names for each transect. The length
2760
- of this list must match the length of the `lines` list.
2761
- reducer (str, optional): The name of the ee.Reducer to apply at each
2762
- transect point (e.g., 'mean', 'median', 'first'). Defaults to 'mean'.
2763
- dist_interval (float, optional): The distance interval in meters for
2764
- sampling points along each transect. Will be overridden if `n_segments` is provided.
2765
- Defaults to 10. Recommended to increase this value when using the
2766
- 'line' processing method, or else you may get blank rows.
2767
- n_segments (int, optional): The number of equal-length segments to split
2768
- each transect line into for sampling. This parameter overrides `dist_interval`.
2769
- Defaults to None.
2770
- scale (int, optional): The nominal scale in meters for the reduction,
2771
- which should typically match the pixel resolution of the imagery.
2772
- Defaults to 10.
2773
- processing_mode (str, optional): The method for processing the collection.
2774
- - 'aggregated' (default): Fast, server-side processing. Fetches all
2775
- results in a single request. Highly recommended. Returns a dictionary
2776
- of pandas DataFrames.
2777
- - 'iterative': Slower, client-side loop that processes one image at a
2778
- time. Kept for backward compatibility. Returns None and saves
2779
- individual CSVs.
2780
- save_folder_path (str, optional): If provided, the function will save the
2781
- resulting transect data to CSV files. The behavior depends on the
2782
- `processing_mode`:
2783
- - In 'aggregated' mode, one CSV is saved for each transect,
2784
- containing all dates. (e.g., 'MyTransect_transects.csv').
2785
- - In 'iterative' mode, one CSV is saved for each date,
2786
- containing all transects. (e.g., '2022-06-15_transects.csv').
2787
- sampling_method (str, optional): The geometric method used for sampling.
2788
- - 'line' (default): Reduces all pixels intersecting each small line
2789
- segment. This can be unreliable and produce blank rows if
2790
- `dist_interval` is too small relative to the `scale`.
2791
- - 'buffered_point': Reduces all pixels within a buffer around the
2792
- midpoint of each line segment. This method is more robust and
2793
- reliably avoids blank rows, but may not reduce all pixels along a line segment.
2794
- point_buffer_radius (int, optional): The radius in meters for the buffer
2795
- when `sampling_method` is 'buffered_point'. Defaults to 5.
2750
+ lines (list): List of ee.Geometry.LineString objects.
2751
+ line_names (list): List of string names for each transect.
2752
+ reducer (str, optional): Reducer name. Defaults to 'mean'.
2753
+ dist_interval (float, optional): Distance interval in meters. Defaults to 30.
2754
+ n_segments (int, optional): Number of segments (overrides dist_interval).
2755
+ scale (int, optional): Scale in meters. Defaults to 10.
2756
+ processing_mode (str, optional): 'aggregated' or 'iterative'.
2757
+ save_folder_path (str, optional): Path to save CSVs.
2758
+ sampling_method (str, optional): 'line' or 'buffered_point'.
2759
+ point_buffer_radius (int, optional): Buffer radius if using 'buffered_point'.
2760
+ batch_size (int, optional): Images per request in 'aggregated' mode. Defaults to 10. Lower the value if you encounter a 'Too many aggregations' error.
2796
2761
 
2797
2762
  Returns:
2798
- dict or None:
2799
- - If `processing_mode` is 'aggregated', returns a dictionary where each
2800
- key is a transect name and each value is a pandas DataFrame. In the
2801
- DataFrame, the index is the distance along the transect and each
2802
- column represents an image date. Optionally saves CSV files if
2803
- `save_folder_path` is provided.
2804
- - If `processing_mode` is 'iterative', returns None as it saves
2805
- files directly.
2806
-
2807
- Raises:
2808
- ValueError: If `lines` and `line_names` have different lengths, or if
2809
- an unknown reducer or processing mode is specified.
2763
+ dict or None: Dictionary of DataFrames (aggregated) or None (iterative).
2810
2764
  """
2811
- # Validating inputs
2812
2765
  if len(lines) != len(line_names):
2813
2766
  raise ValueError("'lines' and 'line_names' must have the same number of elements.")
2814
- ### Current, server-side processing method ###
2767
+
2768
+ first_img = self.collection.first()
2769
+ bands = first_img.bandNames().getInfo()
2770
+ is_multiband = len(bands) > 1
2771
+
2772
+ # Setup robust dictionary for handling masked/zero values
2773
+ default_val = -9999
2774
+ dummy_dict = ee.Dictionary.fromLists(bands, ee.List.repeat(default_val, len(bands)))
2775
+
2776
+ if is_multiband:
2777
+ reducer_cols = [f"{b}_{reducer}" for b in bands]
2778
+ clean_names = bands
2779
+ rename_keys = bands
2780
+ rename_vals = reducer_cols
2781
+ else:
2782
+ reducer_cols = [reducer]
2783
+ clean_names = [bands[0]]
2784
+ rename_keys = bands
2785
+ rename_vals = reducer_cols
2786
+
2787
+ print("Pre-computing transect geometries from input LineString(s)...")
2788
+
2789
+ master_transect_fc = ee.FeatureCollection([])
2790
+ geom_error = 1.0
2791
+
2792
+ for i, line in enumerate(lines):
2793
+ line_name = line_names[i]
2794
+ length = line.length(geom_error)
2795
+
2796
+ eff_interval = length.divide(n_segments) if n_segments else dist_interval
2797
+
2798
+ distances = ee.List.sequence(0, length, eff_interval)
2799
+ cut_lines = line.cutLines(distances, geom_error).geometries()
2800
+
2801
+ def create_feature(l):
2802
+ geom = ee.Geometry(ee.List(l).get(0))
2803
+ dist = ee.Number(ee.List(l).get(1))
2804
+
2805
+ final_geom = ee.Algorithms.If(
2806
+ ee.String(sampling_method).equals('buffered_point'),
2807
+ geom.centroid(geom_error).buffer(point_buffer_radius),
2808
+ geom
2809
+ )
2810
+
2811
+ return ee.Feature(ee.Geometry(final_geom), {
2812
+ 'transect_name': line_name,
2813
+ 'distance': dist
2814
+ })
2815
+
2816
+ line_fc = ee.FeatureCollection(cut_lines.zip(distances).map(create_feature))
2817
+ master_transect_fc = master_transect_fc.merge(line_fc)
2818
+
2819
+ try:
2820
+ ee_reducer = getattr(ee.Reducer, reducer)()
2821
+ except AttributeError:
2822
+ raise ValueError(f"Unknown reducer: '{reducer}'.")
2823
+
2824
+ def process_image(image):
2825
+ date_val = image.get('Date_Filter')
2826
+
2827
+ # Map over points (Slower but Robust)
2828
+ def reduce_point(f):
2829
+ stats = image.reduceRegion(
2830
+ reducer=ee_reducer,
2831
+ geometry=f.geometry(),
2832
+ scale=scale,
2833
+ maxPixels=1e13
2834
+ )
2835
+ # Combine with defaults (preserves 0, handles masked)
2836
+ safe_stats = dummy_dict.combine(stats, overwrite=True)
2837
+ # Rename keys to match expected outputs (e.g. 'ndvi' -> 'ndvi_mean')
2838
+ final_stats = safe_stats.rename(rename_keys, rename_vals)
2839
+
2840
+ return f.set(final_stats).set({'image_date': date_val})
2841
+
2842
+ return master_transect_fc.map(reduce_point)
2843
+
2844
+ export_cols = ['transect_name', 'distance', 'image_date'] + reducer_cols
2845
+
2815
2846
  if processing_mode == 'aggregated':
2816
- # Validating reducer type
2817
- try:
2818
- ee_reducer = getattr(ee.Reducer, reducer)()
2819
- except AttributeError:
2820
- raise ValueError(f"Unknown reducer: '{reducer}'.")
2821
- ### Function to extract transects for a single image
2822
- def get_transects_for_image(image):
2823
- image_date = image.get('Date_Filter')
2824
- # Initialize an empty list to hold all transect FeatureCollections
2825
- all_transects_for_image = ee.List([])
2826
- # Looping through each line and processing
2827
- for i, line in enumerate(lines):
2828
- # Index line and name
2829
- line_name = line_names[i]
2830
- # Determine maxError based on image projection, used for geometry operations
2831
- maxError = image.projection().nominalScale().divide(5)
2832
- # Calculate effective distance interval
2833
- length = line.length(maxError) # using maxError here ensures consistency with cutLines
2834
- # Determine effective distance interval based on n_segments or dist_interval
2835
- effective_dist_interval = ee.Algorithms.If(
2836
- n_segments,
2837
- length.divide(n_segments),
2838
- dist_interval or 30 # Defaults to 30 if both are None
2839
- )
2840
- # Generate distances along the line(s) for segmentation
2841
- distances = ee.List.sequence(0, length, effective_dist_interval)
2842
- # Segmenting the line into smaller lines at the specified distances
2843
- cut_lines_geoms = line.cutLines(distances, maxError).geometries()
2844
- # Function to create features with distance attributes
2845
- # Adjusted to ensure consistent return types
2846
- def set_dist_attr(l):
2847
- # l is a list: [geometry, distance]
2848
- # Extracting geometry portion of line
2849
- geom_segment = ee.Geometry(ee.List(l).get(0))
2850
- # Extracting distance value for attribute
2851
- distance = ee.Number(ee.List(l).get(1))
2852
- ### Determine final geometry based on sampling method
2853
- # If the sampling method is 'buffered_point',
2854
- # create a buffered point feature at the centroid of each segment,
2855
- # otherwise create a line feature
2856
- final_feature = ee.Algorithms.If(
2857
- ee.String(sampling_method).equals('buffered_point'),
2858
- # True Case: Create the buffered point feature
2859
- ee.Feature(
2860
- geom_segment.centroid(maxError).buffer(point_buffer_radius),
2861
- {'distance': distance}
2862
- ),
2863
- # False Case: Create the line segment feature
2864
- ee.Feature(geom_segment, {'distance': distance})
2865
- )
2866
- # Return either the line segment feature or the buffered point feature
2867
- return final_feature
2868
- # Creating a FeatureCollection of the cut lines with distance attributes
2869
- # Using map to apply the set_dist_attr function to each cut line geometry
2870
- line_features = ee.FeatureCollection(cut_lines_geoms.zip(distances).map(set_dist_attr))
2871
- # Reducing the image over the line features to get transect values
2872
- transect_fc = image.reduceRegions(
2873
- collection=line_features, reducer=ee_reducer, scale=scale
2874
- )
2875
- # Adding image date and line name properties to each feature
2876
- def set_props(feature):
2877
- return feature.set({'image_date': image_date, 'transect_name': line_name})
2878
- # Append to the list of all transects for this image
2879
- all_transects_for_image = all_transects_for_image.add(transect_fc.map(set_props))
2880
- # Combine all transect FeatureCollections into a single FeatureCollection and flatten
2881
- # Flatten is used to merge the list of FeatureCollections into one
2882
- return ee.FeatureCollection(all_transects_for_image).flatten()
2883
- # Map the function over the entire image collection and flatten the results
2884
- results_fc = ee.FeatureCollection(self.collection.map(get_transects_for_image)).flatten()
2885
- # Convert the results to a pandas DataFrame
2886
- df = Sentinel1Collection.ee_to_df(results_fc, remove_geom=True)
2887
- # Check if the DataFrame is empty
2888
- if df.empty:
2889
- print("Warning: No transect data was generated.")
2847
+ collection_size = self.collection.size().getInfo()
2848
+ print(f"Starting batch process of {collection_size} images...")
2849
+
2850
+ dfs = []
2851
+ for i in range(0, collection_size, batch_size):
2852
+ print(f" Processing image {i} to {min(i + batch_size, collection_size)}...")
2853
+
2854
+ batch_col = ee.ImageCollection(self.collection.toList(batch_size, i))
2855
+ results_fc = batch_col.map(process_image).flatten()
2856
+
2857
+ # Dynamic Class Call for ee_to_df
2858
+ df_batch = self.__class__.ee_to_df(results_fc, columns=export_cols, remove_geom=True)
2859
+
2860
+ if not df_batch.empty:
2861
+ dfs.append(df_batch)
2862
+
2863
+ if not dfs:
2864
+ print("Warning: No transect data generated.")
2890
2865
  return {}
2891
- # Initialize dictionary to hold output DataFrames for each transect
2866
+
2867
+ df = pd.concat(dfs, ignore_index=True)
2868
+
2869
+ # Post-Process & Split
2892
2870
  output_dfs = {}
2893
- # Loop through each unique transect name and create a pivot table
2871
+ for col in reducer_cols:
2872
+ df[col] = pd.to_numeric(df[col], errors='coerce')
2873
+ df[col] = df[col].replace(-9999, np.nan)
2874
+
2894
2875
  for name in sorted(df['transect_name'].unique()):
2895
- transect_df = df[df['transect_name'] == name]
2896
- pivot_df = transect_df.pivot(index='distance', columns='image_date', values=reducer)
2897
- pivot_df.columns.name = 'Date'
2898
- output_dfs[name] = pivot_df
2899
- # Optionally save each transect DataFrame to CSV
2900
- if save_folder_path:
2901
- for transect_name, transect_df in output_dfs.items():
2902
- safe_filename = "".join(x for x in transect_name if x.isalnum() or x in "._-")
2903
- file_path = f"{save_folder_path}{safe_filename}_transects.csv"
2904
- transect_df.to_csv(file_path)
2905
- print(f"Saved transect data to {file_path}")
2906
-
2876
+ line_df = df[df['transect_name'] == name]
2877
+
2878
+ for raw_col, band_name in zip(reducer_cols, clean_names):
2879
+ try:
2880
+ # Safety drop for duplicates
2881
+ line_df_clean = line_df.drop_duplicates(subset=['distance', 'image_date'])
2882
+
2883
+ pivot = line_df_clean.pivot(index='distance', columns='image_date', values=raw_col)
2884
+ pivot.columns.name = 'Date'
2885
+ key = f"{name}_{band_name}"
2886
+ output_dfs[key] = pivot
2887
+
2888
+ if save_folder_path:
2889
+ safe_key = "".join(x for x in key if x.isalnum() or x in "._-")
2890
+ fname = f"{save_folder_path}{safe_key}_transects.csv"
2891
+ pivot.to_csv(fname)
2892
+ print(f"Saved: {fname}")
2893
+ except Exception as e:
2894
+ print(f"Skipping pivot for {name}/{band_name}: {e}")
2895
+
2907
2896
  return output_dfs
2908
2897
 
2909
- ### old, depreciated iterative client-side processing method ###
2910
2898
  elif processing_mode == 'iterative':
2911
2899
  if not save_folder_path:
2912
- raise ValueError("`save_folder_path` is required for 'iterative' processing mode.")
2900
+ raise ValueError("save_folder_path is required for iterative mode.")
2913
2901
 
2914
2902
  image_collection_dates = self.dates
2915
2903
  for i, date in enumerate(image_collection_dates):
2916
2904
  try:
2917
2905
  print(f"Processing image {i+1}/{len(image_collection_dates)}: {date}")
2918
- image = self.image_grab(i)
2919
- transects_df = Sentinel1Collection.transect(
2920
- image, lines, line_names, reducer, n_segments, dist_interval, to_pandas=True
2921
- )
2922
- transects_df.to_csv(f"{save_folder_path}{date}_transects.csv")
2923
- print(f"{date}_transects saved to csv")
2906
+ image_list = self.collection.toList(self.collection.size())
2907
+ image = ee.Image(image_list.get(i))
2908
+
2909
+ fc_result = process_image(image)
2910
+ df = self.__class__.ee_to_df(fc_result, columns=export_cols, remove_geom=True)
2911
+
2912
+ if not df.empty:
2913
+ for col in reducer_cols:
2914
+ df[col] = pd.to_numeric(df[col], errors='coerce')
2915
+ df[col] = df[col].replace(-9999, np.nan)
2916
+
2917
+ fname = f"{save_folder_path}{date}_transects.csv"
2918
+ df.to_csv(fname, index=False)
2919
+ print(f"Saved: {fname}")
2920
+ else:
2921
+ print(f"Skipping {date}: No data.")
2924
2922
  except Exception as e:
2925
- print(f"An error occurred while processing image {i+1}: {e}")
2923
+ print(f"Error processing {date}: {e}")
2926
2924
  else:
2927
- raise ValueError("`processing_mode` must be 'iterative' or 'aggregated'.")
2925
+ raise ValueError("processing_mode must be 'iterative' or 'aggregated'.")
2928
2926
 
2929
2927
  @staticmethod
2930
2928
  def extract_zonal_stats_from_buffer(
@@ -3212,6 +3210,386 @@ class Sentinel1Collection:
3212
3210
  print(f"Zonal stats saved to {file_path}.csv")
3213
3211
  return
3214
3212
  return pivot_df
3213
+
3214
+ def multiband_zonal_stats(
3215
+ self,
3216
+ geometry,
3217
+ bands,
3218
+ reducer_types,
3219
+ scale=30,
3220
+ geometry_name='geom',
3221
+ dates=None,
3222
+ include_area=False,
3223
+ file_path=None
3224
+ ):
3225
+ """
3226
+ Calculates zonal statistics for multiple bands over a single geometry for each image in the collection.
3227
+ Allows for specifying different reducers for different bands. Optionally includes the geometry area.
3228
+
3229
+ Args:
3230
+ geometry (ee.Geometry or ee.Feature): The single geometry to calculate statistics for.
3231
+ bands (list of str): A list of band names to include in the analysis.
3232
+ reducer_types (str or list of str): A single reducer name (e.g., 'mean') to apply to all bands,
3233
+ or a list of reducer names matching the length of the 'bands' list to apply specific reducers
3234
+ to specific bands.
3235
+ scale (int, optional): The scale in meters for the reduction. Defaults to 30.
3236
+ geometry_name (str, optional): A name for the geometry, used in column naming. Defaults to 'geom'.
3237
+ dates (list of str, optional): A list of date strings ('YYYY-MM-DD') to filter the collection.
3238
+ Defaults to None (processes all images).
3239
+ include_area (bool, optional): If True, adds a column with the area of the geometry in square meters.
3240
+ Defaults to False.
3241
+ file_path (str, optional): If provided, saves the resulting DataFrame to a CSV file at this path.
3242
+
3243
+ Returns:
3244
+ pd.DataFrame: A pandas DataFrame indexed by Date, with columns named as '{band}_{geometry_name}_{reducer}'.
3245
+ """
3246
+ # 1. Input Validation and Setup
3247
+ if not isinstance(geometry, (ee.Geometry, ee.Feature)):
3248
+ raise ValueError("The `geometry` argument must be an ee.Geometry or ee.Feature.")
3249
+
3250
+ region = geometry.geometry() if isinstance(geometry, ee.Feature) else geometry
3251
+
3252
+ if isinstance(bands, str):
3253
+ bands = [bands]
3254
+ if not isinstance(bands, list):
3255
+ raise ValueError("The `bands` argument must be a string or a list of strings.")
3256
+
3257
+ # Handle reducer_types (str vs list)
3258
+ if isinstance(reducer_types, str):
3259
+ reducers_list = [reducer_types] * len(bands)
3260
+ elif isinstance(reducer_types, list):
3261
+ if len(reducer_types) != len(bands):
3262
+ raise ValueError("If `reducer_types` is a list, it must have the same length as `bands`.")
3263
+ reducers_list = reducer_types
3264
+ else:
3265
+ raise ValueError("`reducer_types` must be a string or a list of strings.")
3266
+
3267
+ # 2. Filter Collection
3268
+ processing_col = self.collection
3269
+
3270
+ if dates:
3271
+ processing_col = processing_col.filter(ee.Filter.inList('Date_Filter', dates))
3272
+
3273
+ processing_col = processing_col.select(bands)
3274
+
3275
+ # 3. Pre-calculate Area (if requested)
3276
+ area_val = None
3277
+ area_col_name = f"{geometry_name}_area_m2"
3278
+ if include_area:
3279
+ # Calculate geodesic area in square meters with maxError of 1m
3280
+ area_val = region.area(1)
3281
+
3282
+ # 4. Define the Reduction Logic
3283
+ def calculate_multiband_stats(image):
3284
+ # Base feature with date property
3285
+ date_val = image.get('Date_Filter')
3286
+ feature = ee.Feature(None, {'Date': date_val})
3287
+
3288
+ # If requested, add the static area value to every feature
3289
+ if include_area:
3290
+ feature = feature.set(area_col_name, area_val)
3291
+
3292
+ unique_reducers = list(set(reducers_list))
3293
+
3294
+ # OPTIMIZED PATH: Single reducer type for all bands
3295
+ if len(unique_reducers) == 1:
3296
+ r_type = unique_reducers[0]
3297
+ try:
3298
+ reducer = getattr(ee.Reducer, r_type)()
3299
+ except AttributeError:
3300
+ reducer = ee.Reducer.mean()
3301
+
3302
+ stats = image.reduceRegion(
3303
+ reducer=reducer,
3304
+ geometry=region,
3305
+ scale=scale,
3306
+ maxPixels=1e13
3307
+ )
3308
+
3309
+ for band in bands:
3310
+ col_name = f"{band}_{geometry_name}_{r_type}"
3311
+ val = stats.get(band)
3312
+ feature = feature.set(col_name, val)
3313
+
3314
+ # ITERATIVE PATH: Different reducers for different bands
3315
+ else:
3316
+ for band, r_type in zip(bands, reducers_list):
3317
+ try:
3318
+ reducer = getattr(ee.Reducer, r_type)()
3319
+ except AttributeError:
3320
+ reducer = ee.Reducer.mean()
3321
+
3322
+ stats = image.select(band).reduceRegion(
3323
+ reducer=reducer,
3324
+ geometry=region,
3325
+ scale=scale,
3326
+ maxPixels=1e13
3327
+ )
3328
+
3329
+ val = stats.get(band)
3330
+ col_name = f"{band}_{geometry_name}_{r_type}"
3331
+ feature = feature.set(col_name, val)
3332
+
3333
+ return feature
3334
+
3335
+ # 5. Execute Server-Side Mapping (with explicit Cast)
3336
+ results_fc = ee.FeatureCollection(processing_col.map(calculate_multiband_stats))
3337
+
3338
+ # 6. Client-Side Conversion
3339
+ try:
3340
+ df = Sentinel1Collection.ee_to_df(results_fc, remove_geom=True)
3341
+ except Exception as e:
3342
+ raise RuntimeError(f"Failed to convert Earth Engine results to DataFrame. Error: {e}")
3343
+
3344
+ if df.empty:
3345
+ print("Warning: No results returned. Check if the geometry intersects the imagery or if dates are valid.")
3346
+ return pd.DataFrame()
3347
+
3348
+ # 7. Formatting & Reordering
3349
+ if 'Date' in df.columns:
3350
+ df['Date'] = pd.to_datetime(df['Date'])
3351
+ df = df.sort_values('Date').set_index('Date')
3352
+
3353
+ # Construct the expected column names in the exact order of the input lists
3354
+ expected_order = [f"{band}_{geometry_name}_{r_type}" for band, r_type in zip(bands, reducers_list)]
3355
+
3356
+ # If area was included, append it to the END of the list
3357
+ if include_area:
3358
+ expected_order.append(area_col_name)
3359
+
3360
+ # Reindex the DataFrame to match this order.
3361
+ existing_cols = [c for c in expected_order if c in df.columns]
3362
+ df = df[existing_cols]
3363
+
3364
+ # 8. Export (Optional)
3365
+ if file_path:
3366
+ if not file_path.lower().endswith('.csv'):
3367
+ file_path += '.csv'
3368
+ try:
3369
+ df.to_csv(file_path)
3370
+ print(f"Multiband zonal stats saved to {file_path}")
3371
+ except Exception as e:
3372
+ print(f"Error saving file to {file_path}: {e}")
3373
+
3374
+ return df
3375
+
3376
+ def sample(
3377
+ self,
3378
+ locations,
3379
+ band=None,
3380
+ scale=None,
3381
+ location_names=None,
3382
+ dates=None,
3383
+ file_path=None,
3384
+ tileScale=1
3385
+ ):
3386
+ """
3387
+ Extracts time-series pixel values for a list of locations.
3388
+
3389
+
3390
+ Args:
3391
+ locations (list, tuple, ee.Geometry, or ee.FeatureCollection): Input points.
3392
+ band (str, optional): The name of the band to sample. Defaults to the first band.
3393
+ scale (int, optional): Scale in meters. Defaults to 30 if None.
3394
+ location_names (list of str, optional): Custom names for locations.
3395
+ dates (list, optional): Date filter ['YYYY-MM-DD'].
3396
+ file_path (str, optional): CSV export path.
3397
+ tileScale (int, optional): Aggregation tile scale. Defaults to 1.
3398
+
3399
+ Returns:
3400
+ pd.DataFrame (or CSV if file_path is provided): DataFrame indexed by Date, columns by Location.
3401
+ """
3402
+ col = self.collection
3403
+ if dates:
3404
+ col = col.filter(ee.Filter.inList('Date_Filter', dates))
3405
+
3406
+ first_img = col.first()
3407
+ available_bands = first_img.bandNames().getInfo()
3408
+
3409
+ if band:
3410
+ if band not in available_bands:
3411
+ raise ValueError(f"Band '{band}' not found. Available: {available_bands}")
3412
+ target_band = band
3413
+ else:
3414
+ target_band = available_bands[0]
3415
+
3416
+ processing_col = col.select([target_band])
3417
+
3418
+ def set_name(f):
3419
+ name = ee.Algorithms.If(
3420
+ f.get('geo_name'), f.get('geo_name'),
3421
+ ee.Algorithms.If(f.get('name'), f.get('name'),
3422
+ ee.Algorithms.If(f.get('system:index'), f.get('system:index'), 'unnamed'))
3423
+ )
3424
+ return f.set('geo_name', name)
3425
+
3426
+ if isinstance(locations, (ee.FeatureCollection, ee.Feature)):
3427
+ features = ee.FeatureCollection(locations)
3428
+ elif isinstance(locations, ee.Geometry):
3429
+ lbl = location_names[0] if (location_names and location_names[0]) else 'Point_1'
3430
+ features = ee.FeatureCollection([ee.Feature(locations).set('geo_name', lbl)])
3431
+ elif isinstance(locations, tuple) and len(locations) == 2:
3432
+ lbl = location_names[0] if location_names else 'Location_1'
3433
+ features = ee.FeatureCollection([ee.Feature(ee.Geometry.Point(locations), {'geo_name': lbl})])
3434
+ elif isinstance(locations, list):
3435
+ if all(isinstance(i, tuple) for i in locations):
3436
+ names = location_names if location_names else [f"Loc_{i+1}" for i in range(len(locations))]
3437
+ features = ee.FeatureCollection([
3438
+ ee.Feature(ee.Geometry.Point(p), {'geo_name': str(n)}) for p, n in zip(locations, names)
3439
+ ])
3440
+ elif all(isinstance(i, ee.Geometry) for i in locations):
3441
+ names = location_names if location_names else [f"Geom_{i+1}" for i in range(len(locations))]
3442
+ features = ee.FeatureCollection([
3443
+ ee.Feature(g, {'geo_name': str(n)}) for g, n in zip(locations, names)
3444
+ ])
3445
+ else:
3446
+ raise ValueError("List must contain (lon, lat) tuples or ee.Geometry objects.")
3447
+ else:
3448
+ raise TypeError("Invalid locations input.")
3449
+
3450
+ features = features.map(set_name)
3451
+
3452
+
3453
+ def sample_image(img):
3454
+ date = img.get('Date_Filter')
3455
+ use_scale = scale if scale is not None else 30
3456
+
3457
+
3458
+ default_dict = ee.Dictionary({target_band: -9999})
3459
+
3460
+ def extract_point(f):
3461
+ stats = img.reduceRegion(
3462
+ reducer=ee.Reducer.first(),
3463
+ geometry=f.geometry(),
3464
+ scale=use_scale,
3465
+ tileScale=tileScale
3466
+ )
3467
+
3468
+ # Combine dictionaries.
3469
+ # If stats has 'target_band' (even if 0), it overwrites -9999.
3470
+ # If stats is empty (masked), -9999 remains.
3471
+ safe_stats = default_dict.combine(stats, overwrite=True)
3472
+ val = safe_stats.get(target_band)
3473
+
3474
+ return f.set({
3475
+ target_band: val,
3476
+ 'image_date': date
3477
+ })
3478
+
3479
+ return features.map(extract_point)
3480
+
3481
+ # Flatten the results
3482
+ flat_results = processing_col.map(sample_image).flatten()
3483
+
3484
+ df = Sentinel1Collection.ee_to_df(
3485
+ flat_results,
3486
+ columns=['image_date', 'geo_name', target_band],
3487
+ remove_geom=True
3488
+ )
3489
+
3490
+ if df.empty:
3491
+ print("Warning: No data returned.")
3492
+ return pd.DataFrame()
3493
+
3494
+ # 6. Clean and Pivot
3495
+ df[target_band] = pd.to_numeric(df[target_band], errors='coerce')
3496
+
3497
+ # Filter out ONLY the sentinel value (-9999), preserving 0.
3498
+ df = df[df[target_band] != -9999]
3499
+
3500
+ if df.empty:
3501
+ print(f"Warning: All data points were masked (NoData) for band '{target_band}'.")
3502
+ return pd.DataFrame()
3503
+
3504
+ pivot_df = df.pivot(index='image_date', columns='geo_name', values=target_band)
3505
+ pivot_df.index.name = 'Date'
3506
+ pivot_df.columns.name = None
3507
+ pivot_df = pivot_df.reset_index()
3508
+
3509
+ if file_path:
3510
+ if not file_path.lower().endswith('.csv'):
3511
+ file_path += '.csv'
3512
+ pivot_df.to_csv(file_path, index=False)
3513
+ print(f"Sampled data saved to {file_path}")
3514
+ return None
3515
+
3516
+ return pivot_df
3517
+
3518
+ def multiband_sample(
3519
+ self,
3520
+ location,
3521
+ scale=30,
3522
+ file_path=None
3523
+ ):
3524
+ """
3525
+ Extracts ALL band values for a SINGLE location across the entire collection.
3526
+
3527
+ Args:
3528
+ location (tuple or ee.Geometry): A single (lon, lat) tuple OR ee.Geometry.
3529
+ scale (int, optional): Scale in meters. Defaults to 30.
3530
+ file_path (str, optional): Path to save CSV.
3531
+
3532
+ Returns:
3533
+ pd.DataFrame: DataFrame indexed by Date, with columns for each Band.
3534
+ """
3535
+ if isinstance(location, tuple) and len(location) == 2:
3536
+ geom = ee.Geometry.Point(location)
3537
+ elif isinstance(location, ee.Geometry):
3538
+ geom = location
3539
+ else:
3540
+ raise ValueError("Location must be a single (lon, lat) tuple or ee.Geometry.")
3541
+
3542
+ first_img = self.collection.first()
3543
+ band_names = first_img.bandNames()
3544
+
3545
+ # Create a dictionary of {band_name: -9999}
3546
+ # fill missing values so the Feature structure is consistent
3547
+ dummy_values = ee.List.repeat(-9999, band_names.length())
3548
+ default_dict = ee.Dictionary.fromLists(band_names, dummy_values)
3549
+
3550
+ def get_all_bands(img):
3551
+ date = img.get('Date_Filter')
3552
+
3553
+ # reduceRegion returns a Dictionary.
3554
+ # If a pixel is masked, that band key is missing from 'stats'.
3555
+ stats = img.reduceRegion(
3556
+ reducer=ee.Reducer.first(),
3557
+ geometry=geom,
3558
+ scale=scale,
3559
+ maxPixels=1e13
3560
+ )
3561
+
3562
+ # Combine stats with defaults.
3563
+ # overwrite=True means real data (stats) overwrites the -9999 defaults.
3564
+ complete_stats = default_dict.combine(stats, overwrite=True)
3565
+
3566
+ return ee.Feature(None, complete_stats).set('Date', date)
3567
+
3568
+ fc = ee.FeatureCollection(self.collection.map(get_all_bands))
3569
+
3570
+ df = Sentinel1Collection.ee_to_df(fc, remove_geom=True)
3571
+
3572
+ if df.empty:
3573
+ print("Warning: No data found.")
3574
+ return pd.DataFrame()
3575
+
3576
+ # 6. Cleanup
3577
+ if 'Date' in df.columns:
3578
+ df['Date'] = pd.to_datetime(df['Date'])
3579
+ df = df.set_index('Date').sort_index()
3580
+
3581
+ # Replace our sentinel -9999 with proper NaNs
3582
+ df = df.replace(-9999, np.nan)
3583
+
3584
+ # 7. Export
3585
+ if file_path:
3586
+ if not file_path.lower().endswith('.csv'):
3587
+ file_path += '.csv'
3588
+ df.to_csv(file_path)
3589
+ print(f"Multiband sample saved to {file_path}")
3590
+ return None
3591
+
3592
+ return df
3215
3593
 
3216
3594
  def export_to_asset_collection(
3217
3595
  self,
@@ -3222,7 +3600,8 @@ class Sentinel1Collection:
3222
3600
  filename_prefix="",
3223
3601
  crs=None,
3224
3602
  max_pixels=int(1e13),
3225
- description_prefix="export"
3603
+ description_prefix="export",
3604
+ overwrite=False
3226
3605
  ):
3227
3606
  """
3228
3607
  Exports an image collection to a Google Earth Engine asset collection. The asset collection will be created if it does not already exist,
@@ -3237,6 +3616,7 @@ class Sentinel1Collection:
3237
3616
  crs (str, optional): The coordinate reference system. Defaults to None, which will use the image's CRS.
3238
3617
  max_pixels (int, optional): The maximum number of pixels. Defaults to int(1e13).
3239
3618
  description_prefix (str, optional): The description prefix. Defaults to "export".
3619
+ overwrite (bool, optional): Whether to overwrite existing assets. Defaults to False.
3240
3620
 
3241
3621
  Returns:
3242
3622
  None: (queues export tasks)
@@ -3254,6 +3634,14 @@ class Sentinel1Collection:
3254
3634
  asset_id = asset_collection_path + "/" + filename_prefix + date_str
3255
3635
  desc = description_prefix + "_" + filename_prefix + date_str
3256
3636
 
3637
+ if overwrite:
3638
+ try:
3639
+ ee.data.deleteAsset(asset_id)
3640
+ print(f"Overwriting: Deleted existing asset {asset_id}")
3641
+ except ee.EEException:
3642
+ # Asset does not exist, so nothing to delete. Proceed safely.
3643
+ pass
3644
+
3257
3645
  params = {
3258
3646
  'image': img,
3259
3647
  'description': desc,