RadGEEToolbox 1.6.10__py3-none-any.whl → 1.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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:
@@ -1254,15 +1262,65 @@ class LandsatCollection:
1254
1262
 
1255
1263
  # If an export path is provided, the area data will be exported to a CSV file
1256
1264
  if area_data_export_path:
1257
- LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=band_name, file_path=area_data_export_path+'.csv')
1265
+ LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name], file_path=area_data_export_path+'.csv')
1258
1266
 
1259
1267
  # Returning the result in the desired format based on output_type argument or raising an error for invalid input
1260
- if output_type == 'ImageCollection':
1268
+ if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
1261
1269
  return self._PixelAreaSumCollection
1262
1270
  elif output_type == 'LandsatCollection':
1263
1271
  return LandsatCollection(collection=self._PixelAreaSumCollection)
1272
+ elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
1273
+ return LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name])
1264
1274
  else:
1265
- raise ValueError("output_type must be 'ImageCollection' or 'LandsatCollection'")
1275
+ 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'.")
1276
+
1277
+ @staticmethod
1278
+ def add_month_property_fn(image):
1279
+ """
1280
+ Adds a numeric 'month' property to the image based on its date.
1281
+
1282
+ Args:
1283
+ image (ee.Image): Input image.
1284
+
1285
+ Returns:
1286
+ ee.Image: Image with the 'month' property added.
1287
+ """
1288
+ return image.set('month', image.date().get('month'))
1289
+
1290
+ @property
1291
+ def add_month_property(self):
1292
+ """
1293
+ Adds a numeric 'month' property to each image in the collection.
1294
+
1295
+ Returns:
1296
+ LandsatCollection: A LandsatCollection image collection with the 'month' property added to each image.
1297
+ """
1298
+ col = self.collection.map(LandsatCollection.add_month_property_fn)
1299
+ return LandsatCollection(collection=col)
1300
+
1301
+ def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
1302
+ """
1303
+ Removes duplicate images that share the same date, keeping only the first one encountered.
1304
+ Useful for handling duplicate acquisitions or overlapping path/rows.
1305
+
1306
+ Args:
1307
+ sort_by (str): Property to sort by before filtering distinct dates. Options: 'system:time_start' or 'CLOUD_COVER'.
1308
+ Defaults to 'system:time_start'.
1309
+ Recommended to use 'CLOUD_COVER' (ascending=True) to keep the clearest image.
1310
+ ascending (bool): Sort order. Defaults to True.
1311
+
1312
+ Returns:
1313
+ LandsatCollection: A new LandsatCollection object with distinct dates.
1314
+ """
1315
+ if sort_by not in ['system:time_start', 'CLOUD_COVER']:
1316
+ 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'.")
1317
+ # Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
1318
+ sorted_col = self.collection.sort(sort_by, ascending)
1319
+
1320
+ # distinct() retains the first image for each unique value of the specified property
1321
+ distinct_col = sorted_col.distinct('Date_Filter')
1322
+
1323
+ return LandsatCollection(collection=distinct_col)
1266
1324
 
1267
1325
  def combine(self, other):
