RadGEEToolbox 1.6.4__py3-none-any.whl → 1.6.6__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.
@@ -1,12 +1,35 @@
1
1
  import ee
2
2
  import pandas as pd
3
3
  import numpy as np
4
+
5
+
6
+ # ---- Reflectance scaling for Landsat Collection 2 SR ----
7
+ _LS_SR_BANDS = ["SR_B1", "SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B6", "SR_B7"]
8
+ _LS_SCALE = 0.0000275
9
+ _LS_OFFSET = -0.2
10
+
11
+ def _scale_landsat_sr(img):
12
+ """
13
+ Converts Landsat C2 SR DN values to reflectance values for SR_B1..SR_B7 (overwrite bands).
14
+
15
+ Args:
16
+ img (ee.Image): Input Landsat image without scaled bands.
17
+
18
+ Returns:
19
+ ee.Image: Image with scaled reflectance bands.
20
+ """
21
+ img = ee.Image(img)
22
+ is_scaled = ee.Algorithms.IsEqual(img.get('rgt:scaled'), 'landsat_sr')
23
+ scaled = img.select(_LS_SR_BANDS).multiply(_LS_SCALE).add(_LS_OFFSET)
24
+ out = img.addBands(scaled, None, True).set('rgt:scaled', 'landsat_sr')
25
+ return ee.Image(ee.Algorithms.If(is_scaled, img, out))
26
+
4
27
  class LandsatCollection:
5
28
  """
6
29
  Represents a user-defined collection of NASA/USGS Landsat 5, 8, and 9 TM & OLI surface reflectance satellite images at 30 m/px from Google Earth Engine (GEE).
7
30
 
8
31
  This class enables simplified definition, filtering, masking, and processing of multispectral Landsat imagery.
9
- It supports multiple spatial and temporal filters, caching for efficient computation, and direct computation of
32
+ It supports multiple spatial and temporal filters, caching for efficient computation, and direct computation of
10
33
  key spectral indices like NDWI, NDVI, halite index, and more. It also includes utilities for cloud masking,
11
34
  mosaicking, zonal statistics, and transect analysis.
12
35
 
@@ -22,10 +45,11 @@ class LandsatCollection:
22
45
  cloud_percentage_threshold (int, optional): Max allowed cloud cover percentage. Defaults to 100.
23
46
  boundary (ee.Geometry, optional): A geometry for filtering to images that intersect with the boundary shape. Overrides `tile_path` and `tile_row` if provided.
24
47
  collection (ee.ImageCollection, optional): A pre-filtered Landsat ee.ImageCollection object to be converted to a LandsatCollection object. Overrides all other filters.
48
+ scale_bands (bool, optional): If True, all SR bands will be scaled from DN values to reflectance values. Defaults to False.
25
49
 
26
50
  Attributes:
27
- collection (ee.ImageCollection): The filtered or user-supplied image collection converted to an ee.ImageCollection object.
28
-
51
+ collection (ee.ImageCollection): The filtered or user-supplied image collection converted to an ee.ImageCollection object.
52
+
29
53
  Raises:
30
54
  ValueError: Raised if required filter parameters are missing, or if both `collection` and other filters are provided.
31
55
 
@@ -48,11 +72,31 @@ class LandsatCollection:
48
72
  >>> latest_image = cloud_masked.image_grab(-1)
49
73
  >>> ndwi_collection = image_collection.ndwi
50
74
  """
51
- def __init__(self, start_date=None, end_date=None, tile_row=None, tile_path=None, cloud_percentage_threshold=None, boundary=None, collection=None):
75
+
76
+ def __init__(
77
+ self,
78
+ start_date=None,
79
+ end_date=None,
80
+ tile_row=None,
81
+ tile_path=None,
82
+ cloud_percentage_threshold=None,
83
+ boundary=None,
84
+ collection=None,
85
+ scale_bands=False,
86
+ ):
52
87
  if collection is None and (start_date is None or end_date is None):
53
- raise ValueError("Either provide all required fields (start_date, end_date, tile_row, tile_path ; or boundary in place of tiles) or provide a collection.")
54
- if tile_row is None and tile_path is None and boundary is None and collection is None:
55
- raise ValueError("Provide either tile or boundary/geometry specifications to filter the image collection")
88
+ raise ValueError(
89
+ "Either provide all required fields (start_date, end_date, tile_row, tile_path ; or boundary in place of tiles) or provide a collection."
90
+ )
91
+ if (
92
+ tile_row is None
93
+ and tile_path is None
94
+ and boundary is None
95
+ and collection is None
96
+ ):
97
+ raise ValueError(
98
+ "Provide either tile or boundary/geometry specifications to filter the image collection"
99
+ )
56
100
  if collection is None:
57
101
  self.start_date = start_date
58
102
  self.end_date = end_date
@@ -85,11 +129,13 @@ class LandsatCollection:
85
129
  self.collection = self.get_boundary_filtered_collection()
86
130
  else:
87
131
  self.collection = collection
132
+ if scale_bands:
133
+ self.collection = self.collection.map(_scale_landsat_sr)
88
134
 
89
-
90
135
  self._dates_list = None
91
136
  self._dates = None
92
137
  self.ndwi_threshold = -1
138
+ self.mndwi_threshold = -1
93
139
  self.ndvi_threshold = -1
94
140
  self.halite_threshold = -1
95
141
  self.gypsum_threshold = -1
@@ -105,6 +151,7 @@ class LandsatCollection:
105
151
  self._max = None
106
152
  self._min = None
107
153
  self._ndwi = None
154
+ self._mndwi = None
108
155
  self._ndvi = None
109
156
  self._halite = None
110
157
  self._gypsum = None
@@ -113,42 +160,88 @@ class LandsatCollection:
113
160
  self._LST = None
114
161
  self._MosaicByDate = None
115
162
  self._PixelAreaSumCollection = None
163
+ self._Reflectance = None
116
164
 
117
165
  @staticmethod
118
166
  def image_dater(image):
119
167
  """
120
168
  Adds date to image properties as 'Date_Filter'.
121
169
 
122
- Args:
170
+ Args:
123
171
  image (ee.Image): Input image
124
172
 
125
- Returns:
173
+ Returns:
126
174
  ee.Image: Image with date in properties.
127
175
  """
128
- date = ee.Number(image.date().format('YYYY-MM-dd'))
129
- return image.set({'Date_Filter': date})
130
-
176
+ date = ee.Number(image.date().format("YYYY-MM-dd"))
177
+ return image.set({"Date_Filter": date})
178
+
131
179
  @staticmethod
132
180
  def landsat5bandrename(img):
133
181
  """
134
182
  Renames Landsat 5 bands to match Landsat 8 & 9.
135
183
 
136
- Args:
184
+ Args:
137
185
  image (ee.Image): input image
138
-
139
- Returns:
186
+
187
+ Returns:
140
188
  ee.Image: image with renamed bands
141
189
  """
142
- return img.select('SR_B1', 'SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B7', 'QA_PIXEL').rename('SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B6', 'SR_B7', 'QA_PIXEL')
143
-
190
+ return img.select(
191
+ "SR_B1", "SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B7", "QA_PIXEL"
192
+ ).rename("SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B6", "SR_B7", "QA_PIXEL")
193
+
144
194
  @staticmethod
145
195
  def landsat_ndwi_fn(image, threshold, ng_threshold=None):
146
196
  """
147
- Calculates ndwi from GREEN and NIR bands (McFeeters, 1996 - https://doi.org/10.1080/01431169608948714) for Landsat imagery and mask image based on threshold.
148
-
197
+ Calculates ndwi from GREEN and NIR bands (McFeeters, 1996 - https://doi.org/10.1080/01431169608948714) for Landsat imagery and mask image based on threshold.
198
+
199
+ Can specify separate thresholds for Landsat 5 vs 8 & 9 images, where the threshold argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8 & 9.
200
+
201
+ Args:
202
+ image (ee.Image): input image
203
+ threshold (float): value between -1 and 1 where pixels less than threshold will be masked, applies to landsat 5 when ng_threshold is also set.
204
+ ng_threshold (float, optional): integer threshold to be applied to landsat 8 or 9 where pixels less than threshold are masked
205
+
206
+ Returns:
207
+ ee.Image: ndwi image
208
+ """
209
+ ndwi_calc = image.normalizedDifference(
210
+ ["SR_B3", "SR_B5"]
211
+ ) # green-NIR / green+NIR -- full NDWI image
212
+ water = (
213
+ ndwi_calc.updateMask(ndwi_calc.gte(threshold))
214
+ .rename("ndwi")
215
+ .copyProperties(image)
216
+ )
217
+ if ng_threshold != None:
218
+ water = ee.Algorithms.If(
219
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
220
+ ndwi_calc.updateMask(ndwi_calc.gte(threshold))
221
+ .rename("ndwi")
222
+ .copyProperties(image)
223
+ .set("threshold", threshold),
224
+ ndwi_calc.updateMask(ndwi_calc.gte(ng_threshold))
225
+ .rename("ndwi")
226
+ .copyProperties(image)
227
+ .set("threshold", ng_threshold),
228
+ )
229
+ else:
230
+ water = (
231
+ ndwi_calc.updateMask(ndwi_calc.gte(threshold))
232
+ .rename("ndwi")
233
+ .copyProperties(image)
234
+ )
235
+ return water
236
+
237
+ @staticmethod
238
+ def landsat_mndwi_fn(image, threshold, ng_threshold=None):
239
+ """
240
+ Calculates Modified Normalized Difference Water Index (MNDWI) from GREEN and SWIR bands for Landsat imagery and mask image based on threshold.
241
+
149
242
  Can specify separate thresholds for Landsat 5 vs 8 & 9 images, where the threshold argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8 & 9.
150
243
 
151
- Args:
244
+ Args:
152
245
  image (ee.Image): input image
153
246
  threshold (float): value between -1 and 1 where pixels less than threshold will be masked, applies to landsat 5 when ng_threshold is also set.
154
247
  ng_threshold (float, optional): integer threshold to be applied to landsat 8 or 9 where pixels less than threshold are masked
@@ -156,14 +249,32 @@ class LandsatCollection:
156
249
  Returns:
157
250
  ee.Image: ndwi image
158
251
  """
159
- ndwi_calc = image.normalizedDifference(['SR_B3', 'SR_B5']) #green-NIR / green+NIR -- full NDWI image
160
- water = ndwi_calc.updateMask(ndwi_calc.gte(threshold)).rename('ndwi').copyProperties(image)
252
+ mndwi_calc = image.normalizedDifference(
253
+ ["SR_B3", "SR_B6"]
254
+ ) # green-SWIR / green+SWIR -- full NDWI image
255
+ water = (
256
+ mndwi_calc.updateMask(mndwi_calc.gte(threshold))
257
+ .rename("ndwi")
258
+ .copyProperties(image)
259
+ )
161
260
  if ng_threshold != None:
162
- water = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
163
- ndwi_calc.updateMask(ndwi_calc.gte(threshold)).rename('ndwi').copyProperties(image).set('threshold', threshold), \
164
- ndwi_calc.updateMask(ndwi_calc.gte(ng_threshold)).rename('ndwi').copyProperties(image).set('threshold', ng_threshold))
261
+ water = ee.Algorithms.If(
262
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
263
+ mndwi_calc.updateMask(mndwi_calc.gte(threshold))
264
+ .rename("ndwi")
265
+ .copyProperties(image)
266
+ .set("threshold", threshold),
267
+ mndwi_calc.updateMask(mndwi_calc.gte(ng_threshold))
268
+ .rename("ndwi")
269
+ .copyProperties(image)
270
+ .set("threshold", ng_threshold),
271
+ )
165
272
  else:
166
- water = ndwi_calc.updateMask(ndwi_calc.gte(threshold)).rename('ndwi').copyProperties(image)
273
+ water = (
274
+ mndwi_calc.updateMask(mndwi_calc.gte(threshold))
275
+ .rename("ndwi")
276
+ .copyProperties(image)
277
+ )
167
278
  return water
168
279
 
169
280
  @staticmethod
@@ -181,20 +292,34 @@ class LandsatCollection:
181
292
  Returns:
182
293
  ee.Image: ndvi ee.Image
183
294
  """
184
- ndvi_calc = image.normalizedDifference(['SR_B5', 'SR_B4']) #NIR-RED/NIR+RED -- full NDVI image
295
+ ndvi_calc = image.normalizedDifference(
296
+ ["SR_B5", "SR_B4"]
297
+ ) # NIR-RED/NIR+RED -- full NDVI image
185
298
  if ng_threshold != None:
186
- vegetation = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
187
- ndvi_calc.updateMask(ndvi_calc.gte(threshold)).rename('ndvi').copyProperties(image).set('threshold', threshold), \
188
- ndvi_calc.updateMask(ndvi_calc.gte(ng_threshold)).rename('ndvi').copyProperties(image).set('threshold', ng_threshold))
299
+ vegetation = ee.Algorithms.If(
300
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
301
+ ndvi_calc.updateMask(ndvi_calc.gte(threshold))
302
+ .rename("ndvi")
303
+ .copyProperties(image)
304
+ .set("threshold", threshold),
305
+ ndvi_calc.updateMask(ndvi_calc.gte(ng_threshold))
306
+ .rename("ndvi")
307
+ .copyProperties(image)
308
+ .set("threshold", ng_threshold),
309
+ )
189
310
  else:
190
- vegetation = ndvi_calc.updateMask(ndvi_calc.gte(threshold)).rename('ndvi').copyProperties(image)
311
+ vegetation = (
312
+ ndvi_calc.updateMask(ndvi_calc.gte(threshold))
313
+ .rename("ndvi")
314
+ .copyProperties(image)
315
+ )
191
316
  return vegetation
192
-
317
+
193
318
  @staticmethod
194
319
  def landsat_halite_fn(image, threshold, ng_threshold=None):
195
320
  """
