RadGEEToolbox 1.7.1__py3-none-any.whl → 1.7.3__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.
@@ -81,6 +81,11 @@ class GenericCollection:
81
81
  self._monthly_sum = None
82
82
  self._monthly_max = None
83
83
  self._monthly_min = None
84
+ self._yearly_median = None
85
+ self._yearly_mean = None
86
+ self._yearly_max = None
87
+ self._yearly_min = None
88
+ self._yearly_sum = None
84
89
  self._mean = None
85
90
  self._max = None
86
91
  self._min = None
@@ -163,7 +168,7 @@ class GenericCollection:
163
168
  if replace:
164
169
  return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
165
170
  else:
166
- return image.addBands(anomaly_image, overwrite=True).copyProperties(image)
171
+ return image.addBands(anomaly_image, overwrite=True).copyProperties(image).set('system:time_start', image.get('system:time_start'))
167
172
 
168
173
  @staticmethod
169
174
  def mask_via_band_fn(image, band_to_mask, band_for_mask, threshold, mask_above=False, add_band_to_original_image=False):
@@ -191,7 +196,7 @@ class GenericCollection:
191
196
  if add_band_to_original_image:
192
197
  return image.addBands(band_to_mask_image.updateMask(mask).rename(band_to_mask), overwrite=True)
193
198
  else:
194
- return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image))
199
+ 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
200
 
196
201
  @staticmethod
197
202
  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 +232,7 @@ class GenericCollection:
227
232
  mask = band_for_mask_image.gt(threshold)
228
233
  else:
229
234
  mask = band_for_mask_image.lt(threshold)
230
- return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask)
235
+ 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
236
 
232
237
  @staticmethod
233
238
  def band_rename_fn(image, current_band_name, new_band_name):
@@ -263,7 +268,7 @@ class GenericCollection:
263
268
  return img.rename(ee.List(new_names))
264
269
 
265
270
  out = ee.Image(ee.Algorithms.If(has_band, _rename(), img))
266
- return out.copyProperties(img)
271
+ return out.copyProperties(img).set('system:time_start', img.get('system:time_start'))
267
272
 
268
273
  @staticmethod
269
274
  def PixelAreaSum(
@@ -365,17 +370,18 @@ class GenericCollection:
365
370
  # Storing the result in the instance variable to avoid redundant calculations
366
371
  self._PixelAreaSumCollection = AreaCollection
367
372
 
373
+ prop_names = band_name if isinstance(band_name, list) else [band_name]
374
+
368
375
  # If an export path is provided, the area data will be exported to a CSV file
369
376
  if area_data_export_path:
370
- GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name], file_path=area_data_export_path+'.csv')
371
-
377
+ GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
372
378
  # Returning the result in the desired format based on output_type argument or raising an error for invalid input
373
379
  if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
374
380
  return self._PixelAreaSumCollection
375
381
  elif output_type == 'GenericCollection':
376
382
  return GenericCollection(collection=self._PixelAreaSumCollection)
377
383
  elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
378
- return GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name])
384
+ return GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
379
385
  else:
380
386
  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'.")
381
387
 
@@ -552,6 +558,8 @@ class GenericCollection:
552
558
  # Ensure property_names is a list for consistent processing
553
559
  if isinstance(property_names, str):
554
560
  property_names = [property_names]
561
+ elif isinstance(property_names, list):
562
+ property_names = property_names
555
563
 
556
564
  # Ensure properties are included without duplication, including 'Date_Filter'
557
565
  all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
@@ -956,6 +964,391 @@ class GenericCollection:
956
964
 
957
965
  return self._monthly_sum
958
966
 
