RadGEEToolbox 1.6.4__py3-none-any.whl → 1.6.5__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.
@@ -2,6 +2,8 @@ import ee
2
2
  import math
3
3
  import pandas as pd
4
4
  import numpy as np
5
+
6
+
5
7
  class Sentinel1Collection:
6
8
  """
7
9
  Represents a user-defined collection of ESA Sentinel-1 C-band Synthetic Aperture Radar (SAR) GRD data at 10 m/px resolution from Google Earth Engine (GEE). Units of backscatter are in decibels (dB) by default.
@@ -28,7 +30,7 @@ class Sentinel1Collection:
28
30
  collection (ee.ImageCollection, optional): A pre-filtered Sentinel-1 ee.ImageCollection object to be converted to a Sentinel1Collection object. Overrides all other filters.
29
31
 
30
32
  Attributes:
31
- collection (ee.ImageCollection): The filtered or user-supplied image collection converted to an ee.ImageCollection object.
33
+ collection (ee.ImageCollection): The filtered or user-supplied image collection converted to an ee.ImageCollection object.
32
34
 
33
35
  Raises:
34
36
  ValueError: Raised if required filter parameters are missing, or if both `collection` and other filters are provided.
@@ -58,11 +60,35 @@ class Sentinel1Collection:
58
60
  >>> latest_image = SAR_collection.image_grab(-1)
59
61
  >>> mean_SAR_backscatter = SAR_collection.mean
60
62
  """
61
- def __init__(self, start_date=None, end_date=None, relative_orbit_start=None, relative_orbit_stop=None, instrument_mode=None, polarization=None, bands=None, orbit_direction=None, boundary=None, resolution=None, resolution_meters=None, collection=None):
63
+
64
+ def __init__(
65
+ self,
66
+ start_date=None,
67
+ end_date=None,
68
+ relative_orbit_start=None,
69
+ relative_orbit_stop=None,
70
+ instrument_mode=None,
71
+ polarization=None,
72
+ bands=None,
73
+ orbit_direction=None,
74
+ boundary=None,
75
+ resolution=None,
76
+ resolution_meters=None,
77
+ collection=None,
78
+ ):
62
79
  if collection is None and (start_date is None or end_date is None):
63
- 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.")
64
- if relative_orbit_start is None and relative_orbit_stop is None and boundary is None is None and collection is None:
65
- raise ValueError("Provide either tile or boundary/geometry specifications to filter the image collection")
80
+ raise ValueError(
81
+ "Either provide all required fields (start_date, end_date, tile_row, tile_path ; or boundary in place of tiles) or provide a collection."
82
+ )
83
+ if (
84
+ relative_orbit_start is None
85
+ and relative_orbit_stop is None
86
+ and boundary is None
87
+ and collection is None
88
+ ):
89
+ raise ValueError(
90
+ "Provide either tile or boundary/geometry specifications to filter the image collection"
91
+ )
66
92
  if collection is None:
67
93
  self.start_date = start_date
68
94
  self.end_date = end_date
@@ -77,8 +103,8 @@ class Sentinel1Collection:
77
103
  self.bands = bands
78
104
 
79
105
  if resolution is None:
80
- self.resolution = 'H'
81
- elif resolution not in ['H', 'M']:
106
+ self.resolution = "H"
107
+ elif resolution not in ["H", "M"]:
82
108
  raise ValueError("Resolution must be either 'H' or 'M'")
83
109
  else:
84
110
  pass
@@ -91,32 +117,36 @@ class Sentinel1Collection:
91
117
  else:
92
118
  self.resolution_meters = resolution_meters
93
119
  else:
94
- pass
120
+ pass
95
121
 
96
122
  if orbit_direction is None:
97
- self.orbit_direction = ['ASCENDING', 'DESCENDING']
98
- elif orbit_direction == ['ASCENDING', 'DESCENDING']:
123
+ self.orbit_direction = ["ASCENDING", "DESCENDING"]
124
+ elif orbit_direction == ["ASCENDING", "DESCENDING"]:
99
125
  self.orbit_direction = orbit_direction
100
- elif orbit_direction not in ['ASCENDING', 'DESCENDING']:
101
- raise ValueError("Orbit direction must be either 'ASCENDING' or 'DESCENDING', or '['ASCENDING', 'DESCENDING']' ")
126
+ elif orbit_direction not in ["ASCENDING", "DESCENDING"]:
127
+ raise ValueError(
128
+ "Orbit direction must be either 'ASCENDING' or 'DESCENDING', or '['ASCENDING', 'DESCENDING']' "
129
+ )
102
130
  else:
103
131
  pass
104
132
 
105
133
  if instrument_mode is None:
106
- self.instrument_mode = 'IW'
107
- elif instrument_mode not in ['IW', 'EW', 'SM']:
134
+ self.instrument_mode = "IW"
135
+ elif instrument_mode not in ["IW", "EW", "SM"]:
108
136
  raise ValueError("Instrument mode must be either 'IW', 'EW', or 'SM'")
109
137
  else:
110
138
  pass
111
139
 
112
140
  if polarization is None:
113
- self.polarization = ['VV', 'VH']
114
- elif polarization not in [['VV'], ['HH'], ['VV', 'VH'], ['HH', 'HV']]:
115
- raise ValueError("Polarization must be either ['VV'], ['HH'], ['VV, VH'], or ['HH, HV']")
141
+ self.polarization = ["VV", "VH"]
142
+ elif polarization not in [["VV"], ["HH"], ["VV", "VH"], ["HH", "HV"]]:
143
+ raise ValueError(
144
+ "Polarization must be either ['VV'], ['HH'], ['VV, VH'], or ['HH, HV']"
145
+ )
116
146
  else:
117
147
  pass
118
148
 
119
- valid_bands = ['HH', 'HV', 'VV', 'VH', 'angle']
149
+ valid_bands = ["HH", "HV", "VV", "VH", "angle"]
120
150
 
121
151
  if bands is not None and isinstance(bands, str):
122
152
  bands = [bands]
@@ -124,9 +154,14 @@ class Sentinel1Collection:
124
154
  if bands is None:
125
155
  self.bands = self.polarization
126
156
  elif not all(band in valid_bands for band in bands):
127
- raise ValueError("Band must be either 'HH', 'HV', 'VV', 'VH', or 'angle'")
157
+ raise ValueError(
158
+ "Band must be either 'HH', 'HV', 'VV', 'VH', or 'angle'"
159
+ )
128
160
  elif not all(band in self.polarization for band in bands):
129
- raise ValueError("Band must be associated with chosen polarization type, currently: "+str(self.polarization))
161
+ raise ValueError(
162
+ "Band must be associated with chosen polarization type, currently: "
163
+ + str(self.polarization)
164
+ )
130
165
  else:
131
166
  self.bands = bands
132
167
 
@@ -160,7 +195,6 @@ class Sentinel1Collection:
160
195
  else:
161
196
  self.bands = [self.bands]
162
197
 
163
-
164
198
  # Filter the collection
165
199
  if boundary and relative_orbit_start and relative_orbit_start is not None:
166
200
  self.collection = self.get_boundary_and_orbit_filtered_collection()
@@ -171,7 +205,6 @@ class Sentinel1Collection:
171
205
  else:
172
206
  self.collection = collection
173
207
 
174
-
175
208
  self._dates_list = None
176
209
  self._dates = None
177
210
  self._geometry_masked_collection = None
@@ -192,18 +225,19 @@ class Sentinel1Collection:
192
225
  """
193
226
  Adds date to image properties as 'Date_Filter'.
194
227
 
195
- Args:
228
+ Args:
196
229
  image (ee.Image): Input image
197
230
 
198
- Returns:
231
+ Returns:
199
232
  ee.Image: Image with date in properties.
200
233
  """
201
- date = ee.Number(image.date().format('YYYY-MM-dd'))
202
- return image.set({'Date_Filter': date})
203
-
204
-
234
+ date = ee.Number(image.date().format("YYYY-MM-dd"))
235
+ return image.set({"Date_Filter": date})
236
+
205
237
  @staticmethod
206
- def PixelAreaSum(image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12):
238
+ def PixelAreaSum(
239
+ image, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
240
+ ):
207
241
  """
208
242
  Function to calculate the summation of area for pixels of interest (above a specific threshold) in a geometry
209
243
  and store the value as image property (matching name of chosen band).
@@ -215,26 +249,34 @@ class Sentinel1Collection:
215
249
  threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1)
216
250
  scale (int): integer scale of image resolution (meters) (defaults to 30)
217
251
  maxPixels (int): integer denoting maximum number of pixels for calculations
218
-
219
- Returns:
252
+
253
+ Returns:
220
254
  ee.Image: ee.Image with area calculation stored as property matching name of band
221
255
  """
222
256
  area_image = ee.Image.pixelArea()
223
257
  mask = image.select(band_name).gte(threshold)
