RadGEEToolbox 1.7.0__py3-none-any.whl → 1.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -155,6 +155,10 @@ class LandsatCollection:
155
155
  self._geometry_masked_out_collection = None
156
156
  self._median = None
157
157
  self._monthly_median = None
158
+ self._monthly_mean = None
159
+ self._monthly_max = None
160
+ self._monthly_min = None
161
+ self._monthly_sum = None
158
162
  self._mean = None
159
163
  self._max = None
160
164
  self._min = None
@@ -705,7 +709,7 @@ class LandsatCollection:
705
709
  return nbr
706
710
 
707
711
  @staticmethod
708
- def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True):
712
+ def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=30):
709
713
  """
710
714
  Calculates the anomaly of a singleband image compared to the mean of the singleband image.
711
715
 
@@ -721,6 +725,7 @@ class LandsatCollection:
721
725
  band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
722
726
  anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
723
727
  replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band in the output image. If True, the output image will contain only the anomaly band. If False, the output image will contain both the original band and the anomaly band. Default is True.
728
+ scale (int, optional): The scale in meters to use for the reduction operation. Default is 30 meters.
724
729
 
725
730
  Returns:
726
731
  ee.Image: An ee.Image where each band represents the anomaly (deviation from
@@ -737,12 +742,16 @@ class LandsatCollection:
737
742
  mean_image = image_to_process.reduceRegion(
738
743
  reducer=ee.Reducer.mean(),
739
744
  geometry=geometry,
740
- scale=30,
745
+ scale=scale,
741
746
  maxPixels=1e13
742
747
  ).toImage()
743
748
 
744
749
  # Compute the anomaly by subtracting the mean image from the input image.
745
- anomaly_image = image_to_process.subtract(mean_image)
750
+ if scale == 30:
751
+ anomaly_image = image_to_process.subtract(mean_image)
752
+ else:
753
+ anomaly_image = image_to_process.reproject(crs=image_to_process.projection(), scale=scale).subtract(mean_image)
754
+
746
755
  if anomaly_band_name is None:
747
756
  if band_name:
748
757
  anomaly_image = anomaly_image.rename(band_name)
@@ -753,9 +762,9 @@ class LandsatCollection:
753
762
  anomaly_image = anomaly_image.rename(anomaly_band_name)
754
763
  # return anomaly_image
755
764
  if replace:
756
- return anomaly_image.copyProperties(image)
765
+ return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
757
766
  else:
758
- return image.addBands(anomaly_image, overwrite=True)
767
+ return image.addBands(anomaly_image, overwrite=True).copyProperties(image)
759
768
 
760
769
  @staticmethod
761
770
  def MaskWaterLandsat(image):
@@ -1092,7 +1101,7 @@ class LandsatCollection:
1092
1101
  "downwelling": image.select("downwelling"),
1093
1102
  },
1094
1103
  ).rename("LST")
1095
- return image.addBands(LST).copyProperties(image) # Outputs temperature in C
1104
+ return image.addBands(LST).copyProperties(image).set('system:time_start', image.get('system:time_start')) # Outputs temperature in C
1096
1105
 
1097
1106
  @staticmethod
1098
1107
  def C_to_F_fn(LST_image):
@@ -1114,7 +1123,7 @@ class LandsatCollection:
1114
1123
 
1115
1124
  # Preserve original properties from the input image.
1116
1125
  # return fahrenheit_image.rename('LST_F').copyProperties(LST_image)
1117
- return LST_image.addBands(fahrenheit_image.rename('LST_F'), overwrite=True).copyProperties(LST_image)
1126
+ return LST_image.addBands(fahrenheit_image.rename('LST_F'), overwrite=True).copyProperties(LST_image).set('system:time_start', LST_image.get('system:time_start'))
1118
1127
 
1119
1128
  @staticmethod
1120
1129
  def band_rename_fn(image, current_band_name, new_band_name):
@@ -1214,8 +1223,7 @@ class LandsatCollection:
1214
1223
  return final_image
1215
1224
 
1216
1225
  def PixelAreaSumCollection(
1217
- self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
1218
- ):
1226
+ self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None):
1219
1227
  """
