Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2 

3# gms_preprocessing, spatial and spectral homogenization of satellite remote sensing data 

4# 

5# Copyright (C) 2020 Daniel Scheffler (GFZ Potsdam, daniel.scheffler@gfz-potsdam.de) 

6# 

7# This software was developed within the context of the GeoMultiSens project funded 

8# by the German Federal Ministry of Education and Research 

9# (project grant code: 01 IS 14 010 A-C). 

10# 

11# This program is free software: you can redistribute it and/or modify it under 

12# the terms of the GNU General Public License as published by the Free Software 

13# Foundation, either version 3 of the License, or (at your option) any later version. 

14# Please note the following exception: `gms_preprocessing` depends on tqdm, which 

15# is distributed under the Mozilla Public Licence (MPL) v2.0 except for the files 

16# "tqdm/_tqdm.py", "setup.py", "README.rst", "MANIFEST.in" and ".gitignore". 

17# Details can be found here: https://github.com/tqdm/tqdm/blob/master/LICENCE. 

18# 

19# This program is distributed in the hope that it will be useful, but WITHOUT 

20# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 

21# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 

22# details. 

23# 

24# You should have received a copy of the GNU Lesser General Public License along 

25# with this program. If not, see <http://www.gnu.org/licenses/>. 

26 

27"""Level 1C Processor: Atmospheric correction of TOA-reflectance data.""" 

28 

29import warnings 

30import re 

31import logging 

32import dill 

33import traceback 

34from typing import List # noqa F401 # flake8 issue 

35from time import time 

36import os 

37import timeout_decorator 

38 

39import numpy as np 

40from ecmwfapi.api import APIKeyFetchError 

41from ecmwfapi.api import APIException as ECMWFAPIException 

42 

43from geoarray import GeoArray 

44from py_tools_ds.geo.map_info import mapinfo2geotransform 

45from pyrsr import RSR 

46 

47from ..options.config import GMS_config as CFG 

48from . import geoprocessing as GEOP 

49from .L1B_P import L1B_object 

50from ..model.metadata import get_LayerBandsAssignment 

51from ..misc.definition_dicts import get_outFillZeroSaturated, proc_chain, get_mask_classdefinition 

52from ..misc.locks import MultiSlotLock 

53# from .cloud_masking import Cloud_Mask_Creator # circular dependencies 

54 

55from sicor.sicor_ac import ac_gms 

56from sicor.sensors import RSImage 

57from sicor.Mask import S2Mask 

58from sicor.ECMWF import download_variables 

59 

60__author__ = 'Daniel Scheffler' 

61 

62 

63class L1C_object(L1B_object): 

64 def __init__(self, L1B_obj=None): 

65 super(L1C_object, self).__init__() 

66 

67 if L1B_obj: 

68 # populate attributes 

69 [setattr(self, key, value) for key, value in L1B_obj.__dict__.items()] 

70 

71 # private attributes 

72 self._VZA_arr = None 

73 self._VAA_arr = None 

74 self._SZA_arr = None 

75 self._SAA_arr = None 

76 self._RAA_arr = None 

77 self._lonlat_arr = None 

78 

79 self.proc_level = 'L1C' 

80 self.proc_status = 'initialized' 

81 

82 @property 

83 def lonlat_arr(self): 

84 """Calculates pixelwise 2D-array with longitude and latitude coordinates. 

85 

86 :return: 

87 """ 

88 if self._lonlat_arr is None: 

89 self.logger.info('Calculating LonLat array...') 

90 self._lonlat_arr = \ 

91 GEOP.get_lonlat_coord_array(self.shape_fullArr, self.arr_pos, 

92 mapinfo2geotransform(self.MetaObj.map_info), 

93 self.MetaObj.projection, 

94 meshwidth=10, # for faster processing 

95 nodata_mask=None, # dont overwrite areas outside the image with nodata 

96 outFill=get_outFillZeroSaturated(np.float32)[0])[0] 

97 return self._lonlat_arr 

98 

99 @lonlat_arr.setter 

100 def lonlat_arr(self, lonlat_arr): 

101 self._lonlat_arr = lonlat_arr 

102 

103 @lonlat_arr.deleter 

104 def lonlat_arr(self): 

105 self._lonlat_arr = None 

106 

107 @property 

108 def VZA_arr(self): 

109 """Get viewing zenith angle. 

110 

111 :return: 

112 """ 

113 if self._VZA_arr is None: 

114 self.logger.info('Calculating viewing zenith array...') 

115 if self.MetaObj.ViewingAngle_arrProv: 

116 # Sentinel-2 

117 self._VZA_arr = GEOP.adjust_acquisArrProv_to_shapeFullArr( 

118 {k: v.tolist() for k, v in self.MetaObj.ViewingAngle_arrProv.items()}, 

119 self.shape_fullArr, 

120 meshwidth=10, # for faster processing 

121 subset=None, 

122 bandwise=0) 

123 else: 

124 self._VZA_arr = GEOP.calc_VZA_array(self.shape_fullArr, self.arr_pos, self.fullSceneCornerPos, 

125 float(self.MetaObj.ViewingAngle), 

126 float(self.MetaObj.FOV), 

127 self.logger, 

128 nodata_mask=None, # dont overwrite areas outside image with nodata 

129 outFill=get_outFillZeroSaturated(np.float32)[0], 

130 meshwidth=10) # for faster processing 

131 return self._VZA_arr 

132 

133 @VZA_arr.setter 

134 def VZA_arr(self, VZA_arr): 

135 self._VZA_arr = VZA_arr 

136 

137 @VZA_arr.deleter 

138 def VZA_arr(self): 

139 self._VZA_arr = None 

140 

141 @property 

142 def VAA_arr(self): 

143 """Get viewing azimuth angle. 

144 

145 :return: 

146 """ 

147 if self._VAA_arr is None: 

148 self.logger.info('Calculating viewing azimuth array...') 

149 if self.MetaObj.IncidenceAngle_arrProv: 

150 # Sentinel-2 

151 self._VAA_arr = GEOP.adjust_acquisArrProv_to_shapeFullArr( 

152 {k: v.tolist() for k, v in self.MetaObj.IncidenceAngle_arrProv.items()}, 

153 self.shape_fullArr, 

154 meshwidth=10, # for faster processing 

155 subset=None, 

156 bandwise=0) 

157 else: 

158 # only a mean VAA is available 

159 if self.VAA_mean is None: 

160 self.VAA_mean = \ 

161 GEOP.calc_VAA_using_fullSceneCornerLonLat(self.fullSceneCornerLonLat, self.MetaObj.orbitParams) 

162 assert isinstance(self.VAA_mean, float) 

163 

164 self._VAA_arr = np.full(self.VZA_arr.shape, self.VAA_mean, np.float32) 

165 return self._VAA_arr 

166 

