RadGEEToolbox 1.7.0__py3-none-any.whl → 1.7.2__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/GenericCollection.py +123 -32
- RadGEEToolbox/GetPalette.py +136 -87
- RadGEEToolbox/LandsatCollection.py +465 -37
- RadGEEToolbox/Sentinel1Collection.py +607 -19
- RadGEEToolbox/Sentinel2Collection.py +468 -31
- RadGEEToolbox/VisParams.py +112 -194
- RadGEEToolbox/__init__.py +1 -1
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/METADATA +8 -6
- radgeetoolbox-1.7.2.dist-info/RECORD +13 -0
- radgeetoolbox-1.7.0.dist-info/RECORD +0 -13
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/top_level.txt +0 -0
|
@@ -213,6 +213,11 @@ class Sentinel1Collection:
|
|
|
213
213
|
self._mean = None
|
|
214
214
|
self._max = None
|
|
215
215
|
self._min = None
|
|
216
|
+
self._monthly_median = None
|
|
217
|
+
self._monthly_mean = None
|
|
218
|
+
self._monthly_max = None
|
|
219
|
+
self._monthly_min = None
|
|
220
|
+
self._monthly_sum = None
|
|
216
221
|
self._MosaicByDate = None
|
|
217
222
|
self._PixelAreaSumCollection = None
|
|
218
223
|
self._speckle_filter = None
|
|
@@ -307,11 +312,11 @@ class Sentinel1Collection:
|
|
|
307
312
|
|
|
308
313
|
Args:
|
|
309
314
|
band_name (string or list of strings): name of band(s) (string) for calculating area. If providing multiple band names, pass as a list of strings.
|
|
310
|
-
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
|
|
315
|
+
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation.
|
|
311
316
|
threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1). If providing multiple band names, the same threshold will be applied to all bands. Best practice in this case is to mask the bands prior to passing to this function and leave threshold at default of -1.
|
|
312
|
-
scale (int): integer scale of image resolution (meters) (defaults to
|
|
313
|
-
maxPixels (int): integer denoting maximum number of pixels for calculations
|
|
314
|
-
output_type (str): 'ImageCollection' to return an ee.ImageCollection, 'Sentinel1Collection' to return a Sentinel1Collection object (defaults to 'ImageCollection')
|
|
317
|
+
scale (int): integer scale of image resolution (meters) (defaults to 30).
|
|
318
|
+
maxPixels (int): integer denoting maximum number of pixels for calculations.
|
|
319
|
+
output_type (str): 'ImageCollection' or 'ee.ImageCollection' to return an ee.ImageCollection, 'Sentinel1Collection' to return a Sentinel1Collection object, or 'DataFrame', 'Pandas', 'pd', 'dataframe', 'df' to return a pandas DataFrame (defaults to 'ImageCollection').
|
|
315
320
|
area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
|
|
316
321
|
|
|
317
322
|
Returns:
|
|
@@ -334,17 +339,45 @@ class Sentinel1Collection:
|
|
|
334
339
|
# Storing the result in the instance variable to avoid redundant calculations
|
|
335
340
|
self._PixelAreaSumCollection = AreaCollection
|
|
336
341
|
|
|
342
|
+
prop_names = band_name if isinstance(band_name, list) else [band_name]
|
|
343
|
+
|
|
337
344
|
# If an export path is provided, the area data will be exported to a CSV file
|
|
338
345
|
if area_data_export_path:
|
|
339
|
-
Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=
|
|
340
|
-
|
|
346
|
+
Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
|
|
341
347
|
# Returning the result in the desired format based on output_type argument or raising an error for invalid input
|
|
342
|
-
if output_type == 'ImageCollection':
|
|
348
|
+
if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
|
|
343
349
|
return self._PixelAreaSumCollection
|
|
344
350
|
elif output_type == 'Sentinel1Collection':
|
|
345
351
|
return Sentinel1Collection(collection=self._PixelAreaSumCollection)
|
|
352
|
+
elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
|
|
353
|
+
return Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
|
|
346
354
|
else:
|
|
347
|
-
raise ValueError("output_type must be 'ImageCollection'
|
|
355
|
+
raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'Sentinel1Collection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
|
|
356
|
+
|
|
357
|
+
@staticmethod
|
|
358
|
+
def add_month_property_fn(image):
|
|
359
|
+
"""
|
|
360
|
+
Adds a numeric 'month' property to the image based on its date.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
image (ee.Image): Input image.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
ee.Image: Image with the 'month' property added.
|
|
367
|
+
"""
|
|
368
|
+
return image.set('month', image.date().get('month'))
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def add_month_property(self):
|
|
372
|
+
"""
|
|
373
|
+
Adds a numeric 'month' property to each image in the collection.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Sentinel1Collection: A Sentinel1Collection image collection with the 'month' property added to each image.
|
|
377
|
+
"""
|
|
378
|
+
col = self.collection.map(Sentinel1Collection.add_month_property_fn)
|
|
379
|
+
return Sentinel1Collection(collection=col)
|
|
380
|
+
|
|
348
381
|
|
|
349
382
|
def combine(self, other):
|
|
350
383
|
"""
|
|
@@ -719,6 +752,64 @@ class Sentinel1Collection:
|
|
|
719
752
|
dB_collection = collection.map(conversion)
|
|
720
753
|
self._DbFromSigma0 = dB_collection
|
|
721
754
|
return Sentinel1Collection(collection=self._DbFromSigma0)
|
|
755
|
+
|
|
756
|
+
@staticmethod
|
|
757
|
+
def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=10):
|
|
758
|
+
"""
|
|
759
|
+
Calculates the anomaly of a singleband image compared to the mean of the singleband image.
|
|
760
|
+
|
|
761
|
+
This function computes the anomaly for each band in the input image by
|
|
762
|
+
subtracting the mean value of that band from a provided image.
|
|
763
|
+
The anomaly is a measure of how much the pixel values deviate from the
|
|
764
|
+
average conditions represented by the mean of the image.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
image (ee.Image): An ee.Image for which the anomaly is to be calculated.
|
|
768
|
+
It is assumed that this image is a singleband image.
|
|
769
|
+
geometry (ee.Geometry): The geometry for image reduction to define the mean value to be used for anomaly calculation.
|
|
770
|
+
band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
771
|
+
anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
772
|
+
replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band in the output image. If True, the output image will contain only the anomaly band. If False, the output image will contain both the original band and the anomaly band. Default is True.
|
|
773
|
+
scale (int, optional): The scale (in meters) to use for the image reduction. Default is 10.
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
ee.Image: An ee.Image where each band represents the anomaly (deviation from
|
|
777
|
+
the mean) for that band. The output image retains the same band name.
|
|
778
|
+
"""
|
|
779
|
+
if band_name:
|
|
780
|
+
band_name = band_name
|
|
781
|
+
else:
|
|
782
|
+
band_name = ee.String(image.bandNames().get(0))
|
|
783
|
+
|
|
784
|
+
image_to_process = image.select([band_name])
|
|
785
|
+
|
|
786
|
+
# Calculate the mean image of the provided collection.
|
|
787
|
+
mean_image = image_to_process.reduceRegion(
|
|
788
|
+
reducer=ee.Reducer.mean(),
|
|
789
|
+
geometry=geometry,
|
|
790
|
+
scale=scale,
|
|
791
|
+
maxPixels=1e13
|
|
792
|
+
).toImage()
|
|
793
|
+
|
|
794
|
+
# Compute the anomaly by subtracting the mean image from the input image.
|
|
795
|
+
if scale == 10:
|
|
796
|
+
anomaly_image = image_to_process.subtract(mean_image)
|
|
797
|
+
else:
|
|
798
|
+
anomaly_image = image_to_process.reproject(crs=image_to_process.projection(), scale=scale).subtract(mean_image)
|
|
799
|
+
|
|
800
|
+
if anomaly_band_name is None:
|
|
801
|
+
if band_name:
|
|
802
|
+
anomaly_image = anomaly_image.rename(band_name)
|
|
803
|
+
else:
|
|
804
|
+
# Preserve original properties from the input image.
|
|
805
|
+
anomaly_image = anomaly_image.rename(ee.String(image.bandNames().get(0)))
|
|
806
|
+
else:
|
|
807
|
+
anomaly_image = anomaly_image.rename(anomaly_band_name)
|
|
808
|
+
# return anomaly_image
|
|
809
|
+
if replace:
|
|
810
|
+
return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
811
|
+
else:
|
|
812
|
+
return image.addBands(anomaly_image, overwrite=True)
|
|
722
813
|
|
|
723
814
|
@property
|
|
724
815
|
def dates_list(self):
|
|
@@ -763,6 +854,8 @@ class Sentinel1Collection:
|
|
|
763
854
|
# Ensure property_names is a list for consistent processing
|
|
764
855
|
if isinstance(property_names, str):
|
|
765
856
|
property_names = [property_names]
|
|
857
|
+
elif isinstance(property_names, list):
|
|
858
|
+
property_names = property_names
|
|
766
859
|
|
|
767
860
|
# Ensure properties are included without duplication, including 'Date_Filter'
|
|
768
861
|
all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
|
|
@@ -887,6 +980,27 @@ class Sentinel1Collection:
|
|
|
887
980
|
.select(self.bands)
|
|
888
981
|
)
|
|
889
982
|
return filtered_collection
|
|
983
|
+
|
|
984
|
+
def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
|
|
985
|
+
"""
|
|
986
|
+
Removes duplicate images that share the same date, keeping only the first one encountered.
|
|
987
|
+
Useful for handling duplicate Sentinel-1A/1B acquisitions or overlapping tiles.
|
|
988
|
+
|
|
989
|
+
Args:
|
|
990
|
+
sort_by (str): Property to sort by before filtering distinct dates.
|
|
991
|
+
Defaults to 'system:time_start'. Take care to provide a property that exists in all images if using a custom property.
|
|
992
|
+
ascending (bool): Sort order. Defaults to True.
|
|
993
|
+
|
|
994
|
+
Returns:
|
|
995
|
+
Sentinel1Collection: A new Sentinel1Collection object with distinct dates.
|
|
996
|
+
"""
|
|
997
|
+
# Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
|
|
998
|
+
sorted_col = self.collection.sort(sort_by, ascending)
|
|
999
|
+
|
|
1000
|
+
# distinct() retains the first image for each unique value of the specified property
|
|
1001
|
+
distinct_col = sorted_col.distinct('Date_Filter')
|
|
1002
|
+
|
|
1003
|
+
return Sentinel1Collection(collection=distinct_col)
|
|
890
1004
|
|
|
891
1005
|
@property
|
|
892
1006
|
def median(self):
|
|
@@ -940,6 +1054,450 @@ class Sentinel1Collection:
|
|
|
940
1054
|
col = self.collection.min()
|
|
941
1055
|
self._min = col
|
|
942
1056
|
return self._min
|
|
1057
|
+
|
|
1058
|
+
@property
|
|
1059
|
+
def monthly_median_collection(self):
|
|
1060
|
+
"""Creates a monthly median composite from a Sentinel1Collection image collection.
|
|
1061
|
+
|
|
1062
|
+
This function computes the median for each
|
|
1063
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1064
|
+
temporal extent of the input collection.
|
|
1065
|
+
|
|
1066
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1067
|
+
first day of each month and an 'image_count' property indicating how
|
|
1068
|
+
many images were used in the composite. Months with no images are
|
|
1069
|
+
automatically excluded from the final collection.
|
|
1070
|
+
|
|
1071
|
+
Returns:
|
|
1072
|
+
Sentinel1Collection: A new Sentinel1Collection object with monthly median composites.
|
|
1073
|
+
"""
|
|
1074
|
+
if self._monthly_median is None:
|
|
1075
|
+
collection = self.collection
|
|
1076
|
+
# Get the start and end dates of the entire collection.
|
|
1077
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1078
|
+
start_date = ee.Date(date_range.get('min'))
|
|
1079
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1080
|
+
|
|
1081
|
+
# Calculate the total number of months in the date range.
|
|
1082
|
+
# The .round() is important for ensuring we get an integer.
|
|
1083
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1084
|
+
|
|
1085
|
+
# Generate a list of starting dates for each month.
|
|
1086
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1087
|
+
def get_month_start(i):
|
|
1088
|
+
return start_date.advance(i, 'month')
|
|
1089
|
+
|
|
1090
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1091
|
+
|
|
1092
|
+
# Define a function to map over the list of month start dates.
|
|
1093
|
+
def create_monthly_composite(date):
|
|
1094
|
+
# Cast the input to an ee.Date object.
|
|
1095
|
+
start_of_month = ee.Date(date)
|
|
1096
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1097
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1098
|
+
|
|
1099
|
+
# Filter the original collection to get images for the current month.
|
|
1100
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1101
|
+
|
|
1102
|
+
# Count the number of images in the monthly subset.
|
|
1103
|
+
image_count = monthly_subset.size()
|
|
1104
|
+
|
|
1105
|
+
# Compute the median. This is robust to outliers like clouds.
|
|
1106
|
+
monthly_median = monthly_subset.median()
|
|
1107
|
+
|
|
1108
|
+
# Set essential properties on the resulting composite image.
|
|
1109
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1110
|
+
# The image_count is useful metadata for quality assessment.
|
|
1111
|
+
return monthly_median.set({
|
|
1112
|
+
'system:time_start': start_of_month.millis(),
|
|
1113
|
+
'month': start_of_month.get('month'),
|
|
1114
|
+
'year': start_of_month.get('year'),
|
|
1115
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1116
|
+
'image_count': image_count
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
# Map the composite function over the list of month start dates.
|
|
1120
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1121
|
+
|
|
1122
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1123
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1124
|
+
|
|
1125
|
+
# Filter out any composites that were created from zero images.
|
|
1126
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1127
|
+
final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1128
|
+
self._monthly_median = final_collection
|
|
1129
|
+
else:
|
|
1130
|
+
pass
|
|
1131
|
+
|
|
1132
|
+
return self._monthly_median
|
|
1133
|
+
|
|
1134
|
+
@property
|
|
1135
|
+
def monthly_mean_collection(self):
|
|
1136
|
+
"""Creates a monthly mean composite from a Sentinel1Collection image collection.
|
|
1137
|
+
|
|
1138
|
+
This function computes the mean for each
|
|
1139
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1140
|
+
temporal extent of the input collection.
|
|
1141
|
+
|
|
1142
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1143
|
+
first day of each month and an 'image_count' property indicating how
|
|
1144
|
+
many images were used in the composite. Months with no images are
|
|
1145
|
+
automatically excluded from the final collection.
|
|
1146
|
+
|
|
1147
|
+
NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
|
|
1148
|
+
|
|
1149
|
+
Returns:
|
|
1150
|
+
Sentinel1Collection: A new Sentinel1Collection object with monthly mean composites.
|
|
1151
|
+
"""
|
|
1152
|
+
if self._monthly_mean is None:
|
|
1153
|
+
collection = self.collection
|
|
1154
|
+
target_proj = collection.first().projection()
|
|
1155
|
+
# Get the start and end dates of the entire collection.
|
|
1156
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1157
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1158
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1159
|
+
|
|
1160
|
+
start_year = original_start_date.get('year')
|
|
1161
|
+
start_month = original_start_date.get('month')
|
|
1162
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1163
|
+
|
|
1164
|
+
# Calculate the total number of months in the date range.
|
|
1165
|
+
# The .round() is important for ensuring we get an integer.
|
|
1166
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1167
|
+
|
|
1168
|
+
# Generate a list of starting dates for each month.
|
|
1169
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1170
|
+
def get_month_start(i):
|
|
1171
|
+
return start_date.advance(i, 'month')
|
|
1172
|
+
|
|
1173
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1174
|
+
|
|
1175
|
+
# Define a function to map over the list of month start dates.
|
|
1176
|
+
def create_monthly_composite(date):
|
|
1177
|
+
# Cast the input to an ee.Date object.
|
|
1178
|
+
start_of_month = ee.Date(date)
|
|
1179
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1180
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1181
|
+
|
|
1182
|
+
# Filter the original collection to get images for the current month.
|
|
1183
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1184
|
+
|
|
1185
|
+
# Count the number of images in the monthly subset.
|
|
1186
|
+
image_count = monthly_subset.size()
|
|
1187
|
+
|
|
1188
|
+
# Compute the mean. This is robust to outliers like clouds.
|
|
1189
|
+
monthly_mean = monthly_subset.mean()
|
|
1190
|
+
|
|
1191
|
+
# Set essential properties on the resulting composite image.
|
|
1192
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1193
|
+
# The image_count is useful metadata for quality assessment.
|
|
1194
|
+
return monthly_mean.set({
|
|
1195
|
+
'system:time_start': start_of_month.millis(),
|
|
1196
|
+
'month': start_of_month.get('month'),
|
|
1197
|
+
'year': start_of_month.get('year'),
|
|
1198
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1199
|
+
'image_count': image_count
|
|
1200
|
+
}).reproject(target_proj)
|
|
1201
|
+
|
|
1202
|
+
# Map the composite function over the list of month start dates.
|
|
1203
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1204
|
+
|
|
1205
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1206
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1207
|
+
|
|
1208
|
+
# Filter out any composites that were created from zero images.
|
|
1209
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1210
|
+
final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1211
|
+
self._monthly_mean = final_collection
|
|
1212
|
+
else:
|
|
1213
|
+
pass
|
|
1214
|
+
|
|
1215
|
+
return self._monthly_mean
|
|
1216
|
+
|
|
1217
|
+
@property
|
|
1218
|
+
def monthly_sum_collection(self):
|
|
1219
|
+
"""Creates a monthly sum composite from a Sentinel1Collection image collection.
|
|
1220
|
+
|
|
1221
|
+
This function computes the sum for each
|
|
1222
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1223
|
+
temporal extent of the input collection.
|
|
1224
|
+
|
|
1225
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1226
|
+
first day of each month and an 'image_count' property indicating how
|
|
1227
|
+
many images were used in the composite. Months with no images are
|
|
1228
|
+
automatically excluded from the final collection.
|
|
1229
|
+
|
|
1230
|
+
NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
|
|
1231
|
+
|
|
1232
|
+
Returns:
|
|
1233
|
+
Sentinel1Collection: A new Sentinel1Collection object with monthly sum composites.
|
|
1234
|
+
"""
|
|
1235
|
+
if self._monthly_sum is None:
|
|
1236
|
+
collection = self.collection
|
|
1237
|
+
target_proj = collection.first().projection()
|
|
1238
|
+
# Get the start and end dates of the entire collection.
|
|
1239
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1240
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1241
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1242
|
+
|
|
1243
|
+
start_year = original_start_date.get('year')
|
|
1244
|
+
start_month = original_start_date.get('month')
|
|
1245
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1246
|
+
|
|
1247
|
+
# Calculate the total number of months in the date range.
|
|
1248
|
+
# The .round() is important for ensuring we get an integer.
|
|
1249
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1250
|
+
|
|
1251
|
+
# Generate a list of starting dates for each month.
|
|
1252
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1253
|
+
def get_month_start(i):
|
|
1254
|
+
return start_date.advance(i, 'month')
|
|
1255
|
+
|
|
1256
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1257
|
+
|
|
1258
|
+
# Define a function to map over the list of month start dates.
|
|
1259
|
+
def create_monthly_composite(date):
|
|
1260
|
+
# Cast the input to an ee.Date object.
|
|
1261
|
+
start_of_month = ee.Date(date)
|
|
1262
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1263
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1264
|
+
|
|
1265
|
+
# Filter the original collection to get images for the current month.
|
|
1266
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1267
|
+
|
|
1268
|
+
# Count the number of images in the monthly subset.
|
|
1269
|
+
image_count = monthly_subset.size()
|
|
1270
|
+
|
|
1271
|
+
# Compute the sum. This is robust to outliers like clouds.
|
|
1272
|
+
monthly_sum = monthly_subset.sum()
|
|
1273
|
+
|
|
1274
|
+
# Set essential properties on the resulting composite image.
|
|
1275
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1276
|
+
# The image_count is useful metadata for quality assessment.
|
|
1277
|
+
return monthly_sum.set({
|
|
1278
|
+
'system:time_start': start_of_month.millis(),
|
|
1279
|
+
'month': start_of_month.get('month'),
|
|
1280
|
+
'year': start_of_month.get('year'),
|
|
1281
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1282
|
+
'image_count': image_count
|
|
1283
|
+
}).reproject(target_proj)
|
|
1284
|
+
|
|
1285
|
+
# Map the composite function over the list of month start dates.
|
|
1286
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1287
|
+
|
|
1288
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1289
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1290
|
+
|
|
1291
|
+
# Filter out any composites that were created from zero images.
|
|
1292
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1293
|
+
final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1294
|
+
self._monthly_sum = final_collection
|
|
1295
|
+
else:
|
|
1296
|
+
pass
|
|
1297
|
+
|
|
1298
|
+
return self._monthly_sum
|
|
1299
|
+
|
|
1300
|
+
@property
|
|
1301
|
+
def monthly_max_collection(self):
|
|
1302
|
+
"""Creates a monthly max composite from a Sentinel1Collection image collection.
|
|
1303
|
+
|
|
1304
|
+
This function computes the max for each
|
|
1305
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1306
|
+
temporal extent of the input collection.
|
|
1307
|
+
|
|
1308
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1309
|
+
first day of each month and an 'image_count' property indicating how
|
|
1310
|
+
many images were used in the composite. Months with no images are
|
|
1311
|
+
automatically excluded from the final collection.
|
|
1312
|
+
|
|
1313
|
+
NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
|
|
1314
|
+
|
|
1315
|
+
Returns:
|
|
1316
|
+
Sentinel1Collection: A new Sentinel1Collection object with monthly max composites.
|
|
1317
|
+
"""
|
|
1318
|
+
if self._monthly_max is None:
|
|
1319
|
+
collection = self.collection
|
|
1320
|
+
target_proj = collection.first().projection()
|
|
1321
|
+
# Get the start and end dates of the entire collection.
|
|
1322
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1323
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1324
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1325
|
+
|
|
1326
|
+
start_year = original_start_date.get('year')
|
|
1327
|
+
start_month = original_start_date.get('month')
|
|
1328
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1329
|
+
|
|
1330
|
+
# Calculate the total number of months in the date range.
|
|
1331
|
+
# The .round() is important for ensuring we get an integer.
|
|
1332
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1333
|
+
|
|
1334
|
+
# Generate a list of starting dates for each month.
|
|
1335
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1336
|
+
def get_month_start(i):
|
|
1337
|
+
return start_date.advance(i, 'month')
|
|
1338
|
+
|
|
1339
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1340
|
+
|
|
1341
|
+
# Define a function to map over the list of month start dates.
|
|
1342
|
+
def create_monthly_composite(date):
|
|
1343
|
+
# Cast the input to an ee.Date object.
|
|
1344
|
+
start_of_month = ee.Date(date)
|
|
1345
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1346
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1347
|
+
|
|
1348
|
+
# Filter the original collection to get images for the current month.
|
|
1349
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1350
|
+
|
|
1351
|
+
# Count the number of images in the monthly subset.
|
|
1352
|
+
image_count = monthly_subset.size()
|
|
1353
|
+
|
|
1354
|
+
# Compute the max. This is robust to outliers like clouds.
|
|
1355
|
+
monthly_max = monthly_subset.max()
|
|
1356
|
+
|
|
1357
|
+
# Set essential properties on the resulting composite image.
|
|
1358
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1359
|
+
# The image_count is useful metadata for quality assessment.
|
|
1360
|
+
return monthly_max.set({
|
|
1361
|
+
'system:time_start': start_of_month.millis(),
|
|
1362
|
+
'month': start_of_month.get('month'),
|
|
1363
|
+
'year': start_of_month.get('year'),
|
|
1364
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1365
|
+
'image_count': image_count
|
|
1366
|
+
}).reproject(target_proj)
|
|
1367
|
+
|
|
1368
|
+
# Map the composite function over the list of month start dates.
|
|
1369
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1370
|
+
|
|
1371
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1372
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1373
|
+
|
|
1374
|
+
# Filter out any composites that were created from zero images.
|
|
1375
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1376
|
+
final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1377
|
+
self._monthly_max = final_collection
|
|
1378
|
+
else:
|
|
1379
|
+
pass
|
|
1380
|
+
|
|
1381
|
+
return self._monthly_max
|
|
1382
|
+
|
|
1383
|
+
@property
|
|
1384
|
+
def monthly_min_collection(self):
|
|
1385
|
+
"""Creates a monthly min composite from a Sentinel1Collection image collection.
|
|
1386
|
+
|
|
1387
|
+
This function computes the min for each
|
|
1388
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1389
|
+
temporal extent of the input collection.
|
|
1390
|
+
|
|
1391
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1392
|
+
first day of each month and an 'image_count' property indicating how
|
|
1393
|
+
many images were used in the composite. Months with no images are
|
|
1394
|
+
automatically excluded from the final collection.
|
|
1395
|
+
|
|
1396
|
+
NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
|
|
1397
|
+
|
|
1398
|
+
Returns:
|
|
1399
|
+
Sentinel1Collection: A new Sentinel1Collection object with monthly min composites.
|
|
1400
|
+
"""
|
|
1401
|
+
if self._monthly_min is None:
|
|
1402
|
+
collection = self.collection
|
|
1403
|
+
target_proj = collection.first().projection()
|
|
1404
|
+
# Get the start and end dates of the entire collection.
|
|
1405
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1406
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1407
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1408
|
+
|
|
1409
|
+
start_year = original_start_date.get('year')
|
|
1410
|
+
start_month = original_start_date.get('month')
|
|
1411
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1412
|
+
|
|
1413
|
+
# Calculate the total number of months in the date range.
|
|
1414
|
+
# The .round() is important for ensuring we get an integer.
|
|
1415
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1416
|
+
|
|
1417
|
+
# Generate a list of starting dates for each month.
|
|
1418
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1419
|
+
def get_month_start(i):
|
|
1420
|
+
return start_date.advance(i, 'month')
|
|
1421
|
+
|
|
1422
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1423
|
+
|
|
1424
|
+
# Define a function to map over the list of month start dates.
|
|
1425
|
+
def create_monthly_composite(date):
|
|
1426
|
+
# Cast the input to an ee.Date object.
|
|
1427
|
+
start_of_month = ee.Date(date)
|
|
1428
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1429
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1430
|
+
|
|
1431
|
+
# Filter the original collection to get images for the current month.
|
|
1432
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1433
|
+
|
|
1434
|
+
# Count the number of images in the monthly subset.
|
|
1435
|
+
image_count = monthly_subset.size()
|
|
1436
|
+
|
|
1437
|
+
# Compute the min. This is robust to outliers like clouds.
|
|
1438
|
+
monthly_min = monthly_subset.min()
|
|
1439
|
+
|
|
1440
|
+
# Set essential properties on the resulting composite image.
|
|
1441
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1442
|
+
# The image_count is useful metadata for quality assessment.
|
|
1443
|
+
return monthly_min.set({
|
|
1444
|
+
'system:time_start': start_of_month.millis(),
|
|
1445
|
+
'month': start_of_month.get('month'),
|
|
1446
|
+
'year': start_of_month.get('year'),
|
|
1447
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1448
|
+
'image_count': image_count
|
|
1449
|
+
}).reproject(target_proj)
|
|
1450
|
+
|
|
1451
|
+
# Map the composite function over the list of month start dates.
|
|
1452
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1453
|
+
|
|
1454
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1455
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1456
|
+
|
|
1457
|
+
# Filter out any composites that were created from zero images.
|
|
1458
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1459
|
+
final_collection = Sentinel1Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1460
|
+
self._monthly_min = final_collection
|
|
1461
|
+
else:
|
|
1462
|
+
pass
|
|
1463
|
+
|
|
1464
|
+
return self._monthly_min
|
|
1465
|
+
|
|
1466
|
+
def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=10):
|
|
1467
|
+
"""
|
|
1468
|
+
Calculates the anomaly of each image in a collection compared to the mean of each image.
|
|
1469
|
+
|
|
1470
|
+
This function computes the anomaly for each band in the input image by
|
|
1471
|
+
subtracting the mean value of that band from a provided ImageCollection.
|
|
1472
|
+
The anomaly is a measure of how much the pixel values deviate from the
|
|
1473
|
+
average conditions represented by the collection.
|
|
1474
|
+
|
|
1475
|
+
Args:
|
|
1476
|
+
geometry (ee.Geometry): The geometry for image reduction to define the mean value to be used for anomaly calculation.
|
|
1477
|
+
band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
1478
|
+
anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
1479
|
+
replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band. If True, the output image will only contain the anomaly band. If False, the output image will retain all original bands and add the anomaly band. Default is True.
|
|
1480
|
+
scale (int, optional): The scale (in meters) to use for the image reduction. Default is 10 meters.
|
|
1481
|
+
|
|
1482
|
+
Returns:
|
|
1483
|
+
Sentinel1Collection: A Sentinel1Collection where each image represents the anomaly (deviation from
|
|
1484
|
+
the mean) for the chosen band. The output images retain the same band name.
|
|
1485
|
+
"""
|
|
1486
|
+
if self.collection.size().eq(0).getInfo():
|
|
1487
|
+
raise ValueError("The collection is empty.")
|
|
1488
|
+
if band_name is None:
|
|
1489
|
+
first_image = self.collection.first()
|
|
1490
|
+
band_names = first_image.bandNames()
|
|
1491
|
+
if band_names.size().getInfo() == 0:
|
|
1492
|
+
raise ValueError("No bands available in the collection.")
|
|
1493
|
+
elif band_names.size().getInfo() > 1:
|
|
1494
|
+
band_name = band_names.get(0).getInfo()
|
|
1495
|
+
print("Multiple bands available, will be using the first band in the collection for anomaly calculation. Please specify a band name if you wish to use a different band.")
|
|
1496
|
+
else:
|
|
1497
|
+
band_name = band_names.get(0).getInfo()
|
|
1498
|
+
|
|
1499
|
+
col = self.collection.map(lambda image: Sentinel1Collection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
|
|
1500
|
+
return Sentinel1Collection(collection=col)
|
|
943
1501
|
|
|
944
1502
|
def binary_mask(self, threshold=None, band_name=None):
|
|
945
1503
|
"""
|
|
@@ -1734,24 +2292,30 @@ class Sentinel1Collection:
|
|
|
1734
2292
|
ValueError: If input parameters are invalid.
|
|
1735
2293
|
TypeError: If geometries input type is unsupported.
|
|
1736
2294
|
"""
|
|
2295
|
+
# Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
|
|
1737
2296
|
img_collection_obj = self
|
|
2297
|
+
|
|
2298
|
+
# If a specific band is requested, select only that band
|
|
1738
2299
|
if band:
|
|
1739
2300
|
img_collection_obj = Sentinel1Collection(collection=img_collection_obj.collection.select(band))
|
|
1740
2301
|
else:
|
|
2302
|
+
# If no band is specified, default to using the first band of the first image in the collection
|
|
1741
2303
|
first_image = img_collection_obj.image_grab(0)
|
|
1742
2304
|
first_band = first_image.bandNames().get(0)
|
|
1743
2305
|
img_collection_obj = Sentinel1Collection(collection=img_collection_obj.collection.select([first_band]))
|
|
1744
|
-
|
|
2306
|
+
|
|
2307
|
+
# If a list of dates is provided, filter the collection to include only images matching those dates
|
|
1745
2308
|
if dates:
|
|
1746
2309
|
img_collection_obj = Sentinel1Collection(
|
|
1747
2310
|
collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
|
|
1748
2311
|
)
|
|
1749
2312
|
|
|
1750
|
-
# Initialize variables
|
|
2313
|
+
# Initialize variables to hold the standardized feature collection and coordinates
|
|
1751
2314
|
features = None
|
|
1752
2315
|
validated_coordinates = []
|
|
1753
2316
|
|
|
1754
|
-
#
|
|
2317
|
+
# Define a helper function to ensure every feature has a standardized 'geo_name' property
|
|
2318
|
+
# This handles features that might have different existing name properties or none at all
|
|
1755
2319
|
def set_standard_name(feature):
|
|
1756
2320
|
has_geo_name = feature.get('geo_name')
|
|
1757
2321
|
has_name = feature.get('name')
|
|
@@ -1762,33 +2326,38 @@ class Sentinel1Collection:
|
|
|
1762
2326
|
ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
|
|
1763
2327
|
return feature.set({'geo_name': new_name})
|
|
1764
2328
|
|
|
2329
|
+
# Handle input: FeatureCollection or single Feature
|
|
1765
2330
|
if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
|
|
1766
2331
|
features = ee.FeatureCollection(geometries)
|
|
1767
2332
|
if geometry_names:
|
|
1768
2333
|
print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
|
|
1769
2334
|
|
|
2335
|
+
# Handle input: Single ee.Geometry
|
|
1770
2336
|
elif isinstance(geometries, ee.Geometry):
|
|
1771
2337
|
name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
|
|
1772
2338
|
features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
|
|
1773
2339
|
|
|
2340
|
+
# Handle input: List (could be coordinates or ee.Geometry objects)
|
|
1774
2341
|
elif isinstance(geometries, list):
|
|
1775
2342
|
if not geometries: # Handle empty list case
|
|
1776
2343
|
raise ValueError("'geometries' list cannot be empty.")
|
|
1777
2344
|
|
|
1778
|
-
# Case: List of coordinates
|
|
2345
|
+
# Case: List of tuples (coordinates)
|
|
1779
2346
|
if all(isinstance(i, tuple) for i in geometries):
|
|
1780
2347
|
validated_coordinates = geometries
|
|
2348
|
+
# Generate default names if none provided
|
|
1781
2349
|
if geometry_names is None:
|
|
1782
2350
|
geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
|
|
1783
2351
|
elif len(geometry_names) != len(validated_coordinates):
|
|
1784
2352
|
raise ValueError("geometry_names must have the same length as the coordinates list.")
|
|
2353
|
+
# Create features with buffers around the coordinates
|
|
1785
2354
|
points = [
|
|
1786
2355
|
ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
|
|
1787
2356
|
for coord, name in zip(validated_coordinates, geometry_names)
|
|
1788
2357
|
]
|
|
1789
2358
|
features = ee.FeatureCollection(points)
|
|
1790
2359
|
|
|
1791
|
-
# Case: List of
|
|
2360
|
+
# Case: List of ee.Geometry objects
|
|
1792
2361
|
elif all(isinstance(i, ee.Geometry) for i in geometries):
|
|
1793
2362
|
if geometry_names is None:
|
|
1794
2363
|
geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
|
|
@@ -1803,6 +2372,7 @@ class Sentinel1Collection:
|
|
|
1803
2372
|
else:
|
|
1804
2373
|
raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
|
|
1805
2374
|
|
|
2375
|
+
# Handle input: Single tuple (coordinate)
|
|
1806
2376
|
elif isinstance(geometries, tuple) and len(geometries) == 2:
|
|
1807
2377
|
name = geometry_names[0] if geometry_names else 'Location_1'
|
|
1808
2378
|
features = ee.FeatureCollection([
|
|
@@ -1811,39 +2381,48 @@ class Sentinel1Collection:
|
|
|
1811
2381
|
else:
|
|
1812
2382
|
raise TypeError("Unsupported type for 'geometries'.")
|
|
1813
2383
|
|
|
2384
|
+
# Apply the naming standardization to the created FeatureCollection
|
|
1814
2385
|
features = features.map(set_standard_name)
|
|
1815
2386
|
|
|
2387
|
+
# Dynamically retrieve the Earth Engine reducer based on the string name provided
|
|
1816
2388
|
try:
|
|
1817
2389
|
reducer = getattr(ee.Reducer, reducer_type)()
|
|
1818
2390
|
except AttributeError:
|
|
1819
2391
|
raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
|
|
1820
2392
|
|
|
2393
|
+
# Define the function to map over the image collection
|
|
1821
2394
|
def calculate_stats_for_image(image):
|
|
1822
2395
|
image_date = image.get('Date_Filter')
|
|
2396
|
+
# Calculate statistics for all geometries in 'features' for this specific image
|
|
1823
2397
|
stats_fc = image.reduceRegions(
|
|
1824
2398
|
collection=features, reducer=reducer, scale=scale, tileScale=tileScale
|
|
1825
2399
|
)
|
|
1826
2400
|
|
|
2401
|
+
# Helper to ensure the result has the reducer property, even if masked
|
|
2402
|
+
# If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
|
|
1827
2403
|
def guarantee_reducer_property(f):
|
|
1828
2404
|
has_property = f.propertyNames().contains(reducer_type)
|
|
1829
2405
|
return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
|
|
2406
|
+
|
|
2407
|
+
# Apply the guarantee check
|
|
1830
2408
|
fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
|
|
1831
2409
|
|
|
2410
|
+
# Attach the image date to every feature in the result so we know which image it came from
|
|
1832
2411
|
return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
|
|
1833
2412
|
|
|
2413
|
+
# Map the calculation over the image collection and flatten the resulting FeatureCollections into one
|
|
1834
2414
|
results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
|
|
2415
|
+
|
|
2416
|
+
# Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
|
|
1835
2417
|
df = Sentinel1Collection.ee_to_df(results_fc, remove_geom=True)
|
|
1836
2418
|
|
|
1837
|
-
#
|
|
2419
|
+
# Check for empty results or missing columns
|
|
1838
2420
|
if df.empty:
|
|
1839
|
-
# print("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
|
|
1840
|
-
# return df
|
|
1841
2421
|
raise ValueError("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
|
|
1842
2422
|
if reducer_type not in df.columns:
|
|
1843
2423
|
print(f"Warning: Reducer '{reducer_type}' not found in results.")
|
|
1844
|
-
# return df
|
|
1845
2424
|
|
|
1846
|
-
#
|
|
2425
|
+
# Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
|
|
1847
2426
|
initial_rows = len(df)
|
|
1848
2427
|
df.dropna(subset=[reducer_type], inplace=True)
|
|
1849
2428
|
df = df[df[reducer_type] != -9999]
|
|
@@ -1851,9 +2430,18 @@ class Sentinel1Collection:
|
|
|
1851
2430
|
if dropped_rows > 0:
|
|
1852
2431
|
print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
|
|
1853
2432
|
|
|
1854
|
-
#
|
|
2433
|
+
# Pivot the DataFrame so that each row represents a date and each column represents a geometry location
|
|
1855
2434
|
pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
|
|
2435
|
+
# Rename the column headers (geometry names) to include the reducer type
|
|
2436
|
+
pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
|
|
2437
|
+
# Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
|
|
1856
2438
|
pivot_df.index.name = 'Date'
|
|
2439
|
+
# Remove the name of the columns axis (which defaults to 'geo_name') so it doesn't appear as a confusing label in the final output
|
|
2440
|
+
pivot_df.columns.name = None
|
|
2441
|
+
# Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
|
|
2442
|
+
pivot_df = pivot_df.reset_index(drop=False)
|
|
2443
|
+
|
|
2444
|
+
# If a file path is provided, save the resulting DataFrame to CSV
|
|
1857
2445
|
if file_path:
|
|
1858
2446
|
# Check if file_path ends with .csv and remove it if so for consistency
|
|
1859
2447
|
if file_path.endswith('.csv'):
|