1
|
|
|
#!/usr/bin/env python |
2
|
|
|
# -*- coding: utf-8 -*- |
3
|
|
|
|
4
|
|
|
# Copyright (c) 2016-2018 Pytroll |
5
|
|
|
|
6
|
|
|
# Author(s): |
7
|
|
|
|
8
|
|
|
# Adam.Dybbroe <[email protected]> |
9
|
|
|
# Martin Raspaud <[email protected]> |
10
|
|
|
|
11
|
|
|
# This program is free software: you can redistribute it and/or modify |
12
|
|
|
# it under the terms of the GNU General Public License as published by |
13
|
|
|
# the Free Software Foundation, either version 3 of the License, or |
14
|
|
|
# (at your option) any later version. |
15
|
|
|
|
16
|
|
|
# This program is distributed in the hope that it will be useful, |
17
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
18
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
19
|
|
|
# GNU General Public License for more details. |
20
|
|
|
|
21
|
|
|
# You should have received a copy of the GNU General Public License |
22
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
23
|
|
|
|
24
|
|
|
"""Atmospheric correction of shortwave imager bands in the wavelength range 400 |
25
|
|
|
to 800 nm |
26
|
|
|
|
27
|
|
|
""" |
28
|
|
|
|
29
|
|
|
import logging |
30
|
|
|
import os |
31
|
|
|
from six import integer_types |
32
|
|
|
|
33
|
|
|
import h5py |
34
|
|
|
import numpy as np |
35
|
|
|
import dask.array as da |
36
|
|
|
from scipy.interpolate import interpn |
37
|
|
|
|
38
|
|
|
from pyspectral.rsr_reader import RelativeSpectralResponse |
39
|
|
|
from pyspectral.utils import (INSTRUMENTS, RAYLEIGH_LUT_DIRS, |
40
|
|
|
AEROSOL_TYPES, ATMOSPHERES, |
41
|
|
|
BANDNAMES, |
42
|
|
|
download_luts, get_central_wave, |
43
|
|
|
get_bandname_from_wavelength) |
44
|
|
|
|
45
|
|
|
LOG = logging.getLogger(__name__) |
46
|
|
|
|
47
|
|
|
WITH_CYTHON = True |
48
|
|
|
try: |
49
|
|
|
from geotiepoints.multilinear import MultilinearInterpolator |
50
|
|
|
except ImportError: |
51
|
|
|
LOG.warning( |
52
|
|
|
"Couldn't import fast multilinear interpolation with Cython.") |
53
|
|
|
LOG.warning("Check your geotiepoints installation!") |
54
|
|
|
WITH_CYTHON = False |
55
|
|
|
|
56
|
|
|
|
57
|
|
|
class BandFrequencyOutOfRange(Exception): |
58
|
|
|
|
59
|
|
|
"""Exception when the band frequency is out of the visible range""" |
60
|
|
|
pass |
61
|
|
|
|
62
|
|
|
|
63
|
|
|
class Rayleigh(object): |
64
|
|
|
|
65
|
|
|
"""Container for the atmospheric correction of satellite imager short |
66
|
|
|
wave bands. Removing background contributions of Rayleigh scattering of |
67
|
|
|
molecules and Mie scattering and absorption by aerosols. |
68
|
|
|
|
69
|
|
|
""" |
70
|
|
|
|
71
|
|
|
def __init__(self, platform_name, sensor, **kwargs): |
72
|
|
|
self.platform_name = platform_name |
73
|
|
|
self.sensor = sensor |
74
|
|
|
self.coeff_filename = None |
75
|
|
|
|
76
|
|
|
atm_type = kwargs.get('atmosphere', 'us-standard') |
77
|
|
|
if atm_type not in ATMOSPHERES: |
78
|
|
|
raise AttributeError('Atmosphere type not supported! ' + |
79
|
|
|
'Need to be one of {}'.format(str(ATMOSPHERES))) |
80
|
|
|
|
81
|
|
|
aerosol_type = kwargs.get('aerosol_type', 'marine_clean_aerosol') |
82
|
|
|
|
83
|
|
|
if aerosol_type not in AEROSOL_TYPES: |
84
|
|
|
raise AttributeError('Aerosol type not supported! ' + |
85
|
|
|
'Need to be one of {0}'.format(str(AEROSOL_TYPES))) |
86
|
|
|
|
87
|
|
|
rayleigh_dir = RAYLEIGH_LUT_DIRS[aerosol_type] |
88
|
|
|
|
89
|
|
|
if atm_type not in ATMOSPHERES.keys(): |
90
|
|
|
LOG.error("Atmosphere type %s not supported", atm_type) |
91
|
|
|
|
92
|
|
|
LOG.info("Atmosphere chosen: %s", atm_type) |
93
|
|
|
|
94
|
|
|
# Try fix instrument naming |
95
|
|
|
instr = INSTRUMENTS.get(platform_name, sensor) |
96
|
|
|
if instr != sensor: |
97
|
|
|
sensor = instr |
98
|
|
|
LOG.warning("Inconsistent sensor/satellite input - " + |
99
|
|
|
"sensor set to %s", sensor) |
100
|
|
|
|
101
|
|
|
self.sensor = sensor.replace('/', '') |
102
|
|
|
|
103
|
|
|
ext = atm_type.replace(' ', '_') |
104
|
|
|
lutname = "rayleigh_lut_{0}.h5".format(ext) |
105
|
|
|
self.reflectance_lut_filename = os.path.join(rayleigh_dir, lutname) |
106
|
|
|
if not os.path.exists(self.reflectance_lut_filename): |
107
|
|
|
LOG.warning( |
108
|
|
|
"No lut file %s on disk", self.reflectance_lut_filename) |
109
|
|
|
LOG.info("Will download from internet...") |
110
|
|
|
download_luts(aerosol_type=aerosol_type) |
111
|
|
|
|
112
|
|
|
if (not os.path.exists(self.reflectance_lut_filename) or |
113
|
|
|
not os.path.isfile(self.reflectance_lut_filename)): |
114
|
|
|
raise IOError('pyspectral file for Rayleigh scattering correction ' + |
115
|
|
|
'does not exist! Filename = ' + |
116
|
|
|
str(self.reflectance_lut_filename)) |
117
|
|
|
|
118
|
|
|
LOG.debug('LUT filename: %s', str(self.reflectance_lut_filename)) |
119
|
|
|
self._rayl = None |
120
|
|
|
self._wvl_coord = None |
121
|
|
|
self._azid_coord = None |
122
|
|
|
self._satz_sec_coord = None |
123
|
|
|
self._sunz_sec_coord = None |
124
|
|
|
|
125
|
|
|
def get_effective_wavelength(self, bandname): |
126
|
|
|
"""Get the effective wavelength with Rayleigh scattering in mind""" |
127
|
|
|
|
128
|
|
|
try: |
129
|
|
|
rsr = RelativeSpectralResponse(self.platform_name, self.sensor) |
130
|
|
|
except IOError: |
131
|
|
|
LOG.exception( |
132
|
|
|
"No spectral responses for this platform and sensor: %s %s", self.platform_name, self.sensor) |
133
|
|
|
if isinstance(bandname, (float, integer_types)): |
134
|
|
|
LOG.warning( |
135
|
|
|
"Effective wavelength is set to the requested band wavelength = %f", bandname) |
136
|
|
|
return bandname |
137
|
|
|
raise |
138
|
|
|
|
139
|
|
|
if isinstance(bandname, str): |
140
|
|
|
bandname = BANDNAMES.get(self.sensor, BANDNAMES['generic']).get(bandname, bandname) |
141
|
|
|
elif isinstance(bandname, (float, integer_types)): |
142
|
|
|
if not(0.4 < bandname < 0.8): |
143
|
|
|
raise BandFrequencyOutOfRange( |
144
|
|
|
'Requested band frequency should be between 0.4 and 0.8 microns!') |
145
|
|
|
bandname = get_bandname_from_wavelength(self.sensor, bandname, rsr.rsr) |
146
|
|
|
|
147
|
|
|
wvl, resp = rsr.rsr[bandname][ |
148
|
|
|
'det-1']['wavelength'], rsr.rsr[bandname]['det-1']['response'] |
149
|
|
|
|
150
|
|
|
cwvl = get_central_wave(wvl, resp, weight=1. / wvl**4) |
151
|
|
|
LOG.debug("Band name: %s Effective wavelength: %f", bandname, cwvl) |
152
|
|
|
|
153
|
|
|
return cwvl |
154
|
|
|
|
155
|
|
|
def get_reflectance_lut(self): |
156
|
|
|
"""Read the LUT with reflectances as a function of wavelength, satellite zenith |
157
|
|
|
secant, azimuth difference angle, and sun zenith secant |
158
|
|
|
|
159
|
|
|
""" |
160
|
|
|
if self._rayl is None: |
161
|
|
|
lut_vars = get_reflectance_lut(self.reflectance_lut_filename) |
162
|
|
|
self._rayl = lut_vars[0] |
163
|
|
|
# wvl_coord is used in a lot of non-dask functions, keep in memory |
164
|
|
|
self._wvl_coord = lut_vars[1].persist() |
165
|
|
|
self._azid_coord = lut_vars[2] |
166
|
|
|
self._satz_sec_coord = lut_vars[3] |
167
|
|
|
self._sunz_sec_coord = lut_vars[4] |
168
|
|
|
return self._rayl, self._wvl_coord, self._azid_coord,\ |
169
|
|
|
self._satz_sec_coord, self._sunz_sec_coord |
170
|
|
|
|
171
|
|
|
def get_reflectance(self, sun_zenith, sat_zenith, azidiff, bandname, |
172
|
|
|
redband=None): |
173
|
|
|
"""Get the reflectance from the three sun-sat angles.""" |
174
|
|
|
# Get wavelength in nm for band: |
175
|
|
|
wvl = self.get_effective_wavelength(bandname) * 1000.0 |
176
|
|
|
rayl, wvl_coord, azid_coord, satz_sec_coord, sunz_sec_coord = \ |
177
|
|
|
self.get_reflectance_lut() |
178
|
|
|
wvl_coord = wvl_coord.persist() |
179
|
|
|
|
180
|
|
|
clip_angle = da.rad2deg(da.arccos(1. / sunz_sec_coord.max())) |
181
|
|
|
sun_zenith = da.clip(da.asarray(sun_zenith), 0, clip_angle) |
182
|
|
|
sunzsec = 1. / da.cos(da.deg2rad(sun_zenith)) |
183
|
|
|
clip_angle = da.rad2deg(da.arccos(1. / satz_sec_coord.max())) |
184
|
|
|
sat_zenith = da.clip(da.asarray(sat_zenith), 0, clip_angle) |
185
|
|
|
satzsec = 1. / da.cos(da.deg2rad(da.asarray(sat_zenith))) |
186
|
|
|
|
187
|
|
|
shape = sun_zenith.shape |
188
|
|
|
|
189
|
|
|
if not(wvl_coord.min() < wvl < wvl_coord.max()): |
190
|
|
|
LOG.warning( |
191
|
|
|
"Effective wavelength for band %s outside 400-800 nm range!", |
192
|
|
|
str(bandname)) |
193
|
|
|
LOG.info( |
194
|
|
|
"Set the rayleigh/aerosol reflectance contribution to zero!") |
195
|
|
|
chunks = sun_zenith.chunks if redband is None else redband.chunks |
196
|
|
|
return da.zeros(shape, chunks=chunks) |
197
|
|
|
|
198
|
|
|
idx = np.searchsorted(wvl_coord, wvl) |
199
|
|
|
wvl1 = wvl_coord[idx - 1] |
200
|
|
|
wvl2 = wvl_coord[idx] |
201
|
|
|
|
202
|
|
|
fac = (wvl2 - wvl) / (wvl2 - wvl1) |
203
|
|
|
|
204
|
|
|
# FIXME: Can we do this right before interpolating instead? |
205
|
|
|
raylwvl = fac * rayl[idx - 1, :, :, :] + (1 - fac) * rayl[idx, :, :, :] |
206
|
|
|
|
207
|
|
|
import time |
208
|
|
|
tic = time.time() |
209
|
|
|
|
210
|
|
|
if WITH_CYTHON: |
211
|
|
|
smin = [sunz_sec_coord[0], azid_coord[0], satz_sec_coord[0]] |
212
|
|
|
smax = [sunz_sec_coord[-1], azid_coord[-1], satz_sec_coord[-1]] |
213
|
|
|
orders = [ |
214
|
|
|
len(sunz_sec_coord), len(azid_coord), len(satz_sec_coord)] |
215
|
|
|
minterp = MultilinearInterpolator(smin, smax, orders) |
216
|
|
|
|
217
|
|
|
f_3d_grid = raylwvl |
218
|
|
|
minterp.set_values(da.atleast_2d(f_3d_grid.ravel())) |
219
|
|
|
|
220
|
|
|
def _do_interp(minterp, sunzsec, azidiff, satzsec): |
221
|
|
|
interp_points2 = np.vstack((sunzsec.ravel(), |
222
|
|
|
180 - azidiff.ravel(), |
223
|
|
|
satzsec.ravel())) |
224
|
|
|
res = minterp(interp_points2) |
225
|
|
|
return res.reshape(sunzsec.shape) |
226
|
|
|
|
227
|
|
|
ipn = da.map_blocks(_do_interp, minterp, sunzsec, azidiff, |
228
|
|
|
satzsec, dtype=raylwvl.dtype, |
229
|
|
|
chunks=azidiff.chunks) |
230
|
|
|
else: |
231
|
|
|
# FIXME: Untested |
232
|
|
|
def _do_interp(sunz_sec_coord, azid_coord, satz_sec_coord, |
233
|
|
|
raylwvl): |
234
|
|
|
interp_points = np.dstack((sunzsec, |
235
|
|
|
180. - azidiff, |
236
|
|
|
satzsec)) |
237
|
|
|
|
238
|
|
|
return interpn( |
239
|
|
|
(sunz_sec_coord, azid_coord, satz_sec_coord), |
240
|
|
|
raylwvl[:, :, :], interp_points) |
241
|
|
|
ipn = da.map_blocks(_do_interp, sunz_sec_coord, azid_coord, |
242
|
|
|
satz_sec_coord, raylwvl[:, :, :], |
243
|
|
|
dtype=raylwvl.dtype, |
244
|
|
|
chunks=azidiff.chunks) |
245
|
|
|
|
246
|
|
|
LOG.debug("Time - Interpolation: {0:f}".format(time.time() - tic)) |
247
|
|
|
|
248
|
|
|
ipn *= 100 |
249
|
|
|
res = ipn |
250
|
|
|
if redband is not None: |
251
|
|
|
res = da.where(da.less(redband, 20.), res, |
252
|
|
|
(1 - (redband - 20) / 80) * res) |
253
|
|
|
|
254
|
|
|
return da.clip(res, 0, 100) |
255
|
|
|
|
256
|
|
|
|
257
|
|
|
def get_reflectance_lut(filename): |
258
|
|
|
"""Read the LUT with reflectances as a function of wavelength, satellite |
259
|
|
|
zenith secant, azimuth difference angle, and sun zenith secant |
260
|
|
|
|
261
|
|
|
""" |
262
|
|
|
|
263
|
|
|
h5f = h5py.File(filename, 'r') |
264
|
|
|
|
265
|
|
|
tab = da.from_array(h5f['reflectance'], chunks=(10, 10, 10, 10)) |
266
|
|
|
wvl = da.from_array(h5f['wavelengths'], chunks=(100,)) |
267
|
|
|
azidiff = da.from_array(h5f['azimuth_difference'], chunks=(1000,)) |
268
|
|
|
satellite_zenith_secant = da.from_array(h5f['satellite_zenith_secant'], |
269
|
|
|
chunks=(1000,)) |
270
|
|
|
sun_zenith_secant = da.from_array(h5f['sun_zenith_secant'], |
271
|
|
|
chunks=(1000,)) |
272
|
|
|
|
273
|
|
|
return tab, wvl, azidiff, satellite_zenith_secant, sun_zenith_secant |
274
|
|
|
|
275
|
|
|
|
276
|
|
|
if __name__ == "__main__": |
277
|
|
|
SHAPE = (1000, 3000) |
278
|
|
|
NDIM = SHAPE[0] * SHAPE[1] |
279
|
|
|
SUNZ = np.ma.arange( |
280
|
|
|
NDIM / 2, NDIM + NDIM / 2).reshape(SHAPE) * 60. / float(NDIM) |
281
|
|
|
SATZ = np.ma.arange(NDIM).reshape(SHAPE) * 60. / float(NDIM) |
282
|
|
|
AZIDIFF = np.ma.arange(NDIM).reshape(SHAPE) * 179.9 / float(NDIM) |
283
|
|
|
rfl = this.get_reflectance(SUNZ, SATZ, AZIDIFF, 'M4') |
284
|
|
|
|