167 @VAA_arr.setter 

168 def VAA_arr(self, VAA_arr): 

169 self._VAA_arr = VAA_arr 

170 

171 @VAA_arr.deleter 

172 def VAA_arr(self): 

173 self._VAA_arr = None 

174 

175 @property 

176 def SZA_arr(self): 

177 """Get solar zenith angle. 

178 

179 :return: 

180 """ 

181 if self._SZA_arr is None: 

182 self.logger.info('Calculating solar zenith and azimuth arrays...') 

183 self._SZA_arr, self._SAA_arr = \ 

184 GEOP.calc_SZA_SAA_array( 

185 self.shape_fullArr, self.arr_pos, 

186 self.MetaObj.AcqDate, 

187 self.MetaObj.AcqTime, 

188 self.fullSceneCornerPos, 

189 self.fullSceneCornerLonLat, 

190 self.MetaObj.overpassDurationSec, 

191 self.logger, 

192 meshwidth=10, 

193 nodata_mask=None, # dont overwrite areas outside the image with nodata 

194 outFill=get_outFillZeroSaturated(np.float32)[0], 

195 accurracy=CFG.SZA_SAA_calculation_accurracy, 

196 lonlat_arr=self.lonlat_arr if CFG.SZA_SAA_calculation_accurracy == 'fine' else None) 

197 return self._SZA_arr 

198 

199 @SZA_arr.setter 

200 def SZA_arr(self, SZA_arr): 

201 self._SZA_arr = SZA_arr 

202 

203 @SZA_arr.deleter 

204 def SZA_arr(self): 

205 self._SZA_arr = None 

206 

207 @property 

208 def SAA_arr(self): 

209 """Get solar azimuth angle. 

210 

211 :return: 

212 """ 

213 if self._SAA_arr is None: 

214 # noinspection PyStatementEffect 

215 self.SZA_arr # getter also sets self._SAA_arr 

216 return self._SAA_arr 

217 

218 @SAA_arr.setter 

219 def SAA_arr(self, SAA_arr): 

220 self._SAA_arr = SAA_arr 

221 

222 @SAA_arr.deleter 

223 def SAA_arr(self): 

224 self._SAA_arr = None 

225 

226 @property 

227 def RAA_arr(self): 

228 """Get relative azimuth angle. 

229 

230 :return: 

231 """ 

232 if self._RAA_arr is None: 

233 self.logger.info('Calculating relative azimuth array...') 

234 self._RAA_arr = GEOP.calc_RAA_array(self.SAA_arr, self.VAA_mean, 

235 nodata_mask=None, outFill=get_outFillZeroSaturated(np.float32)[0]) 

236 return self._RAA_arr 

237 

238 @RAA_arr.setter 

239 def RAA_arr(self, RAA_arr): 

240 self._RAA_arr = RAA_arr 

241 

242 @RAA_arr.deleter 

243 def RAA_arr(self): 

244 self._RAA_arr = None 

245 

246 def delete_ac_input_arrays(self): 

247 """Delete AC input arrays if they are not needed anymore.""" 

248 self.logger.info('Deleting input arrays for atmospheric correction...') 

249 del self.VZA_arr 

250 del self.SZA_arr 

251 del self.SAA_arr 

252 del self.RAA_arr 

253 del self.lonlat_arr 

254 

255 # use self.dem deleter 

256 # would have to be resampled when writing MGRS tiles 

257 # -> better to directly warp it to the output dims and projection 

258 del self.dem 

259 

260 

261class AtmCorr(object): 

262 def __init__(self, *L1C_objs, reporting=False): 

263 """Wrapper around atmospheric correction by Andre Hollstein, GFZ Potsdam 

264 

265 Creates the input arguments for atmospheric correction from one or multiple L1C_object instance(s) belonging to 

266 the same scene ID, performs the atmospheric correction and returns the atmospherically corrected L1C object(s). 

267 

268 :param L1C_objs: one or more instances of L1C_object belonging to the same scene ID 

269 """ 

270 # FIXME not yet usable for data < 2012 due to missing ECMWF archive 

271 L1C_objs = L1C_objs if isinstance(L1C_objs, tuple) else (L1C_objs,) 

272 

273 # hidden attributes 

274 self._logger = None 

275 self._GSDs = [] 

276 self._data = {} 

277 self._metadata = {} 

278 self._nodata = {} 

279 self._band_spatial_sampling = {} 

280 self._options = {} 

281 

282 # assertions 

283 scene_IDs = [obj.scene_ID for obj in L1C_objs] 

284 assert len(list(set(scene_IDs))) == 1, \ 

285 "Input GMS objects for 'AtmCorr' must all belong to the same scene ID!. Received %s." % scene_IDs 

286 

287 self.inObjs = L1C_objs # type: List[L1C_object] 

288 self.reporting = reporting 

289 self.ac_input = {} # set by self.run_atmospheric_correction() 

290 self.results = None # direct output of external atmCorr module (set by run_atmospheric_correction) 

291 self.proc_info = {} 

292 self.outObjs = [] # atmospherically corrected L1C objects 

293 

294 # append AtmCorr object to input L1C objects 

295 # [setattr(L1C_obj, 'AtmCorr', self) for L1C_obj in self.inObjs] # too big for serialization 

296 

297 if not re.search(r'Sentinel-2', self.inObjs[0].satellite, re.I): 

298 self.logger.debug('Calculation of acquisition geometry arrays is currently only validated for Sentinel-2!') 

299 # validation possible by comparing S2 angles provided by ESA with own angles # TODO 

300 

301 @property 

302 def logger(self): 

303 if self._logger and self._logger.handlers[:]: 

304 return self._logger 

305 else: 

306 if len(self.inObjs) == 1: 

307 # just use the logger of the inObj 

308 logger_atmCorr = self.inObjs[0].logger 

309 else: 

310 # in case of multiple GMS objects to be processed at once: 

311 # get the logger of the first inObj 

312 logger_atmCorr = self.inObjs[0].logger 

313 

314 # add additional file handlers for the remaining inObj (that belong to the same scene_ID) 

315 for inObj in self.inObjs[1:]: 

316 path_logfile = inObj.pathGen.get_path_logfile() 

317 fileHandler = logging.FileHandler(path_logfile, mode='a') 

318 fileHandler.setFormatter(logger_atmCorr.formatter_fileH) 

319 fileHandler.setLevel(CFG.log_level) 

320 

321 logger_atmCorr.addHandler(fileHandler) 

322 

323 inObj.close_loggers() 

324 

325 self._logger = logger_atmCorr 

326 return self._logger 

327 

328 @logger.setter 

329 def logger(self, logger): 

330 assert isinstance(logger, logging.Logger) or logger in ['not set', None], \ 

331 "AtmCorr.logger can not be set to %s." % logger 

332 if logger in ['not set', None]: 