224
258
  final = image.addBands(area_image)
225
- stats = final.select('area').updateMask(mask).rename(band_name).reduceRegion(
226
- reducer = ee.Reducer.sum(),
227
- geometry= geometry,
228
- scale=scale,
229
- maxPixels = maxPixels)
259
+ stats = (
260
+ final.select("area")
261
+ .updateMask(mask)
262
+ .rename(band_name)
263
+ .reduceRegion(
264
+ reducer=ee.Reducer.sum(),
265
+ geometry=geometry,
266
+ scale=scale,
267
+ maxPixels=maxPixels,
268
+ )
269
+ )
230
270
  return image.set(band_name, stats.get(band_name))
231
-
232
- def PixelAreaSumCollection(self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12):
271
+
272
+ def PixelAreaSumCollection(
273
+ self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12
274
+ ):
233
275
  """
234
- Function to calculate the summation of area for pixels of interest (above a specific threshold)
276
+ Function to calculate the summation of area for pixels of interest (above a specific threshold)
235
277
  within a geometry and store the value as image property (matching name of chosen band) for an entire
236
278
  image collection.
237
- The resulting value has units of square meters.
279
+ The resulting value has units of square meters.
238
280
 
239
281
  Args:
240
282
  band_name (str): name of band (string) for calculating area.
@@ -242,26 +284,41 @@ class Sentinel1Collection:
242
284
  threshold (int): integer threshold to specify masking of pixels below threshold (defaults to -1).
243
285
  scale (int): integer scale of image resolution (meters) (defaults to 30).
244
286
  maxPixels (int): integer denoting maximum number of pixels for calculations.
245
-
246
- Returns:
287
+
288
+ Returns:
247
289
  ee.Image: Image with area calculation stored as property matching name of band.
248
290
  """
249
291
  if self._PixelAreaSumCollection is None:
250
292
  collection = self.collection
251
- AreaCollection = collection.map(lambda image: Sentinel1Collection.PixelAreaSum(image, band_name=band_name, geometry=geometry, threshold=threshold, scale=scale, maxPixels=maxPixels))
293
+ AreaCollection = collection.map(
294
+ lambda image: Sentinel1Collection.PixelAreaSum(
295
+ image,
296
+ band_name=band_name,
297
+ geometry=geometry,
298
+ threshold=threshold,
299
+ scale=scale,
300
+ maxPixels=maxPixels,
301
+ )
302
+ )
252
303
  self._PixelAreaSumCollection = AreaCollection
253
304
  return self._PixelAreaSumCollection
254
-
305
+
255
306
  @staticmethod
256
307
  def multilook_fn(image, looks):
257
308
  if looks not in [1, 2, 3, 4]:
258
- raise ValueError("Looks must be either 1, 2, 3, or 4, corresponding to 1x1, 2x2, 3x3, or 4x4 multilooking")
309
+ raise ValueError(
310
+ "Looks must be either 1, 2, 3, or 4, corresponding to 1x1, 2x2, 3x3, or 4x4 multilooking"
311
+ )
259
312
 
260
313
  default_projection = image.projection()
261
314
  image = image.setDefaultProjection(default_projection)
262
- looked_image = image.reduceResolution(reducer=ee.Reducer.mean(), maxPixels=1024).reproject(crs=default_projection, scale=10*looks)
315
+ looked_image = image.reduceResolution(
316
+ reducer=ee.Reducer.mean(), maxPixels=1024
317
+ ).reproject(crs=default_projection, scale=10 * looks)
263
318
 
264
- return looked_image.copyProperties(image).set('number_of_processed_looks', looks)
319
+ return looked_image.copyProperties(image).set(
320
+ "number_of_processed_looks", looks
321
+ )
265
322
 
266
323
  def multilook(self, looks):
267
324
  """
@@ -274,21 +331,25 @@ class Sentinel1Collection:
274
331
  Sentinel1Collection: Sentinel1Collection image collection
275
332
  """
276
333
  if looks not in [1, 2, 3, 4]:
277
- raise ValueError("Looks must be either 1, 2, 3, or 4, corresponding to 1x1, 2x2, 3x3, or 4x4 multilooking")
334
+ raise ValueError(
335
+ "Looks must be either 1, 2, 3, or 4, corresponding to 1x1, 2x2, 3x3, or 4x4 multilooking"
336
+ )
278
337
  else:
279
338
  pass
280
339
  if self._multilook is None:
281
340
  collection = self.collection
282
341
  looks = looks
283
- multilook_collection = collection.map(lambda image: Sentinel1Collection.multilook_fn(image, looks=looks))
342
+ multilook_collection = collection.map(
343
+ lambda image: Sentinel1Collection.multilook_fn(image, looks=looks)
344
+ )
284
345
  self._multilook = multilook_collection
285
346
  return Sentinel1Collection(collection=self._multilook)
286
347
 
287
348
  @staticmethod
288
349
  def leesigma(image, KERNEL_SIZE, geometry=None, Tk=7, sigma=0.9, looks=1):
289
350
  """
290
- Implements the improved lee sigma filter for speckle filtering, adapted from https://github.com/adugnag/gee_s1_ard (by Dr. Adugna Mullissa).
291
- See: Lee, J.-S. Wen, J.-H. Ainsworth, T.L. Chen, K.-S. Chen, A.J. Improved sigma filter for speckle filtering of SAR imagery.
351
+ Implements the improved lee sigma filter for speckle filtering, adapted from https://github.com/adugnag/gee_s1_ard (by Dr. Adugna Mullissa).
352
+ See: Lee, J.-S. Wen, J.-H. Ainsworth, T.L. Chen, K.-S. Chen, A.J. Improved sigma filter for speckle filtering of SAR imagery.
292
353
  IEEE Trans. Geosci. Remote Sens. 2009, 47, 202–213.
293
354
 
294
355
  Args:
@@ -299,137 +360,157 @@ class Sentinel1Collection:
299
360
  sigma (float): noise standard deviation (default is 0.9)
300
361
  looks (int): number of looks (1, 2, 3, or 4) corresponding to the input image (default is 1). This does NOT perform multilooking, but rather is used to determine the sigma range for filtering.
301
362
 
302
- Returns:
363
+ Returns:
303
364
  ee.Image: Speckle filtered image
304
365
 
305
366
  """
306
367
 
307
- #parameters
308
- Tk = ee.Image.constant(Tk) #number of bright pixels in a 3x3 window
368
+ # parameters
369
+ Tk = ee.Image.constant(Tk) # number of bright pixels in a 3x3 window
309
370
  sigma = 0.9
310
371
  enl = 4
311
372
  target_kernel = 3
312
- bandNames = image.bandNames().remove('angle')
373
+ bandNames = image.bandNames().remove("angle")
313
374
 
314
375
  # Use image bounds as default geometry
315
376
  if geometry is None:
316
377
  geometry = image.geometry()
317
-
318
- #compute the 98 percentile intensity
319
- z98 = ee.Dictionary(image.select(bandNames).reduceRegion(
320
- reducer= ee.Reducer.percentile([98]),
321
- geometry= geometry,
322
- scale=10,
323
- maxPixels=1e13
324
- )).toImage()
325
-
326
-
327
- #select the strong scatterers to retain
378
+
379
+ # compute the 98 percentile intensity
380
+ z98 = ee.Dictionary(
381
+ image.select(bandNames).reduceRegion(
382
+ reducer=ee.Reducer.percentile([98]),
383
+ geometry=geometry,
384
+ scale=10,
385
+ maxPixels=1e13,
386
+ )
387
+ ).toImage()
388
+
389
+ # select the strong scatterers to retain
328
390
  brightPixel = image.select(bandNames).gte(z98)
329
- K = brightPixel.reduceNeighborhood(ee.Reducer.countDistinctNonNull()
330
- ,ee.Kernel.square(target_kernel/2))
391
+ K = brightPixel.reduceNeighborhood(
392
+ ee.Reducer.countDistinctNonNull(), ee.Kernel.square(target_kernel / 2)
393
+ )
331
394
  retainPixel = K.gte(Tk)
332
-
333
-
334
- #compute the a-priori mean within a 3x3 local window
335
- #original noise standard deviation since the data is 5 look
336
- eta = 1.0/math.sqrt(enl)
395
+
396
+ # compute the a-priori mean within a 3x3 local window
397
+ # original noise standard deviation since the data is 5 look
398
+ eta = 1.0 / math.sqrt(enl)
337
399
  eta = ee.Image.constant(eta)