967
+ def yearly_mean_collection(self, start_month=1, end_month=12):
968
+ """
969
+ Creates a yearly mean composite from the collection, with optional monthly filtering.
970
+
971
+ This function computes the mean for each year within the collection's date range.
972
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
973
+ to calculate the mean only using imagery from that specific season for each year.
974
+
975
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
976
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
977
+
978
+ Args:
979
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
980
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
981
+
982
+ Returns:
983
+ Object: A new instance of the same class (e.g., GenericCollection) containing the yearly mean composites.
984
+ """
985
+ if self._yearly_mean is None:
986
+
987
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
988
+ start_date_full = ee.Date(date_range.get('min'))
989
+ end_date_full = ee.Date(date_range.get('max'))
990
+
991
+ start_year = start_date_full.get('year')
992
+ end_year = end_date_full.get('year')
993
+
994
+ if start_month != 1 or end_month != 12:
995
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
996
+ else:
997
+ processing_collection = self.collection
998
+
999
+ # Capture projection from the first image to restore it after reduction
1000
+ target_proj = self.collection.first().projection()
1001
+
1002
+ years = ee.List.sequence(start_year, end_year)
1003
+
1004
+ def create_yearly_composite(year):
1005
+ year = ee.Number(year)
1006
+ # Define the full calendar year range
1007
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1008
+ end_of_year = start_of_year.advance(1, 'year')
1009
+
1010
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1011
+
1012
+ # Calculate stats
1013
+ image_count = yearly_subset.size()
1014
+ yearly_reduction = yearly_subset.mean()
1015
+
1016
+ # Define the timestamp for the composite.
1017
+ # We use the start_month of that year to accurately reflect the data start time.
1018
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1019
+
1020
+ return yearly_reduction.set({
1021
+ 'system:time_start': composite_date.millis(),
1022
+ 'year': year,
1023
+ 'month': start_month,
1024
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1025
+ 'image_count': image_count,
1026
+ 'season_start': start_month,
1027
+ 'season_end': end_month
1028
+ }).reproject(target_proj)
1029
+
1030
+ # Map the function over the years list
1031
+ yearly_composites_list = years.map(create_yearly_composite)
1032
+
1033
+ # Convert to Collection
1034
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1035
+
1036
+ # Filter out any composites that were created from zero images.
1037
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1038
+
1039
+ self._yearly_mean = GenericCollection(collection=final_collection)
1040
+ else:
1041
+ pass
1042
+ return self._yearly_mean
1043
+
1044
+ def yearly_median_collection(self, start_month=1, end_month=12):
1045
+ """
1046
+ Creates a yearly median composite from the collection, with optional monthly filtering.
1047
+
1048
+ This function computes the median for each year within the collection's date range.
1049
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
1050
+ to calculate the median only using imagery from that specific season for each year.
1051
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
1052
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
1053
+
1054
+ Args:
1055
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
1056
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
1057
+
1058
+ Returns:
1059
+ Object: A new instance of the same class (e.g., GenericCollection) containing the yearly median composites.
1060
+ """
1061
+ if self._yearly_median is None:
1062
+
1063
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1064
+ start_date_full = ee.Date(date_range.get('min'))
1065
+ end_date_full = ee.Date(date_range.get('max'))
1066
+
1067
+ start_year = start_date_full.get('year')
1068
+ end_year = end_date_full.get('year')
1069
+
1070
+ if start_month != 1 or end_month != 12:
1071
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
1072
+ else:
1073
+ processing_collection = self.collection
1074
+
1075
+ # Capture projection from the first image to restore it after reduction
1076
+ target_proj = self.collection.first().projection()
1077
+
1078
+ years = ee.List.sequence(start_year, end_year)
1079
+
1080
+ def create_yearly_composite(year):
1081
+ year = ee.Number(year)
1082
+ # Define the full calendar year range
1083
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1084
+ end_of_year = start_of_year.advance(1, 'year')
1085
+
1086
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
1087
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1088
+
1089
+ # Calculate stats
1090
+ image_count = yearly_subset.size()
1091
+ yearly_reduction = yearly_subset.median()
1092
+
1093
+ # Define the timestamp for the composite.
1094
+ # We use the start_month of that year to accurately reflect the data start time.
1095
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1096
+
1097
+ return yearly_reduction.set({
1098
+ 'system:time_start': composite_date.millis(),
1099
+ 'year': year,
1100
+ 'month': start_month,
1101
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1102
+ 'image_count': image_count,
1103
+ 'season_start': start_month,
1104
+ 'season_end': end_month
1105
+ }).reproject(target_proj)
1106
+
1107
+ # Map the function over the years list
1108
+ yearly_composites_list = years.map(create_yearly_composite)
1109
+
1110
+ # Convert to Collection
1111
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1112
+
1113
+ # Filter out any composites that were created from zero images.
1114
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1115
+
1116
+ self._yearly_median = GenericCollection(collection=final_collection)
1117
+ else:
1118
+ pass
1119
+ return self._yearly_median
1120
+
1121
+ def yearly_max_collection(self, start_month=1, end_month=12):
1122
+ """
1123
+ Creates a yearly max composite from the collection, with optional monthly filtering.
1124
+
1125
+ This function computes the max for each year within the collection's date range.
1126
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
1127
+ to calculate the max only using imagery from that specific season for each year.
1128
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
1129
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
1130
+
1131
+ Args:
1132
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
1133
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
1134
+
1135
+ Returns:
1136
+ Object: A new instance of the same class (e.g., GenericCollection) containing the yearly max composites.
1137
+ """
1138
+ if self._yearly_max is None:
1139
+
1140
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1141
+ start_date_full = ee.Date(date_range.get('min'))
1142
+ end_date_full = ee.Date(date_range.get('max'))
1143
+
1144
+ start_year = start_date_full.get('year')
1145
+ end_year = end_date_full.get('year')
1146
+
1147
+ if start_month != 1 or end_month != 12:
1148
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
1149
+ else:
1150
+ processing_collection = self.collection
1151
+
1152
+ # Capture projection from the first image to restore it after reduction
1153
+ target_proj = self.collection.first().projection()
1154
+
1155
+ years = ee.List.sequence(start_year, end_year)
1156
+
1157
+ def create_yearly_composite(year):
1158
+ year = ee.Number(year)
1159
+ # Define the full calendar year range
1160
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1161
+ end_of_year = start_of_year.advance(1, 'year')
1162
+
1163
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
1164
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1165
+
1166
+ # Calculate stats
1167
+ image_count = yearly_subset.size()
1168
+ yearly_reduction = yearly_subset.max()
1169
+
1170
+ # Define the timestamp for the composite.
1171
+ # We use the start_month of that year to accurately reflect the data start time.
1172
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1173
+
1174
+ return yearly_reduction.set({
1175
+ 'system:time_start': composite_date.millis(),
1176
+ 'year': year,
1177
+ 'month': start_month,
1178
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1179
+ 'image_count': image_count,
1180
+ 'season_start': start_month,
1181
+ 'season_end': end_month
1182
+ }).reproject(target_proj)
1183
+
1184
+ # Map the function over the years list
1185
+ yearly_composites_list = years.map(create_yearly_composite)
1186
+
1187
+ # Convert to Collection
1188
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1189
+
1190
+ # Filter out any composites that were created from zero images.
1191
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1192
+
1193
+ self._yearly_max = GenericCollection(collection=final_collection)
1194
+ else:
1195
+ pass
1196
+ return self._yearly_max
1197
+
1198
+ def yearly_min_collection(self, start_month=1, end_month=12):
1199
+ """
1200
+ Creates a yearly min composite from the collection, with optional monthly filtering.
1201
+
1202
+ This function computes the min for each year within the collection's date range.
1203
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
1204
+ to calculate the min only using imagery from that specific season for each year.
1205
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
1206
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
1207
+
1208
+ Args:
1209
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
1210
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
1211
+
1212
+ Returns:
1213
+ Object: A new instance of the same class (e.g., GenericCollection) containing the yearly min composites.
1214
+ """
1215
+ if self._yearly_min is None:
1216
+
1217
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1218
+ start_date_full = ee.Date(date_range.get('min'))
1219
+ end_date_full = ee.Date(date_range.get('max'))
1220
+
1221
+ start_year = start_date_full.get('year')
1222
+ end_year = end_date_full.get('year')
1223
+
1224
+ if start_month != 1 or end_month != 12:
1225
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
1226
+ else:
1227
+ processing_collection = self.collection
1228
+
1229
+ # Capture projection from the first image to restore it after reduction
1230
+ target_proj = self.collection.first().projection()
1231
+
1232
+ years = ee.List.sequence(start_year, end_year)
1233
+
1234
+ def create_yearly_composite(year):
1235
+ year = ee.Number(year)
1236
+ # Define the full calendar year range
1237
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1238
+ end_of_year = start_of_year.advance(1, 'year')
1239
+
1240
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
1241
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1242
+
1243
+ # Calculate stats
1244
+ image_count = yearly_subset.size()
1245
+ yearly_reduction = yearly_subset.min()
1246
+
1247
+ # Define the timestamp for the composite.
1248
+ # We use the start_month of that year to accurately reflect the data start time.
1249
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1250
+
1251
+ return yearly_reduction.set({
1252
+ 'system:time_start': composite_date.millis(),
1253
+ 'year': year,
1254
+ 'month': start_month,
1255
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1256
+ 'image_count': image_count,
1257
+ 'season_start': start_month,
1258
+ 'season_end': end_month
1259
+ }).reproject(target_proj)
1260
+
1261
+ # Map the function over the years list
1262
+ yearly_composites_list = years.map(create_yearly_composite)
1263
+
1264
+ # Convert to Collection
1265
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1266
+
1267
+ # Filter out any composites that were created from zero images.
1268
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1269
+
1270
+ self._yearly_min = GenericCollection(collection=final_collection)
1271
+ else:
1272
+ pass
1273
+ return self._yearly_min
1274
+
1275
+ def yearly_sum_collection(self, start_month=1, end_month=12):
1276
+ """
1277
+ Creates a yearly sum composite from the collection, with optional monthly filtering.
1278
+
1279
+ This function computes the sum for each year within the collection's date range.
1280
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
1281
+ to calculate the sum only using imagery from that specific season for each year.
1282
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
1283
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
1284
+
1285
+ Args:
1286
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
1287
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
1288
+
1289
+ Returns:
1290
+ Object: A new instance of the same class (e.g., GenericCollection) containing the yearly sum composites.
1291
+ """
1292
+ if self._yearly_sum is None:
1293
+
1294
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1295
+ start_date_full = ee.Date(date_range.get('min'))
1296
+ end_date_full = ee.Date(date_range.get('max'))
1297
+
1298
+ start_year = start_date_full.get('year')
1299
+ end_year = end_date_full.get('year')
1300
+
1301
+ if start_month != 1 or end_month != 12:
1302
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
1303
+ else:
1304
+ processing_collection = self.collection
1305
+
1306
+ # Capture projection from the first image to restore it after reduction
1307
+ target_proj = self.collection.first().projection()
1308
+
1309
+ years = ee.List.sequence(start_year, end_year)
1310
+
1311
+ def create_yearly_composite(year):
1312
+ year = ee.Number(year)
1313
+ # Define the full calendar year range
1314
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1315
+ end_of_year = start_of_year.advance(1, 'year')
1316
+
1317
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
1318
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1319
+
1320
+ # Calculate stats
1321
+ image_count = yearly_subset.size()
1322
+ yearly_reduction = yearly_subset.sum()
1323
+
1324
+ # Define the timestamp for the composite.
1325
+ # We use the start_month of that year to accurately reflect the data start time.
1326
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1327
+
1328
+ return yearly_reduction.set({
1329
+ 'system:time_start': composite_date.millis(),
1330
+ 'year': year,
1331
+ 'month': start_month,
1332
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1333
+ 'image_count': image_count,
1334
+ 'season_start': start_month,
1335
+ 'season_end': end_month
1336
+ }).reproject(target_proj)
1337
+
1338
+ # Map the function over the years list
1339
+ yearly_composites_list = years.map(create_yearly_composite)
1340
+
1341
+ # Convert to Collection
1342
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1343
+
1344
+ # Filter out any composites that were created from zero images.
1345
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1346
+
1347
+ self._yearly_sum = GenericCollection(collection=final_collection)
1348
+ else:
1349
+ pass
1350
+ return self._yearly_sum
1351
+
959
1352
  @property
