RadGEEToolbox 1.7.2__py3-none-any.whl → 1.7.4__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 +249 -0
- RadGEEToolbox/GenericCollection.py +763 -42
- RadGEEToolbox/LandsatCollection.py +938 -111
- RadGEEToolbox/Sentinel1Collection.py +801 -39
- RadGEEToolbox/Sentinel2Collection.py +869 -75
- RadGEEToolbox/__init__.py +6 -4
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.4.dist-info}/METADATA +11 -7
- radgeetoolbox-1.7.4.dist-info/RECORD +14 -0
- radgeetoolbox-1.7.2.dist-info/RECORD +0 -13
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.4.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.4.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.7.2.dist-info → radgeetoolbox-1.7.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ee
|
|
2
2
|
import pandas as pd
|
|
3
3
|
import numpy as np
|
|
4
|
+
import warnings
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class GenericCollection:
|
|
@@ -81,6 +82,11 @@ class GenericCollection:
|
|
|
81
82
|
self._monthly_sum = None
|
|
82
83
|
self._monthly_max = None
|
|
83
84
|
self._monthly_min = None
|
|
85
|
+
self._yearly_median = None
|
|
86
|
+
self._yearly_mean = None
|
|
87
|
+
self._yearly_max = None
|
|
88
|
+
self._yearly_min = None
|
|
89
|
+
self._yearly_sum = None
|
|
84
90
|
self._mean = None
|
|
85
91
|
self._max = None
|
|
86
92
|
self._min = None
|
|
@@ -88,6 +94,14 @@ class GenericCollection:
|
|
|
88
94
|
self._PixelAreaSumCollection = None
|
|
89
95
|
self._daily_aggregate_collection = None
|
|
90
96
|
|
|
97
|
+
def __call__(self):
|
|
98
|
+
"""
|
|
99
|
+
Allows the object to be called as a function, returning itself.
|
|
100
|
+
This enables property-like methods to be accessed with or without parentheses
|
|
101
|
+
(e.g., .mosaicByDate or .mosaicByDate()).
|
|
102
|
+
"""
|
|
103
|
+
return self
|
|
104
|
+
|
|
91
105
|
@staticmethod
|
|
92
106
|
def image_dater(image):
|
|
93
107
|
"""
|
|
@@ -163,7 +177,7 @@ class GenericCollection:
|
|
|
163
177
|
if replace:
|
|
164
178
|
return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
165
179
|
else:
|
|
166
|
-
return image.addBands(anomaly_image, overwrite=True).copyProperties(image)
|
|
180
|
+
return image.addBands(anomaly_image, overwrite=True).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
167
181
|
|
|
168
182
|
@staticmethod
|
|
169
183
|
def mask_via_band_fn(image, band_to_mask, band_for_mask, threshold, mask_above=False, add_band_to_original_image=False):
|
|
@@ -191,7 +205,7 @@ class GenericCollection:
|
|
|
191
205
|
if add_band_to_original_image:
|
|
192
206
|
return image.addBands(band_to_mask_image.updateMask(mask).rename(band_to_mask), overwrite=True)
|
|
193
207
|
else:
|
|
194
|
-
return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image))
|
|
208
|
+
return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image).set('system:time_start', image.get('system:time_start')))
|
|
195
209
|
|
|
196
210
|
@staticmethod
|
|
197
211
|
def mask_via_singleband_image_fn(image_to_mask, image_for_mask, threshold, band_name_to_mask=None, band_name_for_mask=None, mask_above=True):
|
|
@@ -227,7 +241,7 @@ class GenericCollection:
|
|
|
227
241
|
mask = band_for_mask_image.gt(threshold)
|
|
228
242
|
else:
|
|
229
243
|
mask = band_for_mask_image.lt(threshold)
|
|
230
|
-
return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask)
|
|
244
|
+
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'))
|
|
231
245
|
|
|
232
246
|
@staticmethod
|
|
233
247
|
def band_rename_fn(image, current_band_name, new_band_name):
|
|
@@ -263,10 +277,10 @@ class GenericCollection:
|
|
|
263
277
|
return img.rename(ee.List(new_names))
|
|
264
278
|
|
|
265
279
|
out = ee.Image(ee.Algorithms.If(has_band, _rename(), img))
|
|
266
|
-
return out.copyProperties(img)
|
|
280
|
+
return out.copyProperties(img).set('system:time_start', img.get('system:time_start'))
|
|
267
281
|
|
|
268
282
|
@staticmethod
|
|
269
|
-
def
|
|
283
|
+
def pixelAreaSum(
|
|
270
284
|
image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
|
|
271
285
|
):
|
|
272
286
|
"""
|
|
@@ -326,7 +340,19 @@ class GenericCollection:
|
|
|
326
340
|
final_image = ee.Image(bands.iterate(calculate_and_set_area, image))
|
|
327
341
|
return final_image #.set('system:time_start', image.get('system:time_start'))
|
|
328
342
|
|
|
329
|
-
|
|
343
|
+
@staticmethod
|
|
344
|
+
def PixelAreaSum(
|
|
345
|
+
image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
|
|
346
|
+
):
|
|
347
|
+
warnings.warn(
|
|
348
|
+
"The `PixelAreaSum` static method is deprecated and will be removed in future versions. Please use the `pixelAreaSum` static method instead.",
|
|
349
|
+
DeprecationWarning,
|
|
350
|
+
stacklevel=2)
|
|
351
|
+
return GenericCollection.pixelAreaSum(
|
|
352
|
+
image, band_name, geometry, threshold, scale, maxPixels
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def pixelAreaSumCollection(
|
|
330
356
|
self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
331
357
|
):
|
|
332
358
|
"""
|
|
@@ -353,7 +379,7 @@ class GenericCollection:
|
|
|
353
379
|
collection = self.collection
|
|
354
380
|
# Area calculation for each image in the collection, using the PixelAreaSum function
|
|
355
381
|
AreaCollection = collection.map(
|
|
356
|
-
lambda image: GenericCollection.
|
|
382
|
+
lambda image: GenericCollection.pixelAreaSum(
|
|
357
383
|
image,
|
|
358
384
|
band_name=band_name,
|
|
359
385
|
geometry=geometry,
|
|
@@ -369,17 +395,28 @@ class GenericCollection:
|
|
|
369
395
|
|
|
370
396
|
# If an export path is provided, the area data will be exported to a CSV file
|
|
371
397
|
if area_data_export_path:
|
|
372
|
-
GenericCollection(collection=self._PixelAreaSumCollection).
|
|
398
|
+
GenericCollection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
|
|
373
399
|
# Returning the result in the desired format based on output_type argument or raising an error for invalid input
|
|
374
400
|
if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
|
|
375
401
|
return self._PixelAreaSumCollection
|
|
376
402
|
elif output_type == 'GenericCollection':
|
|
377
403
|
return GenericCollection(collection=self._PixelAreaSumCollection)
|
|
378
404
|
elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
|
|
379
|
-
return GenericCollection(collection=self._PixelAreaSumCollection).
|
|
405
|
+
return GenericCollection(collection=self._PixelAreaSumCollection).exportProperties(property_names=prop_names)
|
|
380
406
|
else:
|
|
381
407
|
raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'GenericCollection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
|
|
382
408
|
|
|
409
|
+
def PixelAreaSumCollection(
|
|
410
|
+
self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
411
|
+
):
|
|
412
|
+
warnings.warn(
|
|
413
|
+
"The `PixelAreaSumCollection` method is deprecated and will be removed in future versions. Please use the `pixelAreaSumCollection` method instead.",
|
|
414
|
+
DeprecationWarning,
|
|
415
|
+
stacklevel=2)
|
|
416
|
+
return self.pixelAreaSumCollection(
|
|
417
|
+
band_name, geometry, threshold, scale, maxPixels, output_type, area_data_export_path
|
|
418
|
+
)
|
|
419
|
+
|
|
383
420
|
@staticmethod
|
|
384
421
|
def add_month_property_fn(image):
|
|
385
422
|
"""
|
|
@@ -539,7 +576,7 @@ class GenericCollection:
|
|
|
539
576
|
|
|
540
577
|
return GenericCollection(collection=distinct_col)
|
|
541
578
|
|
|
542
|
-
def
|
|
579
|
+
def exportProperties(self, property_names, file_path=None):
|
|
543
580
|
"""
|
|
544
581
|
Fetches and returns specified properties from each image in the collection as a list, and returns a pandas DataFrame and optionally saves the results to a csv file.
|
|
545
582
|
|
|
@@ -594,6 +631,13 @@ class GenericCollection:
|
|
|
594
631
|
print(f"Properties saved to {file_path}")
|
|
595
632
|
|
|
596
633
|
return df
|
|
634
|
+
|
|
635
|
+
def ExportProperties(self, property_names, file_path=None):
|
|
636
|
+
warnings.warn(
|
|
637
|
+
"The `ExportProperties` method is deprecated and will be removed in future versions. Please use the `exportProperties` method instead.",
|
|
638
|
+
DeprecationWarning,
|
|
639
|
+
stacklevel=2)
|
|
640
|
+
return self.exportProperties(property_names, file_path)
|
|
597
641
|
|
|
598
642
|
def get_generic_collection(self):
|
|
599
643
|
"""
|
|
@@ -959,6 +1003,391 @@ class GenericCollection:
|
|
|
959
1003
|
|
|
960
1004
|
return self._monthly_sum
|
|
961
1005
|
|
|
1006
|
+
def yearly_mean_collection(self, start_month=1, end_month=12):
|
|
1007
|
+
"""
|
|
1008
|
+
Creates a yearly mean composite from the collection, with optional monthly filtering.
|
|
1009
|
+
|
|
1010
|
+
This function computes the mean for each year within the collection's date range.
|
|
1011
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1012
|
+
to calculate the mean only using imagery from that specific season for each year.
|
|
1013
|
+
|
|
1014
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1015
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1016
|
+
|
|
1017
|
+
Args:
|
|
1018
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1019
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1020
|
+
|
|
1021
|
+
Returns:
|
|
1022
|
+
Object: A new instance of the same class (e.g., GenericCollection) containing the yearly mean composites.
|
|
1023
|
+
"""
|
|
1024
|
+
if self._yearly_mean is None:
|
|
1025
|
+
|
|
1026
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1027
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1028
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1029
|
+
|
|
1030
|
+
start_year = start_date_full.get('year')
|
|
1031
|
+
end_year = end_date_full.get('year')
|
|
1032
|
+
|
|
1033
|
+
if start_month != 1 or end_month != 12:
|
|
1034
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1035
|
+
else:
|
|
1036
|
+
processing_collection = self.collection
|
|
1037
|
+
|
|
1038
|
+
# Capture projection from the first image to restore it after reduction
|
|
1039
|
+
target_proj = self.collection.first().projection()
|
|
1040
|
+
|
|
1041
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1042
|
+
|
|
1043
|
+
def create_yearly_composite(year):
|
|
1044
|
+
year = ee.Number(year)
|
|
1045
|
+
# Define the full calendar year range
|
|
1046
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1047
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1048
|
+
|
|
1049
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1050
|
+
|
|
1051
|
+
# Calculate stats
|
|
1052
|
+
image_count = yearly_subset.size()
|
|
1053
|
+
yearly_reduction = yearly_subset.mean()
|
|
1054
|
+
|
|
1055
|
+
# Define the timestamp for the composite.
|
|
1056
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1057
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1058
|
+
|
|
1059
|
+
return yearly_reduction.set({
|
|
1060
|
+
'system:time_start': composite_date.millis(),
|
|
1061
|
+
'year': year,
|
|
1062
|
+
'month': start_month,
|
|
1063
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1064
|
+
'image_count': image_count,
|
|
1065
|
+
'season_start': start_month,
|
|
1066
|
+
'season_end': end_month
|
|
1067
|
+
}).reproject(target_proj)
|
|
1068
|
+
|
|
1069
|
+
# Map the function over the years list
|
|
1070
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1071
|
+
|
|
1072
|
+
# Convert to Collection
|
|
1073
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1074
|
+
|
|
1075
|
+
# Filter out any composites that were created from zero images.
|
|
1076
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1077
|
+
|
|
1078
|
+
self._yearly_mean = GenericCollection(collection=final_collection)
|
|
1079
|
+
else:
|
|
1080
|
+
pass
|
|
1081
|
+
return self._yearly_mean
|
|
1082
|
+
|
|
1083
|
+
def yearly_median_collection(self, start_month=1, end_month=12):
|
|
1084
|
+
"""
|
|
1085
|
+
Creates a yearly median composite from the collection, with optional monthly filtering.
|
|
1086
|
+
|
|
1087
|
+
This function computes the median for each year within the collection's date range.
|
|
1088
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1089
|
+
to calculate the median only using imagery from that specific season for each year.
|
|
1090
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1091
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1095
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1096
|
+
|
|
1097
|
+
Returns:
|
|
1098
|
+
Object: A new instance of the same class (e.g., GenericCollection) containing the yearly median composites.
|
|
1099
|
+
"""
|
|
1100
|
+
if self._yearly_median is None:
|
|
1101
|
+
|
|
1102
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1103
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1104
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1105
|
+
|
|
1106
|
+
start_year = start_date_full.get('year')
|
|
1107
|
+
end_year = end_date_full.get('year')
|
|
1108
|
+
|
|
1109
|
+
if start_month != 1 or end_month != 12:
|
|
1110
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1111
|
+
else:
|
|
1112
|
+
processing_collection = self.collection
|
|
1113
|
+
|
|
1114
|
+
# Capture projection from the first image to restore it after reduction
|
|
1115
|
+
target_proj = self.collection.first().projection()
|
|
1116
|
+
|
|
1117
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1118
|
+
|
|
1119
|
+
def create_yearly_composite(year):
|
|
1120
|
+
year = ee.Number(year)
|
|
1121
|
+
# Define the full calendar year range
|
|
1122
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1123
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1124
|
+
|
|
1125
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
1126
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1127
|
+
|
|
1128
|
+
# Calculate stats
|
|
1129
|
+
image_count = yearly_subset.size()
|
|
1130
|
+
yearly_reduction = yearly_subset.median()
|
|
1131
|
+
|
|
1132
|
+
# Define the timestamp for the composite.
|
|
1133
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1134
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1135
|
+
|
|
1136
|
+
return yearly_reduction.set({
|
|
1137
|
+
'system:time_start': composite_date.millis(),
|
|
1138
|
+
'year': year,
|
|
1139
|
+
'month': start_month,
|
|
1140
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1141
|
+
'image_count': image_count,
|
|
1142
|
+
'season_start': start_month,
|
|
1143
|
+
'season_end': end_month
|
|
1144
|
+
}).reproject(target_proj)
|
|
1145
|
+
|
|
1146
|
+
# Map the function over the years list
|
|
1147
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1148
|
+
|
|
1149
|
+
# Convert to Collection
|
|
1150
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1151
|
+
|
|
1152
|
+
# Filter out any composites that were created from zero images.
|
|
1153
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1154
|
+
|
|
1155
|
+
self._yearly_median = GenericCollection(collection=final_collection)
|
|
1156
|
+
else:
|
|
1157
|
+
pass
|
|
1158
|
+
return self._yearly_median
|
|
1159
|
+
|
|
1160
|
+
def yearly_max_collection(self, start_month=1, end_month=12):
|
|
1161
|
+
"""
|
|
1162
|
+
Creates a yearly max composite from the collection, with optional monthly filtering.
|
|
1163
|
+
|
|
1164
|
+
This function computes the max for each year within the collection's date range.
|
|
1165
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1166
|
+
to calculate the max only using imagery from that specific season for each year.
|
|
1167
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1168
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1169
|
+
|
|
1170
|
+
Args:
|
|
1171
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1172
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1173
|
+
|
|
1174
|
+
Returns:
|
|
1175
|
+
Object: A new instance of the same class (e.g., GenericCollection) containing the yearly max composites.
|
|
1176
|
+
"""
|
|
1177
|
+
if self._yearly_max is None:
|
|
1178
|
+
|
|
1179
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1180
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1181
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1182
|
+
|
|
1183
|
+
start_year = start_date_full.get('year')
|
|
1184
|
+
end_year = end_date_full.get('year')
|
|
1185
|
+
|
|
1186
|
+
if start_month != 1 or end_month != 12:
|
|
1187
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1188
|
+
else:
|
|
1189
|
+
processing_collection = self.collection
|
|
1190
|
+
|
|
1191
|
+
# Capture projection from the first image to restore it after reduction
|
|
1192
|
+
target_proj = self.collection.first().projection()
|
|
1193
|
+
|
|
1194
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1195
|
+
|
|
1196
|
+
def create_yearly_composite(year):
|
|
1197
|
+
year = ee.Number(year)
|
|
1198
|
+
# Define the full calendar year range
|
|
1199
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1200
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1201
|
+
|
|
1202
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
1203
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1204
|
+
|
|
1205
|
+
# Calculate stats
|
|
1206
|
+
image_count = yearly_subset.size()
|
|
1207
|
+
yearly_reduction = yearly_subset.max()
|
|
1208
|
+
|
|
1209
|
+
# Define the timestamp for the composite.
|
|
1210
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1211
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1212
|
+
|
|
1213
|
+
return yearly_reduction.set({
|
|
1214
|
+
'system:time_start': composite_date.millis(),
|
|
1215
|
+
'year': year,
|
|
1216
|
+
'month': start_month,
|
|
1217
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1218
|
+
'image_count': image_count,
|
|
1219
|
+
'season_start': start_month,
|
|
1220
|
+
'season_end': end_month
|
|
1221
|
+
}).reproject(target_proj)
|
|
1222
|
+
|
|
1223
|
+
# Map the function over the years list
|
|
1224
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1225
|
+
|
|
1226
|
+
# Convert to Collection
|
|
1227
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1228
|
+
|
|
1229
|
+
# Filter out any composites that were created from zero images.
|
|
1230
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1231
|
+
|
|
1232
|
+
self._yearly_max = GenericCollection(collection=final_collection)
|
|
1233
|
+
else:
|
|
1234
|
+
pass
|
|
1235
|
+
return self._yearly_max
|
|
1236
|
+
|
|
1237
|
+
def yearly_min_collection(self, start_month=1, end_month=12):
|
|
1238
|
+
"""
|
|
1239
|
+
Creates a yearly min composite from the collection, with optional monthly filtering.
|
|
1240
|
+
|
|
1241
|
+
This function computes the min for each year within the collection's date range.
|
|
1242
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1243
|
+
to calculate the min only using imagery from that specific season for each year.
|
|
1244
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1245
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1249
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1250
|
+
|
|
1251
|
+
Returns:
|
|
1252
|
+
Object: A new instance of the same class (e.g., GenericCollection) containing the yearly min composites.
|
|
1253
|
+
"""
|
|
1254
|
+
if self._yearly_min is None:
|
|
1255
|
+
|
|
1256
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1257
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1258
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1259
|
+
|
|
1260
|
+
start_year = start_date_full.get('year')
|
|
1261
|
+
end_year = end_date_full.get('year')
|
|
1262
|
+
|
|
1263
|
+
if start_month != 1 or end_month != 12:
|
|
1264
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1265
|
+
else:
|
|
1266
|
+
processing_collection = self.collection
|
|
1267
|
+
|
|
1268
|
+
# Capture projection from the first image to restore it after reduction
|
|
1269
|
+
target_proj = self.collection.first().projection()
|
|
1270
|
+
|
|
1271
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1272
|
+
|
|
1273
|
+
def create_yearly_composite(year):
|
|
1274
|
+
year = ee.Number(year)
|
|
1275
|
+
# Define the full calendar year range
|
|
1276
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1277
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1278
|
+
|
|
1279
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
1280
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1281
|
+
|
|
1282
|
+
# Calculate stats
|
|
1283
|
+
image_count = yearly_subset.size()
|
|
1284
|
+
yearly_reduction = yearly_subset.min()
|
|
1285
|
+
|
|
1286
|
+
# Define the timestamp for the composite.
|
|
1287
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1288
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1289
|
+
|
|
1290
|
+
return yearly_reduction.set({
|
|
1291
|
+
'system:time_start': composite_date.millis(),
|
|
1292
|
+
'year': year,
|
|
1293
|
+
'month': start_month,
|
|
1294
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1295
|
+
'image_count': image_count,
|
|
1296
|
+
'season_start': start_month,
|
|
1297
|
+
'season_end': end_month
|
|
1298
|
+
}).reproject(target_proj)
|
|
1299
|
+
|
|
1300
|
+
# Map the function over the years list
|
|
1301
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1302
|
+
|
|
1303
|
+
# Convert to Collection
|
|
1304
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1305
|
+
|
|
1306
|
+
# Filter out any composites that were created from zero images.
|
|
1307
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1308
|
+
|
|
1309
|
+
self._yearly_min = GenericCollection(collection=final_collection)
|
|
1310
|
+
else:
|
|
1311
|
+
pass
|
|
1312
|
+
return self._yearly_min
|
|
1313
|
+
|
|
1314
|
+
def yearly_sum_collection(self, start_month=1, end_month=12):
|
|
1315
|
+
"""
|
|
1316
|
+
Creates a yearly sum composite from the collection, with optional monthly filtering.
|
|
1317
|
+
|
|
1318
|
+
This function computes the sum for each year within the collection's date range.
|
|
1319
|
+
You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
|
|
1320
|
+
to calculate the sum only using imagery from that specific season for each year.
|
|
1321
|
+
The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
|
|
1322
|
+
'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
|
|
1323
|
+
|
|
1324
|
+
Args:
|
|
1325
|
+
start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
|
|
1326
|
+
end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
|
|
1327
|
+
|
|
1328
|
+
Returns:
|
|
1329
|
+
Object: A new instance of the same class (e.g., GenericCollection) containing the yearly sum composites.
|
|
1330
|
+
"""
|
|
1331
|
+
if self._yearly_sum is None:
|
|
1332
|
+
|
|
1333
|
+
date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1334
|
+
start_date_full = ee.Date(date_range.get('min'))
|
|
1335
|
+
end_date_full = ee.Date(date_range.get('max'))
|
|
1336
|
+
|
|
1337
|
+
start_year = start_date_full.get('year')
|
|
1338
|
+
end_year = end_date_full.get('year')
|
|
1339
|
+
|
|
1340
|
+
if start_month != 1 or end_month != 12:
|
|
1341
|
+
processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
|
|
1342
|
+
else:
|
|
1343
|
+
processing_collection = self.collection
|
|
1344
|
+
|
|
1345
|
+
# Capture projection from the first image to restore it after reduction
|
|
1346
|
+
target_proj = self.collection.first().projection()
|
|
1347
|
+
|
|
1348
|
+
years = ee.List.sequence(start_year, end_year)
|
|
1349
|
+
|
|
1350
|
+
def create_yearly_composite(year):
|
|
1351
|
+
year = ee.Number(year)
|
|
1352
|
+
# Define the full calendar year range
|
|
1353
|
+
start_of_year = ee.Date.fromYMD(year, 1, 1)
|
|
1354
|
+
end_of_year = start_of_year.advance(1, 'year')
|
|
1355
|
+
|
|
1356
|
+
# Filter to the specific year using the PRE-FILTERED seasonal collection
|
|
1357
|
+
yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
|
|
1358
|
+
|
|
1359
|
+
# Calculate stats
|
|
1360
|
+
image_count = yearly_subset.size()
|
|
1361
|
+
yearly_reduction = yearly_subset.sum()
|
|
1362
|
+
|
|
1363
|
+
# Define the timestamp for the composite.
|
|
1364
|
+
# We use the start_month of that year to accurately reflect the data start time.
|
|
1365
|
+
composite_date = ee.Date.fromYMD(year, start_month, 1)
|
|
1366
|
+
|
|
1367
|
+
return yearly_reduction.set({
|
|
1368
|
+
'system:time_start': composite_date.millis(),
|
|
1369
|
+
'year': year,
|
|
1370
|
+
'month': start_month,
|
|
1371
|
+
'Date_Filter': composite_date.format('YYYY-MM-dd'),
|
|
1372
|
+
'image_count': image_count,
|
|
1373
|
+
'season_start': start_month,
|
|
1374
|
+
'season_end': end_month
|
|
1375
|
+
}).reproject(target_proj)
|
|
1376
|
+
|
|
1377
|
+
# Map the function over the years list
|
|
1378
|
+
yearly_composites_list = years.map(create_yearly_composite)
|
|
1379
|
+
|
|
1380
|
+
# Convert to Collection
|
|
1381
|
+
yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
|
|
1382
|
+
|
|
1383
|
+
# Filter out any composites that were created from zero images.
|
|
1384
|
+
final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
|
|
1385
|
+
|
|
1386
|
+
self._yearly_sum = GenericCollection(collection=final_collection)
|
|
1387
|
+
else:
|
|
1388
|
+
pass
|
|
1389
|
+
return self._yearly_sum
|
|
1390
|
+
|
|
962
1391
|
@property
|
|
963
1392
|
def monthly_max_collection(self):
|
|
964
1393
|
"""Creates a monthly max composite from a GenericCollection image collection.
|
|
@@ -1523,7 +1952,7 @@ class GenericCollection:
|
|
|
1523
1952
|
new_band_names = band_names.map(lambda b: ee.String(b).cat('_mm'))
|
|
1524
1953
|
|
|
1525
1954
|
converted_image = image.multiply(10800).rename(new_band_names)
|
|
1526
|
-
return converted_image.copyProperties(image, image.propertyNames())
|
|
1955
|
+
return converted_image.copyProperties(image, image.propertyNames()).set('system:time_start', image.get('system:time_start'))
|
|
1527
1956
|
|
|
1528
1957
|
# Map the function over the entire collection
|
|
1529
1958
|
converted_collection = self.collection.map(convert_to_mm)
|
|
@@ -1537,6 +1966,236 @@ class GenericCollection:
|
|
|
1537
1966
|
_dates_list=self._dates_list # Pass along the cached dates!
|
|
1538
1967
|
)
|
|
1539
1968
|
|
|
1969
|
+
def mann_kendall_trend(self, target_band=None, join_method='system:time_start', geometry=None):
|
|
1970
|
+
"""
|
|
1971
|
+
Calculates the Mann-Kendall S-value, Variance, Z-Score, and Confidence Level for each pixel in the image collection, in addition to calculating
|
|
1972
|
+
the Sen's slope for each pixel in the image collection. The output is an image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
|
|
1973
|
+
|
|
1974
|
+
This function can be used to identify trends in the image collection over time, such as increasing or decreasing values in the target band, and can be used to assess the significance of these trends.
|
|
1975
|
+
Note that this function is computationally intensive and may take a long time to run for large image collections or high-resolution images.
|
|
1976
|
+
|
|
1977
|
+
The 's_statistic' band represents the Mann-Kendall S-value, which is a measure of the strength and direction of the trend.
|
|
1978
|
+
The 'variance' band represents the variance of the S-value, which is a measure of the variability of the S-value.
|
|
1979
|
+
The 'z_score' band represents the Z-Score, which is a measure of the significance of the trend.
|
|
1980
|
+
The 'confidence' band represents the confidence level of the trend based on the z_score, which is a probabilistic measure of the confidence in the trend (percentage).
|
|
1981
|
+
The 'slope' band represents the Sen's slope, which is a measure of the rate of change in the target band over time. This value can be small as multispectral indices commonly range from -1 to 1, so a slope may have values of <0.2 for most cases.
|
|
1982
|
+
|
|
1983
|
+
Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
|
|
1984
|
+
You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
|
|
1985
|
+
The `geometry` parameter is optional and defaults to None, which means that the trend statistics will be calculated over the entire footprint of the image collection.
|
|
1986
|
+
|
|
1987
|
+
Args:
|
|
1988
|
+
image_collection (GenericCollection or ee.ImageCollection): The input image collection for which the Mann-Kendall and Sen's slope trend statistics will be calculated.
|
|
1989
|
+
target_band (str): The band name to be used for the output anomaly image. e.g. 'ndvi'
|
|
1990
|
+
join_method (str, optional): The method used to join images in the collection. Options are 'system:time_start' or 'Date_Filter'. Default is 'system:time_start'.
|
|
1991
|
+
geometry (ee.Geometry, optional): An ee.Geometry object to limit the area over which the trend statistics are calculated and mask the output image. Default is None.
|
|
1992
|
+
|
|
1993
|
+
Returns:
|
|
1994
|
+
ee.Image: An image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
|
|
1995
|
+
"""
|
|
1996
|
+
########## PART 1 - S-VALUE CALCULATION ##########
|
|
1997
|
+
##### https://vsp.pnnl.gov/help/vsample/design_trend_mann_kendall.htm #####
|
|
1998
|
+
image_collection = self
|
|
1999
|
+
if isinstance(image_collection, GenericCollection):
|
|
2000
|
+
image_collection = image_collection.collection
|
|
2001
|
+
elif isinstance(image_collection, ee.ImageCollection):
|
|
2002
|
+
pass
|
|
2003
|
+
else:
|
|
2004
|
+
raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid GenericCollection or ee.ImageCollection object.')
|
|
2005
|
+
|
|
2006
|
+
if target_band is None:
|
|
2007
|
+
raise ValueError('The `target_band` parameter must be specified.')
|
|
2008
|
+
if not isinstance(target_band, str):
|
|
2009
|
+
raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
|
|
2010
|
+
|
|
2011
|
+
if geometry is not None and not isinstance(geometry, ee.Geometry):
|
|
2012
|
+
raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
|
|
2013
|
+
# define the join, which will join all images newer than the current image
|
|
2014
|
+
# use system:time_start if the image does not have a Date_Filter property
|
|
2015
|
+
if join_method == 'system:time_start':
|
|
2016
|
+
# get all images where the leftField value is less than (before) the rightField value
|
|
2017
|
+
time_filter = ee.Filter.lessThan(leftField='system:time_start',
|
|
2018
|
+
rightField='system:time_start')
|
|
2019
|
+
elif join_method == 'Date_Filter':
|
|
2020
|
+
# get all images where the leftField value is less than (before) the rightField value
|
|
2021
|
+
time_filter = ee.Filter.lessThan(leftField='Date_Filter',
|
|
2022
|
+
rightField='Date_Filter')
|
|
2023
|
+
else:
|
|
2024
|
+
raise ValueError(f'The chosen `join_method`: {join_method} does not match the options of "system:time_start" or "Date_Filter".')
|
|
2025
|
+
|
|
2026
|
+
native_projection = image_collection.first().select(target_band).projection()
|
|
2027
|
+
|
|
2028
|
+
# for any matches during a join, set image as a property key called 'future_image'
|
|
2029
|
+
join = ee.Join.saveAll(matchesKey='future_image')
|
|
2030
|
+
|
|
2031
|
+
# apply the join on the input collection
|
|
2032
|
+
# joining all images newer than the current image with the current image
|
|
2033
|
+
joined_collection = ee.ImageCollection(join.apply(primary=image_collection,
|
|
2034
|
+
secondary=image_collection, condition=time_filter))
|
|
2035
|
+
|
|
2036
|
+
# defining a collection to calculate the partial S value for each match in the join
|
|
2037
|
+
# e.g. t4-t1, t3-t1, t2-1 if there are 4 images
|
|
2038
|
+
def calculate_partial_s(current_image):
|
|
2039
|
+
# select the target band for arithmetic
|
|
2040
|
+
current_val = current_image.select(target_band)
|
|
2041
|
+
# get the joined images from the current image properties and cast the joined images as a list
|
|
2042
|
+
future_image_list = ee.List(current_image.get('future_image'))
|
|
2043
|
+
# convert the joined list to an image collection
|
|
2044
|
+
future_image_collection = ee.ImageCollection(future_image_list)
|
|
2045
|
+
|
|
2046
|
+
# define a function that will calculate the difference between the joined images and the current image,
|
|
2047
|
+
# then calculate the partial S sign based on the value of the difference calculation
|
|
2048
|
+
def get_sign(future_image):
|
|
2049
|
+
# select the target band for arithmetic from the future image
|
|
2050
|
+
future_val = future_image.select(target_band)
|
|
2051
|
+
# calculate the difference, i.e. t2-t1
|
|
2052
|
+
difference = future_val.subtract(current_val)
|
|
2053
|
+
# determine the sign of the difference value (1 if diff > 0, 0 if 0, and -1 if diff < 0)
|
|
2054
|
+
# use .unmask(0) to set any masked pixels as 0 to avoid
|
|
2055
|
+
|
|
2056
|
+
sign = difference.signum().unmask(0)
|
|
2057
|
+
|
|
2058
|
+
return sign
|
|
2059
|
+
|
|
2060
|
+
# map the get_sign() function along the future image col
|
|
2061
|
+
# then sum the values for each pixel to get the partial S value
|
|
2062
|
+
return future_image_collection.map(get_sign).sum()
|
|
2063
|
+
|
|
2064
|
+
# calculate the partial s value for each image in the joined/input image collection
|
|
2065
|
+
partial_s_col = joined_collection.map(calculate_partial_s)
|
|
2066
|
+
|
|
2067
|
+
# convert the image collection to an image of s_statistic values per pixel
|
|
2068
|
+
# where the s_statistic is the sum of partial s values
|
|
2069
|
+
# renaming the band as 's_statistic' for later usage
|
|
2070
|
+
final_s_image = partial_s_col.sum().rename('s_statistic').setDefaultProjection(native_projection)
|
|
2071
|
+
|
|
2072
|
+
|
|
2073
|
+
########## PART 2 - VARIANCE and Z-SCORE ##########
|
|
2074
|
+
# to calculate variance we need to know how many pixels were involved in the partial_s calculations per pixel
|
|
2075
|
+
# we do this by using count() and turn the value to a float for later arithmetic
|
|
2076
|
+
n = image_collection.select(target_band).count().toFloat()
|
|
2077
|
+
|
|
2078
|
+
##### VARIANCE CALCULATION #####
|
|
2079
|
+
# as we are using floating point values with high precision, it is HIGHLY
|
|
2080
|
+
# unlikely that there will be multiple pixel values with the same value.
|
|
2081
|
+
# Thus, we opt to use the simplified variance calculation approach as the
|
|
2082
|
+
# impacts to the output value are negligible and the processing benefits are HUGE
|
|
2083
|
+
# variance = (n * (n - 1) * (2n + 5)) / 18
|
|
2084
|
+
var_s = n.multiply(n.subtract(1))\
|
|
2085
|
+
.multiply(n.multiply(2).add(5))\
|
|
2086
|
+
.divide(18).rename('variance')
|
|
2087
|
+
|
|
2088
|
+
z_score = ee.Image().expression(
|
|
2089
|
+
"""
|
|
2090
|
+
(s > 0) ? (s - 1) / sqrt(var) :
|
|
2091
|
+
(s < 0) ? (s + 1) / sqrt(var) :
|
|
2092
|
+
0
|
|
2093
|
+
""",
|
|
2094
|
+
{'s': final_s_image, 'var': var_s}
|
|
2095
|
+
).rename('z_score')
|
|
2096
|
+
|
|
2097
|
+
confidence = z_score.abs().divide(ee.Number(2).sqrt()).erf().rename('confidence')
|
|
2098
|
+
|
|
2099
|
+
stat_bands = ee.Image([var_s, z_score, confidence])
|
|
2100
|
+
|
|
2101
|
+
mk_stats_image = final_s_image.addBands(stat_bands)
|
|
2102
|
+
|
|
2103
|
+
########## PART 3 - Sen's Slope ##########
|
|
2104
|
+
def add_year_band(image):
|
|
2105
|
+
if join_method == 'Date_Filter':
|
|
2106
|
+
# Get the string 'YYYY-MM-DD'
|
|
2107
|
+
date_string = image.get('Date_Filter')
|
|
2108
|
+
# Parse it into an ee.Date object (handles the conversion to time math)
|
|
2109
|
+
date = ee.Date.parse('YYYY-MM-dd', date_string)
|
|
2110
|
+
else:
|
|
2111
|
+
# Standard way: assumes system:time_start exists
|
|
2112
|
+
date = image.date()
|
|
2113
|
+
years = date.difference(ee.Date('1970-01-01'), 'year')
|
|
2114
|
+
return image.addBands(ee.Image(years).float().rename('year'))
|
|
2115
|
+
|
|
2116
|
+
slope_input = image_collection.map(add_year_band).select(['year', target_band])
|
|
2117
|
+
|
|
2118
|
+
sens_slope = slope_input.reduce(ee.Reducer.sensSlope())
|
|
2119
|
+
|
|
2120
|
+
slope_band = sens_slope.select('slope')
|
|
2121
|
+
|
|
2122
|
+
# add a mask to the final image to remove pixels with less than min_observations
|
|
2123
|
+
# mainly an effort to mask pixels outside of the boundary of the input image collection
|
|
2124
|
+
min_observations = 1
|
|
2125
|
+
valid_mask = n.gte(min_observations)
|
|
2126
|
+
|
|
2127
|
+
final_image = mk_stats_image.addBands(slope_band).updateMask(valid_mask)
|
|
2128
|
+
|
|
2129
|
+
if geometry is not None:
|
|
2130
|
+
mask = ee.Image(1).clip(geometry)
|
|
2131
|
+
final_image = final_image.updateMask(mask)
|
|
2132
|
+
|
|
2133
|
+
return final_image.setDefaultProjection(native_projection)
|
|
2134
|
+
|
|
2135
|
+
def sens_slope_trend(self, target_band=None, join_method='system:time_start', geometry=None):
|
|
2136
|
+
"""
|
|
2137
|
+
Calculates Sen's Slope (trend magnitude) for the collection.
|
|
2138
|
+
This is a lighter-weight alternative to the full `mann_kendall_trend` function if only
|
|
2139
|
+
the direction and magnitude of the trend are needed.
|
|
2140
|
+
|
|
2141
|
+
Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
|
|
2142
|
+
You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
|
|
2143
|
+
The `geometry` parameter is optional and defaults to None, which means that the trend statistics will be calculated over the entire footprint of the image collection.
|
|
2144
|
+
|
|
2145
|
+
Args:
|
|
2146
|
+
target_band (str): The name of the band to analyze. Defaults to 'ndvi'.
|
|
2147
|
+
join_method (str): Property to use for time sorting ('system:time_start' or 'Date_Filter').
|
|
2148
|
+
geometry (ee.Geometry, optional): Geometry to mask the final output.
|
|
2149
|
+
|
|
2150
|
+
Returns:
|
|
2151
|
+
ee.Image: An image containing the 'slope' band.
|
|
2152
|
+
"""
|
|
2153
|
+
image_collection = self
|
|
2154
|
+
if isinstance(image_collection, GenericCollection):
|
|
2155
|
+
image_collection = image_collection.collection
|
|
2156
|
+
elif isinstance(image_collection, ee.ImageCollection):
|
|
2157
|
+
pass
|
|
2158
|
+
else:
|
|
2159
|
+
raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid GenericCollection or ee.ImageCollection object.')
|
|
2160
|
+
|
|
2161
|
+
if target_band is None:
|
|
2162
|
+
raise ValueError('The `target_band` parameter must be specified.')
|
|
2163
|
+
if not isinstance(target_band, str):
|
|
2164
|
+
raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
|
|
2165
|
+
|
|
2166
|
+
if geometry is not None and not isinstance(geometry, ee.Geometry):
|
|
2167
|
+
raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
|
|
2168
|
+
|
|
2169
|
+
# Add Year Band (Time X-Axis)
|
|
2170
|
+
def add_year_band(image):
|
|
2171
|
+
# Handle user-defined date strings vs system time
|
|
2172
|
+
if join_method == 'Date_Filter':
|
|
2173
|
+
date_string = image.get('Date_Filter')
|
|
2174
|
+
date = ee.Date.parse('YYYY-MM-dd', date_string)
|
|
2175
|
+
else:
|
|
2176
|
+
date = image.date()
|
|
2177
|
+
|
|
2178
|
+
# Convert to fractional years relative to epoch
|
|
2179
|
+
years = date.difference(ee.Date('1970-01-01'), 'year')
|
|
2180
|
+
return image.addBands(ee.Image(years).float().rename('year'))
|
|
2181
|
+
|
|
2182
|
+
# Prepare Collection: Select ONLY [Year, Target]
|
|
2183
|
+
# sensSlope expects Band 0 = Independent (X), Band 1 = Dependent (Y)
|
|
2184
|
+
slope_input = self.collection.map(add_year_band).select(['year', target_band])
|
|
2185
|
+
|
|
2186
|
+
# Run the Native Reducer
|
|
2187
|
+
sens_result = slope_input.reduce(ee.Reducer.sensSlope())
|
|
2188
|
+
|
|
2189
|
+
# Extract and Mask
|
|
2190
|
+
slope_band = sens_result.select('slope')
|
|
2191
|
+
|
|
2192
|
+
if geometry is not None:
|
|
2193
|
+
mask = ee.Image(1).clip(geometry)
|
|
2194
|
+
slope_band = slope_band.updateMask(mask)
|
|
2195
|
+
|
|
2196
|
+
return slope_band
|
|
2197
|
+
|
|
2198
|
+
|
|
1540
2199
|
def mask_to_polygon(self, polygon):
|
|
1541
2200
|
"""
|
|
1542
2201
|
Function to mask GenericCollection image collection by a polygon (ee.Geometry), where pixels outside the polygon are masked out.
|
|
@@ -1548,20 +2207,15 @@ class GenericCollection:
|
|
|
1548
2207
|
GenericCollection: masked GenericCollection image collection
|
|
1549
2208
|
|
|
1550
2209
|
"""
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
mask = ee.Image.constant(1).clip(polygon)
|
|
1554
|
-
|
|
1555
|
-
# Update the mask of each image in the collection
|
|
1556
|
-
masked_collection = self.collection.map(lambda img: img.updateMask(mask))
|
|
2210
|
+
# Convert the polygon to a mask
|
|
2211
|
+
mask = ee.Image.constant(1).clip(polygon)
|
|
1557
2212
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
)
|
|
2213
|
+
# Update the mask of each image in the collection
|
|
2214
|
+
masked_collection = self.collection.map(lambda img: img.updateMask(mask)\
|
|
2215
|
+
.copyProperties(img).set('system:time_start', img.get('system:time_start')))
|
|
1562
2216
|
|
|
1563
2217
|
# Return the updated object
|
|
1564
|
-
return
|
|
2218
|
+
return GenericCollection(collection=masked_collection)
|
|
1565
2219
|
|
|
1566
2220
|
def mask_out_polygon(self, polygon):
|
|
1567
2221
|
"""
|
|
@@ -1574,23 +2228,18 @@ class GenericCollection:
|
|
|
1574
2228
|
GenericCollection: masked GenericCollection image collection
|
|
1575
2229
|
|
|
1576
2230
|
"""
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
full_mask = ee.Image.constant(1)
|
|
2231
|
+
# Convert the polygon to a mask
|
|
2232
|
+
full_mask = ee.Image.constant(1)
|
|
1580
2233
|
|
|
1581
|
-
|
|
1582
|
-
|
|
2234
|
+
# Use paint to set pixels inside polygon as 0
|
|
2235
|
+
area = full_mask.paint(polygon, 0)
|
|
1583
2236
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
# Update the internal collection state
|
|
1588
|
-
self._geometry_masked_out_collection = GenericCollection(
|
|
1589
|
-
collection=masked_collection
|
|
1590
|
-
)
|
|
2237
|
+
# Update the mask of each image in the collection
|
|
2238
|
+
masked_collection = self.collection.map(lambda img: img.updateMask(area)\
|
|
2239
|
+
.copyProperties(img).set('system:time_start', img.get('system:time_start')))
|
|
1591
2240
|
|
|
1592
2241
|
# Return the updated object
|
|
1593
|
-
return
|
|
2242
|
+
return GenericCollection(collection=masked_collection)
|
|
1594
2243
|
|
|
1595
2244
|
|
|
1596
2245
|
def binary_mask(self, threshold=None, band_name=None, classify_above_threshold=True, mask_zeros=False):
|
|
@@ -1625,20 +2274,26 @@ class GenericCollection:
|
|
|
1625
2274
|
if classify_above_threshold:
|
|
1626
2275
|
if mask_zeros:
|
|
1627
2276
|
col = self.collection.map(
|
|
1628
|
-
lambda image: image.select(band_name).gte(threshold).rename(band_name)
|
|
2277
|
+
lambda image: image.select(band_name).gte(threshold).rename(band_name)
|
|
2278
|
+
.updateMask(image.select(band_name).gt(0)).copyProperties(image)
|
|
2279
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
1629
2280
|
)
|
|
1630
2281
|
else:
|
|
1631
2282
|
col = self.collection.map(
|
|
1632
|
-
lambda image: image.select(band_name).gte(threshold).rename(band_name)
|
|
2283
|
+
lambda image: image.select(band_name).gte(threshold).rename(band_name)
|
|
2284
|
+
.copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
1633
2285
|
)
|
|
1634
2286
|
else:
|
|
1635
2287
|
if mask_zeros:
|
|
1636
2288
|
col = self.collection.map(
|
|
1637
|
-
lambda image: image.select(band_name).lte(threshold).rename(band_name)
|
|
2289
|
+
lambda image: image.select(band_name).lte(threshold).rename(band_name)
|
|
2290
|
+
.updateMask(image.select(band_name).gt(0)).copyProperties(image)
|
|
2291
|
+
.set('system:time_start', image.get('system:time_start'))
|
|
1638
2292
|
)
|
|
1639
2293
|
else:
|
|
1640
2294
|
col = self.collection.map(
|
|
1641
|
-
lambda image: image.select(band_name).lte(threshold).rename(band_name)
|
|
2295
|
+
lambda image: image.select(band_name).lte(threshold).rename(band_name)
|
|
2296
|
+
.copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
1642
2297
|
)
|
|
1643
2298
|
return GenericCollection(collection=col)
|
|
1644
2299
|
|
|
@@ -1760,7 +2415,8 @@ class GenericCollection:
|
|
|
1760
2415
|
)
|
|
1761
2416
|
|
|
1762
2417
|
# guarantee single band + keep properties
|
|
1763
|
-
out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())
|
|
2418
|
+
out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())\
|
|
2419
|
+
.set('system:time_start', prim.get('system:time_start'))
|
|
1764
2420
|
out = out.set('Date_Filter', prim.get('Date_Filter'))
|
|
1765
2421
|
return ee.Image(out) # <-- return as Image
|
|
1766
2422
|
|
|
@@ -1842,7 +2498,7 @@ class GenericCollection:
|
|
|
1842
2498
|
new_col = self.collection.filter(ee.Filter.eq("Date_Filter", img_date))
|
|
1843
2499
|
return new_col.first()
|
|
1844
2500
|
|
|
1845
|
-
def
|
|
2501
|
+
def collectionStitch(self, img_col2):
|
|
1846
2502
|
"""
|
|
1847
2503
|
Function to mosaic two GenericCollection objects which share image dates.
|
|
1848
2504
|
Mosaics are only formed for dates where both image collections have images.
|
|
@@ -1894,9 +2550,16 @@ class GenericCollection:
|
|
|
1894
2550
|
|
|
1895
2551
|
# Return a GenericCollection instance
|
|
1896
2552
|
return GenericCollection(collection=new_col)
|
|
2553
|
+
|
|
2554
|
+
def CollectionStitch(self, img_col2):
|
|
2555
|
+
warnings.warn(
|
|
2556
|
+
"CollectionStitch is deprecated. Please use collectionStitch instead.",
|
|
2557
|
+
DeprecationWarning,
|
|
2558
|
+
stacklevel=2)
|
|
2559
|
+
return self.collectionStitch(img_col2)
|
|
1897
2560
|
|
|
1898
2561
|
@property
|
|
1899
|
-
def
|
|
2562
|
+
def mosaicByDateDepr(self):
|
|
1900
2563
|
"""
|
|
1901
2564
|
Property attribute function to mosaic collection images that share the same date.
|
|
1902
2565
|
|
|
@@ -1952,6 +2615,64 @@ class GenericCollection:
|
|
|
1952
2615
|
|
|
1953
2616
|
# Convert the list of mosaics to an ImageCollection
|
|
1954
2617
|
return self._MosaicByDate
|
|
2618
|
+
|
|
2619
|
+
@property
|
|
2620
|
+
def mosaicByDate(self):
|
|
2621
|
+
"""
|
|
2622
|
+
Property attribute function to mosaic collection images that share the same date.
|
|
2623
|
+
|
|
2624
|
+
The property CLOUD_COVER for each image is used to calculate an overall mean,
|
|
2625
|
+
which replaces the CLOUD_COVER property for each mosaiced image.
|
|
2626
|
+
Server-side friendly.
|
|
2627
|
+
|
|
2628
|
+
NOTE: if images are removed from the collection from cloud filtering, you may have mosaics composed of only one image.
|
|
2629
|
+
|
|
2630
|
+
Returns:
|
|
2631
|
+
LandsatCollection: LandsatCollection image collection with mosaiced imagery and mean CLOUD_COVER as a property
|
|
2632
|
+
"""
|
|
2633
|
+
if self._MosaicByDate is None:
|
|
2634
|
+
distinct_dates = self.collection.distinct("Date_Filter")
|
|
2635
|
+
|
|
2636
|
+
# Define a join to link images by Date_Filter
|
|
2637
|
+
filter_date = ee.Filter.equals(leftField="Date_Filter", rightField="Date_Filter")
|
|
2638
|
+
join = ee.Join.saveAll(matchesKey="date_matches")
|
|
2639
|
+
|
|
2640
|
+
# Apply the join
|
|
2641
|
+
# Primary: Distinct dates collection
|
|
2642
|
+
# Secondary: The full original collection
|
|
2643
|
+
joined_col = ee.ImageCollection(join.apply(distinct_dates, self.collection, filter_date))
|
|
2644
|
+
|
|
2645
|
+
# Define the mosaicking function
|
|
2646
|
+
def _mosaic_day(img):
|
|
2647
|
+
# Recover the list of images for this day
|
|
2648
|
+
daily_list = ee.List(img.get("date_matches"))
|
|
2649
|
+
daily_col = ee.ImageCollection.fromImages(daily_list)
|
|
2650
|
+
|
|
2651
|
+
# Create the mosaic
|
|
2652
|
+
mosaic = daily_col.mosaic().setDefaultProjection(img.projection())
|
|
2653
|
+
|
|
2654
|
+
# Properties to preserve from the representative image
|
|
2655
|
+
props_of_interest = [
|
|
2656
|
+
"system:time_start",
|
|
2657
|
+
"Date_Filter"
|
|
2658
|
+
]
|
|
2659
|
+
|
|
2660
|
+
# Return mosaic with properties set
|
|
2661
|
+
return mosaic.copyProperties(img, props_of_interest)
|
|
2662
|
+
# 5. Map the function and wrap the result
|
|
2663
|
+
mosaiced_col = joined_col.map(_mosaic_day)
|
|
2664
|
+
self._MosaicByDate = GenericCollection(collection=mosaiced_col)
|
|
2665
|
+
|
|
2666
|
+
# Convert the list of mosaics to an ImageCollection
|
|
2667
|
+
return self._MosaicByDate
|
|
2668
|
+
|
|
2669
|
+
@property
|
|
2670
|
+
def MosaicByDate(self):
|
|
2671
|
+
warnings.warn(
|
|
2672
|
+
"MosaicByDate is deprecated. Please use mosaicByDate instead.",
|
|
2673
|
+
DeprecationWarning,
|
|
2674
|
+
stacklevel=2)
|
|
2675
|
+
return self.mosaicByDate
|
|
1955
2676
|
|
|
1956
2677
|
@staticmethod
|
|
1957
2678
|
def ee_to_df(
|