333 self._logger.close() 

334 self._logger = logger 

335 else: 

336 self._logger = logger 

337 

338 @logger.deleter 

339 def logger(self): 

340 if self._logger not in [None, 'not set']: 

341 self._logger.close() 

342 self._logger = None 

343 

344 [inObj.close_loggers() for inObj in self.inObjs] 

345 

346 @property 

347 def GSDs(self): 

348 """ 

349 Returns a list of spatial samplings within the input GMS objects, e.g. [10,20,60]. 

350 """ 

351 for obj in self.inObjs: 

352 if obj.arr.xgsd != obj.arr.ygsd: 

353 warnings.warn("X/Y GSD is not equal for entity ID %s" % obj.entity_ID + 

354 (' (%s)' % obj.subsystem if obj.subsystem else '') + 

355 'Using X-GSD as key for spatial sampling dictionary.') 

356 self._GSDs.append(obj.arr.xgsd) 

357 

358 return self._GSDs 

359 

360 @property 

361 def data(self): 

362 """ 

363 

364 :return: 

365 ___ attribute: data, type:<class 'dict'> 

366 ______ key:B05, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] 085998540.0803833 ]] 

367 ______ key:B01, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] 131225590.13208008]] 

368 ______ key:B06, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] .14965820.13977051]] 

369 ______ key:B11, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] .11492920.10192871]] 

370 ______ key:B02, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] 104187010.10308838]] 

371 ______ key:B10, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] 013099670.01300049]] 

372 ______ key:B08, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] .16857910.15783691]] 

373 ______ key:B04, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] 065490720.06228638]] 

374 ______ key:B03, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] 082702640.08148193]] 

375 ______ key:B12, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] 068420410.06060791]] 

376 ______ key:B8A, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] 192138670.17553711]] 

377 ______ key:B09, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] .09600830.09887695]] 

378 ______ key:B07, value_type:<class 'numpy.ndarray'>, repr: [[nan nan nan ...,0. [..] 173339840.15600586]] 

379 """ 

380 if not self._data: 

381 data_dict = {} 

382 

383 for inObj in self.inObjs: 

384 for bandN, bandIdx in inObj.arr.bandnames.items(): 

385 if bandN not in data_dict: 

386 # float32! -> conversion to np.float16 will convert -9999 to -10000 

387 arr2pass = inObj.arr[:, :, bandIdx].astype(np.float32) 

388 arr2pass[arr2pass == inObj.arr.nodata] = np.nan # set nodata values to np.nan 

389 data_dict[bandN] = (arr2pass / inObj.MetaObj.ScaleFactor).astype(np.float16) 

390 else: 

391 inObj.logger.warning("Band '%s' cannot be included into atmospheric correction because it " 

392 "exists multiple times." % bandN) 

393 

394 # validate: data must have all bands needed for AC 

395 full_LBA = get_LayerBandsAssignment(self.inObjs[0].GMS_identifier, return_fullLBA=True) 

396 all_bNs_AC = ['B%s' % i if len(i) == 2 else 'B0%s' % i for i in full_LBA] 

397 if not all([bN in list(data_dict.keys()) for bN in all_bNs_AC]): 

398 raise RuntimeError('Atmospheric correction did not receive all the needed bands. \n\tExpected: %s;\n\t' 

399 'Received: %s' % (str(all_bNs_AC), str(list(sorted(data_dict.keys()))))) 

400 

401 self._data = data_dict 

402 

403 return self._data 

404 

405 @data.setter 

406 def data(self, data_dict): 

407 assert isinstance(data_dict, dict), \ 

408 "'data' can only be set to a dictionary with band names as keys and numpy arrays as values." 

409 self._data = data_dict 

410 

411 @property 

412 def nodata(self): 

413 """ 

414 

415 :return: 

416 ___ attribute: nodata, type:<class 'dict'> 

417 ______ key:60.0, value_type:<class 'numpy.ndarray'>, repr: [[ TrueTrueTrue ..., [..] False False False]] 

418 ______ key:10.0, value_type:<class 'numpy.ndarray'>, repr: [[ TrueTrueTrue ..., [..] False False False]] 

419 ______ key:20.0, value_type:<class 'numpy.ndarray'>, repr: [[ TrueTrueTrue ..., [..] False False False]] 

420 """ 

421 

422 if not self._nodata: 

423 for inObj in self.inObjs: 

424 self._nodata[inObj.arr.xgsd] = ~inObj.arr.mask_nodata[:] 

425 

426 return self._nodata 

427 

428 @property 

429 def tile_name(self): 

430 """Returns S2A tile name. 

431 NOTE: this is only needed if no DEM is passed to ac_gms 

432 

433 :return: e.g. 

434 '32UMA' 

435 """ 

436 

437 return '' # FIXME 

438 

439 @property 

440 def band_spatial_sampling(self): 

441 """ 

442 

443 :return: e.g. 

444 {'B01': 60.0, 

445 'B02': 10.0, 

446 'B03': 10.0, 

447 'B04': 10.0, 

448 'B05': 20.0, 

449 'B06': 20.0, 

450 'B07': 20.0, 

451 'B08': 10.0, 

452 'B09': 60.0, 

453 'B10': 60.0, 

454 'B11': 20.0, 

455 'B12': 20.0, 

456 'B8A': 20.0} 

457 """ 

458 

459 if not self._band_spatial_sampling: 

460 for inObj in self.inObjs: 

461 for bandN in inObj.arr.bandnames: 

462 if bandN not in self._band_spatial_sampling: 

463 self._band_spatial_sampling[bandN] = inObj.arr.xgsd 

464 return self._band_spatial_sampling 

465 

466 @property 

467 def metadata(self): 

468 """ 

469 

470 :return: 

471 ___ attribute: metadata, type:<class 'dict'> 

472 ______ key:spatial_samplings 

473 _________ key:60.0 

474 ____________ key:ULY, value_type:<class 'int'>, repr: 4900020 

475 ____________ key:NCOLS, value_type:<class 'int'>, repr: 1830 

476 ____________ key:XDIM, value_type:<class 'int'>, repr: 60 

477 ____________ key:ULX, value_type:<class 'int'>, repr: 600000 

478 ____________ key:NROWS, value_type:<class 'int'>, repr: 1830 

479 ____________ key:YDIM, value_type:<class 'int'>, repr: -60 

480 _________ key:10.0 

481 ____________ key:ULY, value_type:<class 'int'>, repr: 4900020 

482 ____________ key:NCOLS, value_type:<class 'int'>, repr: 10980 

483 ____________ key:XDIM, value_type:<class 'int'>, repr: 10 

484 ____________ key:ULX, value_type:<class 'int'>, repr: 600000 

485 ____________ key:NROWS, value_type:<class 'int'>, repr: 10980 

486 ____________ key:YDIM, value_type:<class 'int'>, repr: -10 

487 _________ key:20.0 

488 ____________ key:ULY, value_type:<class 'int'>, repr: 4900020 

489 ____________ key:NCOLS, value_type:<class 'int'>, repr: 5490 

490 ____________ key:XDIM, value_type:<class 'int'>, repr: 20 

491 ____________ key:ULX, value_type:<class 'int'>, repr: 600000 

492 ____________ key:NROWS, value_type:<class 'int'>, repr: 5490 

493 ____________ key:YDIM, value_type:<class 'int'>, repr: -20 

494 ______ key:SENSING_TIME, value_type:<class 'datetime.datetime'>, repr: 2016-03-26 10:34:06.538000+00:00 

495 """ 