960
1353
  def monthly_max_collection(self):
961
1354
  """Creates a monthly max composite from a GenericCollection image collection.
@@ -1520,7 +1913,7 @@ class GenericCollection:
1520
1913
  new_band_names = band_names.map(lambda b: ee.String(b).cat('_mm'))
1521
1914
 
1522
1915
  converted_image = image.multiply(10800).rename(new_band_names)
1523
- return converted_image.copyProperties(image, image.propertyNames())
1916
+ return converted_image.copyProperties(image, image.propertyNames()).set('system:time_start', image.get('system:time_start'))
1524
1917
 
1525
1918
  # Map the function over the entire collection
1526
1919
  converted_collection = self.collection.map(convert_to_mm)
@@ -1534,6 +1927,234 @@ class GenericCollection:
1534
1927
  _dates_list=self._dates_list # Pass along the cached dates!
1535
1928
  )
1536
1929
 
1930
+ def mann_kendall_trend(self, target_band=None, join_method='system:time_start', geometry=None):
1931
+ """
1932
+ Calculates the Mann-Kendall S-value, Variance, Z-Score, and Confidence Level for each pixel in the image collection, in addition to calculating
1933
+ 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'.
1934
+
1935
+ 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.
1936
+ Note that this function is computationally intensive and may take a long time to run for large image collections or high-resolution images.
1937
+
1938
+ The 's_statistic' band represents the Mann-Kendall S-value, which is a measure of the strength and direction of the trend.
1939
+ The 'variance' band represents the variance of the S-value, which is a measure of the variability of the S-value.
1940
+ The 'z_score' band represents the Z-Score, which is a measure of the significance of the trend.
1941
+ 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).
1942
+ 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.
1943
+
1944
+ Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
1945
+ You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
1946
+ 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.
1947
+
1948
+ Args:
1949
+ image_collection (GenericCollection or ee.ImageCollection): The input image collection for which the Mann-Kendall and Sen's slope trend statistics will be calculated.
1950
+ target_band (str): The band name to be used for the output anomaly image. e.g. 'ndvi'
1951
+ 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'.
1952
+ 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.
1953
+
1954
+ Returns:
1955
+ ee.Image: An image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
1956
+ """
1957
+ ########## PART 1 - S-VALUE CALCULATION ##########
1958
+ ##### https://vsp.pnnl.gov/help/vsample/design_trend_mann_kendall.htm #####
1959
+ image_collection = self
1960
+ if isinstance(image_collection, GenericCollection):
1961
+ image_collection = image_collection.collection
1962
+ elif isinstance(image_collection, ee.ImageCollection):
1963
+ pass
1964
+ else:
1965
+ raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid GenericCollection or ee.ImageCollection object.')
1966
+
1967
+ if target_band is None:
1968
+ raise ValueError('The `target_band` parameter must be specified.')
1969
+ if not isinstance(target_band, str):
1970
+ raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
1971
+
1972
+ if geometry is not None and not isinstance(geometry, ee.Geometry):
1973
+ raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
1974
+ # define the join, which will join all images newer than the current image
1975
+ # use system:time_start if the image does not have a Date_Filter property
1976
+ if join_method == 'system:time_start':
1977
+ # get all images where the leftField value is less than (before) the rightField value
1978
+ time_filter = ee.Filter.lessThan(leftField='system:time_start',
1979
+ rightField='system:time_start')
1980
+ elif join_method == 'Date_Filter':
1981
+ # get all images where the leftField value is less than (before) the rightField value
1982
+ time_filter = ee.Filter.lessThan(leftField='Date_Filter',
1983
+ rightField='Date_Filter')
1984
+ else:
1985
+ raise ValueError(f'The chosen `join_method`: {join_method} does not match the options of "system:time_start" or "Date_Filter".')
1986
+
1987
+ # for any matches during a join, set image as a property key called 'future_image'
1988
+ join = ee.Join.saveAll(matchesKey='future_image')
1989
+
1990
+ # apply the join on the input collection
1991
+ # joining all images newer than the current image with the current image
1992
+ joined_collection = ee.ImageCollection(join.apply(primary=image_collection,
1993
+ secondary=image_collection, condition=time_filter))
1994
+
1995
+ # defining a collection to calculate the partial S value for each match in the join
1996
+ # e.g. t4-t1, t3-t1, t2-1 if there are 4 images
1997
+ def calculate_partial_s(current_image):
1998
+ # select the target band for arithmetic
1999
+ current_val = current_image.select(target_band)
2000
+ # get the joined images from the current image properties and cast the joined images as a list
2001
+ future_image_list = ee.List(current_image.get('future_image'))
2002
+ # convert the joined list to an image collection
2003
+ future_image_collection = ee.ImageCollection(future_image_list)
2004
+
2005
+ # define a function that will calculate the difference between the joined images and the current image,
2006
+ # then calculate the partial S sign based on the value of the difference calculation
2007
+ def get_sign(future_image):
2008
+ # select the target band for arithmetic from the future image
2009
+ future_val = future_image.select(target_band)
2010
+ # calculate the difference, i.e. t2-t1
2011
+ difference = future_val.subtract(current_val)
2012
+ # determine the sign of the difference value (1 if diff > 0, 0 if 0, and -1 if diff < 0)
2013
+ # use .unmask(0) to set any masked pixels as 0 to avoid
2014
+
2015
+ sign = difference.signum().unmask(0)
2016
+
2017
+ return sign
2018
+
2019
+ # map the get_sign() function along the future image col
2020
+ # then sum the values for each pixel to get the partial S value
2021
+ return future_image_collection.map(get_sign).sum()
2022
+
2023
+ # calculate the partial s value for each image in the joined/input image collection
2024
+ partial_s_col = joined_collection.map(calculate_partial_s)
2025
+
2026
+ # convert the image collection to an image of s_statistic values per pixel
2027
+ # where the s_statistic is the sum of partial s values
2028
+ # renaming the band as 's_statistic' for later usage
2029
+ final_s_image = partial_s_col.sum().rename('s_statistic')
2030
+
2031
+
2032
+ ########## PART 2 - VARIANCE and Z-SCORE ##########
2033
+ # to calculate variance we need to know how many pixels were involved in the partial_s calculations per pixel
2034
+ # we do this by using count() and turn the value to a float for later arithmetic
2035
+ n = image_collection.select(target_band).count().toFloat()
2036
+
2037
+ ##### VARIANCE CALCULATION #####
2038
+ # as we are using floating point values with high precision, it is HIGHLY
2039
+ # unlikely that there will be multiple pixel values with the same value.
2040
+ # Thus, we opt to use the simplified variance calculation approach as the
2041
+ # impacts to the output value are negligible and the processing benefits are HUGE
2042
+ # variance = (n * (n - 1) * (2n + 5)) / 18
2043
+ var_s = n.multiply(n.subtract(1))\
2044
+ .multiply(n.multiply(2).add(5))\
2045
+ .divide(18).rename('variance')
2046
+
2047
+ z_score = ee.Image().expression(
2048
+ """
2049
+ (s > 0) ? (s - 1) / sqrt(var) :
2050
+ (s < 0) ? (s + 1) / sqrt(var) :
2051
+ 0
2052
+ """,
2053
+ {'s': final_s_image, 'var': var_s}
2054
+ ).rename('z_score')
2055
+
2056
+ confidence = z_score.abs().divide(ee.Number(2).sqrt()).erf().rename('confidence')
2057
+
2058
+ stat_bands = ee.Image([var_s, z_score, confidence])
2059
+
2060
+ mk_stats_image = final_s_image.addBands(stat_bands)
2061
+
2062
+ ########## PART 3 - Sen's Slope ##########
2063
+ def add_year_band(image):
2064
+ if join_method == 'Date_Filter':
2065
+ # Get the string 'YYYY-MM-DD'
2066
+ date_string = image.get('Date_Filter')
2067
+ # Parse it into an ee.Date object (handles the conversion to time math)
2068
+ date = ee.Date.parse('YYYY-MM-dd', date_string)
2069
+ else:
2070
+ # Standard way: assumes system:time_start exists
2071
+ date = image.date()
2072
+ years = date.difference(ee.Date('1970-01-01'), 'year')
2073
+ return image.addBands(ee.Image(years).float().rename('year'))
2074
+
2075
+ slope_input = image_collection.map(add_year_band).select(['year', target_band])
2076
+
2077
+ sens_slope = slope_input.reduce(ee.Reducer.sensSlope())
2078
+
2079
+ slope_band = sens_slope.select('slope')
2080
+
2081
+ # add a mask to the final image to remove pixels with less than min_observations
2082
+ # mainly an effort to mask pixels outside of the boundary of the input image collection
2083
+ min_observations = 1
2084
+ valid_mask = n.gte(min_observations)
2085
+
2086
+ final_image = mk_stats_image.addBands(slope_band).updateMask(valid_mask)
2087
+
2088
+ if geometry is not None:
2089
+ mask = ee.Image(1).clip(geometry)
2090
+ final_image = final_image.updateMask(mask)
2091
+
2092
+ return final_image
2093
+
2094
+ def sens_slope_trend(self, target_band=None, join_method='system:time_start', geometry=None):
2095
+ """
2096
+ Calculates Sen's Slope (trend magnitude) for the collection.
2097
+ This is a lighter-weight alternative to the full `mann_kendall_trend` function if only
2098
+ the direction and magnitude of the trend are needed.
2099
+
2100
+ Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
2101
+ You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
2102
+ 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.
2103
+
2104
+ Args:
2105
+ target_band (str): The name of the band to analyze. Defaults to 'ndvi'.
2106
+ join_method (str): Property to use for time sorting ('system:time_start' or 'Date_Filter').
2107
+ geometry (ee.Geometry, optional): Geometry to mask the final output.
2108
+
2109
+ Returns:
2110
+ ee.Image: An image containing the 'slope' band.
2111
+ """
2112
+ image_collection = self
2113
+ if isinstance(image_collection, GenericCollection):
2114
+ image_collection = image_collection.collection
2115
+ elif isinstance(image_collection, ee.ImageCollection):
2116
+ pass
2117
+ else:
2118
+ raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid GenericCollection or ee.ImageCollection object.')
2119
+
2120
+ if target_band is None:
2121
+ raise ValueError('The `target_band` parameter must be specified.')
2122
+ if not isinstance(target_band, str):
2123
+ raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
2124
+
2125
+ if geometry is not None and not isinstance(geometry, ee.Geometry):
2126
+ raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
2127
+
2128
+ # Add Year Band (Time X-Axis)
2129
+ def add_year_band(image):
2130
+ # Handle user-defined date strings vs system time
2131
+ if join_method == 'Date_Filter':
2132
+ date_string = image.get('Date_Filter')
2133
+ date = ee.Date.parse('YYYY-MM-dd', date_string)
2134
+ else:
2135
+ date = image.date()
2136
+
2137
+ # Convert to fractional years relative to epoch
2138
+ years = date.difference(ee.Date('1970-01-01'), 'year')
2139
+ return image.addBands(ee.Image(years).float().rename('year'))
2140
+
2141
+ # Prepare Collection: Select ONLY [Year, Target]
2142
+ # sensSlope expects Band 0 = Independent (X), Band 1 = Dependent (Y)
2143
+ slope_input = self.collection.map(add_year_band).select(['year', target_band])
2144
+
2145
+ # Run the Native Reducer
2146
+ sens_result = slope_input.reduce(ee.Reducer.sensSlope())
2147
+
2148
+ # Extract and Mask
2149
+ slope_band = sens_result.select('slope')
2150
+
2151
+ if geometry is not None:
2152
+ mask = ee.Image(1).clip(geometry)
2153
+ slope_band = slope_band.updateMask(mask)
2154
+
2155
+ return slope_band
2156
+
2157
+
1537
2158
  def mask_to_polygon(self, polygon):
1538
2159
  """