1268
1326
  """
@@ -1640,9 +1698,13 @@ class LandsatCollection:
1640
1698
  collection = self.collection
1641
1699
  # Get the start and end dates of the entire collection.
1642
1700
  date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1643
- start_date = ee.Date(date_range.get('min'))
1701
+ original_start_date = ee.Date(date_range.get('min'))
1644
1702
  end_date = ee.Date(date_range.get('max'))
1645
1703
 
1704
+ start_year = original_start_date.get('year')
1705
+ start_month = original_start_date.get('month')
1706
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1707
+
1646
1708
  # Calculate the total number of months in the date range.
1647
1709
  # The .round() is important for ensuring we get an integer.
1648
1710
  num_months = end_date.difference(start_date, 'month').round()
@@ -1695,6 +1757,338 @@ class LandsatCollection:
1695
1757
  pass
1696
1758
 
1697
1759
  return self._monthly_median
1760
+
1761
+ @property
1762
+ def monthly_mean_collection(self):
1763
+ """Creates a monthly mean composite from a LandsatCollection image collection.
1764
+
1765
+ This function computes the mean for each
1766
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1767
+ temporal extent of the input collection.
1768
+
1769
+ The resulting images have a 'system:time_start' property set to the
1770
+ first day of each month and an 'image_count' property indicating how
1771
+ many images were used in the composite. Months with no images are
1772
+ automatically excluded from the final collection.
1773
+
1774
+ 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.
1775
+
1776
+ Returns:
1777
+ LandsatCollection: A new LandsatCollection object with monthly mean composites.
1778
+ """
1779
+ if self._monthly_mean is None:
1780
+ collection = self.collection
1781
+ target_proj = collection.first().projection()
1782
+ # Get the start and end dates of the entire collection.
1783
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1784
+ original_start_date = ee.Date(date_range.get('min'))
1785
+ end_date = ee.Date(date_range.get('max'))
1786
+
1787
+ start_year = original_start_date.get('year')
1788
+ start_month = original_start_date.get('month')
1789
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1790
+
1791
+ # Calculate the total number of months in the date range.
1792
+ # The .round() is important for ensuring we get an integer.
1793
+ num_months = end_date.difference(start_date, 'month').round()
1794
+
1795
+ # Generate a list of starting dates for each month.
1796
+ # This uses a sequence and advances the start date by 'i' months.
1797
+ def get_month_start(i):
1798
+ return start_date.advance(i, 'month')
1799
+
1800
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1801
+
1802
+ # Define a function to map over the list of month start dates.
1803
+ def create_monthly_composite(date):
1804
+ # Cast the input to an ee.Date object.
1805
+ start_of_month = ee.Date(date)
1806
+ # The end date is exclusive, so we advance by 1 month.
1807
+ end_of_month = start_of_month.advance(1, 'month')
1808
+
1809
+ # Filter the original collection to get images for the current month.
1810
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1811
+
1812
+ # Count the number of images in the monthly subset.
1813
+ image_count = monthly_subset.size()
1814
+
1815
+ # Compute the mean. This is robust to outliers like clouds.
1816
+ monthly_mean = monthly_subset.mean()
1817
+
1818
+ # Set essential properties on the resulting composite image.
1819
+ # The timestamp is crucial for time-series analysis and charting.
1820
+ # The image_count is useful metadata for quality assessment.
1821
+ return monthly_mean.set({
1822
+ 'system:time_start': start_of_month.millis(),
1823
+ 'month': start_of_month.get('month'),
1824
+ 'year': start_of_month.get('year'),
1825
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1826
+ 'image_count': image_count
1827
+ }).reproject(target_proj)
1828
+
1829
+ # Map the composite function over the list of month start dates.
1830
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1831
+
1832
+ # Convert the list of images into an ee.ImageCollection.
1833
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1834
+
1835
+ # Filter out any composites that were created from zero images.
1836
+ # This prevents empty/masked images from being in the final collection.
1837
+ final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1838
+ self._monthly_mean = final_collection
1839
+ else:
1840
+ pass
1841
+
1842
+ return self._monthly_mean
1843
+
1844
+ @property
1845
+ def monthly_sum_collection(self):
1846
+ """Creates a monthly sum composite from a LandsatCollection image collection.
1847
+
1848
+ This function computes the sum for each
1849
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1850
+ temporal extent of the input collection.
1851
+
1852
+ The resulting images have a 'system:time_start' property set to the
1853
+ first day of each month and an 'image_count' property indicating how
1854
+ many images were used in the composite. Months with no images are
1855
+ automatically excluded from the final collection.
1856
+
1857
+ 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.
1858
+
1859
+ Returns:
1860
+ LandsatCollection: A new LandsatCollection object with monthly sum composites.
1861
+ """
1862
+ if self._monthly_sum is None:
1863
+ collection = self.collection
1864
+ target_proj = collection.first().projection()
1865
+ # Get the start and end dates of the entire collection.
1866
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1867
+ original_start_date = ee.Date(date_range.get('min'))
1868
+ end_date = ee.Date(date_range.get('max'))
1869
+
1870
+ start_year = original_start_date.get('year')
1871
+ start_month = original_start_date.get('month')
1872
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1873
+
1874
+ # Calculate the total number of months in the date range.
1875
+ # The .round() is important for ensuring we get an integer.
1876
+ num_months = end_date.difference(start_date, 'month').round()
1877
+
1878
+ # Generate a list of starting dates for each month.
1879
+ # This uses a sequence and advances the start date by 'i' months.
1880
+ def get_month_start(i):
1881
+ return start_date.advance(i, 'month')
1882
+
1883
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1884
+
1885
+ # Define a function to map over the list of month start dates.
1886
+ def create_monthly_composite(date):
1887
+ # Cast the input to an ee.Date object.
1888
+ start_of_month = ee.Date(date)
1889
+ # The end date is exclusive, so we advance by 1 month.
1890
+ end_of_month = start_of_month.advance(1, 'month')
1891
+
1892
+ # Filter the original collection to get images for the current month.
1893
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1894
+
1895
+ # Count the number of images in the monthly subset.
1896
+ image_count = monthly_subset.size()
1897
+
1898
+ # Compute the sum. This is robust to outliers like clouds.
1899
+ monthly_sum = monthly_subset.sum()
1900
+
1901
+ # Set essential properties on the resulting composite image.
1902
+ # The timestamp is crucial for time-series analysis and charting.
1903
+ # The image_count is useful metadata for quality assessment.
1904
+ return monthly_sum.set({
1905
+ 'system:time_start': start_of_month.millis(),
1906
+ 'month': start_of_month.get('month'),
1907
+ 'year': start_of_month.get('year'),
1908
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1909
+ 'image_count': image_count
1910
+ }).reproject(target_proj)
1911
+
1912
+ # Map the composite function over the list of month start dates.
1913
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1914
+
1915
+ # Convert the list of images into an ee.ImageCollection.
1916
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
1917
+
1918
+ # Filter out any composites that were created from zero images.
1919
+ # This prevents empty/masked images from being in the final collection.
1920
+ final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
1921
+ self._monthly_sum = final_collection
1922
+ else:
1923
+ pass
1924
+
1925
+ return self._monthly_sum
1926
+
1927
+ @property
1928
+ def monthly_max_collection(self):
1929
+ """Creates a monthly max composite from a LandsatCollection image collection.
1930
+
1931
+ This function computes the max for each
1932
+ month within the collection's date range, for each band in the collection. It automatically handles the full
1933
+ temporal extent of the input collection.
1934
+
1935
+ The resulting images have a 'system:time_start' property set to the
1936
+ first day of each month and an 'image_count' property indicating how
1937
+ many images were used in the composite. Months with no images are
1938
+ automatically excluded from the final collection.
1939
+
1940
+ 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.
1941
+
1942
+ Returns:
1943
+ LandsatCollection: A new LandsatCollection object with monthly max composites.
1944
+ """
1945
+ if self._monthly_max is None:
1946
+ collection = self.collection
1947
+ target_proj = collection.first().projection()
1948
+ # Get the start and end dates of the entire collection.
1949
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1950
+ original_start_date = ee.Date(date_range.get('min'))
1951
+ end_date = ee.Date(date_range.get('max'))
1952
+
1953
+ start_year = original_start_date.get('year')
1954
+ start_month = original_start_date.get('month')
1955
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
1956
+
1957
+ # Calculate the total number of months in the date range.
1958
+ # The .round() is important for ensuring we get an integer.
1959
+ num_months = end_date.difference(start_date, 'month').round()
1960
+
1961
+ # Generate a list of starting dates for each month.
1962
+ # This uses a sequence and advances the start date by 'i' months.
1963
+ def get_month_start(i):
1964
+ return start_date.advance(i, 'month')
1965
+
1966
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
1967
+
1968
+ # Define a function to map over the list of month start dates.
1969
+ def create_monthly_composite(date):
1970
+ # Cast the input to an ee.Date object.
1971
+ start_of_month = ee.Date(date)
1972
+ # The end date is exclusive, so we advance by 1 month.
1973
+ end_of_month = start_of_month.advance(1, 'month')
1974
+
1975
+ # Filter the original collection to get images for the current month.
1976
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
1977
+
1978
+ # Count the number of images in the monthly subset.
1979
+ image_count = monthly_subset.size()
1980
+
1981
+ # Compute the max. This is robust to outliers like clouds.
1982
+ monthly_max = monthly_subset.max()
1983
+
1984
+ # Set essential properties on the resulting composite image.
1985
+ # The timestamp is crucial for time-series analysis and charting.
1986
+ # The image_count is useful metadata for quality assessment.
1987
+ return monthly_max.set({
1988
+ 'system:time_start': start_of_month.millis(),
1989
+ 'month': start_of_month.get('month'),
1990
+ 'year': start_of_month.get('year'),
1991
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
1992
+ 'image_count': image_count
1993
+ }).reproject(target_proj)
1994
+
1995
+ # Map the composite function over the list of month start dates.
1996
+ monthly_composites_list = month_starts.map(create_monthly_composite)
1997
+
1998
+ # Convert the list of images into an ee.ImageCollection.
1999
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
2000
+
2001
+ # Filter out any composites that were created from zero images.
2002
+ # This prevents empty/masked images from being in the final collection.
2003
+ final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
2004
+ self._monthly_max = final_collection
2005
+ else:
2006
+ pass
2007
+
2008
+ return self._monthly_max
2009
+
2010
+ @property
2011
+ def monthly_min_collection(self):
2012
+ """Creates a monthly min composite from a LandsatCollection image collection.
2013
+
2014
+ This function computes the min for each
2015
+ month within the collection's date range, for each band in the collection. It automatically handles the full
2016
+ temporal extent of the input collection.
2017
+
2018
+ The resulting images have a 'system:time_start' property set to the
2019
+ first day of each month and an 'image_count' property indicating how
2020
+ many images were used in the composite. Months with no images are
2021
+ automatically excluded from the final collection.
2022
+
2023
+ 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.
2024
+
2025
+ Returns:
2026
+ LandsatCollection: A new LandsatCollection object with monthly min composites.
2027
+ """
2028
+ if self._monthly_min is None:
2029
+ collection = self.collection
2030
+ target_proj = collection.first().projection()
2031
+ # Get the start and end dates of the entire collection.
2032
+ date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
2033
+ original_start_date = ee.Date(date_range.get('min'))
2034
+ end_date = ee.Date(date_range.get('max'))
2035
+
2036
+ start_year = original_start_date.get('year')
2037
+ start_month = original_start_date.get('month')
2038
+ start_date = ee.Date.fromYMD(start_year, start_month, 1)
2039
+
2040
+ # Calculate the total number of months in the date range.
2041
+ # The .round() is important for ensuring we get an integer.
2042
+ num_months = end_date.difference(start_date, 'month').round()
2043
+
2044
+ # Generate a list of starting dates for each month.
2045
+ # This uses a sequence and advances the start date by 'i' months.
2046
+ def get_month_start(i):
2047
+ return start_date.advance(i, 'month')
2048
+
2049
+ month_starts = ee.List.sequence(0, num_months).map(get_month_start)
2050
+
2051
+ # Define a function to map over the list of month start dates.
2052
+ def create_monthly_composite(date):
2053
+ # Cast the input to an ee.Date object.
2054
+ start_of_month = ee.Date(date)
2055
+ # The end date is exclusive, so we advance by 1 month.
2056
+ end_of_month = start_of_month.advance(1, 'month')
2057
+
2058
+ # Filter the original collection to get images for the current month.
2059
+ monthly_subset = collection.filterDate(start_of_month, end_of_month)
2060
+
2061
+ # Count the number of images in the monthly subset.
2062
+ image_count = monthly_subset.size()
2063
+
2064
+ # Compute the min. This is robust to outliers like clouds.
2065
+ monthly_min = monthly_subset.min()
2066
+
2067
+ # Set essential properties on the resulting composite image.
2068
+ # The timestamp is crucial for time-series analysis and charting.
2069
+ # The image_count is useful metadata for quality assessment.
2070
+ return monthly_min.set({
2071
+ 'system:time_start': start_of_month.millis(),
2072
+ 'month': start_of_month.get('month'),
2073
+ 'year': start_of_month.get('year'),
2074
+ 'Date_Filter': start_of_month.format('YYYY-MM-dd'),
2075
+ 'image_count': image_count
2076
+ }).reproject(target_proj)
2077
+
2078
+ # Map the composite function over the list of month start dates.
2079
+ monthly_composites_list = month_starts.map(create_monthly_composite)
2080
+
2081
+ # Convert the list of images into an ee.ImageCollection.
2082
+ monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
2083
+
2084
+ # Filter out any composites that were created from zero images.
2085
+ # This prevents empty/masked images from being in the final collection.
2086
+ final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
2087
+ self._monthly_min = final_collection
2088
+ else:
2089
+ pass
2090
+
2091
+ return self._monthly_min
1698
2092
 
1699
2093
  @property
1700
2094
  def ndwi(self):
@@ -2413,6 +2807,7 @@ class LandsatCollection:
2413
2807
  )
2414
2808
  return LandsatCollection(collection=col)
2415
2809
 
2810
+ @property
2416
2811
  def C_to_F(self):
2417
2812
  """