338
- #MMSE applied to estimate the apriori mean
339
- reducers = ee.Reducer.mean().combine( \
340
- reducer2= ee.Reducer.variance(), \
341
- sharedInputs= True
342
- )
343
- stats = image.select(bandNames).reduceNeighborhood( \
344
- reducer= reducers, \
345
- kernel= ee.Kernel.square(target_kernel/2,'pixels'), \
346
- optimization= 'window')
347
- meanBand = bandNames.map(lambda bandName: ee.String(bandName).cat('_mean'))
348
- varBand = bandNames.map(lambda bandName: ee.String(bandName).cat('_variance'))
349
-
400
+ # MMSE applied to estimate the apriori mean
401
+ reducers = ee.Reducer.mean().combine(
402
+ reducer2=ee.Reducer.variance(), sharedInputs=True
403
+ )
404
+ stats = image.select(bandNames).reduceNeighborhood(
405
+ reducer=reducers,
406
+ kernel=ee.Kernel.square(target_kernel / 2, "pixels"),
407
+ optimization="window",
408
+ )
409
+ meanBand = bandNames.map(lambda bandName: ee.String(bandName).cat("_mean"))
410
+ varBand = bandNames.map(lambda bandName: ee.String(bandName).cat("_variance"))
411
+
350
412
  z_bar = stats.select(meanBand)
351
413
  varz = stats.select(varBand)
352
-
414
+
353
415
  oneImg = ee.Image.constant(1)
354
- varx = (varz.subtract(z_bar.abs().pow(2).multiply(eta.pow(2)))).divide(oneImg.add(eta.pow(2)))
416
+ varx = (varz.subtract(z_bar.abs().pow(2).multiply(eta.pow(2)))).divide(
417
+ oneImg.add(eta.pow(2))
418
+ )
355
419
  b = varx.divide(varz)
356
- xTilde = oneImg.subtract(b).multiply(z_bar.abs()).add(b.multiply(image.select(bandNames)))
357
-
358
- #step 3: compute the sigma range using lookup tables (J.S.Lee et al 2009) for range and eta values for intensity
420
+ xTilde = (
421
+ oneImg.subtract(b)
422
+ .multiply(z_bar.abs())
423
+ .add(b.multiply(image.select(bandNames)))
424
+ )
425
+
426
+ # step 3: compute the sigma range using lookup tables (J.S.Lee et al 2009) for range and eta values for intensity
359
427
  if looks == 1:
360
- LUT = ee.Dictionary({
361
- 0.5: ee.Dictionary({'I1': 0.436, 'I2': 1.92, 'eta': 0.4057}),
362
- 0.6: ee.Dictionary({'I1': 0.343, 'I2': 2.21, 'eta': 0.4954}),
363
- 0.7: ee.Dictionary({'I1': 0.254, 'I2': 2.582, 'eta': 0.5911}),
364
- 0.8: ee.Dictionary({'I1': 0.168, 'I2': 3.094, 'eta': 0.6966}),
365
- 0.9: ee.Dictionary({'I1': 0.084, 'I2': 3.941, 'eta': 0.8191}),
366
- 0.95: ee.Dictionary({'I1': 0.043, 'I2': 4.840, 'eta': 0.8599})
367
- })
428
+ LUT = ee.Dictionary(
429
+ {
430
+ 0.5: ee.Dictionary({"I1": 0.436, "I2": 1.92, "eta": 0.4057}),
431
+ 0.6: ee.Dictionary({"I1": 0.343, "I2": 2.21, "eta": 0.4954}),
432
+ 0.7: ee.Dictionary({"I1": 0.254, "I2": 2.582, "eta": 0.5911}),
433
+ 0.8: ee.Dictionary({"I1": 0.168, "I2": 3.094, "eta": 0.6966}),
434
+ 0.9: ee.Dictionary({"I1": 0.084, "I2": 3.941, "eta": 0.8191}),
435
+ 0.95: ee.Dictionary({"I1": 0.043, "I2": 4.840, "eta": 0.8599}),
436
+ }
437
+ )
368
438
  elif looks == 2:
369
- LUT = ee.Dictionary({
370
- 0.5: ee.Dictionary({'I1': 0.582, 'I2': 1.584, 'eta': 0.2763}),
371
- 0.6: ee.Dictionary({'I1': 0.501, 'I2': 1.755, 'eta': 0.3388}),
372
- 0.7: ee.Dictionary({'I1': 0.418, 'I2': 1.972, 'eta': 0.4062}),
373
- 0.8: ee.Dictionary({'I1': 0.327, 'I2': 2.260, 'eta': 0.4810}),
374
- 0.9: ee.Dictionary({'I1': 0.221, 'I2': 2.744, 'eta': 0.5699}),
375
- 0.95: ee.Dictionary({'I1': 0.152, 'I2': 3.206, 'eta': 0.6254})
376
- })
439
+ LUT = ee.Dictionary(
440
+ {
441
+ 0.5: ee.Dictionary({"I1": 0.582, "I2": 1.584, "eta": 0.2763}),
442
+ 0.6: ee.Dictionary({"I1": 0.501, "I2": 1.755, "eta": 0.3388}),
443
+ 0.7: ee.Dictionary({"I1": 0.418, "I2": 1.972, "eta": 0.4062}),
444
+ 0.8: ee.Dictionary({"I1": 0.327, "I2": 2.260, "eta": 0.4810}),
445
+ 0.9: ee.Dictionary({"I1": 0.221, "I2": 2.744, "eta": 0.5699}),
446
+ 0.95: ee.Dictionary({"I1": 0.152, "I2": 3.206, "eta": 0.6254}),
447
+ }
448
+ )
377
449
  elif looks == 3:
378
- LUT = ee.Dictionary({
379
- 0.5: ee.Dictionary({'I1': 0.652, 'I2': 1.458, 'eta': 0.2222}),
380
- 0.6: ee.Dictionary({'I1': 0.580, 'I2': 1.586, 'eta': 0.2736}),
381
- 0.7: ee.Dictionary({'I1': 0.505, 'I2': 1.751, 'eta': 0.3280}),
382
- 0.8: ee.Dictionary({'I1': 0.419, 'I2': 1.965, 'eta': 0.3892}),
383
- 0.9: ee.Dictionary({'I1': 0.313, 'I2': 2.320, 'eta': 0.4624}),
384
- 0.95: ee.Dictionary({'I1': 0.238, 'I2': 2.656, 'eta': 0.5084})
385
- })
450
+ LUT = ee.Dictionary(
451
+ {
452
+ 0.5: ee.Dictionary({"I1": 0.652, "I2": 1.458, "eta": 0.2222}),
453
+ 0.6: ee.Dictionary({"I1": 0.580, "I2": 1.586, "eta": 0.2736}),
454
+ 0.7: ee.Dictionary({"I1": 0.505, "I2": 1.751, "eta": 0.3280}),
455
+ 0.8: ee.Dictionary({"I1": 0.419, "I2": 1.965, "eta": 0.3892}),
456
+ 0.9: ee.Dictionary({"I1": 0.313, "I2": 2.320, "eta": 0.4624}),
457
+ 0.95: ee.Dictionary({"I1": 0.238, "I2": 2.656, "eta": 0.5084}),
458
+ }
459
+ )
386
460
  elif looks == 4:
387
- LUT = ee.Dictionary({
388
- 0.5: ee.Dictionary({'I1': 0.694, 'I2': 1.385, 'eta': 0.1921}),
389
- 0.6: ee.Dictionary({'I1': 0.630, 'I2': 1.495, 'eta': 0.2348}),
390
- 0.7: ee.Dictionary({'I1': 0.560, 'I2': 1.627, 'eta': 0.2825}),
391
- 0.8: ee.Dictionary({'I1': 0.480, 'I2': 1.804, 'eta': 0.3354}),
392
- 0.9: ee.Dictionary({'I1': 0.378, 'I2': 2.094, 'eta': 0.3991}),
393
- 0.95: ee.Dictionary({'I1': 0.302, 'I2': 2.360, 'eta': 0.4391})
394
- })
461
+ LUT = ee.Dictionary(
462
+ {
463
+ 0.5: ee.Dictionary({"I1": 0.694, "I2": 1.385, "eta": 0.1921}),
464
+ 0.6: ee.Dictionary({"I1": 0.630, "I2": 1.495, "eta": 0.2348}),
465
+ 0.7: ee.Dictionary({"I1": 0.560, "I2": 1.627, "eta": 0.2825}),
466
+ 0.8: ee.Dictionary({"I1": 0.480, "I2": 1.804, "eta": 0.3354}),
467
+ 0.9: ee.Dictionary({"I1": 0.378, "I2": 2.094, "eta": 0.3991}),
468
+ 0.95: ee.Dictionary({"I1": 0.302, "I2": 2.360, "eta": 0.4391}),
469
+ }
470
+ )
395
471
  else:
396
- raise ValueError("Invalid number of looks. Please choose from 1, 2, 3, or 4.")
472
+ raise ValueError(
473
+ "Invalid number of looks. Please choose from 1, 2, 3, or 4."
474
+ )
397
475
 
398
-
399
- #extract data from lookup
476
+ # extract data from lookup
400
477
  sigmaImage = ee.Dictionary(LUT.get(str(sigma))).toImage()