496 

497 if not self._metadata: 

498 del self.logger # otherwise each input object would have multiple fileHandlers 

499 

500 metadata = dict( 

501 U=self.inObjs[0].MetaObj.EarthSunDist, 

502 SENSING_TIME=self.inObjs[0].acq_datetime, 

503 # SENSING_TIME=datetime.strptime('2015-08-12 10:40:21 +0000', '%Y-%m-%d %H:%M:%S %z'), 

504 viewing_zenith=self._meta_get_viewing_zenith(), 

505 viewing_azimuth=self._meta_get_viewing_azimuth(), 

506 relative_viewing_azimuth=self._meta_get_relative_viewing_azimuth(), 

507 sun_mean_azimuth=self.inObjs[0].MetaObj.SunAzimuth, 

508 sun_mean_zenith=90 - self.inObjs[0].MetaObj.SunElevation, 

509 solar_irradiance=self._meta_get_solar_irradiance(), 

510 aux_data=self._meta_get_aux_data(), 

511 spatial_samplings=self._meta_get_spatial_samplings() 

512 ) 

513 

514 self._metadata = metadata 

515 

516 return self._metadata 

517 

518 @property 

519 def options(self): 

520 # type: () -> dict 

521 """Returns a dictionary containing AC options. 

522 """ 

523 if self._options: 

524 return self._options 

525 else: 

526 self._options = self.inObjs[0].ac_options 

527 self._options["AC"]['bands'] = [b for b in self.data.keys() if b in self._options["AC"]['bands']] 

528 self._options["report"]["reporting"] = self.reporting 

529 return self._options 

530 

531 def _meta_get_spatial_samplings(self): 

532 """ 

533 

534 :return: 

535 {10.0: {'NCOLS': 10980, 

536 'NROWS': 10980, 

537 'ULX': 499980.0, 

538 'ULY': 5800020.0, 

539 'XDIM': 10.0, 

540 'YDIM': -10.0}, 

541 20.0: {'NCOLS': 5490, 

542 'NROWS': 5490, 

543 'ULX': 499980.0, 

544 'ULY': 5800020.0, 

545 'XDIM': 20.0, 

546 'YDIM': -20.0}, 

547 60.0: {'NCOLS': 1830, 

548 'NROWS': 1830, 

549 'ULX': 499980.0, 

550 'ULY': 5800020.0, 

551 'XDIM': 60.0, 

552 'YDIM': -60.0}} 

553 """ 

554 # set corner coordinates and dims 

555 spatial_samplings = {} 

556 

557 for inObj in self.inObjs: 

558 

559 # validate GSD 

560 if inObj.arr.xgsd != inObj.arr.ygsd: 

561 warnings.warn("X/Y GSD is not equal for entity ID %s" % inObj.entity_ID + 

562 (' (%s)' % inObj.subsystem if inObj.subsystem else '') + 

563 'Using X-GSD as key for spatial sampling dictionary.') 

564 

565 # set spatial information 

566 spatial_samplings[inObj.arr.xgsd] = dict( 

567 ULX=inObj.arr.box.boxMapYX[0][1], 

568 ULY=inObj.arr.box.boxMapYX[0][0], 

569 XDIM=inObj.arr.xgsd, 

570 YDIM=-inObj.arr.ygsd, 

571 NROWS=inObj.arr.rows, 

572 NCOLS=inObj.arr.cols) 

573 

574 return spatial_samplings 

575 

576 def _meta_get_solar_irradiance(self): 

577 """ 

578 

579 :return: 

580 {'B01': 1913.57, 

581 'B02': 1941.63, 

582 'B03': 1822.61, 

583 'B04': 1512.79, 

584 'B05': 1425.56, 

585 'B06': 1288.32, 

586 'B07': 1163.19, 

587 'B08': 1036.39, 

588 'B09': 813.04, 

589 'B10': 367.15, 

590 'B11': 245.59, 

591 'B12': 85.25, 

592 'B8A': 955.19} 

593 """ 

594 

595 solar_irradiance = {} 

596 

597 for inObj in self.inObjs: 

598 for bandN in inObj.arr.bandnames: 

599 lba_key = bandN[2:] if bandN.startswith('B0') else bandN[1:] 

600 if bandN not in solar_irradiance: 

601 solar_irradiance[bandN] = inObj.MetaObj.SolIrradiance[lba_key] 

602 

603 return solar_irradiance 

604 

605 def _meta_get_viewing_zenith(self): 

606 """ 

607 

608 :return: {B10:ndarray(dtype=float16),[...],B09:ndarray(dtype=float16)} 

609 """ 

610 

611 viewing_zenith = {} 

612 

613 for inObj in self.inObjs: # type: L1C_object 

614 for bandN, bandIdx in inObj.arr.bandnames.items(): 

615 if bandN not in viewing_zenith: 

616 arr2pass = inObj.VZA_arr[:, :, bandIdx] if inObj.VZA_arr.ndim == 3 else inObj.VZA_arr 

617 viewing_zenith[bandN] = arr2pass.astype(np.float16) 

618 # viewing_zenith[bandN] = inObj.VZA_arr[:, :, bandIdx] if inObj.VZA_arr.ndim==3 else inObj.VZA_arr 

619 return viewing_zenith 

620 

621 def _meta_get_viewing_azimuth(self): 

622 """ 

623 

624 :return: {B10:ndarray(dtype=float16),[...],B09:ndarray(dtype=float16)} 

625 """ 

626 

627 viewing_azimuth = {} 

628 

629 for inObj in self.inObjs: # type: L1C_object 

630 for bandN, bandIdx in inObj.arr.bandnames.items(): 

631 if bandN not in viewing_azimuth: 

632 arr2pass = inObj.VAA_arr[:, :, bandIdx] if inObj.VAA_arr.ndim == 3 else inObj.VAA_arr 

633 viewing_azimuth[bandN] = arr2pass.astype(np.float16) 

634 # viewing_azimuth[bandN] = inObj.VAA_arr[:, :, bandIdx] if inObj.VAA_arr.ndim==3 else inObj.VAA_arr 

635 

636 return viewing_azimuth 

637 