1220
1228
  Calculates the geodesic summation of area for pixels of interest (above a specific threshold)
1221
1229
  within a geometry and stores the value as an image property (matching name of chosen band) for an entire
@@ -1225,11 +1233,11 @@ class LandsatCollection:
1225
1233
 
1226
1234
  Args:
1227
1235
  band_name (string or list of strings): name of band(s) (string) for calculating area. If providing multiple band names, pass as a list of strings.
1228
- geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
1236
+ geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation.
1229
1237
  threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1). If providing multiple band names, the same threshold will be applied to all bands. Best practice in this case is to mask the bands prior to passing to this function and leave threshold at default of -1.
1230
- scale (int): integer scale of image resolution (meters) (defaults to 30)
1231
- maxPixels (int): integer denoting maximum number of pixels for calculations
1232
- output_type (str): 'ImageCollection' to return an ee.ImageCollection, 'LandsatCollection' to return a LandsatCollection object (defaults to 'ImageCollection')
1238
+ scale (int): integer scale of image resolution (meters) (defaults to 30).
1239
+ maxPixels (int): integer denoting maximum number of pixels for calculations.
1240
+ output_type (str): 'ImageCollection' or 'ee.ImageCollection' to return an ee.ImageCollection, 'LandsatCollection' to return a LandsatCollection object, or 'DataFrame', 'Pandas', 'pd', 'dataframe', 'df' to return a pandas DataFrame (defaults to 'ImageCollection').
1233
1241
  area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
1234
1242
 
1235
1243
  Returns:
@@ -1252,17 +1260,68 @@ class LandsatCollection:
1252
1260
  # Storing the result in the instance variable to avoid redundant calculations
1253
1261
  self._PixelAreaSumCollection = AreaCollection
1254
1262
 
1263
+ prop_names = band_name if isinstance(band_name, list) else [band_name]
1264
+
1255
1265
  # If an export path is provided, the area data will be exported to a CSV file
1256
1266
  if area_data_export_path:
1257
- LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=band_name, file_path=area_data_export_path+'.csv')
1258
-
1267
+ LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
1259
1268
  # Returning the result in the desired format based on output_type argument or raising an error for invalid input
1260
- if output_type == 'ImageCollection':
1269
+ if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
1261
1270
  return self._PixelAreaSumCollection
1262
1271
  elif output_type == 'LandsatCollection':
1263
1272
  return LandsatCollection(collection=self._PixelAreaSumCollection)
1273
+ elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
1274
+ return LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
1264
1275
  else:
1265
- raise ValueError("output_type must be 'ImageCollection' or 'LandsatCollection'")
1276
+ raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'LandsatCollection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
1277
+
1278
+ @staticmethod
1279
+ def add_month_property_fn(image):
1280
+ """
1281
+ Adds a numeric 'month' property to the image based on its date.
1282
+
1283
+ Args:
1284
+ image (ee.Image): Input image.
1285
+
1286
+ Returns:
1287
+ ee.Image: Image with the 'month' property added.
1288
+ """
1289
+ return image.set('month', image.date().get('month'))
1290
+
1291
+ @property
1292
+ def add_month_property(self):
1293
+ """
1294
+ Adds a numeric 'month' property to each image in the collection.
1295
+
1296
+ Returns:
1297
+ LandsatCollection: A LandsatCollection image collection with the 'month' property added to each image.
1298
+ """
1299
+ col = self.collection.map(LandsatCollection.add_month_property_fn)
1300
+ return LandsatCollection(collection=col)
1301
+
1302
+ def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
1303
+ """
1304
+ Removes duplicate images that share the same date, keeping only the first one encountered.
1305
+ Useful for handling duplicate acquisitions or overlapping path/rows.
1306
+
1307
+ Args:
1308
+ sort_by (str): Property to sort by before filtering distinct dates. Options: 'system:time_start' or 'CLOUD_COVER'.
1309
+ Defaults to 'system:time_start'.
1310
+ Recommended to use 'CLOUD_COVER' (ascending=True) to keep the clearest image.
1311
+ ascending (bool): Sort order. Defaults to True.
1312
+
1313
+ Returns:
1314
+ LandsatCollection: A new LandsatCollection object with distinct dates.
1315
+ """
1316
+ if sort_by not in ['system:time_start', 'CLOUD_COVER']:
1317
+ raise ValueError(f"The provided `sort_by` argument is invalid: {sort_by}. The `sort_by` argument must be either 'system:time_start' or 'CLOUD_COVER'.")
1318
+ # Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
1319
+ sorted_col = self.collection.sort(sort_by, ascending)
1320
+
1321
+ # distinct() retains the first image for each unique value of the specified property
1322
+ distinct_col = sorted_col.distinct('Date_Filter')
1323
+
1324
+ return LandsatCollection(collection=distinct_col)
1266
1325
 