401
- I1 = sigmaImage.select('I1')
402
- I2 = sigmaImage.select('I2')
403
- #new speckle sigma
404
- nEta = sigmaImage.select('eta')
405
- #establish the sigma ranges
478
+ I1 = sigmaImage.select("I1")
479
+ I2 = sigmaImage.select("I2")
480
+ # new speckle sigma
481
+ nEta = sigmaImage.select("eta")
482
+ # establish the sigma ranges
406
483
  I1 = I1.multiply(xTilde)
407
484
  I2 = I2.multiply(xTilde)
408
-
409
- #step 3: apply MMSE filter for pixels in the sigma range
410
- #MMSE estimator
485
+
486
+ # step 3: apply MMSE filter for pixels in the sigma range
487
+ # MMSE estimator
411
488
  mask = image.select(bandNames).gte(I1).Or(image.select(bandNames).lte(I2))
412
489
  z = image.select(bandNames).updateMask(mask)
413
-
414
- stats = z.reduceNeighborhood(reducer= reducers, kernel= ee.Kernel.square(KERNEL_SIZE/2,'pixels'), optimization= 'window')
415
-
490
+
491
+ stats = z.reduceNeighborhood(
492
+ reducer=reducers,
493
+ kernel=ee.Kernel.square(KERNEL_SIZE / 2, "pixels"),
494
+ optimization="window",
495
+ )
496
+
416
497
  z_bar = stats.select(meanBand)
417
498
  varz = stats.select(varBand)
418
-
419
-
420
- varx = (varz.subtract(z_bar.abs().pow(2).multiply(nEta.pow(2)))).divide(oneImg.add(nEta.pow(2)))
499
+
500
+ varx = (varz.subtract(z_bar.abs().pow(2).multiply(nEta.pow(2)))).divide(
501
+ oneImg.add(nEta.pow(2))
502
+ )
421
503
  b = varx.divide(varz)
422
- #if b is negative set it to zero
504
+ # if b is negative set it to zero
423
505
  new_b = b.where(b.lt(0), 0)
424
506
  xHat = oneImg.subtract(new_b).multiply(z_bar.abs()).add(new_b.multiply(z))
425
-
426
- #remove the applied masks and merge the retained pixels and the filtered pixels
507
+
508
+ # remove the applied masks and merge the retained pixels and the filtered pixels
427
509
  xHat = image.select(bandNames).updateMask(retainPixel).unmask(xHat)
428
510
  output = ee.Image(xHat).rename(bandNames)
429
511
  # return image.addBands(output, None, True)
430
512
  return output.copyProperties(image)
431
-
432
-
513
+
433
514
  def speckle_filter(self, KERNEL_SIZE, geometry=None, Tk=7, sigma=0.9, looks=1):
434
515
  """
435
516
  Property attribute function to apply speckle filter to entire image collection. Results are calculated once per class object then cached for future use.
@@ -446,10 +527,19 @@ class Sentinel1Collection:
446
527
  """
447
528
  if self._speckle_filter is None:
448
529
  collection = self.collection
449
- speckle_filtered_collection = collection.map(lambda image: Sentinel1Collection.leesigma(image, KERNEL_SIZE, geometry=geometry, Tk=Tk, sigma=sigma, looks=looks))
530
+ speckle_filtered_collection = collection.map(
531
+ lambda image: Sentinel1Collection.leesigma(
532
+ image,
533
+ KERNEL_SIZE,
534
+ geometry=geometry,
535
+ Tk=Tk,
536
+ sigma=sigma,
537
+ looks=looks,
538
+ )
539
+ )
450
540
  self._speckle_filter = speckle_filtered_collection
451
541
  return Sentinel1Collection(collection=self._speckle_filter)
452
-
542
+
453
543
  @property
454
544
  def Sigma0FromDb(self):
455
545
  """
@@ -458,10 +548,16 @@ class Sentinel1Collection:
458
548
  Returns:
459
549
  Sentinel1Collection: Sentinel1Collection image collection
460
550
  """
551
+
461
552
  def conversion(image):
462
553
  image = ee.Image(image)
463
554
  band_names = image.bandNames()
464
- sigma_nought = ee.Image(10).pow(image.divide(ee.Image(10))).rename(band_names).copyProperties(image)
555
+ sigma_nought = (
556
+ ee.Image(10)
557
+ .pow(image.divide(ee.Image(10)))
558
+ .rename(band_names)
559
+ .copyProperties(image)
560
+ )
465
561
  return sigma_nought
466
562
 
467
563
  if self._Sigma0FromDb is None:
@@ -469,7 +565,7 @@ class Sentinel1Collection:
469
565
  sigma0_collection = collection.map(conversion)
470
566
  self._Sigma0FromDb = sigma0_collection
471
567
  return Sentinel1Collection(collection=self._Sigma0FromDb)
472
-
568
+
473
569
  @property
474
570
  def DbFromSigma0(self):
475
571
  """
@@ -478,18 +574,24 @@ class Sentinel1Collection:
478
574
  Returns:
479
575
  Sentinel1Collection: Sentinel1Collection image collection
480
576
  """
577
+
481
578
  def conversion(image):
482
579
  image = ee.Image(image)
483
580
  band_names = image.bandNames()
484
- dB = ee.Image(10).multiply(image.log10()).rename(band_names).copyProperties(image)
581
+ dB = (
582
+ ee.Image(10)
583
+ .multiply(image.log10())
584
+ .rename(band_names)
585
+ .copyProperties(image)
586
+ )
485
587
  return dB
486
588
 
487
- if self._Sigma0FromDb is None:
589
+ if self._DbFromSigma0 is None:
488
590
  collection = self.collection
489
591
  dB_collection = collection.map(conversion)
490
592
  self._DbFromSigma0 = dB_collection
491
593
  return Sentinel1Collection(collection=self._DbFromSigma0)
492
-
594
+
493
595
  @property
494
596
  def dates_list(self):
495
597
  """
@@ -499,7 +601,7 @@ class Sentinel1Collection:
499
601
  ee.List: Server-side ee.List of dates.
500
602
  """
501
603
  if self._dates_list is None:
502
- dates = self.collection.aggregate_array('Date_Filter')
604
+ dates = self.collection.aggregate_array("Date_Filter")
503
605
  self._dates_list = dates
504
606
  return self._dates_list
505
607
 
@@ -512,7 +614,7 @@ class Sentinel1Collection:
512
614
  list: list of date strings.
513
615
  """
514
616
  if self._dates_list is None:
515
- dates = self.collection.aggregate_array('Date_Filter')
617
+ dates = self.collection.aggregate_array("Date_Filter")
516
618
  self._dates_list = dates
517
619
  if self._dates is None:
518
620
  dates = self._dates_list.getInfo()
@@ -526,15 +628,29 @@ class Sentinel1Collection:
526
628
  Returns:
527
629
  ee.ImageCollection: Filtered image collection - used for subsequent analyses or to acquire ee.ImageCollection from Sentinel1Collection object
528
630
  """
529
- # filtered_collection = ee.ImageCollection("COPERNICUS/S1_GRD").filterDate(self.start_date, self.end_date).filter(ee.Filter.inList('instrumentMode', self.instrument_mode)).filter(ee.Filter.And(ee.Filter.inList('relativeOrbitNumber_start', self.relative_orbit_stop),
530
- # ee.Filter.inList('relativeOrbitNumber_stop', self.relative_orbit_stop))).filter(ee.Filter.inList('orbitProperties_pass', self.orbit_direction)).filter(ee.Filter.inList('transmitterReceiverPolarisation',
531
- # self.polarization)).filter(ee.Filter.eq('resolution', self.resolution)).map(self.image_dater).select(self.band)
532
631
 
533
- filtered_collection = ee.ImageCollection("COPERNICUS/S1_GRD").filterDate(self.start_date, self.end_date).filter(ee.Filter.inList('instrumentMode', self.instrument_mode)).filter(ee.Filter.And(ee.Filter.inList('relativeOrbitNumber_start', self.relative_orbit_start),
534
- ee.Filter.inList('relativeOrbitNumber_stop', self.relative_orbit_stop))).filter(ee.Filter.inList('orbitProperties_pass', self.orbit_direction)).filter(ee.Filter.eq('transmitterReceiverPolarisation',
535
- self.polarization)).filter(ee.Filter.eq('resolution_meters', self.resolution_meters)).map(self.image_dater).select(self.bands)
632
+ filtered_collection = (
633
+ ee.ImageCollection("COPERNICUS/S1_GRD")
634
+ .filterDate(self.start_date, self.end_date)
635
+ .filter(ee.Filter.inList("instrumentMode", self.instrument_mode))
636
+ .filter(
637
+ ee.Filter.And(
638
+ ee.Filter.inList(
639
+ "relativeOrbitNumber_start", self.relative_orbit_start
640
+ ),
641
+ ee.Filter.inList(
642
+ "relativeOrbitNumber_stop", self.relative_orbit_stop
643
+ ),
644
+ )
645
+ )
646
+ .filter(ee.Filter.inList("orbitProperties_pass", self.orbit_direction))
647
+ .filter(ee.Filter.eq("transmitterReceiverPolarisation", self.polarization))
648
+ .filter(ee.Filter.eq("resolution_meters", self.resolution_meters))
649
+ .map(self.image_dater)
650
+ .select(self.bands)
651
+ )
536
652
  return filtered_collection
