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