RadGEEToolbox 1.6.9__py3-none-any.whl → 1.6.10__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/LandsatCollection.py +593 -9
- RadGEEToolbox/Sentinel1Collection.py +126 -3
- RadGEEToolbox/Sentinel2Collection.py +575 -10
- RadGEEToolbox/__init__.py +1 -1
- {radgeetoolbox-1.6.9.dist-info → radgeetoolbox-1.6.10.dist-info}/METADATA +18 -9
- radgeetoolbox-1.6.10.dist-info/RECORD +12 -0
- radgeetoolbox-1.6.9.dist-info/RECORD +0 -12
- {radgeetoolbox-1.6.9.dist-info → radgeetoolbox-1.6.10.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.6.9.dist-info → radgeetoolbox-1.6.10.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.6.9.dist-info → radgeetoolbox-1.6.10.dist-info}/top_level.txt +0 -0
|
@@ -161,9 +161,11 @@ class Sentinel2Collection:
|
|
|
161
161
|
self._geometry_masked_collection = None
|
|
162
162
|
self._geometry_masked_out_collection = None
|
|
163
163
|
self._masked_clouds_collection = None
|
|
164
|
+
self._masked_shadows_collection = None
|
|
164
165
|
self._masked_to_water_collection = None
|
|
165
166
|
self._masked_water_collection = None
|
|
166
167
|
self._median = None
|
|
168
|
+
self._monthly_median = None
|
|
167
169
|
self._mean = None
|
|
168
170
|
self._max = None
|
|
169
171
|
self._min = None
|
|
@@ -478,11 +480,64 @@ class Sentinel2Collection:
|
|
|
478
480
|
nbr_calc = image.expression(nbr_expression)
|
|
479
481
|
nbr = nbr_calc.updateMask(nbr_calc.gte(threshold)).rename("nbr").copyProperties(image).set("threshold", threshold)
|
|
480
482
|
return nbr
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True):
|
|
486
|
+
"""
|
|
487
|
+
Calculates the anomaly of a singleband image compared to the mean of the singleband image.
|
|
488
|
+
|
|
489
|
+
This function computes the anomaly for each band in the input image by
|
|
490
|
+
subtracting the mean value of that band from a provided image.
|
|
491
|
+
The anomaly is a measure of how much the pixel values deviate from the
|
|
492
|
+
average conditions represented by the mean of the image.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
image (ee.Image): An ee.Image for which the anomaly is to be calculated.
|
|
496
|
+
It is assumed that this image is a singleband image.
|
|
497
|
+
geometry (ee.Geometry): The geometry for image reduction to define the mean value to be used for anomaly calculation.
|
|
498
|
+
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.
|
|
499
|
+
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.
|
|
500
|
+
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.
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
ee.Image: An ee.Image where each band represents the anomaly (deviation from
|
|
504
|
+
the mean) for that band. The output image retains the same band name.
|
|
505
|
+
"""
|
|
506
|
+
if band_name:
|
|
507
|
+
band_name = band_name
|
|
508
|
+
else:
|
|
509
|
+
band_name = ee.String(image.bandNames().get(0))
|
|
510
|
+
|
|
511
|
+
image_to_process = image.select([band_name])
|
|
512
|
+
|
|
513
|
+
# Calculate the mean image of the provided collection.
|
|
514
|
+
mean_image = image_to_process.reduceRegion(
|
|
515
|
+
reducer=ee.Reducer.mean(),
|
|
516
|
+
geometry=geometry,
|
|
517
|
+
scale=10,
|
|
518
|
+
maxPixels=1e13
|
|
519
|
+
).toImage()
|
|
520
|
+
|
|
521
|
+
# Compute the anomaly by subtracting the mean image from the input image.
|
|
522
|
+
anomaly_image = image_to_process.subtract(mean_image)
|
|
523
|
+
if anomaly_band_name is None:
|
|
524
|
+
if band_name:
|
|
525
|
+
anomaly_image = anomaly_image.rename(band_name)
|
|
526
|
+
else:
|
|
527
|
+
# Preserve original properties from the input image.
|
|
528
|
+
anomaly_image = anomaly_image.rename(ee.String(image.bandNames().get(0)))
|
|
529
|
+
else:
|
|
530
|
+
anomaly_image = anomaly_image.rename(anomaly_band_name)
|
|
531
|
+
# return anomaly_image
|
|
532
|
+
if replace:
|
|
533
|
+
return anomaly_image.copyProperties(image)
|
|
534
|
+
else:
|
|
535
|
+
return image.addBands(anomaly_image, overwrite=True)
|
|
481
536
|
|
|
482
537
|
@staticmethod
|
|
483
538
|
def MaskCloudsS2(image):
|
|
484
539
|
"""
|
|
485
|
-
Function to
|
|
540
|
+
Function to mask clouds using SCL band data.
|
|
486
541
|
|
|
487
542
|
Args:
|
|
488
543
|
image (ee.Image): input image
|
|
@@ -493,6 +548,21 @@ class Sentinel2Collection:
|
|
|
493
548
|
SCL = image.select("SCL")
|
|
494
549
|
CloudMask = SCL.neq(9)
|
|
495
550
|
return image.updateMask(CloudMask).copyProperties(image)
|
|
551
|
+
|
|
552
|
+
@staticmethod
|
|
553
|
+
def MaskShadowsS2(image):
|
|
554
|
+
"""
|
|
555
|
+
Function to mask cloud shadows using SCL band data.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
image (ee.Image): input image
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
ee.Image: output ee.Image with cloud shadows masked
|
|
562
|
+
"""
|
|
563
|
+
SCL = image.select("SCL")
|
|
564
|
+
ShadowMask = SCL.neq(3)
|
|
565
|
+
return image.updateMask(ShadowMask).copyProperties(image)
|
|
496
566
|
|
|
497
567
|
@staticmethod
|
|
498
568
|
def MaskWaterS2(image):
|
|
@@ -582,6 +652,70 @@ class Sentinel2Collection:
|
|
|
582
652
|
.copyProperties(image)
|
|
583
653
|
)
|
|
584
654
|
return mask
|
|
655
|
+
|
|
656
|
+
@staticmethod
|
|
657
|
+
def mask_via_band_fn(image, band_to_mask, band_for_mask, threshold, mask_above=False, add_band_to_original_image=False):
|
|
658
|
+
"""
|
|
659
|
+
Masks pixels of interest from a specified band of a target image, based on a specified reference band and threshold.
|
|
660
|
+
Designed for single image input which contains both the target and reference band.
|
|
661
|
+
Example use case is masking vegetation from image when targeting land pixels. Can specify whether to mask pixels above or below the threshold.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
image (ee.Image): input ee.Image
|
|
665
|
+
band_to_mask (str): name of the band which will be masked (target image)
|
|
666
|
+
band_for_mask (str): name of the band to use for the mask (band you want to remove/mask from target image)
|
|
667
|
+
threshold (float): value where pixels less or more than threshold (depending on `mask_above` argument) will be masked
|
|
668
|
+
mask_above (bool): if True, masks pixels above the threshold; if False, masks pixels below the threshold
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
ee.Image: masked ee.Image
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
band_to_mask_image = image.select(band_to_mask)
|
|
675
|
+
band_for_mask_image = image.select(band_for_mask)
|
|
676
|
+
|
|
677
|
+
mask = band_for_mask_image.lte(threshold) if mask_above else band_for_mask_image.gte(threshold)
|
|
678
|
+
|
|
679
|
+
if add_band_to_original_image:
|
|
680
|
+
return image.addBands(band_to_mask_image.updateMask(mask).rename(band_to_mask), overwrite=True)
|
|
681
|
+
else:
|
|
682
|
+
return ee.Image(band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image))
|
|
683
|
+
|
|
684
|
+
@staticmethod
|
|
685
|
+
def mask_via_singleband_image_fn(image_to_mask, image_for_mask, threshold, band_name_to_mask=None, band_name_for_mask=None, mask_above=True):
|
|
686
|
+
"""
|
|
687
|
+
Masks pixels of interest from a specified band of a target image, based on a specified reference band and threshold.
|
|
688
|
+
Designed for the case where the target and reference bands are in separate images.
|
|
689
|
+
Example use case is masking vegetation from image when targeting land pixels. Can specify whether to mask pixels above or below the threshold.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
image_to_mask (ee.Image): image which will be masked (target image). If multiband, only the first band will be masked.
|
|
693
|
+
image_for_mask (ee.Image): image to use for the mask (image you want to remove/mask from target image). If multiband, only the first band will be used for the masked.
|
|
694
|
+
threshold (float): value where pixels less or more than threshold (depending on `mask_above` argument) will be masked
|
|
695
|
+
band_name_to_mask (str, optional): name of the band in image_to_mask to be masked. If None, the first band will be used.
|
|
696
|
+
band_name_for_mask (str, optional): name of the band in image_for_mask to be used for masking. If None, the first band will be used.
|
|
697
|
+
mask_above (bool): if True, masks pixels above the threshold; if False, masks pixels below the threshold.
|
|
698
|
+
|
|
699
|
+
Returns:
|
|
700
|
+
ee.Image: masked ee.Image
|
|
701
|
+
"""
|
|
702
|
+
if band_name_to_mask is None:
|
|
703
|
+
band_to_mask = ee.String(image_to_mask.bandNames().get(0))
|
|
704
|
+
else:
|
|
705
|
+
band_to_mask = ee.String(band_name_to_mask)
|
|
706
|
+
|
|
707
|
+
if band_name_for_mask is None:
|
|
708
|
+
band_for_mask = ee.String(image_for_mask.bandNames().get(0))
|
|
709
|
+
else:
|
|
710
|
+
band_for_mask = ee.String(band_name_for_mask)
|
|
711
|
+
|
|
712
|
+
band_to_mask_image = image_to_mask.select(band_to_mask)
|
|
713
|
+
band_for_mask_image = image_for_mask.select(band_for_mask)
|
|
714
|
+
if mask_above:
|
|
715
|
+
mask = band_for_mask_image.gt(threshold)
|
|
716
|
+
else:
|
|
717
|
+
mask = band_for_mask_image.lt(threshold)
|
|
718
|
+
return band_to_mask_image.updateMask(mask).rename(band_to_mask).copyProperties(image_to_mask)
|
|
585
719
|
|
|
586
720
|
@staticmethod
|
|
587
721
|
def MaskToWaterS2ByNDWI(image, threshold):
|
|
@@ -713,12 +847,12 @@ class Sentinel2Collection:
|
|
|
713
847
|
else:
|
|
714
848
|
raise ValueError("output_type must be 'ImageCollection' or 'Sentinel2Collection'")
|
|
715
849
|
|
|
716
|
-
def
|
|
850
|
+
def combine(self, other):
|
|
717
851
|
"""
|
|
718
|
-
|
|
852
|
+
Combines the current Sentinel2Collection with another Sentinel2Collection, using the `combine` method.
|
|
719
853
|
|
|
720
854
|
Args:
|
|
721
|
-
other (Sentinel2Collection): Another Sentinel2Collection to
|
|
855
|
+
other (Sentinel2Collection): Another Sentinel2Collection to combine with current collection.
|
|
722
856
|
|
|
723
857
|
Returns:
|
|
724
858
|
Sentinel2Collection: A new Sentinel2Collection containing images from both collections.
|
|
@@ -726,11 +860,77 @@ class Sentinel2Collection:
|
|
|
726
860
|
# Checking if 'other' is an instance of Sentinel2Collection
|
|
727
861
|
if not isinstance(other, Sentinel2Collection):
|
|
728
862
|
raise ValueError("The 'other' parameter must be an instance of Sentinel2Collection.")
|
|
729
|
-
|
|
863
|
+
|
|
730
864
|
# Merging the collections using the .combine() method
|
|
731
865
|
merged_collection = self.collection.combine(other.collection)
|
|
732
866
|
return Sentinel2Collection(collection=merged_collection)
|
|
733
867
|
|
|
868
|
+
def merge(self, collections=None, multiband_collection=None, date_key='Date_Filter'):
|
|
869
|
+
"""
|
|
870
|
+
Merge many singleband Sentinel2Collection products into the parent collection,
|
|
871
|
+
or merge a single multiband collection with parent collection,
|
|
872
|
+
pairing images by exact Date_Filter and returning one multiband image per date.
|
|
873
|
+
|
|
874
|
+
NOTE: if you want to merge two multiband collections, use the `combine` method instead.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
collections (list): List of singleband collections to merge with parent collection, effectively adds one band per collection to each image in parent
|
|
878
|
+
multiband_collection (Sentinel2Collection, optional): A multiband collection to merge with parent. Specifying a collection here will override `collections`.
|
|
879
|
+
date_key (str): image property key for exact pairing (default 'Date_Filter')
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
Sentinel2Collection: parent with extra single bands attached (one image per date)
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
if collections is None and multiband_collection is not None:
|
|
886
|
+
# Exact-date inner-join merge of two collections (adds ALL bands from 'other').
|
|
887
|
+
join = ee.Join.inner()
|
|
888
|
+
flt = ee.Filter.equals(leftField=date_key, rightField=date_key)
|
|
889
|
+
paired = join.apply(self.collection, multiband_collection.collection, flt)
|
|
890
|
+
|
|
891
|
+
def _pair_two(f):
|
|
892
|
+
f = ee.Feature(f)
|
|
893
|
+
a = ee.Image(f.get('primary'))
|
|
894
|
+
b = ee.Image(f.get('secondary'))
|
|
895
|
+
# Overwrite on name collision
|
|
896
|
+
merged = a.addBands(b, None, True)
|
|
897
|
+
# Keep parent props + date key
|
|
898
|
+
merged = merged.copyProperties(a, a.propertyNames())
|
|
899
|
+
merged = merged.set(date_key, a.get(date_key))
|
|
900
|
+
return ee.Image(merged)
|
|
901
|
+
|
|
902
|
+
return Sentinel2Collection(collection=ee.ImageCollection(paired.map(_pair_two)))
|
|
903
|
+
|
|
904
|
+
# Preferred path: merge many singleband products into the parent
|
|
905
|
+
if not isinstance(collections, list) or len(collections) == 0:
|
|
906
|
+
raise ValueError("Provide a non-empty list of Sentinel2Collection objects in `collections`.")
|
|
907
|
+
|
|
908
|
+
result = self.collection
|
|
909
|
+
for extra in collections:
|
|
910
|
+
if not isinstance(extra, Sentinel2Collection):
|
|
911
|
+
raise ValueError("All items in `collections` must be Sentinel2Collection objects.")
|
|
912
|
+
|
|
913
|
+
join = ee.Join.inner()
|
|
914
|
+
flt = ee.Filter.equals(leftField=date_key, rightField=date_key)
|
|
915
|
+
paired = join.apply(result, extra.collection, flt)
|
|
916
|
+
|
|
917
|
+
def _attach_one(f):
|
|
918
|
+
f = ee.Feature(f)
|
|
919
|
+
parent = ee.Image(f.get('primary'))
|
|
920
|
+
sb = ee.Image(f.get('secondary'))
|
|
921
|
+
# Assume singleband product; grab its first band name server-side
|
|
922
|
+
bname = ee.String(sb.bandNames().get(0))
|
|
923
|
+
# Add the single band; overwrite if the name already exists in parent
|
|
924
|
+
merged = parent.addBands(sb.select([bname]).rename([bname]), None, True)
|
|
925
|
+
# Preserve parent props + date key
|
|
926
|
+
merged = merged.copyProperties(parent, parent.propertyNames())
|
|
927
|
+
merged = merged.set(date_key, parent.get(date_key))
|
|
928
|
+
return ee.Image(merged)
|
|
929
|
+
|
|
930
|
+
result = ee.ImageCollection(paired.map(_attach_one))
|
|
931
|
+
|
|
932
|
+
return Sentinel2Collection(collection=result)
|
|
933
|
+
|
|
734
934
|
@property
|
|
735
935
|
def dates_list(self):
|
|
736
936
|
"""
|
|
@@ -934,6 +1134,82 @@ class Sentinel2Collection:
|
|
|
934
1134
|
self._median = col
|
|
935
1135
|
return self._median
|
|
936
1136
|
|
|
1137
|
+
@property
|
|
1138
|
+
def monthly_median_collection(self):
|
|
1139
|
+
"""Creates a monthly median composite from a Sentinel2Collection image collection.
|
|
1140
|
+
|
|
1141
|
+
This function computes the median for each
|
|
1142
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1143
|
+
temporal extent of the input collection.
|
|
1144
|
+
|
|
1145
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1146
|
+
first day of each month and an 'image_count' property indicating how
|
|
1147
|
+
many images were used in the composite. Months with no images are
|
|
1148
|
+
automatically excluded from the final collection.
|
|
1149
|
+
|
|
1150
|
+
Returns:
|
|
1151
|
+
Sentinel2Collection: A new Sentinel2Collection object with monthly median composites.
|
|
1152
|
+
"""
|
|
1153
|
+
if self._monthly_median is None:
|
|
1154
|
+
collection = self.collection
|
|
1155
|
+
# Get the start and end dates of the entire collection.
|
|
1156
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1157
|
+
start_date = ee.Date(date_range.get('min'))
|
|
1158
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1159
|
+
|
|
1160
|
+
# Calculate the total number of months in the date range.
|
|
1161
|
+
# The .round() is important for ensuring we get an integer.
|
|
1162
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1163
|
+
|
|
1164
|
+
# Generate a list of starting dates for each month.
|
|
1165
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1166
|
+
def get_month_start(i):
|
|
1167
|
+
return start_date.advance(i, 'month')
|
|
1168
|
+
|
|
1169
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1170
|
+
|
|
1171
|
+
# Define a function to map over the list of month start dates.
|
|
1172
|
+
def create_monthly_composite(date):
|
|
1173
|
+
# Cast the input to an ee.Date object.
|
|
1174
|
+
start_of_month = ee.Date(date)
|
|
1175
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1176
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1177
|
+
|
|
1178
|
+
# Filter the original collection to get images for the current month.
|
|
1179
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1180
|
+
|
|
1181
|
+
# Count the number of images in the monthly subset.
|
|
1182
|
+
image_count = monthly_subset.size()
|
|
1183
|
+
|
|
1184
|
+
# Compute the median. This is robust to outliers like clouds.
|
|
1185
|
+
monthly_median = monthly_subset.median()
|
|
1186
|
+
|
|
1187
|
+
# Set essential properties on the resulting composite image.
|
|
1188
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1189
|
+
# The image_count is useful metadata for quality assessment.
|
|
1190
|
+
return monthly_median.set({
|
|
1191
|
+
'system:time_start': start_of_month.millis(),
|
|
1192
|
+
'month': start_of_month.get('month'),
|
|
1193
|
+
'year': start_of_month.get('year'),
|
|
1194
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1195
|
+
'image_count': image_count
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
# Map the composite function over the list of month start dates.
|
|
1199
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1200
|
+
|
|
1201
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1202
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1203
|
+
|
|
1204
|
+
# Filter out any composites that were created from zero images.
|
|
1205
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1206
|
+
final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1207
|
+
self._monthly_median = final_collection
|
|
1208
|
+
else:
|
|
1209
|
+
pass
|
|
1210
|
+
|
|
1211
|
+
return self._monthly_median
|
|
1212
|
+
|
|
937
1213
|
@property
|
|
938
1214
|
def mean(self):
|
|
939
1215
|
"""
|
|
@@ -973,6 +1249,84 @@ class Sentinel2Collection:
|
|
|
973
1249
|
col = self.collection.min()
|
|
974
1250
|
self._min = col
|
|
975
1251
|
return self._min
|
|
1252
|
+
|
|
1253
|
+
@property
|
|
1254
|
+
def monthly_median_collection(self):
|
|
1255
|
+
"""Creates a monthly median composite from a Sentinel2Collection image collection.
|
|
1256
|
+
|
|
1257
|
+
This function computes the median for each
|
|
1258
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1259
|
+
temporal extent of the input collection.
|
|
1260
|
+
|
|
1261
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1262
|
+
first day of each month and an 'image_count' property indicating how
|
|
1263
|
+
many images were used in the composite. Months with no images are
|
|
1264
|
+
automatically excluded from the final collection.
|
|
1265
|
+
|
|
1266
|
+
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.
|
|
1267
|
+
|
|
1268
|
+
Returns:
|
|
1269
|
+
Sentinel2Collection: A new Sentinel2Collection object with monthly median composites.
|
|
1270
|
+
"""
|
|
1271
|
+
if self._monthly_median is None:
|
|
1272
|
+
collection = self.collection
|
|
1273
|
+
# Get the start and end dates of the entire collection.
|
|
1274
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1275
|
+
start_date = ee.Date(date_range.get('min'))
|
|
1276
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1277
|
+
|
|
1278
|
+
# Calculate the total number of months in the date range.
|
|
1279
|
+
# The .round() is important for ensuring we get an integer.
|
|
1280
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1281
|
+
|
|
1282
|
+
# Generate a list of starting dates for each month.
|
|
1283
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1284
|
+
def get_month_start(i):
|
|
1285
|
+
return start_date.advance(i, 'month')
|
|
1286
|
+
|
|
1287
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1288
|
+
|
|
1289
|
+
# Define a function to map over the list of month start dates.
|
|
1290
|
+
def create_monthly_composite(date):
|
|
1291
|
+
# Cast the input to an ee.Date object.
|
|
1292
|
+
start_of_month = ee.Date(date)
|
|
1293
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1294
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1295
|
+
|
|
1296
|
+
# Filter the original collection to get images for the current month.
|
|
1297
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1298
|
+
|
|
1299
|
+
# Count the number of images in the monthly subset.
|
|
1300
|
+
image_count = monthly_subset.size()
|
|
1301
|
+
|
|
1302
|
+
# Compute the median. This is robust to outliers like clouds.
|
|
1303
|
+
monthly_median = monthly_subset.median()
|
|
1304
|
+
|
|
1305
|
+
# Set essential properties on the resulting composite image.
|
|
1306
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1307
|
+
# The image_count is useful metadata for quality assessment.
|
|
1308
|
+
return monthly_median.set({
|
|
1309
|
+
'system:time_start': start_of_month.millis(),
|
|
1310
|
+
'month': start_of_month.get('month'),
|
|
1311
|
+
'year': start_of_month.get('year'),
|
|
1312
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1313
|
+
'image_count': image_count
|
|
1314
|
+
})
|
|
1315
|
+
|
|
1316
|
+
# Map the composite function over the list of month start dates.
|
|
1317
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1318
|
+
|
|
1319
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1320
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1321
|
+
|
|
1322
|
+
# Filter out any composites that were created from zero images.
|
|
1323
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1324
|
+
final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1325
|
+
self._monthly_median = final_collection
|
|
1326
|
+
else:
|
|
1327
|
+
pass
|
|
1328
|
+
|
|
1329
|
+
return self._monthly_median
|
|
976
1330
|
|
|
977
1331
|
@property
|
|
978
1332
|
def ndwi(self):
|
|
@@ -1327,7 +1681,7 @@ class Sentinel2Collection:
|
|
|
1327
1681
|
The calculation is performed only once when the property is first accessed, and the cached result is returned on subsequent accesses.
|
|
1328
1682
|
|
|
1329
1683
|
Returns:
|
|
1330
|
-
|
|
1684
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1331
1685
|
"""
|
|
1332
1686
|
if self._albedo is None:
|
|
1333
1687
|
self._albedo = self.albedo_collection(snow_free=True)
|
|
@@ -1344,7 +1698,7 @@ class Sentinel2Collection:
|
|
|
1344
1698
|
snow_free (bool): If True, applies a snow mask to the albedo calculation. Defaults to True.
|
|
1345
1699
|
|
|
1346
1700
|
Returns:
|
|
1347
|
-
|
|
1701
|
+
Sentinel2Collection: A Sentinel2Collection image collection
|
|
1348
1702
|
"""
|
|
1349
1703
|
first_image = self.collection.first()
|
|
1350
1704
|
available_bands = first_image.bandNames()
|
|
@@ -1581,6 +1935,19 @@ class Sentinel2Collection:
|
|
|
1581
1935
|
self._masked_clouds_collection = Sentinel2Collection(collection=col)
|
|
1582
1936
|
return self._masked_clouds_collection
|
|
1583
1937
|
|
|
1938
|
+
@property
|
|
1939
|
+
def masked_shadows_collection(self):
|
|
1940
|
+
"""
|
|
1941
|
+
Property attribute to mask shadows and return collection as class object.
|
|
1942
|
+
|
|
1943
|
+
Returns:
|
|
1944
|
+
Sentinel2Collection: Sentinel2Collection image collection
|
|
1945
|
+
"""
|
|
1946
|
+
if self._masked_shadows_collection is None:
|
|
1947
|
+
col = self.collection.map(Sentinel2Collection.MaskShadowsS2)
|
|
1948
|
+
self._masked_shadows_collection = Sentinel2Collection(collection=col)
|
|
1949
|
+
return self._masked_shadows_collection
|
|
1950
|
+
|
|
1584
1951
|
def mask_to_polygon(self, polygon):
|
|
1585
1952
|
"""
|
|
1586
1953
|
Function to mask Sentinel2Collection image collection by a polygon (ee.Geometry), where pixels outside the polygon are masked out.
|
|
@@ -1671,14 +2038,17 @@ class Sentinel2Collection:
|
|
|
1671
2038
|
)
|
|
1672
2039
|
return Sentinel2Collection(collection=col)
|
|
1673
2040
|
|
|
1674
|
-
def binary_mask(self, threshold=None, band_name=None):
|
|
2041
|
+
def binary_mask(self, threshold=None, band_name=None, classify_above_threshold=True, mask_zeros=False):
|
|
1675
2042
|
"""
|
|
1676
|
-
|
|
2043
|
+
Function to create a binary mask (value of 1 for pixels above set threshold and value of 0 for all other pixels) of the Sentinel2Collection image collection based on a specified band.
|
|
1677
2044
|
If a singleband image is provided, the band name is automatically determined.
|
|
1678
2045
|
If multiple bands are available, the user must specify the band name to use for masking.
|
|
1679
2046
|
|
|
1680
2047
|
Args:
|
|
2048
|
+
threshold (float, optional): The threshold value for creating the binary mask. Defaults to None.
|
|
1681
2049
|
band_name (str, optional): The name of the band to use for masking. Defaults to None.
|
|
2050
|
+
classifiy_above_threshold (bool, optional): If True, pixels above the threshold are classified as 1. If False, pixels below the threshold are classified as 1. Defaults to True.
|
|
2051
|
+
mask_zeros (bool, optional): If True, pixels with a value of 0 after the binary mask are masked out in the output binary mask. Useful for classifications. Defaults to False.
|
|
1682
2052
|
|
|
1683
2053
|
Returns:
|
|
1684
2054
|
Sentinel2Collection: Sentinel2Collection singleband image collection with binary masks applied.
|
|
@@ -1697,11 +2067,150 @@ class Sentinel2Collection:
|
|
|
1697
2067
|
if threshold is None:
|
|
1698
2068
|
raise ValueError("Threshold must be specified for binary masking.")
|
|
1699
2069
|
|
|
2070
|
+
if classify_above_threshold:
|
|
2071
|
+
if mask_zeros:
|
|
2072
|
+
col = self.collection.map(
|
|
2073
|
+
lambda image: image.select(band_name).gte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
|
|
2074
|
+
)
|
|
2075
|
+
else:
|
|
2076
|
+
col = self.collection.map(
|
|
2077
|
+
lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
|
|
2078
|
+
)
|
|
2079
|
+
else:
|
|
2080
|
+
if mask_zeros:
|
|
2081
|
+
col = self.collection.map(
|
|
2082
|
+
lambda image: image.select(band_name).lte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
|
|
2083
|
+
)
|
|
2084
|
+
else:
|
|
2085
|
+
col = self.collection.map(
|
|
2086
|
+
lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
|
|
2087
|
+
)
|
|
2088
|
+
return Sentinel2Collection(collection=col)
|
|
2089
|
+
|
|
2090
|
+
def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
|
|
2091
|
+
"""
|
|
2092
|
+
Calculates the anomaly of each image in a collection compared to the mean of each image.
|
|
2093
|
+
|
|
2094
|
+
This function computes the anomaly for each band in the input image by
|
|
2095
|
+
subtracting the mean value of that band from a provided ImageCollection.
|
|
2096
|
+
The anomaly is a measure of how much the pixel values deviate from the
|
|
2097
|
+
average conditions represented by the collection.
|
|
2098
|
+
|
|
2099
|
+
Args:
|
|
2100
|
+
geometry (ee.Geometry): The geometry for image reduction to define the mean value to be used for anomaly calculation.
|
|
2101
|
+
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.
|
|
2102
|
+
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.
|
|
2103
|
+
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.
|
|
2104
|
+
|
|
2105
|
+
Returns:
|
|
2106
|
+
Sentinel2Collection: A Sentinel2Collection where each image represents the anomaly (deviation from
|
|
2107
|
+
the mean) for the chosen band. The output images retain the same band name.
|
|
2108
|
+
"""
|
|
2109
|
+
if self.collection.size().eq(0).getInfo():
|
|
2110
|
+
raise ValueError("The collection is empty.")
|
|
2111
|
+
if band_name is None:
|
|
2112
|
+
first_image = self.collection.first()
|
|
2113
|
+
band_names = first_image.bandNames()
|
|
2114
|
+
if band_names.size().getInfo() == 0:
|
|
2115
|
+
raise ValueError("No bands available in the collection.")
|
|
2116
|
+
elif band_names.size().getInfo() > 1:
|
|
2117
|
+
band_name = band_names.get(0).getInfo()
|
|
2118
|
+
print("Multiple bands available, will be using the first band in the collection for anomaly calculation. Please specify a band name if you wish to use a different band.")
|
|
2119
|
+
else:
|
|
2120
|
+
band_name = band_names.get(0).getInfo()
|
|
2121
|
+
|
|
2122
|
+
col = self.collection.map(lambda image: Sentinel2Collection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace))
|
|
2123
|
+
return Sentinel2Collection(collection=col)
|
|
2124
|
+
|
|
2125
|
+
def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
|
|
2126
|
+
"""
|
|
2127
|
+
Masks select pixels of a selected band from an image based on another specified band and threshold (optional).
|
|
2128
|
+
Example use case is masking vegetation from image when targeting land pixels. Can specify whether to mask pixels above or below the threshold.
|
|
2129
|
+
|
|
2130
|
+
Args:
|
|
2131
|
+
band_to_mask (str): name of the band which will be masked (target image)
|
|
2132
|
+
band_for_mask (str): name of the band to use for the mask (band you want to remove/mask from target image)
|
|
2133
|
+
threshold (float): value between -1 and 1 where pixels less than threshold will be masked; defaults to -1 assuming input band is already classified (masked to pixels of interest).
|
|
2134
|
+
mask_above (bool): if True, masks pixels above the threshold; if False, masks pixels below the threshold
|
|
2135
|
+
|
|
2136
|
+
Returns:
|
|
2137
|
+
Sentinel2Collection: A new Sentinel2Collection with the specified band masked to pixels excluding from `band_for_mask`.
|
|
2138
|
+
"""
|
|
2139
|
+
if self.collection.size().eq(0).getInfo():
|
|
2140
|
+
raise ValueError("The collection is empty.")
|
|
2141
|
+
|
|
1700
2142
|
col = self.collection.map(
|
|
1701
|
-
lambda image:
|
|
2143
|
+
lambda image: Sentinel2Collection.mask_via_band_fn(
|
|
2144
|
+
image,
|
|
2145
|
+
band_to_mask=band_to_mask,
|
|
2146
|
+
band_for_mask=band_for_mask,
|
|
2147
|
+
threshold=threshold,
|
|
2148
|
+
mask_above=mask_above,
|
|
2149
|
+
add_band_to_original_image=add_band_to_original_image
|
|
2150
|
+
)
|
|
1702
2151
|
)
|
|
1703
2152
|
return Sentinel2Collection(collection=col)
|
|
1704
2153
|
|
|
2154
|
+
def mask_via_singleband_image(self, image_collection_for_mask, band_name_to_mask, band_name_for_mask, threshold=-1, mask_above=False, add_band_to_original_image=False):
|
|
2155
|
+
"""
|
|
2156
|
+
Masks select pixels of a selected band from an image collection based on another specified singleband image collection and threshold (optional).
|
|
2157
|
+
Example use case is masking vegetation from image when targeting land pixels. Can specify whether to mask pixels above or below the threshold.
|
|
2158
|
+
This function pairs images from the two collections based on an exact match of the 'Date_Filter' property.
|
|
2159
|
+
|
|
2160
|
+
Args:
|
|
2161
|
+
image_collection_for_mask (Sentinel2Collection): Sentinel2Collection image collection to use for masking (source of pixels that will be used to mask the parent image collection)
|
|
2162
|
+
band_name_to_mask (str): name of the band which will be masked (target image)
|
|
2163
|
+
band_name_for_mask (str): name of the band to use for the mask (band which contains pixels the user wants to remove/mask from target image)
|
|
2164
|
+
threshold (float): threshold value where pixels less (or more, depending on `mask_above`) than threshold will be masked; defaults to -1.
|
|
2165
|
+
mask_above (bool): if True, masks pixels above the threshold; if False, masks pixels below the threshold
|
|
2166
|
+
add_band_to_original_image (bool): if True, adds the band used for masking to the original image as an additional band; if False, only the masked band is retained in the output image.
|
|
2167
|
+
|
|
2168
|
+
Returns:
|
|
2169
|
+
Sentinel2Collection: A new Sentinel2Collection with the specified band masked to pixels excluding from `band_for_mask`.
|
|
2170
|
+
"""
|
|
2171
|
+
|
|
2172
|
+
if self.collection.size().eq(0).getInfo():
|
|
2173
|
+
raise ValueError("The collection is empty.")
|
|
2174
|
+
if not isinstance(image_collection_for_mask, Sentinel2Collection):
|
|
2175
|
+
raise ValueError("image_collection_for_mask must be a Sentinel2Collection object.")
|
|
2176
|
+
size1 = self.collection.size().getInfo()
|
|
2177
|
+
size2 = image_collection_for_mask.collection.size().getInfo()
|
|
2178
|
+
if size1 != size2:
|
|
2179
|
+
raise ValueError(f"Warning: Collections have different sizes ({size1} vs {size2}). Please ensure both collections have the same number of images and matching dates.")
|
|
2180
|
+
if size1 == 0 or size2 == 0:
|
|
2181
|
+
raise ValueError("Warning: One of the input collections is empty.")
|
|
2182
|
+
|
|
2183
|
+
# Pair by exact Date_Filter property
|
|
2184
|
+
primary = self.collection.select([band_name_to_mask])
|
|
2185
|
+
secondary = image_collection_for_mask.collection.select([band_name_for_mask])
|
|
2186
|
+
join = ee.Join.inner()
|
|
2187
|
+
flt = ee.Filter.equals(leftField='Date_Filter', rightField='Date_Filter')
|
|
2188
|
+
paired = join.apply(primary, secondary, flt)
|
|
2189
|
+
|
|
2190
|
+
def _map_pair(f):
|
|
2191
|
+
f = ee.Feature(f) # <-- treat as Feature
|
|
2192
|
+
prim = ee.Image(f.get('primary')) # <-- get the primary Image
|
|
2193
|
+
sec = ee.Image(f.get('secondary')) # <-- get the secondary Image
|
|
2194
|
+
|
|
2195
|
+
merged = prim.addBands(sec.select([band_name_for_mask]))
|
|
2196
|
+
|
|
2197
|
+
out = Sentinel2Collection.mask_via_band_fn(
|
|
2198
|
+
merged,
|
|
2199
|
+
band_to_mask=band_name_to_mask,
|
|
2200
|
+
band_for_mask=band_name_for_mask,
|
|
2201
|
+
threshold=threshold,
|
|
2202
|
+
mask_above=mask_above,
|
|
2203
|
+
add_band_to_original_image=add_band_to_original_image
|
|
2204
|
+
)
|
|
2205
|
+
|
|
2206
|
+
# guarantee single band + keep properties
|
|
2207
|
+
out = ee.Image(out).select([band_name_to_mask]).copyProperties(prim, prim.propertyNames())
|
|
2208
|
+
out = out.set('Date_Filter', prim.get('Date_Filter'))
|
|
2209
|
+
return ee.Image(out) # <-- return as Image
|
|
2210
|
+
|
|
2211
|
+
col = ee.ImageCollection(paired.map(_map_pair))
|
|
2212
|
+
return Sentinel2Collection(collection=col)
|
|
2213
|
+
|
|
1705
2214
|
def image_grab(self, img_selector):
|
|
1706
2215
|
"""
|
|
1707
2216
|
Function to select ("grab") an image by index from the collection. Easy way to get latest image or browse imagery one-by-one.
|
|
@@ -2536,3 +3045,59 @@ class Sentinel2Collection:
|
|
|
2536
3045
|
print(f"Zonal stats saved to {file_path}.csv")
|
|
2537
3046
|
return
|
|
2538
3047
|
return pivot_df
|
|
3048
|
+
|
|
3049
|
+
def export_to_asset_collection(
|
|
3050
|
+
self,
|
|
3051
|
+
asset_collection_path,
|
|
3052
|
+
region,
|
|
3053
|
+
scale,
|
|
3054
|
+
dates=None,
|
|
3055
|
+
filename_prefix="",
|
|
3056
|
+
crs=None,
|
|
3057
|
+
max_pixels=int(1e13),
|
|
3058
|
+
description_prefix="export"
|
|
3059
|
+
):
|
|
3060
|
+
"""
|
|
3061
|
+
Exports an image collection to a Google Earth Engine asset collection. The asset collection will be created if it does not already exist,
|
|
3062
|
+
and each image exported will be named according to the provided filename prefix and date.
|
|
3063
|
+
|
|
3064
|
+
Args:
|
|
3065
|
+
asset_collection_path (str): The path to the asset collection.
|
|
3066
|
+
region (ee.Geometry): The region to export.
|
|
3067
|
+
scale (int): The scale of the export.
|
|
3068
|
+
dates (list, optional): The dates to export. Defaults to None.
|
|
3069
|
+
filename_prefix (str, optional): The filename prefix. Defaults to "", i.e. blank.
|
|
3070
|
+
crs (str, optional): The coordinate reference system. Defaults to None, which will use the image's CRS.
|
|
3071
|
+
max_pixels (int, optional): The maximum number of pixels. Defaults to int(1e13).
|
|
3072
|
+
description_prefix (str, optional): The description prefix. Defaults to "export".
|
|
3073
|
+
|
|
3074
|
+
Returns:
|
|
3075
|
+
None: (queues export tasks)
|
|
3076
|
+
"""
|
|
3077
|
+
ic = self.collection
|
|
3078
|
+
if dates is None:
|
|
3079
|
+
dates = self.dates
|
|
3080
|
+
try:
|
|
3081
|
+
ee.data.createAsset({'type': 'ImageCollection'}, asset_collection_path)
|
|
3082
|
+
except Exception:
|
|
3083
|
+
pass
|
|
3084
|
+
|
|
3085
|
+
for date_str in dates:
|
|
3086
|
+
img = ee.Image(ic.filter(ee.Filter.eq('Date_Filter', date_str)).first())
|
|
3087
|
+
asset_id = asset_collection_path + "/" + filename_prefix + date_str
|
|
3088
|
+
desc = description_prefix + "_" + filename_prefix + date_str
|
|
3089
|
+
|
|
3090
|
+
params = {
|
|
3091
|
+
'image': img,
|
|
3092
|
+
'description': desc,
|
|
3093
|
+
'assetId': asset_id,
|
|
3094
|
+
'region': region,
|
|
3095
|
+
'scale': scale,
|
|
3096
|
+
'maxPixels': max_pixels
|
|
3097
|
+
}
|
|
3098
|
+
if crs:
|
|
3099
|
+
params['crs'] = crs
|
|
3100
|
+
|
|
3101
|
+
ee.batch.Export.image.toAsset(**params).start()
|
|
3102
|
+
|
|
3103
|
+
print("Queued", len(dates), "export tasks to", asset_collection_path)
|