537
-
653
+
538
654
  def get_boundary_filtered_collection(self):
539
655
  """
540
656
  Function to filter and mask image collection based on Sentinel1Collection class arguments. Automatically calculated when using collection method, depending on provided class arguments (when boundary info is provided).
@@ -543,10 +659,19 @@ class Sentinel1Collection:
543
659
  ee.ImageCollection: Filtered image collection - used for subsequent analyses or to acquire ee.ImageCollection from Sentinel1Collection object
544
660
 
545
661
  """
546
- filtered_collection = ee.ImageCollection("COPERNICUS/S1_GRD").filterDate(self.start_date, self.end_date).filterBounds(self.boundary).filter(ee.Filter.inList('instrumentMode', self.instrument_mode)).filter(ee.Filter.inList('orbitProperties_pass', self.orbit_direction)).filter(ee.Filter.eq('transmitterReceiverPolarisation',
547
- self.polarization)).filter(ee.Filter.eq('resolution_meters', self.resolution_meters)).map(self.image_dater).select(self.bands)
662
+ filtered_collection = (
663
+ ee.ImageCollection("COPERNICUS/S1_GRD")
664
+ .filterDate(self.start_date, self.end_date)
665
+ .filterBounds(self.boundary)
666
+ .filter(ee.Filter.inList("instrumentMode", self.instrument_mode))
667
+ .filter(ee.Filter.inList("orbitProperties_pass", self.orbit_direction))
668
+ .filter(ee.Filter.eq("transmitterReceiverPolarisation", self.polarization))
669
+ .filter(ee.Filter.eq("resolution_meters", self.resolution_meters))
670
+ .map(self.image_dater)
671
+ .select(self.bands)
672
+ )
548
673
  return filtered_collection
549
-
674
+
550
675
  def get_boundary_and_orbit_filtered_collection(self):
551
676
  """
552
677
  Function to filter image collection based on Sentinel1Collection class arguments. Automatically calculated when using collection method, depending on provided class arguments (when tile info is provided).
@@ -555,33 +680,51 @@ class Sentinel1Collection:
555
680
  ee.ImageCollection: Filtered image collection - used for subsequent analyses or to acquire ee.ImageCollection from Sentinel1Collection object
556
681
  """
557
682
  # filtered_collection = ee.ImageCollection("COPERNICUS/S1_GRD").filterDate(self.start_date, self.end_date).filter(ee.Filter.inList('instrumentMode', self.instrument_mode)).filter(ee.Filter.And(ee.Filter.inList('relativeOrbitNumber_start', self.relative_orbit_stop),
558
- # ee.Filter.inList('relativeOrbitNumber_stop', self.relative_orbit_stop))).filter(ee.Filter.inList('orbitProperties_pass', self.orbit_direction)).filter(ee.Filter.inList('transmitterReceiverPolarisation',
683
+ # ee.Filter.inList('relativeOrbitNumber_stop', self.relative_orbit_stop))).filter(ee.Filter.inList('orbitProperties_pass', self.orbit_direction)).filter(ee.Filter.inList('transmitterReceiverPolarisation',
559
684
  # self.polarization)).filter(ee.Filter.eq('resolution', self.resolution)).map(self.image_dater).select(self.band)
560
685
 
561
- filtered_collection = ee.ImageCollection("COPERNICUS/S1_GRD").filterDate(self.start_date, self.end_date).filter(ee.Filter.inList('instrumentMode', self.instrument_mode)).filterBounds(self.boundary).filter(ee.Filter.And(ee.Filter.inList('relativeOrbitNumber_start', self.relative_orbit_start),
562
- ee.Filter.inList('relativeOrbitNumber_stop', self.relative_orbit_stop))).filter(ee.Filter.inList('orbitProperties_pass', self.orbit_direction)).filter(ee.Filter.eq('transmitterReceiverPolarisation',
563
- self.polarization)).filter(ee.Filter.eq('resolution_meters', self.resolution_meters)).map(self.image_dater).select(self.bands)
686
+ filtered_collection = (
687
+ ee.ImageCollection("COPERNICUS/S1_GRD")
688
+ .filterDate(self.start_date, self.end_date)
689
+ .filter(ee.Filter.inList("instrumentMode", self.instrument_mode))
690
+ .filterBounds(self.boundary)
691
+ .filter(
692
+ ee.Filter.And(
693
+ ee.Filter.inList(
694
+ "relativeOrbitNumber_start", self.relative_orbit_start
695
+ ),
696
+ ee.Filter.inList(
697
+ "relativeOrbitNumber_stop", self.relative_orbit_stop
698
+ ),
699
+ )
700
+ )
701
+ .filter(ee.Filter.inList("orbitProperties_pass", self.orbit_direction))
702
+ .filter(ee.Filter.eq("transmitterReceiverPolarisation", self.polarization))
703
+ .filter(ee.Filter.eq("resolution_meters", self.resolution_meters))
704
+ .map(self.image_dater)
705
+ .select(self.bands)
706
+ )
564
707
  return filtered_collection
565
-
708
+
566
709
  @property
567
710
  def median(self):
568
711
  """
569
712
  Property attribute function to calculate median image from image collection. Results are calculated once per class object then cached for future use.
570
713
 
571
- Returns:
714
+ Returns:
572
715
  ee.Image: median image from entire collection.
573
716
  """
574
717
  if self._median is None:
575
718
  col = self.collection.median()
576
719
  self._median = col
577
720
  return self._median
578
-
721
+
579
722
  @property
580
723
  def mean(self):
581
724
  """
582
725
  Property attribute function to calculate mean image from image collection. Results are calculated once per class object then cached for future use.
583
726
 
584
- Returns:
727
+ Returns:
585
728
  ee.Image: mean image from entire collection.
586
729
 
587
730
  """
@@ -589,33 +732,33 @@ class Sentinel1Collection:
589
732
  col = self.collection.mean()
590
733
  self._mean = col
591
734
  return self._mean
592
-
735
+
593
736
  @property
594
737
  def max(self):
595
738
  """
596
739
  Property attribute function to calculate max image from image collection. Results are calculated once per class object then cached for future use.
597
740
 
598
- Returns:
741
+ Returns:
599
742
  ee.Image: max image from entire collection.
600
743
  """
601
744
  if self._max is None:
602
745
  col = self.collection.max()
603
746
  self._max = col
604
747
  return self._max
605
-
748
+
606
749
  @property
607
750
  def min(self):
608
751
  """
609
752
  Property attribute function to calculate min image from image collection. Results are calculated once per class object then cached for future use.
610
-
611
- Returns:
753
+
754
+ Returns:
612
755
  ee.Image: min image from entire collection.
613
756
  """
614
757
  if self._min is None:
615
758
  col = self.collection.min()
616
759
  self._min = col
617
760
  return self._min
618
-
761
+
619
762
  def mask_to_polygon(self, polygon):
620
763
  """
621
764
  Function to mask Sentinel1Collection image collection by a polygon (ee.Geometry), where pixels outside the polygon are masked out.
@@ -625,21 +768,23 @@ class Sentinel1Collection:
625
768
 
626
769
  Returns:
627
770
  Sentinel1Collection: masked Sentinel1Collection image collection
628
-
771
+
629
772
  """
630
773
  if self._geometry_masked_collection is None:
631
774
  # Convert the polygon to a mask
632
775
  mask = ee.Image.constant(1).clip(polygon)
633
-
776
+
634
777
  # Update the mask of each image in the collection
635
778
  masked_collection = self.collection.map(lambda img: img.updateMask(mask))
636
-
779
+
637
780
  # Update the internal collection state
638
- self._geometry_masked_collection = Sentinel1Collection(collection=masked_collection)
639
-
781
+ self._geometry_masked_collection = Sentinel1Collection(
782
+ collection=masked_collection
783
+ )
784
+
640
785
  # Return the updated object
641
786
  return self._geometry_masked_collection
642
-
787
+
643
788
  def mask_out_polygon(self, polygon):
644
789
  """
645
790
  Function to mask Sentinel1Collection image collection by a polygon (ee.Geometry), where pixels inside the polygon are masked out.
@@ -649,7 +794,7 @@ class Sentinel1Collection:
649
794
 
650
795
  Returns:
651
796
  Sentinel1Collection: masked Sentinel1Collection image collection
652
-
797
+
653
798
  """
654
799
  if self._geometry_masked_out_collection is None:
655
800
  # Convert the polygon to a mask