638 def _meta_get_relative_viewing_azimuth(self): 

639 """ 

640 

641 :return: {B10:ndarray(dtype=float16),[...],B09:ndarray(dtype=float16)} 

642 """ 

643 

644 relative_viewing_azimuth = {} 

645 

646 for inObj in self.inObjs: # type: L1C_object 

647 for bandN, bandIdx in inObj.arr.bandnames.items(): 

648 if bandN not in relative_viewing_azimuth: 

649 arr2pass = inObj.RAA_arr[:, :, bandIdx] if inObj.RAA_arr.ndim == 3 else inObj.RAA_arr 

650 relative_viewing_azimuth[bandN] = arr2pass.astype(np.float16) 

651 # relative_viewing_azimuth[bandN] = \ 

652 # inObj.RAA_arr[:, :, bandIdx] if inObj.RAA_arr.ndim==3 else inObj.RAA_arr 

653 

654 return relative_viewing_azimuth 

655 

656 def _meta_get_aux_data(self): 

657 """ 

658 

659 :return: {lons:ndarray(dtype=float16),,lats:ndarray(dtype=float16)} 

660 """ 

661 

662 aux_data = dict( 

663 # set lons and lats (a 2D array for all bands is enough (different band resolutions dont matter)) 

664 lons=self.inObjs[0].lonlat_arr[::10, ::10, 0].astype(np.float16), # 2D array of lon values: 0° - 360° 

665 lats=self.inObjs[0].lonlat_arr[::10, ::10, 1].astype(np.float16) # 2D array of lat values: -90° - 90° 

666 # FIXME correct to reduce resolution here by factor 10? 

667 ) 

668 

669 return aux_data 

670 

671 def _get_dem(self): 

672 """Get a DEM to be used in atmospheric correction. 

673 

674 :return: <np.ndarray> 2D array (with 20m resolution in case of Sentinel-2) 

675 """ 

676 # determine which input GMS object is used to generate DEM 

677 if re.search(r'Sentinel-2', self.inObjs[0].satellite): 

678 # in case of Sentinel-2 the 20m DEM must be passed 

679 inObj4dem = [obj for obj in self.inObjs if obj.arr.xgsd == 20] 

680 if not inObj4dem: 

681 self.logger.warning('Sentinel-2 20m subsystem could not be found. DEM passed to ' 

682 'atmospheric correction might have wrong resolution.') 

683 inObj4dem = inObj4dem[0] 

684 else: 

685 inObj4dem = self.inObjs[0] 

686 

687 try: 

688 dem = inObj4dem.dem[:].astype(np.float32) 

689 except Exception as e: 

690 dem = None 

691 self.logger.warning('A static elevation is assumed during atmospheric correction due to an error during ' 

692 'creation of the DEM corresponding to scene %s (entity ID: %s). Error message was: ' 

693 '\n%s\n' % (self.inObjs[0].scene_ID, self.inObjs[0].entity_ID, repr(e))) 

694 self.logger.info("Print traceback in case you care:") 

695 self.logger.warning(traceback.format_exc()) 

696 

697 return dem 

698 

699 def _get_srf(self): 

700 """Returns an instance of SRF in the same structure like sicor.sensors.SRF.SensorSRF 

701 """ 

702 # FIXME calculation of center wavelengths within RSR() used not the GMS algorithm 

703 # SRF instance must be created for all bands and the previous proc level 

704 GMSid_fullScene = self.inObjs[0].GMS_identifier 

705 GMSid_fullScene.subsystem = '' 

706 GMSid_fullScene.proc_level = proc_chain[proc_chain.index(self.inObjs[0].proc_level) - 1] 

707 

708 return RSR(satellite=GMSid_fullScene.satellite, 

709 sensor=GMSid_fullScene.sensor, 

710 subsystem=GMSid_fullScene.subsystem, 

711 wvl_unit='nanometers', 

712 format_bandnames=True, 

713 no_pan=CFG.skip_pan, 

714 no_thermal=CFG.skip_thermal, 

715 after_ac=GMSid_fullScene.proc_level in ['L1C', 'L2A', 'L2B', 'L2C'], 

716 sort_by_cwl=CFG.sort_bands_by_cwl 

717 ) 

718 

719 def _get_mask_clouds(self): 

720 """Returns an instance of S2Mask in case cloud mask is given by input GMS objects. Otherwise None is returned. 

721 

722 :return: 

723 """ 

724 

725 tgt_res = self.inObjs[0].ac_options['cld_mask']['target_resolution'] 

726 

727 # check if input GMS objects provide a cloud mask 

728 avail_cloud_masks = {inObj.GMS_identifier.subsystem: inObj.mask_clouds for inObj in self.inObjs} 

729 no_avail_CMs = list(set(avail_cloud_masks.values())) == [None] 

730 

731 # compute cloud mask if not already provided 

732 if no_avail_CMs: 

733 algorithm = CFG.cloud_masking_algorithm[self.inObjs[0].satellite] 

734 

735 if algorithm == 'SICOR': 

736 return None 

737 

738 else: 

739 # FMASK or Classical Bayesian 

740 try: 

741 from .cloud_masking import Cloud_Mask_Creator 

742 

743 CMC = Cloud_Mask_Creator(self.inObjs[0], algorithm=algorithm, tempdir_root=CFG.path_tempdir) 

744 CMC.calc_cloud_mask() 

745 cm_geoarray = CMC.cloud_mask_geoarray 

746 cm_array = CMC.cloud_mask_array 

747 cm_legend = CMC.cloud_mask_legend 

748 except Exception: 

749 self.logger.error('\nAn error occurred during FMASK cloud masking. Error message was: ') 

750 self.logger.error(traceback.format_exc()) 

751 return None 

752 

753 else: 

754 # check if there is a cloud mask with suitable GSD 

755 inObjs2use = [obj for obj in self.inObjs if obj.mask_clouds is not None and obj.mask_clouds.xgsd == tgt_res] 

756 if not inObjs2use: 

757 raise ValueError('Error appending cloud mask to input arguments of atmospheric correction. No input ' 

758 'GMS object provides a cloud mask with spatial resolution of %s.' % tgt_res) 

759 inObj2use = inObjs2use[0] 

760 

761 # get mask (geo)array 

762 cm_geoarray = inObj2use.mask_clouds 

763 cm_array = inObj2use.mask_clouds[:] 

764 

765 # get legend 

766 cm_legend = get_mask_classdefinition('mask_clouds', inObj2use.satellite) 

767 # {'Clear': 10, 'Thick Clouds': 20, 'Thin Clouds': 30, 'Snow': 40} # FIXME hardcoded 

768 

769 # validate that xGSD equals yGSD 

770 if cm_geoarray.xgsd != cm_geoarray.ygsd: 

