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.
- RadGEEToolbox/GenericCollection.py +123 -32
- RadGEEToolbox/GetPalette.py +136 -87
- RadGEEToolbox/LandsatCollection.py +465 -37
- RadGEEToolbox/Sentinel1Collection.py +607 -19
- RadGEEToolbox/Sentinel2Collection.py +468 -31
- RadGEEToolbox/VisParams.py +112 -194
- RadGEEToolbox/__init__.py +1 -1
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/METADATA +8 -6
- radgeetoolbox-1.7.2.dist-info/RECORD +13 -0
- radgeetoolbox-1.7.0.dist-info/RECORD +0 -13
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.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:
|
|
@@ -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=
|
|
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'
|
|
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
|
-
|
|
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).
|
|
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).
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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'):
|