1539
2160
  Function to mask GenericCollection image collection by a polygon (ee.Geometry), where pixels outside the polygon are masked out.
@@ -1550,7 +2171,7 @@ class GenericCollection:
1550
2171
  mask = ee.Image.constant(1).clip(polygon)
1551
2172
 
1552
2173
  # Update the mask of each image in the collection
1553
- masked_collection = self.collection.map(lambda img: img.updateMask(mask))
2174
+ masked_collection = self.collection.map(lambda img: img.updateMask(mask).copyProperties(img).set('system:time_start', img.get('system:time_start')))
1554
2175
 
1555
2176
  # Update the internal collection state
1556
2177
  self._geometry_masked_collection = GenericCollection(
@@ -1579,7 +2200,7 @@ class GenericCollection:
1579
2200
  area = full_mask.paint(polygon, 0)
1580
2201
 
1581
2202
  # Update the mask of each image in the collection
1582
- masked_collection = self.collection.map(lambda img: img.updateMask(area))
2203
+ masked_collection = self.collection.map(lambda img: img.updateMask(area).copyProperties(img).set('system:time_start', img.get('system:time_start')))
1583
2204
 
1584
2205
  # Update the internal collection state
1585
2206
  self._geometry_masked_out_collection = GenericCollection(
@@ -1622,20 +2243,26 @@ class GenericCollection:
1622
2243
  if classify_above_threshold:
1623
2244
  if mask_zeros:
1624
2245
  col = self.collection.map(
1625
- lambda image: image.select(band_name).gte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
2246
+ lambda image: image.select(band_name).gte(threshold).rename(band_name)
2247
+ .updateMask(image.select(band_name).gt(0)).copyProperties(image)
2248
+ .set('system:time_start', image.get('system:time_start'))
1626
2249
  )
1627
2250
  else:
1628
2251
  col = self.collection.map(
1629
- lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image).set('system:time_start', image.get('system:time_start'))
2252
+ lambda image: image.select(band_name).gte(threshold).rename(band_name)
2253
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
1630
2254
  )
1631
2255
  else:
1632
2256
  if mask_zeros:
1633
2257
  col = self.collection.map(
1634
- lambda image: image.select(band_name).lte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
2258
+ lambda image: image.select(band_name).lte(threshold).rename(band_name)
2259
+ .updateMask(image.select(band_name).gt(0)).copyProperties(image)
2260
+ .set('system:time_start', image.get('system:time_start'))
1635
2261
  )
1636
2262
  else:
1637
2263
  col = self.collection.map(
1638
- lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image).set('system:time_start', image.get('system:time_start'))
2264
+ lambda image: image.select(band_name).lte(threshold).rename(band_name)
2265
+ .copyProperties(image).set('system:time_start', image.get('system:time_start'))
1639
2266
  )
1640
2267
  return GenericCollection(collection=col)
1641
2268
 
@@ -1757,7 +2384,8 @@ class GenericCollection:
1757
2384
  )
1758
2385
 
1759
2386
  # guarantee single band + keep properties
1760
- out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())
2387
+ out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())\
2388
+ .set('system:time_start', prim.get('system:time_start'))
1761
2389
  out = out.set('Date_Filter', prim.get('Date_Filter'))
1762
2390
  return ee.Image(out) # <-- return as Image
1763
2391