2418
2813
  Function to convert an LST collection from Celcius to Fahrenheit, adding a new band 'LST_F' to each image in the collection.
@@ -2420,9 +2815,10 @@ class LandsatCollection:
2420
2815
  Returns:
2421
2816
  LandsatCollection: A LandsatCollection image collection with LST in Fahrenheit as band titled 'LST_F'.
2422
2817
  """
2423
- if self._LST is None:
2424
- raise ValueError("LST has not been calculated yet. Access the LST property first.")
2425
- col = self._LST.collection.map(LandsatCollection.C_to_F_fn)
2818
+ # if self._LST is None:
2819
+ # raise ValueError("LST has not been calculated yet. Access the LST property first.")
2820
+ # col = self._LST.collection.map(LandsatCollection.C_to_F_fn)
2821
+ col = self.collection.map(LandsatCollection.C_to_F_fn)
2426
2822
  return LandsatCollection(collection=col)
2427
2823
 
2428
2824
  def mask_to_polygon(self, polygon):
@@ -2560,27 +2956,29 @@ class LandsatCollection:
2560
2956
  if threshold is None:
2561
2957
  raise ValueError("Threshold must be specified for binary masking.")
2562
2958
 
2959
+
2563
2960
  if classify_above_threshold:
2564
- if mask_zeros:
2961
+ if mask_zeros is True:
2565
2962
  col = self.collection.map(
2566
- lambda image: image.select(band_name).gte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
2963
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).selfMask().copyProperties(image)
2567
2964
  )
2568
2965
  else:
2569
2966
  col = self.collection.map(
2570
2967
  lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
2571
2968
  )
2572
2969
  else:
2573
- if mask_zeros:
2970
+ if mask_zeros is True:
2574
2971
  col = self.collection.map(
2575
- lambda image: image.select(band_name).lte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
2972
+ lambda image: image.select(band_name).lte(threshold).rename(band_name).selfMask().copyProperties(image)
2576
2973
  )
2577
2974
  else:
2578
2975
  col = self.collection.map(
2579
2976
  lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
2580
2977
  )
2978
+
2581
2979
  return LandsatCollection(collection=col)
2582
2980
 
2583
- def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
2981
+ def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=30):
2584
2982
  """
