gee-acolite 1.0.0__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.
- gee_acolite/__init__.py +49 -0
- gee_acolite/bathymetry.py +4 -0
- gee_acolite/correction.py +567 -0
- gee_acolite/sensors/__init__.py +10 -0
- gee_acolite/sensors/sentinel2.py +6 -0
- gee_acolite/utils/__init__.py +44 -0
- gee_acolite/utils/l1_convert.py +47 -0
- gee_acolite/utils/masks.py +113 -0
- gee_acolite/utils/search.py +79 -0
- gee_acolite/water_quality.py +140 -0
- gee_acolite-1.0.0.dist-info/METADATA +161 -0
- gee_acolite-1.0.0.dist-info/RECORD +15 -0
- gee_acolite-1.0.0.dist-info/WHEEL +5 -0
- gee_acolite-1.0.0.dist-info/licenses/LICENSE +21 -0
- gee_acolite-1.0.0.dist-info/top_level.txt +1 -0
gee_acolite/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GEE ACOLITE - Atmospheric correction for Google Earth Engine
|
|
3
|
+
|
|
4
|
+
This package provides ACOLITE atmospheric correction optimized for
|
|
5
|
+
Google Earth Engine workflows with Sentinel-2 imagery.
|
|
6
|
+
|
|
7
|
+
Main Features:
|
|
8
|
+
- Dark spectrum fitting atmospheric correction
|
|
9
|
+
- AOT estimation (fixed geometry)
|
|
10
|
+
- Water quality parameter computation
|
|
11
|
+
- Cloud masking integration
|
|
12
|
+
|
|
13
|
+
Note: Requires ACOLITE package to be installed separately.
|
|
14
|
+
Install from: https://github.com/acolite/acolite
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__author__ = "Sergio"
|
|
19
|
+
__license__ = "GPL-3.0"
|
|
20
|
+
|
|
21
|
+
# Main atmospheric correction class
|
|
22
|
+
from gee_acolite.correction import ACOLITE
|
|
23
|
+
|
|
24
|
+
# Bathymetry utilities
|
|
25
|
+
from gee_acolite.bathymetry import multi_image
|
|
26
|
+
|
|
27
|
+
# Water quality parameters
|
|
28
|
+
from gee_acolite.water_quality import (
|
|
29
|
+
compute_water_bands,
|
|
30
|
+
compute_water_mask,
|
|
31
|
+
PRODUCTS
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Utility modules
|
|
35
|
+
from gee_acolite import utils, sensors
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Main class
|
|
39
|
+
"ACOLITE",
|
|
40
|
+
# Bathymetry
|
|
41
|
+
"multi_image",
|
|
42
|
+
# Water quality
|
|
43
|
+
"compute_water_bands",
|
|
44
|
+
"compute_water_mask",
|
|
45
|
+
"PRODUCTS",
|
|
46
|
+
# Modules
|
|
47
|
+
"utils",
|
|
48
|
+
"sensors",
|
|
49
|
+
]
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.stats
|
|
3
|
+
import ee
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from typing import List, Tuple
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from functools import partial
|
|
10
|
+
|
|
11
|
+
from gee_acolite.utils.l1_convert import l1_to_rrs
|
|
12
|
+
from gee_acolite.utils.masks import mask_negative_reflectance
|
|
13
|
+
from gee_acolite.water_quality import compute_water_bands
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ACOLITE(object):
|
|
17
|
+
def __init__(self, acolite: ModuleType, settings: str | dict) -> None:
|
|
18
|
+
self.acolite = acolite
|
|
19
|
+
self.settings = self.__load_settings(settings)
|
|
20
|
+
|
|
21
|
+
def __load_settings(self, settings: str | dict):
|
|
22
|
+
# return self.acolite.acolite.settings.load(settings)
|
|
23
|
+
return self.acolite.acolite.settings.parse('S2A_MSI', settings = settings)
|
|
24
|
+
|
|
25
|
+
def correct(self, images : ee.ImageCollection) -> Tuple[ee.ImageCollection, dict]:
|
|
26
|
+
images = l1_to_rrs(images, self.settings.get('s2_target_res', 10))
|
|
27
|
+
images, settings = self.l1_to_l2(images.toList(images.size()), images.size().getInfo(), self.settings)
|
|
28
|
+
|
|
29
|
+
return images, settings
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def l1_to_l2(self, images : ee.List, size : int, settings : dict) -> Tuple[ee.ImageCollection, dict]:
|
|
33
|
+
corrected_images = []
|
|
34
|
+
|
|
35
|
+
if settings['aerosol_correction'] == 'dark_spectrum':
|
|
36
|
+
for index in range(size):
|
|
37
|
+
rhos, bands, glint_params = self.dask_spectrum_fitting(ee.Image(images.get(index)), settings)
|
|
38
|
+
|
|
39
|
+
if settings['dsf_residual_glint_correction'] and settings['dsf_residual_glint_correction_method'] == 'alternative':
|
|
40
|
+
rhos = self.deglint_alternative(rhos,
|
|
41
|
+
bands,
|
|
42
|
+
glint_params,
|
|
43
|
+
glint_max=float(settings.get('glint_mask_rhos_threshold', 0.05)))
|
|
44
|
+
|
|
45
|
+
rhos = rhos.copyProperties(ee.Image(images.get(index)))
|
|
46
|
+
rhos = rhos.set('system:time_start', ee.Image(images.get(index)).get('system:time_start'))
|
|
47
|
+
|
|
48
|
+
corrected_images.append(rhos)
|
|
49
|
+
|
|
50
|
+
corrected_images = ee.ImageCollection.fromImages(corrected_images)
|
|
51
|
+
|
|
52
|
+
if settings['l2w_parameters']:
|
|
53
|
+
corrected_images = corrected_images.map(partial(compute_water_bands, settings=settings))
|
|
54
|
+
|
|
55
|
+
return corrected_images, settings
|
|
56
|
+
|
|
57
|
+
def dask_spectrum_fitting(self, image : ee.Image, settings : dict) -> Tuple[ee.Image, List[str], dict]:
|
|
58
|
+
if settings.get('ancillary_data', False):
|
|
59
|
+
settings = self.get_ancillary_data(image, settings)
|
|
60
|
+
else:
|
|
61
|
+
for data, default in [(key, f'{key}_default') for key in ['uoz', 'uwv', 'wind', 'pressure']]:
|
|
62
|
+
settings[data] = settings.get(default)
|
|
63
|
+
|
|
64
|
+
# Check if fixed AOT and LUT are provided
|
|
65
|
+
if settings.get('dsf_fixed_aot') is not None and settings.get('dsf_fixed_lut') is not None:
|
|
66
|
+
# Use fixed AOT and LUT (bypass dark spectrum fitting)
|
|
67
|
+
am, glint_ave, bands = self.compute_correction_with_fixed_aot(
|
|
68
|
+
image,
|
|
69
|
+
float(settings['dsf_fixed_aot']),
|
|
70
|
+
settings['dsf_fixed_lut'],
|
|
71
|
+
settings
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
# Normal dark spectrum fitting workflow
|
|
75
|
+
am, glint_ave, bands = self.select_lut(image, settings)
|
|
76
|
+
|
|
77
|
+
rhos = self.compute_rhos(image, am)
|
|
78
|
+
|
|
79
|
+
return rhos, bands, glint_ave
|
|
80
|
+
|
|
81
|
+
def estimate_aot_per_lut(self, pdark: dict, lutd: dict, rsrd: dict, ttg: dict,
|
|
82
|
+
geometry: dict, settings: dict,
|
|
83
|
+
aot_skip_bands: List[str] = ['9', '10', '11', '12']) -> dict:
|
|
84
|
+
"""
|
|
85
|
+
Estimate AOT for each LUT model using dark spectrum fitting.
|
|
86
|
+
|
|
87
|
+
Parameters:
|
|
88
|
+
- pdark: Dark spectrum values per band
|
|
89
|
+
- lutd: LUT dictionary with atmospheric models
|
|
90
|
+
- rsrd: Relative spectral response dictionary
|
|
91
|
+
- ttg: Gas transmittance dictionary
|
|
92
|
+
- geometry: Dict with 'raa', 'vza', 'sza', 'pressure'
|
|
93
|
+
- settings: Processing settings
|
|
94
|
+
- aot_skip_bands: Bands to skip in AOT estimation
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
- results: Dict with AOT estimation results per LUT
|
|
98
|
+
"""
|
|
99
|
+
results = {}
|
|
100
|
+
nbands = settings['dsf_nbands']
|
|
101
|
+
|
|
102
|
+
raa = geometry['raa']
|
|
103
|
+
vza = geometry['vza']
|
|
104
|
+
sza = geometry['sza']
|
|
105
|
+
pressure = geometry['pressure']
|
|
106
|
+
|
|
107
|
+
for lut in lutd:
|
|
108
|
+
taua_arr = None
|
|
109
|
+
rhot_arr = None
|
|
110
|
+
taua_bands = []
|
|
111
|
+
|
|
112
|
+
# Run through bands to estimate AOT
|
|
113
|
+
for b in rsrd['rsr_bands']:
|
|
114
|
+
if b in aot_skip_bands:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Get path reflectance from LUT for this band
|
|
118
|
+
ret = lutd[lut]['rgi'][b]((pressure, lutd[lut]['ipd']['romix'],
|
|
119
|
+
raa, vza, sza, lutd[lut]['meta']['tau']))
|
|
120
|
+
|
|
121
|
+
# Dark spectrum value for this band
|
|
122
|
+
rhot = np.asarray([pdark['B{}'.format(b)]])
|
|
123
|
+
|
|
124
|
+
# Gas correction
|
|
125
|
+
rhot /= ttg['tt_gas'][b]
|
|
126
|
+
|
|
127
|
+
# Interpolate AOT from observed rhot
|
|
128
|
+
taua = np.interp(rhot, ret, lutd[lut]['meta']['tau'])
|
|
129
|
+
|
|
130
|
+
if taua_arr is None:
|
|
131
|
+
rhot_arr = 1.0 * rhot
|
|
132
|
+
taua_arr = 1.0 * taua
|
|
133
|
+
else:
|
|
134
|
+
rhot_arr = np.vstack((rhot_arr, rhot))
|
|
135
|
+
taua_arr = np.vstack((taua_arr, taua))
|
|
136
|
+
|
|
137
|
+
taua_bands.append(b)
|
|
138
|
+
|
|
139
|
+
# Aggregate AOT from darkest bands
|
|
140
|
+
bidx = np.argsort(taua_arr[:, 0])
|
|
141
|
+
taua = np.nanmean(taua_arr[bidx[0: nbands], 0])
|
|
142
|
+
taua_std = np.nanstd(taua_arr[bidx[0: nbands], 0])
|
|
143
|
+
taua_cv = taua_std / taua
|
|
144
|
+
|
|
145
|
+
# Store results for this LUT
|
|
146
|
+
results[lut] = {
|
|
147
|
+
'taua_bands': taua_bands,
|
|
148
|
+
'taua_arr': taua_arr,
|
|
149
|
+
'rhot_arr': rhot_arr,
|
|
150
|
+
'taua': taua,
|
|
151
|
+
'taua_std': taua_std,
|
|
152
|
+
'taua_cv': taua_cv,
|
|
153
|
+
'bidx': bidx
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return results
|
|
157
|
+
|
|
158
|
+
def select_best_model(self, results: dict, lutd: dict, geometry: dict,
|
|
159
|
+
settings: dict) -> Tuple[str, float, str, float]:
|
|
160
|
+
"""
|
|
161
|
+
Select best atmospheric model based on selection criterion.
|
|
162
|
+
|
|
163
|
+
Parameters:
|
|
164
|
+
- results: AOT estimation results per LUT from estimate_aot_per_lut
|
|
165
|
+
- lutd: LUT dictionary with atmospheric models
|
|
166
|
+
- geometry: Dict with 'raa', 'vza', 'sza', 'pressure'
|
|
167
|
+
- settings: Processing settings with selection method
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
- sel_lut: Selected LUT name
|
|
171
|
+
- sel_aot: Selected AOT value
|
|
172
|
+
- sel_par: Selection parameter name
|
|
173
|
+
- sel_val: Selection parameter value
|
|
174
|
+
"""
|
|
175
|
+
dsf_model_selection = settings.get('dsf_model_selection', 'min_drmsd')
|
|
176
|
+
dsf_nbands_fit = settings.get('dsf_nbands_fit', 2)
|
|
177
|
+
|
|
178
|
+
pressure = geometry['pressure']
|
|
179
|
+
raa = geometry['raa']
|
|
180
|
+
vza = geometry['vza']
|
|
181
|
+
sza = geometry['sza']
|
|
182
|
+
|
|
183
|
+
if dsf_model_selection == 'min_drmsd':
|
|
184
|
+
# RMSD validation: compare predictions vs observations
|
|
185
|
+
for lut in results:
|
|
186
|
+
fit_band_indices = results[lut]['bidx'][0:dsf_nbands_fit]
|
|
187
|
+
|
|
188
|
+
rmsd_values = []
|
|
189
|
+
for fit_idx in fit_band_indices:
|
|
190
|
+
band = results[lut]['taua_bands'][fit_idx]
|
|
191
|
+
|
|
192
|
+
# Observed rhot (gas corrected)
|
|
193
|
+
rhot_obs = results[lut]['rhot_arr'][fit_idx, 0]
|
|
194
|
+
|
|
195
|
+
# Modeled rhot using estimated AOT
|
|
196
|
+
rhot_model = lutd[lut]['rgi'][band]((pressure, lutd[lut]['ipd']['romix'],
|
|
197
|
+
raa, vza, sza, results[lut]['taua']))
|
|
198
|
+
|
|
199
|
+
# Squared difference
|
|
200
|
+
rmsd_values.append((rhot_obs - rhot_model) ** 2)
|
|
201
|
+
|
|
202
|
+
# Compute RMSD
|
|
203
|
+
results[lut]['rmsd'] = np.sqrt(np.mean(rmsd_values))
|
|
204
|
+
|
|
205
|
+
sel_par = 'rmsd'
|
|
206
|
+
|
|
207
|
+
elif dsf_model_selection == 'min_dtau':
|
|
208
|
+
# Minimum delta tau between two darkest bands
|
|
209
|
+
for lut in results:
|
|
210
|
+
if len(results[lut]['taua_arr']) >= 2:
|
|
211
|
+
dtau = np.abs(results[lut]['taua_arr'][results[lut]['bidx'][0], 0] -
|
|
212
|
+
results[lut]['taua_arr'][results[lut]['bidx'][1], 0])
|
|
213
|
+
results[lut]['dtau'] = dtau
|
|
214
|
+
else:
|
|
215
|
+
results[lut]['dtau'] = np.inf
|
|
216
|
+
|
|
217
|
+
sel_par = 'dtau'
|
|
218
|
+
|
|
219
|
+
else:
|
|
220
|
+
# Fallback: coefficient of variation
|
|
221
|
+
sel_par = 'taua_cv'
|
|
222
|
+
|
|
223
|
+
# Select LUT with minimum selection parameter
|
|
224
|
+
sel_lut = None
|
|
225
|
+
sel_aot = None
|
|
226
|
+
sel_val = np.inf
|
|
227
|
+
|
|
228
|
+
for lut in results:
|
|
229
|
+
if results[lut][sel_par] < sel_val:
|
|
230
|
+
sel_val = results[lut][sel_par] * 1.0
|
|
231
|
+
sel_aot = results[lut]['taua'] * 1.0
|
|
232
|
+
sel_lut = '{}'.format(lut)
|
|
233
|
+
|
|
234
|
+
return sel_lut, sel_aot, sel_par, sel_val
|
|
235
|
+
|
|
236
|
+
def select_lut(self, image : ee.Image, settings : dict, aot_skip_bands : List[str] = ['9', '10', '11', '12']) -> Tuple[dict, dict, List[str]]:
|
|
237
|
+
"""
|
|
238
|
+
Main function for LUT selection and atmospheric correction parameter computation.
|
|
239
|
+
|
|
240
|
+
This function orchestrates:
|
|
241
|
+
1. Dark spectrum extraction
|
|
242
|
+
2. AOT estimation per LUT model
|
|
243
|
+
3. Model selection based on validation criterion
|
|
244
|
+
4. Computation of atmospheric correction parameters
|
|
245
|
+
|
|
246
|
+
Parameters:
|
|
247
|
+
- image: Input ee.Image
|
|
248
|
+
- settings: Processing settings
|
|
249
|
+
- aot_skip_bands: Bands to skip in AOT estimation
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
- am: Atmospheric correction parameters (romix, dutott, astot, tg)
|
|
253
|
+
- glint_ave: Glint correction parameters per band
|
|
254
|
+
- bands: List of band names
|
|
255
|
+
"""
|
|
256
|
+
# Extract dark spectrum
|
|
257
|
+
pdark = self.compute_pdark(image, settings)
|
|
258
|
+
|
|
259
|
+
# Get geometry and atmospheric parameters
|
|
260
|
+
raa = image.get('raa').getInfo()
|
|
261
|
+
vza = image.get('vza').getInfo()
|
|
262
|
+
sza = image.get('sza').getInfo()
|
|
263
|
+
|
|
264
|
+
geometry = {
|
|
265
|
+
'raa': raa,
|
|
266
|
+
'vza': vza,
|
|
267
|
+
'sza': sza,
|
|
268
|
+
'pressure': settings['pressure']
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Get sensor and load LUTs
|
|
272
|
+
sensor = 'S2A_MSI' if 'S2A' in image.get('PRODUCT_ID').getInfo() else 'S2B_MSI'
|
|
273
|
+
|
|
274
|
+
lutd = self.acolite.aerlut.import_luts(sensor=sensor)
|
|
275
|
+
rsrd = self.acolite.shared.rsr_dict(sensor=sensor)[sensor]
|
|
276
|
+
ttg = self.acolite.ac.gas_transmittance(sza, vza,
|
|
277
|
+
pressure=settings['pressure'],
|
|
278
|
+
uoz=settings['uoz'],
|
|
279
|
+
uwv=settings['uwv'],
|
|
280
|
+
rsr=rsrd['rsr'])
|
|
281
|
+
luti = self.acolite.aerlut.import_rsky_luts(models=[1,2],
|
|
282
|
+
lutbase='ACOLITE-RSKY-202102-82W',
|
|
283
|
+
sensor=sensor)
|
|
284
|
+
|
|
285
|
+
# Step 1: Estimate AOT for each LUT model
|
|
286
|
+
results = self.estimate_aot_per_lut(pdark, lutd, rsrd, ttg, geometry,
|
|
287
|
+
settings, aot_skip_bands)
|
|
288
|
+
|
|
289
|
+
# Step 2: Select best model
|
|
290
|
+
sel_lut, sel_aot, sel_par, sel_val = self.select_best_model(results, lutd,
|
|
291
|
+
geometry, settings)
|
|
292
|
+
|
|
293
|
+
print(f'Selected model {sel_lut}: AOT={sel_aot:.3f}, {sel_par}={sel_val:.4e}')
|
|
294
|
+
print(f' Geometry: SZA={geometry["sza"]:.2f}°, VZA={geometry["vza"]:.2f}°, RAA={geometry["raa"]:.2f}°')
|
|
295
|
+
print(f' Pressure: {geometry["pressure"]:.2f} hPa')
|
|
296
|
+
|
|
297
|
+
# Step 3: Compute atmospheric correction parameters for selected model
|
|
298
|
+
am = {}
|
|
299
|
+
for par in lutd[sel_lut]['ipd']:
|
|
300
|
+
am[par] = {b: lutd[sel_lut]['rgi'][b]((geometry['pressure'],
|
|
301
|
+
lutd[sel_lut]['ipd'][par],
|
|
302
|
+
raa, vza, sza, sel_aot))
|
|
303
|
+
for b in rsrd['rsr_bands']}
|
|
304
|
+
am.update({'tg': ttg['tt_gas']})
|
|
305
|
+
|
|
306
|
+
# Step 4: Compute glint correction parameters
|
|
307
|
+
model = int(sel_lut[-1])
|
|
308
|
+
glint_wind = 20
|
|
309
|
+
glint_bands = ['11', '12']
|
|
310
|
+
|
|
311
|
+
glint_dict = {b: luti[model]['rgi'][b]((raa, vza, sza, glint_wind, sel_aot))
|
|
312
|
+
for b in rsrd['rsr_bands']}
|
|
313
|
+
glint_ave = {b: glint_dict[b] / ((glint_dict[glint_bands[0]] +
|
|
314
|
+
glint_dict[glint_bands[1]]) / 2)
|
|
315
|
+
for b in glint_dict}
|
|
316
|
+
|
|
317
|
+
return am, glint_ave, rsrd['rsr_bands']
|
|
318
|
+
|
|
319
|
+
def compute_correction_with_fixed_aot(self, image: ee.Image, aot: float, lut_name: str,
|
|
320
|
+
settings: dict) -> Tuple[dict, dict, List[str]]:
|
|
321
|
+
"""
|
|
322
|
+
Compute atmospheric correction parameters using fixed AOT and LUT.
|
|
323
|
+
|
|
324
|
+
This function bypasses dark spectrum fitting and directly computes
|
|
325
|
+
correction parameters for a specified AOT and atmospheric model.
|
|
326
|
+
|
|
327
|
+
Parameters:
|
|
328
|
+
- image: Input ee.Image (TOA reflectance)
|
|
329
|
+
- aot: Fixed aerosol optical thickness at 550nm
|
|
330
|
+
- lut_name: LUT name (e.g., 'ACOLITE-LUT-202110-MOD2')
|
|
331
|
+
- settings: Processing settings
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
- am: Atmospheric correction parameters (romix, dutott, astot, tg)
|
|
335
|
+
- glint_ave: Glint correction parameters per band
|
|
336
|
+
- bands: List of band names
|
|
337
|
+
"""
|
|
338
|
+
# Extract geometry from image
|
|
339
|
+
raa = image.get('raa').getInfo()
|
|
340
|
+
vza = image.get('vza').getInfo()
|
|
341
|
+
sza = image.get('sza').getInfo()
|
|
342
|
+
|
|
343
|
+
geometry = {
|
|
344
|
+
'raa': raa,
|
|
345
|
+
'vza': vza,
|
|
346
|
+
'sza': sza,
|
|
347
|
+
'pressure': settings['pressure']
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
# Get sensor
|
|
351
|
+
sensor = 'S2A_MSI' if 'S2A' in image.get('PRODUCT_ID').getInfo() else 'S2B_MSI'
|
|
352
|
+
|
|
353
|
+
# Load specified LUT
|
|
354
|
+
lutd = self.acolite.aerlut.import_luts(sensor=sensor)
|
|
355
|
+
|
|
356
|
+
# Verify LUT exists
|
|
357
|
+
if lut_name not in lutd:
|
|
358
|
+
available_luts = list(lutd.keys())
|
|
359
|
+
raise ValueError(f"LUT '{lut_name}' not found. Available LUTs: {available_luts}")
|
|
360
|
+
|
|
361
|
+
# Get RSR data
|
|
362
|
+
rsrd = self.acolite.shared.rsr_dict(sensor=sensor)[sensor]
|
|
363
|
+
pressure = settings['pressure']
|
|
364
|
+
uoz = settings['uoz']
|
|
365
|
+
uwv = settings['uwv']
|
|
366
|
+
|
|
367
|
+
# Compute gas transmittance
|
|
368
|
+
ttg = self.acolite.ac.gas_transmittance(sza, vza,
|
|
369
|
+
pressure=pressure,
|
|
370
|
+
uoz=uoz,
|
|
371
|
+
uwv=uwv,
|
|
372
|
+
rsr=rsrd['rsr'])
|
|
373
|
+
|
|
374
|
+
# Load sky glint LUTs
|
|
375
|
+
luti = self.acolite.aerlut.import_rsky_luts(models=[1,2],
|
|
376
|
+
lutbase='ACOLITE-RSKY-202102-82W',
|
|
377
|
+
sensor=sensor)
|
|
378
|
+
|
|
379
|
+
print(f'Using fixed AOT={aot:.3f} with LUT={lut_name}')
|
|
380
|
+
print(f' Geometry: SZA={geometry["sza"]:.2f}°, VZA={geometry["vza"]:.2f}°, RAA={geometry["raa"]:.2f}°')
|
|
381
|
+
print(f' Pressure: {geometry["pressure"]:.2f} hPa')
|
|
382
|
+
|
|
383
|
+
# Compute atmospheric correction parameters for specified LUT and AOT
|
|
384
|
+
am = {}
|
|
385
|
+
for par in lutd[lut_name]['ipd']:
|
|
386
|
+
am[par] = {b: lutd[lut_name]['rgi'][b]((geometry['pressure'],
|
|
387
|
+
lutd[lut_name]['ipd'][par],
|
|
388
|
+
raa, vza, sza, aot))
|
|
389
|
+
for b in rsrd['rsr_bands']}
|
|
390
|
+
am.update({'tg': ttg['tt_gas']})
|
|
391
|
+
|
|
392
|
+
# Compute glint correction parameters
|
|
393
|
+
model = int(lut_name[-1]) # Extract model number from LUT name
|
|
394
|
+
glint_wind = 20
|
|
395
|
+
glint_bands = ['11', '12']
|
|
396
|
+
|
|
397
|
+
glint_dict = {b: luti[model]['rgi'][b]((raa, vza, sza, glint_wind, aot))
|
|
398
|
+
for b in rsrd['rsr_bands']}
|
|
399
|
+
glint_ave = {b: glint_dict[b] / ((glint_dict[glint_bands[0]] +
|
|
400
|
+
glint_dict[glint_bands[1]]) / 2)
|
|
401
|
+
for b in glint_dict}
|
|
402
|
+
|
|
403
|
+
return am, glint_ave, rsrd['rsr_bands']
|
|
404
|
+
|
|
405
|
+
def compute_pdark(self, image : ee.Image, settings : dict):
|
|
406
|
+
obands_rhot = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B8A', 'B9', 'B10', 'B11', 'B12']
|
|
407
|
+
|
|
408
|
+
image_to_reduce = image.updateMask(image.gt(0))
|
|
409
|
+
|
|
410
|
+
pdark_by_band = {}
|
|
411
|
+
indexes = np.arange(settings['dsf_intercept_pixels'])
|
|
412
|
+
|
|
413
|
+
for band in obands_rhot:
|
|
414
|
+
if settings.get('dsf_spectrum_option', 'darkest') == 'darkest':
|
|
415
|
+
band_data = image_to_reduce.select(band).reduceRegion(reducer = ee.Reducer.percentile([0]),
|
|
416
|
+
bestEffort = True, scale = 10, maxPixels = 1e8)
|
|
417
|
+
|
|
418
|
+
if band_data:
|
|
419
|
+
pdark_by_band[band] = band_data.getInfo()[band]
|
|
420
|
+
else:
|
|
421
|
+
pdark_by_band[band] = 0.0
|
|
422
|
+
|
|
423
|
+
elif settings.get('dsf_spectrum_option', 'darkest') == 'percentile':
|
|
424
|
+
band_data = image_to_reduce.select(band).reduceRegion(reducer = ee.Reducer.percentile([settings['dsf_percentile']]),
|
|
425
|
+
bestEffort = True, scale = 10, maxPixels = 1e8)
|
|
426
|
+
|
|
427
|
+
if band_data:
|
|
428
|
+
pdark_by_band[band] = band_data.getInfo()[band]
|
|
429
|
+
else:
|
|
430
|
+
pdark_by_band[band] = 0.0
|
|
431
|
+
elif settings.get('dsf_spectrum_option', 'darkest') == 'intercept':
|
|
432
|
+
data = image_to_reduce.select(band).reduceRegion(reducer = ee.Reducer.toList(), scale = 30, bestEffort = True, maxPixels = 1e8)
|
|
433
|
+
band_data = data.get(band)
|
|
434
|
+
|
|
435
|
+
if band_data:
|
|
436
|
+
values = ee.List(band_data).sort().slice(0, settings['dsf_intercept_pixels']).getInfo()
|
|
437
|
+
slope, intercept, r, p, se = scipy.stats.linregress(indexes, values)
|
|
438
|
+
pdark_by_band[band] = intercept
|
|
439
|
+
else:
|
|
440
|
+
pdark_by_band[band] = 0.0
|
|
441
|
+
|
|
442
|
+
return pdark_by_band
|
|
443
|
+
|
|
444
|
+
def compute_rhos(self, image : ee.Image, am : dict) -> ee.Image:
|
|
445
|
+
l2r_rrs = ee.Image().select([])
|
|
446
|
+
|
|
447
|
+
romix = am['romix']
|
|
448
|
+
dutott = am['dutott']
|
|
449
|
+
astot = am['astot']
|
|
450
|
+
tgas = am['tg']
|
|
451
|
+
|
|
452
|
+
for band in romix:
|
|
453
|
+
band_name = 'B' + band
|
|
454
|
+
rhot_noatm = ee.Image().expression('(data / tg) - ppath', {'data' : image.select(band_name), 'tg' : tgas[band], 'ppath' : float(romix[band])}).rename(band_name)
|
|
455
|
+
rhos = ee.Image().expression('(data) / (tdu + sa * data)', {'data' : rhot_noatm.select(band_name), 'tdu' : float(dutott[band]), 'sa' : float(astot[band])}).rename(band_name)
|
|
456
|
+
rhos = mask_negative_reflectance(rhos, band_name)
|
|
457
|
+
l2r_rrs = l2r_rrs.addBands(image.select(band_name).rename(f'rhot_{band_name}').toFloat())
|
|
458
|
+
l2r_rrs = l2r_rrs.addBands(rhos.rename(f'rhos_{band_name}').toFloat())
|
|
459
|
+
|
|
460
|
+
return l2r_rrs
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def get_ancillary_data(self, image: ee.Image, settings : dict) -> dict:
|
|
464
|
+
settings = self.prepare_earthdata_credentials(settings)
|
|
465
|
+
iso_date, lon, lat = self.prepare_query(image)
|
|
466
|
+
|
|
467
|
+
anc = self.acolite.ac.ancillary.get(iso_date, lon, lat)
|
|
468
|
+
|
|
469
|
+
for data, default in [('uoz', 'uoz_default'), ('uwv', 'uwv_default'),
|
|
470
|
+
('wind', 'wind_default'), ('pressure', 'pressure_default')]:
|
|
471
|
+
settings[data] = anc.get(data, settings.get(default))
|
|
472
|
+
|
|
473
|
+
return settings
|
|
474
|
+
|
|
475
|
+
def prepare_query(self, image: ee.Image):
|
|
476
|
+
coords = image.geometry().centroid().coordinates().getInfo()
|
|
477
|
+
iso_date = ee.Date(image.get('system:time_start')).format('YYYY-MM-dd HH:mm:ss').getInfo()
|
|
478
|
+
|
|
479
|
+
lon, lat = coords
|
|
480
|
+
return iso_date,lon,lat
|
|
481
|
+
|
|
482
|
+
def prepare_earthdata_credentials(self, settings: dict) -> dict:
|
|
483
|
+
for k in ['EARTHDATA_u', 'EARTHDATA_p']:
|
|
484
|
+
kv = settings[k] if k in settings else self.acolite.ac.config[k]
|
|
485
|
+
if len(kv) == 0: continue
|
|
486
|
+
os.environ[k] = kv
|
|
487
|
+
|
|
488
|
+
return settings
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def deglint_alternative(self, image : ee.Image, bands : List[str],
|
|
492
|
+
glint_ave : dict, glint_min : float = 0, glint_max : float = 0.08) -> ee.Image:
|
|
493
|
+
"""
|
|
494
|
+
Alternative glint correction method based on ACOLITE's acolite_l2r.py implementation.
|
|
495
|
+
|
|
496
|
+
This method:
|
|
497
|
+
1. Uses reference SWIR bands (B11, B12) to estimate observed glint
|
|
498
|
+
2. Computes average modeled surface reflectance (glint_ave) for reference bands
|
|
499
|
+
3. For each band, scales the observed glint by the ratio of:
|
|
500
|
+
- Modeled surface reflectance for current band
|
|
501
|
+
- Average modeled surface reflectance of reference bands
|
|
502
|
+
4. Subtracts the scaled glint from each band's rhos
|
|
503
|
+
|
|
504
|
+
Parameters:
|
|
505
|
+
- image: Image with rhos bands
|
|
506
|
+
- bands: List of band numbers (as strings)
|
|
507
|
+
- glint_ave: Dict with modeled surface reflectance ratio per band
|
|
508
|
+
- glint_min: Minimum threshold for glint mask (default: 0)
|
|
509
|
+
- glint_max: Maximum threshold for glint mask (glint_mask_rhos_threshold)
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
# Start with a copy of the original image to preserve all bands
|
|
513
|
+
deglinted = image
|
|
514
|
+
|
|
515
|
+
# Reference bands for glint estimation (SWIR bands: B11=1610nm, B12=2190nm)
|
|
516
|
+
# These bands are sensitive to sun glint but have minimal water-leaving radiance
|
|
517
|
+
glint_ref_bands = ['rhos_B11', 'rhos_B12']
|
|
518
|
+
|
|
519
|
+
# Step 1: Compute observed glint reference (gc_ref_mean in ACOLITE)
|
|
520
|
+
# Average of reference bands gives the observed glint signal
|
|
521
|
+
rhos_b11 = image.select('rhos_B11')
|
|
522
|
+
rhos_b12 = image.select('rhos_B12')
|
|
523
|
+
gc_ref_mean = (rhos_b11.add(rhos_b12)).divide(2).rename('gc_ref_mean')
|
|
524
|
+
|
|
525
|
+
# Step 2: Compute average modeled surface reflectance for reference bands (gc_sur_mean in ACOLITE)
|
|
526
|
+
# This is the average of glint_ave values for B11 and B12
|
|
527
|
+
gc_sur_mean = (glint_ave['11'] + glint_ave['12']) / 2.0
|
|
528
|
+
|
|
529
|
+
# Step 3: Create glint mask (gc_sub in ACOLITE)
|
|
530
|
+
# Only apply correction where observed glint is below threshold and positive
|
|
531
|
+
glint_mask = gc_ref_mean.gt(glint_min).And(gc_ref_mean.lt(glint_max))
|
|
532
|
+
|
|
533
|
+
# Step 4: Apply band-specific glint correction
|
|
534
|
+
for band in bands:
|
|
535
|
+
band_name = 'rhos_B' + band
|
|
536
|
+
|
|
537
|
+
# Skip if modeled surface reflectance is invalid
|
|
538
|
+
if np.isinf(glint_ave[band]) or np.isnan(glint_ave[band]):
|
|
539
|
+
continue
|
|
540
|
+
|
|
541
|
+
# Compute current band glint (cur_rhog in ACOLITE):
|
|
542
|
+
# cur_rhog = gc_ref_mean * (surf_current_band / gc_sur_mean)
|
|
543
|
+
#
|
|
544
|
+
# This scales the observed glint by the ratio of:
|
|
545
|
+
# - Modeled surface reflectance for current band (glint_ave[band])
|
|
546
|
+
# - Average modeled surface reflectance of reference bands (gc_sur_mean)
|
|
547
|
+
#
|
|
548
|
+
# The ratio accounts for spectral variation in surface reflectance
|
|
549
|
+
glint_ratio = glint_ave[band] / gc_sur_mean
|
|
550
|
+
cur_rhog = gc_ref_mean.multiply(glint_ratio).rename('cur_rhog')
|
|
551
|
+
|
|
552
|
+
# Mask the glint correction to valid pixels
|
|
553
|
+
cur_rhog = cur_rhog.updateMask(glint_mask)
|
|
554
|
+
|
|
555
|
+
# Apply glint correction: rhos_corrected = rhos_original - glint
|
|
556
|
+
rhos_corrected = image.select(band_name).subtract(cur_rhog)
|
|
557
|
+
|
|
558
|
+
# Mask negative values (can occur in dark pixels or over-correction)
|
|
559
|
+
rhos_corrected = mask_negative_reflectance(rhos_corrected, band_name)
|
|
560
|
+
|
|
561
|
+
# Replace the band in the output image (overwrite=True)
|
|
562
|
+
deglinted = deglinted.addBands(rhos_corrected, overwrite=True)
|
|
563
|
+
|
|
564
|
+
# Optional: Write glint mean for visualization/validation
|
|
565
|
+
deglinted = deglinted.addBands(gc_ref_mean.rename('glint_mean'))
|
|
566
|
+
|
|
567
|
+
return deglinted
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for GEE ACOLITE processing
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from gee_acolite.utils.l1_convert import l1_to_rrs, DN_to_rrs, resample
|
|
6
|
+
from gee_acolite.utils.masks import (
|
|
7
|
+
mask_negative_reflectance,
|
|
8
|
+
toa_mask,
|
|
9
|
+
cirrus_mask,
|
|
10
|
+
non_water,
|
|
11
|
+
add_cloud_bands,
|
|
12
|
+
add_shadow_bands,
|
|
13
|
+
add_cld_shdw_mask,
|
|
14
|
+
cld_shdw_mask,
|
|
15
|
+
)
|
|
16
|
+
from gee_acolite.utils.search import (
|
|
17
|
+
search,
|
|
18
|
+
search_list,
|
|
19
|
+
search_with_cloud_proba,
|
|
20
|
+
search_list_with_cloud_proba,
|
|
21
|
+
join_s2_with_cloud_prob,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# L1 conversion
|
|
26
|
+
"l1_to_rrs",
|
|
27
|
+
"DN_to_rrs",
|
|
28
|
+
"resample",
|
|
29
|
+
# Masks
|
|
30
|
+
"mask_negative_reflectance",
|
|
31
|
+
"toa_mask",
|
|
32
|
+
"cirrus_mask",
|
|
33
|
+
"non_water",
|
|
34
|
+
"add_cloud_bands",
|
|
35
|
+
"add_shadow_bands",
|
|
36
|
+
"add_cld_shdw_mask",
|
|
37
|
+
"cld_shdw_mask",
|
|
38
|
+
# Search
|
|
39
|
+
"search",
|
|
40
|
+
"search_list",
|
|
41
|
+
"search_with_cloud_proba",
|
|
42
|
+
"search_list_with_cloud_proba",
|
|
43
|
+
"join_s2_with_cloud_prob",
|
|
44
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import ee
|
|
2
|
+
from functools import partial
|
|
3
|
+
|
|
4
|
+
from gee_acolite.sensors.sentinel2 import SENTINEL2_BANDS, BAND_BY_SCALE
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def l1_to_rrs(images : ee.ImageCollection, scale: int) -> ee.ImageCollection:
|
|
8
|
+
resample_scale = partial(resample, band = BAND_BY_SCALE.get(scale, 'B2'))
|
|
9
|
+
return images.select(SENTINEL2_BANDS).map(DN_to_rrs).map(resample_scale)
|
|
10
|
+
|
|
11
|
+
def DN_to_rrs(image : ee.Image) -> ee.Image:
|
|
12
|
+
rrs = image.divide(10_000)
|
|
13
|
+
|
|
14
|
+
rrs = rrs.set('sza', image.get('MEAN_SOLAR_ZENITH_ANGLE'))
|
|
15
|
+
rrs = rrs.set('saa', image.get('MEAN_SOLAR_AZIMUTH_ANGLE'))
|
|
16
|
+
rrs = rrs.set('vza', get_mean_band_angle(image, 'ZENITH'))
|
|
17
|
+
rrs = rrs.set('vaa', get_mean_band_angle(image, 'AZIMUTH'))
|
|
18
|
+
|
|
19
|
+
raa = ee.Number(rrs.get('saa')).subtract(rrs.get('vaa')).abs()
|
|
20
|
+
|
|
21
|
+
raa = ee.Algorithms.If(raa.gt(180), raa.subtract(360).abs(), raa)
|
|
22
|
+
rrs = rrs.set('raa', raa)
|
|
23
|
+
rrs = rrs.set('PRODUCT_ID', ee.String(ee.String(image.get('PRODUCT_ID')).split('L1C').get(0)))
|
|
24
|
+
|
|
25
|
+
rrs = rrs.set('system:time_start', image.get('system:time_start'))
|
|
26
|
+
|
|
27
|
+
rrs = rrs.copyProperties(image)
|
|
28
|
+
rrs = rrs.set('system:time_start', image.get('system:time_start'))
|
|
29
|
+
|
|
30
|
+
return rrs
|
|
31
|
+
|
|
32
|
+
def get_mean_band_angle(image : ee.Image, angle_name : str) -> ee.Number:
|
|
33
|
+
bands = image.bandNames()
|
|
34
|
+
|
|
35
|
+
for index in range(13):
|
|
36
|
+
bands = bands.set(index, ee.String('MEAN_INCIDENCE_' + angle_name + '_ANGLE_').cat(bands.get(index)))
|
|
37
|
+
|
|
38
|
+
angle = ee.Number(0)
|
|
39
|
+
for index in range(13):
|
|
40
|
+
angle = angle.add(ee.Number(image.get(bands.get(index))))
|
|
41
|
+
else:
|
|
42
|
+
angle = angle.divide(image.bandNames().length())
|
|
43
|
+
|
|
44
|
+
return angle
|
|
45
|
+
|
|
46
|
+
def resample(image : ee.Image, band: str) -> ee.Image:
|
|
47
|
+
return image.resample('bilinear').reproject(image.select(band).projection())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import ee
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def mask_negative_reflectance(image : ee.Image, band : str) -> ee.Image:
|
|
5
|
+
return image.updateMask(image.select(band).gte(0)).rename(band)
|
|
6
|
+
|
|
7
|
+
def toa_mask(image : ee.Image, band : str = 'rhot_B11', threshold : float = 0.03):
|
|
8
|
+
return image.select(band).lt(threshold)
|
|
9
|
+
|
|
10
|
+
def cirrus_mask(image : ee.Image, band : str = 'rhot_B10', threshold : float = 0.005):
|
|
11
|
+
return image.select(band).lt(threshold)
|
|
12
|
+
|
|
13
|
+
def non_water(image : ee.Image, band : str = 'rhot_B11', threshold : float = 0.05) -> ee.Image:
|
|
14
|
+
return image.select(band).lt(threshold)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_cloud_bands(img : ee.Image, cloud_prob_threshold : float = 50) -> ee.Image:
|
|
18
|
+
"""
|
|
19
|
+
Añade banda de máscara de nubes a la imagen
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
img: Imagen con banda 'probability' de Cloud Probability
|
|
23
|
+
cloud_prob_threshold: Umbral de probabilidad de nube (0-100)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Imagen con banda 'clouds' añadida
|
|
27
|
+
"""
|
|
28
|
+
# Obtener probabilidad de nubes (ya está como banda)
|
|
29
|
+
cld_prb = ee.Image(img.get('cloud_prob')).select('probability')
|
|
30
|
+
|
|
31
|
+
# Máscara de nubes basada en umbral de probabilidad
|
|
32
|
+
is_cloud = cld_prb.gt(cloud_prob_threshold).rename('clouds')
|
|
33
|
+
|
|
34
|
+
return img.addBands(is_cloud)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def add_shadow_bands(img : ee.Image, nir_dark_threshold : float = 0.15,
|
|
38
|
+
cloud_proj_distance : float = 1) -> ee.Image:
|
|
39
|
+
"""
|
|
40
|
+
Añade bandas de máscara de sombras a la imagen L1C
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
img: Imagen con banda 'clouds' ya añadida
|
|
44
|
+
nir_dark_threshold: Umbral para píxeles oscuros en NIR (0-1)
|
|
45
|
+
cloud_proj_distance: Distancia de proyección de sombras en km
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Imagen con bandas 'dark_pixels', 'cloud_transform' y 'shadows' añadidas
|
|
49
|
+
"""
|
|
50
|
+
# Píxeles oscuros en NIR (posibles sombras) - para L1C usar B8 directamente
|
|
51
|
+
dark_pixels = img.select('rhot_B8').lt(nir_dark_threshold * 10000).rename('dark_pixels')
|
|
52
|
+
|
|
53
|
+
# Dirección de proyección de sombras
|
|
54
|
+
shadow_azimuth = ee.Number(90).subtract(ee.Number(img.get('MEAN_SOLAR_AZIMUTH_ANGLE')))
|
|
55
|
+
|
|
56
|
+
# Proyectar sombras desde nubes
|
|
57
|
+
cld_proj = img.select('clouds').directionalDistanceTransform(shadow_azimuth, cloud_proj_distance * 10) \
|
|
58
|
+
.reproject(**{'crs': img.select(0).projection(), 'scale': 100}) \
|
|
59
|
+
.select('distance') \
|
|
60
|
+
.mask() \
|
|
61
|
+
.rename('cloud_transform')
|
|
62
|
+
|
|
63
|
+
# Identificar sombras como intersección de píxeles oscuros y proyección de nubes
|
|
64
|
+
shadows = cld_proj.multiply(dark_pixels).rename('shadows')
|
|
65
|
+
|
|
66
|
+
return img.addBands(ee.Image([dark_pixels, cld_proj, shadows]))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def add_cld_shdw_mask(img : ee.Image, cloud_prob_threshold : float = 50,
|
|
70
|
+
nir_dark_threshold : float = 0.15, cloud_proj_distance : float = 1,
|
|
71
|
+
buffer : int = 50) -> ee.Image:
|
|
72
|
+
"""
|
|
73
|
+
Añade máscara combinada de nubes y sombras con buffer
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
img: Imagen con banda 'probability' de Cloud Probability
|
|
77
|
+
cloud_prob_threshold: Umbral de probabilidad de nube (0-100)
|
|
78
|
+
nir_dark_threshold: Umbral para píxeles oscuros en NIR (0-1)
|
|
79
|
+
cloud_proj_distance: Distancia de proyección de sombras en km
|
|
80
|
+
buffer: Buffer alrededor de nubes en metros
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Imagen con banda 'cloudmask' añadida
|
|
84
|
+
"""
|
|
85
|
+
# Añadir bandas de nubes
|
|
86
|
+
img_cloud = add_cloud_bands(img, cloud_prob_threshold)
|
|
87
|
+
|
|
88
|
+
# Añadir bandas de sombras
|
|
89
|
+
img_cloud_shadow = add_shadow_bands(img_cloud, nir_dark_threshold, cloud_proj_distance)
|
|
90
|
+
|
|
91
|
+
# Combinar máscaras
|
|
92
|
+
is_cld_shdw = img_cloud_shadow.select('clouds').add(img_cloud_shadow.select('shadows')).gt(0)
|
|
93
|
+
|
|
94
|
+
# Aplicar buffer
|
|
95
|
+
is_cld_shdw = is_cld_shdw.focal_min(2).focal_max(buffer * 2 / 20) \
|
|
96
|
+
.reproject(**{'crs': img.select([0]).projection(), 'scale': 20}) \
|
|
97
|
+
.rename('cloudmask')
|
|
98
|
+
|
|
99
|
+
return img_cloud_shadow.addBands(is_cld_shdw)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cld_shdw_mask(img : ee.Image) -> ee.Image:
|
|
103
|
+
"""
|
|
104
|
+
Aplica la máscara de nubes y sombras a la imagen
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
img: Imagen con banda 'cloudmask' ya añadida
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Imagen enmascarada con solo las bandas B* (bandas Sentinel-2)
|
|
111
|
+
"""
|
|
112
|
+
# Obtener máscara
|
|
113
|
+
return img.select('cloudmask').Not()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import ee
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def search(roi : ee.Geometry, start : str, end : str,
|
|
7
|
+
collection : str = 'S2_HARMONIZED', tile : Optional[str] = None) -> ee.ImageCollection:
|
|
8
|
+
if tile is None:
|
|
9
|
+
sentinel2_l1 = ee.ImageCollection(f'COPERNICUS/{collection}').filterBounds(roi).filterDate(start, end)
|
|
10
|
+
else:
|
|
11
|
+
sentinel2_l1 = ee.ImageCollection(f'COPERNICUS/{collection}').filterBounds(roi).filterDate(start, end).filter(ee.Filter.stringContains('PRODUCT_ID', tile))
|
|
12
|
+
|
|
13
|
+
return sentinel2_l1
|
|
14
|
+
|
|
15
|
+
def search_list(roi : ee.Geometry, starts : list[str], ends : list[str],
|
|
16
|
+
collection : str = 'S2_HARMONIZED', tile : Optional[str] = None) -> ee.ImageCollection:
|
|
17
|
+
|
|
18
|
+
return ee.ImageCollection.fromImages([search(roi, start, end, collection, tile).first() for start, end in zip(starts, ends)])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def search_with_cloud_proba(roi : ee.Geometry, start : str, end : str,
|
|
22
|
+
collection : str = 'S2_HARMONIZED', tile : Optional[str] = None) -> ee.ImageCollection:
|
|
23
|
+
"""
|
|
24
|
+
Busca imágenes Sentinel-2 y une con Cloud Probability
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
roi: Región de interés
|
|
28
|
+
start: Fecha de inicio (formato 'YYYY-MM-DD')
|
|
29
|
+
end: Fecha de fin (formato 'YYYY-MM-DD')
|
|
30
|
+
collection: Nombre de la colección de Sentinel-2 (default: 'S2_HARMONIZED')
|
|
31
|
+
tile: Identificador del tile (opcional)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Colección de imágenes con Cloud Probability unida
|
|
35
|
+
"""
|
|
36
|
+
s2_collection = search(roi, start, end, collection, tile)
|
|
37
|
+
return join_s2_with_cloud_prob(s2_collection)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def search_list_with_cloud_proba(roi : ee.Geometry, starts : list[str], ends : list[str],
|
|
41
|
+
collection : str = 'S2_HARMONIZED', tile : Optional[str] = None) -> ee.ImageCollection:
|
|
42
|
+
"""
|
|
43
|
+
Busca una lista de imágenes Sentinel-2 y une con Cloud Probability
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
roi: Región de interés
|
|
47
|
+
starts: Lista de fechas de inicio (formato 'YYYY-MM-DD')
|
|
48
|
+
ends: Lista de fechas de fin (formato 'YYYY-MM-DD')
|
|
49
|
+
collection: Nombre de la colección de Sentinel-2 (default: 'S2_HARMONIZED')
|
|
50
|
+
tile: Identificador del tile (opcional)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Colección de imágenes con Cloud Probability unida
|
|
54
|
+
"""
|
|
55
|
+
s2_collection = search_list(roi, starts, ends, collection, tile)
|
|
56
|
+
return join_s2_with_cloud_prob(s2_collection)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def join_s2_with_cloud_prob(s2_collection : ee.ImageCollection) -> ee.ImageCollection:
|
|
60
|
+
"""
|
|
61
|
+
Une la colección Sentinel-2 con Cloud Probability
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
s2_collection: Colección de imágenes Sentinel-2 L1C
|
|
65
|
+
roi: Región de interés
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Colección de imágenes con Cloud Probability unida como propiedad 'cloud_prob'
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def add_cloud_prob(img):
|
|
72
|
+
img_index = img.get('system:index')
|
|
73
|
+
cloud = ee.ImageCollection('COPERNICUS/S2_CLOUD_PROBABILITY') \
|
|
74
|
+
.filter(ee.Filter.equals('system:index', img_index)) \
|
|
75
|
+
.first()
|
|
76
|
+
|
|
77
|
+
return img.set('cloud_prob', cloud)
|
|
78
|
+
|
|
79
|
+
return s2_collection.map(add_cloud_prob)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import ee
|
|
3
|
+
|
|
4
|
+
import gee_acolite.utils.masks as masks
|
|
5
|
+
from gee_acolite.sensors.sentinel2 import SENTINEL2_BANDS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_water_mask(image: ee.Image, settings: dict) -> ee.Image:
|
|
9
|
+
mask = masks.non_water(image,
|
|
10
|
+
threshold = settings.get('l2w_mask_threshold', 0.05))
|
|
11
|
+
mask = mask.updateMask(masks.cirrus_mask(image,
|
|
12
|
+
threshold = settings.get('l2w_mask_cirrus_threshold', 0.005)))
|
|
13
|
+
|
|
14
|
+
for band in SENTINEL2_BANDS:
|
|
15
|
+
mask = mask.updateMask( masks.toa_mask(image,
|
|
16
|
+
f'rhot_{band}',
|
|
17
|
+
settings.get('l2w_mask_high_toa_threshold', 0.3)) )
|
|
18
|
+
|
|
19
|
+
if settings.get('s2_cloud_proba', False):
|
|
20
|
+
CLOUD_PROB_THRESHOLD = int(settings.get('s2_cloud_proba__cloud_threshold', 50))
|
|
21
|
+
NIR_DARK_THRESHOLD = float(settings.get('s2_cloud_proba__nir_dark_threshold', 0.15))
|
|
22
|
+
CLOUD_PROJ_DISTANCE = int(settings.get('s2_cloud_proba__cloud_proj_distance', 10))
|
|
23
|
+
BUFFER = int(settings.get('s2_cloud_proba__buffer', 50))
|
|
24
|
+
mask = mask.updateMask(masks.cld_shdw_mask(masks.add_cld_shdw_mask(image,
|
|
25
|
+
CLOUD_PROB_THRESHOLD,
|
|
26
|
+
NIR_DARK_THRESHOLD,
|
|
27
|
+
CLOUD_PROJ_DISTANCE,
|
|
28
|
+
BUFFER)))
|
|
29
|
+
|
|
30
|
+
return mask
|
|
31
|
+
|
|
32
|
+
def compute_water_bands(image: ee.Image, settings: dict) -> ee.Image:
|
|
33
|
+
mask = compute_water_mask(image, settings)
|
|
34
|
+
|
|
35
|
+
for product in settings['l2w_parameters']:
|
|
36
|
+
new_band = PRODUCTS[product](image)
|
|
37
|
+
|
|
38
|
+
if isinstance(new_band, list):
|
|
39
|
+
new_band = [band.updateMask(mask) for band in new_band]
|
|
40
|
+
else:
|
|
41
|
+
new_band = new_band.updateMask(mask)
|
|
42
|
+
|
|
43
|
+
image = image.addBands(new_band)
|
|
44
|
+
|
|
45
|
+
return image
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def spm_nechad2016_665(image : ee.Image) -> ee.Image:
|
|
49
|
+
return image.expression('(A * red) / (1 - (red / C))', {'A' : 342.10,
|
|
50
|
+
'C' : 0.19563,
|
|
51
|
+
'red' : image.select('rhos_B4') }).rename('SPM_Nechad2016_665').toFloat()
|
|
52
|
+
|
|
53
|
+
def spm_nechad2016_704(image : ee.Image) -> ee.Image:
|
|
54
|
+
return image.expression('(A * red) / (1 - (red / C))', {'A' : 444.36,
|
|
55
|
+
'C' : 0.18753,
|
|
56
|
+
'red' : image.select('rhos_B5') }).rename('SPM_Nechad2016_704').toFloat()
|
|
57
|
+
|
|
58
|
+
def spm_nechad2016_740(image : ee.Image) -> ee.Image:
|
|
59
|
+
return image.expression('(A * red) / (1 - (red / C))', {'A' : 1517.00,
|
|
60
|
+
'C' : 0.19736,
|
|
61
|
+
'red' : image.select('rhos_B6') }).rename('SPM_Nechad2016_739').toFloat()
|
|
62
|
+
|
|
63
|
+
def tur_nechad2016_665(image : ee.Image) -> ee.Image:
|
|
64
|
+
return image.expression('(A * red) / (1 - (red / C))', {'A' : 366.14,
|
|
65
|
+
'C' : 0.19563,
|
|
66
|
+
'red' : image.select('rhos_B4') }).rename('TUR_Nechad2016_665').toFloat()
|
|
67
|
+
|
|
68
|
+
def tur_nechad2016_704(image : ee.Image) -> ee.Image:
|
|
69
|
+
return image.expression('(A * red) / (1 - (red / C))', {'A' : 439.09,
|
|
70
|
+
'C' : 0.18753,
|
|
71
|
+
'red' : image.select('rhos_B5') }).rename('TUR_Nechad2016_704').toFloat()
|
|
72
|
+
|
|
73
|
+
def tur_nechad2016_740(image : ee.Image) -> ee.Image:
|
|
74
|
+
return image.expression('(A * red) / (1 - (red / C))', {'A' : 1590.66,
|
|
75
|
+
'C' : 0.19736,
|
|
76
|
+
'red' : image.select('rhos_B6') }).rename('TUR_Nechad2016_739').toFloat()
|
|
77
|
+
|
|
78
|
+
def chl_oc2(image : ee.Image) -> ee.Image:
|
|
79
|
+
A, B, C, D, E = 0.1977,-1.8117,1.9743,-2.5635,-0.7218 # TODO : interpolar B1 a las dimensiones de B2
|
|
80
|
+
x = image.expression('log( x / y )', {'x' : image.select('rhos_B2'),
|
|
81
|
+
'y' : image.select('rhos_B3') }).rename('x')
|
|
82
|
+
return image.expression('10 ** (A + B * x + C * (x**2) + D * (x**3) + E * (x**4) )', {'A' : A,
|
|
83
|
+
'B' : B,
|
|
84
|
+
'C' : C,
|
|
85
|
+
'D' : D,
|
|
86
|
+
'E' : E,
|
|
87
|
+
'x' : x }).rename('chl_oc2').toFloat()
|
|
88
|
+
|
|
89
|
+
def chl_oc3(image : ee.Image) -> ee.Image:
|
|
90
|
+
A, B, C, D, E = 0.2412,-2.0546,1.1776,-0.5538,-0.4570 # TODO : interpolar B1 a las dimensiones de B2
|
|
91
|
+
x = image.expression('log( x / y )', {'x' : image.select('rhos_B2').max(image.select('rhos_B1')).rename('x'),
|
|
92
|
+
'y' : image.select('rhos_B3') }).rename('x')
|
|
93
|
+
return image.expression('10 ** (A + B * x + C * (x**2) + D * (x**3) + E * (x**4) )', {'A' : A,
|
|
94
|
+
'B' : B,
|
|
95
|
+
'C' : C,
|
|
96
|
+
'D' : D,
|
|
97
|
+
'E' : E,
|
|
98
|
+
'x' : x }).rename('chl_oc3').toFloat()
|
|
99
|
+
|
|
100
|
+
def chl_re_mishra(image : ee.Image) -> ee.Image:
|
|
101
|
+
a, b, c = 14.039, 86.11, 194.325
|
|
102
|
+
ndci = image.normalizedDifference(['rhos_B5', 'rhos_B4']).rename('ndci')
|
|
103
|
+
return image.expression('a + b * ndci + c * ndci * ndci', {'a' : a,
|
|
104
|
+
'b' : b,
|
|
105
|
+
'c' : c,
|
|
106
|
+
'ndci' : ndci }).rename('chl_re_mishra').toFloat()
|
|
107
|
+
|
|
108
|
+
def ndwi(image : ee.Image) -> ee.Image:
|
|
109
|
+
return image.normalizedDifference(['Rrs_B3', 'Rrs_B8']).rename('ndwi').toFloat()
|
|
110
|
+
|
|
111
|
+
def pSDB_red(image : ee.Image) -> ee.Image:
|
|
112
|
+
return image.expression('log(n * pi * blue) / log(n * pi * red)', {'n' : 1_000,
|
|
113
|
+
'pi' : float(np.pi),
|
|
114
|
+
'blue' : image.select('Rrs_B2'),
|
|
115
|
+
'red' : image.select('Rrs_B4') }).rename('pSDB_red').toFloat()
|
|
116
|
+
|
|
117
|
+
def pSDB_green(image : ee.Image) -> ee.Image:
|
|
118
|
+
return image.expression('log(n * pi * blue) / log(n * pi * green)', {'n' : 1_000,
|
|
119
|
+
'pi' : float(np.pi),
|
|
120
|
+
'blue' : image.select('Rrs_B2'),
|
|
121
|
+
'green' : image.select('Rrs_B3') }).rename('pSDB_green').toFloat()
|
|
122
|
+
|
|
123
|
+
def rrs(image : ee.Image) -> ee.image:
|
|
124
|
+
bands = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B8A', 'B9', 'B10', 'B11', 'B12']
|
|
125
|
+
return [ image.select(f'rhos_{band}').divide(np.pi).rename(f'Rrs_{band}').toFloat() for band in bands ]
|
|
126
|
+
|
|
127
|
+
PRODUCTS = {
|
|
128
|
+
'spm_nechad2016' : spm_nechad2016_665,
|
|
129
|
+
'spm_nechad2016_704' : spm_nechad2016_704,
|
|
130
|
+
'spm_nechad2016_740' : spm_nechad2016_740,
|
|
131
|
+
'tur_nechad2016' : tur_nechad2016_665,
|
|
132
|
+
'tur_nechad2016_704' : tur_nechad2016_704,
|
|
133
|
+
'tur_nechad2016_740' : tur_nechad2016_740,
|
|
134
|
+
'chl_oc2' : chl_oc2,
|
|
135
|
+
'chl_oc3' : chl_oc3,
|
|
136
|
+
'chl_re_mishra' : chl_re_mishra,
|
|
137
|
+
'pSDB_red' : pSDB_red,
|
|
138
|
+
'pSDB_green' : pSDB_green,
|
|
139
|
+
'Rrs_*' : rrs
|
|
140
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gee_acolite
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ACOLITE atmospheric correction for Google Earth Engine
|
|
5
|
+
Home-page: https://github.com/Aouei/gee_acolite
|
|
6
|
+
Author: Sergio
|
|
7
|
+
Author-email: Aouei <sergiohercar1@gmail.com>
|
|
8
|
+
License: GPL-3.0
|
|
9
|
+
Project-URL: Homepage, https://github.com/Aouei/gee_acolite
|
|
10
|
+
Project-URL: Documentation, https://github.com/Aouei/gee_acolite
|
|
11
|
+
Project-URL: Repository, https://github.com/Aouei/gee_acolite
|
|
12
|
+
Project-URL: Bug Tracker, https://github.com/Aouei/gee_acolite/issues
|
|
13
|
+
Keywords: earth-engine,atmospheric-correction,acolite,sentinel-2,remote-sensing,water-quality
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
18
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: earthengine-api>=0.1.350
|
|
29
|
+
Requires-Dist: numpy>=1.20.0
|
|
30
|
+
Requires-Dist: scipy>=1.7.0
|
|
31
|
+
Requires-Dist: netcdf4>=1.7.0
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
|
|
37
|
+
# GEE ACOLITE
|
|
38
|
+
|
|
39
|
+
ACOLITE atmospheric correction implementation for Google Earth Engine (GEE).
|
|
40
|
+
|
|
41
|
+
## Description
|
|
42
|
+
|
|
43
|
+
This package provides atmospheric correction for Sentinel-2 imagery using the ACOLITE dark spectrum fitting method, optimized for Google Earth Engine workflows. It includes:
|
|
44
|
+
|
|
45
|
+
- **Dark Spectrum Fitting**: Multiple methods (darkest, percentile, intercept)
|
|
46
|
+
- **AOT Estimation**: Fixed geometry mode
|
|
47
|
+
- **Atmospheric Correction**: Full LUT-based correction
|
|
48
|
+
- **Water Quality Parameters**: SPM, turbidity, chlorophyll, bathymetry indices
|
|
49
|
+
- **Cloud Masking**: Integration with Sentinel-2 Cloud Probability
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### From PyPI (when published)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install gee_acolite
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### From source
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/Aouei/gee_acolite.git
|
|
63
|
+
cd gee_acolite
|
|
64
|
+
pip install -e .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
### Atmospheric Correction Methods
|
|
70
|
+
|
|
71
|
+
- **Fixed Geometry**: Single AOT estimation for entire image
|
|
72
|
+
- **Dark Spectrum Options**:
|
|
73
|
+
- `darkest`: Use minimum values
|
|
74
|
+
- `percentile`: Use Nth percentile
|
|
75
|
+
- `intercept`: Linear regression intercept
|
|
76
|
+
|
|
77
|
+
### Model Selection Criteria
|
|
78
|
+
|
|
79
|
+
- `min_drmsd`: Minimum RMSD between observed and modeled reflectance
|
|
80
|
+
- `min_dtau`: Minimum delta AOT between darkest bands
|
|
81
|
+
- `taua_cv`: Minimum coefficient of variation
|
|
82
|
+
|
|
83
|
+
### Water Quality Products
|
|
84
|
+
|
|
85
|
+
- **SPM**: Suspended particulate matter (Nechad 2016)
|
|
86
|
+
- **Turbidity**: Water turbidity (Nechad 2016)
|
|
87
|
+
- **Chlorophyll**: Chl-a concentration (OC2, OC3, Mishra)
|
|
88
|
+
- **Bathymetry**: Pseudo satellite-derived bathymetry (pSDB)
|
|
89
|
+
- **Indices**: NDWI, custom band ratios
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Python ≥ 3.8
|
|
94
|
+
- earthengine-api ≥ 0.1.350
|
|
95
|
+
- numpy ≥ 1.20.0
|
|
96
|
+
- scipy ≥ 1.7.0
|
|
97
|
+
- **ACOLITE** (standalone installation required)
|
|
98
|
+
|
|
99
|
+
### Installing ACOLITE
|
|
100
|
+
|
|
101
|
+
This package requires the ACOLITE atmospheric correction software to be installed separately:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Clone ACOLITE repository
|
|
105
|
+
git clone https://github.com/acolite/acolite.git
|
|
106
|
+
|
|
107
|
+
# Install ACOLITE
|
|
108
|
+
cd acolite
|
|
109
|
+
pip install -e .
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Important:** ACOLITE is not available on PyPI and must be installed from source.
|
|
113
|
+
|
|
114
|
+
## Contributing
|
|
115
|
+
|
|
116
|
+
Contributions are welcome! Please:
|
|
117
|
+
|
|
118
|
+
1. Fork the repository
|
|
119
|
+
2. Create a feature branch
|
|
120
|
+
3. Add tests for new features
|
|
121
|
+
4. Submit a pull request
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
|
|
126
|
+
|
|
127
|
+
## Citation
|
|
128
|
+
|
|
129
|
+
If you use this package in your research, please cite:
|
|
130
|
+
|
|
131
|
+
```bibtex
|
|
132
|
+
@software{gee_acolite,
|
|
133
|
+
author = {Sergio},
|
|
134
|
+
title = {GEE ACOLITE: Atmospheric Correction for Google Earth Engine},
|
|
135
|
+
year = {2025},
|
|
136
|
+
url = {https://github.com/Aouei/gee_acolite}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
And the original ACOLITE paper:
|
|
141
|
+
|
|
142
|
+
```bibtex
|
|
143
|
+
@article{vanhellemont2019,
|
|
144
|
+
title={Adaptation of the dark spectrum fitting atmospheric correction for aquatic applications of the Landsat and Sentinel-2 archives},
|
|
145
|
+
author={Vanhellemont, Quinten and Ruddick, Kevin},
|
|
146
|
+
journal={Remote Sensing of Environment},
|
|
147
|
+
volume={225},
|
|
148
|
+
pages={175--192},
|
|
149
|
+
year={2019},
|
|
150
|
+
publisher={Elsevier}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Acknowledgments
|
|
155
|
+
|
|
156
|
+
This package is based on the ACOLITE software developed by RBINS (Royal Belgian Institute of Natural Sciences).
|
|
157
|
+
|
|
158
|
+
## Contact
|
|
159
|
+
|
|
160
|
+
- GitHub: [@Aouei](https://github.com/Aouei)
|
|
161
|
+
- Email: sergiohercar1@gmail.com
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
gee_acolite/__init__.py,sha256=GF2_P0j4Frb51YDbpENtKTsPAAEyhYi9ZtZkIqvCpPg,1138
|
|
2
|
+
gee_acolite/bathymetry.py,sha256=1NgFc50BVJRtRfu5-5Ue-3J1T9_00_v3re_RfdFSKTE,134
|
|
3
|
+
gee_acolite/correction.py,sha256=OwkXaEvnJsOZPjeO26u_ZkcZU-0bvypXkjl1OlhELUI,25368
|
|
4
|
+
gee_acolite/water_quality.py,sha256=AIfm0-3SLVXKPC4HcCvQ1xjlOFI4YLLTGteXRTP3ixA,8404
|
|
5
|
+
gee_acolite/sensors/__init__.py,sha256=MVEkrBW8bpheVUEVY59pKZWGj9aLAc8XMB0D1dCIYvo,196
|
|
6
|
+
gee_acolite/sensors/sentinel2.py,sha256=YsVwS_Iz8jjN2-_JzCFovUuxnD-FGrV2Wh_eaXyv53U,170
|
|
7
|
+
gee_acolite/utils/__init__.py,sha256=QjU8DYaaHQCVSCEF8VhduH9_06qnXJ7JvJ854bVuO0U,953
|
|
8
|
+
gee_acolite/utils/l1_convert.py,sha256=mmGtI84c8NklsV6qDSxddVePah8cwW-Y3YZfuHVztck,1786
|
|
9
|
+
gee_acolite/utils/masks.py,sha256=r3miWzc_zASPg-L_WpGyZPrd_tWuJNAuGdhtl52YoTI,4352
|
|
10
|
+
gee_acolite/utils/search.py,sha256=Qek9UdVlJna-ENr58GyU6bnA_UTDNND-Eop9xrwVSLI,3190
|
|
11
|
+
gee_acolite-1.0.0.dist-info/licenses/LICENSE,sha256=9xIEV9y9JDhPyeJTfbFWv247NO3RCoDhczSd0kUjsSA,787
|
|
12
|
+
gee_acolite-1.0.0.dist-info/METADATA,sha256=Q6gNfaDotwpWUCvQJgSFODnNWSajMBoQdg5QQsz-Tk8,4842
|
|
13
|
+
gee_acolite-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
gee_acolite-1.0.0.dist-info/top_level.txt,sha256=KQskKluvsN5NxC3MP24oMVA6iLs88Fk_NkobsHX8BSY,12
|
|
15
|
+
gee_acolite-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
GNU GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 29 June 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2025 Sergio
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
Full GPL 3.0 License Text: https://www.gnu.org/licenses/gpl-3.0.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gee_acolite
|