diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a6b932a51e8432b7cb636800ca082d04e7b7a877..d33908a484fe8db81c5905c071d875ee77003e40 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,6 @@ test_enpt: # update some dependencies # - pip install 'geoarray>=0.15.8' # - pip install 'py_tools_ds>=0.14.23' - - mamba install "sicor>=0.19.1" # install sicor and perhaps switch sicor branch # - rm -rf context/sicor diff --git a/HISTORY.rst b/HISTORY.rst index 46d1eb7f2a86661e753400d9ad8eb96729669524..5fc40733c81dbe98d89b2fe36a78b8773fc2a9fa 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,14 @@ History ======= +0.20.0 (coming soon) +-------------------- + +* !103: Revised the EnPT orthorectification to support the completely re-implemented sensormapgeo package (v1.0.0), + which leads to 30-50x faster orthorectification and adds support for all GDAL-supported resampling techniques. +* !103: Fixed #58 (Holes in the orthorectified image in case of geographic target projection and bilinear resampling). + + 0.19.7 (2024-01-04) ------------------- diff --git a/enpt/cli.py b/enpt/cli.py index c044bda66c26655bde40ccd9ef2561fb0b45df4a..cc4f30cf17361d9c0c2c39938f53cf3c381f94f9 100644 --- a/enpt/cli.py +++ b/enpt/cli.py @@ -140,7 +140,8 @@ def get_enpt_argparser(): help="Spatial interpolation algorithm to be used during dead pixel correction " "('linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic')") add('--ortho_resampAlg', type=str, default='bilinear', - help="Ortho-rectification resampling algorithm ('nearest', 'bilinear', 'gauss')") + help="Ortho-rectification resampling algorithm ('nearest', 'bilinear', 'gauss', 'cubic', 'cubic_spline', " + "'lanczos', 'average', 'mode', 'max', 'min', 'med', 'q1', 'q3')") add('--vswir_overlap_algorithm', type=str, default='swir_only', help="Algorithm specifying how to deal with the spectral bands in the VNIR/SWIR spectral overlap region " "('order_by_wvl', 'average', 'vnir_only', 'swir_only')") diff --git a/enpt/model/images/image_baseclasses.py b/enpt/model/images/image_baseclasses.py index 982dd575f80e480216e1621794677a3e99b3b65e..d0be043ebcc1cbb1229f23f9f86647fd954d277f 100644 --- a/enpt/model/images/image_baseclasses.py +++ b/enpt/model/images/image_baseclasses.py @@ -124,7 +124,7 @@ class _EnMAP_Image(object): self.data = '/path/to/image.tif' # sets self.data to GeoArray('/path/to/image.tif') - Link a numpy.ndarray instance with self.data (remaining attributes like geocoding, projection, etc. - are copied from the previous self.data attribute. + are copied from the previous self.data attribute). self.data = numpy.array([[1,2,3],[4,5,6]]) - Set self.data to an existing instance of GeoArray @@ -197,7 +197,7 @@ class _EnMAP_Image(object): @property def mask_cloudshadow(self) -> GeoArray: - """Return the cloud shadow mask (0=no cloud shadow, 1=cloud shadow).. + """Return the cloud shadow mask (0=no cloud shadow, 1=cloud shadow). :return: geoarray.GeoArray """ @@ -230,7 +230,7 @@ class _EnMAP_Image(object): @property def mask_snow(self) -> GeoArray: - """Return the snow mask (0=no snow, 1=snow).. + """Return the snow mask (0=no snow, 1=snow). :return: geoarray.GeoArray """ @@ -246,7 +246,7 @@ class _EnMAP_Image(object): @property def mask_cirrus(self) -> GeoArray: - """Return the cirrus mask (0=none, 1=thin, 2=medium, 3=thick).. + """Return the cirrus mask (0=none, 1=thin, 2=medium, 3=thick). :return: geoarray.GeoArray """ diff --git a/enpt/model/images/images_sensorgeo.py b/enpt/model/images/images_sensorgeo.py index f4388d2c12c7f0c410468169c704496adccec745..4cc7120638c9d9bd50bf49746f72141b25946da9 100644 --- a/enpt/model/images/images_sensorgeo.py +++ b/enpt/model/images/images_sensorgeo.py @@ -372,9 +372,11 @@ class EnMAP_Detector_SensorGeo(_EnMAP_Image): src_lats: np.ndarray, src_epsg: int, resamp_alg: str = 'nearest', - respect_keystone: bool = False + respect_keystone: bool = False, + src_nodata: int = None, + tgt_nodata: int = None ) -> np.ndarray: - """Transform the given input raster from SWIR to VNIR or from SWIR to VNIR sensor geometry. + """Transform the given input raster from VNIR to SWIR sensor geometry or vice-versa. NOTE: @@ -391,9 +393,12 @@ class EnMAP_Detector_SensorGeo(_EnMAP_Image): :param src_lats: geolayer latitudes corresponding to the input array (same nbands like the source detector) :param src_epsg: projection EPSG code of the source array - :param resamp_alg: resampling algorithm ('nearest', 'bilinear', 'gauss', 'custom') + :param resamp_alg: resampling algorithm ('nearest', 'near', 'bilinear', 'cubic', 'cubic_spline', + 'lanczos', 'average', 'mode', 'max', 'min', 'med', 'q1', 'q3') :param respect_keystone: whether to use the full geoarray (all bands) in case a 3D array in the dimension of the source detector is passed (default: False) + :param src_nodata: nodata value of the input array + :param tgt_nodata: pixel value to be used for undetermined pixels in the output :return: """ detN = self.detector_name @@ -431,7 +436,8 @@ class EnMAP_Detector_SensorGeo(_EnMAP_Image): res_vnir=(30, 30), res_swir=(30, 30), resamp_alg=resamp_alg, - # radius_of_influence=45, + src_nodata=src_nodata, + tgt_nodata=tgt_nodata, nprocs=self.cfg.CPUs ) if detN == 'VNIR': @@ -478,7 +484,9 @@ class EnMAP_VNIR_SensorGeo(EnMAP_Detector_SensorGeo): swir_lats: np.ndarray, swir_epsg: int, resamp_alg: str = 'nearest', - respect_keystone: bool = False + respect_keystone: bool = False, + src_nodata: int = None, + tgt_nodata: int = None ) -> np.ndarray: """Transform the given SWIR sensor-geometry raster array into VNIR sensor geometry. @@ -486,12 +494,15 @@ class EnMAP_VNIR_SensorGeo(EnMAP_Detector_SensorGeo): :param swir_lons: longitude geolayer array of the SWIR :param swir_lats: latitude geolayer array of the SWIR :param swir_epsg: EPSG code of the SWIR when transformed to map geometry - :param resamp_alg: resampling algorith ('nearest', 'bilinear', 'gauss', 'custom') + :param resamp_alg: resampling algorith ('nearest', 'near', 'bilinear', 'cubic', 'cubic_spline', + 'lanczos', 'average', 'mode', 'max', 'min', 'med', 'q1', 'q3') :param respect_keystone: whether to use the full geoarray (all bands) in case a 3D array in the dimension of the SWIR detector is passed (default: False) + :param src_nodata: nodata value of the input array + :param tgt_nodata: pixel value to be used for undetermined pixels in the output """ return self._transform_raster_geometry_from_other_detector( - array_swirsensorgeo, swir_lons, swir_lats, swir_epsg, resamp_alg, respect_keystone) + array_swirsensorgeo, swir_lons, swir_lats, swir_epsg, resamp_alg, respect_keystone, src_nodata, tgt_nodata) class EnMAP_SWIR_SensorGeo(EnMAP_Detector_SensorGeo): @@ -513,7 +524,9 @@ class EnMAP_SWIR_SensorGeo(EnMAP_Detector_SensorGeo): vnir_lats: np.ndarray, vnir_epsg: int, resamp_alg: str = 'nearest', - respect_keystone: bool = False + respect_keystone: bool = False, + src_nodata: int = None, + tgt_nodata: int = None ) -> np.ndarray: """Transform the given VNIR sensor-geometry raster array into SWIR sensor geometry. @@ -521,12 +534,15 @@ class EnMAP_SWIR_SensorGeo(EnMAP_Detector_SensorGeo): :param vnir_lons: longitude geolayer array of the VNIR :param vnir_lats: latitude geolayer array of the VNIR :param vnir_epsg: EPSG code of the VNIR when transformed to map geometry - :param resamp_alg: resampling algorith ('nearest', 'bilinear', 'gauss', 'custom') + :param resamp_alg: resampling algorith ('nearest', 'near', 'bilinear', 'cubic', 'cubic_spline', + 'lanczos', 'average', 'mode', 'max', 'min', 'med', 'q1', 'q3') :param respect_keystone: whether to use the full geoarray (all bands) in case a 3D array in the dimension of the VNIR detector is passed (default: False) + :param src_nodata: nodata value of the input array + :param tgt_nodata: pixel value to be used for undetermined pixels in the output """ return self._transform_raster_geometry_from_other_detector( - array_vnirsensorgeo, vnir_lons, vnir_lats, vnir_epsg, resamp_alg, respect_keystone) + array_vnirsensorgeo, vnir_lons, vnir_lats, vnir_epsg, resamp_alg, respect_keystone, src_nodata, tgt_nodata) class EnMAPL1Product_SensorGeo(object): @@ -798,14 +814,19 @@ class EnMAPL1Product_SensorGeo(object): def transform_vnir_to_swir_raster(self, array_vnirsensorgeo: np.ndarray, resamp_alg: str = 'nearest', - respect_keystone: bool = False + respect_keystone: bool = False, + src_nodata: int = None, + tgt_nodata: int = None ) -> np.ndarray: """Transform the given array from VNIR into SWIR sensor geometry. :param array_vnirsensorgeo: raster array in VNIR sensor geometry to be transformed into SWIR sensor geometry - :param resamp_alg: resampling algorithm ('nearest', 'bilinear', 'gauss', 'custom') + :param resamp_alg: resampling algorithm ('nearest', 'near', 'bilinear', 'cubic', 'cubic_spline', + 'lanczos', 'average', 'mode', 'max', 'min', 'med', 'q1', 'q3') :param respect_keystone: whether to use the full geoarray (all bands) in case a 3D array in the dimension of the VNIR detector is passed (default: False) + :param src_nodata: nodata value of the input array + :param tgt_nodata: pixel value to be used for undetermined pixels in the output """ if self.meta.vnir.lons is None or self.meta.vnir.lats is None or \ self.meta.swir.lons is None or self.meta.swir.lats is None: @@ -817,18 +838,26 @@ class EnMAPL1Product_SensorGeo(object): vnir_lats=self.meta.vnir.lats, vnir_epsg=self.meta.vnir.epsg_ortho, resamp_alg=resamp_alg, - respect_keystone=respect_keystone) + respect_keystone=respect_keystone, + src_nodata=src_nodata, + tgt_nodata=tgt_nodata + ) def transform_swir_to_vnir_raster(self, array_swirsensorgeo: np.ndarray, resamp_alg: str = 'nearest', - respect_keystone: bool = False + respect_keystone: bool = False, + src_nodata: int = None, + tgt_nodata: int = None ) -> np.ndarray: """Transform the given array from SWIR into VNIR sensor geometry. :param array_swirsensorgeo: raster array in SWIR sensor geometry to be transformed into VNIR sensor geometry - :param resamp_alg: resampling algorithm ('nearest', 'bilinear', 'gauss', 'custom') + :param resamp_alg: resampling algorithm ('nearest', 'near', 'bilinear', 'cubic', 'cubic_spline', + 'lanczos', 'average', 'mode', 'max', 'min', 'med', 'q1', 'q3') :param respect_keystone: whether to use the full geoarray (all bands) in case a 3D array in the dimension of the VNIR detector is passed (default: False) + :param src_nodata: nodata value of the input array + :param tgt_nodata: pixel value to be used for undetermined pixels in the output """ if self.meta.vnir.lons is None or self.meta.vnir.lats is None or \ self.meta.swir.lons is None or self.meta.swir.lats is None: @@ -840,18 +869,26 @@ class EnMAPL1Product_SensorGeo(object): swir_lats=self.meta.swir.lats, swir_epsg=self.meta.swir.epsg_ortho, resamp_alg=resamp_alg, - respect_keystone=respect_keystone) + respect_keystone=respect_keystone, + src_nodata=src_nodata, + tgt_nodata=tgt_nodata + ) def set_SWIRattr_with_transformedVNIRattr(self, attrName: str, resamp_alg: str = 'nearest', - respect_keystone: bool = False + respect_keystone: bool = False, + src_nodata: int = None, + tgt_nodata: int = None ) -> None: """Set the specified SWIR raster attribute with a VNIR attribute transformed to SWIR sensor geometry. :param attrName: name of the attribute to be set - :param resamp_alg: resampling algorithm ('nearest', 'bilinear', 'gauss', 'custom') + :param resamp_alg: resampling algorithm ('nearest', 'near', 'bilinear', 'cubic', 'cubic_spline', + 'lanczos', 'average', 'mode', 'max', 'min', 'med', 'q1', 'q3') :param respect_keystone: whether to use the full geoarray (all bands) in case the attribute to be transformed is 'data' (default: False) + :param src_nodata: nodata value of the input array + :param tgt_nodata: pixel value to be used for undetermined pixels in the output """ self.logger.info("Transforming the '%s' attribute from VNIR to SWIR sensor geometry." % attrName) @@ -862,7 +899,10 @@ class EnMAPL1Product_SensorGeo(object): attr_transformed = self.transform_vnir_to_swir_raster(array_vnirsensorgeo=np.array(vnir_rasterAttr), resamp_alg=resamp_alg, - respect_keystone=respect_keystone) + respect_keystone=respect_keystone, + src_nodata=src_nodata, + tgt_nodata=tgt_nodata, + ) setattr(self.swir, attrName, attr_transformed) def run_AC(self): diff --git a/enpt/model/metadata/metadata_sensorgeo.py b/enpt/model/metadata/metadata_sensorgeo.py index 3d7b4c7376999afd1964f5dbfd494107a1ca9062..a95da215ed2ac49fc320095a490ba4317cd56c79 100644 --- a/enpt/model/metadata/metadata_sensorgeo.py +++ b/enpt/model/metadata/metadata_sensorgeo.py @@ -105,8 +105,9 @@ class EnMAP_Metadata_L1B_Detector_SensorGeo(object): self.epsg_ortho: Optional[int] = None # EPSG code of the orthorectified image self.rpc_coeffs: OrderedDict = OrderedDict() # RPC coefficients for geolayer computation self.ll_mapPoly: Optional[Polygon] = None # footprint polygon in longitude/latitude map coordinates - self.lats: Optional[np.ndarray] = None # 2D array of latitude coordinates according to given lon/lat sampling - self.lons: Optional[np.ndarray] = None # 2D array of longitude coordinates according to given lon/lat sampling + self.lats: Optional[np.ndarray] = None # 2D/3D array of latitude coordinates + self.lons: Optional[np.ndarray] = None # 2D/3D array of longitude coordinates + self.geolayer_has_keystone: Optional[bool] = None # indicates if lon/lat geolayer considers keystone (3D array) self.unit: str = '' # radiometric unit of pixel values self.unitcode: str = '' # code of radiometric unit self.preview_wvls: Optional[List[float]] = None # wavelengths to be used for quicklook images @@ -267,6 +268,7 @@ class EnMAP_Metadata_L1B_Detector_SensorGeo(object): self.lats = self.interpolate_corners(*self.lat_UL_UR_LL_LR, self.ncols, self.nrows) self.lons = self.interpolate_corners(*self.lon_UL_UR_LL_LR, self.ncols, self.nrows) + self.geolayer_has_keystone = False self.preview_wvls = np.array([self.wvl_center[i] for i in self.preview_bands]) # drop bad bands from metadata if desired @@ -427,13 +429,17 @@ class EnMAP_Metadata_L1B_Detector_SensorGeo(object): def compute_geolayer_for_cube(self): self.logger.info('Computing %s geolayer...' % self.detector_name) - lons, lats = \ - RPC_3D_Geolayer_Generator(rpc_coeffs_per_band=self.rpc_coeffs, - elevation=self.cfg.path_dem if self.cfg.path_dem else self.cfg.average_elevation, - enmapIm_cornerCoords=tuple(zip(self.lon_UL_UR_LL_LR, self.lat_UL_UR_LL_LR)), - enmapIm_dims_sensorgeo=(self.nrows, self.ncols), - CPUs=self.cfg.CPUs)\ - .compute_geolayer() + GeolayerGen = \ + RPC_3D_Geolayer_Generator( + rpc_coeffs_per_band=self.rpc_coeffs, + elevation=self.cfg.path_dem if self.cfg.path_dem else self.cfg.average_elevation, + enmapIm_cornerCoords=tuple(zip(self.lon_UL_UR_LL_LR, self.lat_UL_UR_LL_LR)), + enmapIm_dims_sensorgeo=(self.nrows, self.ncols), + CPUs=self.cfg.CPUs + ) + lons, lats = GeolayerGen.compute_geolayer() + + self.geolayer_has_keystone = len(GeolayerGen.bandgroups_with_unique_rpc_coeffs) > 1 return lons, lats diff --git a/enpt/options/config.py b/enpt/options/config.py index a5c11c7d381166afaad7145ea2c6f4940339fd5f..b9671cce5b0997939acc1dee16dffbb26aa0bbb2 100644 --- a/enpt/options/config.py +++ b/enpt/options/config.py @@ -97,7 +97,7 @@ config_for_testing_water = dict( enable_keystone_correction=False, enable_vnir_swir_coreg=False, n_lines_to_append=None, - ortho_resampAlg='gauss', + ortho_resampAlg='bilinear', run_deadpix_P=True, run_smile_P=False, scale_factor_boa_ref=10000, @@ -190,7 +190,7 @@ config_for_testing_dlr = dict( enable_ac=True, mode_ac='land', CPUs=32, - ortho_resampAlg='gauss', + ortho_resampAlg='bilinear', vswir_overlap_algorithm='swir_only' ) @@ -326,7 +326,8 @@ class EnPTConfig(object): ('linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic') :key ortho_resampAlg: - Ortho-rectification resampling algorithm ('nearest', 'bilinear', 'gauss') + Ortho-rectification resampling algorithm ('nearest', 'bilinear', 'gauss', 'cubic', 'cubic_spline', + 'lanczos', 'average', 'mode', 'max', 'min', 'med', 'q1', 'q3') :key target_projection_type: Projection type of the raster output files ('UTM', 'Geographic') (default: 'UTM') @@ -466,12 +467,6 @@ class EnPTConfig(object): self.target_coord_grid if self.target_coord_grid else \ enmap_coordinate_grid_utm if self.target_projection_type == 'UTM' else None - # bug warning regarding holes in bilinear resampling output - if self.target_projection_type == 'Geographic' and self.ortho_resampAlg == 'bilinear': - warnings.warn("There is currently a bug that causes holes in the bilinear resampling results if the " - "target projection is 'Geographic'. It is recommended to use 'nearest' or 'gauss' instead.", - UserWarning) - @staticmethod def absPath(path): return path if not path or os.path.isabs(path) else os.path.abspath(os.path.join(path_enptlib, path)) diff --git a/enpt/options/options_default.json b/enpt/options/options_default.json index 2cd19cd91b53988e9403bba93a8f57d6dd4697c5..1096e7c821841662cb6ed7b22c8702b79307de6b 100644 --- a/enpt/options/options_default.json +++ b/enpt/options/options_default.json @@ -90,7 +90,9 @@ }, "orthorectification": { - "resamp_alg": "gauss", /*Ortho-rectification resampling algorithm ('nearest', 'bilinear', 'gauss')*/ + "resamp_alg": "bilinear", /*Ortho-rectification resampling algorithm ('nearest', 'bilinear', 'gauss', + 'cubic', 'cubic_spline', 'lanczos', 'average', 'mode', 'max', 'min', 'med', + 'q1', 'q3')*/ "vswir_overlap_algorithm": "swir_only", /*Algorithm how to output the spectral bands in the VNIR/SWIR spectral overlap region 'order_by_wvl': keep spectral bands unchanged, order bands by wavelength 'average': average the spectral information within the overlap diff --git a/enpt/options/options_schema.py b/enpt/options/options_schema.py index 45b0bfbf409d49ee2f962abccd126cf737110eeb..3b832b35696d37a03a9f9dca7cd9a060a242a782 100644 --- a/enpt/options/options_schema.py +++ b/enpt/options/options_schema.py @@ -114,7 +114,9 @@ enpt_schema_input = dict( orthorectification=dict( type='dict', required=False, schema=dict( - resamp_alg=dict(type='string', required=False, allowed=['nearest', 'bilinear', 'gauss']), + resamp_alg=dict(type='string', required=False, allowed=['nearest', 'bilinear', 'gauss', 'cubic', + 'cubic_spline', 'lanczos', 'average', + 'mode', 'max', 'min', 'med', 'q1', 'q3']), vswir_overlap_algorithm=dict(type='string', required=False, allowed=['order_by_wvl', 'average', 'vnir_only', 'swir_only']), target_projection_type=dict(type='string', required=False, allowed=['UTM', 'Geographic']), diff --git a/enpt/processors/atmospheric_correction/atmospheric_correction.py b/enpt/processors/atmospheric_correction/atmospheric_correction.py index 128e49c7af9bb6ef80df8bf18c35393dc7cb748d..a75688c995e5d6c3a4ca2d674b2e009ac70ee67d 100644 --- a/enpt/processors/atmospheric_correction/atmospheric_correction.py +++ b/enpt/processors/atmospheric_correction/atmospheric_correction.py @@ -292,7 +292,7 @@ class AtmosphericCorrector(object): :return: atmospherically corrected output EnMAP image containing BOA reflectance / water leaving reflectance (an instance EnMAPL1Product_SensorGeo) """ - enmap_ImageL1.set_SWIRattr_with_transformedVNIRattr('mask_landwater') + enmap_ImageL1.set_SWIRattr_with_transformedVNIRattr('mask_landwater', src_nodata=0, tgt_nodata=0) enmap_ImageL1.logger.info( f"Starting atmospheric correction for VNIR and SWIR detector in '{self.cfg.mode_ac}' mode. " diff --git a/enpt/processors/dem_preprocessing/dem_preprocessing.py b/enpt/processors/dem_preprocessing/dem_preprocessing.py index e798dbedd88d25f1ec90ea81909684430f261592..1bccf7c3074c172bf90b499d2f3c0cc7dd601547 100644 --- a/enpt/processors/dem_preprocessing/dem_preprocessing.py +++ b/enpt/processors/dem_preprocessing/dem_preprocessing.py @@ -127,7 +127,7 @@ class DEM_Processor(object): def to_sensor_geometry(self, lons: np.ndarray, lats: np.ndarray): - GT = Geometry_Transformer(lons=lons, lats=lats, nprocs=self.CPUs) + GT = Geometry_Transformer(lons=lons, lats=lats, backend='gdal', resamp_alg='bilinear', nprocs=self.CPUs) data_sensorgeo = GT.to_sensor_geometry(self.dem) return GeoArray(data_sensorgeo) diff --git a/enpt/processors/orthorectification/orthorectification.py b/enpt/processors/orthorectification/orthorectification.py index aad2a8c0c28d4b1065077e2b7c081bb327cc919d..a43a042fdd76585e61cf7ab71a0259c03f60dd3b 100644 --- a/enpt/processors/orthorectification/orthorectification.py +++ b/enpt/processors/orthorectification/orthorectification.py @@ -36,6 +36,7 @@ from typing import Tuple, Union # noqa: F401 from types import SimpleNamespace import numpy as np +from pyproj import Geod from mvgavg import mvgavg from geoarray import GeoArray from py_tools_ds.geo.coord_trafo import transform_any_prj @@ -46,7 +47,6 @@ from ...model.images import EnMAPL1Product_SensorGeo, EnMAPL2Product_MapGeo from ...model.metadata import EnMAP_Metadata_L2A_MapGeo from ..spatial_transform import \ Geometry_Transformer, \ - Geometry_Transformer_3D, \ move_extent_to_coord_grid __author__ = 'Daniel Scheffler' @@ -72,6 +72,16 @@ class Orthorectifier(object): raise RuntimeError('Expected a %s geolayer shape of %s or %s. Received %s.' % (detector.detector_name, str(datashape), str(datashape[:2]), str(XY.shape))) + @staticmethod + def get_enmap_coordinate_grid_ll(lon: float, lat: float + ) -> (Tuple[float, float], Tuple[float, float]): + """Return EnMAP-like (30x30m) longitude/latitude pixel grid specs at the given position.""" + geod = Geod(ellps="WGS84") + delta_lon = abs(lon - geod.fwd(lon, lat, az=90, dist=30)[0]) + delta_lat = abs(lat - geod.fwd(lon, lat, az=0, dist=30)[1]) + + return (lon, lon + delta_lon), (lat, lat + delta_lat) + def run_transformation(self, enmap_ImageL1: EnMAPL1Product_SensorGeo) -> EnMAPL2Product_MapGeo: self.validate_input(enmap_ImageL1) @@ -85,36 +95,48 @@ class Orthorectifier(object): lons_vnir, lats_vnir = enmap_ImageL1.vnir.detector_meta.lons, enmap_ImageL1.vnir.detector_meta.lats lons_swir, lats_swir = enmap_ImageL1.swir.detector_meta.lons, enmap_ImageL1.swir.detector_meta.lats + if not enmap_ImageL1.vnir.detector_meta.geolayer_has_keystone and lons_vnir.ndim == 3: + lons_vnir, lats_vnir = lons_vnir[:, :, 0], lats_vnir[:, :, 0] + if not enmap_ImageL1.swir.detector_meta.geolayer_has_keystone and lons_swir.ndim == 3: + lons_swir, lats_swir = lons_swir[:, :, 0], lats_swir[:, :, 0] # get target EPSG code and common extent + # (VNIR/SWIR overlap, i.e., INNER extent - non-overlapping parts are cleared later) tgt_epsg = enmap_ImageL1.meta.vnir.epsg_ortho tgt_extent = self._get_common_extent(enmap_ImageL1, tgt_epsg, enmap_grid=True) - kw_init = dict(resamp_alg=self.cfg.ortho_resampAlg, - nprocs=self.cfg.CPUs, - radius_of_influence=30 if not self.cfg.ortho_resampAlg == 'bilinear' else 45 - ) - if self.cfg.ortho_resampAlg == 'bilinear': - # increase that if the resampling result contains gaps (default is 32 but this is quite slow) - kw_init['neighbours'] = 8 - - kw_trafo = dict(tgt_prj=tgt_epsg, tgt_extent=tgt_extent, - tgt_coordgrid=((self.cfg.target_coord_grid['x'], self.cfg.target_coord_grid['y']) - if self.cfg.target_coord_grid else None) - ) - # transform VNIR and SWIR to map geometry - GeoTransformer = Geometry_Transformer if lons_vnir.ndim == 2 else Geometry_Transformer_3D + # set up parameters for Geometry_Transformer initialization and execution of the transformation + kw_init = dict( + backend='gdal' if self.cfg.ortho_resampAlg != 'gauss' else 'pyresample', + resamp_alg=self.cfg.ortho_resampAlg, + nprocs=self.cfg.CPUs + ) + kw_trafo = dict( + tgt_prj=tgt_epsg, + tgt_extent=tgt_extent, + tgt_coordgrid=((self.cfg.target_coord_grid['x'], + self.cfg.target_coord_grid['y']) + if self.cfg.target_coord_grid else + None), + src_nodata=enmap_ImageL1.vnir.data.nodata, + tgt_nodata=self.cfg.output_nodata_value + ) + # make sure VNIR and SWIR are also transformed to the same lon/lat pixel grid + if self.cfg.target_projection_type == 'Geographic' and kw_trafo['tgt_coordgrid'] is None: + center_row, center_col = lons_vnir.shape[0] // 2, lons_vnir.shape[1] // 2 + center_lon, center_lat = lons_vnir[center_row, center_col], lats_vnir[center_row, center_col] + kw_trafo['tgt_coordgrid'] = self.get_enmap_coordinate_grid_ll(center_lon, center_lat) - # FIXME So far, the fill value is set to 0. Is this meaningful? + # transform VNIR and SWIR to map geometry enmap_ImageL1.logger.info("Orthorectifying VNIR data using '%s' resampling algorithm..." % self.cfg.ortho_resampAlg) - GT_vnir = GeoTransformer(lons=lons_vnir, lats=lats_vnir, fill_value=self.cfg.output_nodata_value, **kw_init) + GT_vnir = Geometry_Transformer(lons=lons_vnir, lats=lats_vnir, **kw_init) vnir_mapgeo_gA = GeoArray(*GT_vnir.to_map_geometry(enmap_ImageL1.vnir.data, **kw_trafo), nodata=self.cfg.output_nodata_value) enmap_ImageL1.logger.info("Orthorectifying SWIR data using '%s' resampling algorithm..." % self.cfg.ortho_resampAlg) - GT_swir = GeoTransformer(lons=lons_swir, lats=lats_swir, fill_value=self.cfg.output_nodata_value, **kw_init) + GT_swir = Geometry_Transformer(lons=lons_swir, lats=lats_swir, **kw_init) swir_mapgeo_gA = GeoArray(*GT_swir.to_map_geometry(enmap_ImageL1.swir.data, **kw_trafo), nodata=self.cfg.output_nodata_value) @@ -132,9 +154,9 @@ class Orthorectifier(object): # TODO allow to set geolayer band to be used for warping of 2D arrays # always use nearest neighbour resampling for masks and bitmasks with discrete values - rsp_nearest_list = ['mask_landwater', 'mask_clouds', 'mask_cloudshadow', 'mask_haze', 'mask_snow', - 'mask_cirrus', 'polymer_bitmask'] - kw_init_nearest = dict(resamp_alg='nearest', nprocs=self.cfg.CPUs) + rsp_nearest_list = ['mask_landwater', 'mask_clouds', 'mask_cloudshadow', + 'mask_haze', 'mask_snow', 'mask_cirrus', 'polymer_bitmask'] + kw_init_nearest = dict(backend='gdal', resamp_alg='nearest', nprocs=self.cfg.CPUs) # run the orthorectification for attrName in ['mask_landwater', 'mask_clouds', 'mask_cloudshadow', 'mask_haze', 'mask_snow', 'mask_cirrus', @@ -142,14 +164,18 @@ class Orthorectifier(object): attr = getattr(enmap_ImageL1.vnir, attrName) if attr is not None: + kw_init_attr = kw_init.copy() if attrName not in rsp_nearest_list else kw_init_nearest + kw_trafo_attr = kw_trafo.copy() + kw_trafo_attr['src_nodata'] = attr.nodata + kw_trafo_attr['tgt_nodata'] = attr.nodata + GT = Geometry_Transformer( lons=lons_vnir if lons_vnir.ndim == 2 else lons_vnir[:, :, 0], lats=lats_vnir if lats_vnir.ndim == 2 else lats_vnir[:, :, 0], - fill_value=attr.nodata, - **(kw_init if attrName not in rsp_nearest_list else kw_init_nearest)) + **kw_init_attr) enmap_ImageL1.logger.info("Orthorectifying '%s' attribute..." % attrName) - attr_ortho = GeoArray(*GT.to_map_geometry(attr, **kw_trafo), nodata=attr.nodata) + attr_ortho = GeoArray(*GT.to_map_geometry(attr, **kw_trafo_attr), nodata=attr.nodata) setattr(L2_obj, attrName, attr_ortho) # TODO transform dead pixel map, quality test flags? @@ -206,9 +232,13 @@ class Orthorectifier(object): V_UL_UR_LL_LR_ll = [(V_lons[y, x], V_lats[y, x]) for y, x in [(0, 0), (0, -1), (-1, 0), (-1, -1)]] S_UL_UR_LL_LR_ll = [(S_lons[y, x], S_lats[y, x]) for y, x in [(0, 0), (0, -1), (-1, 0), (-1, -1)]] - # transform them to UTM - V_UL_UR_LL_LR_prj = [transform_any_prj(4326, tgt_epsg, x, y) for x, y in V_UL_UR_LL_LR_ll] - S_UL_UR_LL_LR_prj = [transform_any_prj(4326, tgt_epsg, x, y) for x, y in S_UL_UR_LL_LR_ll] + # transform them to tgt_epsg + if tgt_epsg != 4326: + V_UL_UR_LL_LR_prj = [transform_any_prj(4326, tgt_epsg, x, y) for x, y in V_UL_UR_LL_LR_ll] + S_UL_UR_LL_LR_prj = [transform_any_prj(4326, tgt_epsg, x, y) for x, y in S_UL_UR_LL_LR_ll] + else: + V_UL_UR_LL_LR_prj = V_UL_UR_LL_LR_ll + S_UL_UR_LL_LR_prj = S_UL_UR_LL_LR_ll # separate X and Y V_X_prj, V_Y_prj = zip(*V_UL_UR_LL_LR_prj) @@ -216,7 +246,7 @@ class Orthorectifier(object): # in case of 3D geolayers, the corner coordinates have multiple values for multiple bands # -> use the innermost coordinates to avoid pixels with VNIR-only/SWIR-only values due to keystone - # (these pixels would be set to nodata later anyways, so we don't need to increase the extent for them) + # (these pixels would be set to nodata later anyway, so we don't need to increase the extent for them) if V_lons.ndim == 3: V_X_prj = (V_X_prj[0].max(), V_X_prj[1].min(), V_X_prj[2].max(), V_X_prj[3].min()) V_Y_prj = (V_Y_prj[0].min(), V_Y_prj[1].min(), V_Y_prj[2].max(), V_Y_prj[3].max()) diff --git a/enpt/processors/spatial_optimization/spatial_optimization.py b/enpt/processors/spatial_optimization/spatial_optimization.py index b4832c59cc777a7ed257e4e0ce147adaa21b4872..fc9fc5d0029ebdd156d1cf391ab556da069c18e9 100644 --- a/enpt/processors/spatial_optimization/spatial_optimization.py +++ b/enpt/processors/spatial_optimization/spatial_optimization.py @@ -39,7 +39,7 @@ import os from typing import Optional import numpy as np -from osgeo import gdal +from osgeo import gdal # noqa from arosics import COREG_LOCAL from geoarray import GeoArray @@ -49,7 +49,7 @@ from py_tools_ds.processing.progress_mon import ProgressBar from ...options.config import EnPTConfig from ...model.images.images_sensorgeo import EnMAPL1Product_SensorGeo -from ..spatial_transform import Geometry_Transformer, Geometry_Transformer_3D +from ..spatial_transform import Geometry_Transformer class Spatial_Optimizer(object): @@ -78,15 +78,17 @@ class Spatial_Optimizer(object): % (self._EnMAP_bandIdx + 1)) GT = Geometry_Transformer(lons=self._EnMAP_Im.meta.vnir.lons[:, :, self._EnMAP_bandIdx], lats=self._EnMAP_Im.meta.vnir.lats[:, :, self._EnMAP_bandIdx], - fill_value=0, - resamp_alg='gauss', - radius_of_influence=30, + backend='gdal', + resamp_alg='bilinear', nprocs=self.cfg.CPUs) self._EnMAP_band = \ GeoArray(*GT.to_map_geometry(enmap_band_sensorgeo, tgt_prj=self._get_optimal_utm_epsg(), - tgt_coordgrid=((0, 15), (0, -15))), + tgt_coordgrid=((0, 15), (0, -15)), + src_nodata=self._EnMAP_Im.vnir.data.nodata, + tgt_nodata=0 + ), nodata=0) return self._EnMAP_band @@ -101,14 +103,17 @@ class Spatial_Optimizer(object): self._EnMAP_Im.logger.info('Temporarily transforming EnMAP water mask to map geometry for co-registration.') GT = Geometry_Transformer(lons=self._EnMAP_Im.meta.vnir.lons[:, :, self._EnMAP_bandIdx], lats=self._EnMAP_Im.meta.vnir.lats[:, :, self._EnMAP_bandIdx], - fill_value=0, + backend='gdal', resamp_alg='nearest', nprocs=self.cfg.CPUs) self._EnMAP_mask = \ GeoArray(*GT.to_map_geometry(enmap_mask_sensorgeo, tgt_prj=self._get_optimal_utm_epsg(), - tgt_coordgrid=((0, 15), (0, -15))), + tgt_coordgrid=((0, 15), (0, -15)), + src_nodata=0, # 0=background + tgt_nodata=0 + ), nodata=0) return self._EnMAP_mask @@ -305,18 +310,18 @@ class Spatial_Optimizer(object): enmap_ImageL1.logger.info('Transforming spatial optimization results back to sensor geometry.') lons_band = self._EnMAP_Im.meta.vnir.lons[:, :, self._EnMAP_bandIdx] lats_band = self._EnMAP_Im.meta.vnir.lats[:, :, self._EnMAP_bandIdx] - GT = Geometry_Transformer_3D(lons=np.repeat(lons_band[:, :, np.newaxis], 2, axis=2), - lats=np.repeat(lats_band[:, :, np.newaxis], 2, axis=2), - fill_value=0, - # resamp_alg='bilinear', - resamp_alg='gauss', - nprocs=self.cfg.CPUs) + GT = Geometry_Transformer(lons=lons_band, + lats=lats_band, + backend='gdal', + resamp_alg='bilinear', + nprocs=self.cfg.CPUs) geolayer_sensorgeo = \ GT.to_sensor_geometry(GeoArray(np.dstack([xgrid_map_coreg, ygrid_map_coreg]), geotransform=self._EnMAP_band.gt, - projection=self._EnMAP_band.prj)) + projection=self._EnMAP_band.prj), + tgt_nodata=0) enmap_ImageL1.logger.info('Applying results of spatial optimization to geolayer.') lons_coreg, lats_coreg = transform_coordArray(prj_src=self._ref_band_prep.prj, diff --git a/enpt/processors/spatial_transform/spatial_transform.py b/enpt/processors/spatial_transform/spatial_transform.py index 0109c15334494186c441ad4ee4321448cbfde895..17dcbf0581b2e446d37d92032c912075c663bd99 100644 --- a/enpt/processors/spatial_transform/spatial_transform.py +++ b/enpt/processors/spatial_transform/spatial_transform.py @@ -28,7 +28,6 @@ # with this program. If not, see <https://www.gnu.org/licenses/>. """EnPT module 'spatial transform', containing everything related to spatial transformations.""" -import warnings from typing import Union, Tuple, List, Optional, Sequence # noqa: F401 from multiprocessing import Pool, cpu_count from collections import OrderedDict @@ -40,8 +39,8 @@ from natsort import natsorted import numpy_indexed as npi from pyproj import CRS -from sensormapgeo import SensorMapGeometryTransformer, SensorMapGeometryTransformer3D -from sensormapgeo.transformer_2d import AreaDefinition +from sensormapgeo import Transformer +from sensormapgeo.pyresample_backend.transformer_2d import AreaDefinition from py_tools_ds.geo.projection import prj_equal from py_tools_ds.geo.coord_grid import find_nearest from py_tools_ds.geo.coord_trafo import transform_any_prj, transform_coordArray @@ -51,86 +50,38 @@ from ...options.config import enmap_coordinate_grid_utm __author__ = 'Daniel Scheffler' -class Geometry_Transformer(SensorMapGeometryTransformer): +class Geometry_Transformer(Transformer): # use Sentinel-2 grid (30m grid with origin at 0/0) # EnMAP geolayer contains pixel center coordinate def to_sensor_geometry(self, path_or_geoarray_mapgeo: Union[str, GeoArray], + src_gt: Tuple[float, float, float, float, float, float] = None, src_prj: Union[str, int] = None, - src_extent: Tuple[float, float, float, float] = None): + src_nodata: int = None, + tgt_nodata: int = None + ) -> np.ndarray: data_mapgeo = GeoArray(path_or_geoarray_mapgeo) if not data_mapgeo.is_map_geo: raise RuntimeError('The dataset to be transformed into sensor geometry already represents sensor geometry.') - with warnings.catch_warnings(): - # FIXME remove that after removing pyresample pinning - warnings.filterwarnings('ignore', category=DeprecationWarning, - message='.*is a deprecated alias for the builtin.*') - - return super().to_sensor_geometry( - data_mapgeo[:], - src_prj=src_prj or data_mapgeo.prj, - src_extent=src_extent or list(np.array(data_mapgeo.box.boundsMap)[[0, 2, 1, 3]])) - - def to_map_geometry(self, - path_or_geoarray_sensorgeo: Union[str, GeoArray, np.ndarray], - tgt_prj: Union[str, int] = None, - tgt_extent: Tuple[float, float, float, float] = None, - tgt_res: Tuple[float, float] = None, - tgt_coordgrid: Tuple[Tuple, Tuple] = None, - area_definition: AreaDefinition = None): - data_sensorgeo = GeoArray(path_or_geoarray_sensorgeo) - - if data_sensorgeo.is_map_geo and not data_sensorgeo.is_rotated: - raise RuntimeError('The dataset to be transformed into map geometry already represents map geometry.') - - # run transformation (output extent/area definition etc. is internally computed from the geolayers if not given) - with warnings.catch_warnings(): - # FIXME remove that after removing pyresample pinning - warnings.filterwarnings('ignore', category=DeprecationWarning, - message='.*is a deprecated alias for the builtin.*') - - out_data, out_gt, out_prj = \ - super(Geometry_Transformer, self).to_map_geometry(data_sensorgeo[:], - tgt_prj=tgt_prj, - tgt_extent=tgt_extent, - tgt_res=tgt_res, - tgt_coordgrid=tgt_coordgrid, - area_definition=self.area_definition) - - return out_data, out_gt, out_prj - - -class Geometry_Transformer_3D(SensorMapGeometryTransformer3D): - # use Sentinel-2 grid (30m grid with origin at 0/0) - # EnMAP geolayer contains pixel center coordinate - def to_sensor_geometry(self, - path_or_geoarray_mapgeo: Union[str, GeoArray], - src_prj: Union[str, int] = None, - src_extent: Tuple[float, float, float, float] = None): - data_mapgeo = GeoArray(path_or_geoarray_mapgeo) - - if not data_mapgeo.is_map_geo: - raise RuntimeError('The dataset to be transformed into sensor geometry already represents sensor geometry.') - - with warnings.catch_warnings(): - # FIXME remove that after removing pyresample pinning - warnings.filterwarnings('ignore', category=DeprecationWarning, - message='.*is a deprecated alias for the builtin.*') - - return super().to_sensor_geometry( - data_mapgeo[:], - src_prj=src_prj or data_mapgeo.prj, - src_extent=src_extent or list(np.array(data_mapgeo.box.boundsMap)[[0, 2, 1, 3]])) + return super().to_sensor_geometry( + data_mapgeo[:], + src_gt=src_gt or data_mapgeo.gt, + src_prj=src_prj or data_mapgeo.prj, + src_nodata=src_nodata, + tgt_nodata=tgt_nodata + ) def to_map_geometry(self, path_or_geoarray_sensorgeo: Union[str, GeoArray, np.ndarray], - tgt_prj: Union[str, int] = None, + tgt_prj: Union[str, int], tgt_extent: Tuple[float, float, float, float] = None, - tgt_res: Tuple[float, float] = None, + tgt_res: Tuple = None, tgt_coordgrid: Tuple[Tuple, Tuple] = None, + src_nodata: int = None, + tgt_nodata: int = None, area_definition: AreaDefinition = None ) -> Tuple[np.ndarray, tuple, str]: data_sensorgeo = GeoArray(path_or_geoarray_sensorgeo) @@ -139,18 +90,17 @@ class Geometry_Transformer_3D(SensorMapGeometryTransformer3D): raise RuntimeError('The dataset to be transformed into map geometry already represents map geometry.') # run transformation (output extent/area definition etc. is internally computed from the geolayers if not given) - with warnings.catch_warnings(): - # FIXME remove that after removing pyresample pinning - warnings.filterwarnings('ignore', category=DeprecationWarning, - message='.*is a deprecated alias for the builtin.*') - - out_data, out_gt, out_prj = \ - super(Geometry_Transformer_3D, self).to_map_geometry(data_sensorgeo[:], - tgt_prj=tgt_prj, - tgt_extent=tgt_extent, - tgt_res=tgt_res, - tgt_coordgrid=tgt_coordgrid, - area_definition=area_definition) + out_data, out_gt, out_prj = \ + super().to_map_geometry( + data_sensorgeo[:], + tgt_prj=tgt_prj, + tgt_extent=tgt_extent, + tgt_res=tgt_res, + tgt_coordgrid=tgt_coordgrid, + src_nodata=src_nodata, + tgt_nodata=tgt_nodata, + area_definition=area_definition + ) return out_data, out_gt, out_prj @@ -168,7 +118,9 @@ class VNIR_SWIR_SensorGeometryTransformer(object): res_vnir: Tuple[float, float], res_swir: Tuple[float, float], resamp_alg: str = 'nearest', - **gt_opts) -> None: + src_nodata: int = None, + tgt_nodata: int = None, + nprocs: int = cpu_count()) -> None: """Get an instance of VNIR_SWIR_SensorGeometryTransformer. :param lons_vnir: VNIR longitude array corresponding to the sensor geometry arrays passed later (2D or 3D) @@ -179,9 +131,11 @@ class VNIR_SWIR_SensorGeometryTransformer(object): :param prj_swir: projection of the SWIR if it would be transformed to map geometry (WKT string or EPSG code) :param res_vnir: pixel dimensions of the VNIR if it would be transformed to map geometry (X, Y) :param res_swir: pixel dimensions of the SWIR if it would be transformed to map geometry (X, Y) - :param resamp_alg: resampling algorithm ('nearest', 'bilinear', 'gauss', 'custom') - :param gt_opts: additional options to be passed to the Geometric_Transformer class, - e.g., 'fill_value', 'radius_of_influence', 'nprocs'... + :param resamp_alg: resampling algorithm ('nearest', 'near', 'bilinear', 'cubic', 'cubic_spline', + 'lanczos', 'average', 'mode', 'max', 'min', 'med', 'q1', 'q3') + :param src_nodata: nodata value of the input array + :param tgt_nodata: pixel value to be used for undetermined pixels in the output + :param nprocs: number of CPU cores to use """ self.vnir_meta = dict( lons=lons_vnir, @@ -196,7 +150,9 @@ class VNIR_SWIR_SensorGeometryTransformer(object): res=res_swir ) self.resamp_alg = resamp_alg - self.gt_opts = gt_opts + self.src_nodata = src_nodata + self.tgt_nodata = tgt_nodata + self.cpus = nprocs def transform_sensorgeo_VNIR_to_SWIR(self, data_vnirsensorgeo: np.ndarray) -> np.ndarray: """Transform any array in VNIR sensor geometry to SWIR sensor geometry to remove geometric shifts. @@ -216,12 +172,15 @@ class VNIR_SWIR_SensorGeometryTransformer(object): def _transform_sensorgeo(self, data2transform: np.ndarray, - inputgeo: str) -> np.ndarray: + inputgeo: str, + band_outputgeo: int = 0 + ) -> np.ndarray: """Transform the input array between VNIR and SWIR sensor geometry. :param data2transform: input array to be transformed :param inputgeo: 'vnir' if data2transform is given in VNIR sensor geometry 'swir' if data2transform is given in SWIR sensor geometry + :param band_outputgeo: band index of the band from the sensor geometry should be used to generate the output """ # TODO: Avoid the resampling here, maybe by replacing the lon/lat arrays by image coordinates for the source # geometry and by image coordinate differences for the target geometry. Maybe also the proj string for @@ -232,35 +191,40 @@ class VNIR_SWIR_SensorGeometryTransformer(object): src, tgt = (self.vnir_meta, self.swir_meta) if inputgeo == 'vnir' else (self.swir_meta, self.vnir_meta) - # get 2D or 3D GeoTransformer - if data2transform.ndim == 3 and src['lons'].ndim == 3 and src['lons'].shape[2] == data2transform.shape[2]: - GT_src = Geometry_Transformer_3D(lons=src['lons'], - lats=src['lats'], - resamp_alg=self.resamp_alg, - **self.gt_opts) - GT_tgt = Geometry_Transformer_3D(lons=tgt['lons'], - lats=tgt['lats'], - resamp_alg=self.resamp_alg, - **self.gt_opts) + # get GeoTransformer + # NOTE: We can only use a 3D geolayer to temporarily transform to map geometry but not back to sensor geometry + # of the other detector. For this, we need to select the band of which the sensor geometry should be used. + def ensure_2d(coord_array): + return coord_array if coord_array.ndim == 2 else coord_array[:, :, band_outputgeo] + if data2transform.ndim == 3 and src['lons'].ndim == 3 and src['lons'].shape[2] == data2transform.shape[2]: + src_lons, src_lats = src['lons'], src['lats'] + tgt_lons, tgt_lats = ensure_2d(tgt['lons']), ensure_2d(tgt['lats']) else: - def ensure_2D(coord_array): - return coord_array if coord_array.ndim == 2 else coord_array[:, :, 0] - - GT_src = Geometry_Transformer(lons=ensure_2D(src['lons']), - lats=ensure_2D(src['lats']), - resamp_alg=self.resamp_alg, - **self.gt_opts) - GT_tgt = Geometry_Transformer(lons=ensure_2D(tgt['lons']), - lats=ensure_2D(tgt['lats']), - resamp_alg=self.resamp_alg, - **self.gt_opts) + src_lons, src_lats = ensure_2d(src['lons']), ensure_2d(src['lats']) + tgt_lons, tgt_lats = ensure_2d(tgt['lons']), ensure_2d(tgt['lats']) + + GT_src = Geometry_Transformer(src_lons, src_lats, backend='gdal', resamp_alg=self.resamp_alg, nprocs=self.cpus) + GT_tgt = Geometry_Transformer(tgt_lons, tgt_lats, backend='gdal', resamp_alg=self.resamp_alg, nprocs=self.cpus) # temporarily transform the input sensor geometry array to map geometry - gA_mapgeo = GeoArray(*GT_src.to_map_geometry(data2transform, tgt_prj=src['prj'])) + gA_mapgeo = GeoArray( + *GT_src.to_map_geometry( + data2transform, + tgt_prj=src['prj'], + src_nodata=self.src_nodata, + tgt_nodata=self.tgt_nodata + ), + nodata=self.tgt_nodata + ) - # generate the target sensor geometry array (target lons/lats define the target swath definition) - tgt_data_sensorgeo = GT_tgt.to_sensor_geometry(gA_mapgeo) + # generate the target sensor geometry array (target lons/lats define the target swath definition + tgt_data_sensorgeo =\ + GT_tgt.to_sensor_geometry( + gA_mapgeo, + src_nodata=gA_mapgeo._nodata, # noqa + tgt_nodata=self.tgt_nodata + ) return tgt_data_sensorgeo @@ -686,7 +650,7 @@ def compute_mapCoords_within_sensorGeoDims(sensorgeoCoords_YX: List[Tuple[float, enmapIm_cornerCoords: Tuple[Tuple[float, float]], enmapIm_dims_sensorgeo: Tuple[int, int], ) -> List[Tuple[float, float]]: - """Compute map coordinates for a given image coordinate pair of an EnMAP image in sensor geometry. + """Compute map coordinates for a given image coordinate-pair of an EnMAP image in sensor geometry. :param sensorgeoCoords_YX: list of requested sensor geometry positions [(row, column), (row, column), ...] :param rpc_coeffs: RPC coefficients describing the relation between sensor and map geometry diff --git a/requirements.txt b/requirements.txt index de0b5bf965b00e7335d50b61cd064b9f66447410..6174902902616447860837294c197743c7b19a52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ pyproj>=3.4.0 py_tools_ds>=0.14.25 scikit-image scipy -sensormapgeo>=0.4.0 +sensormapgeo>=1.0.0 sicor>=0.19.1 tqdm utm diff --git a/setup.py b/setup.py index 00904bff3e96111b2611dea89fe727855e6ef34b..ce02015556f49eb4c09fe865645d88881cace97e 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ req = [ 'py_tools_ds>=0.14.25', 'scikit-image', 'scipy', - 'sensormapgeo>=0.4.0', + 'sensormapgeo>=1.0.0', 'sicor>=0.19.1', 'tqdm', 'utm', diff --git a/tests/gitlab_CI_docker/context/environment_enpt.yml b/tests/gitlab_CI_docker/context/environment_enpt.yml index 186183d438cdd18fc644b33bf036611369438347..05c88c7b7ae078fbf5dfe375d3e91d9224e121a5 100644 --- a/tests/gitlab_CI_docker/context/environment_enpt.yml +++ b/tests/gitlab_CI_docker/context/environment_enpt.yml @@ -24,7 +24,7 @@ dependencies: - py-tools-ds>=0.14.25 - scikit-image - scipy - - sensormapgeo>=0.4.0 + - sensormapgeo>=1.0.0 - sicor>=0.19.1 - tqdm - utm