@@ -657,17 +802,17 @@ class Sentinel1Collection:
657
802
 
658
803
  # Use paint to set pixels inside polygon as 0
659
804
  area = full_mask.paint(polygon, 0)
660
-
805
+
661
806
  # Update the mask of each image in the collection
662
807
  masked_collection = self.collection.map(lambda img: img.updateMask(area))
663
-
808
+
664
809
  # Update the internal collection state
665
- self._geometry_masked_out_collection = Sentinel1Collection(collection=masked_collection)
666
-
810
+ self._geometry_masked_out_collection = Sentinel1Collection(
811
+ collection=masked_collection
812
+ )
813
+
667
814
  # Return the updated object
668
815
  return self._geometry_masked_out_collection
669
-
670
-
671
816
 
672
817
  def image_grab(self, img_selector):
673
818
  """
@@ -675,8 +820,8 @@ class Sentinel1Collection:
675
820
 
676
821
  Args:
677
822
  img_selector (int): index of image in the collection for which user seeks to select/"grab".
678
-
679
- Returns:
823
+
824
+ Returns:
680
825
  ee.Image: ee.Image of selected image
681
826
  """
682
827
  # Convert the collection to a list
@@ -694,8 +839,8 @@ class Sentinel1Collection:
694
839
  Args:
695
840
  img_col (ee.ImageCollection): ee.ImageCollection with same dates as another Sentinel1Collection image collection object.
696
841
  img_selector (int): index of image in list of dates for which user seeks to "select".
697
-
698
- Returns:
842
+
843
+ Returns:
699
844
  ee.Image: ee.Image of selected image
700
845
  """
701
846
  # Convert the collection to a list
@@ -705,7 +850,7 @@ class Sentinel1Collection:
705
850
  image = ee.Image(image_list.get(img_selector))
706
851
 
707
852
  return image
708
-
853
+
709
854
  def image_pick(self, img_date):
710
855
  """
711
856
  Function to select ("grab") 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.
@@ -713,16 +858,16 @@ class Sentinel1Collection:
713
858
  Args:
714
859
  img_date (str): date of image to select from collection, in format of 'YYYY-MM-DD'
715
860
 
716
- Returns:
861
+ Returns:
717
862
  ee.Image: ee.Image of selected image
718
863
  """
719
- new_col = self.collection.filter(ee.Filter.eq('Date_Filter', img_date))
864
+ new_col = self.collection.filter(ee.Filter.eq("Date_Filter", img_date))
720
865
  return new_col.first()
721
866
 
722
867
  def CollectionStitch(self, img_col2):
723
868
  """
724
- Function to mosaic two Sentinel1Collection objects which share image dates.
725
- Mosaics are only formed for dates where both image collections have images.
869
+ Function to mosaic two Sentinel1Collection objects which share image dates.
870
+ Mosaics are only formed for dates where both image collections have images.
726
871
  Image properties are copied from the primary collection. Server-side friendly.
727
872
 
728
873
  Args:
@@ -731,26 +876,38 @@ class Sentinel1Collection:
731
876
  Returns:
732
877
  Sentinel1Collection: Sentinel1Collection image collection
733
878
  """
734
- dates_list = ee.List(self._dates_list).cat(ee.List(img_col2.dates_list)).distinct()
879
+ dates_list = (
880
+ ee.List(self._dates_list).cat(ee.List(img_col2.dates_list)).distinct()
881
+ )
735
882
  filtered_dates1 = self._dates_list
736
883
  filtered_dates2 = img_col2._dates_list
737
884
 
738
- filtered_col2 = img_col2.collection.filter(ee.Filter.inList('Date_Filter', filtered_dates1))
739
- filtered_col1 = self.collection.filter(ee.Filter.inList('Date_Filter', filtered_col2.aggregate_array('Date_Filter')))
885
+ filtered_col2 = img_col2.collection.filter(
886
+ ee.Filter.inList("Date_Filter", filtered_dates1)
887
+ )
888
+ filtered_col1 = self.collection.filter(
889
+ ee.Filter.inList(
890
+ "Date_Filter", filtered_col2.aggregate_array("Date_Filter")
891
+ )
892
+ )
740
893
 
741
894
  # Create a function that will be mapped over filtered_col1
742
895
  def mosaic_images(img):
743
896
  # Get the date of the image
744
- date = img.get('Date_Filter')
745
-
897
+ date = img.get("Date_Filter")
898
+
746
899
  # Get the corresponding image from filtered_col2
747
- img2 = filtered_col2.filter(ee.Filter.equals('Date_Filter', date)).first()
900
+ img2 = filtered_col2.filter(ee.Filter.equals("Date_Filter", date)).first()
748
901
 
749
902
  # Create a mosaic of the two images
750
903
  mosaic = ee.ImageCollection.fromImages([img, img2]).mosaic()
751
904
 
752
905
  # Copy properties from the first image and set the 'Date_Filter' property
753
- mosaic = mosaic.copyProperties(img).set('Date_Filter', date).set('system:time_start', img.get('system:time_start'))
906
+ mosaic = (
907
+ mosaic.copyProperties(img)
908
+ .set("Date_Filter", date)
909
+ .set("system:time_start", img.get("system:time_start"))
910
+ )
754
911
 
755
912
  return mosaic
756
913
 
@@ -759,13 +916,13 @@ class Sentinel1Collection:
759
916
 
760
917
  # Return a Sentinel1Collection instance
761
918
  return Sentinel1Collection(collection=new_col)
762
-
919
+
763
920
  @property
764
921
  def MosaicByDate(self):
765
922
  """
766
- Property attribute function to mosaic collection images that share the same date.
767
- The property CLOUD_COVER for each image is used to calculate an overall mean,
768
- which replaces the CLOUD_COVER property for each mosaiced image.
923
+ Property attribute function to mosaic collection images that share the same date.
924
+ The property CLOUD_COVER for each image is used to calculate an overall mean,
925
+ which replaces the CLOUD_COVER property for each mosaiced image.
769
926
  Server-side friendly. NOTE: if images are removed from the collection from cloud filtering, you may have mosaics composed of only one image.
770
927
 
771
928
  Returns:
@@ -773,11 +930,12 @@ class Sentinel1Collection:
773
930
  """
774
931
  if self._MosaicByDate is None:
775
932
  input_collection = self.collection
933
+
776
934
  # Function to mosaic images of the same date and accumulate them
777
935
  def mosaic_and_accumulate(date, list_accumulator):
778
936
 
779
937
  list_accumulator = ee.List(list_accumulator)
780
- date_filter = ee.Filter.eq('Date_Filter', date)
938
+ date_filter = ee.Filter.eq("Date_Filter", date)
781
939
  date_collection = input_collection.filter(date_filter)
782
940
  # Convert the collection to a list
783
941
  image_list = date_collection.toList(date_collection.size())
@@ -785,16 +943,29 @@ class Sentinel1Collection:
785
943
  # Get the image at the specified index
786
944
  first_image = ee.Image(image_list.get(0))
787
945
  # Create mosaic
788
- mosaic = date_collection.mosaic().set('Date_Filter', date)
789
-
790
- props_of_interest = ['platform_number', 'instrument', 'instrumentMode', 'orbitNumber_start', 'orbitNumber_stop', 'orbitProperties_pass', 'resolution_meters', 'transmitterReceiverPolarisation','system:time_start', 'crs']
791
-
792
- mosaic = mosaic.setDefaultProjection(first_image.projection()).copyProperties(first_image, props_of_interest)
946
+ mosaic = date_collection.mosaic().set("Date_Filter", date)
947
+
948
+ props_of_interest = [
949
+ "platform_number",
950
+ "instrument",
951
+ "instrumentMode",
952
+ "orbitNumber_start",
953
+ "orbitNumber_stop",
954
+ "orbitProperties_pass",
955
+ "resolution_meters",
956
+ "transmitterReceiverPolarisation",
957
+ "system:time_start",
958
+ "crs",
959
+ ]
960
+
961
+ mosaic = mosaic.setDefaultProjection(
962
+ first_image.projection()
963
+ ).copyProperties(first_image, props_of_interest)
793
964
 
794
965
  return list_accumulator.add(mosaic)
795
966
 
796
967
  # Get distinct dates
797
- distinct_dates = input_collection.aggregate_array('Date_Filter').distinct()
968
+ distinct_dates = input_collection.aggregate_array("Date_Filter").distinct()
798
969
 
799
970
  # Initialize an empty list as the accumulator
800
971
  initial = ee.List([])
@@ -808,9 +979,11 @@ class Sentinel1Collection:
808
979
 
809
980
  # Convert the list of mosaics to an ImageCollection
810
981
  return self._MosaicByDate
811
-
982
+
812
983
  @staticmethod
813
- def ee_to_df(ee_object, columns=None, remove_geom=True, sort_columns=False, **kwargs):
984
+ def ee_to_df(
985
+ ee_object, columns=None, remove_geom=True, sort_columns=False, **kwargs
986
+ ):
814
987
  """Converts an ee.FeatureCollection to pandas dataframe. Adapted from the geemap package (https://geemap.org/common/#geemap.common.ee_to_df)
815
988
 
816
989
  Args:
@@ -860,8 +1033,19 @@ class Sentinel1Collection:
860
1033
  raise Exception(e)
861
1034
 
862
1035
  @staticmethod
863
- 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):
864
-
1036
+ def extract_transect(
1037
+ image,
1038
+ line,
1039
+ reducer="mean",
1040
+ n_segments=100,
1041
+ dist_interval=None,
1042
+ scale=None,
1043
+ crs=None,
1044
+ crsTransform=None,
1045
+ tileScale=1.0,
1046
+ to_pandas=False,
1047
+ **kwargs,
1048
+ ):
865
1049
  """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.
866
1050
 
867
1051
  Args:
@@ -925,9 +1109,17 @@ class Sentinel1Collection:
925
1109
 
926
1110
  except Exception as e:
927
1111
  raise Exception(e)
928
-
1112
+
929
1113
  @staticmethod
930
- def transect(image, lines, line_names, reducer='mean', n_segments=None, dist_interval=10, to_pandas=True):
1114
+ def transect(
1115
+ image,
1116
+ lines,
1117
+ line_names,
1118
+ reducer="mean",
1119
+ n_segments=None,
1120
+ dist_interval=10,
1121
+ to_pandas=True,
1122
+ ):
931
1123
  """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
