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.
- RadGEEToolbox/GenericCollection.py +2711 -0
- RadGEEToolbox/GetPalette.py +136 -87
- RadGEEToolbox/LandsatCollection.py +468 -37
- RadGEEToolbox/Sentinel1Collection.py +603 -18
- RadGEEToolbox/Sentinel2Collection.py +464 -30
- RadGEEToolbox/VisParams.py +112 -194
- RadGEEToolbox/__init__.py +3 -1
- {radgeetoolbox-1.6.10.dist-info → radgeetoolbox-1.7.1.dist-info}/METADATA +11 -7
- radgeetoolbox-1.7.1.dist-info/RECORD +13 -0
- radgeetoolbox-1.6.10.dist-info/RECORD +0 -12
- {radgeetoolbox-1.6.10.dist-info → radgeetoolbox-1.7.1.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.6.10.dist-info → radgeetoolbox-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.6.10.dist-info → radgeetoolbox-1.7.1.dist-info}/top_level.txt +0 -0
|
@@ -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=
|
|
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
|
-
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
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).
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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'):
|