196
321
  Calculates multispectral halite index from RED and SWIR1 bands (Radwin & Bowen, 2021 - https://onlinelibrary.wiley.com/doi/10.1002/esp.5089) for Landsat imagery and mask image based on threshold.
197
-
322
+
198
323
  Can specify separate thresholds for Landsat 5 vs 8 & 9 images, where the threshold argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8 & 9.
199
324
 
200
325
  Args:
@@ -205,20 +330,32 @@ class LandsatCollection:
205
330
  Returns:
206
331
  ee.Image: halite ee.Image
207
332
  """
208
- halite_index = image.normalizedDifference(['SR_B4', 'SR_B6'])
333
+ halite_index = image.normalizedDifference(["SR_B4", "SR_B6"])
209
334
  if ng_threshold != None:
210
- halite = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
211
- halite_index.updateMask(halite_index.gte(threshold)).rename('halite').copyProperties(image).set('threshold', threshold), \
212
- halite_index.updateMask(halite_index.gte(ng_threshold)).rename('halite').copyProperties(image).set('threshold', ng_threshold))
335
+ halite = ee.Algorithms.If(
336
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
337
+ halite_index.updateMask(halite_index.gte(threshold))
338
+ .rename("halite")
339
+ .copyProperties(image)
340
+ .set("threshold", threshold),
341
+ halite_index.updateMask(halite_index.gte(ng_threshold))
342
+ .rename("halite")
343
+ .copyProperties(image)
344
+ .set("threshold", ng_threshold),
345
+ )
213
346
  else:
214
- halite = halite_index.updateMask(halite_index.gte(threshold)).rename('halite').copyProperties(image)
215
- return halite
216
-
347
+ halite = (
348
+ halite_index.updateMask(halite_index.gte(threshold))
349
+ .rename("halite")
350
+ .copyProperties(image)
351
+ )
352
+ return halite
353
+
217
354
  @staticmethod
218
355
  def landsat_gypsum_fn(image, threshold, ng_threshold=None):
219
356
  """
220
357
  Calculates multispectral gypsum index from SWIR1 and SWIR2 bands(Radwin & Bowen, 2024 - https://onlinelibrary.wiley.com/doi/10.1002/esp.5089) for Landsat imagery and mask image based on threshold.
221
-
358
+
222
359
  Can specify separate thresholds for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8&9.
223
360
 
224
361
  Args:
@@ -229,19 +366,31 @@ class LandsatCollection:
229
366
  Returns:
230
367
  ee.Image: gypsum ee.Image
231
368
  """
232
- gypsum_index = image.normalizedDifference(['SR_B6', 'SR_B7'])
369
+ gypsum_index = image.normalizedDifference(["SR_B6", "SR_B7"])
233
370
  if ng_threshold != None:
234
- gypsum = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
235
- gypsum_index.updateMask(gypsum_index.gte(threshold)).rename('gypsum').copyProperties(image).set('threshold', threshold), \
236
- gypsum_index.updateMask(gypsum_index.gte(ng_threshold)).rename('gypsum').copyProperties(image).set('threshold', ng_threshold))
371
+ gypsum = ee.Algorithms.If(
372
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
373
+ gypsum_index.updateMask(gypsum_index.gte(threshold))
374
+ .rename("gypsum")
375
+ .copyProperties(image)
376
+ .set("threshold", threshold),
377
+ gypsum_index.updateMask(gypsum_index.gte(ng_threshold))
378
+ .rename("gypsum")
379
+ .copyProperties(image)
380
+ .set("threshold", ng_threshold),
381
+ )
237
382
  else:
238
- gypsum = gypsum_index.updateMask(gypsum_index.gte(threshold)).rename('gypsum').copyProperties(image)
383
+ gypsum = (
384
+ gypsum_index.updateMask(gypsum_index.gte(threshold))
385
+ .rename("gypsum")
386
+ .copyProperties(image)
387
+ )
239
388
  return gypsum
240
-
389
+
241
390
  @staticmethod
242
391
  def landsat_ndti_fn(image, threshold, ng_threshold=None):
243
392
  """
244
- Calculates turbidity of water pixels using Normalized Difference Turbidity Index (NDTI; Lacaux et al., 2007 - https://doi.org/10.1016/j.rse.2006.07.012)
393
+ Calculates turbidity of water pixels using Normalized Difference Turbidity Index (NDTI; Lacaux et al., 2007 - https://doi.org/10.1016/j.rse.2006.07.012)
245
394
  and mask image based on threshold. Can specify separate thresholds for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8&9.
246
395
 
247
396
  Args:
@@ -252,20 +401,32 @@ class LandsatCollection:
252
401
  Returns:
253
402
  ee.Image: turbidity ee.Image
254
403
  """
255
- NDTI = image.normalizedDifference(['SR_B4', 'SR_B3'])
404
+ NDTI = image.normalizedDifference(["SR_B4", "SR_B3"])
256
405
  if ng_threshold != None:
257
- turbidity = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
258
- NDTI.updateMask(NDTI.gte(threshold)).rename('ndti').copyProperties(image).set('threshold', threshold), \
259
- NDTI.updateMask(NDTI.gte(ng_threshold)).rename('ndti').copyProperties(image).set('threshold', ng_threshold))
406
+ turbidity = ee.Algorithms.If(
407
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
408
+ NDTI.updateMask(NDTI.gte(threshold))
409
+ .rename("ndti")
410
+ .copyProperties(image)
411
+ .set("threshold", threshold),
412
+ NDTI.updateMask(NDTI.gte(ng_threshold))
413
+ .rename("ndti")
414
+ .copyProperties(image)
415
+ .set("threshold", ng_threshold),
416
+ )
260
417
  else:
261
- turbidity = NDTI.updateMask(NDTI.gte(threshold)).rename('ndti').copyProperties(image)
418
+ turbidity = (
419
+ NDTI.updateMask(NDTI.gte(threshold))
420
+ .rename("ndti")
421
+ .copyProperties(image)
422
+ )
262
423
  return turbidity
263
-
424
+
264
425
  @staticmethod
265
426
  def landsat_kivu_chla_fn(image, threshold, ng_threshold=None):
266
427
  """
267
- Calculates relative chlorophyll-a concentrations of water pixels using 3BDA/KIVU index
268
- (see Boucher et al., 2018 for review - https://esajournals.onlinelibrary.wiley.com/doi/10.1002/eap.1708) and mask image based on threshold. Can specify separate thresholds
428
+ Calculates relative chlorophyll-a concentrations of water pixels using 3BDA/KIVU index
429
+ (see Boucher et al., 2018 for review - https://esajournals.onlinelibrary.wiley.com/doi/10.1002/eap.1708) and mask image based on threshold. Can specify separate thresholds
269
430
  for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5 and the ng_threshold
270
431
  argument applies to Landsat 8&9.
271
432
 
@@ -277,16 +438,34 @@ class LandsatCollection:
277
438
  Returns:
278
439
  ee.Image: chlorophyll-a ee.Image
279
440
  """
280
- KIVU = image.expression('(BLUE - RED) / GREEN', {'BLUE':image.select('SR_B2'), 'RED':image.select('SR_B4'), 'GREEN':image.select('SR_B3')})
441
+ KIVU = image.expression(
442
+ "(BLUE - RED) / GREEN",
443
+ {
444
+ "BLUE": image.select("SR_B2"),
445
+ "RED": image.select("SR_B4"),
446
+ "GREEN": image.select("SR_B3"),
447
+ },
448
+ )
281
449
  if ng_threshold != None:
282
- chlorophyll = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
283
- KIVU.updateMask(KIVU.gte(threshold)).rename('kivu').copyProperties(image).set('threshold', threshold), \
284
- KIVU.updateMask(KIVU.gte(ng_threshold)).rename('kivu').copyProperties(image).set('threshold', ng_threshold))
450
+ chlorophyll = ee.Algorithms.If(
451
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
452
+ KIVU.updateMask(KIVU.gte(threshold))
453
+ .rename("kivu")
454
+ .copyProperties(image)
455
+ .set("threshold", threshold),
456
+ KIVU.updateMask(KIVU.gte(ng_threshold))
457
+ .rename("kivu")
458
+ .copyProperties(image)
459
+ .set("threshold", ng_threshold),
460
+ )
285
461
  else:
286
- chlorophyll = KIVU.updateMask(KIVU.gte(threshold)).rename('kivu').copyProperties(image)
462
+ chlorophyll = (
463
+ KIVU.updateMask(KIVU.gte(threshold))
464
+ .rename("kivu")
465
+ .copyProperties(image)
466
+ )
287
467
  return chlorophyll
288
468
 
289
-
290
469
  @staticmethod
291
470
  def MaskWaterLandsat(image):
292
471
  """
@@ -299,11 +478,11 @@ class LandsatCollection:
299
478
  ee.Image: ee.Image with water pixels masked.
300
479
  """
301
480
  WaterBitMask = ee.Number(2).pow(7).int()
302
- qa = image.select('QA_PIXEL')
481
+ qa = image.select("QA_PIXEL")
303
482
  water_extract = qa.bitwiseAnd(WaterBitMask).eq(0)
304
483
  masked_image = image.updateMask(water_extract).copyProperties(image)
305
484
  return masked_image
306
-
485
+
307
486
  @staticmethod
308
487
  def MaskWaterLandsatByNDWI(image, threshold, ng_threshold=None):
309
488
  """
@@ -311,24 +490,36 @@ class LandsatCollection:
311
490
  all pixels less than NDWI threshold are masked out. Can specify separate thresholds for Landsat 5 vs 8&9 images, where the threshold
312
491
  argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8&9
313
492
 
314
- Args:
493
+ Args:
315
494
  image (ee.Image): input image
316
495
  threshold (float): value between -1 and 1 where NDWI pixels greater than threshold will be masked, applies to landsat 5 when ng_threshold is also set.
317
496
  ng_threshold (float, optional): integer threshold to be applied to landsat 8 or 9 where NDWI pixels greater than threshold are masked
318
-
497
+
319
498
  Returns:
320
499
  ee.Image: ee.Image with water pixels masked
321
500
  """
322
- ndwi_calc = image.normalizedDifference(['SR_B3', 'SR_B5']) #green-NIR / green+NIR -- full NDWI image
323
- water = ndwi_calc.updateMask(ndwi_calc.gte(threshold)).rename('ndwi').copyProperties(image)
501
+ ndwi_calc = image.normalizedDifference(
502
+ ["SR_B3", "SR_B5"]
503
+ ) # green-NIR / green+NIR -- full NDWI image
504
+ water = (
505
+ ndwi_calc.updateMask(ndwi_calc.gte(threshold))
506
+ .rename("ndwi")
507
+ .copyProperties(image)
508
+ )
324
509
  if ng_threshold != None:
325
- water = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
326
- image.updateMask(ndwi_calc.lt(threshold)).set('threshold', threshold), \
327
- image.updateMask(ndwi_calc.lt(ng_threshold)).set('threshold', ng_threshold))
510
+ water = ee.Algorithms.If(
511
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
512
+ image.updateMask(ndwi_calc.lt(threshold)).set("threshold", threshold),
513
+ image.updateMask(ndwi_calc.lt(ng_threshold)).set(
514
+ "threshold", ng_threshold
515
+ ),
516
+ )
328
517
  else:
329
- water = image.updateMask(ndwi_calc.lt(threshold)).set('threshold', threshold)
518
+ water = image.updateMask(ndwi_calc.lt(threshold)).set(
519
+ "threshold", threshold
520
+ )
330
521
  return water
331
-
522
+
332
523
  @staticmethod
333
524
  def MaskToWaterLandsat(image):
334
525
  """
@@ -341,86 +532,123 @@ class LandsatCollection:
341
532
  ee.Image: ee.Image with water pixels masked.
342
533
  """
343
534
  WaterBitMask = ee.Number(2).pow(7).int()
344
- qa = image.select('QA_PIXEL')
535
+ qa = image.select("QA_PIXEL")
345
536
  water_extract = qa.bitwiseAnd(WaterBitMask).neq(0)
346
537
  masked_image = image.updateMask(water_extract).copyProperties(image)
347
538
  return masked_image
348
-
539
+
349
540
  @staticmethod
350
541
  def MaskToWaterLandsatByNDWI(image, threshold, ng_threshold=None):
351
542
  """
352
543
  Masks water pixels using NDWI based on threshold. Can specify separate thresholds for Landsat 5 vs 8&9 images, where the threshold
353
544
  argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8&9
354
545
 
355
- Args:
546
+ Args:
356
547
  image (ee.Image): input image
357
548
  threshold (float): value between -1 and 1 where NDWI pixels less than threshold will be masked, applies to landsat 5 when ng_threshold is also set.
358
549
  ng_threshold (float, optional): integer threshold to be applied to landsat 8 or 9 where NDWI pixels less than threshold are masked
359
-
550
+
360
551
  Returns:
361
552
  ee.Image: ee.Image with water pixels masked.
362
553
  """
363
- ndwi_calc = image.normalizedDifference(['SR_B3', 'SR_B5']) #green-NIR / green+NIR -- full NDWI image
364
- water = ndwi_calc.updateMask(ndwi_calc.gte(threshold)).rename('ndwi').copyProperties(image)
554
+ ndwi_calc = image.normalizedDifference(
555
+ ["SR_B3", "SR_B5"]
556
+ ) # green-NIR / green+NIR -- full NDWI image
557
+ water = (
558
+ ndwi_calc.updateMask(ndwi_calc.gte(threshold))
559
+ .rename("ndwi")
560
+ .copyProperties(image)
561
+ )
365
562
  if ng_threshold != None:
366
- water = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
367
- image.updateMask(ndwi_calc.gte(threshold)).set('threshold', threshold), \
368
- image.updateMask(ndwi_calc.gte(ng_threshold)).set('threshold', ng_threshold))
563
+ water = ee.Algorithms.If(
564
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
565
+ image.updateMask(ndwi_calc.gte(threshold)).set("threshold", threshold),
566
+ image.updateMask(ndwi_calc.gte(ng_threshold)).set(
567
+ "threshold", ng_threshold
568
+ ),
569
+ )
369
570
  else:
370
- water = image.updateMask(ndwi_calc.gte(threshold)).set('threshold', threshold)
571
+ water = image.updateMask(ndwi_calc.gte(threshold)).set(
572
+ "threshold", threshold
573
+ )
371
574
  return water
372
575
 
373
576
  @staticmethod
374
577
  def halite_mask(image, threshold, ng_threshold=None):
375
578
  """
376
- Masks halite pixels after specifying index to isolate/mask-to halite pixels.
377
-
579
+ Masks halite pixels after specifying index to isolate/mask-to halite pixels.
580
+
378
581
  Can specify separate thresholds for Landsat 5 vs 8&9 images where the threshold
379
582
  argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8&9.
380
583
 
381
584
  Args:
382
585
  image (ee.Image): input ee.Image
383
586
  threshold (float): value between -1 and 1 where pixels less than threshold will be masked, applies to landsat 5 when ng_threshold is also set.
384
- ng_threshold (float, optional): integer threshold to be applied to landsat 8 or 9 where pixels less than threshold are masked
587
+ ng_threshold (float, optional): integer threshold to be applied to landsat 8 or 9 where pixels less than threshold are masked
385
588
 
386
589
  Returns:
387
590
  image (ee.Image): masked ee.Image
388
591
  """
389
- halite_index = image.normalizedDifference(['SR_B4', 'SR_B6']) # red-swir1 / red+swir1
592
+ halite_index = image.normalizedDifference(
593
+ ["SR_B4", "SR_B6"]
594
+ ) # red-swir1 / red+swir1
390
595
  if ng_threshold != None:
391
- mask = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
392
- image.updateMask(halite_index.lt(threshold)).copyProperties(image), \
393
- image.updateMask(halite_index.lt(ng_threshold)).copyProperties(image))
596
+ mask = ee.Algorithms.If(
597
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
598
+ image.updateMask(halite_index.lt(threshold)).copyProperties(image),
599
+ image.updateMask(halite_index.lt(ng_threshold)).copyProperties(image),
600
+ )
394
601
  else:
395
602
  mask = image.updateMask(halite_index.lt(threshold)).copyProperties(image)
396
- return mask
397
-
603
+ return mask
604
+
398
605
  @staticmethod
399
- def gypsum_and_halite_mask(image, halite_threshold, gypsum_threshold, halite_ng_threshold=None, gypsum_ng_threshold=None):
606
+ def gypsum_and_halite_mask(
607
+ image,
608
+ halite_threshold,
609
+ gypsum_threshold,
610
+ halite_ng_threshold=None,
611
+ gypsum_ng_threshold=None,
612
+ ):
400
613
  """
401
- Masks both gypsum and halite pixels. Must specify threshold for isolating halite and gypsum pixels.
402
-
403
- Can specify separate thresholds for Landsat 5 vs 8&9 images where the threshold argument applies to Landsat 5
614
+ Masks both gypsum and halite pixels. Must specify threshold for isolating halite and gypsum pixels.
615
+
616
+ Can specify separate thresholds for Landsat 5 vs 8&9 images where the threshold argument applies to Landsat 5
404
617
  and the ng_threshold argument applies to Landsat 8&9.
405
618
 
406
619
  Args:
407
620
  image (ee.Image): input ee.Image
408
621
  halite_threshold (float): integer threshold for halite where pixels less than threshold are masked, applies to landsat 5 when ng_threshold is also set.
409
622
  gypsum_threshold (float): integer threshold for gypsum where pixels less than threshold are masked, applies to landsat 5 when ng_threshold is also set.
410
- halite_ng_threshold (float, optional): integer threshold for halite to be applied to landsat 8 or 9 where pixels less than threshold are masked
411
- gypsum_ng_threshold (float, optional): integer threshold for gypsum to be applied to landsat 8 or 9 where pixels less than threshold are masked
623
+ halite_ng_threshold (float, optional): integer threshold for halite to be applied to landsat 8 or 9 where pixels less than threshold are masked
624
+ gypsum_ng_threshold (float, optional): integer threshold for gypsum to be applied to landsat 8 or 9 where pixels less than threshold are masked
412
625
 
413
626
  Returns:
414
627
  image (ee.Image): masked ee.Image
415
628
  """
416
- halite_index = image.normalizedDifference(['SR_B4', 'SR_B6']) # red-swir1 / red+swir1
417
- gypsum_index = image.normalizedDifference(['SR_B6', 'SR_B7'])
629
+ halite_index = image.normalizedDifference(
630
+ ["SR_B4", "SR_B6"]
631
+ ) # red-swir1 / red+swir1
632
+ gypsum_index = image.normalizedDifference(["SR_B6", "SR_B7"])
418
633
  if halite_ng_threshold and gypsum_ng_threshold != None:
419
- mask = ee.Algorithms.If(ee.String(image.get('SPACECRAFT_ID')).equals('LANDSAT_5'), \
420
- gypsum_index.updateMask(halite_index.lt(halite_threshold)).updateMask(gypsum_index.lt(gypsum_threshold)).rename('carbonate_muds').copyProperties(image), \
421
- gypsum_index.updateMask(halite_index.lt(halite_ng_threshold)).updateMask(gypsum_index.lt(gypsum_ng_threshold)).rename('carbonate_muds').copyProperties(image))
634
+ mask = ee.Algorithms.If(
635
+ ee.String(image.get("SPACECRAFT_ID")).equals("LANDSAT_5"),
636
+ gypsum_index.updateMask(halite_index.lt(halite_threshold))
637
+ .updateMask(gypsum_index.lt(gypsum_threshold))
638
+ .rename("carbonate_muds")
639
+ .copyProperties(image),
640
+ gypsum_index.updateMask(halite_index.lt(halite_ng_threshold))
641
+ .updateMask(gypsum_index.lt(gypsum_ng_threshold))
642
+ .rename("carbonate_muds")
643
+ .copyProperties(image),
644
+ )
422
645
  else:
423
- mask = gypsum_index.updateMask(halite_index.lt(halite_threshold)).updateMask(gypsum_index.lt(gypsum_threshold)).rename('carbonate_muds').copyProperties(image)
646
+ mask = (
647
+ gypsum_index.updateMask(halite_index.lt(halite_threshold))
648
+ .updateMask(gypsum_index.lt(gypsum_threshold))
649
+ .rename("carbonate_muds")
650
+ .copyProperties(image)
651
+ )
424
652
  return mask
425
653
 
426
654
  @staticmethod
@@ -436,11 +664,11 @@ class LandsatCollection:
436
664
  """
437
665
  cloudBitMask = ee.Number(2).pow(3).int()
438
666
  CirrusBitMask = ee.Number(2).pow(2).int()
439
- qa = image.select('QA_PIXEL')
667
+ qa = image.select("QA_PIXEL")
440
668
  cloud_mask = qa.bitwiseAnd(cloudBitMask).eq(0)
441
669
  cirrus_mask = qa.bitwiseAnd(CirrusBitMask).eq(0)
442
670
  return image.updateMask(cloud_mask).updateMask(cirrus_mask)
443
-
671
+
444
672
  @staticmethod
445
673
  def temperature_bands(img):
446
674
  """
@@ -452,44 +680,53 @@ class LandsatCollection:
452
680
  Returns:
453
681
  ee.Image: ee.Image
454
682
  """
455
- #date = ee.Number(img.date().format('YYYY-MM-dd'))
456
- scale1 = ['ST_ATRAN', 'ST_EMIS']
457
- scale2 = ['ST_DRAD', 'ST_TRAD', 'ST_URAD']
458
- scale1_names = ['transmittance', 'emissivity']
459
- scale2_names = ['downwelling', 'B10_radiance', 'upwelling']
460
- scale1_bands = img.select(scale1).multiply(0.0001).rename(scale1_names) #Scaled to new L8 collection
461
- scale2_bands = img.select(scale2).multiply(0.001).rename(scale2_names) #Scaled to new L8 collection
683
+ # date = ee.Number(img.date().format('YYYY-MM-dd'))
684
+ scale1 = ["ST_ATRAN", "ST_EMIS"]
685
+ scale2 = ["ST_DRAD", "ST_TRAD", "ST_URAD"]
686
+ scale1_names = ["transmittance", "emissivity"]
687
+ scale2_names = ["downwelling", "B10_radiance", "upwelling"]
688
+ scale1_bands = (
689
+ img.select(scale1).multiply(0.0001).rename(scale1_names)
690
+ ) # Scaled to new L8 collection
691
+ scale2_bands = (
692
+ img.select(scale2).multiply(0.001).rename(scale2_names)
693
+ ) # Scaled to new L8 collection
462
694
  return img.addBands(scale1_bands).addBands(scale2_bands).copyProperties(img)
463
-
695
+
464
696
  @staticmethod
465
697
  def landsat_LST(image):
466
698
  """
467
- Calculates land surface temperature (LST) from landsat TIR bands.
699
+ Calculates land surface temperature (LST) from landsat TIR bands.
468
700
  Based on Sekertekin, A., & Bonafoni, S. (2020) https://doi.org/10.3390/rs12020294
469
701
 
470
702
  Args:
471
703
  image (ee.Image): input ee.Image
472
704
 
473
705
  Returns:
474
- ee.Image: LST ee.Image
706
+ ee.Image: LST ee.Image
475
707
  """
476
708
  # Based on Sekertekin, A., & Bonafoni, S. (2020) https://doi.org/10.3390/rs12020294
477
-
709
+
478
710
  k1 = 774.89
479
711
  k2 = 1321.08
480
712
  LST = image.expression(
481
- '(k2/log((k1/((B10_rad - upwelling - transmittance*(1 - emissivity)*downwelling)/(transmittance*emissivity)))+1)) - 273.15',
482
- {'k1': k1,
483
- 'k2': k2,
484
- 'B10_rad': image.select('B10_radiance'),
485
- 'upwelling': image.select('upwelling'),
486
- 'transmittance': image.select('transmittance'),
487
- 'emissivity': image.select('emissivity'),
488
- 'downwelling': image.select('downwelling')}).rename('LST')
489
- return image.addBands(LST).copyProperties(image) #Outputs temperature in C
490
-
713
+ "(k2/log((k1/((B10_rad - upwelling - transmittance*(1 - emissivity)*downwelling)/(transmittance*emissivity)))+1)) - 273.15",
714
+ {
715
+ "k1": k1,
716
+ "k2": k2,
717
+ "B10_rad": image.select("B10_radiance"),
718
+ "upwelling": image.select("upwelling"),
719
+ "transmittance": image.select("transmittance"),
720
+ "emissivity": image.select("emissivity"),
721
+ "downwelling": image.select("downwelling"),
722
+ },
723
+ ).rename("LST")
724
+ return image.addBands(LST).copyProperties(image) # Outputs temperature in C
725
+
491
726
  @staticmethod
492
- def PixelAreaSum(image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12):
727
+ def PixelAreaSum(
728
+ image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
729
+ ):
493
730
  """
494
731
  Calculates the summation of area for pixels of interest (above a specific threshold) in a geometry
495
732
  and store the value as image property (matching name of chosen band).
@@ -501,27 +738,35 @@ class LandsatCollection:
501
738
  threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1)
502
739
  scale (int): integer scale of image resolution (meters) (defaults to 30)
503
740
  maxPixels (int): integer denoting maximum number of pixels for calculations
504
-
741
+
505
742
  Returns:
506
743
  ee.Image: ee.Image with area calculation stored as property matching name of band
507
744
  """
508
745
  area_image = ee.Image.pixelArea()
509
746
  mask = image.select(band_name).gte(threshold)
510
747
  final = image.addBands(area_image)
511
- stats = final.select('area').updateMask(mask).rename(band_name).reduceRegion(
512
- reducer = ee.Reducer.sum(),
513
- geometry= geometry,
514
- scale=scale,
515
- maxPixels = maxPixels)
748
+ stats = (
749
+ final.select("area")
750
+ .updateMask(mask)
751
+ .rename(band_name)
752
+ .reduceRegion(
753
+ reducer=ee.Reducer.sum(),
754
+ geometry=geometry,
755
+ scale=scale,
756
+ maxPixels=maxPixels,
757
+ )
758
+ )
516
759
  return image.set(band_name, stats.get(band_name))
517
-
518
- def PixelAreaSumCollection(self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12):
760
+
761
+ def PixelAreaSumCollection(
762
+ self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
763
+ ):
519
764
  """
520
- Calculates the summation of area for pixels of interest (above a specific threshold)
765
+ Calculates the summation of area for pixels of interest (above a specific threshold)
521
766
  within a geometry and store the value as image property (matching name of chosen band) for an entire
522
767
  image collection.
523
768
 
524
- The resulting value has units of square meters.
769
+ The resulting value has units of square meters.
525
770
 
526
771
  Args:
527
772
  band_name (string): name of band (string) for calculating area
@@ -529,18 +774,27 @@ class LandsatCollection:
529
774
  threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1)
530
775
  scale (int): integer scale of image resolution (meters) (defaults to 30)
531
776
  maxPixels (int): integer denoting maximum number of pixels for calculations
532
-
777
+
533
778
  Returns:
534
779
  ee.ImageCollection: Image with area calculation stored as property matching name of band.
535
780
  """
536
781
  if self._PixelAreaSumCollection is None:
537
782
  collection = self.collection
538
- AreaCollection = collection.map(lambda image: LandsatCollection.PixelAreaSum(image, band_name=band_name, geometry=geometry, threshold=threshold, scale=scale, maxPixels=maxPixels))
783
+ AreaCollection = collection.map(
784
+ lambda image: LandsatCollection.PixelAreaSum(
785
+ image,
786
+ band_name=band_name,
787
+ geometry=geometry,
788
+ threshold=threshold,
789
+ scale=scale,
790
+ maxPixels=maxPixels,
791
+ )
792
+ )
539
793
  self._PixelAreaSumCollection = AreaCollection
540
794
  return self._PixelAreaSumCollection
541
795
 
542
796
  @staticmethod
543
- def dNDWIPixelAreaSum(image, geometry, band_name='ndwi', scale=30, maxPixels=1e12):
797
+ def dNDWIPixelAreaSum(image, geometry, band_name="ndwi", scale=30, maxPixels=1e12):
544
798
  """
545
799
  Dynamically calulates the summation of area for water pixels of interest and store the value as image property named 'ndwi'
546
800
  Uses Otsu thresholding to dynamically choose the best threshold rather than needing to specify threshold.
@@ -556,9 +810,10 @@ class LandsatCollection:
556
810
  Returns:
557
811
  ee.Image: ee.Image with area calculation stored as property matching name of band
558
812
  """
813
+
559
814
  def OtsuThreshold(histogram):
560
- counts = ee.Array(ee.Dictionary(histogram).get('histogram'))
561
- means = ee.Array(ee.Dictionary(histogram).get('bucketMeans'))
815
+ counts = ee.Array(ee.Dictionary(histogram).get("histogram"))
816
+ means = ee.Array(ee.Dictionary(histogram).get("bucketMeans"))
562
817
  size = means.length().get([0])
563
818
  total = counts.reduce(ee.Reducer.sum(), [0]).get([0])
564
819
  sum = means.multiply(counts).reduce(ee.Reducer.sum(), [0]).get([0])
@@ -578,27 +833,35 @@ class LandsatCollection:
578
833
  bCount = total.subtract(aCount)
579
834
  bMean = sum.subtract(aCount.multiply(aMean)).divide(bCount)
580
835
  return aCount.multiply(aMean.subtract(mean).pow(2)).add(
581
- bCount.multiply(bMean.subtract(mean).pow(2)))
836
+ bCount.multiply(bMean.subtract(mean).pow(2))
837
+ )
582
838
 
583
839
  bss = indices.map(func_xxx)
584
840
  return means.sort(bss).get([-1])
585
841
 
586
842
  area_image = ee.Image.pixelArea()
587
843
  histogram = image.select(band_name).reduceRegion(
588
- reducer = ee.Reducer.histogram(255, 2),
589
- geometry = geometry.geometry().buffer(6000),
590
- scale = scale,
591
- bestEffort= True,)
844
+ reducer=ee.Reducer.histogram(255, 2),
845
+ geometry=geometry.geometry().buffer(6000),
846
+ scale=scale,
847
+ bestEffort=True,
848
+ )
592
849
  threshold = OtsuThreshold(histogram.get(band_name)).add(0.15)
593
850
  mask = image.select(band_name).gte(threshold)
594
851
  final = image.addBands(area_image)
595
- stats = final.select('area').updateMask(mask).rename(band_name).reduceRegion(
596
- reducer = ee.Reducer.sum(),
597
- geometry= geometry,
598
- scale=scale,
599
- maxPixels = maxPixels)
852
+ stats = (
853
+ final.select("area")
854
+ .updateMask(mask)
855
+ .rename(band_name)
856
+ .reduceRegion(
857
+ reducer=ee.Reducer.sum(),
858
+ geometry=geometry,
859
+ scale=scale,
860
+ maxPixels=maxPixels,
861
+ )
862
+ )
600
863
  return image.set(band_name, stats.get(band_name))
601
-
864
+
602
865
  @property
603
866
  def dates_list(self):
604
867
  """
@@ -608,7 +871,7 @@ class LandsatCollection:
608
871
  ee.List: Server-side ee.List of dates.
609
872
  """
610
873
  if self._dates_list is None:
611
- dates = self.collection.aggregate_array('Date_Filter')
874
+ dates = self.collection.aggregate_array("Date_Filter")
612
875
  self._dates_list = dates
613
876
  return self._dates_list
614
877
 
@@ -621,7 +884,7 @@ class LandsatCollection:
621
884
  list: list of date strings.
622
885
  """
623
886
  if self._dates_list is None:
624
- dates = self.collection.aggregate_array('Date_Filter')
887
+ dates = self.collection.aggregate_array("Date_Filter")
625
888
  self._dates_list = dates
626
889
  if self._dates is None:
627
890
  dates = self._dates_list.getInfo()
@@ -637,11 +900,25 @@ class LandsatCollection:
637
900
  """
638
901
  landsat8 = ee.ImageCollection("LANDSAT/LC08/C02/T1_L2")
639
902
  landsat9 = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")
640
- landsat5 = ee.ImageCollection("LANDSAT/LT05/C02/T1_L2").map(LandsatCollection.landsat5bandrename) # Replace with the correct Landsat 5 collection ID
641
- filtered_collection = landsat8.merge(landsat9).merge(landsat5).filterDate(self.start_date, self.end_date).filter(ee.Filter.And(ee.Filter.inList('WRS_PATH', self.tile_path),
642
- ee.Filter.inList('WRS_ROW', self.tile_row))).filter(ee.Filter.lte('CLOUD_COVER', self.cloud_percentage_threshold)).map(LandsatCollection.image_dater).sort('Date_Filter')
903
+ landsat5 = ee.ImageCollection("LANDSAT/LT05/C02/T1_L2").map(
904
+ LandsatCollection.landsat5bandrename
905
+ ) # Replace with the correct Landsat 5 collection ID
906
+ filtered_collection = (
907
+ landsat8.merge(landsat9)
908
+ .merge(landsat5)
909
+ .filterDate(self.start_date, self.end_date)
910
+ .filter(
911
+ ee.Filter.And(
912
+ ee.Filter.inList("WRS_PATH", self.tile_path),
913
+ ee.Filter.inList("WRS_ROW", self.tile_row),
914
+ )
915
+ )
916
+ .filter(ee.Filter.lte("CLOUD_COVER", self.cloud_percentage_threshold))
917
+ .map(LandsatCollection.image_dater)
918
+ .sort("Date_Filter")
919
+ )
643
920
  return filtered_collection
644
-
921
+
645
922
  def get_boundary_filtered_collection(self):
646
923
  """
647
924
  Filters and masks image collection based on LandsatCollection class arguments. Automatically calculated when using collection method, depending on provided class arguments (when boundary info is provided).
@@ -652,10 +929,33 @@ class LandsatCollection:
652
929
  """
653
930
  landsat8 = ee.ImageCollection("LANDSAT/LC08/C02/T1_L2")
654
931
  landsat9 = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")
655
- landsat5 = ee.ImageCollection("LANDSAT/LT05/C02/T1_L2").map(LandsatCollection.landsat5bandrename) # Replace with the correct Landsat 5 collection ID
656
- filtered_collection = landsat8.merge(landsat9).merge(landsat5).filterDate(self.start_date, self.end_date).filterBounds(self.boundary).filter(ee.Filter.lte('CLOUD_COVER', self.cloud_percentage_threshold)).map(LandsatCollection.image_dater).sort('Date_Filter')
932
+ landsat5 = ee.ImageCollection("LANDSAT/LT05/C02/T1_L2").map(
933
+ LandsatCollection.landsat5bandrename
934
+ ) # Replace with the correct Landsat 5 collection ID
935
+ filtered_collection = (
936
+ landsat8.merge(landsat9)
937
+ .merge(landsat5)
938
+ .filterDate(self.start_date, self.end_date)
939
+ .filterBounds(self.boundary)
940
+ .filter(ee.Filter.lte("CLOUD_COVER", self.cloud_percentage_threshold))
941
+ .map(LandsatCollection.image_dater)
942
+ .sort("Date_Filter")
943
+ )
657
944
  return filtered_collection
658
945
 
946
+ @property
947
+ def scale_to_reflectance(self):
948
+ """
949
+ Scales each band in the Landsat collection from DN values to surface reflectance values.
950
+
951
+ Returns:
952
+ LandsatCollection: A new LandsatCollection object with bands scaled to reflectance.
953
+ """
954
+ if self._Reflectance is None:
955
+ self._Reflectance = self.collection.map(_scale_landsat_sr)
956
+ return LandsatCollection(collection=self._Reflectance)
957
+
958
+
659
959
  @property
660
960
  def median(self):
661
961
  """
@@ -668,7 +968,7 @@ class LandsatCollection:
668
968
  col = self.collection.median()
669
969
  self._median = col
670
970
  return self._median
671
-
971
+
672
972
  @property
673
973
  def mean(self):
674
974
  """
@@ -682,7 +982,7 @@ class LandsatCollection:
682
982
  col = self.collection.mean()
683
983
  self._mean = col
684
984
  return self._mean
685
-
985
+
686
986
  @property
687
987
  def max(self):
688
988
  """
@@ -695,7 +995,7 @@ class LandsatCollection:
695
995
  col = self.collection.max()
696
996
  self._max = col
697
997
  return self._max
698
-
998
+
699
999
  @property
700
1000
  def min(self):
701
1001
  """
@@ -708,28 +1008,43 @@ class LandsatCollection:
708
1008
  col = self.collection.min()
709
1009
  self._min = col
710
1010
  return self._min
711
-
1011
+
712
1012
  @property
713
1013
  def ndwi(self):
714
1014
  """
715
- Property attribute to calculate and access the NDWI (Normalized Difference Water Index) imagery of the LandsatCollection.
716
- This property initiates the calculation of NDWI using a default threshold of -1 (or a previously set threshold of self.ndwi_threshold)
717
- and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
1015
+ Property attribute to calculate and access the NDWI (Normalized Difference Water Index) imagery of the LandsatCollection.
1016
+ This property initiates the calculation of NDWI using a default threshold of -1 (or a previously set threshold of self.ndwi_threshold)
1017
+ and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
718
1018
  on subsequent accesses.
719
1019
 
720
1020
  Returns:
721
- LandsatCollection: A LandsatCollection image collection
1021
+ LandsatCollection: A LandsatCollection image collection
722
1022
  """
723
1023
  if self._ndwi is None:
724
1024
  self._ndwi = self.ndwi_collection(self.ndwi_threshold)
725
1025
  return self._ndwi
726
1026
 
1027
+ @property
1028
+ def mndwi(self):
1029
+ """
1030
+ Property attribute to calculate and access the MNDWI (Modified Normalized Difference Water Index) imagery of the LandsatCollection.
1031
+ This property initiates the calculation of MNDWI using a default threshold of -1 (or a previously set threshold of self.mndwi_threshold)
1032
+ and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
1033
+ on subsequent accesses.
1034
+
1035
+ Returns:
1036
+ LandsatCollection: A LandsatCollection image collection
1037
+ """
1038
+ if self._mndwi is None:
1039
+ self._mndwi = self.mndwi_collection(self.mndwi_threshold)
1040
+ return self._mndwi
1041
+
727
1042
  def ndwi_collection(self, threshold, ng_threshold=None):
728
1043
  """
729
1044
  Calculates ndwi and returns collection as class object, allows specifying threshold(s) for masking.
730
- Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
731
- and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
732
- by default when using the ndwi property attribute.
1045
+ Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
1046
+ and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
1047
+ by default when using the ndwi property attribute.
733
1048
 
734
1049
  Args:
735
1050
  threshold (float): specify threshold for NDWI function (values less than threshold are masked)
@@ -740,23 +1055,54 @@ class LandsatCollection:
740
1055
  first_image = self.collection.first()
741
1056
  available_bands = first_image.bandNames()
742
1057
 
743
- if available_bands.contains('SR_B3') and available_bands.contains('SR_B5'):
1058
+ if available_bands.contains("SR_B3") and available_bands.contains("SR_B5"):
744
1059
  pass
745
1060
  else:
746
1061
  raise ValueError("Insufficient Bands for ndwi calculation")
747
- col = self.collection.map(lambda image: LandsatCollection.landsat_ndwi_fn(image, threshold=threshold, ng_threshold=ng_threshold))
1062
+ col = self.collection.map(
1063
+ lambda image: LandsatCollection.landsat_ndwi_fn(
1064
+ image, threshold=threshold, ng_threshold=ng_threshold
1065
+ )
1066
+ )
748
1067
  return LandsatCollection(collection=col)
749
1068
 
1069
+ def mndwi_collection(self, threshold, ng_threshold=None):
1070
+ """
1071
+ Calculates mndwi and returns collection as class object, allows specifying threshold(s) for masking.
1072
+ Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
1073
+ and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
1074
+ by default when using the mndwi property attribute.
1075
+
1076
+ Args:
1077
+ threshold (float): specify threshold for MNDWI function (values less than threshold are masked)
1078
+
1079
+ Returns:
1080
+ LandsatCollection: A LandsatCollection image collection
1081
+ """
1082
+ first_image = self.collection.first()
1083
+ available_bands = first_image.bandNames()
1084
+
1085
+ if available_bands.contains("SR_B3") and available_bands.contains("SR_B6"):
1086
+ pass
1087
+ else:
1088
+ raise ValueError("Insufficient bands for mndwi calculation")
1089
+ col = self.collection.map(
1090
+ lambda image: LandsatCollection.landsat_mndwi_fn(
1091
+ image, threshold=threshold, ng_threshold=ng_threshold
1092
+ )
1093
+ )
1094
+ return LandsatCollection(collection=col)
1095
+
750
1096
  @property
751
1097
  def ndvi(self):
752
1098
  """
753
- Property attribute to calculate and access the NDVI (Normalized Difference Vegetation Index) imagery of the LandsatCollection.
754
- This property initiates the calculation of NDVI using a default threshold of -1 (or a previously set threshold of self.ndvi_threshold)
755
- and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
1099
+ Property attribute to calculate and access the NDVI (Normalized Difference Vegetation Index) imagery of the LandsatCollection.
1100
+ This property initiates the calculation of NDVI using a default threshold of -1 (or a previously set threshold of self.ndvi_threshold)
1101
+ and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
756
1102
  on subsequent accesses.
757
1103
 
758
1104
  Returns:
759
- LandsatCollection: A LandsatCollection image collection
1105
+ LandsatCollection: A LandsatCollection image collection
760
1106
  """
761
1107
  if self._ndvi is None:
762
1108
  self._ndvi = self.ndvi_collection(self.ndvi_threshold)
@@ -765,35 +1111,39 @@ class LandsatCollection:
765
1111
  def ndvi_collection(self, threshold, ng_threshold=None):
766
1112
  """
767
1113
  Function to calculate the NDVI (Normalized Difference Vegetation Index) and return collection as class object, allows specifying threshold(s) for masking.
768
- Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
769
- and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
1114
+ Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
1115
+ and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
770
1116
  by default when using the ndwi property attribute.
771
1117
 
772
1118
  Args:
773
1119
  threshold (float): specify threshold for NDVI function (values less than threshold are masked)
774
1120
 
775
1121
  Returns:
776
- LandsatCollection: A LandsatCollection image collection
1122
+ LandsatCollection: A LandsatCollection image collection
777
1123
  """
778
1124
  first_image = self.collection.first()
779
1125
  available_bands = first_image.bandNames()
780
- if available_bands.contains('SR_B4') and available_bands.contains('SR_B5'):
1126
+ if available_bands.contains("SR_B4") and available_bands.contains("SR_B5"):
781
1127
  pass
782
1128
  else:
783
1129
  raise ValueError("Insufficient Bands for ndwi calculation")
784
- col = self.collection.map(lambda image: LandsatCollection.landsat_ndvi_fn(image, threshold=threshold, ng_threshold=ng_threshold))
1130
+ col = self.collection.map(
1131
+ lambda image: LandsatCollection.landsat_ndvi_fn(
1132
+ image, threshold=threshold, ng_threshold=ng_threshold
1133
+ )
1134
+ )
785
1135
  return LandsatCollection(collection=col)
786
1136
 
787
1137
  @property
788
1138
  def halite(self):
789
1139
  """
790
- Property attribute to calculate and access the halite index (see Radwin & Bowen, 2021) imagery of the LandsatCollection.
791
- This property initiates the calculation of halite using a default threshold of -1 (or a previously set threshold of self.halite_threshold)
792
- and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
1140
+ Property attribute to calculate and access the halite index (see Radwin & Bowen, 2021) imagery of the LandsatCollection.
1141
+ This property initiates the calculation of halite using a default threshold of -1 (or a previously set threshold of self.halite_threshold)
1142
+ and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
793
1143
  on subsequent accesses.
794
1144
 
795
1145
  Returns:
796
- LandsatCollection: A LandsatCollection image collection
1146
+ LandsatCollection: A LandsatCollection image collection
797
1147
  """
798
1148
  if self._halite is None:
799
1149
  self._halite = self.halite_collection(self.halite_threshold)
@@ -802,8 +1152,8 @@ class LandsatCollection:
802
1152
  def halite_collection(self, threshold, ng_threshold=None):
803
1153
  """
804
1154
  Function to calculate the halite index (see Radwin & Bowen, 2021) and return collection as class object, allows specifying threshold(s) for masking.
805
- Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
806
- and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
1155
+ Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
1156
+ and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
807
1157
  by default when using the ndwi property attribute.
808
1158
 
809
1159
  Args:
@@ -811,27 +1161,31 @@ class LandsatCollection:
811
1161
  ng_threshold (float, optional): specify threshold for Landsat 8&9 halite function (values less than threshold are masked)
812
1162
 
813
1163
  Returns:
814
- LandsatCollection: A LandsatCollection image collection
1164
+ LandsatCollection: A LandsatCollection image collection
815
1165
  """
816
1166
  first_image = self.collection.first()
817
1167
  available_bands = first_image.bandNames()
818
- if available_bands.contains('SR_B4') and available_bands.contains('SR_B6'):
1168
+ if available_bands.contains("SR_B4") and available_bands.contains("SR_B6"):
819
1169
  pass
820
1170
  else:
821
1171
  raise ValueError("Insufficient Bands for halite calculation")
822
- col = self.collection.map(lambda image: LandsatCollection.landsat_halite_fn(image, threshold=threshold, ng_threshold=ng_threshold))
1172
+ col = self.collection.map(
1173
+ lambda image: LandsatCollection.landsat_halite_fn(
1174
+ image, threshold=threshold, ng_threshold=ng_threshold
1175
+ )
1176
+ )
823
1177
  return LandsatCollection(collection=col)
824
1178
 
825
1179
  @property
826
1180
  def gypsum(self):
827
1181
  """
828
- Property attribute to calculate and access the gypsum/sulfate index (see Radwin & Bowen, 2021) imagery of the LandsatCollection.
829
- This property initiates the calculation of gypsum using a default threshold of -1 (or a previously set threshold of self.gypsum_threshold)
830
- and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
1182
+ Property attribute to calculate and access the gypsum/sulfate index (see Radwin & Bowen, 2021) imagery of the LandsatCollection.
1183
+ This property initiates the calculation of gypsum using a default threshold of -1 (or a previously set threshold of self.gypsum_threshold)
1184
+ and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
831
1185
  on subsequent accesses.
832
1186
 
833
1187
  Returns:
834
- LandsatCollection: A LandsatCollection image collection
1188
+ LandsatCollection: A LandsatCollection image collection
835
1189
  """
836
1190
  if self._gypsum is None:
837
1191
  self._gypsum = self.gypsum_collection(self.gypsum_threshold)
@@ -840,8 +1194,8 @@ class LandsatCollection:
840
1194
  def gypsum_collection(self, threshold, ng_threshold=None):
841
1195
  """
842
1196
  Function to calculate the gypsum index (see Radwin & Bowen, 2021) and return collection as class object, allows specifying threshold(s) for masking.
843
- Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
844
- and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
1197
+ Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
1198
+ and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
845
1199
  by default when using the ndwi property attribute.
846
1200
 
847
1201
  Args:
@@ -849,27 +1203,31 @@ class LandsatCollection:
849
1203
  ng_threshold (float, optional): specify threshold for Landsat 8&9 gypsum function (values less than threshold are masked)
850
1204
 
851
1205
  Returns:
852
- LandsatCollection: A LandsatCollection image collection
1206
+ LandsatCollection: A LandsatCollection image collection
853
1207
  """
854
1208
  first_image = self.collection.first()
855
1209
  available_bands = first_image.bandNames()
856
- if available_bands.contains('SR_B6') and available_bands.contains('SR_B7'):
1210
+ if available_bands.contains("SR_B6") and available_bands.contains("SR_B7"):
857
1211
  pass
858
1212
  else:
859
1213
  raise ValueError("Insufficient Bands for gypsum calculation")
860
- col = self.collection.map(lambda image: LandsatCollection.landsat_gypsum_fn(image, threshold=threshold, ng_threshold=ng_threshold))
1214
+ col = self.collection.map(
1215
+ lambda image: LandsatCollection.landsat_gypsum_fn(
1216
+ image, threshold=threshold, ng_threshold=ng_threshold
1217
+ )
1218
+ )
861
1219
  return LandsatCollection(collection=col)
862
-
1220
+
863
1221
  @property
864
1222
  def turbidity(self):
865
1223
  """
866
- Property attribute to calculate and access the turbidity (NDTI) imagery of the LandsatCollection.
867
- This property initiates the calculation of turbidity using a default threshold of -1 (or a previously set threshold of self.turbidity_threshold)
868
- and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
1224
+ Property attribute to calculate and access the turbidity (NDTI) imagery of the LandsatCollection.
1225
+ This property initiates the calculation of turbidity using a default threshold of -1 (or a previously set threshold of self.turbidity_threshold)
1226
+ and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
869
1227
  on subsequent accesses.
870
1228
 
871
1229
  Returns:
872
- LandsatCollection: A LandsatCollection image collection
1230
+ LandsatCollection: A LandsatCollection image collection
873
1231
  """
874
1232
  if self._turbidity is None:
875
1233
  self._turbidity = self.turbidity_collection(self.turbidity_threshold)
@@ -878,8 +1236,8 @@ class LandsatCollection:
878
1236
  def turbidity_collection(self, threshold, ng_threshold=None):
879
1237
  """
880
1238
  Calculates the turbidity (NDTI) index and return collection as class object, allows specifying threshold(s) for masking.
881
- Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
882
- and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
1239
+ Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
1240
+ and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
883
1241
  by default when using the ndwi property attribute.
884
1242
 
885
1243
  Args:
@@ -887,28 +1245,32 @@ class LandsatCollection:
887
1245
  ng_threshold (float, optional): specify threshold for Landsat 8&9 turbidity function (values less than threshold are masked)
888
1246
 
889
1247
  Returns:
890
- LandsatCollection: A LandsatCollection image collection
1248
+ LandsatCollection: A LandsatCollection image collection
891
1249
  """
892
1250
  first_image = self.collection.first()
893
1251
  available_bands = first_image.bandNames()
894
- if available_bands.contains('SR_B4') and available_bands.contains('SR_B3'):
1252
+ if available_bands.contains("SR_B4") and available_bands.contains("SR_B3"):
895
1253
  pass
896
1254
  else:
897
1255
  raise ValueError("Insufficient Bands for turbidity calculation")
898
- col = self.collection.map(lambda image: LandsatCollection.landsat_ndti_fn(image, threshold=threshold, ng_threshold=ng_threshold))
1256
+ col = self.collection.map(
1257
+ lambda image: LandsatCollection.landsat_ndti_fn(
1258
+ image, threshold=threshold, ng_threshold=ng_threshold
1259
+ )
1260
+ )
899
1261
 
900
1262
  return LandsatCollection(collection=col)
901
-
1263
+
902
1264
  @property
903
1265
  def chlorophyll(self):
904
1266
  """
905
- Property attribute to calculate and access the chlorophyll (NDTI) imagery of the LandsatCollection.
906
- This property initiates the calculation of chlorophyll using a default threshold of -1 (or a previously set threshold of self.chlorophyll_threshold)
907
- and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
1267
+ Property attribute to calculate and access the chlorophyll (NDTI) imagery of the LandsatCollection.
1268
+ This property initiates the calculation of chlorophyll using a default threshold of -1 (or a previously set threshold of self.chlorophyll_threshold)
1269
+ and caches the result. The calculation is performed only once when the property is first accessed, and the cached result is returned
908
1270
  on subsequent accesses.
909
1271
 
910
1272
  Returns:
911
- LandsatCollection: A LandsatCollection image collection
1273
+ LandsatCollection: A LandsatCollection image collection
912
1274
  """
913
1275
  if self._chlorophyll is None:
914
1276
  self._chlorophyll = self.chlorophyll_collection(self.chlorophyll_threshold)
@@ -917,8 +1279,8 @@ class LandsatCollection:
917
1279
  def chlorophyll_collection(self, threshold, ng_threshold=None):
918
1280
  """
919
1281
  Calculates the KIVU chlorophyll index and return collection as class object, allows specifying threshold(s) for masking.
920
- Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
921
- and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
1282
+ Thresholds can be specified for Landsat 5 vs 8&9 images, where the threshold argument applies to Landsat 5
1283
+ and the ng_threshold argument applies to Landsat 8&9. This function can be called as a method but is called
922
1284
  by default when using the ndwi property attribute.
923
1285
 
924
1286
  Args:
@@ -930,26 +1292,34 @@ class LandsatCollection:
930
1292
  """
931
1293
  first_image = self.collection.first()
932
1294
  available_bands = first_image.bandNames()
933
- if available_bands.contains('SR_B4') and available_bands.contains('SR_B3') and available_bands.contains('SR_B2'):
1295
+ if (
1296
+ available_bands.contains("SR_B4")
1297
+ and available_bands.contains("SR_B3")
1298
+ and available_bands.contains("SR_B2")
1299
+ ):
934
1300
  pass
935
1301
  else:
936
1302
  raise ValueError("Insufficient Bands for chlorophyll calculation")
937
- col = self.collection.map(lambda image: LandsatCollection.landsat_kivu_chla_fn(image, threshold=threshold, ng_threshold=ng_threshold))
1303
+ col = self.collection.map(
1304
+ lambda image: LandsatCollection.landsat_kivu_chla_fn(
1305
+ image, threshold=threshold, ng_threshold=ng_threshold
1306
+ )
1307
+ )
938
1308
  return LandsatCollection(collection=col)
939
-
1309
+
940
1310
  @property
941
1311
  def masked_water_collection(self):
942
1312
  """
943
1313
  Property attribute to mask water and return collection as class object.
944
1314
 
945
1315
  Returns:
946
- LandsatCollection: LandsatCollection image collection
1316
+ LandsatCollection: LandsatCollection image collection
947
1317
  """
948
1318
  if self._masked_water_collection is None:
949
1319
  col = self.collection.map(LandsatCollection.MaskWaterLandsat)
950
1320
  self._masked_water_collection = LandsatCollection(collection=col)
951
1321
  return self._masked_water_collection
952
-
1322
+
953
1323
  def masked_water_collection_NDWI(self, threshold):
954
1324
  """
955
1325
  Masks water pixels based on NDWI and user set threshold.
@@ -958,11 +1328,15 @@ class LandsatCollection:
958
1328
  threshold (float): specify threshold for NDWI function (values greater than threshold are masked)
959
1329
 
960
1330
  Returns:
961
- LandsatCollection: LandsatCollection image collection
1331
+ LandsatCollection: LandsatCollection image collection
962
1332
  """
963
- col = self.collection.map(lambda image: LandsatCollection.MaskWaterLandsatByNDWI(image, threshold=threshold))
1333
+ col = self.collection.map(
1334
+ lambda image: LandsatCollection.MaskWaterLandsatByNDWI(
1335
+ image, threshold=threshold
1336
+ )
1337
+ )
964
1338
  return LandsatCollection(collection=col)
965
-
1339
+
966
1340
  @property
967
1341
  def masked_to_water_collection(self):
968
1342
  """
@@ -975,7 +1349,7 @@ class LandsatCollection:
975
1349
  col = self.collection.map(LandsatCollection.MaskToWaterLandsat)
976
1350
  self._masked_to_water_collection = LandsatCollection(collection=col)
977
1351
  return self._masked_to_water_collection
978
-
1352
+
979
1353
  def masked_to_water_collection_NDWI(self, threshold):
980
1354
  """
981
1355
  Function to mask all but water pixels based on NDWI and user set threshold.
@@ -984,29 +1358,33 @@ class LandsatCollection:
984
1358
  threshold (float): specify threshold for NDWI function (values less than threshold are masked)
985
1359
 
986
1360
  Returns:
987
- LandsatCollection: LandsatCollection image collection
1361
+ LandsatCollection: LandsatCollection image collection
988
1362
  """
989
- col = self.collection.map(lambda image: LandsatCollection.MaskToWaterLandsatByNDWI(image, threshold=threshold))
1363
+ col = self.collection.map(
1364
+ lambda image: LandsatCollection.MaskToWaterLandsatByNDWI(
1365
+ image, threshold=threshold
1366
+ )
1367
+ )
990
1368
  return LandsatCollection(collection=col)
991
-
1369
+
992
1370
  @property
993
1371
  def masked_clouds_collection(self):
994
1372
  """
995
1373
  Property attribute to mask clouds and return collection as class object.
996
1374
 
997
1375
  Returns:
998
- LandsatCollection: LandsatCollection image collection
1376
+ LandsatCollection: LandsatCollection image collection
999
1377
  """
1000
1378
  if self._masked_clouds_collection is None:
1001
1379
  col = self.collection.map(LandsatCollection.maskL8clouds)
1002
1380
  self._masked_clouds_collection = LandsatCollection(collection=col)
1003
1381
  return self._masked_clouds_collection
1004
-
1382
+
1005
1383
  @property
1006
1384
  def LST(self):
1007
1385
  """
1008
- Property attribute to calculate and access the LST (Land Surface Temperature - in Celcius) imagery of the LandsatCollection.
1009
- This property initiates the calculation of LST and caches the result. The calculation is performed only once
1386
+ Property attribute to calculate and access the LST (Land Surface Temperature - in Celcius) imagery of the LandsatCollection.
1387
+ This property initiates the calculation of LST and caches the result. The calculation is performed only once
1010
1388
  when the property is first accessed, and the cached result is returned on subsequent accesses.
1011
1389
 
1012
1390
  Returns:
@@ -1015,7 +1393,7 @@ class LandsatCollection:
1015
1393
  if self._LST is None:
1016
1394
  self._LST = self.surface_temperature_collection()
1017
1395
  return self._LST
1018
-
1396
+
1019
1397
  def surface_temperature_collection(self):
1020
1398
  """
1021
1399
  Function to calculate LST (Land Surface Temperature - in Celcius) and return collection as class object.
@@ -1025,13 +1403,23 @@ class LandsatCollection:
1025
1403
  """
1026
1404
  first_image = self.collection.first()
1027
1405
  available_bands = first_image.bandNames()
1028
- if available_bands.contains('ST_ATRAN') and available_bands.contains('ST_EMIS') and available_bands.contains('ST_DRAD') and available_bands.contains('ST_TRAD') and available_bands.contains('ST_URAD') :
1406
+ if (
1407
+ available_bands.contains("ST_ATRAN")
1408
+ and available_bands.contains("ST_EMIS")
1409
+ and available_bands.contains("ST_DRAD")
1410
+ and available_bands.contains("ST_TRAD")
1411
+ and available_bands.contains("ST_URAD")
1412
+ ):
1029
1413
  pass
1030
1414
  else:
1031
1415
  raise ValueError("Insufficient Bands for temperature calculation")
1032
- col = self.collection.map(LandsatCollection.temperature_bands).map(LandsatCollection.landsat_LST).map(LandsatCollection.image_dater)
1416
+ col = (
1417
+ self.collection.map(LandsatCollection.temperature_bands)
1418
+ .map(LandsatCollection.landsat_LST)
1419
+ .map(LandsatCollection.image_dater)
1420
+ )
1033
1421
  return LandsatCollection(collection=col)
1034
-
1422
+
1035
1423
  def mask_to_polygon(self, polygon):
1036
1424
  """
1037
1425
  Function to mask LandsatCollection image collection by a polygon (ee.Geometry), where pixels outside the polygon are masked out.
@@ -1041,21 +1429,23 @@ class LandsatCollection:
1041
1429
 
1042
1430
  Returns:
1043
1431
  LandsatCollection: masked LandsatCollection image collection
1044
-
1432
+
1045
1433
  """
1046
1434
  if self._geometry_masked_collection is None:
1047
1435
  # Convert the polygon to a mask
1048
1436
  mask = ee.Image.constant(1).clip(polygon)
1049
-
1437
+
1050
1438
  # Update the mask of each image in the collection
1051
1439
  masked_collection = self.collection.map(lambda img: img.updateMask(mask))
1052
-
1440
+
1053
1441
  # Update the internal collection state
1054
- self._geometry_masked_collection = LandsatCollection(collection=masked_collection)
1055
-
1442
+ self._geometry_masked_collection = LandsatCollection(
1443
+ collection=masked_collection
1444
+ )
1445
+
1056
1446
  # Return the updated object
1057
1447
  return self._geometry_masked_collection
1058
-
1448
+
1059
1449
  def mask_out_polygon(self, polygon):
1060
1450
  """
1061
1451
  Function to mask LandsatCollection image collection by a polygon (ee.Geometry), where pixels inside the polygon are masked out.
@@ -1065,7 +1455,7 @@ class LandsatCollection:
1065
1455
 
1066
1456
  Returns:
1067
1457
  LandsatCollection: masked LandsatCollection image collection
1068
-
1458
+
1069
1459
  """
1070
1460
  if self._geometry_masked_out_collection is None:
1071
1461
  # Convert the polygon to a mask
@@ -1073,19 +1463,21 @@ class LandsatCollection:
1073
1463
 
1074
1464
  # Use paint to set pixels inside polygon as 0
1075
1465
  area = full_mask.paint(polygon, 0)
1076
-
1466
+
1077
1467
  # Update the mask of each image in the collection
1078
1468
  masked_collection = self.collection.map(lambda img: img.updateMask(area))
1079
-
1469
+
1080
1470
  # Update the internal collection state
1081
- self._geometry_masked_out_collection = LandsatCollection(collection=masked_collection)
1082
-
1471
+ self._geometry_masked_out_collection = LandsatCollection(
1472
+ collection=masked_collection
1473
+ )
1474
+
1083
1475
  # Return the updated object
1084
1476
  return self._geometry_masked_out_collection
1085
1477
 
1086
1478
  def mask_halite(self, threshold, ng_threshold=None):
1087
1479
  """
1088
- Masks halite and returns collection as class object. Can specify separate thresholds for Landsat 5 vs 8&9 images
1480
+ Masks halite and returns collection as class object. Can specify separate thresholds for Landsat 5 vs 8&9 images
1089
1481
  where the threshold argument applies to Landsat 5 and the ng_threshold argument applies to Landsat 8&9.
1090
1482
 
1091
1483
  Args:
@@ -1095,12 +1487,22 @@ class LandsatCollection:
1095
1487
  Returns:
1096
1488
  LandsatCollection: LandsatCollection image collection
1097
1489
  """
1098
- col = self.collection.map(lambda image: LandsatCollection.halite_mask(image, threshold=threshold, ng_threshold=ng_threshold))
1490
+ col = self.collection.map(
1491
+ lambda image: LandsatCollection.halite_mask(
1492
+ image, threshold=threshold, ng_threshold=ng_threshold
1493
+ )
1494
+ )
1099
1495
  return LandsatCollection(collection=col)
1100
-
1101
- def mask_halite_and_gypsum(self, halite_threshold, gypsum_threshold, halite_ng_threshold=None, gypsum_ng_threshold=None):
1496
+
1497
+ def mask_halite_and_gypsum(
1498
+ self,
1499
+ halite_threshold,
1500
+ gypsum_threshold,
1501
+ halite_ng_threshold=None,
1502
+ gypsum_ng_threshold=None,
1503
+ ):
1102
1504
  """
1103
- Masks halite and gypsum and returns collection as class object.
1505
+ Masks halite and gypsum and returns collection as class object.
1104
1506
  Can specify separate thresholds for Landsat 5 vs 8&9 images where the threshold argument applies to Landsat 5
1105
1507
  and the ng_threshold argument applies to Landsat 8&9.
1106
1508
 
@@ -1111,9 +1513,48 @@ class LandsatCollection:
1111
1513
  gypsum_ng_threshold (float, optional): specify threshold for Landsat 8&9 gypsum function (values less than threshold are masked)
1112
1514
 
1113
1515
  Returns:
1114
- LandsatCollection: LandsatCollection image collection
1516
+ LandsatCollection: LandsatCollection image collection
1115
1517
  """
1116
- col = self.collection.map(lambda image: LandsatCollection.gypsum_and_halite_mask(image, halite_threshold=halite_threshold, gypsum_threshold=gypsum_threshold, halite_ng_threshold=halite_ng_threshold, gypsum_ng_threshold=gypsum_ng_threshold))
1518
+ col = self.collection.map(
1519
+ lambda image: LandsatCollection.gypsum_and_halite_mask(
1520
+ image,
1521
+ halite_threshold=halite_threshold,
1522
+ gypsum_threshold=gypsum_threshold,
1523
+ halite_ng_threshold=halite_ng_threshold,
1524
+ gypsum_ng_threshold=gypsum_ng_threshold,
1525
+ )
1526
+ )
1527
+ return LandsatCollection(collection=col)
1528
+
1529
+ def binary_mask(self, threshold=None, band_name=None):
1530
+ """
1531
+ Function to create a binary mask (value of 1 for pixels above set threshold and value of 0 for all other pixels) of the LandsatCollection image collection based on a specified band.
1532
+ If a singleband image is provided, the band name is automatically determined.
1533
+ If multiple bands are available, the user must specify the band name to use for masking.
1534
+
1535
+ Args:
1536
+ band_name (str, optional): The name of the band to use for masking. Defaults to None.
1537
+
1538
+ Returns:
1539
+ LandsatCollection: LandsatCollection singleband image collection with binary masks applied.
1540
+ """
1541
+ if self.collection.size().eq(0).getInfo():
1542
+ raise ValueError("The collection is empty. Cannot create a binary mask.")
1543
+ if band_name is None:
1544
+ first_image = self.collection.first()
1545
+ band_names = first_image.bandNames()
1546
+ if band_names.size().getInfo() == 0:
1547
+ raise ValueError("No bands available in the collection.")
1548
+ if band_names.size().getInfo() > 1:
1549
+ raise ValueError("Multiple bands available, please specify a band name.")
1550
+ else:
1551
+ band_name = band_names.get(0).getInfo()
1552
+ if threshold is None:
1553
+ raise ValueError("Threshold must be specified for binary masking.")
1554
+
1555
+ col = self.collection.map(
1556
+ lambda image: image.select(band_name).gte(threshold).rename(band_name)
1557
+ )
1117
1558
  return LandsatCollection(collection=col)
1118
1559
 
1119
1560
  def image_grab(self, img_selector):
@@ -1122,7 +1563,7 @@ class LandsatCollection:
1122
1563
 
1123
1564
  Args:
1124
1565
  img_selector: index of image in the collection for which user seeks to select/"grab".
1125
-
1566
+
1126
1567
  Returns:
1127
1568
  ee.Image: ee.Image of selected image
1128
1569
  """
@@ -1141,7 +1582,7 @@ class LandsatCollection:
1141
1582
  Args:
1142
1583
  img_col: ee.ImageCollection with same dates as another LandsatCollection image collection object.
1143
1584
  img_selector: index of image in list of dates for which user seeks to "select".
1144
-
1585
+
1145
1586
  Returns:
1146
1587
  ee.Image: ee.Image of selected image
1147
1588
  """
@@ -1152,7 +1593,7 @@ class LandsatCollection:
1152
1593
  image = ee.Image(image_list.get(img_selector))
1153
1594
 
1154
1595
  return image
1155
-
1596
+
1156
1597
  def image_pick(self, img_date):
1157
1598
  """
1158
1599
  Selects ("grabs") image of a specific date in format of 'YYYY-MM-DD' - will not work correctly if collection is composed of multiple images of the same date.
@@ -1163,13 +1604,13 @@ class LandsatCollection:
1163
1604
  Returns:
1164
1605
  ee.Image: ee.Image of selected image
1165
1606
  """
1166
- new_col = self.collection.filter(ee.Filter.eq('Date_Filter', img_date))
1607
+ new_col = self.collection.filter(ee.Filter.eq("Date_Filter", img_date))
1167
1608
  return new_col.first()
1168
1609
 
1169
1610
  def CollectionStitch(self, img_col2):
1170
1611
  """
1171
- Function to mosaic two LandsatCollection objects which share image dates.
1172
- Mosaics are only formed for dates where both image collections have images.
1612
+ Function to mosaic two LandsatCollection objects which share image dates.
1613
+ Mosaics are only formed for dates where both image collections have images.
1173
1614
  Image properties are copied from the primary collection. Server-side friendly.
1174
1615
 
1175
1616
  Args:
@@ -1178,26 +1619,38 @@ class LandsatCollection:
1178
1619
  Returns:
1179
1620
  LandsatCollection: LandsatCollection image collection
1180
1621
  """
1181
- dates_list = ee.List(self._dates_list).cat(ee.List(img_col2.dates_list)).distinct()
1622
+ dates_list = (
1623
+ ee.List(self._dates_list).cat(ee.List(img_col2.dates_list)).distinct()
1624
+ )
1182
1625
  filtered_dates1 = self._dates_list
1183
1626
  filtered_dates2 = img_col2._dates_list
1184
1627
 
1185
- filtered_col2 = img_col2.collection.filter(ee.Filter.inList('Date_Filter', filtered_dates1))
1186
- filtered_col1 = self.collection.filter(ee.Filter.inList('Date_Filter', filtered_col2.aggregate_array('Date_Filter')))
1628
+ filtered_col2 = img_col2.collection.filter(
1629
+ ee.Filter.inList("Date_Filter", filtered_dates1)
1630
+ )
1631
+ filtered_col1 = self.collection.filter(
1632
+ ee.Filter.inList(
1633
+ "Date_Filter", filtered_col2.aggregate_array("Date_Filter")
1634
+ )
1635
+ )
1187
1636
 
1188
1637
  # Create a function that will be mapped over filtered_col1
1189
1638
  def mosaic_images(img):
1190
1639
  # Get the date of the image
1191
- date = img.get('Date_Filter')
1192
-
1640
+ date = img.get("Date_Filter")
1641
+
1193
1642
  # Get the corresponding image from filtered_col2
1194
- img2 = filtered_col2.filter(ee.Filter.equals('Date_Filter', date)).first()
1643
+ img2 = filtered_col2.filter(ee.Filter.equals("Date_Filter", date)).first()
1195
1644
 
1196
1645
  # Create a mosaic of the two images
1197
1646
  mosaic = ee.ImageCollection.fromImages([img, img2]).mosaic()
1198
1647
 
1199
1648
  # Copy properties from the first image and set the 'Date_Filter' property
1200
- mosaic = mosaic.copyProperties(img).set('Date_Filter', date).set('system:time_start', img.get('system:time_start'))
1649
+ mosaic = (
1650
+ mosaic.copyProperties(img)
1651
+ .set("Date_Filter", date)
1652
+ .set("system:time_start", img.get("system:time_start"))
1653
+ )
1201
1654
 
1202
1655
  return mosaic
1203
1656
 
@@ -1206,16 +1659,16 @@ class LandsatCollection:
1206
1659
 
1207
1660
  # Return a LandsatCollection instance
1208
1661
  return LandsatCollection(collection=new_col)
1209
-
1662
+
1210
1663
  @property
1211
1664
  def MosaicByDate(self):
1212
1665
  """
1213
1666
  Property attribute function to mosaic collection images that share the same date.
1214
1667
 
1215
- The property CLOUD_COVER for each image is used to calculate an overall mean,
1216
- which replaces the CLOUD_COVER property for each mosaiced image.
1217
- Server-side friendly.
1218
-
1668
+ The property CLOUD_COVER for each image is used to calculate an overall mean,
1669
+ which replaces the CLOUD_COVER property for each mosaiced image.
1670
+ Server-side friendly.
1671
+
1219
1672
  NOTE: if images are removed from the collection from cloud filtering, you may have mosaics composed of only one image.
1220
1673
 
1221
1674
  Returns:
@@ -1223,11 +1676,12 @@ class LandsatCollection:
1223
1676
  """
1224
1677
  if self._MosaicByDate is None:
1225
1678
  input_collection = self.collection
1679
+
1226
1680
  # Function to mosaic images of the same date and accumulate them
1227
1681
  def mosaic_and_accumulate(date, list_accumulator):
1228
1682
  # date = ee.Date(date)
1229
1683
  list_accumulator = ee.List(list_accumulator)
1230
- date_filter = ee.Filter.eq('Date_Filter', date)
1684
+ date_filter = ee.Filter.eq("Date_Filter", date)
1231
1685
  date_collection = input_collection.filter(date_filter)
1232
1686
  # Convert the collection to a list
1233
1687
  image_list = date_collection.toList(date_collection.size())
@@ -1235,24 +1689,30 @@ class LandsatCollection:
1235
1689
  # Get the image at the specified index
1236
1690
  first_image = ee.Image(image_list.get(0))
1237
1691
  # Create mosaic
1238
- mosaic = date_collection.mosaic().set('Date_Filter', date)
1692
+ mosaic = date_collection.mosaic().set("Date_Filter", date)
1239
1693
 
1240
1694
  # Calculate cumulative cloud and no data percentages
1241
- cloud_percentage = date_collection.aggregate_mean('CLOUD_COVER')
1695
+ cloud_percentage = date_collection.aggregate_mean("CLOUD_COVER")
1242
1696
 
1243
- props_of_interest = ['SPACECRAFT_ID', 'SENSOR_ID', 'PROCESSING_LEVEL', 'ACQUISITION_DATE', 'system:time_start']
1697
+ props_of_interest = [
1698
+ "SPACECRAFT_ID",
1699
+ "SENSOR_ID",
1700
+ "PROCESSING_LEVEL",
1701
+ "ACQUISITION_DATE",
1702
+ "system:time_start",
1703
+ ]
1244
1704
 
1245
1705
  # mosaic = mosaic.copyProperties(self.image_grab(0), props_of_interest).set({
1246
1706
  # 'CLOUD_COVER': cloud_percentage
1247
1707
  # })
1248
- mosaic = mosaic.copyProperties(first_image, props_of_interest).set({
1249
- 'CLOUD_COVER': cloud_percentage
1250
- })
1708
+ mosaic = mosaic.copyProperties(first_image, props_of_interest).set(
1709
+ {"CLOUD_COVER": cloud_percentage}
1710
+ )
1251
1711
 
1252
1712
  return list_accumulator.add(mosaic)
1253
1713
 
1254
1714
  # Get distinct dates
1255
- distinct_dates = input_collection.aggregate_array('Date_Filter').distinct()
1715
+ distinct_dates = input_collection.aggregate_array("Date_Filter").distinct()
1256
1716
 
1257
1717
  # Initialize an empty list as the accumulator
1258
1718
  initial = ee.List([])
@@ -1266,9 +1726,11 @@ class LandsatCollection:
1266
1726
 
1267
1727
  # Convert the list of mosaics to an ImageCollection
1268
1728
  return self._MosaicByDate
1269
-
1729
+
1270
1730
  @staticmethod
1271
- def ee_to_df(ee_object, columns=None, remove_geom=True, sort_columns=False, **kwargs):
1731
+ def ee_to_df(
1732
+ ee_object, columns=None, remove_geom=True, sort_columns=False, **kwargs
1733
+ ):
1272
1734
  """Converts an ee.FeatureCollection to pandas dataframe. Adapted from the geemap package (https://geemap.org/common/#geemap.common.ee_to_df)
1273
1735
 
1274
1736
  Args:
@@ -1318,8 +1780,19 @@ class LandsatCollection:
1318
1780
  raise Exception(e)
1319
1781
 
1320
1782
  @staticmethod
1321
- def extract_transect(image, line, reducer="mean", n_segments=100, dist_interval=None, scale=None, crs=None, crsTransform=None, tileScale=1.0, to_pandas=False, **kwargs):
1322
-
1783
+ def extract_transect(
1784
+ image,
1785
+ line,
1786
+ reducer="mean",
1787
+ n_segments=100,
1788
+ dist_interval=None,
1789
+ scale=None,
1790
+ crs=None,
1791
+ crsTransform=None,
1792
+ tileScale=1.0,
1793
+ to_pandas=False,
1794
+ **kwargs,
1795
+ ):
1323
1796
  """Extracts transect from an image. Adapted from the geemap package (https://geemap.org/common/#geemap.common.extract_transect). Exists as an alternative to RadGEEToolbox 'transect' function.
1324
1797
 
1325
1798
  Args:
@@ -1383,9 +1856,17 @@ class LandsatCollection:
1383
1856
 
1384
1857
  except Exception as e:
1385
1858
  raise Exception(e)
1386
-
1859
+
1387
1860
  @staticmethod
1388
- def transect(image, lines, line_names, reducer='mean', n_segments=None, dist_interval=30, to_pandas=True):
1861
+ def transect(
1862
+ image,
1863
+ lines,
1864
+ line_names,
1865
+ reducer="mean",
1866
+ n_segments=None,
1867
+ dist_interval=30,
1868
+ to_pandas=True,
1869
+ ):
1389
1870
  """Computes and stores the values along a transect for each line in a list of lines. Builds off of the extract_transect function from the geemap package
1390
1871
  where checks are ran to ensure that the reducer column is present in the transect data. If the reducer column is not present, a column of NaNs is created.
1391
1872
  An ee reducer is used to aggregate the values along the transect, depending on the number of segments or distance interval specified. Defaults to 'mean' reducer.
@@ -1402,46 +1883,71 @@ class LandsatCollection:
1402
1883
  Returns:
1403
1884
  pd.DataFrame or ee.FeatureCollection: organized list of values along the transect(s)
1404
1885
  """
1405
- #Create empty dataframe
1886
+ # Create empty dataframe
1406
1887
  transects_df = pd.DataFrame()
1407
1888
 
1408
- #Check if line is a list of lines or a single line - if single line, convert to list
1889
+ # Check if line is a list of lines or a single line - if single line, convert to list
1409
1890
  if isinstance(lines, list):
1410
1891
  pass
1411
1892
  else:
1412
1893
  lines = [lines]
1413
-
1894
+
1414
1895
  for i, line in enumerate(lines):
1415
1896
  if n_segments is None:
1416
- transect_data = LandsatCollection.extract_transect(image=image, line=line, reducer=reducer, dist_interval=dist_interval, to_pandas=to_pandas)
1897
+ transect_data = LandsatCollection.extract_transect(
1898
+ image=image,
1899
+ line=line,
1900
+ reducer=reducer,
1901
+ dist_interval=dist_interval,
1902
+ to_pandas=to_pandas,
1903
+ )
1417
1904
  if reducer in transect_data.columns:
1418
1905
  # Extract the 'mean' column and rename it
1419
- mean_column = transect_data[['mean']]
1906
+ mean_column = transect_data[["mean"]]
1420
1907
  else:
1421
1908
  # Handle the case where 'mean' column is not present
1422
- print(f"{reducer} column not found in transect data for line {line_names[i]}")
1909
+ print(
1910
+ f"{reducer} column not found in transect data for line {line_names[i]}"
1911
+ )
1423
1912
  # Create a column of NaNs with the same length as the longest column in transects_df
1424
1913
  max_length = max(transects_df.shape[0], transect_data.shape[0])
1425
1914
  mean_column = pd.Series([np.nan] * max_length)
1426
1915
  else:
1427
- transect_data = LandsatCollection.extract_transect(image=image, line=line, reducer=reducer, n_segments=n_segments, to_pandas=to_pandas)
1916
+ transect_data = LandsatCollection.extract_transect(
1917
+ image=image,
1918
+ line=line,
1919
+ reducer=reducer,
1920
+ n_segments=n_segments,
1921
+ to_pandas=to_pandas,
1922
+ )
1428
1923
  if reducer in transect_data.columns:
1429
1924
  # Extract the 'mean' column and rename it
1430
- mean_column = transect_data[['mean']]
1925
+ mean_column = transect_data[["mean"]]
1431
1926
  else:
1432
1927
  # Handle the case where 'mean' column is not present
1433
- print(f"{reducer} column not found in transect data for line {line_names[i]}")
1928
+ print(
1929
+ f"{reducer} column not found in transect data for line {line_names[i]}"
1930
+ )
1434
1931
  # Create a column of NaNs with the same length as the longest column in transects_df
1435
1932
  max_length = max(transects_df.shape[0], transect_data.shape[0])
1436
1933
  mean_column = pd.Series([np.nan] * max_length)
1437
-
1934
+
1438
1935
  transects_df = pd.concat([transects_df, mean_column], axis=1)
1439
1936
 
1440
1937
  transects_df.columns = line_names
1441
-
1938
+
1442
1939
  return transects_df
1443
-
1444
- def transect_iterator(self, lines, line_names, save_folder_path, reducer='mean', n_segments=None, dist_interval=30, to_pandas=True):
1940
+
1941
+ def transect_iterator(
1942
+ self,
1943
+ lines,
1944
+ line_names,
1945
+ save_folder_path,
1946
+ reducer="mean",
1947
+ n_segments=None,
1948
+ dist_interval=30,
1949
+ to_pandas=True,
1950
+ ):
1445
1951
  """Computes and stores the values along a transect for each line in a list of lines for each image in a LandsatCollection image collection, then saves the data for each image to a csv file. Builds off of the extract_transect function from the geemap package
1446
1952
  where checks are ran to ensure that the reducer column is present in the transect data. If the reducer column is not present, a column of NaNs is created.
1447
1953
  An ee reducer is used to aggregate the values along the transect, depending on the number of segments or distance interval specified. Defaults to 'mean' reducer.
@@ -1462,22 +1968,38 @@ class LandsatCollection:
1462
1968
  Returns:
1463
1969
  csv file: file for each image with an organized list of values along the transect(s)
1464
1970
  """
1465
- image_collection = self #.collection
1971
+ image_collection = self # .collection
1466
1972
  # image_collection_dates = self._dates
1467
1973
  image_collection_dates = self.dates
1468
1974
  for i, date in enumerate(image_collection_dates):
1469
1975
  try:
1470
1976
  print(f"Processing image {i+1}/{len(image_collection_dates)}: {date}")
1471
1977
  image = image_collection.image_grab(i)
1472
- transects_df = LandsatCollection.transect(image, lines, line_names, reducer=reducer, n_segments=n_segments, dist_interval=dist_interval, to_pandas=to_pandas)
1978
+ transects_df = LandsatCollection.transect(
1979
+ image,
1980
+ lines,
1981
+ line_names,
1982
+ reducer=reducer,
1983
+ n_segments=n_segments,
1984
+ dist_interval=dist_interval,
1985
+ to_pandas=to_pandas,
1986
+ )
1473
1987
  image_id = date
1474
- transects_df.to_csv(f'{save_folder_path}{image_id}_transects.csv')
1475
- print(f'{image_id}_transects saved to csv')
1988
+ transects_df.to_csv(f"{save_folder_path}{image_id}_transects.csv")
1989
+ print(f"{image_id}_transects saved to csv")
1476
1990
  except Exception as e:
1477
1991
  print(f"An error occurred while processing image {i+1}: {e}")
1478
1992
 
1479
1993
  @staticmethod
1480
- def extract_zonal_stats_from_buffer(image, coordinates, buffer_size=1, reducer_type='mean', scale=30, tileScale=1, coordinate_names=None):
1994
+ def extract_zonal_stats_from_buffer(
1995
+ image,
1996
+ coordinates,
1997
+ buffer_size=1,
1998
+ reducer_type="mean",
1999
+ scale=30,
2000
+ tileScale=1,
2001
+ coordinate_names=None,
2002
+ ):
1481
2003
  """
1482
2004
  Function to extract spatial statistics from an image for a list of coordinates, providing individual statistics for each location.
1483
2005
  A radial buffer is applied around each coordinate to extract the statistics, which defaults to 1 meter.
@@ -1499,15 +2021,26 @@ class LandsatCollection:
1499
2021
  # Check if coordinates is a single tuple and convert it to a list of tuples if necessary
1500
2022
  if isinstance(coordinates, tuple) and len(coordinates) == 2:
1501
2023
  coordinates = [coordinates]
1502
- elif not (isinstance(coordinates, list) and all(isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates)):
1503
- raise ValueError("Coordinates must be a list of tuples with two elements each (latitude, longitude).")
1504
-
2024
+ elif not (
2025
+ isinstance(coordinates, list)
2026
+ and all(
2027
+ isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates
2028
+ )
2029
+ ):
2030
+ raise ValueError(
2031
+ "Coordinates must be a list of tuples with two elements each (latitude, longitude)."
2032
+ )
2033
+
1505
2034
  # Check if coordinate_names is a list of strings
1506
2035
  if coordinate_names is not None:
1507
- if not isinstance(coordinate_names, list) or not all(isinstance(name, str) for name in coordinate_names):
2036
+ if not isinstance(coordinate_names, list) or not all(
2037
+ isinstance(name, str) for name in coordinate_names
2038
+ ):
1508
2039
  raise ValueError("coordinate_names must be a list of strings.")
1509
2040
  if len(coordinate_names) != len(coordinates):
1510
- raise ValueError("coordinate_names must have the same length as the coordinates list.")
2041
+ raise ValueError(
2042
+ "coordinate_names must have the same length as the coordinates list."
2043
+ )
1511
2044
  else:
1512
2045
  coordinate_names = [f"Location {i+1}" for i in range(len(coordinates))]
1513
2046
 
@@ -1519,73 +2052,95 @@ class LandsatCollection:
1519
2052
  # image = ee.Image(check_singleband(image))
1520
2053
  image = ee.Image(check_singleband(image))
1521
2054
 
1522
- #Convert coordinates to ee.Geometry.Point, buffer them, and add label/name to feature
1523
- points = [ee.Feature(ee.Geometry.Point([coord[0], coord[1]]).buffer(buffer_size), {'name': str(coordinate_names[i])}) for i, coord in enumerate(coordinates)]
2055
+ # Convert coordinates to ee.Geometry.Point, buffer them, and add label/name to feature
2056
+ points = [
2057
+ ee.Feature(
2058
+ ee.Geometry.Point([coord[0], coord[1]]).buffer(buffer_size),
2059
+ {"name": str(coordinate_names[i])},
2060
+ )
2061
+ for i, coord in enumerate(coordinates)
2062
+ ]
1524
2063
  # Create a feature collection from the buffered points
1525
2064
  features = ee.FeatureCollection(points)
1526
2065
  # Reduce the image to the buffered points - handle different reducer types
1527
- if reducer_type == 'mean':
2066
+ if reducer_type == "mean":
1528
2067
  img_stats = image.reduceRegions(
1529
- collection=features,
1530
- reducer=ee.Reducer.mean(),
1531
- scale=scale,
1532
- tileScale=tileScale)
2068
+ collection=features,
2069
+ reducer=ee.Reducer.mean(),
2070
+ scale=scale,
2071
+ tileScale=tileScale,
2072
+ )
1533
2073
  mean_values = img_stats.getInfo()
1534
2074
  means = []
1535
2075
  names = []
1536
- for feature in mean_values['features']:
1537
- names.append(feature['properties']['name'])
1538
- means.append(feature['properties']['mean'])
2076
+ for feature in mean_values["features"]:
2077
+ names.append(feature["properties"]["name"])
2078
+ means.append(feature["properties"]["mean"])
1539
2079
  organized_values = pd.DataFrame([means], columns=names)
1540
- elif reducer_type == 'median':
2080
+ elif reducer_type == "median":
1541
2081
  img_stats = image.reduceRegions(
1542
- collection=features,
1543
- reducer=ee.Reducer.median(),
1544
- scale=scale,
1545
- tileScale=tileScale)
2082
+ collection=features,
2083
+ reducer=ee.Reducer.median(),
2084
+ scale=scale,
2085
+ tileScale=tileScale,
2086
+ )
1546
2087
  median_values = img_stats.getInfo()
1547
2088
  medians = []
1548
2089
  names = []
1549
- for feature in median_values['features']:
1550
- names.append(feature['properties']['name'])
1551
- medians.append(feature['properties']['median'])
2090
+ for feature in median_values["features"]:
2091
+ names.append(feature["properties"]["name"])
2092
+ medians.append(feature["properties"]["median"])
1552
2093
  organized_values = pd.DataFrame([medians], columns=names)
1553
- elif reducer_type == 'min':
2094
+ elif reducer_type == "min":
1554
2095
  img_stats = image.reduceRegions(
1555
- collection=features,
1556
- reducer=ee.Reducer.min(),
1557
- scale=scale,
1558
- tileScale=tileScale)
2096
+ collection=features,
2097
+ reducer=ee.Reducer.min(),
2098
+ scale=scale,
2099
+ tileScale=tileScale,
2100
+ )
1559
2101
  min_values = img_stats.getInfo()
1560
2102
  mins = []
1561
2103
  names = []
1562
- for feature in min_values['features']:
1563
- names.append(feature['properties']['name'])
1564
- mins.append(feature['properties']['min'])
2104
+ for feature in min_values["features"]:
2105
+ names.append(feature["properties"]["name"])
2106
+ mins.append(feature["properties"]["min"])
1565
2107
  organized_values = pd.DataFrame([mins], columns=names)
1566
- elif reducer_type == 'max':
2108
+ elif reducer_type == "max":
1567
2109
  img_stats = image.reduceRegions(
1568
- collection=features,
1569
- reducer=ee.Reducer.max(),
1570
- scale=scale,
1571
- tileScale=tileScale)
2110
+ collection=features,
2111
+ reducer=ee.Reducer.max(),
2112
+ scale=scale,
2113
+ tileScale=tileScale,
2114
+ )
1572
2115
  max_values = img_stats.getInfo()