2585
2983
  Calculates the anomaly of each image in a collection compared to the mean of each image.
2586
2984
 
@@ -2594,6 +2992,7 @@ class LandsatCollection:
2594
2992
  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.
2595
2993
  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.
2596
2994
  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.
2995
+ scale (int, optional): The scale (in meters) to be used for image reduction. Default is 30 meters.
2597
2996
 
2598
2997
  Returns:
2599
2998
  LandsatCollection: A LandsatCollection where each image represents the anomaly (deviation from
@@ -2612,7 +3011,7 @@ class LandsatCollection:
2612
3011
  else:
2613
3012
  band_name = band_names.get(0).getInfo()
2614
3013
 
2615
- col = self.collection.map(lambda image: LandsatCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace))
3014
+ 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))
2616
3015
  return LandsatCollection(collection=col)
2617
3016
 
2618
3017
  def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
@@ -3422,6 +3821,8 @@ class LandsatCollection:
3422
3821
  When coordinates are provided, a radial buffer is applied around each coordinate to extract the statistics, where the size of the buffer is determined by the buffer_size argument (defaults to 1 meter).
3423
3822
  The function returns a pandas DataFrame with the statistics for each coordinate and date, or optionally exports the data to a table in .csv format.
3424
3823
 
3824
+ NOTE: The expected input is a singleband image. If a multiband image is provided, the first band will be used for zonal statistic extraction, unless `band` is specified.
3825
+
3425
3826
  Args:
