def geocode(scene, dem, tempdir, outdir, targetres, scaling='linear', func_geoback=1, func_interp=2, nodata=(0, -99), sarSimCC=False, osvdir=None, allow_RES_OSV=False, cleanup=True, normalization_method=2, export_extra=None): """ general function for geocoding SAR images with GAMMA Parameters ---------- scene: str or ~pyroSAR.drivers.ID the SAR scene to be processed dem: str the reference DEM in GAMMA format tempdir: str a temporary directory for writing intermediate files outdir: str the directory for the final GeoTiff output files targetres: int the target resolution in meters scaling: {'linear', 'db'} or list the value scaling of the backscatter values; either 'linear', 'db' or a list of both, i.e. ['linear', 'db'] func_geoback: {0, 1, 2, 3, 4, 5, 6, 7} backward geocoding interpolation mode (see GAMMA command geocode_back) - 0: nearest-neighbor - 1: bicubic spline (default) - 2: bicubic-spline, interpolate log(data) - 3: bicubic-spline, interpolate sqrt(data) - 4: B-spline interpolation (default B-spline degree: 5) - 5: B-spline interpolation sqrt(x) (default B-spline degree: 5) - 6: Lanczos interpolation (default Lanczos function order: 5) - 7: Lanczos interpolation sqrt(x) (default Lanczos function order: 5) NOTE: log and sqrt interpolation modes should only be used with non-negative data! NOTE: Gamma reccomendation for MLI data: "The interpolation should be performed on the square root of the data. A mid-order (3 to 5) B-spline interpolation is recommended." func_interp: {0, 1, 2, 3} output lookup table values in regions of layover, shadow, or DEM gaps (see GAMMA command gc_map) - 0: set to (0., 0.) - 1: linear interpolation across these regions - 2: actual value - 3: nn-thinned nodata: tuple the nodata values for the output files; defined as a tuple with two values, the first for linear, the second for logarithmic scaling sarSimCC: bool perform geocoding with SAR simulation cross correlation? If False, geocoding is performed with the Range-Doppler approach using orbit state vectors osvdir: str a directory for Orbit State Vector files; this is currently only used by for Sentinel-1 where two subdirectories POEORB and RESORB are created; if set to None, a subdirectory OSV is created in the directory of the unpacked scene. allow_RES_OSV: bool also allow the less accurate RES orbit files to be used? Otherwise the function will raise an error if no POE file exists cleanup: bool should all files written to the temporary directory during function execution be deleted after processing? normalization_method: {1, 2} the topographic normalization approach to be used - 1: first geocoding, then terrain flattening - 2: first terrain flattening, then geocoding; see `Small 2011 <https://doi.org/10.1109/Tgrs.2011.2120616>`_ export_extra: list or None a list of image file IDs to be exported to outdir - format is GeoTiff if the file is geocoded and ENVI otherwise. Non-geocoded images can be converted via Gamma command data2tiff yet the output was found impossible to read with GIS software - scaling of SAR image products is applied as defined by parameter `scaling` - see Notes for ID options Returns ------- Note ---- | intermediate output files | DEM products are named <scene identifier>_<ID>, e.g. `S1A__IW___A_20141012T162337_inc_geo` | SAR products will additionally contain the polarization, e.g. `S1A__IW___A_20141012T162337_VV_grd_mli` | IDs in brackets are only written if selected by `export_extra` - images in range-Doppler geometry * **grd**: the ground range detected SAR intensity image * **grd_mli**: the multi-looked grd image with approached target resolution * specific to normalization method 2: + **pix_ellip_sigma0**: ellipsoid-based pixel area + **pix_area_sigma0**: actual illuminated area as obtained from integrating DEM-facets (command pixel_area) + **pix_fine**: refined pixel area normalization factor (pix_ellip_sigma0 / pix_area_sigma0) + **grd_mli_pan**: the pixel area normalized MLI (grd_mli * pix_fine) - images in map geometry * **dem_seg_geo**: dem subsetted to the extent of the intersect between input DEM and SAR image * (**u_geo**): zenith angle of surface normal vector n (angle between z and n) * (**v_geo**): orientation angle of n (between x and projection of n in xy plane) * **inc_geo**: local incidence angle (between surface normal and look vector) * (**psi_geo**): projection angle (between surface normal and image plane normal) * **pix_geo**: pixel area normalization factor (command gc_map) * **ls_map_geo**: layover and shadow map (in map projection) * (**sim_sar_geo**): simulated SAR backscatter image - additional files * **lut_init**: initial geocoding lookup table - files specific to SAR simulation cross-correlation geocoding * **lut_fine**: refined geocoding lookup table * **diffpar**: ISP offset/interferogram parameter file * **offs**: offset estimates (fcomplex) * **coffs**: culled range and azimuth offset estimates (fcomplex) * **coffsets**: culled offset estimates and cross correlation values (text format) * **ccp**: cross-correlation of each patch (0.0->1.0) (float) Examples -------- geocode a Sentinel-1 scene and export the local incidence angle map with it >>> from pyroSAR.gamma import geocode >>> filename = 'S1A_IW_GRDH_1SDV_20180829T170656_20180829T170721_023464_028DE0_F7BD.zip' >>> geocode(scene=filename, dem='demfile', outdir='outdir', targetres=20, scaling='db', >>> export_extra=['dem_seg_geo', 'inc_geo', 'ls_map_geo']) .. figure:: figures/gamma_geocode.png :scale: 25% :align: center Workflow diagram for function geocode using normalization method 2 for processing a Sentinel-1 Ground Range Detected (GRD) scene to radiometrically terrain corrected (RTC) backscatter. """ if normalization_method == 2 and func_interp != 2: raise RuntimeError( 'parameter func_interp must be set to 2 if normalization_method is set to 2; ' 'see documentation of Gamma command pixel_area') if isinstance(scene, ID): scene = identify(scene.scene) elif isinstance(scene, str): scene = identify(scene) else: raise RuntimeError("'scene' must be of type str or pyroSAR.ID") if scene.sensor not in ['S1A', 'S1B']: raise IOError( 'this method is currently only available for Sentinel-1. Please stay tuned...' ) if sarSimCC: raise IOError( 'geocoding with cross correlation offset refinement is still in the making. Please stay tuned...' ) if export_extra is not None and not isinstance(export_extra, list): raise TypeError( "parameter 'export_extra' must either be None or a list") for dir in [tempdir, outdir]: if not os.path.isdir(dir): os.makedirs(dir) if scene.is_processed(outdir): print('scene {} already processed'.format(scene.outname_base())) return scaling = [scaling] if isinstance( scaling, str) else scaling if isinstance(scaling, list) else [] scaling = union(scaling, ['db', 'linear']) if len(scaling) == 0: raise IOError('wrong input type for parameter scaling') if scene.compression is not None: print('unpacking scene..') try: scene.unpack(tempdir) except RuntimeError: print('scene was attempted to be processed before, exiting') return else: scene.scene = os.path.join(tempdir, os.path.basename(scene.file)) os.makedirs(scene.scene) shellscript = os.path.join(scene.scene, scene.outname_base() + '_commands.sh') path_log = os.path.join(scene.scene, 'logfiles') if not os.path.isdir(path_log): os.makedirs(path_log) if scene.sensor in ['S1A', 'S1B']: print('removing border noise..') scene.removeGRDBorderNoise() print('converting scene to GAMMA format..') convert2gamma(scene, scene.scene, logpath=path_log, outdir=scene.scene, shellscript=shellscript) if scene.sensor in ['S1A', 'S1B']: print('updating orbit state vectors..') if allow_RES_OSV: osvtype = ['POE', 'RES'] else: osvtype = 'POE' try: correctOSV(id=scene, osvdir=osvdir, osvType=osvtype, logpath=path_log, outdir=scene.scene, shellscript=shellscript) except RuntimeError: print('orbit state vector correction failed for scene {}'.format( scene.scene)) return calibrate(scene, scene.scene, logpath=path_log, outdir=scene.scene, shellscript=shellscript) images = [ x for x in scene.getGammaImages(scene.scene) if x.endswith('_grd') or x.endswith('_slc_cal') ] products = list(images) print('multilooking..') for image in images: multilook(infile=image, outfile=image + '_mli', targetres=targetres, logpath=path_log, outdir=scene.scene, shellscript=shellscript) images = [x + '_mli' for x in images] products.extend(images) master = images[0] # create output names for files to be written # appreciated files will be written # depreciated files will be set to '-' in the GAMMA function call and are thus not written n = Namespace(scene.scene, scene.outname_base()) n.appreciate( ['dem_seg_geo', 'lut_init', 'pix_geo', 'inc_geo', 'ls_map_geo']) n.depreciate(['sim_sar_geo', 'u_geo', 'v_geo', 'psi_geo']) # if sarSimCC: # n.appreciate(['ccp', 'lut_fine']) if export_extra is not None: n.appreciate(export_extra) ovs_lat, ovs_lon = ovs(dem + '.par', targetres) master_par = ISPPar(master + '.par') gc_map_args = { 'DEM_par': dem + '.par', 'DEM': dem, 'DEM_seg_par': n.dem_seg_geo + '.par', 'DEM_seg': n.dem_seg_geo, 'lookup_table': n.lut_init, 'lat_ovr': ovs_lat, 'lon_ovr': ovs_lon, 'sim_sar': n.sim_sar_geo, 'u': n.u_geo, 'v': n.v_geo, 'inc': n.inc_geo, 'psi': n.psi_geo, 'pix': n.pix_geo, 'ls_map': n.ls_map_geo, 'frame': 8, 'ls_mode': func_interp, 'logpath': path_log, 'shellscript': shellscript, 'outdir': scene.scene } print('creating DEM products..') if master_par.image_geometry == 'GROUND_RANGE': gc_map_args.update({'GRD_par': master + '.par'}) diff.gc_map_grd(**gc_map_args) else: gc_map_args.update({'MLI_par': master + '.par', 'OFF_par': '-'}) diff.gc_map(**gc_map_args) for item in [ 'dem_seg_geo', 'sim_sar_geo', 'u_geo', 'v_geo', 'psi_geo', 'pix_geo', 'inc_geo', 'ls_map_geo' ]: if n.isappreciated(item): mods = {'data_type': 1} if item == 'ls_map_geo' else None par2hdr(n.dem_seg_geo + '.par', n.get(item) + '.hdr', mods) sim_width = ISPPar(n.dem_seg_geo + '.par').width if sarSimCC: raise IOError( 'geocoding with cross correlation offset refinement is still in the making. Please stay tuned...' ) else: lut_final = n.lut_init ###################################################################### # normalization and backward geocoding approach 1 #################### ###################################################################### print('geocoding and normalization..') if normalization_method == 1: method_suffix = 'geo_norm' for image in images: diff.geocode_back(data_in=image, width_in=master_par.range_samples, lookup_table=lut_final, data_out=image + '_geo', width_out=sim_width, interp_mode=func_geoback, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(n.dem_seg_geo + '.par', image + '_geo.hdr') lat.product(data_1=image + '_geo', data_2=n.pix_geo, product=image + '_geo_pan', width=sim_width, bx=1, by=1, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(n.dem_seg_geo + '.par', image + '_geo_pan.hdr') lat.sigma2gamma(pwr1=image + '_geo_pan', inc=n.inc_geo, gamma=image + '_{}'.format(method_suffix), width=sim_width, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(n.dem_seg_geo + '.par', image + '_{}.hdr'.format(method_suffix)) products.extend([image + '_geo', image + '_geo_pan']) ###################################################################### # normalization and backward geocoding approach 2 #################### ###################################################################### elif normalization_method == 2: method_suffix = 'norm_geo' # newer versions of Gamma enable creating the ratio of ellipsoid based # pixel area and DEM-facet pixel area directly with command pixel_area if hasarg(diff.pixel_area, 'sigma0_ratio'): n.appreciate(['pix_fine']) n.depreciate(['pix_area_sigma0']) diff.pixel_area(MLI_par=master + '.par', DEM_par=n.dem_seg_geo + '.par', DEM=n.dem_seg_geo, lookup_table=lut_final, ls_map=n.ls_map_geo, inc_map=n.inc_geo, pix_sigma0=n.pix_area_sigma0, sigma0_ratio=n.pix_fine, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(master + '.par', n.pix_fine + '.hdr') else: n.appreciate(['pix_area_sigma0', 'pix_ellip_sigma0', 'pix_fine']) # actual illuminated area as obtained from integrating DEM-facets (pix_area_sigma0 | pix_area_gamma0) diff.pixel_area(MLI_par=master + '.par', DEM_par=n.dem_seg_geo + '.par', DEM=n.dem_seg_geo, lookup_table=lut_final, ls_map=n.ls_map_geo, inc_map=n.inc_geo, pix_sigma0=n.pix_area_sigma0, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(master + '.par', n.pix_area_sigma0 + '.hdr') # ellipsoid-based pixel area (ellip_pix_sigma0) isp.radcal_MLI( MLI=master, MLI_par=master + '.par', OFF_par='-', CMLI=master + '_cal', refarea_flag= 1, # calculate sigma0, scale area by sin(inc_ang)/sin(ref_inc_ang) pix_area=n.pix_ellip_sigma0, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(master + '.par', n.pix_ellip_sigma0 + '.hdr') par2hdr(master + '.par', master + '_cal.hdr') # ratio of ellipsoid based pixel area and DEM-facet pixel area lat.ratio(d1=n.pix_ellip_sigma0, d2=n.pix_area_sigma0, ratio=n.pix_fine, width=master_par.range_samples, bx=1, by=1, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(master + '.par', n.pix_fine + '.hdr') for image in images: # sigma0 = MLI * ellip_pix_sigma0 / pix_area_sigma0 # gamma0 = MLI * ellip_pix_sigma0 / pix_area_gamma0 lat.product(data_1=image, data_2=n.pix_fine, product=image + '_pan', width=master_par.range_samples, bx=1, by=1, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(master + '.par', image + '_pan.hdr') diff.geocode_back(data_in=image + '_pan', width_in=master_par.range_samples, lookup_table=lut_final, data_out=image + '_pan_geo', width_out=sim_width, interp_mode=func_geoback, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(n.dem_seg_geo + '.par', image + '_pan_geo.hdr') lat.sigma2gamma(pwr1=image + '_pan_geo', inc=n.inc_geo, gamma=image + '_{}'.format(method_suffix), width=sim_width, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(n.dem_seg_geo + '.par', image + '_{}.hdr'.format(method_suffix)) products.extend([image + '_pan', image + '_pan_geo']) else: raise RuntimeError('unknown option for normalization_method') ###################################################################### print('conversion to (dB and) geotiff..') def exporter(data_in, outdir, nodata, scale='linear', dtype=2): if scale == 'db': if re.search('_geo', os.path.basename(data_in)): width = sim_width refpar = n.dem_seg_geo + '.par' else: width = master_par.range_samples refpar = master + '.par' lat.linear_to_dB(data_in=data_in, data_out=data_in + '_db', width=width, inverse_flag=0, null_value=nodata, logpath=path_log, outdir=scene.scene, shellscript=shellscript) par2hdr(refpar, data_in + '_db.hdr') data_in += '_db' if re.search('_geo', os.path.basename(data_in)): outfile = os.path.join(outdir, os.path.basename(data_in) + '.tif') disp.data2geotiff(DEM_par=n.dem_seg_geo + '.par', data=data_in, type=dtype, GeoTIFF=outfile, nodata=nodata, logpath=path_log, outdir=scene.scene, shellscript=shellscript) else: outfile = os.path.join(outdir, os.path.basename(data_in)) shutil.copyfile(data_in, outfile) shutil.copyfile(data_in + '.hdr', outfile + '.hdr') for image in images: for scale in scaling: exporter(data_in=image + '_{}'.format(method_suffix), scale=scale, dtype=2, nodata=dict(zip(('linear', 'db'), nodata))[scale], outdir=outdir) if scene.sensor in ['S1A', 'S1B']: shutil.copyfile( os.path.join(scene.scene, 'manifest.safe'), os.path.join(outdir, scene.outname_base() + '_manifest.safe')) if export_extra is not None: print('exporting extra products..') for key in export_extra: # SAR image products product_match = [x for x in products if x.endswith(key)] if len(product_match) > 0: for product in product_match: for scale in scaling: exporter(data_in=product, outdir=outdir, scale=scale, dtype=2, nodata=dict(zip(('linear', 'db'), nodata))[scale]) # ancillary (DEM) products elif n.isfile(key) and key not in ['lut_init']: filename = n[key] dtype = 5 if key == 'ls_map_geo' else 2 nodata = 0 exporter(filename, outdir, dtype=dtype, nodata=nodata) else: print('cannot not export file {}'.format(key)) shutil.copyfile(shellscript, os.path.join(outdir, os.path.basename(shellscript))) if cleanup: print('cleaning up temporary files..') shutil.rmtree(scene.scene)
def dem_autocreate(geometry, demType, outfile, buffer=0.01, t_srs=4326, tr=None, logpath=None, username=None, password=None, geoid_mode='gamma'): """ | automatically create a DEM in Gamma format for a defined spatial geometry | the following steps will be performed: - collect all tiles overlapping with the geometry * if they don't yet exist locally they will automatically be downloaded * the tiles will be downloaded into the SNAP auxdata directory structure, e.g. ``$HOME/.snap/auxdata/dem/SRTM 3Sec`` - create a mosaic GeoTiff of the same spatial extent as the input geometry plus a defined buffer using ``gdalwarp`` - subtract the EGM96-WGS84 Geoid-Ellipsoid difference and convert the result to Gamma format using Gamma command ``srtm2dem`` * this correction is not done for TanDEM-X data, which contains ellipsoid heights; see `here <https://geoservice.dlr.de/web/dataguide/tdm90>`_ - if the command ``create_dem_par`` accepts a parameter EPSG and the command ``dem_import`` exists, an arbitrary CRS can be defined via parameter ``t_srs``. In this case and if parameter ``t_srs`` is not kept at its default of 4326, conversion to Gamma format is done with command ``dem_import`` instead of ``srtm2dem`` Parameters ---------- geometry: spatialist.vector.Vector a vector geometry delimiting the output DEM size; CRS must be WGS84 LatLon (EPSG 4326) demType: str the type of DEM to be used; see :func:`~pyroSAR.auxdata.dem_autoload` for options outfile: str the name of the final DEM file buffer: float a buffer in degrees to create around the geometry t_srs: int, str or osr.SpatialReference A target geographic reference system in WKT, EPSG, PROJ4 or OPENGIS format. See function :func:`spatialist.auxil.crsConvert()` for details. Default: `4326 <http://spatialreference.org/ref/epsg/4326/>`_. tr: tuple or None the target resolution as (xres, yres) in units of ``t_srs``; if ``t_srs`` is kept at its default value of 4326, ``tr`` does not need to be defined and the original resolution is preserved; in all other cases the default of None is rejected logpath: str a directory to write Gamma logfiles to username: str or None (optional) the user name for services requiring registration; see :func:`~pyroSAR.auxdata.dem_autoload` password: str or None (optional) the password for the registration account geoid_mode: str the software to be used for converting geoid to ellipsoid heights; options: - 'gamma' - 'gdal' Returns ------- """ epsg = crsConvert(t_srs, 'epsg') if t_srs != 4326 else t_srs if epsg != 4326: if not hasarg(diff.create_dem_par, 'EPSG'): raise RuntimeError('using a different CRS than 4326 is currently not supported for this version of Gamma') if 'dem_import' not in dir(diff): raise RuntimeError('using a different CRS than 4326 currently requires command dem_import, ' 'which is not part of this version of Gamma') if tr is None: raise RuntimeError('tr needs to be defined if t_srs is not 4326') if os.path.isfile(outfile): print('outfile already exists') return tmpdir = outfile + '__tmp' os.makedirs(tmpdir) try: if logpath is not None and not os.path.isdir(logpath): os.makedirs(logpath) vrt = os.path.join(tmpdir, 'dem.vrt') dem = os.path.join(tmpdir, 'dem.tif') print('collecting DEM tiles') vrt = dem_autoload([geometry], demType, vrt=vrt, username=username, password=password, buffer=buffer) # The heights of the TanDEM-X DEM products are ellipsoidal heights, all others are EGM96 Geoid heights # Gamma works only with Ellipsoid heights and the offset needs to be corrected # starting from GDAL 2.2 the conversion can be done directly in GDAL; see docs of gdalwarp gflg = 0 gdal_geoid = False message = 'conversion to Gamma format' if demType != 'TDX90m': message = 'geoid correction and conversion to Gamma format' if geoid_mode == 'gdal': gdal_geoid = True elif geoid_mode == 'gamma': gflg = 2 else: raise RuntimeError("'geoid_mode' not supported") dem_create(vrt, dem, t_srs=epsg, tr=tr, geoid_convert=gdal_geoid) outfile_tmp = os.path.join(tmpdir, os.path.basename(outfile)) print(message) if epsg == 4326: # old approach for backwards compatibility diff.srtm2dem(SRTM_DEM=dem, DEM=outfile_tmp, DEM_par=outfile_tmp + '.par', gflg=gflg, geoid='-', logpath=logpath, outdir=tmpdir) else: # new approach enabling an arbitrary target CRS diff.create_dem_par(DEM_par=outfile_tmp + '.par', inlist=[''] * 9, EPSG=epsg) dem_import_pars = {'input_DEM': dem, 'DEM': outfile_tmp, 'DEM_par': outfile_tmp + '.par'} if gflg == 2: home = ExamineGamma().home egm96 = os.path.join(home, 'DIFF', 'scripts', 'egm96.dem') dem_import_pars['geoid'] = egm96 dem_import_pars['geoid_par'] = egm96 + '_par' diff.dem_import(**dem_import_pars) par2hdr(outfile_tmp + '.par', outfile_tmp + '.hdr', nodata=0) for suffix in ['', '.par', '.hdr']: shutil.copyfile(outfile_tmp + suffix, outfile + suffix) except RuntimeError as e: raise e finally: shutil.rmtree(tmpdir)