1573
2116
  maxs = []
1574
2117
  names = []
1575
- for feature in max_values['features']:
1576
- names.append(feature['properties']['name'])
1577
- maxs.append(feature['properties']['max'])
2118
+ for feature in max_values["features"]:
2119
+ names.append(feature["properties"]["name"])
2120
+ maxs.append(feature["properties"]["max"])
1578
2121
  organized_values = pd.DataFrame([maxs], columns=names)
1579
2122
  else:
1580
- raise ValueError("reducer_type must be one of 'mean', 'median', 'min', or 'max'.")
2123
+ raise ValueError(
2124
+ "reducer_type must be one of 'mean', 'median', 'min', or 'max'."
2125
+ )
1581
2126
  return organized_values
1582
2127
 
1583
- def iterate_zonal_stats(self, coordinates, buffer_size=1, reducer_type='mean', scale=30, tileScale=1, coordinate_names=None, file_path=None, dates=None):
2128
+ def iterate_zonal_stats(
2129
+ self,
2130
+ coordinates,
2131
+ buffer_size=1,
2132
+ reducer_type="mean",
2133
+ scale=30,
2134
+ tileScale=1,
2135
+ coordinate_names=None,
2136
+ file_path=None,
2137
+ dates=None,
2138
+ ):
1584
2139
  """
1585
2140
  Function to iterate over a collection of images and extract spatial statistics for a list of coordinates (defaults to mean). Individual statistics are provided for each location.
1586
2141
  A radial buffer is applied around each coordinate to extract the statistics, which defaults to 1 meter.
1587
2142
  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.
1588
-
2143
+
1589
2144
  Args:
1590
2145
  coordinates (list): Single tuple or a list of tuples with the coordinates as decimal degrees in the format of (longitude, latitude) for which to extract the statistics. NOTE the format needs to be [(x1, y1), (x2, y2), ...].
1591
2146
  buffer_size (int, optional): The radial buffer size in meters around the coordinates. Defaults to 1.
@@ -1601,22 +2156,32 @@ class LandsatCollection:
1601
2156
  .csv file: Optionally exports the data to a table in .csv format. If file_path is None, the function returns the DataFrame - otherwise the function will only export the csv file.
1602
2157
  """