3426
3827
  geometries (ee.Geometry, ee.Feature, ee.FeatureCollection, list, or tuple): Input geometries for which to extract statistics. Can be a single ee.Geometry, an ee.Feature, an ee.FeatureCollection, a list of (lon, lat) tuples, or a list of ee.Geometry objects. Be careful to NOT provide coordinates as (lat, lon)!
3427
3828
  band (str, optional): The name of the band to use for statistics. If None, the first band is used. Defaults to None.
@@ -3441,24 +3842,30 @@ class LandsatCollection:
3441
3842
  ValueError: If input parameters are invalid.
3442
3843
  TypeError: If geometries input type is unsupported.
3443
3844
  """
3845
+ # Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
3444
3846
  img_collection_obj = self
3847
+
3848
+ # If a specific band is requested, select only that band
3445
3849
  if band:
3446
3850
  img_collection_obj = LandsatCollection(collection=img_collection_obj.collection.select(band))
3447
3851
  else:
3852
+ # If no band is specified, default to using the first band of the first image in the collection
3448
3853
  first_image = img_collection_obj.image_grab(0)
3449
3854
  first_band = first_image.bandNames().get(0)
3450
3855
  img_collection_obj = LandsatCollection(collection=img_collection_obj.collection.select([first_band]))
3451
- # Filter collection by dates if provided
3856
+
3857
+ # If a list of dates is provided, filter the collection to include only images matching those dates
3452
3858
  if dates:
3453
3859
  img_collection_obj = LandsatCollection(
3454
3860
  collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
3455
3861
  )
3456
3862
 
3457
- # Initialize variables
3863
+ # Initialize variables to hold the standardized feature collection and coordinates
3458
3864
  features = None
3459
3865
  validated_coordinates = []
3460
3866
 
3461
- # Function to standardize feature names if no names are provided
3867
+ # Define a helper function to ensure every feature has a standardized 'geo_name' property
3868
+ # This handles features that might have different existing name properties or none at all
3462
3869
  def set_standard_name(feature):
3463
3870
  has_geo_name = feature.get('geo_name')
3464
3871
  has_name = feature.get('name')
@@ -3469,33 +3876,38 @@ class LandsatCollection:
3469
3876
  ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
3470
3877
  return feature.set({'geo_name': new_name})
3471
3878
 
3879
+ # Handle input: FeatureCollection or single Feature
3472
3880
  if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
3473
3881
  features = ee.FeatureCollection(geometries)
3474
3882
  if geometry_names:
3475
3883
  print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
3476
3884
 
3885
+ # Handle input: Single ee.Geometry
3477
3886
  elif isinstance(geometries, ee.Geometry):
3478
3887
  name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
3479
3888
  features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
3480
3889
 
3890
+ # Handle input: List (could be coordinates or ee.Geometry objects)
3481
3891
  elif isinstance(geometries, list):
3482
3892
  if not geometries: # Handle empty list case
3483
3893
  raise ValueError("'geometries' list cannot be empty.")
3484
3894
 
3485
- # Case: List of coordinates
3895
+ # Case: List of tuples (coordinates)
3486
3896
  if all(isinstance(i, tuple) for i in geometries):
3487
3897
  validated_coordinates = geometries
3898
+ # Generate default names if none provided
3488
3899
  if geometry_names is None:
3489
3900
  geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
3490
3901
  elif len(geometry_names) != len(validated_coordinates):
3491
3902
  raise ValueError("geometry_names must have the same length as the coordinates list.")
3903
+ # Create features with buffers around the coordinates
3492
3904
  points = [
3493
3905
  ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
3494
3906
  for coord, name in zip(validated_coordinates, geometry_names)
3495
3907
  ]
3496
3908
  features = ee.FeatureCollection(points)
3497
3909
 
3498
- # Case: List of Geometries
3910
+ # Case: List of ee.Geometry objects
3499
3911
  elif all(isinstance(i, ee.Geometry) for i in geometries):
3500
3912
  if geometry_names is None:
3501
3913
  geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
@@ -3510,6 +3922,7 @@ class LandsatCollection:
3510
3922
  else:
3511
3923
  raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
3512
3924
 
3925
+ # Handle input: Single tuple (coordinate)
3513
3926
  elif isinstance(geometries, tuple) and len(geometries) == 2:
3514
3927
  name = geometry_names[0] if geometry_names else 'Location_1'
3515
3928
  features = ee.FeatureCollection([
@@ -3518,39 +3931,48 @@ class LandsatCollection:
3518
3931
  else:
3519
3932
  raise TypeError("Unsupported type for 'geometries'.")
3520
3933
 
3934
+ # Apply the naming standardization to the created FeatureCollection
3521
3935
  features = features.map(set_standard_name)
3522
3936
 
3937
+ # Dynamically retrieve the Earth Engine reducer based on the string name provided
3523
3938
  try:
3524
3939
  reducer = getattr(ee.Reducer, reducer_type)()
3525
3940
  except AttributeError:
3526
3941
  raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
3527
3942
 
3943
+ # Define the function to map over the image collection
3528
3944
  def calculate_stats_for_image(image):
3529
3945
  image_date = image.get('Date_Filter')
3946
+ # Calculate statistics for all geometries in 'features' for this specific image
3530
3947
  stats_fc = image.reduceRegions(
3531
3948
  collection=features, reducer=reducer, scale=scale, tileScale=tileScale
3532
3949
  )
3533
3950
 
3951
+ # Helper to ensure the result has the reducer property, even if masked
3952
+ # If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
3534
3953
  def guarantee_reducer_property(f):
3535
3954
  has_property = f.propertyNames().contains(reducer_type)
3536
3955
  return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
3956
+
3957
+ # Apply the guarantee check
3537
3958
  fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
3538
3959
 
3960
+ # Attach the image date to every feature in the result so we know which image it came from
3539
3961
  return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
3540
3962
 
3963
+ # Map the calculation over the image collection and flatten the resulting FeatureCollections into one
3541
3964
  results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
3965
+
3966
+ # Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
3542
3967
  df = LandsatCollection.ee_to_df(results_fc, remove_geom=True)
3543
3968
 
3544
- # Checking for issues
3969
+ # Check for empty results or missing columns
3545
3970
  if df.empty:
3546
- # 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.")
3547
- # return df
3548
3971
  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.")
3549
3972
  if reducer_type not in df.columns:
3550
3973
  print(f"Warning: Reducer '{reducer_type}' not found in results.")
3551
- # return df
3552
3974
 
3553
- # Get the number of rows before dropping nulls for a helpful message
3975
+ # Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
3554
3976
  initial_rows = len(df)
3555
3977
  df.dropna(subset=[reducer_type], inplace=True)
3556
3978
  df = df[df[reducer_type] != -9999]
@@ -3558,9 +3980,18 @@ class LandsatCollection:
3558
3980
  if dropped_rows > 0:
3559
3981
  print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
3560
3982
 
3561
- # Reshape DataFrame to have dates as index and geometry names as columns
3983
+ # Pivot the DataFrame so that each row represents a date and each column represents a geometry location
3562
3984
  pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
3985
+ # Rename the column headers (geometry names) to include the reducer type
3986
+ pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
3987
+ # Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
3563
3988
  pivot_df.index.name = 'Date'
3989
+ # 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
3990
+ pivot_df.columns.name = None
3991
+ # Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
3992
+ pivot_df = pivot_df.reset_index(drop=False)
3993
+
3994
+ # If a file path is provided, save the resulting DataFrame to CSV
3564
3995
  if file_path:
3565
3996
  # Check if file_path ends with .csv and remove it if so for consistency
3566
3997
  if file_path.endswith('.csv'):