1267
1326
  def combine(self, other):
1268
1327
  """
@@ -1460,6 +1519,8 @@ class LandsatCollection:
1460
1519
  # Ensure property_names is a list for consistent processing
1461
1520
  if isinstance(property_names, str):
1462
1521
  property_names = [property_names]
1522
+ elif isinstance(property_names, list):
1523
+ property_names = property_names
1463
1524
 
1464
1525
  # Ensure properties are included without duplication, including 'Date_Filter'
1465
1526
  all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
@@ -1699,6 +1760,338 @@ class LandsatCollection:
1699
1760
  pass
1700
1761
 
1701
1762
  return self._monthly_median
1763
+
1764
+ @property
1765
+ def monthly_mean_collection(self):
1766
+ """Creates a monthly mean composite from a LandsatCollection image collection.
1767
+
1768
+ This function computes the mean for each
1769
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1770
+ temporal extent of the input collection.
1771
+
1772
+ The resulting images have a 'system:time_start' property set to the
1773
+ first day of each month and an 'image_count' property indicating how
1774
+ many images were used in the composite. Months with no images are
1775
+ automatically excluded from the final collection.
1776
+
1777
+ NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
1778
+
1779
+ Returns:
1780
+ LandsatCollection: A new LandsatCollection object with monthly mean composites.
1781
+ """
1782
+ if self._monthly_mean is None:
1783
+ collection = self.collection
1784
+ target_proj = collection.first().projection()
1785
+ # Get the start and end dates of the entire collection.
1786
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1787
+ original_start_date = ee.Date(date_range.get('min'))
1788
+ end_date = ee.Date(date_range.get('max'))
1789
+
1790
+ start_year = original_start_date.get('year')
1791
+ start_month = original_start_date.get('month')
1792
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1793
+
1794
+ # Calculate the total number of months in the date range.
1795
+ # The .round() is important for ensuring we get an integer.
1796
+ num_months = end_date.difference(start_date, 'month').round()
1797
+
1798
+ # Generate a list of starting dates for each month.
1799
+ # This uses a sequence and advances the start date by 'i' months.
1800
+ def get_month_start(i):
1801
+ return start_date.advance(i, 'month')
1802
+
1803
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1804
+
1805
+ # Define a function to map over the list of month start dates.
1806
+ def create_monthly_composite(date):
1807
+ # Cast the input to an ee.Date object.
1808
+ start_of_month = ee.Date(date)
1809
+ # The end date is exclusive, so we advance by 1 month.
1810
+ end_of_month = start_of_month.advance(1, 'month')
1811
+
1812
+ # Filter the original collection to get images for the current month.
1813
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1814
+
1815
+ # Count the number of images in the monthly subset.
1816
+ image_count = monthly_subset.size()
1817
+
1818
+ # Compute the mean. This is robust to outliers like clouds.
1819
+ monthly_mean = monthly_subset.mean()
1820
+
1821
+ # Set essential properties on the resulting composite image.
1822
+ # The timestamp is crucial for time-series analysis and charting.
1823
+ # The image_count is useful metadata for quality assessment.
1824
+ return monthly_mean.set({
1825
+ 'system:time_start': start_of_month.millis(),
1826
+ 'month': start_of_month.get('month'),
1827
+ 'year': start_of_month.get('year'),
1828
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1829
+ 'image_count': image_count
1830
+ }).reproject(target_proj)
1831
+
1832
+ # Map the composite function over the list of month start dates.
1833
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1834
+
1835
+ # Convert the list of images into an ee.ImageCollection.
1836
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1837
+
1838
+ # Filter out any composites that were created from zero images.
1839
+ # This prevents empty/masked images from being in the final collection.
1840
+ final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1841
+ self._monthly_mean = final_collection
1842
+ else:
1843
+ pass
1844
+
1845
+ return self._monthly_mean
1846
+
1847
+ @property
1848
+ def monthly_sum_collection(self):
1849
+ """Creates a monthly sum composite from a LandsatCollection image collection.
1850
+
1851
+ This function computes the sum for each
1852
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1853
+ temporal extent of the input collection.
1854
+
1855
+ The resulting images have a 'system:time_start' property set to the
1856
+ first day of each month and an 'image_count' property indicating how
1857
+ many images were used in the composite. Months with no images are
1858
+ automatically excluded from the final collection.
1859
+
1860
+ NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
1861
+
1862
+ Returns:
1863
+ LandsatCollection: A new LandsatCollection object with monthly sum composites.
1864
+ """
1865
+ if self._monthly_sum is None:
1866
+ collection = self.collection
1867
+ target_proj = collection.first().projection()
1868
+ # Get the start and end dates of the entire collection.
1869
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1870
+ original_start_date = ee.Date(date_range.get('min'))
1871
+ end_date = ee.Date(date_range.get('max'))
1872
+
1873
+ start_year = original_start_date.get('year')
1874
+ start_month = original_start_date.get('month')
1875
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1876
+
1877
+ # Calculate the total number of months in the date range.
1878
+ # The .round() is important for ensuring we get an integer.
1879
+ num_months = end_date.difference(start_date, 'month').round()
1880
+
1881
+ # Generate a list of starting dates for each month.
1882
+ # This uses a sequence and advances the start date by 'i' months.
1883
+ def get_month_start(i):
1884
+ return start_date.advance(i, 'month')
1885
+
1886
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1887
+
1888
+ # Define a function to map over the list of month start dates.
1889
+ def create_monthly_composite(date):
1890
+ # Cast the input to an ee.Date object.
1891
+ start_of_month = ee.Date(date)
1892
+ # The end date is exclusive, so we advance by 1 month.
1893
+ end_of_month = start_of_month.advance(1, 'month')
1894
+
1895
+ # Filter the original collection to get images for the current month.
1896
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1897
+
1898
+ # Count the number of images in the monthly subset.
1899
+ image_count = monthly_subset.size()
1900
+
1901
+ # Compute the sum. This is robust to outliers like clouds.
1902
+ monthly_sum = monthly_subset.sum()
1903
+
1904
+ # Set essential properties on the resulting composite image.
1905
+ # The timestamp is crucial for time-series analysis and charting.
1906
+ # The image_count is useful metadata for quality assessment.
1907
+ return monthly_sum.set({
1908
+ 'system:time_start': start_of_month.millis(),
1909
+ 'month': start_of_month.get('month'),
1910
+ 'year': start_of_month.get('year'),
1911
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1912
+ 'image_count': image_count
1913
+ }).reproject(target_proj)
1914
+
1915
+ # Map the composite function over the list of month start dates.
1916
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1917
+
1918
+ # Convert the list of images into an ee.ImageCollection.
1919
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1920
+
1921
+ # Filter out any composites that were created from zero images.
1922
+ # This prevents empty/masked images from being in the final collection.
1923
+ final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1924
+ self._monthly_sum = final_collection
1925
+ else:
1926
+ pass
1927
+
1928
+ return self._monthly_sum
1929
+
1930
+ @property
1931
+ def monthly_max_collection(self):
1932
+ """Creates a monthly max composite from a LandsatCollection image collection.
1933
+
1934
+ This function computes the max for each
1935
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1936
+ temporal extent of the input collection.
1937
+
1938
+ The resulting images have a 'system:time_start' property set to the
1939
+ first day of each month and an 'image_count' property indicating how
1940
+ many images were used in the composite. Months with no images are
1941
+ automatically excluded from the final collection.
1942
+
1943
+ NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
1944
+
1945
+ Returns:
1946
+ LandsatCollection: A new LandsatCollection object with monthly max composites.
1947
+ """
1948
+ if self._monthly_max is None:
1949
+ collection = self.collection
1950
+ target_proj = collection.first().projection()
1951
+ # Get the start and end dates of the entire collection.
1952
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1953
+ original_start_date = ee.Date(date_range.get('min'))
1954
+ end_date = ee.Date(date_range.get('max'))
1955
+
1956
+ start_year = original_start_date.get('year')
1957
+ start_month = original_start_date.get('month')
1958
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1959
+
1960
+ # Calculate the total number of months in the date range.
1961
+ # The .round() is important for ensuring we get an integer.
1962
+ num_months = end_date.difference(start_date, 'month').round()
1963
+
1964
+ # Generate a list of starting dates for each month.
1965
+ # This uses a sequence and advances the start date by 'i' months.
1966
+ def get_month_start(i):
1967
+ return start_date.advance(i, 'month')
1968
+
1969
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1970
+
1971
+ # Define a function to map over the list of month start dates.
1972
+ def create_monthly_composite(date):
1973
+ # Cast the input to an ee.Date object.
1974
+ start_of_month = ee.Date(date)
1975
+ # The end date is exclusive, so we advance by 1 month.
1976
+ end_of_month = start_of_month.advance(1, 'month')
1977
+
1978
+ # Filter the original collection to get images for the current month.
1979
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1980
+
1981
+ # Count the number of images in the monthly subset.
1982
+ image_count = monthly_subset.size()
1983
+
1984
+ # Compute the max. This is robust to outliers like clouds.
1985
+ monthly_max = monthly_subset.max()
1986
+
1987
+ # Set essential properties on the resulting composite image.
1988
+ # The timestamp is crucial for time-series analysis and charting.
1989
+ # The image_count is useful metadata for quality assessment.
1990
+ return monthly_max.set({
1991
+ 'system:time_start': start_of_month.millis(),
1992
+ 'month': start_of_month.get('month'),
1993
+ 'year': start_of_month.get('year'),
1994
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1995
+ 'image_count': image_count
1996
+ }).reproject(target_proj)
1997
+
1998
+ # Map the composite function over the list of month start dates.
1999
+ monthly_composites_list = month_starts.map(create_monthly_composite)
2000
+
2001
+ # Convert the list of images into an ee.ImageCollection.
2002
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
2003
+
2004
+ # Filter out any composites that were created from zero images.
2005
+ # This prevents empty/masked images from being in the final collection.
2006
+ final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
2007
+ self._monthly_max = final_collection
2008
+ else:
2009
+ pass
2010
+
2011
+ return self._monthly_max
2012
+
2013
+ @property
2014
+ def monthly_min_collection(self):
2015
+ """Creates a monthly min composite from a LandsatCollection image collection.
2016
+
2017
+ This function computes the min for each
2018
+ month within the collection's date range, for each band in the collection. It automatically handles the full
2019
+ temporal extent of the input collection.
2020
+
2021
+ The resulting images have a 'system:time_start' property set to the
2022
+ first day of each month and an 'image_count' property indicating how
2023
+ many images were used in the composite. Months with no images are
2024
+ automatically excluded from the final collection.
2025
+
2026
+ NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
2027
+
2028
+ Returns:
2029
+ LandsatCollection: A new LandsatCollection object with monthly min composites.
2030
+ """
2031
+ if self._monthly_min is None:
2032
+ collection = self.collection
2033
+ target_proj = collection.first().projection()
2034
+ # Get the start and end dates of the entire collection.
2035
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
2036
+ original_start_date = ee.Date(date_range.get('min'))
2037
+ end_date = ee.Date(date_range.get('max'))
2038
+
2039
+ start_year = original_start_date.get('year')
2040
+ start_month = original_start_date.get('month')
2041
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
2042
+
2043
+ # Calculate the total number of months in the date range.
2044
+ # The .round() is important for ensuring we get an integer.
2045
+ num_months = end_date.difference(start_date, 'month').round()
2046
+
2047
+ # Generate a list of starting dates for each month.
2048
+ # This uses a sequence and advances the start date by 'i' months.
2049
+ def get_month_start(i):
2050
+ return start_date.advance(i, 'month')
2051
+
2052
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
2053
+
2054
+ # Define a function to map over the list of month start dates.
2055
+ def create_monthly_composite(date):
2056
+ # Cast the input to an ee.Date object.
2057
+ start_of_month = ee.Date(date)
2058
+ # The end date is exclusive, so we advance by 1 month.
2059
+ end_of_month = start_of_month.advance(1, 'month')
2060
+
2061
+ # Filter the original collection to get images for the current month.
2062
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
2063
+
2064
+ # Count the number of images in the monthly subset.
2065
+ image_count = monthly_subset.size()
2066
+
2067
+ # Compute the min. This is robust to outliers like clouds.
2068
+ monthly_min = monthly_subset.min()
2069
+
2070
+ # Set essential properties on the resulting composite image.
2071
+ # The timestamp is crucial for time-series analysis and charting.
2072
+ # The image_count is useful metadata for quality assessment.
2073
+ return monthly_min.set({
2074
+ 'system:time_start': start_of_month.millis(),
2075
+ 'month': start_of_month.get('month'),
2076
+ 'year': start_of_month.get('year'),
2077
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
2078
+ 'image_count': image_count
2079
+ }).reproject(target_proj)
2080
+
2081
+ # Map the composite function over the list of month start dates.
2082
+ monthly_composites_list = month_starts.map(create_monthly_composite)
2083
+
2084
+ # Convert the list of images into an ee.ImageCollection.
2085
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
2086
+
2087
+ # Filter out any composites that were created from zero images.
2088
+ # This prevents empty/masked images from being in the final collection.
2089
+ final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
2090
+ self._monthly_min = final_collection
2091
+ else:
2092
+ pass
2093
+
2094
+ return self._monthly_min
1702
2095
 
1703
2096
  @property
1704
2097
  def ndwi(self):
@@ -2417,6 +2810,7 @@ class LandsatCollection:
2417
2810
  )
2418
2811
  return LandsatCollection(collection=col)
2419
2812
 
2813
+ @property
2420
2814
  def C_to_F(self):
2421
2815
  """