1603
2158
  img_collection = self
1604
- #Create empty DataFrame to accumulate results
2159
+ # Create empty DataFrame to accumulate results
1605
2160
  accumulated_df = pd.DataFrame()
1606
- #Check if dates is None, if not use the dates provided
2161
+ # Check if dates is None, if not use the dates provided
1607
2162
  if dates is None:
1608
2163
  dates = img_collection.dates
1609
2164
  else:
1610
2165
  dates = dates
1611
- #Iterate over the dates and extract the zonal statistics for each date
2166
+ # Iterate over the dates and extract the zonal statistics for each date
1612
2167
  for date in dates:
1613
- image = img_collection.collection.filter(ee.Filter.eq('Date_Filter', date)).first()
1614
- single_df = LandsatCollection.extract_zonal_stats_from_buffer(image, coordinates, buffer_size=buffer_size, reducer_type=reducer_type, scale=scale, tileScale=tileScale, coordinate_names=coordinate_names)
1615
- single_df['Date'] = date
1616
- single_df.set_index('Date', inplace=True)
2168
+ image = img_collection.collection.filter(
2169
+ ee.Filter.eq("Date_Filter", date)
2170
+ ).first()
2171
+ single_df = LandsatCollection.extract_zonal_stats_from_buffer(
2172
+ image,
2173
+ coordinates,
2174
+ buffer_size=buffer_size,
2175
+ reducer_type=reducer_type,
2176
+ scale=scale,
2177
+ tileScale=tileScale,
2178
+ coordinate_names=coordinate_names,
2179
+ )
2180
+ single_df["Date"] = date
2181
+ single_df.set_index("Date", inplace=True)
1617
2182
  accumulated_df = pd.concat([accumulated_df, single_df])
1618
- #Return the DataFrame or export the data to a .csv file
2183
+ # Return the DataFrame or export the data to a .csv file
1619
2184
  if file_path is None:
1620
2185
  return accumulated_df
1621
2186
  else:
1622
- return accumulated_df.to_csv(f'{file_path}.csv')
2187
+ return accumulated_df.to_csv(f"{file_path}.csv")