Source code for hgrs.driver

import os
import glob

import numpy as np
import pandas as pd
import xarray as xr
import rioxarray as rxr

import h5py
import xml.etree.ElementTree as ET

from scipy.interpolate import RegularGridInterpolator

import datetime as dt
import logging

from . import SolarIrradiance, Reproj, Misc, Spectral


[docs] class Driver(): def __init__(self, satellite='enmap'): self.satellite = satellite if 'prisma' in satellite: self.driver = self.read_prisma elif 'enmap' in satellite: self.driver = self.read_l1c_enmap else: logging.info('satellite mission not recognized, stop') return def read_prisma(self, l1c_path: str, l2c_path: str, reflectance_unit=True, drop_vars=True, geoproject=True, parallel=False ): logging.info('construct L1C image plus angle rasters') try: dc_l1c = self.read_l1c_prisma(l1c_path, reflectance_unit=reflectance_unit, drop_vars=drop_vars) dc_l2c = self.read_l2c_prisma(l2c_path) except: logging.info('input file format not recognized, stop') return for param in ['sza', 'vza', 'raa']: dc_l1c[param] = dc_l2c[param] del dc_l2c # dc_l1c = dc_l1c.chunk({'x': 200, 'y': 200, 'wl': 10}) if geoproject: dc_l1c = Reproj().regridding(dc_l1c, parallel=parallel) return dc_l1c
[docs] def read_l1c_prisma(self, l1c_path: str, reflectance_unit=False, drop_vars=False): ''' Load PRISMA L1C data into xarray rasters :param l1c_path: absolute path to the .h5 prisma file :param reflectance_unit: to convert from TOA radiance to TOA reflectance :param drop_vars: if True remove the radiance raster to keep reflectance only :return: ''' # ============================================================================= # Load geolocation, solar irradiance and TOA radiance # ============================================================================= ds = h5py.File(l1c_path) # coarse geometry sza = ds.attrs["Sun_zenith_angle"] # Geolocation lat = ds["/HDFEOS/SWATHS/PRS_L1_HCO/Geolocation Fields/Latitude_VNIR"][:].T lon = ds["/HDFEOS/SWATHS/PRS_L1_HCO/Geolocation Fields/Longitude_VNIR"][:].T xdim, ydim = lat.shape # Wavelength / fwhm wl = ds.attrs["List_Cw_Vnir"][5:] wl = np.append(wl, ds.attrs["List_Cw_Swir"][:-2]) fwhm = ds.attrs["List_Fwhm_Vnir"][5:] fwhm = np.append(fwhm, ds.attrs["List_Fwhm_Swir"][:-2]) sort_index = np.argsort(wl) wl = wl[sort_index] fwhm = fwhm[sort_index] fwhm = xr.DataArray(data=fwhm, name='fwhm', coords=dict(wl=wl), attrs=dict(description="PRISMA relative spectral response parameter")) # solar irradiance convolution to the PRISMA spectral response function and scaled # by the day of the year solar_irr = SolarIrradiance() F0 = solar_irr.tsis # huillier # gueymard # kurucz date_str = ds.attrs["Product_StartTime"].decode('UTF-8') DOY = dt.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f").timetuple().tm_yday # get correction for Sun-Earth distance and correct solar irradiance D2 = Misc.earth_sun_correction(DOY) F0 = F0 * D2 F0_sensor = solar_irr.convolve(F0, fwhm, info={'description': 'Convolved solar irradiance from TSIS data', 'unit': 'mW/m2/nm'}) # DN to TOA radiance gain = {"vnir": ds.attrs["ScaleFactor_Vnir"], "swir": ds.attrs["ScaleFactor_Swir"]} # ------------------------------------------------------------------------------- VNIR = np.moveaxis(ds["/HDFEOS/SWATHS/PRS_L1_HCO/Data Fields/VNIR_Cube"][:, 5:, :] / gain["vnir"], [0, 1, 2], [1, 2, 0]) SWIR = np.moveaxis(ds["/HDFEOS/SWATHS/PRS_L1_HCO/Data Fields/SWIR_Cube"][:, :-2, :] / gain["swir"], [0, 1, 2], [1, 2, 0]) Ltoa = np.dstack((VNIR, SWIR)) del VNIR, SWIR Ltoa = Ltoa[:, :, sort_index] data = xr.Dataset(data_vars=dict(Ltoa=(["y", "x", "wl"], Ltoa), F0=(['wl'], F0_sensor.values), fwhm=(['wl'], fwhm.values), lon=(["y", "x"], lon), lat=(["y", "x"], lat)), coords=dict( x=np.arange(xdim)[::-1], y=np.arange(ydim)[::-1], time=dt.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f"), wl=wl), attrs=dict(description="PRISMA L1C cube data")) # chunk data for dask # data = data.chunk({'x': 200, 'y': 200, 'wl': -1}) # TODO check errors due to bulk SZA value instead of per pixel values if reflectance_unit: data['Rtoa'] = np.pi * data.Ltoa / (data.F0 * np.cos(np.radians(sza))) if drop_vars: data = data.drop_vars('Ltoa') # ============================================================================= # Load other metadata # ============================================================================= data.attrs["L1C_product_name"] = os.path.basename(l1c_path) data.attrs["acquisition_date"] = date_str data.attrs["sza"] = ds.attrs["Sun_zenith_angle"] data.attrs["saa"] = ds.attrs["Sun_azimuth_angle"] data.F0.attrs['unit'] = 'mW/m2/nm' data.F0.attrs['definition'] = 'Solar irradiance corrected for Sun-Earth distance' # ============================================================================= # Load masks # ============================================================================= data = data.assign(cloud_mask=(["y", "x"], ds["/HDFEOS/SWATHS/PRS_L1_HCO/Data Fields/Cloud_Mask"][:].T)) data = data.assign(sunglint_mask=(["y", "x"], ds["/HDFEOS/SWATHS/PRS_L1_HCO/Data Fields/SunGlint_Mask"][:].T)) data = data.assign(landcover_mask=(["y", "x"], ds["/HDFEOS/SWATHS/PRS_L1_HCO/Data Fields/LandCover_Mask"][:].T)) return data.sel(wl=slice(350, 2550))
[docs] def read_l2c_prisma(self, l2c_path: str): ''' Load PRISMA L2C data (including observation angles) into xarray rasters :param l2c_path: absolute path to the .h5 prisma file :return: ''' # ============================================================================= # Load geolocation, solar irradiance and TOA radiance # ============================================================================= ds = h5py.File(l2c_path) # Geolocation lat = ds["/HDFEOS/SWATHS/PRS_L2C_HCO/Geolocation Fields/Latitude"][:].T lon = ds["/HDFEOS/SWATHS/PRS_L2C_HCO/Geolocation Fields/Longitude"][:].T xdim, ydim = lat.shape # Wavelength / fwhm wl = ds.attrs["List_Cw_Vnir"][3:] wl = np.append(wl, ds.attrs["List_Cw_Swir"][:-2]) fwhm = ds.attrs["List_Fwhm_Vnir"][3:] fwhm = np.append(fwhm, ds.attrs["List_Fwhm_Swir"][:-2]) sort_index = np.argsort(wl) wl = wl[sort_index] fwhm = fwhm[sort_index] # # Thuillier solar irradiance convolved to the PRISMA ISRF and scaled # # by the day of the year # I0 = load_thuillier_solar_spectrum(wl, fwhm) # DOY = dt.datetime.strptime(ds.attrs["Product_StartTime"].decode('UTF-8'), # "%Y-%m-%dT%H:%M:%S.%f").timetuple().tm_yday # U = 1 - 0.01672 * np.cos(0.9856 * (DOY - 4)) # I0 = I0 * U # DN to TOA radiance gain = {"vnir_min": ds.attrs["L2ScaleVnirMin"], "vnir_max": ds.attrs["L2ScaleVnirMax"], "swir_min": ds.attrs["L2ScaleSwirMin"], "swir_max": ds.attrs["L2ScaleSwirMax"]} # ------------------------------------------------------------------------------- VNIR = ds["/HDFEOS/SWATHS/PRS_L2C_HCO/Data Fields/VNIR_Cube"][:, 3:, :] VNIR = gain["vnir_min"] + VNIR * (gain["vnir_max"] - gain["vnir_min"]) / 65535 VNIR = np.moveaxis(VNIR, [0, 1, 2], [1, 2, 0]) SWIR = ds["/HDFEOS/SWATHS/PRS_L2C_HCO/Data Fields/SWIR_Cube"][:, :-2, :] SWIR = gain["swir_min"] + SWIR * (gain["swir_max"] - gain["swir_min"]) / 65535 SWIR = np.moveaxis(SWIR, [0, 1, 2], [1, 2, 0]) # print(f"VNIR shape = {VNIR.shape}") # print(f"SWIR shape = {SWIR.shape}") rho = np.dstack((VNIR, SWIR)) rho = rho[:, :, sort_index] del VNIR, SWIR # # ------------------------------------------------------------------------------- data = xr.Dataset(data_vars=dict(rho=(["y", "x", "wl"], rho), lon=(["y", "x"], lon), lat=(["y", "x"], lat)), coords=dict( x=np.arange(xdim)[::-1], y=np.arange(ydim)[::-1], wl=wl), attrs=dict(description="PRISMA L2C cube data")) # ============================================================================= # Load other metadata # ============================================================================= data.attrs["L2C_product_name"] = os.path.basename(l2c_path) data.attrs["acquisition_date"] = ds.attrs["Product_StartTime"].decode('UTF-8') # ============================================================================= # Load geometries # ============================================================================= data = data.assign(vza=(["y", "x"], ds["/HDFEOS/SWATHS/PRS_L2C_HCO/Geometric Fields/Observing_Angle"][:].T)) data = data.assign(raa=(["y", "x"], ds["/HDFEOS/SWATHS/PRS_L2C_HCO/Geometric Fields/Rel_Azimuth_Angle"][:].T)) data = data.assign(sza=(["y", "x"], ds["/HDFEOS/SWATHS/PRS_L2C_HCO/Geometric Fields/Solar_Zenith_Angle"][:].T)) # ============================================================================= # Read atmospheric data # ============================================================================= hdf_variables = ["AOT", "AEX", "WVM", "COT"] ds_variables = ["aot", "aex", "wvm", "cot"] dims = {"AOT": ["y2", "x2"], "AEX": ["y2", "x2"], "WVM": ["y", "x"], "COT": ["y", "x"]} for ii, var in enumerate(hdf_variables): gain_min = ds.attrs[f"L2Scale{var}Min"] gain_max = ds.attrs[f"L2Scale{var}Max"] matrix = ds[f"/HDFEOS/SWATHS/PRS_L2C_{var}/Data Fields/{var}_Map"][:].T var_dims = dims[var] data = eval(f'data.assign({ds_variables[ii]}=({var_dims},gain_min + matrix*(gain_max-gain_min)/65535))') x2dim, y2dim = data['aot'].shape data = data.assign_coords({'x2': np.arange(x2dim)[::-1], 'y2': np.arange(y2dim)[::-1]}) return data
def read_l1c_enmap(self, l1c_path: str, reflectance_unit=False, drop_vars=False, filter_bad_bands=True, expon=1.8 ): for ext in ['BIL','TIF']: l1c_raster_path = glob.glob(os.path.join(l1c_path, "*SPECTRAL_IMAGE."+ext)) if len(l1c_raster_path)>0: l1c_raster_path =l1c_raster_path[0] break metadata_path = glob.glob(os.path.join(l1c_path, "*METADATA*.XML"))[0] # get XML metadata tree = ET.parse(metadata_path) root = tree.getroot() specific = root.find('specific') fwhm, offset, gain = {}, {}, {} for child in root.find('specific').find('bandCharacterisation'): wl = child.findtext('wavelengthCenterOfBand') fwhm[wl] = child.findtext('FWHMOfBand') offset[wl] = child.findtext('OffsetOfBand') gain[wl] = child.findtext('GainOfBand') fwhm = pd.DataFrame(fwhm.items(), columns=['wl', 'fwhm']).astype(float).set_index('wl').to_xarray() offset = pd.DataFrame(offset.items(), columns=['wl', 'offset']).astype(float).set_index('wl').to_xarray() gain = pd.DataFrame(gain.items(), columns=['wl', 'gain']).astype(float).set_index('wl').to_xarray() metadata = xr.merge([fwhm, offset, gain]) # get respective indexes of vnir and swir sensors self.vnir_idx = np.array(root.find('specific').find('vnirProductQuality' ).findtext('expectedChannelsList').split(',')).astype(int) - 1 self.swir_idx= np.array(root.find('specific').find('swirProductQuality' ).findtext('expectedChannelsList').split(',')).astype(int) - 1 ### set wavelength to keep (EnMAP shows strange values over the overlap between vnir and swir sensors self.vnir_idx_tokeep = self.vnir_idx[:-13] self.swir_idx_tokeep = self.swir_idx[3:] date_str = specific.findtext('datatakeStart').strip().replace("Z", "") # open raster data = rxr.open_rasterio(l1c_raster_path, chunks={'x': 512, 'y': 512, 'band': 20}, mask_and_scale=True).to_dataset(name='Ltoa') if '.BIL' in l1c_raster_path: data = data.swap_dims({'band': 'wavelength'}).rename({'wavelength': 'wl'}) else: # get FWHM data and scale radiance data = data.rename({'band': 'wl'}) data['wl'] = metadata.wl data['fwhm'] = metadata.fwhm data['Ltoa'] = metadata.gain * data['Ltoa'] + metadata.offset data = data.transpose("wl", "y", "x") # data['time'] = dt.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f") # data = data.set_coords('time') # convert from W.m-2.sr-1.nm-1 to mW.m-2.sr-1.nm-1 data['Ltoa'] = 1e3 * data['Ltoa'].drop_attrs() data['Ltoa'].attrs['unit'] = 'mW.m-2.sr-1.nm-1' data['Ltoa'].attrs['description'] = 'top-of-atmosphere radiance' data['Ltoa'].attrs['name'] = 'radiance' x = data.x.values y = data.y.values x_coarse = [x[0], x[-1]] y_coarse = [y[0], y[-1]] XX, YY = np.meshgrid(x, y) points = np.stack([YY.ravel(), XX.ravel()], axis=-1) sza_values = np.zeros((2, 2)) sza_values[0, 0] = 90 - float(specific.find('sunElevationAngle').findtext('upper_left')) sza_values[0, 1] = 90 - float(specific.find('sunElevationAngle').findtext('upper_right')) sza_values[1, 0] = 90 - float(specific.find('sunElevationAngle').findtext('lower_left')) sza_values[1, 1] = 90 - float(specific.find('sunElevationAngle').findtext('lower_right')) sza_interp = RegularGridInterpolator((y_coarse, x_coarse), sza_values, method='linear') sza = sza_interp(points).reshape(YY.shape) saa_values = np.zeros((2, 2)) saa_values[0, 0] = float(specific.find('sunAzimuthAngle').findtext('upper_left')) saa_values[0, 1] = float(specific.find('sunAzimuthAngle').findtext('upper_right')) saa_values[1, 0] = float(specific.find('sunAzimuthAngle').findtext('lower_left')) saa_values[1, 1] = float(specific.find('sunAzimuthAngle').findtext('lower_right')) saa_interp = RegularGridInterpolator((y_coarse, x_coarse), saa_values, method='linear') saa = saa_interp(points).reshape(YY.shape) vza_values = np.zeros((2, 2)) vza_values[0, 0] = float(specific.find('viewingZenithAngle').findtext('upper_left')) vza_values[0, 1] = float(specific.find('viewingZenithAngle').findtext('upper_right')) vza_values[1, 0] = float(specific.find('viewingZenithAngle').findtext('lower_left')) vza_values[1, 1] = float(specific.find('viewingZenithAngle').findtext('lower_right')) vza_interp = RegularGridInterpolator((y_coarse, x_coarse), vza_values, method='linear') vza = vza_interp(points).reshape(YY.shape) vaa_values = np.zeros((2, 2)) vaa_values[0, 0] = float(specific.find('viewingAzimuthAngle').findtext('upper_left')) vaa_values[0, 1] = float(specific.find('viewingAzimuthAngle').findtext('upper_right')) vaa_values[1, 0] = float(specific.find('viewingAzimuthAngle').findtext('lower_left')) vaa_values[1, 1] = float(specific.find('viewingAzimuthAngle').findtext('lower_right')) vaa_interp = RegularGridInterpolator((y_coarse, x_coarse), vaa_values, method='linear') vaa = vaa_interp(points).reshape(YY.shape) raa = (saa - vaa) % 360 solar_irr = SolarIrradiance() F0 = solar_irr.tsis # thuillier # gueymard # kurucz ## Compute irradiance for the Day Of the Year (date of acquisition) DOY = dt.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f").timetuple().tm_yday # get correction for Sun-Earth distance and correct solar irradiance D2 = Misc.earth_sun_correction(DOY) F0 = F0 * D2 self.F0 = F0 # convolution with spectral responses spectral = Spectral(data.wl, data.fwhm.values) # TODO check which one better #F0 = spectral.convolve2(F0, expon=expon) F0 = spectral.convolve(F0) F0_sensor = solar_irr.convolve(F0, data.fwhm, info={'description': 'Convolved solar irradiance from TSIS data', 'unit': 'mW/m2/nm'}) data = xr.Dataset(data_vars=dict(Ltoa=data.Ltoa, F0=(['wl'], F0_sensor.values), fwhm=(['wl'], data.fwhm.values), sza=(["y", "x"], sza), saa=(["y", "x"], saa), vza=(["y", "x"], vza), vaa=(["y", "x"], vaa), raa=(['y', 'x'], raa), ), coords=dict( x=data.x.values, y=data.y.values, wl=data.wl.values, time=dt.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f")), attrs=dict(description="EnMAP L1C cube data")) ## Compute Top of Atmosphere Reflectance if reflectance_unit: ## WARNING we take the mean SZA value to save time/memory ## ## this could induce 0.1% uncertainty onn the Ltoa to Rtoa conversion ## mu0 = np.cos(np.radians(data.sza.mean())) data['Rtoa'] = np.pi * data.Ltoa / (data.F0 * mu0) if drop_vars: data = data.drop_vars('Ltoa') # discard angles outside the image frame params = ['sza', 'vza', 'raa', 'saa', 'vaa'] for i in range(len(params)): data[params[i]] = data[params[i]].where(data.Rtoa.isel(wl=0) > 0) wl_vnir = data.wl.isel(wl=self.vnir_idx).values wl_swir = data.wl.isel(wl=self.swir_idx).values if filter_bad_bands: wl_vnir = data.wl.isel(wl=self.vnir_idx_tokeep).values wl_swir = data.wl.isel(wl=self.swir_idx_tokeep).values data =data.isel(wl=[*self.vnir_idx_tokeep,*self.swir_idx_tokeep]) # Attributes data.attrs["L1C_product_name"] = os.path.basename(l1c_path) data.attrs["acquisition_date"] = dt.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f") data.attrs["vnir_index"] = self.vnir_idx data.attrs["swir_index"] = self.swir_idx data.attrs["vnir_index_tokeep"] = self.vnir_idx_tokeep data.attrs["swir_index_tokeep"] = self.swir_idx_tokeep data.attrs["vnir_bands"] = wl_vnir data.attrs["swir_bands"] = wl_swir data.sza.attrs['definition'] = " Sun Zenith Angle" data.saa.attrs['definition'] = " Sun Azimuth Angle" data.vza.attrs['definition'] = " Viewing Zenith Angle" data.vaa.attrs['definition'] = " Viewing Azimuth Angle" data.raa.attrs['definition'] = " Relative Azimuth Angle" data.sza.attrs['unit'] = "degree" data.saa.attrs['unit'] = "degree" data.vza.attrs['unit'] = "degree" data.vaa.attrs['unit'] = "degree" data.raa.attrs['unit'] = "degree" data.F0.attrs['unit'] = 'mW/m2/nm' data.Rtoa.attrs['definition'] = 'Reflectance at the Top of the Atmosphere' data.Rtoa.attrs['unit'] = '-' data.F0.attrs['definition'] = 'Solar irradiance corrected for Sun-Earth distance' data.Ltoa.attrs['unit'] = 'mW/m2/sr/nm' data.Ltoa.attrs['definition'] = 'Top-of-atmosphere radiance' data.fwhm.attrs['unit'] = 'nm' data.fwhm.attrs['definition'] = 'Full Width at Half Maximum' # data.attrs['crs'] = CRS.from_epsg(32630) return data