2422
2816
  Function to convert an LST collection from Celcius to Fahrenheit, adding a new band 'LST_F' to each image in the collection.
@@ -2424,9 +2818,10 @@ class LandsatCollection:
2424
2818
  Returns:
2425
2819
  LandsatCollection: A LandsatCollection image collection with LST in Fahrenheit as band titled 'LST_F'.
2426
2820
  """
2427
- if self._LST is None:
2428
- raise ValueError("LST has not been calculated yet. Access the LST property first.")
2429
- col = self._LST.collection.map(LandsatCollection.C_to_F_fn)
2821
+ # if self._LST is None:
2822
+ # raise ValueError("LST has not been calculated yet. Access the LST property first.")
2823
+ # col = self._LST.collection.map(LandsatCollection.C_to_F_fn)
2824
+ col = self.collection.map(LandsatCollection.C_to_F_fn)
2430
2825
  return LandsatCollection(collection=col)
2431
2826
 
2432
2827
  def mask_to_polygon(self, polygon):
@@ -2564,27 +2959,29 @@ class LandsatCollection:
2564
2959
  if threshold is None:
2565
2960
  raise ValueError("Threshold must be specified for binary masking.")
2566
2961
 
2962
+
2567
2963
  if classify_above_threshold:
2568
- if mask_zeros:
2964
+ if mask_zeros is True:
2569
2965
  col = self.collection.map(
2570
- lambda image: image.select(band_name).gte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
2966
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).selfMask().copyProperties(image)
2571
2967
  )
2572
2968
  else:
2573
2969
  col = self.collection.map(
2574
2970
  lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
2575
2971
  )
2576
2972
  else:
2577
- if mask_zeros:
2973
+ if mask_zeros is True:
2578
2974
  col = self.collection.map(
2579
- lambda image: image.select(band_name).lte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
2975
+ lambda image: image.select(band_name).lte(threshold).rename(band_name).selfMask().copyProperties(image)
2580
2976
  )
2581
2977
  else:
2582
2978
  col = self.collection.map(
2583
2979
  lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
2584
2980
  )
2981
+
2585
2982
  return LandsatCollection(collection=col)
2586
2983
 
2587
- def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
2984
+ def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=30):
2588
2985
  """