771 warnings.warn("Cloud mask X/Y GSD is not equal for entity ID %s" % inObj2use.entity_ID + 

772 (' (%s)' % inObj2use.subsystem if inObj2use.subsystem else '') + 

773 'Using X-GSD as key for cloud mask geocoding.') 

774 

775 # get geocoding 

776 cm_geocoding = self.metadata["spatial_samplings"][tgt_res] 

777 

778 # get nodata value 

779 self.options['cld_mask']['nodata_value_mask'] = cm_geoarray.nodata 

780 

781 # append cloud mask to input object with the same spatial resolution if there was no mask before 

782 for inObj in self.inObjs: 

783 if inObj.arr.xgsd == cm_geoarray.xgsd: 

784 inObj.mask_clouds = cm_geoarray 

785 inObj.build_combined_masks_array() 

786 break # appending it to one inObj is enough 

787 

788 return S2Mask(mask_array=cm_array, 

789 mask_legend=cm_legend, 

790 geo_coding=cm_geocoding) 

791 

792 def _check_or_download_ECMWF_data(self): 

793 """Check if ECMWF files are already downloaded. If not, start the downloader.""" 

794 self.logger.info('Checking if ECMWF data are available... (if not, run download!)') 

795 

796 default_products = [ 

797 "fc_T2M", 

798 "fc_O3", 

799 "fc_SLP", 

800 "fc_TCWV", 

801 "fc_GMES_ozone", 

802 "fc_total_AOT_550nm", 

803 "fc_sulphate_AOT_550nm", 

804 "fc_black_carbon_AOT_550nm", 

805 "fc_dust_AOT_550nm", 

806 "fc_organic_matter_AOT_550nm", 

807 "fc_sea_salt_AOT_550nm"] 

808 

809 # NOTE: use_signals must be set to True while executed as multiprocessing worker (e.g., in multiprocessing.Pool) 

810 @timeout_decorator.timeout(seconds=60*10, timeout_exception=TimeoutError) 

811 def run_request(): 

812 try: 

813 with MultiSlotLock('ECMWF download lock', allowed_slots=1, logger=self.logger): 

814 t0 = time() 

815 # NOTE: download_variables does not accept a logger -> so the output may be invisible in WebApp 

816 results = download_variables(date_from=self.inObjs[0].acq_datetime, 

817 date_to=self.inObjs[0].acq_datetime, 

818 db_path=CFG.path_ECMWF_db, 

819 max_step=120, # default 

820 ecmwf_variables=default_products, 

821 processes=0, # singleprocessing 

822 force=False) # dont force download if files already exist 

823 t1 = time() 

824 self.logger.info("Runtime: %.2f" % (t1 - t0)) 

825 for result in results: 

826 self.logger.info(result) 

827 

828 except APIKeyFetchError: 

829 self.logger.error("ECMWF data download failed due to missing API credentials.") 

830 

831 except (ECMWFAPIException, Exception): 

832 self.logger.error("ECMWF data download failed for scene %s (entity ID: %s). Traceback: " 

833 % (self.inObjs[0].scene_ID, self.inObjs[0].entity_ID)) 

834 self.logger.error(traceback.format_exc()) 

835 

836 try: 

837 run_request() 

838 except TimeoutError: 

839 self.logger.error("ECMWF data download failed due to API request timeout after waiting 5 minutes.") 

840 

841 def _validate_snr_source(self): 

842 """Check if the given file path for the SNR model exists - if not, use a constant SNR of 500.""" 

843 if not os.path.isfile(self.options["uncertainties"]["snr_model"]): 

844 self.logger.warning('No valid SNR model found for %s %s. Using constant SNR to compute uncertainties of ' 

845 'atmospheric correction.' % (self.inObjs[0].satellite, self.inObjs[0].sensor)) 

846 # self.options["uncertainties"]["snr_model"] = np.nan # causes the computed uncertainties to be np.nan 

847 self.options["uncertainties"]["snr_model"] = 500 # use a constant SNR of 500 to compute uncertainties 

848 

849 def run_atmospheric_correction(self, dump_ac_input=False): 

850 # type: (bool) -> list 

851 """Collects all input data for atmospheric correction, runs the AC and returns the corrected L1C objects 

852 containing surface reflectance. 

853 

854 :param dump_ac_input: allows to dump the inputs of AC to the scene's processing folder in case AC fails 

855 :return: list of L1C_object instances containing atmospherically corrected data 

856 """ 

857 

858 # collect input args/kwargs for AC 

859 self.logger.info('Calculating input data for atmospheric correction...') 

860 

861 rs_data = dict( 

862 data=self.data, 

863 metadata=self.metadata, 

864 nodata=self.nodata, 

865 band_spatial_sampling=self.band_spatial_sampling, 

866 tile_name=self.tile_name, 

867 dem=self._get_dem(), 

868 srf=self._get_srf(), 

869 mask_clouds=self._get_mask_clouds() 

870 # returns an instance of S2Mask or None if cloud mask is not given by input GMS objects 

871 ) # NOTE: all keys of this dict are later converted to attributes of RSImage 

872 

873 # remove empty values from RSImage kwargs because SICOR treats any kind of RSImage attributes as given 

874 # => 'None'-attributes may cause issues 

875 rs_data = {k: v for k, v in rs_data.items() if v is not None} 

876 

877 script = False 

878 

879 # check if ECMWF data are available - if not, start the download 

880 if CFG.auto_download_ecmwf: 

881 self._check_or_download_ECMWF_data() 

882 

883 # validate SNR 

884 if CFG.ac_estimate_accuracy: 

885 self._validate_snr_source() 

886 

887 # create an instance of RSImage 

888 rs_image = RSImage(**rs_data) 

889 

890 self.ac_input = dict( 

891 rs_image=rs_image, 

892 options=self.options, # dict 

893 logger=repr(self.logger), # only a string 

894 script=script 

895 ) 

896 

897 # path_dump = self.inObjs[0].pathGen.get_path_ac_input_dump() 

898 # with open(path_dump, 'wb') as outF: 

899 # dill.dump(self.ac_input, outF) 

900 

901 # run AC 

902 self.logger.info('Atmospheric correction started.') 

903 try: 

904 rs_image.logger = self.logger 

905 self.results = ac_gms(rs_image, self.options, logger=self.logger, script=script) 

906 

907 except Exception as e: 

908 self.logger.error('\nAn error occurred during atmospheric correction. BE AWARE THAT THE SCENE %s ' 

909 '(ENTITY ID %s) HAS NOT BEEN ATMOSPHERICALLY CORRECTED! Error message was: \n%s\n' 

910 % (self.inObjs[0].scene_ID, self.inObjs[0].entity_ID, repr(e))) 

911 self.logger.error(traceback.format_exc()) 

912 # TODO include that in the job summary 

913 

914 # serialize AC input 

915 if dump_ac_input: 

