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
|
# ---- Reflectance scaling for Sentinel-2 L2A (HARMONIZED) ----
|
|
@@ -68,7 +69,7 @@ class Sentinel2Collection:
|
|
|
68
69
|
... cloud_percentage_threshold=20,
|
|
69
70
|
... nodata_threshold=10,
|
|
70
71
|
... )
|
|
71
|
-
>>> mosaic_collection = image_collection.
|
|
72
|
+
>>> mosaic_collection = image_collection.mosaicByDate #mosaic images/tiles with same date
|
|
72
73
|
>>> cloud_masked = mosaic_collection.masked_clouds_collection #mask out clouds
|
|
73
74
|
>>> latest_image = cloud_masked.image_grab(-1) #grab latest image for viewing
|
|
74
75
|
>>> ndwi_collection = cloud_masked.ndwi #calculate ndwi for all images
|
|
@@ -196,6 +197,14 @@ class Sentinel2Collection:
|
|
|
196
197
|
self._PixelAreaSumCollection = None
|
|
197
198
|
self._Reflectance = None
|
|
198
199
|
|
|
200
|
+
def __call__(self):
|
|
201
|
+
"""
|
|
202
|
+
Allows the object to be called as a function, returning itself.
|
|
203
|
+
This enables property-like methods to be accessed with or without parentheses
|
|
204
|
+
(e.g., .mosaicByDate or .mosaicByDate()).
|
|
205
|
+
"""
|
|
206
|
+
return self
|
|
207
|
+
|
|
199
208
|
@staticmethod
|
|
200
209
|
def image_dater(image):
|
|
201
210
|
"""
|
|
@@ -564,7 +573,7 @@ class Sentinel2Collection:
|
|
|
564
573
|
return image.addBands(anomaly_image, overwrite=True)
|
|
565
574
|
|
|
566
575
|
@staticmethod
|
|
567
|
-
def
|
|
576
|
+
def maskClouds(image):
|
|
568
577
|
"""
|
|
569
578
|
Function to mask clouds using SCL band data.
|
|
570
579
|
|
|
@@ -579,7 +588,14 @@ class Sentinel2Collection:
|
|
|
579
588
|
return image.updateMask(CloudMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
580
589
|
|
|
581
590
|
@staticmethod
|
|
582
|
-
def
|
|
591
|
+
def MaskCloudsS2(image):
|
|
592
|
+
warnings.warn("MaskCloudsS2 is deprecated. Please use maskClouds instead.",
|
|
593
|
+
DeprecationWarning,
|
|
594
|
+
stacklevel=2)
|
|
595
|
+
return Sentinel2Collection.maskClouds(image)
|
|
596
|
+
|
|
597
|
+
@staticmethod
|
|
598
|
+
def maskShadows(image):
|
|
583
599
|
"""
|
|
584
600
|
Function to mask cloud shadows using SCL band data.
|
|
585
601
|
|
|
@@ -592,9 +608,16 @@ class Sentinel2Collection:
|
|
|
592
608
|
SCL = image.select("SCL")
|
|
593
609
|
ShadowMask = SCL.neq(3)
|
|
594
610
|
return image.updateMask(ShadowMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
611
|
+
|
|
612
|
+
@staticmethod
|
|
613
|
+
def MaskShadowsS2(image):
|
|
614
|
+
warnings.warn("MaskShadowsS2 is deprecated. Please use maskShadows instead.",
|
|
615
|
+
DeprecationWarning,
|
|
616
|
+
stacklevel=2)
|
|
617
|
+
return Sentinel2Collection.maskShadows(image)
|
|
595
618
|
|
|
596
619
|
@staticmethod
|
|
597
|
-
def
|
|
620
|
+
def maskWater(image):
|
|
598
621
|
"""
|
|
599
622
|
Function to mask water pixels using SCL band data.
|
|
600
623
|
|
|
@@ -607,9 +630,16 @@ class Sentinel2Collection:
|
|
|
607
630
|
SCL = image.select("SCL")
|
|
608
631
|
WaterMask = SCL.neq(6)
|
|
609
632
|
return image.updateMask(WaterMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
633
|
+
|
|
634
|
+
@staticmethod
|
|
635
|
+
def MaskWaterS2(image):
|
|
636
|
+
warnings.warn("MaskWaterS2 is deprecated. Please use maskWater instead.",
|
|
637
|
+
DeprecationWarning,
|
|
638
|
+
stacklevel=2)
|
|
639
|
+
return Sentinel2Collection.maskWater(image)
|
|
610
640
|
|
|
611
641
|
@staticmethod
|
|
612
|
-
def
|
|
642
|
+
def maskWaterByNDWI(image, threshold):
|
|
613
643
|
"""
|
|
614
644
|
Function to mask water pixels (mask land and cloud pixels) for all bands based on NDWI and a set threshold where
|
|
615
645
|
all pixels less than NDWI threshold are masked out.
|
|
@@ -626,9 +656,16 @@ class Sentinel2Collection:
|
|
|
626
656
|
) # green-NIR / green+NIR -- full NDWI image
|
|
627
657
|
water = image.updateMask(ndwi_calc.lt(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
628
658
|
return water
|
|
659
|
+
|
|
660
|
+
@staticmethod
|
|
661
|
+
def MaskWaterS2ByNDWI(image, threshold):
|
|
662
|
+
warnings.warn("MaskWaterS2ByNDWI is deprecated. Please use maskWaterByNDWI instead.",
|
|
663
|
+
DeprecationWarning,
|
|
664
|
+
stacklevel=2)
|
|
665
|
+
return Sentinel2Collection.maskWaterByNDWI(image, threshold)
|
|
629
666
|
|
|
630
667
|
@staticmethod
|
|
631
|
-
def
|
|
668
|
+
def maskToWater(image):
|
|
632
669
|
"""
|
|
633
670
|
Function to mask to water pixels (mask land and cloud pixels) using SCL band data.
|
|
634
671
|
|
|
@@ -641,6 +678,13 @@ class Sentinel2Collection:
|
|
|
641
678
|
SCL = image.select("SCL")
|
|
642
679
|
WaterMask = SCL.eq(6)
|
|
643
680
|
return image.updateMask(WaterMask).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
681
|
+
|
|
682
|
+
@staticmethod
|
|
683
|
+
def MaskToWaterS2(image):
|
|
684
|
+
warnings.warn("MaskToWaterS2 is deprecated. Please use maskToWater instead.",
|
|
685
|
+
DeprecationWarning,
|
|
686
|
+
stacklevel=2)
|
|
687
|
+
return Sentinel2Collection.maskToWater(image)
|
|
644
688
|
|
|
645
689
|
@staticmethod
|
|
646
690
|
def halite_mask(image, threshold):
|
|
@@ -748,7 +792,7 @@ class Sentinel2Collection:
|
|
|
748
792
|
return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask).set('system:time_start', image_to_mask.get('system:time_start'))
|
|
749
793
|
|
|
750
794
|
@staticmethod
|
|
751
|
-
def
|
|
795
|
+
def maskToWaterByNDWI(image, threshold):
|
|
752
796
|
"""
|
|
753
797
|
Function to mask all bands to water pixels (mask land and cloud pixels) based on NDWI.
|
|
754
798
|
|
|
@@ -764,9 +808,16 @@ class Sentinel2Collection:
|
|
|
764
808
|
) # green-NIR / green+NIR -- full NDWI image
|
|
765
809
|
water = image.updateMask(ndwi_calc.gte(threshold)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
766
810
|
return water
|
|
811
|
+
|
|
812
|
+
@staticmethod
|
|
813
|
+
def MaskToWaterS2ByNDWI(image, threshold):
|
|
814
|
+
warnings.warn("MaskToWaterS2ByNDWI is deprecated. Please use maskToWaterByNDWI instead.",
|
|
815
|
+
DeprecationWarning,
|
|
816
|
+
stacklevel=2)
|
|
817
|
+
return Sentinel2Collection.maskToWaterByNDWI(image, threshold)
|
|
767
818
|
|
|
768
819
|
@staticmethod
|
|
769
|
-
def
|
|
820
|
+
def pixelAreaSum(
|
|
770
821
|
image, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12
|
|
771
822
|
):
|
|
772
823
|
"""
|
|
@@ -825,8 +876,17 @@ class Sentinel2Collection:
|
|
|
825
876
|
# Call to iterate the calculate_and_set_area function over the list of bands, starting with the original image
|
|
826
877
|
final_image = ee.Image(bands.iterate(calculate_and_set_area, image))
|
|
827
878
|
return final_image
|
|
879
|
+
|
|
880
|
+
@staticmethod
|
|
881
|
+
def PixelAreaSum(
|
|
882
|
+
image, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12
|
|
883
|
+
):
|
|
884
|
+
warnings.warn("PixelAreaSum is deprecated. Please use pixelAreaSum instead.",
|
|
885
|
+
DeprecationWarning,
|
|
886
|
+
stacklevel=2)
|
|
887
|
+
return Sentinel2Collection.pixelAreaSum(image, band_name, geometry, threshold, scale, maxPixels)
|
|
828
888
|
|
|
829
|
-
def
|
|
889
|
+
def pixelAreaSumCollection(
|
|
830
890
|
self, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
831
891
|
):
|
|
832
892
|
"""
|
|
@@ -853,7 +913,7 @@ class Sentinel2Collection:
|
|
|
853
913
|
collection = self.collection
|
|
854
914
|
# Area calculation for each image in the collection, using the PixelAreaSum function
|
|
855
915
|
AreaCollection = collection.map(
|
|
856
|
-
lambda image: Sentinel2Collection.
|
|
916
|
+
lambda image: Sentinel2Collection.pixelAreaSum(
|
|
857
917
|
image,
|
|
858
918
|
band_name=band_name,
|
|
859
919
|
geometry=geometry,
|
|
@@ -869,17 +929,25 @@ class Sentinel2Collection:
|
|
|
869
929
|
|
|
870
930
|
# If an export path is provided, the area data will be exported to a CSV file
|
|
871
931
|
if area_data_export_path:
|
|
872
|
-
Sentinel2Collection(collection=self._PixelAreaSumCollection).
|
|
932
|
+
Sentinel2Collection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
|
|
873
933
|
# Returning the result in the desired format based on output_type argument or raising an error for invalid input
|
|
874
934
|
if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
|
|
875
935
|
return self._PixelAreaSumCollection
|
|
876
936
|
elif output_type == 'Sentinel2Collection':
|
|
877
937
|
return Sentinel2Collection(collection=self._PixelAreaSumCollection)
|
|
878
938
|
elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
|
|
879
|
-
return Sentinel2Collection(collection=self._PixelAreaSumCollection).
|
|
939
|
+
return Sentinel2Collection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names)
|
|
880
940
|
else:
|
|
881
941
|
raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'Sentinel2Collection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
|
|
882
942
|
|
|
943
|
+
def PixelAreaSumCollection(
|
|
944
|
+
self, band_name, geometry, threshold=-1, scale=10, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
945
|
+
):
|
|
946
|
+
warnings.warn("PixelAreaSumCollection is deprecated. Please use pixelAreaSumCollection instead.",
|
|
947
|
+
DeprecationWarning,
|
|
948
|
+
stacklevel=2)
|
|
949
|
+
return self.pixelAreaSumCollection(band_name, geometry, threshold, scale, maxPixels, output_type, area_data_export_path)
|
|
950
|
+
|
|
883
951
|
@staticmethod
|
|
884
952
|
def add_month_property_fn(image):
|
|
885
953
|
"""
|
|
@@ -960,8 +1028,13 @@ class Sentinel2Collection:
|
|
|
960
1028
|
return Sentinel2Collection(collection=ee.ImageCollection(paired.map(_pair_two)))
|
|
961
1029
|
|
|
962
1030
|
# Preferred path: merge many singleband products into the parent
|
|
963
|
-
if not isinstance(collections, list) or len(collections) == 0:
|
|
964
|
-
|
|
1031
|
+
# if not isinstance(collections, list) or len(collections) == 0:
|
|
1032
|
+
# raise ValueError("Provide a non-empty list of Sentinel2Collection objects in `collections`.")
|
|
1033
|
+
if not isinstance(collections, list):
|
|
1034
|
+
collections = [collections]
|
|
1035
|
+
|
|
1036
|
+
if len(collections) == 0:
|
|
1037
|
+
raise ValueError("Provide a non-empty list of LandsatCollection objects in `collections`.")
|
|
965
1038
|
|
|
966
1039
|
result = self.collection
|
|
967
1040
|
for extra in collections:
|
|
@@ -1018,7 +1091,7 @@ class Sentinel2Collection:
|
|
|
1018
1091
|
self._dates = dates
|
|
1019
1092
|
return self._dates
|
|
1020
1093
|
|
|
1021
|
-
def
|
|
1094
|
+
def exportProperties(self, property_names, file_path=None):
|
|
1022
1095
|
"""
|
|
1023
1096
|
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.
|
|
1024
1097
|
|
|
@@ -1073,6 +1146,13 @@ class Sentinel2Collection:
|
|
|
1073
1146
|
print(f"Properties saved to {file_path}")
|
|
1074
1147
|
|
|
1075
1148
|
return df
|
|
1149
|
+
|
|
1150
|
+
def ExportProperties(self, property_names, file_path=None):
|
|
1151
|
+
warnings.warn(
|
|
1152
|
+
"The `ExportProperties` method is deprecated and will be removed in future versions. Please use the `exportProperties` method instead.",
|
|
1153
|
+
DeprecationWarning,
|
|
1154
|
+
stacklevel=2)
|
|
1155
|
+
return self.exportProperties(property_names, file_path)
|
|
1076
1156
|
|
|
1077
1157
|
def get_filtered_collection(self):
|
|
1078
1158
|
"""
|
|
@@ -2678,7 +2758,7 @@ class Sentinel2Collection:
|
|
|
2678
2758
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
2679
2759
|
"""
|
|
2680
2760
|
if self._masked_water_collection is None:
|
|
2681
|
-
col = self.collection.map(Sentinel2Collection.
|
|
2761
|
+
col = self.collection.map(Sentinel2Collection.maskWater)
|
|
2682
2762
|
self._masked_water_collection = Sentinel2Collection(collection=col)
|
|
2683
2763
|
return self._masked_water_collection
|
|
2684
2764
|
|
|
@@ -2690,7 +2770,7 @@ class Sentinel2Collection:
|
|
|
2690
2770
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
2691
2771
|
"""
|
|
2692
2772
|
col = self.collection.map(
|
|
2693
|
-
lambda image: Sentinel2Collection.
|
|
2773
|
+
lambda image: Sentinel2Collection.maskWaterByNDWI(
|
|
2694
2774
|
image, threshold=threshold
|
|
2695
2775
|
)
|
|
2696
2776
|
)
|
|
@@ -2705,7 +2785,7 @@ class Sentinel2Collection:
|
|
|
2705
2785
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
2706
2786
|
"""
|
|
2707
2787
|
if self._masked_to_water_collection is None:
|
|
2708
|
-
col = self.collection.map(Sentinel2Collection.
|
|
2788
|
+
col = self.collection.map(Sentinel2Collection.maskToWater)
|
|
2709
2789
|
self._masked_water_collection = Sentinel2Collection(collection=col)
|
|
2710
2790
|
return self._masked_water_collection
|
|
2711
2791
|
|
|
@@ -2717,7 +2797,7 @@ class Sentinel2Collection:
|
|
|
2717
2797
|
Sentinel2Collection: Sentinel2Collection image collection.
|
|
2718
2798
|
"""
|
|
2719
2799
|
col = self.collection.map(
|
|
2720
|
-
lambda image: Sentinel2Collection.
|
|
2800
|
+
lambda image: Sentinel2Collection.maskToWaterByNDWI(
|
|
2721
2801
|
image, threshold=threshold
|
|
2722
2802
|
)
|
|
2723
2803
|
)
|
|
@@ -2732,7 +2812,7 @@ class Sentinel2Collection:
|
|
|
2732
2812
|
Sentinel2Collection: masked Sentinel2Collection image collection.
|
|
2733
2813
|
"""
|
|
2734
2814
|
if self._masked_clouds_collection is None:
|
|
2735
|
-
col = self.collection.map(Sentinel2Collection.
|
|
2815
|
+
col = self.collection.map(Sentinel2Collection.maskClouds)
|
|
2736
2816
|
self._masked_clouds_collection = Sentinel2Collection(collection=col)
|
|
2737
2817
|
return self._masked_clouds_collection
|
|
2738
2818
|
|
|
@@ -2745,7 +2825,7 @@ class Sentinel2Collection:
|
|
|
2745
2825
|
Sentinel2Collection: Sentinel2Collection image collection
|
|
2746
2826
|
"""
|
|
2747
2827
|
if self._masked_shadows_collection is None:
|
|
2748
|
-
col = self.collection.map(Sentinel2Collection.
|
|
2828
|
+
col = self.collection.map(Sentinel2Collection.maskShadows)
|
|
2749
2829
|
self._masked_shadows_collection = Sentinel2Collection(collection=col)
|
|
2750
2830
|
return self._masked_shadows_collection
|
|
2751
2831
|
|
|
@@ -2760,20 +2840,15 @@ class Sentinel2Collection:
|
|
|
2760
2840
|
Sentinel2Collection: masked Sentinel2Collection image collection.
|
|
2761
2841
|
|
|
2762
2842
|
"""
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
mask = ee.Image.constant(1).clip(polygon)
|
|
2766
|
-
|
|
2767
|
-
# Update the mask of each image in the collection
|
|
2768
|
-
masked_collection = self.collection.map(lambda img: img.updateMask(mask))
|
|
2843
|
+
# Convert the polygon to a mask
|
|
2844
|
+
mask = ee.Image.constant(1).clip(polygon)
|
|
2769
2845
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
)
|
|
2846
|
+
# Update the mask of each image in the collection
|
|
2847
|
+
masked_collection = self.collection.map(lambda img: img.updateMask(mask)\
|
|
2848
|
+
.copyProperties(img).set('system:time_start', img.get('system:time_start')))
|
|
2774
2849
|
|
|
2775
2850
|
# Return the updated object
|
|
2776
|
-
return
|
|
2851
|
+
return Sentinel2Collection(collection=masked_collection)
|
|
2777
2852
|
|
|
2778
2853
|
def mask_out_polygon(self, polygon):
|
|
2779
2854
|
"""
|
|
@@ -2786,23 +2861,17 @@ class Sentinel2Collection:
|
|
|
2786
2861
|
Sentinel2Collection: masked Sentinel2Collection image collection.
|
|
2787
2862
|
|
|
2788
2863
|
"""
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
full_mask = ee.Image.constant(1)
|
|
2792
|
-
|
|
2793
|
-
# Use paint to set pixels inside polygon as 0
|
|
2794
|
-
area = full_mask.paint(polygon, 0)
|
|
2864
|
+
# Convert the polygon to a mask
|
|
2865
|
+
full_mask = ee.Image.constant(1)
|
|
2795
2866
|
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
# Update the internal collection state
|
|
2800
|
-
self._geometry_masked_out_collection = Sentinel2Collection(
|
|
2801
|
-
collection=masked_collection
|
|
2802
|
-
)
|
|
2867
|
+
# Use paint to set pixels inside polygon as 0
|
|
2868
|
+
area = full_mask.paint(polygon, 0)
|
|
2803
2869
|
|
|
2870
|
+
# Update the mask of each image in the collection
|
|
2871
|
+
masked_collection = self.collection.map(lambda img: img.updateMask(area)\
|
|
2872
|
+
.copyProperties(img).set('system:time_start', img.get('system:time_start')))
|
|
2804
2873
|
# Return the updated object
|
|
2805
|
-
return
|
|
2874
|
+
return Sentinel2Collection(collection=masked_collection)
|
|
2806
2875
|
|
|
2807
2876
|
def mask_halite(self, threshold):
|
|
2808
2877
|
"""
|
|
@@ -2977,6 +3046,9 @@ class Sentinel2Collection:
|
|
|
2977
3046
|
|
|
2978
3047
|
if geometry is not None and not isinstance(geometry, ee.Geometry):
|
|
2979
3048
|
raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
|
|
3049
|
+
|
|
3050
|
+
native_projection = image_collection.first().select(target_band).projection()
|
|
3051
|
+
|
|
2980
3052
|
# define the join, which will join all images newer than the current image
|
|
2981
3053
|
# use system:time_start if the image does not have a Date_Filter property
|
|
2982
3054
|
if join_method == 'system:time_start':
|
|
@@ -3032,7 +3104,7 @@ class Sentinel2Collection:
|
|
|
3032
3104
|
# convert the image collection to an image of s_statistic values per pixel
|
|
3033
3105
|
# where the s_statistic is the sum of partial s values
|
|
3034
3106
|
# renaming the band as 's_statistic' for later usage
|
|
3035
|
-
final_s_image = partial_s_col.sum().rename('s_statistic')
|
|
3107
|
+
final_s_image = partial_s_col.sum().rename('s_statistic').setDefaultProjection(native_projection)
|
|
3036
3108
|
|
|
3037
3109
|
|
|
3038
3110
|
########## PART 2 - VARIANCE and Z-SCORE ##########
|
|
@@ -3095,7 +3167,7 @@ class Sentinel2Collection:
|
|
|
3095
3167
|
mask = ee.Image(1).clip(geometry)
|
|
3096
3168
|
final_image = final_image.updateMask(mask)
|
|
3097
3169
|
|
|
3098
|
-
return final_image
|
|
3170
|
+
return final_image.setDefaultProjection(native_projection)
|
|
3099
3171
|
|
|
3100
3172
|
def sens_slope_trend(self, target_band=None, join_method='system:time_start', geometry=None):
|
|
3101
3173
|
"""
|
|
@@ -3130,6 +3202,8 @@ class Sentinel2Collection:
|
|
|
3130
3202
|
|
|
3131
3203
|
if geometry is not None and not isinstance(geometry, ee.Geometry):
|
|
3132
3204
|
raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
|
|
3205
|
+
|
|
3206
|
+
native_projection = image_collection.first().select(target_band).projection()
|
|
3133
3207
|
|
|
3134
3208
|
# Add Year Band (Time X-Axis)
|
|
3135
3209
|
def add_year_band(image):
|
|
@@ -3158,7 +3232,7 @@ class Sentinel2Collection:
|
|
|
3158
3232
|
mask = ee.Image(1).clip(geometry)
|
|
3159
3233
|
slope_band = slope_band.updateMask(mask)
|
|
3160
3234
|
|
|
3161
|
-
return slope_band
|
|
3235
|
+
return slope_band.setDefaultProjection(native_projection)
|
|
3162
3236
|
|
|
3163
3237
|
|
|
3164
3238
|
def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
|
|
@@ -3302,7 +3376,7 @@ class Sentinel2Collection:
|
|
|
3302
3376
|
new_col = self.collection.filter(ee.Filter.eq("Date_Filter", img_date))
|
|
3303
3377
|
return new_col.first()
|
|
3304
3378
|
|
|
3305
|
-
def
|
|
3379
|
+
def collectionStitch(self, img_col2):
|
|
3306
3380
|
"""
|
|
3307
3381
|
Function to mosaic two Sentinel2Collection objects which share image dates.
|
|
3308
3382
|
Mosaics are only formed for dates where both image collections have images.
|
|
@@ -3356,8 +3430,15 @@ class Sentinel2Collection:
|
|
|
3356
3430
|
# Return a Sentinel2Collection instance
|
|
3357
3431
|
return Sentinel2Collection(collection=new_col)
|
|
3358
3432
|
|
|
3433
|
+
def CollectionStitch(self, img_col2):
|
|
3434
|
+
warnings.warn(
|
|
3435
|
+
"The `CollectionStitch` method is deprecated and will be removed in future versions. Please use the `collectionStitch` method instead.",
|
|
3436
|
+
DeprecationWarning,
|
|
3437
|
+
stacklevel=2)
|
|
3438
|
+
return self.collectionStitch(img_col2)
|
|
3439
|
+
|
|
3359
3440
|
@property
|
|
3360
|
-
def
|
|
3441
|
+
def mosaicByDateDepr(self):
|
|
3361
3442
|
"""
|
|
3362
3443
|
Property attribute function to mosaic collection images that share the same date. The properties CLOUD_PIXEL_PERCENTAGE and NODATA_PIXEL_PERCENTAGE
|
|
3363
3444
|
for each image are used to calculate an overall mean, which replaces the CLOUD_PIXEL_PERCENTAGE and NODATA_PIXEL_PERCENTAGE for each mosaiced image.
|
|
@@ -3423,6 +3504,77 @@ class Sentinel2Collection:
|
|
|
3423
3504
|
self._MosaicByDate = col
|
|
3424
3505
|
|
|
3425
3506
|
return self._MosaicByDate
|
|
3507
|
+
|
|
3508
|
+
@property
|
|
3509
|
+
def mosaicByDate(self):
|
|
3510
|
+
"""
|
|
3511
|
+
Property attribute function to mosaic collection images that share the same date.
|
|
3512
|
+
|
|
3513
|
+
The property CLOUD_COVER for each image is used to calculate an overall mean,
|
|
3514
|
+
which replaces the CLOUD_COVER property for each mosaiced image.
|
|
3515
|
+
Server-side friendly.
|
|
3516
|
+
|
|
3517
|
+
NOTE: if images are removed from the collection from cloud filtering, you may have mosaics composed of only one image.
|
|
3518
|
+
|
|
3519
|
+
Returns:
|
|
3520
|
+
LandsatCollection: LandsatCollection image collection with mosaiced imagery and mean CLOUD_COVER as a property
|
|
3521
|
+
"""
|
|
3522
|
+
if self._MosaicByDate is None:
|
|
3523
|
+
distinct_dates = self.collection.distinct("Date_Filter")
|
|
3524
|
+
|
|
3525
|
+
# Define a join to link images by Date_Filter
|
|
3526
|
+
filter_date = ee.Filter.equals(leftField="Date_Filter", rightField="Date_Filter")
|
|
3527
|
+
join = ee.Join.saveAll(matchesKey="date_matches")
|
|
3528
|
+
|
|
3529
|
+
# Apply the join
|
|
3530
|
+
# Primary: Distinct dates collection
|
|
3531
|
+
# Secondary: The full original collection
|
|
3532
|
+
joined_col = ee.ImageCollection(join.apply(distinct_dates, self.collection, filter_date))
|
|
3533
|
+
|
|
3534
|
+
# Define the mosaicking function
|
|
3535
|
+
def _mosaic_day(img):
|
|
3536
|
+
# Recover the list of images for this day
|
|
3537
|
+
daily_list = ee.List(img.get("date_matches"))
|
|
3538
|
+
daily_col = ee.ImageCollection.fromImages(daily_list)
|
|
3539
|
+
|
|
3540
|
+
# Create the mosaic
|
|
3541
|
+
mosaic = daily_col.mosaic().setDefaultProjection(img.select([0]).projection())
|
|
3542
|
+
|
|
3543
|
+
# Calculate means for Sentinel-2 specific props
|
|
3544
|
+
cloud_pct = daily_col.aggregate_mean("CLOUDY_PIXEL_PERCENTAGE")
|
|
3545
|
+
nodata_pct = daily_col.aggregate_mean("NODATA_PIXEL_PERCENTAGE")
|
|
3546
|
+
|
|
3547
|
+
# Properties to preserve from the representative image
|
|
3548
|
+
props_of_interest = [
|
|
3549
|
+
"SPACECRAFT_NAME",
|
|
3550
|
+
"SENSING_ORBIT_NUMBER",
|
|
3551
|
+
"SENSING_ORBIT_DIRECTION",
|
|
3552
|
+
"MISSION_ID",
|
|
3553
|
+
"PLATFORM_IDENTIFIER",
|
|
3554
|
+
"system:time_start",
|
|
3555
|
+
"Date_Filter"
|
|
3556
|
+
]
|
|
3557
|
+
|
|
3558
|
+
# Return mosaic with properties set
|
|
3559
|
+
return mosaic.copyProperties(img, props_of_interest).set({
|
|
3560
|
+
"CLOUDY_PIXEL_PERCENTAGE": cloud_pct,
|
|
3561
|
+
"NODATA_PIXEL_PERCENTAGE": nodata_pct
|
|
3562
|
+
})
|
|
3563
|
+
|
|
3564
|
+
# 5. Map the function and wrap the result
|
|
3565
|
+
mosaiced_col = joined_col.map(_mosaic_day)
|
|
3566
|
+
self._MosaicByDate = Sentinel2Collection(collection=mosaiced_col)
|
|
3567
|
+
|
|
3568
|
+
# Convert the list of mosaics to an ImageCollection
|
|
3569
|
+
return self._MosaicByDate
|
|
3570
|
+
|
|
3571
|
+
@property
|
|
3572
|
+
def MosaicByDate(self):
|
|
3573
|
+
warnings.warn(
|
|
3574
|
+
"The `MosaicByDate` property is deprecated and will be removed in future versions. Please use the `mosaicByDate` property instead.",
|
|
3575
|
+
DeprecationWarning,
|
|
3576
|
+
stacklevel=2)
|
|
3577
|
+
return self.mosaicByDate
|
|
3426
3578
|
|
|
3427
3579
|
@staticmethod
|
|
3428
3580
|
def ee_to_df(
|
|
@@ -3642,200 +3794,197 @@ class Sentinel2Collection:
|
|
|
3642
3794
|
lines,
|
|
3643
3795
|
line_names,
|
|
3644
3796
|
reducer="mean",
|
|
3645
|
-
dist_interval=
|
|
3797
|
+
dist_interval=30,
|
|
3646
3798
|
n_segments=None,
|
|
3647
3799
|
scale=10,
|
|
3648
3800
|
processing_mode='aggregated',
|
|
3649
3801
|
save_folder_path=None,
|
|
3650
3802
|
sampling_method='line',
|
|
3651
|
-
point_buffer_radius=
|
|
3803
|
+
point_buffer_radius=15,
|
|
3804
|
+
batch_size=10
|
|
3652
3805
|
):
|
|
3653
3806
|
"""
|
|
3654
|
-
Computes and returns pixel values along transects
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
for maximum flexibility and performance.
|
|
3659
|
-
|
|
3660
|
-
There are two processing modes available, aggregated and iterative:
|
|
3661
|
-
- 'aggregated' (default; suggested): Fast, server-side processing. Fetches all results
|
|
3662
|
-
in a single request. Highly recommended. Returns a dictionary of pandas DataFrames.
|
|
3663
|
-
- 'iterative': Slower, client-side loop that processes one image at a time.
|
|
3664
|
-
Kept for backward compatibility (effectively depreciated). Returns None and saves individual CSVs.
|
|
3665
|
-
This method is not recommended unless absolutely necessary, as it is less efficient and may be subject to client-side timeouts.
|
|
3666
|
-
|
|
3807
|
+
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
|
|
3808
|
+
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).
|
|
3809
|
+
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`.
|
|
3810
|
+
|
|
3667
3811
|
Args:
|
|
3668
|
-
lines (list):
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
each transect line into for sampling. This parameter overrides `dist_interval`.
|
|
3680
|
-
Defaults to None.
|
|
3681
|
-
scale (int, optional): The nominal scale in meters for the reduction,
|
|
3682
|
-
which should typically match the pixel resolution of the imagery.
|
|
3683
|
-
Defaults to 10.
|
|
3684
|
-
processing_mode (str, optional): The method for processing the collection.
|
|
3685
|
-
- 'aggregated' (default): Fast, server-side processing. Fetches all
|
|
3686
|
-
results in a single request. Highly recommended. Returns a dictionary
|
|
3687
|
-
of pandas DataFrames.
|
|
3688
|
-
- 'iterative': Slower, client-side loop that processes one image at a
|
|
3689
|
-
time. Kept for backward compatibility. Returns None and saves
|
|
3690
|
-
individual CSVs.
|
|
3691
|
-
save_folder_path (str, optional): If provided, the function will save the
|
|
3692
|
-
resulting transect data to CSV files. The behavior depends on the
|
|
3693
|
-
`processing_mode`:
|
|
3694
|
-
- In 'aggregated' mode, one CSV is saved for each transect,
|
|
3695
|
-
containing all dates. (e.g., 'MyTransect_transects.csv').
|
|
3696
|
-
- In 'iterative' mode, one CSV is saved for each date,
|
|
3697
|
-
containing all transects. (e.g., '2022-06-15_transects.csv').
|
|
3698
|
-
sampling_method (str, optional): The geometric method used for sampling.
|
|
3699
|
-
- 'line' (default): Reduces all pixels intersecting each small line
|
|
3700
|
-
segment. This can be unreliable and produce blank rows if
|
|
3701
|
-
`dist_interval` is too small relative to the `scale`.
|
|
3702
|
-
- 'buffered_point': Reduces all pixels within a buffer around the
|
|
3703
|
-
midpoint of each line segment. This method is more robust and
|
|
3704
|
-
reliably avoids blank rows, but may not reduce all pixels along a line segment.
|
|
3705
|
-
point_buffer_radius (int, optional): The radius in meters for the buffer
|
|
3706
|
-
when `sampling_method` is 'buffered_point'. Defaults to 5.
|
|
3812
|
+
lines (list): List of ee.Geometry.LineString objects.
|
|
3813
|
+
line_names (list): List of string names for each transect.
|
|
3814
|
+
reducer (str, optional): Reducer name. Defaults to 'mean'.
|
|
3815
|
+
dist_interval (float, optional): Distance interval in meters. Defaults to 30.
|
|
3816
|
+
n_segments (int, optional): Number of segments (overrides dist_interval).
|
|
3817
|
+
scale (int, optional): Scale in meters. Defaults to 10.
|
|
3818
|
+
processing_mode (str, optional): 'aggregated' or 'iterative'.
|
|
3819
|
+
save_folder_path (str, optional): Path to save CSVs.
|
|
3820
|
+
sampling_method (str, optional): 'line' or 'buffered_point'.
|
|
3821
|
+
point_buffer_radius (int, optional): Buffer radius if using 'buffered_point'.
|
|
3822
|
+
batch_size (int, optional): Images per request in 'aggregated' mode. Defaults to 10. Lower the value if you encounter a 'Too many aggregations' error.
|
|
3707
3823
|
|
|
3708
3824
|
Returns:
|
|
3709
|
-
dict or None:
|
|
3710
|
-
- If `processing_mode` is 'aggregated', returns a dictionary where each
|
|
3711
|
-
key is a transect name and each value is a pandas DataFrame. In the
|
|
3712
|
-
DataFrame, the index is the distance along the transect and each
|
|
3713
|
-
column represents an image date. Optionally saves CSV files if
|
|
3714
|
-
`save_folder_path` is provided.
|
|
3715
|
-
- If `processing_mode` is 'iterative', returns None as it saves
|
|
3716
|
-
files directly.
|
|
3717
|
-
|
|
3718
|
-
Raises:
|
|
3719
|
-
ValueError: If `lines` and `line_names` have different lengths, or if
|
|
3720
|
-
an unknown reducer or processing mode is specified.
|
|
3825
|
+
dict or None: Dictionary of DataFrames (aggregated) or None (iterative).
|
|
3721
3826
|
"""
|
|
3722
|
-
# Validating inputs
|
|
3723
3827
|
if len(lines) != len(line_names):
|
|
3724
3828
|
raise ValueError("'lines' and 'line_names' must have the same number of elements.")
|
|
3725
|
-
|
|
3829
|
+
|
|
3830
|
+
first_img = self.collection.first()
|
|
3831
|
+
bands = first_img.bandNames().getInfo()
|
|
3832
|
+
is_multiband = len(bands) > 1
|
|
3833
|
+
|
|
3834
|
+
# Setup robust dictionary for handling masked/zero values
|
|
3835
|
+
default_val = -9999
|
|
3836
|
+
dummy_dict = ee.Dictionary.fromLists(bands, ee.List.repeat(default_val, len(bands)))
|
|
3837
|
+
|
|
3838
|
+
if is_multiband:
|
|
3839
|
+
reducer_cols = [f"{b}_{reducer}" for b in bands]
|
|
3840
|
+
clean_names = bands
|
|
3841
|
+
rename_keys = bands
|
|
3842
|
+
rename_vals = reducer_cols
|
|
3843
|
+
else:
|
|
3844
|
+
reducer_cols = [reducer]
|
|
3845
|
+
clean_names = [bands[0]]
|
|
3846
|
+
rename_keys = bands
|
|
3847
|
+
rename_vals = reducer_cols
|
|
3848
|
+
|
|
3849
|
+
print("Pre-computing transect geometries from input LineString(s)...")
|
|
3850
|
+
|
|
3851
|
+
master_transect_fc = ee.FeatureCollection([])
|
|
3852
|
+
geom_error = 1.0
|
|
3853
|
+
|
|
3854
|
+
for i, line in enumerate(lines):
|
|
3855
|
+
line_name = line_names[i]
|
|
3856
|
+
length = line.length(geom_error)
|
|
3857
|
+
|
|
3858
|
+
eff_interval = length.divide(n_segments) if n_segments else dist_interval
|
|
3859
|
+
|
|
3860
|
+
distances = ee.List.sequence(0, length, eff_interval)
|
|
3861
|
+
cut_lines = line.cutLines(distances, geom_error).geometries()
|
|
3862
|
+
|
|
3863
|
+
def create_feature(l):
|
|
3864
|
+
geom = ee.Geometry(ee.List(l).get(0))
|
|
3865
|
+
dist = ee.Number(ee.List(l).get(1))
|
|
3866
|
+
|
|
3867
|
+
final_geom = ee.Algorithms.If(
|
|
3868
|
+
ee.String(sampling_method).equals('buffered_point'),
|
|
3869
|
+
geom.centroid(geom_error).buffer(point_buffer_radius),
|
|
3870
|
+
geom
|
|
3871
|
+
)
|
|
3872
|
+
|
|
3873
|
+
return ee.Feature(ee.Geometry(final_geom), {
|
|
3874
|
+
'transect_name': line_name,
|
|
3875
|
+
'distance': dist
|
|
3876
|
+
})
|
|
3877
|
+
|
|
3878
|
+
line_fc = ee.FeatureCollection(cut_lines.zip(distances).map(create_feature))
|
|
3879
|
+
master_transect_fc = master_transect_fc.merge(line_fc)
|
|
3880
|
+
|
|
3881
|
+
try:
|
|
3882
|
+
ee_reducer = getattr(ee.Reducer, reducer)()
|
|
3883
|
+
except AttributeError:
|
|
3884
|
+
raise ValueError(f"Unknown reducer: '{reducer}'.")
|
|
3885
|
+
|
|
3886
|
+
def process_image(image):
|
|
3887
|
+
date_val = image.get('Date_Filter')
|
|
3888
|
+
|
|
3889
|
+
# Map over points (Slower but Robust)
|
|
3890
|
+
def reduce_point(f):
|
|
3891
|
+
stats = image.reduceRegion(
|
|
3892
|
+
reducer=ee_reducer,
|
|
3893
|
+
geometry=f.geometry(),
|
|
3894
|
+
scale=scale,
|
|
3895
|
+
maxPixels=1e13
|
|
3896
|
+
)
|
|
3897
|
+
# Combine with defaults (preserves 0, handles masked)
|
|
3898
|
+
safe_stats = dummy_dict.combine(stats, overwrite=True)
|
|
3899
|
+
# Rename keys to match expected outputs (e.g. 'ndvi' -> 'ndvi_mean')
|
|
3900
|
+
final_stats = safe_stats.rename(rename_keys, rename_vals)
|
|
3901
|
+
|
|
3902
|
+
return f.set(final_stats).set({'image_date': date_val})
|
|
3903
|
+
|
|
3904
|
+
return master_transect_fc.map(reduce_point)
|
|
3905
|
+
|
|
3906
|
+
export_cols = ['transect_name', 'distance', 'image_date'] + reducer_cols
|
|
3907
|
+
|
|
3726
3908
|
if processing_mode == 'aggregated':
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
#
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
# Determine effective distance interval based on n_segments or dist_interval
|
|
3746
|
-
effective_dist_interval = ee.Algorithms.If(
|
|
3747
|
-
n_segments,
|
|
3748
|
-
length.divide(n_segments),
|
|
3749
|
-
dist_interval or 30 # Defaults to 30 if both are None
|
|
3750
|
-
)
|
|
3751
|
-
# Generate distances along the line(s) for segmentation
|
|
3752
|
-
distances = ee.List.sequence(0, length, effective_dist_interval)
|
|
3753
|
-
# Segmenting the line into smaller lines at the specified distances
|
|
3754
|
-
cut_lines_geoms = line.cutLines(distances, maxError).geometries()
|
|
3755
|
-
# Function to create features with distance attributes
|
|
3756
|
-
# Adjusted to ensure consistent return types
|
|
3757
|
-
def set_dist_attr(l):
|
|
3758
|
-
# l is a list: [geometry, distance]
|
|
3759
|
-
# Extracting geometry portion of line
|
|
3760
|
-
geom_segment = ee.Geometry(ee.List(l).get(0))
|
|
3761
|
-
# Extracting distance value for attribute
|
|
3762
|
-
distance = ee.Number(ee.List(l).get(1))
|
|
3763
|
-
### Determine final geometry based on sampling method
|
|
3764
|
-
# If the sampling method is 'buffered_point',
|
|
3765
|
-
# create a buffered point feature at the centroid of each segment,
|
|
3766
|
-
# otherwise create a line feature
|
|
3767
|
-
final_feature = ee.Algorithms.If(
|
|
3768
|
-
ee.String(sampling_method).equals('buffered_point'),
|
|
3769
|
-
# True Case: Create the buffered point feature
|
|
3770
|
-
ee.Feature(
|
|
3771
|
-
geom_segment.centroid(maxError).buffer(point_buffer_radius),
|
|
3772
|
-
{'distance': distance}
|
|
3773
|
-
),
|
|
3774
|
-
# False Case: Create the line segment feature
|
|
3775
|
-
ee.Feature(geom_segment, {'distance': distance})
|
|
3776
|
-
)
|
|
3777
|
-
# Return either the line segment feature or the buffered point feature
|
|
3778
|
-
return final_feature
|
|
3779
|
-
# Creating a FeatureCollection of the cut lines with distance attributes
|
|
3780
|
-
# Using map to apply the set_dist_attr function to each cut line geometry
|
|
3781
|
-
line_features = ee.FeatureCollection(cut_lines_geoms.zip(distances).map(set_dist_attr))
|
|
3782
|
-
# Reducing the image over the line features to get transect values
|
|
3783
|
-
transect_fc = image.reduceRegions(
|
|
3784
|
-
collection=line_features, reducer=ee_reducer, scale=scale
|
|
3785
|
-
)
|
|
3786
|
-
# Adding image date and line name properties to each feature
|
|
3787
|
-
def set_props(feature):
|
|
3788
|
-
return feature.set({'image_date': image_date, 'transect_name': line_name})
|
|
3789
|
-
# Append to the list of all transects for this image
|
|
3790
|
-
all_transects_for_image = all_transects_for_image.add(transect_fc.map(set_props))
|
|
3791
|
-
# Combine all transect FeatureCollections into a single FeatureCollection and flatten
|
|
3792
|
-
# Flatten is used to merge the list of FeatureCollections into one
|
|
3793
|
-
return ee.FeatureCollection(all_transects_for_image).flatten()
|
|
3794
|
-
# Map the function over the entire image collection and flatten the results
|
|
3795
|
-
results_fc = ee.FeatureCollection(self.collection.map(get_transects_for_image)).flatten()
|
|
3796
|
-
# Convert the results to a pandas DataFrame
|
|
3797
|
-
df = Sentinel2Collection.ee_to_df(results_fc, remove_geom=True)
|
|
3798
|
-
# Check if the DataFrame is empty
|
|
3799
|
-
if df.empty:
|
|
3800
|
-
print("Warning: No transect data was generated.")
|
|
3909
|
+
collection_size = self.collection.size().getInfo()
|
|
3910
|
+
print(f"Starting batch process of {collection_size} images...")
|
|
3911
|
+
|
|
3912
|
+
dfs = []
|
|
3913
|
+
for i in range(0, collection_size, batch_size):
|
|
3914
|
+
print(f" Processing image {i} to {min(i + batch_size, collection_size)}...")
|
|
3915
|
+
|
|
3916
|
+
batch_col = ee.ImageCollection(self.collection.toList(batch_size, i))
|
|
3917
|
+
results_fc = batch_col.map(process_image).flatten()
|
|
3918
|
+
|
|
3919
|
+
# Dynamic Class Call for ee_to_df
|
|
3920
|
+
df_batch = self.__class__.ee_to_df(results_fc, columns=export_cols, remove_geom=True)
|
|
3921
|
+
|
|
3922
|
+
if not df_batch.empty:
|
|
3923
|
+
dfs.append(df_batch)
|
|
3924
|
+
|
|
3925
|
+
if not dfs:
|
|
3926
|
+
print("Warning: No transect data generated.")
|
|
3801
3927
|
return {}
|
|
3802
|
-
|
|
3928
|
+
|
|
3929
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
3930
|
+
|
|
3931
|
+
# Post-Process & Split
|
|
3803
3932
|
output_dfs = {}
|
|
3804
|
-
|
|
3933
|
+
for col in reducer_cols:
|
|
3934
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
3935
|
+
df[col] = df[col].replace(-9999, np.nan)
|
|
3936
|
+
|
|
3805
3937
|
for name in sorted(df['transect_name'].unique()):
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3938
|
+
line_df = df[df['transect_name'] == name]
|
|
3939
|
+
|
|
3940
|
+
for raw_col, band_name in zip(reducer_cols, clean_names):
|
|
3941
|
+
try:
|
|
3942
|
+
# Safety drop for duplicates
|
|
3943
|
+
line_df_clean = line_df.drop_duplicates(subset=['distance', 'image_date'])
|
|
3944
|
+
|
|
3945
|
+
pivot = line_df_clean.pivot(index='distance', columns='image_date', values=raw_col)
|
|
3946
|
+
pivot.columns.name = 'Date'
|
|
3947
|
+
key = f"{name}_{band_name}"
|
|
3948
|
+
output_dfs[key] = pivot
|
|
3949
|
+
|
|
3950
|
+
if save_folder_path:
|
|
3951
|
+
safe_key = "".join(x for x in key if x.isalnum() or x in "._-")
|
|
3952
|
+
fname = f"{save_folder_path}{safe_key}_transects.csv"
|
|
3953
|
+
pivot.to_csv(fname)
|
|
3954
|
+
print(f"Saved: {fname}")
|
|
3955
|
+
except Exception as e:
|
|
3956
|
+
print(f"Skipping pivot for {name}/{band_name}: {e}")
|
|
3957
|
+
|
|
3818
3958
|
return output_dfs
|
|
3819
3959
|
|
|
3820
|
-
### old, depreciated iterative client-side processing method ###
|
|
3821
3960
|
elif processing_mode == 'iterative':
|
|
3822
3961
|
if not save_folder_path:
|
|
3823
|
-
raise ValueError("
|
|
3962
|
+
raise ValueError("save_folder_path is required for iterative mode.")
|
|
3824
3963
|
|
|
3825
3964
|
image_collection_dates = self.dates
|
|
3826
3965
|
for i, date in enumerate(image_collection_dates):
|
|
3827
3966
|
try:
|
|
3828
3967
|
print(f"Processing image {i+1}/{len(image_collection_dates)}: {date}")
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
)
|
|
3833
|
-
|
|
3834
|
-
|
|
3968
|
+
image_list = self.collection.toList(self.collection.size())
|
|
3969
|
+
image = ee.Image(image_list.get(i))
|
|
3970
|
+
|
|
3971
|
+
fc_result = process_image(image)
|
|
3972
|
+
df = self.__class__.ee_to_df(fc_result, columns=export_cols, remove_geom=True)
|
|
3973
|
+
|
|
3974
|
+
if not df.empty:
|
|
3975
|
+
for col in reducer_cols:
|
|
3976
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
3977
|
+
df[col] = df[col].replace(-9999, np.nan)
|
|
3978
|
+
|
|
3979
|
+
fname = f"{save_folder_path}{date}_transects.csv"
|
|
3980
|
+
df.to_csv(fname, index=False)
|
|
3981
|
+
print(f"Saved: {fname}")
|
|
3982
|
+
else:
|
|
3983
|
+
print(f"Skipping {date}: No data.")
|
|
3835
3984
|
except Exception as e:
|
|
3836
|
-
print(f"
|
|
3985
|
+
print(f"Error processing {date}: {e}")
|
|
3837
3986
|
else:
|
|
3838
|
-
raise ValueError("
|
|
3987
|
+
raise ValueError("processing_mode must be 'iterative' or 'aggregated'.")
|
|
3839
3988
|
|
|
3840
3989
|
@staticmethod
|
|
3841
3990
|
def extract_zonal_stats_from_buffer(
|
|
@@ -3939,7 +4088,8 @@ class Sentinel2Collection:
|
|
|
3939
4088
|
buffer_size=1,
|
|
3940
4089
|
tileScale=1,
|
|
3941
4090
|
dates=None,
|
|
3942
|
-
file_path=None
|
|
4091
|
+
file_path=None,
|
|
4092
|
+
unweighted=False
|
|
3943
4093
|
):
|
|
3944
4094
|
"""
|
|
3945
4095
|
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.
|
|
@@ -3958,6 +4108,7 @@ class Sentinel2Collection:
|
|
|
3958
4108
|
tileScale (int, optional): A scaling factor to reduce aggregation tile size. Defaults to 1.
|
|
3959
4109
|
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.
|
|
3960
4110
|
file_path (str, optional): File path to save the output CSV.
|
|
4111
|
+
unweighted (bool, optional): Whether to use an unweighted reducer. Defaults to False.
|
|
3961
4112
|
|
|
3962
4113
|
Returns:
|
|
3963
4114
|
pd.DataFrame or None: A pandas DataFrame with dates as the index and coordinate names
|
|
@@ -4064,6 +4215,9 @@ class Sentinel2Collection:
|
|
|
4064
4215
|
reducer = getattr(ee.Reducer, reducer_type)()
|
|
4065
4216
|
except AttributeError:
|
|
4066
4217
|
raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
|
|
4218
|
+
|
|
4219
|
+
if unweighted:
|
|
4220
|
+
reducer = reducer.unweighted()
|
|
4067
4221
|
|
|
4068
4222
|
# Define the function to map over the image collection
|
|
4069
4223
|
def calculate_stats_for_image(image):
|
|
@@ -4125,6 +4279,394 @@ class Sentinel2Collection:
|
|
|
4125
4279
|
print(f"Zonal stats saved to {file_path}.csv")
|
|
4126
4280
|
return
|
|
4127
4281
|
return pivot_df
|
|
4282
|
+
|
|
4283
|
+
def multiband_zonal_stats(
|
|
4284
|
+
self,
|
|
4285
|
+
geometry,
|
|
4286
|
+
bands,
|
|
4287
|
+
reducer_types,
|
|
4288
|
+
scale=30,
|
|
4289
|
+
geometry_name='geom',
|
|
4290
|
+
dates=None,
|
|
4291
|
+
include_area=False,
|
|
4292
|
+
file_path=None,
|
|
4293
|
+
unweighted=False
|
|
4294
|
+
):
|
|
4295
|
+
"""
|
|
4296
|
+
Calculates zonal statistics for multiple bands over a single geometry for each image in the collection.
|
|
4297
|
+
Allows for specifying different reducers for different bands. Optionally includes the geometry area.
|
|
4298
|
+
|
|
4299
|
+
Args:
|
|
4300
|
+
geometry (ee.Geometry or ee.Feature): The single geometry to calculate statistics for.
|
|
4301
|
+
bands (list of str): A list of band names to include in the analysis.
|
|
4302
|
+
reducer_types (str or list of str): A single reducer name (e.g., 'mean') to apply to all bands,
|
|
4303
|
+
or a list of reducer names matching the length of the 'bands' list to apply specific reducers
|
|
4304
|
+
to specific bands.
|
|
4305
|
+
scale (int, optional): The scale in meters for the reduction. Defaults to 30.
|
|
4306
|
+
geometry_name (str, optional): A name for the geometry, used in column naming. Defaults to 'geom'.
|
|
4307
|
+
dates (list of str, optional): A list of date strings ('YYYY-MM-DD') to filter the collection.
|
|
4308
|
+
Defaults to None (processes all images).
|
|
4309
|
+
include_area (bool, optional): If True, adds a column with the area of the geometry in square meters.
|
|
4310
|
+
Defaults to False.
|
|
4311
|
+
file_path (str, optional): If provided, saves the resulting DataFrame to a CSV file at this path.
|
|
4312
|
+
unweighted (bool, optional): Whether to use unweighted reducers. Defaults to False.
|
|
4313
|
+
|
|
4314
|
+
Returns:
|
|
4315
|
+
pd.DataFrame: A pandas DataFrame indexed by Date, with columns named as '{band}_{geometry_name}_{reducer}'.
|
|
4316
|
+
"""
|
|
4317
|
+
# 1. Input Validation and Setup
|
|
4318
|
+
if not isinstance(geometry, (ee.Geometry, ee.Feature)):
|
|
4319
|
+
raise ValueError("The `geometry` argument must be an ee.Geometry or ee.Feature.")
|
|
4320
|
+
|
|
4321
|
+
region = geometry.geometry() if isinstance(geometry, ee.Feature) else geometry
|
|
4322
|
+
|
|
4323
|
+
if isinstance(bands, str):
|
|
4324
|
+
bands = [bands]
|
|
4325
|
+
if not isinstance(bands, list):
|
|
4326
|
+
raise ValueError("The `bands` argument must be a string or a list of strings.")
|
|
4327
|
+
|
|
4328
|
+
# Handle reducer_types (str vs list)
|
|
4329
|
+
if isinstance(reducer_types, str):
|
|
4330
|
+
reducers_list = [reducer_types] * len(bands)
|
|
4331
|
+
elif isinstance(reducer_types, list):
|
|
4332
|
+
if len(reducer_types) != len(bands):
|
|
4333
|
+
raise ValueError("If `reducer_types` is a list, it must have the same length as `bands`.")
|
|
4334
|
+
reducers_list = reducer_types
|
|
4335
|
+
else:
|
|
4336
|
+
raise ValueError("`reducer_types` must be a string or a list of strings.")
|
|
4337
|
+
|
|
4338
|
+
# 2. Filter Collection
|
|
4339
|
+
processing_col = self.collection
|
|
4340
|
+
|
|
4341
|
+
if dates:
|
|
4342
|
+
processing_col = processing_col.filter(ee.Filter.inList('Date_Filter', dates))
|
|
4343
|
+
|
|
4344
|
+
processing_col = processing_col.select(bands)
|
|
4345
|
+
|
|
4346
|
+
# 3. Pre-calculate Area (if requested)
|
|
4347
|
+
area_val = None
|
|
4348
|
+
area_col_name = f"{geometry_name}_area_m2"
|
|
4349
|
+
if include_area:
|
|
4350
|
+
# Calculate geodesic area in square meters with maxError of 1m
|
|
4351
|
+
area_val = region.area(1)
|
|
4352
|
+
|
|
4353
|
+
# 4. Define the Reduction Logic
|
|
4354
|
+
def calculate_multiband_stats(image):
|
|
4355
|
+
# Base feature with date property
|
|
4356
|
+
date_val = image.get('Date_Filter')
|
|
4357
|
+
feature = ee.Feature(None, {'Date': date_val})
|
|
4358
|
+
|
|
4359
|
+
# If requested, add the static area value to every feature
|
|
4360
|
+
if include_area:
|
|
4361
|
+
feature = feature.set(area_col_name, area_val)
|
|
4362
|
+
|
|
4363
|
+
unique_reducers = list(set(reducers_list))
|
|
4364
|
+
|
|
4365
|
+
# OPTIMIZED PATH: Single reducer type for all bands
|
|
4366
|
+
if len(unique_reducers) == 1:
|
|
4367
|
+
r_type = unique_reducers[0]
|
|
4368
|
+
try:
|
|
4369
|
+
reducer = getattr(ee.Reducer, r_type)()
|
|
4370
|
+
except AttributeError:
|
|
4371
|
+
reducer = ee.Reducer.mean()
|
|
4372
|
+
|
|
4373
|
+
if unweighted:
|
|
4374
|
+
reducer = reducer.unweighted()
|
|
4375
|
+
|
|
4376
|
+
stats = image.reduceRegion(
|
|
4377
|
+
reducer=reducer,
|
|
4378
|
+
geometry=region,
|
|
4379
|
+
scale=scale,
|
|
4380
|
+
maxPixels=1e13
|
|
4381
|
+
)
|
|
4382
|
+
|
|
4383
|
+
for band in bands:
|
|
4384
|
+
col_name = f"{band}_{geometry_name}_{r_type}"
|
|
4385
|
+
val = stats.get(band)
|
|
4386
|
+
feature = feature.set(col_name, val)
|
|
4387
|
+
|
|
4388
|
+
# ITERATIVE PATH: Different reducers for different bands
|
|
4389
|
+
else:
|
|
4390
|
+
for band, r_type in zip(bands, reducers_list):
|
|
4391
|
+
try:
|
|
4392
|
+
reducer = getattr(ee.Reducer, r_type)()
|
|
4393
|
+
except AttributeError:
|
|
4394
|
+
reducer = ee.Reducer.mean()
|
|
4395
|
+
|
|
4396
|
+
if unweighted:
|
|
4397
|
+
reducer = reducer.unweighted()
|
|
4398
|
+
|
|
4399
|
+
stats = image.select(band).reduceRegion(
|
|
4400
|
+
reducer=reducer,
|
|
4401
|
+
geometry=region,
|
|
4402
|
+
scale=scale,
|
|
4403
|
+
maxPixels=1e13
|
|
4404
|
+
)
|
|
4405
|
+
|
|
4406
|
+
val = stats.get(band)
|
|
4407
|
+
col_name = f"{band}_{geometry_name}_{r_type}"
|
|
4408
|
+
feature = feature.set(col_name, val)
|
|
4409
|
+
|
|
4410
|
+
return feature
|
|
4411
|
+
|
|
4412
|
+
# 5. Execute Server-Side Mapping (with explicit Cast)
|
|
4413
|
+
results_fc = ee.FeatureCollection(processing_col.map(calculate_multiband_stats))
|
|
4414
|
+
|
|
4415
|
+
# 6. Client-Side Conversion
|
|
4416
|
+
try:
|
|
4417
|
+
df = Sentinel2Collection.ee_to_df(results_fc, remove_geom=True)
|
|
4418
|
+
except Exception as e:
|
|
4419
|
+
raise RuntimeError(f"Failed to convert Earth Engine results to DataFrame. Error: {e}")
|
|
4420
|
+
|
|
4421
|
+
if df.empty:
|
|
4422
|
+
print("Warning: No results returned. Check if the geometry intersects the imagery or if dates are valid.")
|
|
4423
|
+
return pd.DataFrame()
|
|
4424
|
+
|
|
4425
|
+
# 7. Formatting & Reordering
|
|
4426
|
+
if 'Date' in df.columns:
|
|
4427
|
+
df['Date'] = pd.to_datetime(df['Date'])
|
|
4428
|
+
df = df.sort_values('Date').set_index('Date')
|
|
4429
|
+
|
|
4430
|
+
# Construct the expected column names in the exact order of the input lists
|
|
4431
|
+
expected_order = [f"{band}_{geometry_name}_{r_type}" for band, r_type in zip(bands, reducers_list)]
|
|
4432
|
+
|
|
4433
|
+
# If area was included, append it to the END of the list
|
|
4434
|
+
if include_area:
|
|
4435
|
+
expected_order.append(area_col_name)
|
|
4436
|
+
|
|
4437
|
+
# Reindex the DataFrame to match this order.
|
|
4438
|
+
existing_cols = [c for c in expected_order if c in df.columns]
|
|
4439
|
+
df = df[existing_cols]
|
|
4440
|
+
|
|
4441
|
+
# 8. Export (Optional)
|
|
4442
|
+
if file_path:
|
|
4443
|
+
if not file_path.lower().endswith('.csv'):
|
|
4444
|
+
file_path += '.csv'
|
|
4445
|
+
try:
|
|
4446
|
+
df.to_csv(file_path)
|
|
4447
|
+
print(f"Multiband zonal stats saved to {file_path}")
|
|
4448
|
+
except Exception as e:
|
|
4449
|
+
print(f"Error saving file to {file_path}: {e}")
|
|
4450
|
+
|
|
4451
|
+
return df
|
|
4452
|
+
|
|
4453
|
+
def sample(
|
|
4454
|
+
self,
|
|
4455
|
+
locations,
|
|
4456
|
+
band=None,
|
|
4457
|
+
scale=None,
|
|
4458
|
+
location_names=None,
|
|
4459
|
+
dates=None,
|
|
4460
|
+
file_path=None,
|
|
4461
|
+
tileScale=1
|
|
4462
|
+
):
|
|
4463
|
+
"""
|
|
4464
|
+
Extracts time-series pixel values for a list of locations.
|
|
4465
|
+
|
|
4466
|
+
|
|
4467
|
+
Args:
|
|
4468
|
+
locations (list, tuple, ee.Geometry, or ee.FeatureCollection): Input points.
|
|
4469
|
+
band (str, optional): The name of the band to sample. Defaults to the first band.
|
|
4470
|
+
scale (int, optional): Scale in meters. Defaults to 30 if None.
|
|
4471
|
+
location_names (list of str, optional): Custom names for locations.
|
|
4472
|
+
dates (list, optional): Date filter ['YYYY-MM-DD'].
|
|
4473
|
+
file_path (str, optional): CSV export path.
|
|
4474
|
+
tileScale (int, optional): Aggregation tile scale. Defaults to 1.
|
|
4475
|
+
|
|
4476
|
+
Returns:
|
|
4477
|
+
pd.DataFrame (or CSV if file_path is provided): DataFrame indexed by Date, columns by Location.
|
|
4478
|
+
"""
|
|
4479
|
+
col = self.collection
|
|
4480
|
+
if dates:
|
|
4481
|
+
col = col.filter(ee.Filter.inList('Date_Filter', dates))
|
|
4482
|
+
|
|
4483
|
+
first_img = col.first()
|
|
4484
|
+
available_bands = first_img.bandNames().getInfo()
|
|
4485
|
+
|
|
4486
|
+
if band:
|
|
4487
|
+
if band not in available_bands:
|
|
4488
|
+
raise ValueError(f"Band '{band}' not found. Available: {available_bands}")
|
|
4489
|
+
target_band = band
|
|
4490
|
+
else:
|
|
4491
|
+
target_band = available_bands[0]
|
|
4492
|
+
|
|
4493
|
+
processing_col = col.select([target_band])
|
|
4494
|
+
|
|
4495
|
+
def set_name(f):
|
|
4496
|
+
name = ee.Algorithms.If(
|
|
4497
|
+
f.get('geo_name'), f.get('geo_name'),
|
|
4498
|
+
ee.Algorithms.If(f.get('name'), f.get('name'),
|
|
4499
|
+
ee.Algorithms.If(f.get('system:index'), f.get('system:index'), 'unnamed'))
|
|
4500
|
+
)
|
|
4501
|
+
return f.set('geo_name', name)
|
|
4502
|
+
|
|
4503
|
+
if isinstance(locations, (ee.FeatureCollection, ee.Feature)):
|
|
4504
|
+
features = ee.FeatureCollection(locations)
|
|
4505
|
+
elif isinstance(locations, ee.Geometry):
|
|
4506
|
+
lbl = location_names[0] if (location_names and location_names[0]) else 'Point_1'
|
|
4507
|
+
features = ee.FeatureCollection([ee.Feature(locations).set('geo_name', lbl)])
|
|
4508
|
+
elif isinstance(locations, tuple) and len(locations) == 2:
|
|
4509
|
+
lbl = location_names[0] if location_names else 'Location_1'
|
|
4510
|
+
features = ee.FeatureCollection([ee.Feature(ee.Geometry.Point(locations), {'geo_name': lbl})])
|
|
4511
|
+
elif isinstance(locations, list):
|
|
4512
|
+
if all(isinstance(i, tuple) for i in locations):
|
|
4513
|
+
names = location_names if location_names else [f"Loc_{i+1}" for i in range(len(locations))]
|
|
4514
|
+
features = ee.FeatureCollection([
|
|
4515
|
+
ee.Feature(ee.Geometry.Point(p), {'geo_name': str(n)}) for p, n in zip(locations, names)
|
|
4516
|
+
])
|
|
4517
|
+
elif all(isinstance(i, ee.Geometry) for i in locations):
|
|
4518
|
+
names = location_names if location_names else [f"Geom_{i+1}" for i in range(len(locations))]
|
|
4519
|
+
features = ee.FeatureCollection([
|
|
4520
|
+
ee.Feature(g, {'geo_name': str(n)}) for g, n in zip(locations, names)
|
|
4521
|
+
])
|
|
4522
|
+
else:
|
|
4523
|
+
raise ValueError("List must contain (lon, lat) tuples or ee.Geometry objects.")
|
|
4524
|
+
else:
|
|
4525
|
+
raise TypeError("Invalid locations input.")
|
|
4526
|
+
|
|
4527
|
+
features = features.map(set_name)
|
|
4528
|
+
|
|
4529
|
+
|
|
4530
|
+
def sample_image(img):
|
|
4531
|
+
date = img.get('Date_Filter')
|
|
4532
|
+
use_scale = scale if scale is not None else 30
|
|
4533
|
+
|
|
4534
|
+
|
|
4535
|
+
default_dict = ee.Dictionary({target_band: -9999})
|
|
4536
|
+
|
|
4537
|
+
def extract_point(f):
|
|
4538
|
+
stats = img.reduceRegion(
|
|
4539
|
+
reducer=ee.Reducer.first(),
|
|
4540
|
+
geometry=f.geometry(),
|
|
4541
|
+
scale=use_scale,
|
|
4542
|
+
tileScale=tileScale
|
|
4543
|
+
)
|
|
4544
|
+
|
|
4545
|
+
# Combine dictionaries.
|
|
4546
|
+
# If stats has 'target_band' (even if 0), it overwrites -9999.
|
|
4547
|
+
# If stats is empty (masked), -9999 remains.
|
|
4548
|
+
safe_stats = default_dict.combine(stats, overwrite=True)
|
|
4549
|
+
val = safe_stats.get(target_band)
|
|
4550
|
+
|
|
4551
|
+
return f.set({
|
|
4552
|
+
target_band: val,
|
|
4553
|
+
'image_date': date
|
|
4554
|
+
})
|
|
4555
|
+
|
|
4556
|
+
return features.map(extract_point)
|
|
4557
|
+
|
|
4558
|
+
# Flatten the results
|
|
4559
|
+
flat_results = processing_col.map(sample_image).flatten()
|
|
4560
|
+
|
|
4561
|
+
df = Sentinel2Collection.ee_to_df(
|
|
4562
|
+
flat_results,
|
|
4563
|
+
columns=['image_date', 'geo_name', target_band],
|
|
4564
|
+
remove_geom=True
|
|
4565
|
+
)
|
|
4566
|
+
|
|
4567
|
+
if df.empty:
|
|
4568
|
+
print("Warning: No data returned.")
|
|
4569
|
+
return pd.DataFrame()
|
|
4570
|
+
|
|
4571
|
+
# 6. Clean and Pivot
|
|
4572
|
+
df[target_band] = pd.to_numeric(df[target_band], errors='coerce')
|
|
4573
|
+
|
|
4574
|
+
# Filter out ONLY the sentinel value (-9999), preserving 0.
|
|
4575
|
+
df = df[df[target_band] != -9999]
|
|
4576
|
+
|
|
4577
|
+
if df.empty:
|
|
4578
|
+
print(f"Warning: All data points were masked (NoData) for band '{target_band}'.")
|
|
4579
|
+
return pd.DataFrame()
|
|
4580
|
+
|
|
4581
|
+
pivot_df = df.pivot(index='image_date', columns='geo_name', values=target_band)
|
|
4582
|
+
pivot_df.index.name = 'Date'
|
|
4583
|
+
pivot_df.columns.name = None
|
|
4584
|
+
pivot_df = pivot_df.reset_index()
|
|
4585
|
+
|
|
4586
|
+
if file_path:
|
|
4587
|
+
if not file_path.lower().endswith('.csv'):
|
|
4588
|
+
file_path += '.csv'
|
|
4589
|
+
pivot_df.to_csv(file_path, index=False)
|
|
4590
|
+
print(f"Sampled data saved to {file_path}")
|
|
4591
|
+
return None
|
|
4592
|
+
|
|
4593
|
+
return pivot_df
|
|
4594
|
+
|
|
4595
|
+
def multiband_sample(
|
|
4596
|
+
self,
|
|
4597
|
+
location,
|
|
4598
|
+
scale=30,
|
|
4599
|
+
file_path=None
|
|
4600
|
+
):
|
|
4601
|
+
"""
|
|
4602
|
+
Extracts ALL band values for a SINGLE location across the entire collection.
|
|
4603
|
+
|
|
4604
|
+
Args:
|
|
4605
|
+
location (tuple or ee.Geometry): A single (lon, lat) tuple OR ee.Geometry.
|
|
4606
|
+
scale (int, optional): Scale in meters. Defaults to 30.
|
|
4607
|
+
file_path (str, optional): Path to save CSV.
|
|
4608
|
+
|
|
4609
|
+
Returns:
|
|
4610
|
+
pd.DataFrame: DataFrame indexed by Date, with columns for each Band.
|
|
4611
|
+
"""
|
|
4612
|
+
if isinstance(location, tuple) and len(location) == 2:
|
|
4613
|
+
geom = ee.Geometry.Point(location)
|
|
4614
|
+
elif isinstance(location, ee.Geometry):
|
|
4615
|
+
geom = location
|
|
4616
|
+
else:
|
|
4617
|
+
raise ValueError("Location must be a single (lon, lat) tuple or ee.Geometry.")
|
|
4618
|
+
|
|
4619
|
+
first_img = self.collection.first()
|
|
4620
|
+
band_names = first_img.bandNames()
|
|
4621
|
+
|
|
4622
|
+
# Create a dictionary of {band_name: -9999}
|
|
4623
|
+
# fill missing values so the Feature structure is consistent
|
|
4624
|
+
dummy_values = ee.List.repeat(-9999, band_names.length())
|
|
4625
|
+
default_dict = ee.Dictionary.fromLists(band_names, dummy_values)
|
|
4626
|
+
|
|
4627
|
+
def get_all_bands(img):
|
|
4628
|
+
date = img.get('Date_Filter')
|
|
4629
|
+
|
|
4630
|
+
# reduceRegion returns a Dictionary.
|
|
4631
|
+
# If a pixel is masked, that band key is missing from 'stats'.
|
|
4632
|
+
stats = img.reduceRegion(
|
|
4633
|
+
reducer=ee.Reducer.first(),
|
|
4634
|
+
geometry=geom,
|
|
4635
|
+
scale=scale,
|
|
4636
|
+
maxPixels=1e13
|
|
4637
|
+
)
|
|
4638
|
+
|
|
4639
|
+
# Combine stats with defaults.
|
|
4640
|
+
# overwrite=True means real data (stats) overwrites the -9999 defaults.
|
|
4641
|
+
complete_stats = default_dict.combine(stats, overwrite=True)
|
|
4642
|
+
|
|
4643
|
+
return ee.Feature(None, complete_stats).set('Date', date)
|
|
4644
|
+
|
|
4645
|
+
fc = ee.FeatureCollection(self.collection.map(get_all_bands))
|
|
4646
|
+
|
|
4647
|
+
df = Sentinel2Collection.ee_to_df(fc, remove_geom=True)
|
|
4648
|
+
|
|
4649
|
+
if df.empty:
|
|
4650
|
+
print("Warning: No data found.")
|
|
4651
|
+
return pd.DataFrame()
|
|
4652
|
+
|
|
4653
|
+
# 6. Cleanup
|
|
4654
|
+
if 'Date' in df.columns:
|
|
4655
|
+
df['Date'] = pd.to_datetime(df['Date'])
|
|
4656
|
+
df = df.set_index('Date').sort_index()
|
|
4657
|
+
|
|
4658
|
+
# Replace our sentinel -9999 with proper NaNs
|
|
4659
|
+
df = df.replace(-9999, np.nan)
|
|
4660
|
+
|
|
4661
|
+
# 7. Export
|
|
4662
|
+
if file_path:
|
|
4663
|
+
if not file_path.lower().endswith('.csv'):
|
|
4664
|
+
file_path += '.csv'
|
|
4665
|
+
df.to_csv(file_path)
|
|
4666
|
+
print(f"Multiband sample saved to {file_path}")
|
|
4667
|
+
return None
|
|
4668
|
+
|
|
4669
|
+
return df
|
|
4128
4670
|
|
|
4129
4671
|
def export_to_asset_collection(
|
|
4130
4672
|
self,
|
|
@@ -4135,7 +4677,8 @@ class Sentinel2Collection:
|
|
|
4135
4677
|
filename_prefix="",
|
|
4136
4678
|
crs=None,
|
|
4137
4679
|
max_pixels=int(1e13),
|
|
4138
|
-
description_prefix="export"
|
|
4680
|
+
description_prefix="export",
|
|
4681
|
+
overwrite=False
|
|
4139
4682
|
):
|
|
4140
4683
|
"""
|
|
4141
4684
|
Exports an image collection to a Google Earth Engine asset collection. The asset collection will be created if it does not already exist,
|
|
@@ -4150,6 +4693,7 @@ class Sentinel2Collection:
|
|
|
4150
4693
|
crs (str, optional): The coordinate reference system. Defaults to None, which will use the image's CRS.
|
|
4151
4694
|
max_pixels (int, optional): The maximum number of pixels. Defaults to int(1e13).
|
|
4152
4695
|
description_prefix (str, optional): The description prefix. Defaults to "export".
|
|
4696
|
+
overwrite (bool, optional): Whether to overwrite existing assets. Defaults to False.
|
|
4153
4697
|
|
|
4154
4698
|
Returns:
|
|
4155
4699
|
None: (queues export tasks)
|
|
@@ -4167,6 +4711,14 @@ class Sentinel2Collection:
|
|
|
4167
4711
|
asset_id = asset_collection_path + "/" + filename_prefix + date_str
|
|
4168
4712
|
desc = description_prefix + "_" + filename_prefix + date_str
|
|
4169
4713
|
|
|
4714
|
+
if overwrite:
|
|
4715
|
+
try:
|
|
4716
|
+
ee.data.deleteAsset(asset_id)
|
|
4717
|
+
print(f"Overwriting: Deleted existing asset {asset_id}")
|
|
4718
|
+
except ee.EEException:
|
|
4719
|
+
# Asset does not exist, so nothing to delete. Proceed safely.
|
|
4720
|
+
pass
|
|
4721
|
+
|
|
4170
4722
|
params = {
|
|
4171
4723
|
'image': img,
|
|
4172
4724
|
'description': desc,
|