2589
2986
  Calculates the anomaly of each image in a collection compared to the mean of each image.
2590
2987
 
@@ -2598,6 +2995,7 @@ class LandsatCollection:
2598
2995
  band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
2599
2996
  anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
2600
2997
  replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band. If True, the output image will only contain the anomaly band. If False, the output image will retain all original bands and add the anomaly band. Default is True.
2998
+ scale (int, optional): The scale (in meters) to be used for image reduction. Default is 30 meters.
2601
2999
 
2602
3000
  Returns:
2603
3001
  LandsatCollection: A LandsatCollection where each image represents the anomaly (deviation from
@@ -2616,7 +3014,7 @@ class LandsatCollection:
2616
3014
  else:
2617
3015
  band_name = band_names.get(0).getInfo()
2618
3016
 
2619
- col = self.collection.map(lambda image: LandsatCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace))
3017
+ col = self.collection.map(lambda image: LandsatCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
2620
3018
  return LandsatCollection(collection=col)
2621
3019
 
2622
3020
  def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
@@ -3447,24 +3845,30 @@ class LandsatCollection:
3447
3845
  ValueError: If input parameters are invalid.
3448
3846
  TypeError: If geometries input type is unsupported.
3449
3847
  """
3848
+ # Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
3450
3849
  img_collection_obj = self
3850
+
3851
+ # If a specific band is requested, select only that band
3451
3852
  if band:
3452
3853
  img_collection_obj = LandsatCollection(collection=img_collection_obj.collection.select(band))
3453
3854
  else:
3855
+ # If no band is specified, default to using the first band of the first image in the collection
3454
3856
  first_image = img_collection_obj.image_grab(0)
3455
3857
  first_band = first_image.bandNames().get(0)
3456
3858
  img_collection_obj = LandsatCollection(collection=img_collection_obj.collection.select([first_band]))
3457
- # Filter collection by dates if provided
3859
+
3860
+ # If a list of dates is provided, filter the collection to include only images matching those dates
3458
3861
  if dates:
3459
3862
  img_collection_obj = LandsatCollection(
3460
3863
  collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
3461
3864
  )
3462
3865
 
3463
- # Initialize variables
3866
+ # Initialize variables to hold the standardized feature collection and coordinates
3464
3867
  features = None
3465
3868
  validated_coordinates = []
3466
3869
 
3467
- # Function to standardize feature names if no names are provided
3870
+ # Define a helper function to ensure every feature has a standardized 'geo_name' property
3871
+ # This handles features that might have different existing name properties or none at all
3468
3872
  def set_standard_name(feature):
3469
3873
  has_geo_name = feature.get('geo_name')
3470
3874
  has_name = feature.get('name')
@@ -3475,33 +3879,38 @@ class LandsatCollection:
3475
3879
  ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
3476
3880
  return feature.set({'geo_name': new_name})
3477
3881
 
3882
+ # Handle input: FeatureCollection or single Feature
3478
3883
  if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
3479
3884
  features = ee.FeatureCollection(geometries)
3480
3885
  if geometry_names:
3481
3886
  print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
3482
3887
 
3888
+ # Handle input: Single ee.Geometry
3483
3889
  elif isinstance(geometries, ee.Geometry):
3484
3890
  name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
3485
3891
  features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
3486
3892
 
3893
+ # Handle input: List (could be coordinates or ee.Geometry objects)
3487
3894
  elif isinstance(geometries, list):
3488
3895
  if not geometries: # Handle empty list case
3489
3896
  raise ValueError("'geometries' list cannot be empty.")
3490
3897
 
3491
- # Case: List of coordinates
3898
+ # Case: List of tuples (coordinates)
3492
3899
  if all(isinstance(i, tuple) for i in geometries):
3493
3900
  validated_coordinates = geometries
3901
+ # Generate default names if none provided
3494
3902
  if geometry_names is None:
3495
3903
  geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
3496
3904
  elif len(geometry_names) != len(validated_coordinates):
3497
3905
  raise ValueError("geometry_names must have the same length as the coordinates list.")
3906
+ # Create features with buffers around the coordinates
3498
3907
  points = [
3499
3908
  ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
3500
3909
  for coord, name in zip(validated_coordinates, geometry_names)
3501
3910
  ]
3502
3911
  features = ee.FeatureCollection(points)
3503
3912
 
3504
- # Case: List of Geometries
3913
+ # Case: List of ee.Geometry objects
3505
3914
  elif all(isinstance(i, ee.Geometry) for i in geometries):
3506
3915
  if geometry_names is None:
3507
3916
  geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
@@ -3516,6 +3925,7 @@ class LandsatCollection:
3516
3925
  else:
3517
3926
  raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
3518
3927
 
3928
+ # Handle input: Single tuple (coordinate)
3519
3929
  elif isinstance(geometries, tuple) and len(geometries) == 2:
3520
3930
  name = geometry_names[0] if geometry_names else 'Location_1'
3521
3931
  features = ee.FeatureCollection([
@@ -3524,39 +3934,48 @@ class LandsatCollection:
3524
3934
  else:
3525
3935
  raise TypeError("Unsupported type for 'geometries'.")
3526
3936
 
3937
+ # Apply the naming standardization to the created FeatureCollection
3527
3938
  features = features.map(set_standard_name)
3528
3939
 
3940
+ # Dynamically retrieve the Earth Engine reducer based on the string name provided
3529
3941
  try:
3530
3942
  reducer = getattr(ee.Reducer, reducer_type)()
3531
3943
  except AttributeError:
3532
3944
  raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
3533
3945
 
3946
+ # Define the function to map over the image collection
3534
3947
  def calculate_stats_for_image(image):
3535
3948
  image_date = image.get('Date_Filter')
3949
+ # Calculate statistics for all geometries in 'features' for this specific image
3536
3950
  stats_fc = image.reduceRegions(
3537
3951
  collection=features, reducer=reducer, scale=scale, tileScale=tileScale
3538
3952
  )
3539
3953
 
3954
+ # Helper to ensure the result has the reducer property, even if masked
3955
+ # If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
3540
3956
  def guarantee_reducer_property(f):
3541
3957
  has_property = f.propertyNames().contains(reducer_type)
3542
3958
  return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
3959
+
3960
+ # Apply the guarantee check
3543
3961
  fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
3544
3962
 
3963
+ # Attach the image date to every feature in the result so we know which image it came from
3545
3964
  return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
3546
3965
 
3966
+ # Map the calculation over the image collection and flatten the resulting FeatureCollections into one
3547
3967
  results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
3968
+
3969
+ # Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
3548
3970
  df = LandsatCollection.ee_to_df(results_fc, remove_geom=True)
3549
3971
 
3550
- # Checking for issues
3972
+ # Check for empty results or missing columns
3551
3973
  if df.empty:
3552
- # print("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
3553
- # return df
3554
3974
  raise ValueError("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
3555
3975
  if reducer_type not in df.columns:
3556
3976
  print(f"Warning: Reducer '{reducer_type}' not found in results.")
3557
- # return df
3558
3977
 
3559
- # Get the number of rows before dropping nulls for a helpful message
3978
+ # Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
3560
3979
  initial_rows = len(df)
3561
3980
  df.dropna(subset=[reducer_type], inplace=True)
3562
3981
  df = df[df[reducer_type] != -9999]
@@ -3564,9 +3983,18 @@ class LandsatCollection:
3564
3983
  if dropped_rows > 0:
3565
3984
  print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
3566
3985
 
3567
- # Reshape DataFrame to have dates as index and geometry names as columns
3986
+ # Pivot the DataFrame so that each row represents a date and each column represents a geometry location
3568
3987
  pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
3988
+ # Rename the column headers (geometry names) to include the reducer type
3989
+ pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
3990
+ # Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
3569
3991
  pivot_df.index.name = 'Date'
3992
+ # Remove the name of the columns axis (which defaults to 'geo_name') so it doesn't appear as a confusing label in the final output
3993
+ pivot_df.columns.name = None
3994
+ # Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
3995
+ pivot_df = pivot_df.reset_index(drop=False)
3996
+
3997
+ # If a file path is provided, save the resulting DataFrame to CSV
3570
3998
  if file_path:
3571
3999
  # Check if file_path ends with .csv and remove it if so for consistency
3572
4000
  if file_path.endswith('.csv'):