932
1124
  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.
933
1125
  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.
@@ -944,46 +1136,71 @@ class Sentinel1Collection:
944
1136
  Returns:
945
1137
  pd.DataFrame or ee.FeatureCollection: organized list of values along the transect(s)
946
1138
  """
947
- #Create empty dataframe
1139
+ # Create empty dataframe
948
1140
  transects_df = pd.DataFrame()
949
1141
 
950
- #Check if line is a list of lines or a single line - if single line, convert to list
1142
+ # Check if line is a list of lines or a single line - if single line, convert to list
951
1143
  if isinstance(lines, list):
952
1144
  pass
953
1145
  else:
954
1146
  lines = [lines]
955
-
1147
+
956
1148
  for i, line in enumerate(lines):
957
1149
  if n_segments is None:
958
- transect_data = Sentinel1Collection.extract_transect(image=image, line=line, reducer=reducer, dist_interval=dist_interval, to_pandas=to_pandas)
1150
+ transect_data = Sentinel1Collection.extract_transect(
1151
+ image=image,
1152
+ line=line,
1153
+ reducer=reducer,
1154
+ dist_interval=dist_interval,
1155
+ to_pandas=to_pandas,
1156
+ )
959
1157
  if reducer in transect_data.columns:
960
1158
  # Extract the 'mean' column and rename it
961
- mean_column = transect_data[['mean']]
1159
+ mean_column = transect_data[["mean"]]
962
1160
  else:
963
1161
  # Handle the case where 'mean' column is not present
964
- print(f"{reducer} column not found in transect data for line {line_names[i]}")
1162
+ print(
1163
+ f"{reducer} column not found in transect data for line {line_names[i]}"
1164
+ )
965
1165
  # Create a column of NaNs with the same length as the longest column in transects_df
966
1166
  max_length = max(transects_df.shape[0], transect_data.shape[0])
967
1167
  mean_column = pd.Series([np.nan] * max_length)
968
1168
  else:
969
- transect_data = Sentinel1Collection.extract_transect(image=image, line=line, reducer=reducer, n_segments=n_segments, to_pandas=to_pandas)
1169
+ transect_data = Sentinel1Collection.extract_transect(
1170
+ image=image,
1171
+ line=line,
1172
+ reducer=reducer,
1173
+ n_segments=n_segments,
1174
+ to_pandas=to_pandas,
1175
+ )
970
1176
  if reducer in transect_data.columns:
971
1177
  # Extract the 'mean' column and rename it
972
- mean_column = transect_data[['mean']]
1178
+ mean_column = transect_data[["mean"]]
973
1179
  else:
974
1180
  # Handle the case where 'mean' column is not present
975
- print(f"{reducer} column not found in transect data for line {line_names[i]}")
1181
+ print(
1182
+ f"{reducer} column not found in transect data for line {line_names[i]}"
1183
+ )
976
1184
  # Create a column of NaNs with the same length as the longest column in transects_df
977
1185
  max_length = max(transects_df.shape[0], transect_data.shape[0])
978
1186
  mean_column = pd.Series([np.nan] * max_length)
979
-
1187
+
980
1188
  transects_df = pd.concat([transects_df, mean_column], axis=1)
981
1189
 
982
1190
  transects_df.columns = line_names
983
-
1191
+
984
1192
  return transects_df
985
-
986
- def transect_iterator(self, lines, line_names, save_folder_path, reducer='mean', n_segments=None, dist_interval=10, to_pandas=True):
1193
+
1194
+ def transect_iterator(
1195
+ self,
1196
+ lines,
1197
+ line_names,
1198
+ save_folder_path,
1199
+ reducer="mean",
1200
+ n_segments=None,
1201
+ dist_interval=10,
1202
+ to_pandas=True,
1203
+ ):
987
1204
  """Computes and stores the values along a transect for each line in a list of lines for each image in a Sentinel1Collection image collection, then saves the data for each image to a csv file. Builds off of the extract_transect function from the geemap package
988
1205
  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.
989
1206
  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.
@@ -1004,21 +1221,37 @@ class Sentinel1Collection:
1004
1221
  Returns:
1005
1222
  csv file: file for each image with an organized list of values along the transect(s)
1006
1223
  """
1007
- image_collection = self
1224
+ image_collection = self
1008
1225
  image_collection_dates = self.dates
1009
1226
  for i, date in enumerate(image_collection_dates):
1010
1227
  try:
1011
1228
  print(f"Processing image {i+1}/{len(image_collection_dates)}: {date}")
1012
1229
  image = image_collection.image_grab(i)
1013
- transects_df = Sentinel1Collection.transect(image, lines, line_names, reducer=reducer, n_segments=n_segments, dist_interval=dist_interval, to_pandas=to_pandas)
1230
+ transects_df = Sentinel1Collection.transect(
1231
+ image,
1232
+ lines,
1233
+ line_names,
1234
+ reducer=reducer,
1235
+ n_segments=n_segments,
1236
+ dist_interval=dist_interval,
1237
+ to_pandas=to_pandas,
1238
+ )
1014
1239
  image_id = date
1015
- transects_df.to_csv(f'{save_folder_path}{image_id}_transects.csv')
1016
- print(f'{image_id}_transects saved to csv')
1240
+ transects_df.to_csv(f"{save_folder_path}{image_id}_transects.csv")
1241
+ print(f"{image_id}_transects saved to csv")
1017
1242
  except Exception as e:
1018
1243
  print(f"An error occurred while processing image {i+1}: {e}")
1019
1244
 
1020
1245
  @staticmethod
1021
- def extract_zonal_stats_from_buffer(image, coordinates, buffer_size=1, reducer_type='mean', scale=40, tileScale=1, coordinate_names=None):
1246
+ def extract_zonal_stats_from_buffer(
1247
+ image,
1248
+ coordinates,
1249
+ buffer_size=1,
1250
+ reducer_type="mean",
1251
+ scale=40,
1252
+ tileScale=1,
1253
+ coordinate_names=None,
1254
+ ):
1022
1255
  """
1023
1256
  Function to extract spatial statistics from an image for a list of coordinates, providing individual statistics for each location.
1024
1257
  A radial buffer is applied around each coordinate to extract the statistics, which defaults to 1 meter.
@@ -1040,15 +1273,26 @@ class Sentinel1Collection:
1040
1273
  # Check if coordinates is a single tuple and convert it to a list of tuples if necessary
1041
1274
  if isinstance(coordinates, tuple) and len(coordinates) == 2:
1042
1275
  coordinates = [coordinates]
1043
- elif not (isinstance(coordinates, list) and all(isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates)):
1044
- raise ValueError("Coordinates must be a list of tuples with two elements each (latitude, longitude).")
1045
-
1276
+ elif not (
1277
+ isinstance(coordinates, list)
1278
+ and all(
1279
+ isinstance(coord, tuple) and len(coord) == 2 for coord in coordinates
1280
+ )
1281
+ ):
1282
+ raise ValueError(
1283
+ "Coordinates must be a list of tuples with two elements each (latitude, longitude)."
1284
+ )
1285
+
1046
1286
  # Check if coordinate_names is a list of strings
1047
1287
  if coordinate_names is not None:
1048
- if not isinstance(coordinate_names, list) or not all(isinstance(name, str) for name in coordinate_names):
1288
+ if not isinstance(coordinate_names, list) or not all(
1289
+ isinstance(name, str) for name in coordinate_names
1290
+ ):
1049
1291
  raise ValueError("coordinate_names must be a list of strings.")