916 path_dump = self.inObjs[0].pathGen.get_path_ac_input_dump() 

917 with open(path_dump, 'wb') as outF: 

918 self.ac_input['rs_image'].logger = None 

919 dill.dump(self.ac_input, outF) 

920 

921 self.logger.error('An error occurred during atmospheric correction. Inputs have been dumped to %s.' 

922 % path_dump) 

923 

924 # delete AC input arrays 

925 for inObj in self.inObjs: # type: L1C_object 

926 inObj.delete_ac_input_arrays() 

927 

928 return list(self.inObjs) 

929 

930 finally: 

931 # rs_image.logger must be closed properly in any case 

932 if rs_image.logger is not None: 

933 rs_image.logger.close() 

934 

935 # get processing infos 

936 self.proc_info = self.ac_input['options']['processing'] 

937 

938 # join results 

939 self._join_results_to_inObjs() # sets self.outObjs 

940 

941 # delete input arrays that are not needed anymore 

942 [inObj.delete_ac_input_arrays() for inObj in self.inObjs] 

943 

944 return self.outObjs 

945 

946 def _join_results_to_inObjs(self): 

947 """ 

948 Join results of atmospheric correction to the input GMS objects. 

949 """ 

950 

951 self.logger.info('Joining results of atmospheric correction to input GMS objects.') 

952 # delete logger 

953 # -> otherwise logging in inObjs would open a second FileHandler to the same file (which is permitted) 

954 del self.logger 

955 

956 self._join_data_ac() 

957 self._join_data_errors(bandwise=CFG.ac_bandwise_accuracy) 

958 self._join_mask_clouds() 

959 self._join_mask_confidence_array() 

960 

961 # update masks (always do that because masks can also only contain one layer) 

962 [inObj.build_combined_masks_array() for inObj in self.inObjs] 

963 

964 # update AC processing info 

965 [inObj.ac_options['processing'].update(self.proc_info) for inObj in self.inObjs] 

966 

967 self.outObjs = self.inObjs 

968 

969 def _join_data_ac(self): 

970 """ 

971 Join ATMOSPHERICALLY CORRECTED ARRAY as 3D int8 or int16 BOA reflectance array, scaled to scale factor from 

972 config. 

973 """ 

974 

975 if self.results.data_ac is not None: 

976 for inObj in self.inObjs: 

977 self.logger.info('Joining image data to %s.' % (inObj.entity_ID if not inObj.subsystem else 

978 '%s %s' % (inObj.entity_ID, inObj.subsystem))) 

979 

980 assert isinstance(inObj, L1B_object) 

981 nodata = self.results.nodata[inObj.arr.xgsd] # 2D mask with True outside of image coverage 

982 ac_bandNs = [bandN for bandN in inObj.arr.bandnames if bandN in self.results.data_ac.keys()] 

983 out_LBA = [bN.split('B0')[1] if bN.startswith('B0') else bN.split('B')[1] for bN in ac_bandNs] 

984 

985 # update metadata # 

986 ################### 

987 

988 inObj.arr_desc = 'BOA_Ref' 

989 inObj.MetaObj.bands = len(self.results.data_ac) 

990 inObj.MetaObj.PhysUnit = 'BOA_Reflectance in [0-%d]' % CFG.scale_factor_BOARef 

991 inObj.MetaObj.LayerBandsAssignment = out_LBA 

992 inObj.LayerBandsAssignment = out_LBA 

993 inObj.MetaObj.filter_layerdependent_metadata() 

994 

995 ################################################################################## 

996 # join SURFACE REFLECTANCE as 3D int16 array, scaled to scale factor from config # 

997 ################################################################################## 

998 

999 oF_refl, oZ_refl, oS_refl = get_outFillZeroSaturated(inObj.arr.dtype) 

1000 surf_refl = np.dstack([self.results.data_ac[bandN] for bandN in ac_bandNs]) 

1001 surf_refl *= CFG.scale_factor_BOARef # scale using scale factor (output is float16) 

1002 

1003 # set AC nodata values to GMS outFill 

1004 # NOTE: AC nodata contains a pixel mask where at least one band is no data 

1005 # => Setting these pixels to outZero would also reduce pixel values of surrounding pixels in 

1006 # spatial homogenization (because resampling only ignores -9999). 

1007 # It would be possible to generate a zero-data mask here for each subsystem and apply it after 

1008 # spatial homogenization. Alternatively zero-data pixels could be interpolated spectrally or 

1009 # spatially within L1A processor (also see issue #74). 

1010 surf_refl[nodata] = oF_refl # overwrite AC nodata values with GMS outFill 

1011 

1012 # apply the original nodata mask (indicating background values) 

1013 surf_refl[np.array(inObj.mask_nodata).astype(np.int8) == 0] = oF_refl 

1014 

1015 # set AC NaNs to GMS outFill 

1016 # NOTE: SICOR result has NaNs at no data positions AND non-clear positions 

1017 if self.results.bad_data_value is np.nan: 

1018 surf_refl[np.isnan(surf_refl)] = oF_refl 

1019 else: 

1020 surf_refl[surf_refl == self.results.bad_data_value] = oF_refl 

1021 

1022 # use inObj.arr setter to generate a GeoArray 

1023 inObj.arr = surf_refl.astype(inObj.arr.dtype) # -> int16 (also converts NaNs to 0 if needed 

1024 

1025 else: 

1026 self.logger.warning('Atmospheric correction did not return a result for the input array. ' 

1027 'Thus the output keeps NOT atmospherically corrected.') 

1028 

1029 def _join_data_errors(self, bandwise=False): 

1030 """Join ERRORS ARRAY as 3D or 2D int8 or int16 BOA reflectance array, scaled to scale factor from config. 

1031 

1032 :param bandwise: if True, a 3D array with bandwise information for each pixel is generated 

1033 :return: 

1034 """ 

1035 # TODO ac_error values are very close to 0 -> a scale factor of 255 yields int8 values below 10 

1036 # TODO => better to stretch the whole array to values between 0 and 100 and save original min/max? 

1037 if self.results.data_errors is not None: 

1038 

1039 for inObj in self.inObjs: 

1040 inObj.logger.info('Joining AC errors to %s.' % (inObj.entity_ID if not inObj.subsystem else 

1041 '%s %s' % (inObj.entity_ID, inObj.subsystem))) 

1042 

1043 nodata = self.results.nodata[inObj.arr.xgsd] # 2D mask with True outside of image coverage 

1044 ac_bandNs = [bandN for bandN in inObj.arr.bandnames if bandN in self.results.data_ac.keys()] 

1045 out_dtype = np.int8 if CFG.ac_scale_factor_errors <= 255 else np.int16 

1046 out_nodata_val = get_outFillZeroSaturated(out_dtype)[0] 

1047 

1048 # generate raw ac_errors array 

