RadGEEToolbox 1.7.3__py3-none-any.whl → 1.7.5__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.
- RadGEEToolbox/CollectionStitch.py +16 -3
- RadGEEToolbox/Export.py +16 -0
- RadGEEToolbox/GenericCollection.py +698 -202
- RadGEEToolbox/LandsatCollection.py +818 -218
- RadGEEToolbox/Sentinel1Collection.py +734 -204
- RadGEEToolbox/Sentinel2Collection.py +771 -219
- RadGEEToolbox/__init__.py +4 -4
- {radgeetoolbox-1.7.3.dist-info → radgeetoolbox-1.7.5.dist-info}/METADATA +6 -6
- radgeetoolbox-1.7.5.dist-info/RECORD +14 -0
- {radgeetoolbox-1.7.3.dist-info → radgeetoolbox-1.7.5.dist-info}/WHEEL +1 -1
- radgeetoolbox-1.7.3.dist-info/RECORD +0 -14
- {radgeetoolbox-1.7.3.dist-info → radgeetoolbox-1.7.5.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.7.3.dist-info → radgeetoolbox-1.7.5.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ee
|
|
2
2
|
import pandas as pd
|
|
3
3
|
import numpy as np
|
|
4
|
+
import warnings
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class GenericCollection:
|
|
@@ -93,6 +94,14 @@ class GenericCollection:
|
|
|
93
94
|
self._PixelAreaSumCollection = None
|
|
94
95
|
self._daily_aggregate_collection = None
|
|
95
96
|
|
|
97
|
+
def __call__(self):
|
|
98
|
+
"""
|
|
99
|
+
Allows the object to be called as a function, returning itself.
|
|
100
|
+
This enables property-like methods to be accessed with or without parentheses
|
|
101
|
+
(e.g., .mosaicByDate or .mosaicByDate()).
|
|
102
|
+
"""
|
|
103
|
+
return self
|
|
104
|
+
|
|
96
105
|
@staticmethod
|
|
97
106
|
def image_dater(image):
|
|
98
107
|
"""
|
|
@@ -271,7 +280,7 @@ class GenericCollection:
|
|
|
271
280
|
return out.copyProperties(img).set('system:time_start', img.get('system:time_start'))
|
|
272
281
|
|
|
273
282
|
@staticmethod
|
|
274
|
-
def
|
|
283
|
+
def pixelAreaSum(
|
|
275
284
|
image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
|
|
276
285
|
):
|
|
277
286
|
"""
|
|
@@ -331,7 +340,19 @@ class GenericCollection:
|
|
|
331
340
|
final_image = ee.Image(bands.iterate(calculate_and_set_area, image))
|
|
332
341
|
return final_image #.set('system:time_start', image.get('system:time_start'))
|
|
333
342
|
|
|
334
|
-
|
|
343
|
+
@staticmethod
|
|
344
|
+
def PixelAreaSum(
|
|
345
|
+
image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
|
|
346
|
+
):
|
|
347
|
+
warnings.warn(
|
|
348
|
+
"The `PixelAreaSum` static method is deprecated and will be removed in future versions. Please use the `pixelAreaSum` static method instead.",
|
|
349
|
+
DeprecationWarning,
|
|
350
|
+
stacklevel=2)
|
|
351
|
+
return GenericCollection.pixelAreaSum(
|
|
352
|
+
image, band_name, geometry, threshold, scale, maxPixels
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def pixelAreaSumCollection(
|
|
335
356
|
self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
336
357
|
):
|
|
337
358
|
"""
|
|
@@ -358,7 +379,7 @@ class GenericCollection:
|
|
|
358
379
|
collection = self.collection
|
|
359
380
|
# Area calculation for each image in the collection, using the PixelAreaSum function
|
|
360
381
|
AreaCollection = collection.map(
|
|
361
|
-
lambda image: GenericCollection.
|
|
382
|
+
lambda image: GenericCollection.pixelAreaSum(
|
|
362
383
|
image,
|
|
363
384
|
band_name=band_name,
|
|
364
385
|
geometry=geometry,
|
|
@@ -374,17 +395,28 @@ class GenericCollection:
|
|
|
374
395
|
|
|
375
396
|
# If an export path is provided, the area data will be exported to a CSV file
|
|
376
397
|
if area_data_export_path:
|
|
377
|
-
GenericCollection(collection=self._PixelAreaSumCollection).
|
|
398
|
+
GenericCollection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
|
|
378
399
|
# Returning the result in the desired format based on output_type argument or raising an error for invalid input
|
|
379
400
|
if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
|
|
380
401
|
return self._PixelAreaSumCollection
|
|
381
402
|
elif output_type == 'GenericCollection':
|
|
382
403
|
return GenericCollection(collection=self._PixelAreaSumCollection)
|
|
383
404
|
elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
|
|
384
|
-
return GenericCollection(collection=self._PixelAreaSumCollection).
|
|
405
|
+
return GenericCollection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names)
|
|
385
406
|
else:
|
|
386
407
|
raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'GenericCollection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
|
|
387
408
|
|
|
409
|
+
def PixelAreaSumCollection(
|
|
410
|
+
self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
411
|
+
):
|
|
412
|
+
warnings.warn(
|
|
413
|
+
"The `PixelAreaSumCollection` method is deprecated and will be removed in future versions. Please use the `pixelAreaSumCollection` method instead.",
|
|
414
|
+
DeprecationWarning,
|
|
415
|
+
stacklevel=2)
|
|
416
|
+
return self.pixelAreaSumCollection(
|
|
417
|
+
band_name, geometry, threshold, scale, maxPixels, output_type, area_data_export_path
|
|
418
|
+
)
|
|
419
|
+
|
|
388
420
|
@staticmethod
|
|
389
421
|
def add_month_property_fn(image):
|
|
390
422
|
"""
|
|
@@ -544,7 +576,7 @@ class GenericCollection:
|
|
|
544
576
|
|
|
545
577
|
return GenericCollection(collection=distinct_col)
|
|
546
578
|
|
|
547
|
-
def
|
|
579
|
+
def exportProperties(self, property_names, file_path=None):
|
|
548
580
|
"""
|
|
549
581
|
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.
|
|
550
582
|
|
|
@@ -599,6 +631,13 @@ class GenericCollection:
|
|
|
599
631
|
print(f"Properties saved to {file_path}")
|
|
600
632
|
|
|
601
633
|
return df
|
|
634
|
+
|
|
635
|
+
def ExportProperties(self, property_names, file_path=None):
|
|
636
|
+
warnings.warn(
|
|
637
|
+
"The `ExportProperties` method is deprecated and will be removed in future versions. Please use the `exportProperties` method instead.",
|
|
638
|
+
DeprecationWarning,
|
|
639
|
+
stacklevel=2)
|
|
640
|
+
return self.exportProperties(property_names, file_path)
|
|
602
641
|
|
|
603
642
|
def get_generic_collection(self):
|
|
604
643
|
"""
|
|
@@ -1983,6 +2022,8 @@ class GenericCollection:
|
|
|
1983
2022
|
rightField='Date_Filter')
|
|
1984
2023
|
else:
|
|
1985
2024
|
raise ValueError(f'The chosen `join_method`: {join_method} does not match the options of "system:time_start" or "Date_Filter".')
|
|
2025
|
+
|
|
2026
|
+
native_projection = image_collection.first().select(target_band).projection()
|
|
1986
2027
|
|
|
1987
2028
|
# for any matches during a join, set image as a property key called 'future_image'
|
|
1988
2029
|
join = ee.Join.saveAll(matchesKey='future_image')
|
|
@@ -2026,7 +2067,7 @@ class GenericCollection:
|
|
|
2026
2067
|
# convert the image collection to an image of s_statistic values per pixel
|
|
2027
2068
|
# where the s_statistic is the sum of partial s values
|
|
2028
2069
|
# renaming the band as 's_statistic' for later usage
|
|
2029
|
-
final_s_image = partial_s_col.sum().rename('s_statistic')
|
|
2070
|
+
final_s_image = partial_s_col.sum().rename('s_statistic').setDefaultProjection(native_projection)
|
|
2030
2071
|
|
|
2031
2072
|
|
|
2032
2073
|
########## PART 2 - VARIANCE and Z-SCORE ##########
|
|
@@ -2089,7 +2130,7 @@ class GenericCollection:
|
|
|
2089
2130
|
mask = ee.Image(1).clip(geometry)
|
|
2090
2131
|
final_image = final_image.updateMask(mask)
|
|
2091
2132
|
|
|
2092
|
-
return final_image
|
|
2133
|
+
return final_image.setDefaultProjection(native_projection)
|
|
2093
2134
|
|
|
2094
2135
|
def sens_slope_trend(self, target_band=None, join_method='system:time_start', geometry=None):
|
|
2095
2136
|
"""
|
|
@@ -2166,20 +2207,15 @@ class GenericCollection:
|
|
|
2166
2207
|
GenericCollection: masked GenericCollection image collection
|
|
2167
2208
|
|
|
2168
2209
|
"""
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
mask = ee.Image.constant(1).clip(polygon)
|
|
2172
|
-
|
|
2173
|
-
# Update the mask of each image in the collection
|
|
2174
|
-
masked_collection = self.collection.map(lambda img: img.updateMask(mask).copyProperties(img).set('system:time_start', img.get('system:time_start')))
|
|
2210
|
+
# Convert the polygon to a mask
|
|
2211
|
+
mask = ee.Image.constant(1).clip(polygon)
|
|
2175
2212
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
)
|
|
2213
|
+
# Update the mask of each image in the collection
|
|
2214
|
+
masked_collection = self.collection.map(lambda img: img.updateMask(mask)\
|
|
2215
|
+
.copyProperties(img).set('system:time_start', img.get('system:time_start')))
|
|
2180
2216
|
|
|
2181
2217
|
# Return the updated object
|
|
2182
|
-
return
|
|
2218
|
+
return GenericCollection(collection=masked_collection)
|
|
2183
2219
|
|
|
2184
2220
|
def mask_out_polygon(self, polygon):
|
|
2185
2221
|
"""
|
|
@@ -2192,23 +2228,18 @@ class GenericCollection:
|
|
|
2192
2228
|
GenericCollection: masked GenericCollection image collection
|
|
2193
2229
|
|
|
2194
2230
|
"""
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
full_mask = ee.Image.constant(1)
|
|
2231
|
+
# Convert the polygon to a mask
|
|
2232
|
+
full_mask = ee.Image.constant(1)
|
|
2198
2233
|
|
|
2199
|
-
|
|
2200
|
-
|
|
2234
|
+
# Use paint to set pixels inside polygon as 0
|
|
2235
|
+
area = full_mask.paint(polygon, 0)
|
|
2201
2236
|
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
# Update the internal collection state
|
|
2206
|
-
self._geometry_masked_out_collection = GenericCollection(
|
|
2207
|
-
collection=masked_collection
|
|
2208
|
-
)
|
|
2237
|
+
# Update the mask of each image in the collection
|
|
2238
|
+
masked_collection = self.collection.map(lambda img: img.updateMask(area)\
|
|
2239
|
+
.copyProperties(img).set('system:time_start', img.get('system:time_start')))
|
|
2209
2240
|
|
|
2210
2241
|
# Return the updated object
|
|
2211
|
-
return
|
|
2242
|
+
return GenericCollection(collection=masked_collection)
|
|
2212
2243
|
|
|
2213
2244
|
|
|
2214
2245
|
def binary_mask(self, threshold=None, band_name=None, classify_above_threshold=True, mask_zeros=False):
|
|
@@ -2467,7 +2498,7 @@ class GenericCollection:
|
|
|
2467
2498
|
new_col = self.collection.filter(ee.Filter.eq("Date_Filter", img_date))
|
|
2468
2499
|
return new_col.first()
|
|
2469
2500
|
|
|
2470
|
-
def
|
|
2501
|
+
def collectionStitch(self, img_col2):
|
|
2471
2502
|
"""
|
|
2472
2503
|
Function to mosaic two GenericCollection objects which share image dates.
|
|
2473
2504
|
Mosaics are only formed for dates where both image collections have images.
|
|
@@ -2519,9 +2550,16 @@ class GenericCollection:
|
|
|
2519
2550
|
|
|
2520
2551
|
# Return a GenericCollection instance
|
|
2521
2552
|
return GenericCollection(collection=new_col)
|
|
2553
|
+
|
|
2554
|
+
def CollectionStitch(self, img_col2):
|
|
2555
|
+
warnings.warn(
|
|
2556
|
+
"CollectionStitch is deprecated. Please use collectionStitch instead.",
|
|
2557
|
+
DeprecationWarning,
|
|
2558
|
+
stacklevel=2)
|
|
2559
|
+
return self.collectionStitch(img_col2)
|
|
2522
2560
|
|
|
2523
2561
|
@property
|
|
2524
|
-
def
|
|
2562
|
+
def mosaicByDateDepr(self):
|
|
2525
2563
|
"""
|
|
2526
2564
|
Property attribute function to mosaic collection images that share the same date.
|
|
2527
2565
|
|
|
@@ -2577,6 +2615,64 @@ class GenericCollection:
|
|
|
2577
2615
|
|
|
2578
2616
|
# Convert the list of mosaics to an ImageCollection
|
|
2579
2617
|
return self._MosaicByDate
|
|
2618
|
+
|
|
2619
|
+
@property
|
|
2620
|
+
def mosaicByDate(self):
|
|
2621
|
+
"""
|
|
2622
|
+
Property attribute function to mosaic collection images that share the same date.
|
|
2623
|
+
|
|
2624
|
+
The property CLOUD_COVER for each image is used to calculate an overall mean,
|
|
2625
|
+
which replaces the CLOUD_COVER property for each mosaiced image.
|
|
2626
|
+
Server-side friendly.
|
|
2627
|
+
|
|
2628
|
+
NOTE: if images are removed from the collection from cloud filtering, you may have mosaics composed of only one image.
|
|
2629
|
+
|
|
2630
|
+
Returns:
|
|
2631
|
+
LandsatCollection: LandsatCollection image collection with mosaiced imagery and mean CLOUD_COVER as a property
|
|
2632
|
+
"""
|
|
2633
|
+
if self._MosaicByDate is None:
|
|
2634
|
+
distinct_dates = self.collection.distinct("Date_Filter")
|
|
2635
|
+
|
|
2636
|
+
# Define a join to link images by Date_Filter
|
|
2637
|
+
filter_date = ee.Filter.equals(leftField="Date_Filter", rightField="Date_Filter")
|
|
2638
|
+
join = ee.Join.saveAll(matchesKey="date_matches")
|
|
2639
|
+
|
|
2640
|
+
# Apply the join
|
|
2641
|
+
# Primary: Distinct dates collection
|
|
2642
|
+
# Secondary: The full original collection
|
|
2643
|
+
joined_col = ee.ImageCollection(join.apply(distinct_dates, self.collection, filter_date))
|
|
2644
|
+
|
|
2645
|
+
# Define the mosaicking function
|
|
2646
|
+
def _mosaic_day(img):
|
|
2647
|
+
# Recover the list of images for this day
|
|
2648
|
+
daily_list = ee.List(img.get("date_matches"))
|
|
2649
|
+
daily_col = ee.ImageCollection.fromImages(daily_list)
|
|
2650
|
+
|
|
2651
|
+
# Create the mosaic
|
|
2652
|
+
mosaic = daily_col.mosaic().setDefaultProjection(img.projection())
|
|
2653
|
+
|
|
2654
|
+
# Properties to preserve from the representative image
|
|
2655
|
+
props_of_interest = [
|
|
2656
|
+
"system:time_start",
|
|
2657
|
+
"Date_Filter"
|
|
2658
|
+
]
|
|
2659
|
+
|
|
2660
|
+
# Return mosaic with properties set
|
|
2661
|
+
return mosaic.copyProperties(img, props_of_interest)
|
|
2662
|
+
# 5. Map the function and wrap the result
|
|
2663
|
+
mosaiced_col = joined_col.map(_mosaic_day)
|
|
2664
|
+
self._MosaicByDate = GenericCollection(collection=mosaiced_col)
|
|
2665
|
+
|
|
2666
|
+
# Convert the list of mosaics to an ImageCollection
|
|
2667
|
+
return self._MosaicByDate
|
|
2668
|
+
|
|
2669
|
+
@property
|
|
2670
|
+
def MosaicByDate(self):
|
|
2671
|
+
warnings.warn(
|
|
2672
|
+
"MosaicByDate is deprecated. Please use mosaicByDate instead.",
|
|
2673
|
+
DeprecationWarning,
|
|
2674
|
+
stacklevel=2)
|
|
2675
|
+
return self.mosaicByDate
|
|
2580
2676
|
|
|
2581
2677
|
@staticmethod
|
|
2582
2678
|
def ee_to_df(
|
|
@@ -2797,200 +2893,197 @@ class GenericCollection:
|
|
|
2797
2893
|
lines,
|
|
2798
2894
|
line_names,
|
|
2799
2895
|
reducer="mean",
|
|
2800
|
-
dist_interval=
|
|
2896
|
+
dist_interval=90,
|
|
2801
2897
|
n_segments=None,
|
|
2802
2898
|
scale=30,
|
|
2803
2899
|
processing_mode='aggregated',
|
|
2804
2900
|
save_folder_path=None,
|
|
2805
2901
|
sampling_method='line',
|
|
2806
|
-
point_buffer_radius=15
|
|
2902
|
+
point_buffer_radius=15,
|
|
2903
|
+
batch_size=10
|
|
2807
2904
|
):
|
|
2808
2905
|
"""
|
|
2809
|
-
Computes and returns pixel values along transects
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
for maximum flexibility and performance.
|
|
2814
|
-
|
|
2815
|
-
There are two processing modes available, aggregated and iterative:
|
|
2816
|
-
- 'aggregated' (default; suggested): Fast, server-side processing. Fetches all results
|
|
2817
|
-
in a single request. Highly recommended. Returns a dictionary of pandas DataFrames.
|
|
2818
|
-
- 'iterative': Slower, client-side loop that processes one image at a time.
|
|
2819
|
-
Kept for backward compatibility (effectively depreciated). Returns None and saves individual CSVs.
|
|
2820
|
-
This method is not recommended unless absolutely necessary, as it is less efficient and may be subject to client-side timeouts.
|
|
2821
|
-
|
|
2906
|
+
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
|
|
2907
|
+
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).
|
|
2908
|
+
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`.
|
|
2909
|
+
|
|
2822
2910
|
Args:
|
|
2823
|
-
lines (list):
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
each transect line into for sampling. This parameter overrides `dist_interval`.
|
|
2835
|
-
Defaults to None.
|
|
2836
|
-
scale (int, optional): The nominal scale in meters for the reduction,
|
|
2837
|
-
which should typically match the pixel resolution of the imagery.
|
|
2838
|
-
Defaults to 30.
|
|
2839
|
-
processing_mode (str, optional): The method for processing the collection.
|
|
2840
|
-
- 'aggregated' (default): Fast, server-side processing. Fetches all
|
|
2841
|
-
results in a single request. Highly recommended. Returns a dictionary
|
|
2842
|
-
of pandas DataFrames.
|
|
2843
|
-
- 'iterative': Slower, client-side loop that processes one image at a
|
|
2844
|
-
time. Kept for backward compatibility. Returns None and saves
|
|
2845
|
-
individual CSVs.
|
|
2846
|
-
save_folder_path (str, optional): If provided, the function will save the
|
|
2847
|
-
resulting transect data to CSV files. The behavior depends on the
|
|
2848
|
-
`processing_mode`:
|
|
2849
|
-
- In 'aggregated' mode, one CSV is saved for each transect,
|
|
2850
|
-
containing all dates. (e.g., 'MyTransect_transects.csv').
|
|
2851
|
-
- In 'iterative' mode, one CSV is saved for each date,
|
|
2852
|
-
containing all transects. (e.g., '2022-06-15_transects.csv').
|
|
2853
|
-
sampling_method (str, optional): The geometric method used for sampling.
|
|
2854
|
-
- 'line' (default): Reduces all pixels intersecting each small line
|
|
2855
|
-
segment. This can be unreliable and produce blank rows if
|
|
2856
|
-
`dist_interval` is too small relative to the `scale`.
|
|
2857
|
-
- 'buffered_point': Reduces all pixels within a buffer around the
|
|
2858
|
-
midpoint of each line segment. This method is more robust and
|
|
2859
|
-
reliably avoids blank rows, but may not reduce all pixels along a line segment.
|
|
2860
|
-
point_buffer_radius (int, optional): The radius in meters for the buffer
|
|
2861
|
-
when `sampling_method` is 'buffered_point'. Defaults to 15.
|
|
2911
|
+
lines (list): List of ee.Geometry.LineString objects.
|
|
2912
|
+
line_names (list): List of string names for each transect.
|
|
2913
|
+
reducer (str, optional): Reducer name. Defaults to 'mean'.
|
|
2914
|
+
dist_interval (float, optional): Distance interval in meters. Defaults to 90.
|
|
2915
|
+
n_segments (int, optional): Number of segments (overrides dist_interval).
|
|
2916
|
+
scale (int, optional): Scale in meters. Defaults to 30.
|
|
2917
|
+
processing_mode (str, optional): 'aggregated' or 'iterative'.
|
|
2918
|
+
save_folder_path (str, optional): Path to save CSVs.
|
|
2919
|
+
sampling_method (str, optional): 'line' or 'buffered_point'.
|
|
2920
|
+
point_buffer_radius (int, optional): Buffer radius if using 'buffered_point'.
|
|
2921
|
+
batch_size (int, optional): Images per request in 'aggregated' mode. Defaults to 10. Lower the value if you encounter a 'Too many aggregations' error.
|
|
2862
2922
|
|
|
2863
2923
|
Returns:
|
|
2864
|
-
dict or None:
|
|
2865
|
-
- If `processing_mode` is 'aggregated', returns a dictionary where each
|
|
2866
|
-
key is a transect name and each value is a pandas DataFrame. In the
|
|
2867
|
-
DataFrame, the index is the distance along the transect and each
|
|
2868
|
-
column represents an image date. Optionally saves CSV files if
|
|
2869
|
-
`save_folder_path` is provided.
|
|
2870
|
-
- If `processing_mode` is 'iterative', returns None as it saves
|
|
2871
|
-
files directly.
|
|
2872
|
-
|
|
2873
|
-
Raises:
|
|
2874
|
-
ValueError: If `lines` and `line_names` have different lengths, or if
|
|
2875
|
-
an unknown reducer or processing mode is specified.
|
|
2924
|
+
dict or None: Dictionary of DataFrames (aggregated) or None (iterative).
|
|
2876
2925
|
"""
|
|
2877
|
-
# Validating inputs
|
|
2878
2926
|
if len(lines) != len(line_names):
|
|
2879
2927
|
raise ValueError("'lines' and 'line_names' must have the same number of elements.")
|
|
2880
|
-
|
|
2928
|
+
|
|
2929
|
+
first_img = self.collection.first()
|
|
2930
|
+
bands = first_img.bandNames().getInfo()
|
|
2931
|
+
is_multiband = len(bands) > 1
|
|
2932
|
+
|
|
2933
|
+
# Setup robust dictionary for handling masked/zero values
|
|
2934
|
+
default_val = -9999
|
|
2935
|
+
dummy_dict = ee.Dictionary.fromLists(bands, ee.List.repeat(default_val, len(bands)))
|
|
2936
|
+
|
|
2937
|
+
if is_multiband:
|
|
2938
|
+
reducer_cols = [f"{b}_{reducer}" for b in bands]
|
|
2939
|
+
clean_names = bands
|
|
2940
|
+
rename_keys = bands
|
|
2941
|
+
rename_vals = reducer_cols
|
|
2942
|
+
else:
|
|
2943
|
+
reducer_cols = [reducer]
|
|
2944
|
+
clean_names = [bands[0]]
|
|
2945
|
+
rename_keys = bands
|
|
2946
|
+
rename_vals = reducer_cols
|
|
2947
|
+
|
|
2948
|
+
print("Pre-computing transect geometries from input LineString(s)...")
|
|
2949
|
+
|
|
2950
|
+
master_transect_fc = ee.FeatureCollection([])
|
|
2951
|
+
geom_error = 1.0
|
|
2952
|
+
|
|
2953
|
+
for i, line in enumerate(lines):
|
|
2954
|
+
line_name = line_names[i]
|
|
2955
|
+
length = line.length(geom_error)
|
|
2956
|
+
|
|
2957
|
+
eff_interval = length.divide(n_segments) if n_segments else dist_interval
|
|
2958
|
+
|
|
2959
|
+
distances = ee.List.sequence(0, length, eff_interval)
|
|
2960
|
+
cut_lines = line.cutLines(distances, geom_error).geometries()
|
|
2961
|
+
|
|
2962
|
+
def create_feature(l):
|
|
2963
|
+
geom = ee.Geometry(ee.List(l).get(0))
|
|
2964
|
+
dist = ee.Number(ee.List(l).get(1))
|
|
2965
|
+
|
|
2966
|
+
final_geom = ee.Algorithms.If(
|
|
2967
|
+
ee.String(sampling_method).equals('buffered_point'),
|
|
2968
|
+
geom.centroid(geom_error).buffer(point_buffer_radius),
|
|
2969
|
+
geom
|
|
2970
|
+
)
|
|
2971
|
+
|
|
2972
|
+
return ee.Feature(ee.Geometry(final_geom), {
|
|
2973
|
+
'transect_name': line_name,
|
|
2974
|
+
'distance': dist
|
|
2975
|
+
})
|
|
2976
|
+
|
|
2977
|
+
line_fc = ee.FeatureCollection(cut_lines.zip(distances).map(create_feature))
|
|
2978
|
+
master_transect_fc = master_transect_fc.merge(line_fc)
|
|
2979
|
+
|
|
2980
|
+
try:
|
|
2981
|
+
ee_reducer = getattr(ee.Reducer, reducer)()
|
|
2982
|
+
except AttributeError:
|
|
2983
|
+
raise ValueError(f"Unknown reducer: '{reducer}'.")
|
|
2984
|
+
|
|
2985
|
+
def process_image(image):
|
|
2986
|
+
date_val = image.get('Date_Filter')
|
|
2987
|
+
|
|
2988
|
+
# Map over points (Slower but Robust)
|
|
2989
|
+
def reduce_point(f):
|
|
2990
|
+
stats = image.reduceRegion(
|
|
2991
|
+
reducer=ee_reducer,
|
|
2992
|
+
geometry=f.geometry(),
|
|
2993
|
+
scale=scale,
|
|
2994
|
+
maxPixels=1e13
|
|
2995
|
+
)
|
|
2996
|
+
# Combine with defaults (preserves 0, handles masked)
|
|
2997
|
+
safe_stats = dummy_dict.combine(stats, overwrite=True)
|
|
2998
|
+
# Rename keys to match expected outputs (e.g. 'ndvi' -> 'ndvi_mean')
|
|
2999
|
+
final_stats = safe_stats.rename(rename_keys, rename_vals)
|
|
3000
|
+
|
|
3001
|
+
return f.set(final_stats).set({'image_date': date_val})
|
|
3002
|
+
|
|
3003
|
+
return master_transect_fc.map(reduce_point)
|
|
3004
|
+
|
|
3005
|
+
export_cols = ['transect_name', 'distance', 'image_date'] + reducer_cols
|
|
3006
|
+
|
|
2881
3007
|
if processing_mode == 'aggregated':
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
#
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
# Determine effective distance interval based on n_segments or dist_interval
|
|
2901
|
-
effective_dist_interval = ee.Algorithms.If(
|
|
2902
|
-
n_segments,
|
|
2903
|
-
length.divide(n_segments),
|
|
2904
|
-
dist_interval or 30 # Defaults to 30 if both are None
|
|
2905
|
-
)
|
|
2906
|
-
# Generate distances along the line(s) for segmentation
|
|
2907
|
-
distances = ee.List.sequence(0, length, effective_dist_interval)
|
|
2908
|
-
# Segmenting the line into smaller lines at the specified distances
|
|
2909
|
-
cut_lines_geoms = line.cutLines(distances, maxError).geometries()
|
|
2910
|
-
# Function to create features with distance attributes
|
|
2911
|
-
# Adjusted to ensure consistent return types
|
|
2912
|
-
def set_dist_attr(l):
|
|
2913
|
-
# l is a list: [geometry, distance]
|
|
2914
|
-
# Extracting geometry portion of line
|
|
2915
|
-
geom_segment = ee.Geometry(ee.List(l).get(0))
|
|
2916
|
-
# Extracting distance value for attribute
|
|
2917
|
-
distance = ee.Number(ee.List(l).get(1))
|
|
2918
|
-
### Determine final geometry based on sampling method
|
|
2919
|
-
# If the sampling method is 'buffered_point',
|
|
2920
|
-
# create a buffered point feature at the centroid of each segment,
|
|
2921
|
-
# otherwise create a line feature
|
|
2922
|
-
final_feature = ee.Algorithms.If(
|
|
2923
|
-
ee.String(sampling_method).equals('buffered_point'),
|
|
2924
|
-
# True Case: Create the buffered point feature
|
|
2925
|
-
ee.Feature(
|
|
2926
|
-
geom_segment.centroid(maxError).buffer(point_buffer_radius),
|
|
2927
|
-
{'distance': distance}
|
|
2928
|
-
),
|
|
2929
|
-
# False Case: Create the line segment feature
|
|
2930
|
-
ee.Feature(geom_segment, {'distance': distance})
|
|
2931
|
-
)
|
|
2932
|
-
# Return either the line segment feature or the buffered point feature
|
|
2933
|
-
return final_feature
|
|
2934
|
-
# Creating a FeatureCollection of the cut lines with distance attributes
|
|
2935
|
-
# Using map to apply the set_dist_attr function to each cut line geometry
|
|
2936
|
-
line_features = ee.FeatureCollection(cut_lines_geoms.zip(distances).map(set_dist_attr))
|
|
2937
|
-
# Reducing the image over the line features to get transect values
|
|
2938
|
-
transect_fc = image.reduceRegions(
|
|
2939
|
-
collection=line_features, reducer=ee_reducer, scale=scale
|
|
2940
|
-
)
|
|
2941
|
-
# Adding image date and line name properties to each feature
|
|
2942
|
-
def set_props(feature):
|
|
2943
|
-
return feature.set({'image_date': image_date, 'transect_name': line_name})
|
|
2944
|
-
# Append to the list of all transects for this image
|
|
2945
|
-
all_transects_for_image = all_transects_for_image.add(transect_fc.map(set_props))
|
|
2946
|
-
# Combine all transect FeatureCollections into a single FeatureCollection and flatten
|
|
2947
|
-
# Flatten is used to merge the list of FeatureCollections into one
|
|
2948
|
-
return ee.FeatureCollection(all_transects_for_image).flatten()
|
|
2949
|
-
# Map the function over the entire image collection and flatten the results
|
|
2950
|
-
results_fc = ee.FeatureCollection(self.collection.map(get_transects_for_image)).flatten()
|
|
2951
|
-
# Convert the results to a pandas DataFrame
|
|
2952
|
-
df = GenericCollection.ee_to_df(results_fc, remove_geom=True)
|
|
2953
|
-
# Check if the DataFrame is empty
|
|
2954
|
-
if df.empty:
|
|
2955
|
-
print("Warning: No transect data was generated.")
|
|
3008
|
+
collection_size = self.collection.size().getInfo()
|
|
3009
|
+
print(f"Starting batch process of {collection_size} images...")
|
|
3010
|
+
|
|
3011
|
+
dfs = []
|
|
3012
|
+
for i in range(0, collection_size, batch_size):
|
|
3013
|
+
print(f" Processing image {i} to {min(i + batch_size, collection_size)}...")
|
|
3014
|
+
|
|
3015
|
+
batch_col = ee.ImageCollection(self.collection.toList(batch_size, i))
|
|
3016
|
+
results_fc = batch_col.map(process_image).flatten()
|
|
3017
|
+
|
|
3018
|
+
# Dynamic Class Call for ee_to_df
|
|
3019
|
+
df_batch = self.__class__.ee_to_df(results_fc, columns=export_cols, remove_geom=True)
|
|
3020
|
+
|
|
3021
|
+
if not df_batch.empty:
|
|
3022
|
+
dfs.append(df_batch)
|
|
3023
|
+
|
|
3024
|
+
if not dfs:
|
|
3025
|
+
print("Warning: No transect data generated.")
|
|
2956
3026
|
return {}
|
|
2957
|
-
|
|
3027
|
+
|
|
3028
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
3029
|
+
|
|
3030
|
+
# Post-Process & Split
|
|
2958
3031
|
output_dfs = {}
|
|
2959
|
-
|
|
3032
|
+
for col in reducer_cols:
|
|
3033
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
3034
|
+
df[col] = df[col].replace(-9999, np.nan)
|
|
3035
|
+
|
|
2960
3036
|
for name in sorted(df['transect_name'].unique()):
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3037
|
+
line_df = df[df['transect_name'] == name]
|
|
3038
|
+
|
|
3039
|
+
for raw_col, band_name in zip(reducer_cols, clean_names):
|
|
3040
|
+
try:
|
|
3041
|
+
# Safety drop for duplicates
|
|
3042
|
+
line_df_clean = line_df.drop_duplicates(subset=['distance', 'image_date'])
|
|
3043
|
+
|
|
3044
|
+
pivot = line_df_clean.pivot(index='distance', columns='image_date', values=raw_col)
|
|
3045
|
+
pivot.columns.name = 'Date'
|
|
3046
|
+
key = f"{name}_{band_name}"
|
|
3047
|
+
output_dfs[key] = pivot
|
|
3048
|
+
|
|
3049
|
+
if save_folder_path:
|
|
3050
|
+
safe_key = "".join(x for x in key if x.isalnum() or x in "._-")
|
|
3051
|
+
fname = f"{save_folder_path}{safe_key}_transects.csv"
|
|
3052
|
+
pivot.to_csv(fname)
|
|
3053
|
+
print(f"Saved: {fname}")
|
|
3054
|
+
except Exception as e:
|
|
3055
|
+
print(f"Skipping pivot for {name}/{band_name}: {e}")
|
|
3056
|
+
|
|
2973
3057
|
return output_dfs
|
|
2974
3058
|
|
|
2975
|
-
### old, depreciated iterative client-side processing method ###
|
|
2976
3059
|
elif processing_mode == 'iterative':
|
|
2977
3060
|
if not save_folder_path:
|
|
2978
|
-
raise ValueError("
|
|
3061
|
+
raise ValueError("save_folder_path is required for iterative mode.")
|
|
2979
3062
|
|
|
2980
3063
|
image_collection_dates = self.dates
|
|
2981
3064
|
for i, date in enumerate(image_collection_dates):
|
|
2982
3065
|
try:
|
|
2983
3066
|
print(f"Processing image {i+1}/{len(image_collection_dates)}: {date}")
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
)
|
|
2988
|
-
|
|
2989
|
-
|
|
3067
|
+
image_list = self.collection.toList(self.collection.size())
|
|
3068
|
+
image = ee.Image(image_list.get(i))
|
|
3069
|
+
|
|
3070
|
+
fc_result = process_image(image)
|
|
3071
|
+
df = self.__class__.ee_to_df(fc_result, columns=export_cols, remove_geom=True)
|
|
3072
|
+
|
|
3073
|
+
if not df.empty:
|
|
3074
|
+
for col in reducer_cols:
|
|
3075
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
3076
|
+
df[col] = df[col].replace(-9999, np.nan)
|
|
3077
|
+
|
|
3078
|
+
fname = f"{save_folder_path}{date}_transects.csv"
|
|
3079
|
+
df.to_csv(fname, index=False)
|
|
3080
|
+
print(f"Saved: {fname}")
|
|
3081
|
+
else:
|
|
3082
|
+
print(f"Skipping {date}: No data.")
|
|
2990
3083
|
except Exception as e:
|
|
2991
|
-
print(f"
|
|
3084
|
+
print(f"Error processing {date}: {e}")
|
|
2992
3085
|
else:
|
|
2993
|
-
raise ValueError("
|
|
3086
|
+
raise ValueError("processing_mode must be 'iterative' or 'aggregated'.")
|
|
2994
3087
|
|
|
2995
3088
|
@staticmethod
|
|
2996
3089
|
def extract_zonal_stats_from_buffer(
|
|
@@ -3094,7 +3187,8 @@ class GenericCollection:
|
|
|
3094
3187
|
buffer_size=1,
|
|
3095
3188
|
tileScale=1,
|
|
3096
3189
|
dates=None,
|
|
3097
|
-
file_path=None
|
|
3190
|
+
file_path=None,
|
|
3191
|
+
unweighted=False
|
|
3098
3192
|
):
|
|
3099
3193
|
"""
|
|
3100
3194
|
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.
|
|
@@ -3111,6 +3205,7 @@ class GenericCollection:
|
|
|
3111
3205
|
tileScale (int, optional): A scaling factor to reduce aggregation tile size. Defaults to 1.
|
|
3112
3206
|
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.
|
|
3113
3207
|
file_path (str, optional): File path to save the output CSV.
|
|
3208
|
+
unweighted (bool, optional): If True, uses unweighted statistics when applicable (e.g., for 'mean'). Defaults to False.
|
|
3114
3209
|
|
|
3115
3210
|
Returns:
|
|
3116
3211
|
pd.DataFrame or None: A pandas DataFrame with dates as the index and coordinate names
|
|
@@ -3217,6 +3312,9 @@ class GenericCollection:
|
|
|
3217
3312
|
reducer = getattr(ee.Reducer, reducer_type)()
|
|
3218
3313
|
except AttributeError:
|
|
3219
3314
|
raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
|
|
3315
|
+
|
|
3316
|
+
if unweighted:
|
|
3317
|
+
reducer = reducer.unweighted()
|
|
3220
3318
|
|
|
3221
3319
|
# Define the function to map over the image collection
|
|
3222
3320
|
def calculate_stats_for_image(image):
|
|
@@ -3278,6 +3376,394 @@ class GenericCollection:
|
|
|
3278
3376
|
print(f"Zonal stats saved to {file_path}.csv")
|
|
3279
3377
|
return
|
|
3280
3378
|
return pivot_df
|
|
3379
|
+
|
|
3380
|
+
def multiband_zonal_stats(
|
|
3381
|
+
self,
|
|
3382
|
+
geometry,
|
|
3383
|
+
bands,
|
|
3384
|
+
reducer_types,
|
|
3385
|
+
scale=30,
|
|
3386
|
+
geometry_name='geom',
|
|
3387
|
+
dates=None,
|
|
3388
|
+
include_area=False,
|
|
3389
|
+
file_path=None,
|
|
3390
|
+
unweighted=False
|
|
3391
|
+
):
|
|
3392
|
+
"""
|
|
3393
|
+
Calculates zonal statistics for multiple bands over a single geometry for each image in the collection.
|
|
3394
|
+
Allows for specifying different reducers for different bands. Optionally includes the geometry area.
|
|
3395
|
+
|
|
3396
|
+
Args:
|
|
3397
|
+
geometry (ee.Geometry or ee.Feature): The single geometry to calculate statistics for.
|
|
3398
|
+
bands (list of str): A list of band names to include in the analysis.
|
|
3399
|
+
reducer_types (str or list of str): A single reducer name (e.g., 'mean') to apply to all bands,
|
|
3400
|
+
or a list of reducer names matching the length of the 'bands' list to apply specific reducers
|
|
3401
|
+
to specific bands.
|
|
3402
|
+
scale (int, optional): The scale in meters for the reduction. Defaults to 30.
|
|
3403
|
+
geometry_name (str, optional): A name for the geometry, used in column naming. Defaults to 'geom'.
|
|
3404
|
+
dates (list of str, optional): A list of date strings ('YYYY-MM-DD') to filter the collection.
|
|
3405
|
+
Defaults to None (processes all images).
|
|
3406
|
+
include_area (bool, optional): If True, adds a column with the area of the geometry in square meters.
|
|
3407
|
+
Defaults to False.
|
|
3408
|
+
file_path (str, optional): If provided, saves the resulting DataFrame to a CSV file at this path.
|
|
3409
|
+
unweighted (bool, optional): If True, uses unweighted statistics when applicable (e.g., for 'mean'). Defaults to False.
|
|
3410
|
+
|
|
3411
|
+
Returns:
|
|
3412
|
+
pd.DataFrame: A pandas DataFrame indexed by Date, with columns named as '{band}_{geometry_name}_{reducer}'.
|
|
3413
|
+
"""
|
|
3414
|
+
# 1. Input Validation and Setup
|
|
3415
|
+
if not isinstance(geometry, (ee.Geometry, ee.Feature)):
|
|
3416
|
+
raise ValueError("The `geometry` argument must be an ee.Geometry or ee.Feature.")
|
|
3417
|
+
|
|
3418
|
+
region = geometry.geometry() if isinstance(geometry, ee.Feature) else geometry
|
|
3419
|
+
|
|
3420
|
+
if isinstance(bands, str):
|
|
3421
|
+
bands = [bands]
|
|
3422
|
+
if not isinstance(bands, list):
|
|
3423
|
+
raise ValueError("The `bands` argument must be a string or a list of strings.")
|
|
3424
|
+
|
|
3425
|
+
# Handle reducer_types (str vs list)
|
|
3426
|
+
if isinstance(reducer_types, str):
|
|
3427
|
+
reducers_list = [reducer_types] * len(bands)
|
|
3428
|
+
elif isinstance(reducer_types, list):
|
|
3429
|
+
if len(reducer_types) != len(bands):
|
|
3430
|
+
raise ValueError("If `reducer_types` is a list, it must have the same length as `bands`.")
|
|
3431
|
+
reducers_list = reducer_types
|
|
3432
|
+
else:
|
|
3433
|
+
raise ValueError("`reducer_types` must be a string or a list of strings.")
|
|
3434
|
+
|
|
3435
|
+
# 2. Filter Collection
|
|
3436
|
+
processing_col = self.collection
|
|
3437
|
+
|
|
3438
|
+
if dates:
|
|
3439
|
+
processing_col = processing_col.filter(ee.Filter.inList('Date_Filter', dates))
|
|
3440
|
+
|
|
3441
|
+
processing_col = processing_col.select(bands)
|
|
3442
|
+
|
|
3443
|
+
# 3. Pre-calculate Area (if requested)
|
|
3444
|
+
area_val = None
|
|
3445
|
+
area_col_name = f"{geometry_name}_area_m2"
|
|
3446
|
+
if include_area:
|
|
3447
|
+
# Calculate geodesic area in square meters with maxError of 1m
|
|
3448
|
+
area_val = region.area(1)
|
|
3449
|
+
|
|
3450
|
+
# 4. Define the Reduction Logic
|
|
3451
|
+
def calculate_multiband_stats(image):
|
|
3452
|
+
# Base feature with date property
|
|
3453
|
+
date_val = image.get('Date_Filter')
|
|
3454
|
+
feature = ee.Feature(None, {'Date': date_val})
|
|
3455
|
+
|
|
3456
|
+
# If requested, add the static area value to every feature
|
|
3457
|
+
if include_area:
|
|
3458
|
+
feature = feature.set(area_col_name, area_val)
|
|
3459
|
+
|
|
3460
|
+
unique_reducers = list(set(reducers_list))
|
|
3461
|
+
|
|
3462
|
+
# OPTIMIZED PATH: Single reducer type for all bands
|
|
3463
|
+
if len(unique_reducers) == 1:
|
|
3464
|
+
r_type = unique_reducers[0]
|
|
3465
|
+
try:
|
|
3466
|
+
reducer = getattr(ee.Reducer, r_type)()
|
|
3467
|
+
except AttributeError:
|
|
3468
|
+
reducer = ee.Reducer.mean()
|
|
3469
|
+
|
|
3470
|
+
if unweighted:
|
|
3471
|
+
reducer = reducer.unweighted()
|
|
3472
|
+
|
|
3473
|
+
stats = image.reduceRegion(
|
|
3474
|
+
reducer=reducer,
|
|
3475
|
+
geometry=region,
|
|
3476
|
+
scale=scale,
|
|
3477
|
+
maxPixels=1e13
|
|
3478
|
+
)
|
|
3479
|
+
|
|
3480
|
+
for band in bands:
|
|
3481
|
+
col_name = f"{band}_{geometry_name}_{r_type}"
|
|
3482
|
+
val = stats.get(band)
|
|
3483
|
+
feature = feature.set(col_name, val)
|
|
3484
|
+
|
|
3485
|
+
# ITERATIVE PATH: Different reducers for different bands
|
|
3486
|
+
else:
|
|
3487
|
+
for band, r_type in zip(bands, reducers_list):
|
|
3488
|
+
try:
|
|
3489
|
+
reducer = getattr(ee.Reducer, r_type)()
|
|
3490
|
+
except AttributeError:
|
|
3491
|
+
reducer = ee.Reducer.mean()
|
|
3492
|
+
|
|
3493
|
+
if unweighted:
|
|
3494
|
+
reducer = reducer.unweighted()
|
|
3495
|
+
|
|
3496
|
+
stats = image.select(band).reduceRegion(
|
|
3497
|
+
reducer=reducer,
|
|
3498
|
+
geometry=region,
|
|
3499
|
+
scale=scale,
|
|
3500
|
+
maxPixels=1e13
|
|
3501
|
+
)
|
|
3502
|
+
|
|
3503
|
+
val = stats.get(band)
|
|
3504
|
+
col_name = f"{band}_{geometry_name}_{r_type}"
|
|
3505
|
+
feature = feature.set(col_name, val)
|
|
3506
|
+
|
|
3507
|
+
return feature
|
|
3508
|
+
|
|
3509
|
+
# 5. Execute Server-Side Mapping (with explicit Cast)
|
|
3510
|
+
results_fc = ee.FeatureCollection(processing_col.map(calculate_multiband_stats))
|
|
3511
|
+
|
|
3512
|
+
# 6. Client-Side Conversion
|
|
3513
|
+
try:
|
|
3514
|
+
df = GenericCollection.ee_to_df(results_fc, remove_geom=True)
|
|
3515
|
+
except Exception as e:
|
|
3516
|
+
raise RuntimeError(f"Failed to convert Earth Engine results to DataFrame. Error: {e}")
|
|
3517
|
+
|
|
3518
|
+
if df.empty:
|
|
3519
|
+
print("Warning: No results returned. Check if the geometry intersects the imagery or if dates are valid.")
|
|
3520
|
+
return pd.DataFrame()
|
|
3521
|
+
|
|
3522
|
+
# 7. Formatting & Reordering
|
|
3523
|
+
if 'Date' in df.columns:
|
|
3524
|
+
df['Date'] = pd.to_datetime(df['Date'])
|
|
3525
|
+
df = df.sort_values('Date').set_index('Date')
|
|
3526
|
+
|
|
3527
|
+
# Construct the expected column names in the exact order of the input lists
|
|
3528
|
+
expected_order = [f"{band}_{geometry_name}_{r_type}" for band, r_type in zip(bands, reducers_list)]
|
|
3529
|
+
|
|
3530
|
+
# If area was included, append it to the END of the list
|
|
3531
|
+
if include_area:
|
|
3532
|
+
expected_order.append(area_col_name)
|
|
3533
|
+
|
|
3534
|
+
# Reindex the DataFrame to match this order.
|
|
3535
|
+
existing_cols = [c for c in expected_order if c in df.columns]
|
|
3536
|
+
df = df[existing_cols]
|
|
3537
|
+
|
|
3538
|
+
# 8. Export (Optional)
|
|
3539
|
+
if file_path:
|
|
3540
|
+
if not file_path.lower().endswith('.csv'):
|
|
3541
|
+
file_path += '.csv'
|
|
3542
|
+
try:
|
|
3543
|
+
df.to_csv(file_path)
|
|
3544
|
+
print(f"Multiband zonal stats saved to {file_path}")
|
|
3545
|
+
except Exception as e:
|
|
3546
|
+
print(f"Error saving file to {file_path}: {e}")
|
|
3547
|
+
|
|
3548
|
+
return df
|
|
3549
|
+
|
|
3550
|
+
def sample(
|
|
3551
|
+
self,
|
|
3552
|
+
locations,
|
|
3553
|
+
band=None,
|
|
3554
|
+
scale=None,
|
|
3555
|
+
location_names=None,
|
|
3556
|
+
dates=None,
|
|
3557
|
+
file_path=None,
|
|
3558
|
+
tileScale=1
|
|
3559
|
+
):
|
|
3560
|
+
"""
|
|
3561
|
+
Extracts time-series pixel values for a list of locations.
|
|
3562
|
+
|
|
3563
|
+
|
|
3564
|
+
Args:
|
|
3565
|
+
locations (list, tuple, ee.Geometry, or ee.FeatureCollection): Input points.
|
|
3566
|
+
band (str, optional): The name of the band to sample. Defaults to the first band.
|
|
3567
|
+
scale (int, optional): Scale in meters. Defaults to 30 if None.
|
|
3568
|
+
location_names (list of str, optional): Custom names for locations.
|
|
3569
|
+
dates (list, optional): Date filter ['YYYY-MM-DD'].
|
|
3570
|
+
file_path (str, optional): CSV export path.
|
|
3571
|
+
tileScale (int, optional): Aggregation tile scale. Defaults to 1.
|
|
3572
|
+
|
|
3573
|
+
Returns:
|
|
3574
|
+
pd.DataFrame (or CSV if file_path is provided): DataFrame indexed by Date, columns by Location.
|
|
3575
|
+
"""
|
|
3576
|
+
col = self.collection
|
|
3577
|
+
if dates:
|
|
3578
|
+
col = col.filter(ee.Filter.inList('Date_Filter', dates))
|
|
3579
|
+
|
|
3580
|
+
first_img = col.first()
|
|
3581
|
+
available_bands = first_img.bandNames().getInfo()
|
|
3582
|
+
|
|
3583
|
+
if band:
|
|
3584
|
+
if band not in available_bands:
|
|
3585
|
+
raise ValueError(f"Band '{band}' not found. Available: {available_bands}")
|
|
3586
|
+
target_band = band
|
|
3587
|
+
else:
|
|
3588
|
+
target_band = available_bands[0]
|
|
3589
|
+
|
|
3590
|
+
processing_col = col.select([target_band])
|
|
3591
|
+
|
|
3592
|
+
def set_name(f):
|
|
3593
|
+
name = ee.Algorithms.If(
|
|
3594
|
+
f.get('geo_name'), f.get('geo_name'),
|
|
3595
|
+
ee.Algorithms.If(f.get('name'), f.get('name'),
|
|
3596
|
+
ee.Algorithms.If(f.get('system:index'), f.get('system:index'), 'unnamed'))
|
|
3597
|
+
)
|
|
3598
|
+
return f.set('geo_name', name)
|
|
3599
|
+
|
|
3600
|
+
if isinstance(locations, (ee.FeatureCollection, ee.Feature)):
|
|
3601
|
+
features = ee.FeatureCollection(locations)
|
|
3602
|
+
elif isinstance(locations, ee.Geometry):
|
|
3603
|
+
lbl = location_names[0] if (location_names and location_names[0]) else 'Point_1'
|
|
3604
|
+
features = ee.FeatureCollection([ee.Feature(locations).set('geo_name', lbl)])
|
|
3605
|
+
elif isinstance(locations, tuple) and len(locations) == 2:
|
|
3606
|
+
lbl = location_names[0] if location_names else 'Location_1'
|
|
3607
|
+
features = ee.FeatureCollection([ee.Feature(ee.Geometry.Point(locations), {'geo_name': lbl})])
|
|
3608
|
+
elif isinstance(locations, list):
|
|
3609
|
+
if all(isinstance(i, tuple) for i in locations):
|
|
3610
|
+
names = location_names if location_names else [f"Loc_{i+1}" for i in range(len(locations))]
|
|
3611
|
+
features = ee.FeatureCollection([
|
|
3612
|
+
ee.Feature(ee.Geometry.Point(p), {'geo_name': str(n)}) for p, n in zip(locations, names)
|
|
3613
|
+
])
|
|
3614
|
+
elif all(isinstance(i, ee.Geometry) for i in locations):
|
|
3615
|
+
names = location_names if location_names else [f"Geom_{i+1}" for i in range(len(locations))]
|
|
3616
|
+
features = ee.FeatureCollection([
|
|
3617
|
+
ee.Feature(g, {'geo_name': str(n)}) for g, n in zip(locations, names)
|
|
3618
|
+
])
|
|
3619
|
+
else:
|
|
3620
|
+
raise ValueError("List must contain (lon, lat) tuples or ee.Geometry objects.")
|
|
3621
|
+
else:
|
|
3622
|
+
raise TypeError("Invalid locations input.")
|
|
3623
|
+
|
|
3624
|
+
features = features.map(set_name)
|
|
3625
|
+
|
|
3626
|
+
|
|
3627
|
+
def sample_image(img):
|
|
3628
|
+
date = img.get('Date_Filter')
|
|
3629
|
+
use_scale = scale if scale is not None else 30
|
|
3630
|
+
|
|
3631
|
+
|
|
3632
|
+
default_dict = ee.Dictionary({target_band: -9999})
|
|
3633
|
+
|
|
3634
|
+
def extract_point(f):
|
|
3635
|
+
stats = img.reduceRegion(
|
|
3636
|
+
reducer=ee.Reducer.first(),
|
|
3637
|
+
geometry=f.geometry(),
|
|
3638
|
+
scale=use_scale,
|
|
3639
|
+
tileScale=tileScale
|
|
3640
|
+
)
|
|
3641
|
+
|
|
3642
|
+
# Combine dictionaries.
|
|
3643
|
+
# If stats has 'target_band' (even if 0), it overwrites -9999.
|
|
3644
|
+
# If stats is empty (masked), -9999 remains.
|
|
3645
|
+
safe_stats = default_dict.combine(stats, overwrite=True)
|
|
3646
|
+
val = safe_stats.get(target_band)
|
|
3647
|
+
|
|
3648
|
+
return f.set({
|
|
3649
|
+
target_band: val,
|
|
3650
|
+
'image_date': date
|
|
3651
|
+
})
|
|
3652
|
+
|
|
3653
|
+
return features.map(extract_point)
|
|
3654
|
+
|
|
3655
|
+
# Flatten the results
|
|
3656
|
+
flat_results = processing_col.map(sample_image).flatten()
|
|
3657
|
+
|
|
3658
|
+
df = GenericCollection.ee_to_df(
|
|
3659
|
+
flat_results,
|
|
3660
|
+
columns=['image_date', 'geo_name', target_band],
|
|
3661
|
+
remove_geom=True
|
|
3662
|
+
)
|
|
3663
|
+
|
|
3664
|
+
if df.empty:
|
|
3665
|
+
print("Warning: No data returned.")
|
|
3666
|
+
return pd.DataFrame()
|
|
3667
|
+
|
|
3668
|
+
# 6. Clean and Pivot
|
|
3669
|
+
df[target_band] = pd.to_numeric(df[target_band], errors='coerce')
|
|
3670
|
+
|
|
3671
|
+
# Filter out ONLY the sentinel value (-9999), preserving 0.
|
|
3672
|
+
df = df[df[target_band] != -9999]
|
|
3673
|
+
|
|
3674
|
+
if df.empty:
|
|
3675
|
+
print(f"Warning: All data points were masked (NoData) for band '{target_band}'.")
|
|
3676
|
+
return pd.DataFrame()
|
|
3677
|
+
|
|
3678
|
+
pivot_df = df.pivot(index='image_date', columns='geo_name', values=target_band)
|
|
3679
|
+
pivot_df.index.name = 'Date'
|
|
3680
|
+
pivot_df.columns.name = None
|
|
3681
|
+
pivot_df = pivot_df.reset_index()
|
|
3682
|
+
|
|
3683
|
+
if file_path:
|
|
3684
|
+
if not file_path.lower().endswith('.csv'):
|
|
3685
|
+
file_path += '.csv'
|
|
3686
|
+
pivot_df.to_csv(file_path, index=False)
|
|
3687
|
+
print(f"Sampled data saved to {file_path}")
|
|
3688
|
+
return None
|
|
3689
|
+
|
|
3690
|
+
return pivot_df
|
|
3691
|
+
|
|
3692
|
+
def multiband_sample(
|
|
3693
|
+
self,
|
|
3694
|
+
location,
|
|
3695
|
+
scale=30,
|
|
3696
|
+
file_path=None
|
|
3697
|
+
):
|
|
3698
|
+
"""
|
|
3699
|
+
Extracts ALL band values for a SINGLE location across the entire collection.
|
|
3700
|
+
|
|
3701
|
+
Args:
|
|
3702
|
+
location (tuple or ee.Geometry): A single (lon, lat) tuple OR ee.Geometry.
|
|
3703
|
+
scale (int, optional): Scale in meters. Defaults to 30.
|
|
3704
|
+
file_path (str, optional): Path to save CSV.
|
|
3705
|
+
|
|
3706
|
+
Returns:
|
|
3707
|
+
pd.DataFrame: DataFrame indexed by Date, with columns for each Band.
|
|
3708
|
+
"""
|
|
3709
|
+
if isinstance(location, tuple) and len(location) == 2:
|
|
3710
|
+
geom = ee.Geometry.Point(location)
|
|
3711
|
+
elif isinstance(location, ee.Geometry):
|
|
3712
|
+
geom = location
|
|
3713
|
+
else:
|
|
3714
|
+
raise ValueError("Location must be a single (lon, lat) tuple or ee.Geometry.")
|
|
3715
|
+
|
|
3716
|
+
first_img = self.collection.first()
|
|
3717
|
+
band_names = first_img.bandNames()
|
|
3718
|
+
|
|
3719
|
+
# Create a dictionary of {band_name: -9999}
|
|
3720
|
+
# fill missing values so the Feature structure is consistent
|
|
3721
|
+
dummy_values = ee.List.repeat(-9999, band_names.length())
|
|
3722
|
+
default_dict = ee.Dictionary.fromLists(band_names, dummy_values)
|
|
3723
|
+
|
|
3724
|
+
def get_all_bands(img):
|
|
3725
|
+
date = img.get('Date_Filter')
|
|
3726
|
+
|
|
3727
|
+
# reduceRegion returns a Dictionary.
|
|
3728
|
+
# If a pixel is masked, that band key is missing from 'stats'.
|
|
3729
|
+
stats = img.reduceRegion(
|
|
3730
|
+
reducer=ee.Reducer.first(),
|
|
3731
|
+
geometry=geom,
|
|
3732
|
+
scale=scale,
|
|
3733
|
+
maxPixels=1e13
|
|
3734
|
+
)
|
|
3735
|
+
|
|
3736
|
+
# Combine stats with defaults.
|
|
3737
|
+
# overwrite=True means real data (stats) overwrites the -9999 defaults.
|
|
3738
|
+
complete_stats = default_dict.combine(stats, overwrite=True)
|
|
3739
|
+
|
|
3740
|
+
return ee.Feature(None, complete_stats).set('Date', date)
|
|
3741
|
+
|
|
3742
|
+
fc = ee.FeatureCollection(self.collection.map(get_all_bands))
|
|
3743
|
+
|
|
3744
|
+
df = GenericCollection.ee_to_df(fc, remove_geom=True)
|
|
3745
|
+
|
|
3746
|
+
if df.empty:
|
|
3747
|
+
print("Warning: No data found.")
|
|
3748
|
+
return pd.DataFrame()
|
|
3749
|
+
|
|
3750
|
+
# 6. Cleanup
|
|
3751
|
+
if 'Date' in df.columns:
|
|
3752
|
+
df['Date'] = pd.to_datetime(df['Date'])
|
|
3753
|
+
df = df.set_index('Date').sort_index()
|
|
3754
|
+
|
|
3755
|
+
# Replace our sentinel -9999 with proper NaNs
|
|
3756
|
+
df = df.replace(-9999, np.nan)
|
|
3757
|
+
|
|
3758
|
+
# 7. Export
|
|
3759
|
+
if file_path:
|
|
3760
|
+
if not file_path.lower().endswith('.csv'):
|
|
3761
|
+
file_path += '.csv'
|
|
3762
|
+
df.to_csv(file_path)
|
|
3763
|
+
print(f"Multiband sample saved to {file_path}")
|
|
3764
|
+
return None
|
|
3765
|
+
|
|
3766
|
+
return df
|
|
3281
3767
|
|
|
3282
3768
|
def export_to_asset_collection(
|
|
3283
3769
|
self,
|
|
@@ -3288,7 +3774,8 @@ class GenericCollection:
|
|
|
3288
3774
|
filename_prefix="",
|
|
3289
3775
|
crs=None,
|
|
3290
3776
|
max_pixels=int(1e13),
|
|
3291
|
-
description_prefix="export"
|
|
3777
|
+
description_prefix="export",
|
|
3778
|
+
overwrite=False
|
|
3292
3779
|
):
|
|
3293
3780
|
"""
|
|
3294
3781
|
Exports an image collection to a Google Earth Engine asset collection. The asset collection will be created if it does not already exist,
|
|
@@ -3303,6 +3790,7 @@ class GenericCollection:
|
|
|
3303
3790
|
crs (str, optional): The coordinate reference system. Defaults to None, which will use the image's CRS.
|
|
3304
3791
|
max_pixels (int, optional): The maximum number of pixels. Defaults to int(1e13).
|
|
3305
3792
|
description_prefix (str, optional): The description prefix. Defaults to "export".
|
|
3793
|
+
overwrite (bool, optional): Whether to overwrite existing assets. Defaults to False.
|
|
3306
3794
|
|
|
3307
3795
|
Returns:
|
|
3308
3796
|
None: (queues export tasks)
|
|
@@ -3320,6 +3808,14 @@ class GenericCollection:
|
|
|
3320
3808
|
asset_id = asset_collection_path + "/" + filename_prefix + date_str
|
|
3321
3809
|
desc = description_prefix + "_" + filename_prefix + date_str
|
|
3322
3810
|
|
|
3811
|
+
if overwrite:
|
|
3812
|
+
try:
|
|
3813
|
+
ee.data.deleteAsset(asset_id)
|
|
3814
|
+
print(f"Overwriting: Deleted existing asset {asset_id}")
|
|
3815
|
+
except ee.EEException:
|
|
3816
|
+
# Asset does not exist, so nothing to delete. Proceed safely.
|
|
3817
|
+
pass
|
|
3818
|
+
|
|
3323
3819
|
params = {
|
|
3324
3820
|
'image': img,
|
|
3325
3821
|
'description': desc,
|