1050
1292
  if len(coordinate_names) != len(coordinates):
1051
- raise ValueError("coordinate_names must have the same length as the coordinates list.")
1293
+ raise ValueError(
1294
+ "coordinate_names must have the same length as the coordinates list."
1295
+ )
1052
1296
  else:
1053
1297
  coordinate_names = [f"Location {i+1}" for i in range(len(coordinates))]
1054
1298
 
@@ -1060,75 +1304,97 @@ class Sentinel1Collection:
1060
1304
  # Check if the image is a singleband image
1061
1305
  image = ee.Image(check_singleband(image))
1062
1306
 
1063
- #Convert coordinates to ee.Geometry.Point, buffer them, and add label/name to feature
1064
- points = [ee.Feature(ee.Geometry.Point([coord[0], coord[1]]).buffer(buffer_size), {'name': str(coordinate_names[i])}) for i, coord in enumerate(coordinates)]
1307
+ # Convert coordinates to ee.Geometry.Point, buffer them, and add label/name to feature
1308
+ points = [
1309
+ ee.Feature(
1310
+ ee.Geometry.Point([coord[0], coord[1]]).buffer(buffer_size),
1311
+ {"name": str(coordinate_names[i])},
1312
+ )
1313
+ for i, coord in enumerate(coordinates)
1314
+ ]
1065
1315
  # Create a feature collection from the buffered points
1066
1316
  features = ee.FeatureCollection(points)
1067
1317
  # Reduce the image to the buffered points - handle different reducer types
1068
- if reducer_type == 'mean':
1318
+ if reducer_type == "mean":
1069
1319
  img_stats = image.reduceRegions(
1070
- collection=features,
1071
- reducer=ee.Reducer.mean(),
1072
- scale=scale,
1073
- tileScale=tileScale)
1320
+ collection=features,
1321
+ reducer=ee.Reducer.mean(),
1322
+ scale=scale,
1323
+ tileScale=tileScale,
1324
+ )
1074
1325
  mean_values = img_stats.getInfo()
1075
1326
  means = []
1076
1327
  names = []
1077
- for feature in mean_values['features']:
1078
- names.append(feature['properties']['name'])
1079
- means.append(feature['properties']['mean'])
1328
+ for feature in mean_values["features"]:
1329
+ names.append(feature["properties"]["name"])
1330
+ means.append(feature["properties"]["mean"])
1080
1331
  organized_values = pd.DataFrame([means], columns=names)
1081
- elif reducer_type == 'median':
1332
+ elif reducer_type == "median":
1082
1333
  img_stats = image.reduceRegions(
1083
- collection=features,
1084
- reducer=ee.Reducer.median(),
1085
- scale=scale,
1086
- tileScale=tileScale)
1334
+ collection=features,
1335
+ reducer=ee.Reducer.median(),
1336
+ scale=scale,
1337
+ tileScale=tileScale,
1338
+ )
1087
1339
  median_values = img_stats.getInfo()
1088
1340
  medians = []
1089
1341
  names = []
1090
- for feature in median_values['features']:
1091
- names.append(feature['properties']['name'])
1092
- medians.append(feature['properties']['median'])
1342
+ for feature in median_values["features"]:
1343
+ names.append(feature["properties"]["name"])
1344
+ medians.append(feature["properties"]["median"])
1093
1345
  organized_values = pd.DataFrame([medians], columns=names)
1094
- elif reducer_type == 'min':
1346
+ elif reducer_type == "min":
1095
1347
  img_stats = image.reduceRegions(
1096
- collection=features,
1097
- reducer=ee.Reducer.min(),
1098
- scale=scale,
1099
- tileScale=tileScale)
1348
+ collection=features,
1349
+ reducer=ee.Reducer.min(),
1350
+ scale=scale,
1351
+ tileScale=tileScale,
1352
+ )
1100
1353
  min_values = img_stats.getInfo()
1101
1354
  mins = []
1102
1355
  names = []
1103
- for feature in min_values['features']:
1104
- names.append(feature['properties']['name'])
1105
- mins.append(feature['properties']['min'])
1356
+ for feature in min_values["features"]:
1357
+ names.append(feature["properties"]["name"])
1358
+ mins.append(feature["properties"]["min"])
1106
1359
  organized_values = pd.DataFrame([mins], columns=names)
1107
- elif reducer_type == 'max':
1360
+ elif reducer_type == "max":
1108
1361
  img_stats = image.reduceRegions(
1109
- collection=features,
1110
- reducer=ee.Reducer.max(),
1111
- scale=scale,
1112
- tileScale=tileScale)
1362
+ collection=features,
1363
+ reducer=ee.Reducer.max(),
1364
+ scale=scale,
1365
+ tileScale=tileScale,
1366
+ )
1113
1367
  max_values = img_stats.getInfo()
1114
1368
  maxs = []
1115
1369
  names = []
1116
- for feature in max_values['features']:
1117
- names.append(feature['properties']['name'])
1118
- maxs.append(feature['properties']['max'])
1370
+ for feature in max_values["features"]:
1371
+ names.append(feature["properties"]["name"])
1372
+ maxs.append(feature["properties"]["max"])
1119
1373
  organized_values = pd.DataFrame([maxs], columns=names)
1120
1374
  else:
1121
- raise ValueError("reducer_type must be one of 'mean', 'median', 'min', or 'max'.")
1375
+ raise ValueError(
1376
+ "reducer_type must be one of 'mean', 'median', 'min', or 'max'."
1377
+ )
1122
1378
  return organized_values
1123
1379
 
1124
- def iterate_zonal_stats(self, coordinates, buffer_size=1, reducer_type='mean', scale=40, tileScale=1, coordinate_names=None, file_path=None, dates=None):
1380
+ def iterate_zonal_stats(
1381
+ self,
1382
+ coordinates,
1383
+ buffer_size=1,
1384
+ reducer_type="mean",
1385
+ scale=40,
1386
+ tileScale=1,
1387
+ coordinate_names=None,
1388
+ file_path=None,
1389
+ dates=None,
1390
+ ):
1125
1391
  """
1126
1392
  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.
1127
1393
  A radial buffer is applied around each coordinate to extract the statistics, which defaults to 1 meter.
1128
1394
  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.
1129
1395
 
1130
1396
  NOTE: The input RadGEEToolbox class object but be a collection of singleband images or else resulting values will all be zero!
1131
-
1397
+
1132
1398
  Args:
1133
1399
  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), ...].
1134
1400
  buffer_size (int, optional): The radial buffer size in meters around the coordinates. Defaults to 1.
@@ -1144,22 +1410,32 @@ class Sentinel1Collection:
1144
1410
  .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.
1145
1411
  """
1146
1412
  img_collection = self
1147
- #Create empty DataFrame to accumulate results
1413
+ # Create empty DataFrame to accumulate results
1148
1414
  accumulated_df = pd.DataFrame()
1149
- #Check if dates is None, if not use the dates provided
1415
+ # Check if dates is None, if not use the dates provided
1150
1416
  if dates is None:
1151
1417
  dates = img_collection.dates
1152
1418
  else:
1153
1419
  dates = dates
1154
- #Iterate over the dates and extract the zonal statistics for each date
1420
+ # Iterate over the dates and extract the zonal statistics for each date
1155
1421
  for date in dates:
1156
- image = img_collection.collection.filter(ee.Filter.eq('Date_Filter', date)).first()
1157
- single_df = Sentinel1Collection.extract_zonal_stats_from_buffer(image, coordinates, buffer_size=buffer_size, reducer_type=reducer_type, scale=scale, tileScale=tileScale, coordinate_names=coordinate_names)
1158
- single_df['Date'] = date
1159
- single_df.set_index('Date', inplace=True)
1422
+ image = img_collection.collection.filter(
1423
+ ee.Filter.eq("Date_Filter", date)
1424
+ ).first()
1425
+ single_df = Sentinel1Collection.extract_zonal_stats_from_buffer(
1426
+ image,
1427
+ coordinates,
1428
+ buffer_size=buffer_size,
1429
+ reducer_type=reducer_type,
1430
+ scale=scale,
1431
+ tileScale=tileScale,
1432
+ coordinate_names=coordinate_names,
1433
+ )
1434
+ single_df["Date"] = date
1435
+ single_df.set_index("Date", inplace=True)
1160
1436
  accumulated_df = pd.concat([accumulated_df, single_df])
1161
- #Return the DataFrame or export the data to a .csv file
1437
+ # Return the DataFrame or export the data to a .csv file
1162
1438
  if file_path is None:
1163
1439
  return accumulated_df
1164
1440
  else:
1165
- return accumulated_df.to_csv(f'{file_path}.csv')
1441
+ return accumulated_df.to_csv(f"{file_path}.csv")