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.
@@ -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 PixelAreaSum(
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
- def PixelAreaSumCollection(
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.PixelAreaSum(
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).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
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).ExportProperties(property_names=prop_names)
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 ExportProperties(self, property_names, file_path=None):
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
- if self._geometry_masked_collection is None:
1552
- # Convert the polygon to a mask
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
- # Update the internal collection state
1559
- self._geometry_masked_collection = GenericCollection(
1560
- collection=masked_collection
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 self._geometry_masked_collection
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
- if self._geometry_masked_out_collection is None:
1578
- # Convert the polygon to a mask
1579
- full_mask = ee.Image.constant(1)
2231
+ # Convert the polygon to a mask
2232
+ full_mask = ee.Image.constant(1)
1580
2233
 
1581
- # Use paint to set pixels inside polygon as 0
1582
- area = full_mask.paint(polygon, 0)
2234
+ # Use paint to set pixels inside polygon as 0
2235
+ area = full_mask.paint(polygon, 0)
1583
2236
 
1584
- # Update the mask of each image in the collection
1585
- masked_collection = self.collection.map(lambda img: img.updateMask(area))
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 self._geometry_masked_out_collection
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).updateMask(image.select(band_name).gt(0)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
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).copyProperties(image).set('system:time_start', image.get('system:time_start'))
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).updateMask(image.select(band_name).gt(0)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
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).copyProperties(image).set('system:time_start', image.get('system:time_start'))
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 CollectionStitch(self, img_col2):
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 MosaicByDate(self):
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(