1049 ac_errors = np.dstack([self.results.data_errors[bandN] for bandN in ac_bandNs]) 

1050 

1051 # apply scale factor from config to data pixels and overwrite nodata area with nodata value 

1052 ac_errors *= CFG.ac_scale_factor_errors # scale using scale factor (output is float16) 

1053 ac_errors[np.isnan(ac_errors)] = out_nodata_val # replace NaNs with outnodata 

1054 ac_errors[nodata] = out_nodata_val # set all positions where SICOR nodata mask is 1 to outnodata 

1055 ac_errors = np.around(ac_errors).astype(out_dtype) # round floats to next even int8/int16 value 

1056 

1057 # average array over bands if no bandwise information is requested 

1058 if not bandwise: 

1059 # in case of only one subsystem: directly compute median errors here 

1060 if len(self.inObjs) == 1: 

1061 ac_errors = np.median(ac_errors, axis=2).astype(ac_errors.dtype) 

1062 

1063 # in case of multiple subsystems: dont compute median here but first apply geometric homogenization 

1064 # -> median could only be computed for each subsystem separately 

1065 else: 

1066 pass 

1067 

1068 # set inObj.ac_errors 

1069 inObj.ac_errors = ac_errors # setter generates a GeoArray with the same bandnames like inObj.arr 

1070 

1071 elif not CFG.ac_estimate_accuracy: 

1072 self.logger.info("Atmospheric correction did not provide a 'data_errors' array because " 

1073 "'ac_estimate_accuracy' was set to False in the job configuration.") 

1074 else: 

1075 self.logger.warning("Atmospheric correction did not provide a 'data_errors' array. Maybe due to " 

1076 "missing SNR model? GMS_object.ac_errors kept None.") 

1077 

1078 def _join_mask_clouds(self): 

1079 """ 

1080 Join CLOUD MASK as 2D uint8 array. 

1081 NOTE: mask_clouds has also methods 'export_mask_rgb()', 'export_confidence_to_jpeg2000()', ... 

1082 """ 

1083 

1084 if self.results.mask_clouds.mask_array is not None: 

1085 mask_clouds_ac = self.results.mask_clouds.mask_array # uint8 2D array 

1086 

1087 joined = False 

1088 for inObj in self.inObjs: 

1089 

1090 # delete all previous cloud masks 

1091 del inObj.mask_clouds # FIXME validate if FMask product is within AC results 

1092 

1093 # append mask_clouds only to the input GMS object with the same dimensions 

1094 if inObj.arr.shape[:2] == mask_clouds_ac.shape: 

1095 inObj.logger.info('Joining mask_clouds to %s.' % (inObj.entity_ID if not inObj.subsystem else 

1096 '%s %s' % (inObj.entity_ID, inObj.subsystem))) 

1097 

1098 inObj.mask_clouds = mask_clouds_ac 

1099 inObj.mask_clouds.legend = self.results.mask_clouds.mask_legend # dict(value=string, string=value)) 

1100 # FIXME legend is not used later 

1101 

1102 # set cloud mask nodata value 

1103 tgt_nodata = get_outFillZeroSaturated(mask_clouds_ac.dtype)[0] 

1104 ac_out_nodata = self.ac_input['options']['cld_mask']['nodata_value_mask'] 

1105 if tgt_nodata not in self.results.mask_clouds.mask_legend.keys(): 

1106 inObj.mask_clouds[inObj.mask_clouds[:] == ac_out_nodata] = tgt_nodata 

1107 mask_clouds_nodata = tgt_nodata 

1108 else: 

1109 warnings.warn('The cloud mask from AC output already uses the desired nodata value %s for the ' 

1110 'class %s. Using AC output nodata value %s.' 

1111 % (tgt_nodata, self.results.mask_clouds.mask_legend[tgt_nodata], ac_out_nodata)) 

1112 mask_clouds_nodata = ac_out_nodata 

1113 

1114 inObj.mask_clouds.nodata = mask_clouds_nodata 

1115 

1116 joined = True 

1117 

1118 if not joined: 

1119 self.logger.warning('Cloud mask has not been appended to one of the AC inputs because there was no' 

1120 'input GMS object with the same dimensions.') 

1121 

1122 else: 

1123 self.logger.warning("Atmospheric correction did not provide a 'mask_clouds.mask_array' array. " 

1124 "GMS_object.mask_clouds kept None.") 

1125 

1126 def _join_mask_confidence_array(self): 

1127 """ 

1128 Join confidence array for mask_clouds. 

1129 """ 

1130 if self.results.mask_clouds.mask_confidence_array is not None and CFG.ac_estimate_accuracy: 

1131 cfd_arr = self.results.mask_clouds.mask_confidence_array # float32 2D array, scaled [0-1, nodata 255] 

1132 cfd_arr[cfd_arr == self.ac_input['options']['cld_mask']['nodata_value_mask']] = -1 

1133 cfd_arr = (cfd_arr * CFG.scale_factor_BOARef).astype(np.int16) 

1134 cfd_arr[cfd_arr == -CFG.scale_factor_BOARef] = get_outFillZeroSaturated(cfd_arr.dtype)[0] 

1135 

1136 joined = False 

1137 for inObj in self.inObjs: 

1138 

1139 # append mask_clouds confidence only to the input GMS object with the same dimensions 

1140 if inObj.arr.shape[:2] == cfd_arr.shape: 

1141 inObj.logger.info( 

1142 'Joining mask_clouds_confidence to %s.' % (inObj.entity_ID if not inObj.subsystem else 

1143 '%s %s' % (inObj.entity_ID, inObj.subsystem))) 

1144 

1145 # set cloud mask confidence array 

1146 inObj.mask_clouds_confidence = GeoArray(cfd_arr, inObj.arr.gt, inObj.arr.prj, 

1147 nodata=get_outFillZeroSaturated(cfd_arr.dtype)[0]) 

1148 joined = True 

1149 

1150 if not joined: 

1151 self.logger.warning('Cloud mask confidence array has not been appended to one of the AC inputs because ' 

1152 'there was no input GMS object with the same dimensions.') 

1153 

1154 elif CFG.cloud_masking_algorithm[self.inObjs[0].satellite] == 'FMASK': 

1155 self.logger.info("Cloud mask confidence array is not appended to AC outputs because " 

1156 "'cloud_masking_algorithm' was set to FMASK in the job configuration and FMASK does not " 

1157 "confidence arrays.") 

1158 

1159 elif not CFG.ac_estimate_accuracy: 

1160 self.logger.info("Cloud mask confidence array is not appended to AC outputs because " 

1161 "'ac_estimate_accuracy' was set to False in the job configuration.") 

1162 else: 

1163 self.logger.warning("Atmospheric correction did not provide a 'mask_confidence_array' array for " 

1164 "attribute 'mask_clouds. GMS_object.mask_clouds_confidence kept None.")