def test_Vector(testdata): scene = Raster(testdata['tif']) bbox1 = scene.bbox() assert bbox1.getArea() == 23262400.0 assert bbox1.extent == { 'ymax': 4830114.70107, 'ymin': 4825774.70107, 'xmin': 620048.241204, 'xmax': 625408.241204 } assert bbox1.nlayers == 1 assert bbox1.getProjection('epsg') == 32631 assert bbox1.proj4 == '+proj=utm +zone=31 +datum=WGS84 +units=m +no_defs' assert isinstance(bbox1.getFeatureByIndex(0), ogr.Feature) with pytest.raises(IndexError): bbox1.getFeatureByIndex(1) bbox1.reproject(4326) assert bbox1.proj4 == '+proj=longlat +datum=WGS84 +no_defs' assert isinstance(bbox1['fid=0'], Vector) with pytest.raises(RuntimeError): test = bbox1[0.1] assert bbox1.fieldnames == ['area'] assert bbox1.getUniqueAttributes('area') == [23262400.0] feat = bbox1.getFeatureByAttribute('area', 23262400.0) assert isinstance(feat, ogr.Feature) bbox2 = feature2vector(feat, ref=bbox1) bbox2.close() feat.Destroy() with pytest.raises(KeyError): select = bbox1.getFeatureByAttribute('foo', 'bar') with pytest.raises(RuntimeError): vec = Vector(driver='foobar') bbox1.close()
def test_png(tmpdir, testdata): outname = os.path.join(str(tmpdir), 'test') with Raster(testdata['tif']) as ras: png(src=ras, dst=outname, percent=100, scale=(2, 98), worldfile=True) assert os.path.isfile(outname + '.png') with pytest.raises(TypeError): png(src=testdata['tif'], dst=outname, percent=100, scale=(2, 98), worldfile=True) src = [testdata['tif'], testdata['tif2']] with pytest.raises(ValueError): with Raster(src) as ras: png(src=ras, dst=outname, percent=100, scale=(2, 98), worldfile=True) src.append(testdata['tif3']) outname = os.path.join(str(tmpdir), 'test_rgb.png') with Raster(src) as ras: png(src=ras, dst=outname, percent=100, scale=(2, 98), worldfile=True)
def test_dissolve(tmpdir, travis, testdata): scene = Raster(testdata['tif']) bbox1 = scene.bbox() # retrieve extent and shift its coordinates by one unit ext = bbox1.extent for key in ext.keys(): ext[key] += 1 # create new bbox shapefile with modified extent bbox2_name = os.path.join(str(tmpdir), 'bbox2.shp') bbox(ext, bbox1.srs, bbox2_name) # assert intersection between the two bboxes and combine them into one with Vector(bbox2_name) as bbox2: assert intersect(bbox1, bbox2) is not None bbox1.addvector(bbox2) # write combined bbox into new shapefile bbox3_name = os.path.join(str(tmpdir), 'bbox3.shp') bbox1.write(bbox3_name) bbox1.close() if not travis and platform.system() != 'Windows': # dissolve the geometries in bbox3 and write the result to new bbox4 # this test is currently disabled for Travis as the current sqlite3 version on Travis seems to not support # loading gdal as extension; Travis CI setup: Ubuntu 14.04 (Trusty), sqlite3 version 3.8.2 (2018-06-04) bbox4_name = os.path.join(str(tmpdir), 'bbox4.shp') dissolve(bbox3_name, bbox4_name, field='area') assert os.path.isfile(bbox4_name)
def test_Raster_filestack(testdata): with pytest.raises(RuntimeError): with Raster([testdata['tif']]) as ras: print(ras) with Raster([testdata['tif'], testdata['tif2']]) as ras: assert ras.bands == 2 arr = ras.array() mean = parallel_apply_along_axis(np.nanmean, axis=2, arr=arr, cores=4) assert mean.shape == (217, 268)
def uzh_prepare(reference, outdir, source): """ create an UZH incident angle subset resampled to a reference image. Parameters ---------- reference: str the reference file with the target extent outdir: str the directory to write the new file to; new files are named uzh_{epsg}_{index}.tif, e.g. uzh_4326_1.tif. source: str the original product to be subsetted Returns ------- numpy.ndarray the content of the file written to `outdir` """ with Raster(reference) as ras: xRes, yRes = ras.res epsg = ras.epsg ext = ras.extent warp_opts = {'options': ['-q'], 'format': 'GTiff', 'multithread': True, 'dstNodata': -99, 'resampleAlg': 'bilinear'} if not os.path.isdir(outdir): os.makedirs(outdir) # find existing files uzh_subs = finder(outdir, ['uzh_[0-9]{4,5}_[0-9].tif'], regex=True) # check if any of the existing files matches the extent of the reference match = False if len(uzh_subs) > 0: for j, sub in enumerate(uzh_subs): with Raster(sub) as ras: if ras.extent == ext: uzh_sub = sub match = True if not match: with Raster(source) as ras: if ras.epsg != epsg: raise RuntimeError('CRS mismatch') basename = 'uzh_{}_{}.tif'.format(epsg, len(uzh_subs)) uzh_sub = os.path.join(outdir, basename) print('creating', uzh_sub) warp_opts['dstSRS'] = 'EPSG:{}'.format(epsg) warp_opts['xRes'] = xRes warp_opts['yRes'] = yRes warp_opts['outputBounds'] = (ext['xmin'], ext['ymin'], ext['xmax'], ext['ymax']) gdalwarp(src=source, dst=uzh_sub, options=warp_opts) return uzh_sub
def clc_prepare(reference, outdir, source): """ create a CLC subset resampled to a reference image. Parameters ---------- reference: str the reference file with the target CRS and extent outdir: str the directory to write the new file to; new files are named clc{index}.tif, e.g. clc1.tif. source: str the original product to be subsetted Returns ------- str the name of the file written to `outdir` """ with Raster(reference) as ras: xRes, yRes = ras.res epsg = ras.epsg ext = ras.extent ######################################################################### warp_opts = { 'options': ['-q'], 'format': 'GTiff', 'multithread': True, 'dstNodata': -99, 'resampleAlg': 'mode' } if not os.path.isdir(outdir): os.makedirs(outdir) clc_subs = finder(outdir, ['clc[0-9].tif'], regex=True) match = False if len(clc_subs) > 0: for j, sub in enumerate(clc_subs): with Raster(sub) as ras: if ras.extent == ext: clc_sub = sub match = True if not match: clc_sub = os.path.join(outdir, 'clc{}.tif'.format(len(clc_subs))) print('creating', clc_sub) warp_opts['dstSRS'] = 'EPSG:{}'.format(epsg) warp_opts['xRes'] = xRes warp_opts['yRes'] = yRes warp_opts['outputBounds'] = (ext['xmin'], ext['ymin'], ext['xmax'], ext['ymax']) gdalwarp(src=source, dst=clc_sub, options=warp_opts) return clc_sub
def test_Raster_extract(testdata): with Raster(testdata['tif']) as ras: assert ras.extract(px=624000, py=4830000, radius=5) == -10.48837461270875 with pytest.raises(RuntimeError): ras.extract(1, 4830000) with pytest.raises(RuntimeError): ras.extract(624000, 1) # ensure corner extraction capability assert ras.extract(px=ras.geo['xmin'], py=ras.geo['ymax']) == -10.147890090942383 assert ras.extract(px=ras.geo['xmin'], py=ras.geo['ymin']) == -14.640368461608887 assert ras.extract(px=ras.geo['xmax'], py=ras.geo['ymax']) == -9.599242210388182 assert ras.extract(px=ras.geo['xmax'], py=ras.geo['ymin']) == -9.406558990478516 # test nodata handling capability and correct indexing mat = ras.matrix() mat[0:10, 0:10] = ras.nodata mat[207:217, 258:268] = ras.nodata ras.assign(mat, band=0) assert ras.extract(px=ras.geo['xmin'], py=ras.geo['ymax'], radius=5) == ras.nodata assert ras.extract(px=ras.geo['xmax'], py=ras.geo['ymin'], radius=5) == ras.nodata
def dem_degree2meter(demfile): """ compute the spatial resolution in meters for a DEM with WGS84 degree coordinates. Parameters ---------- demfile: str the DEM file Returns ------- tuple (posting_east, posting_north) See Also -------- spatialist.auxil.haversine """ with Raster(demfile) as ras: res_lon, res_lat = ras.res lat = (ras.geo['ymin'] + ras.geo['ymax']) / 2 lon = (ras.geo['xmin'] + ras.geo['xmax']) / 2 post_north = haversine(lat, lon, lat + res_lat, lon) post_east = haversine(lat, lon, lat, lon + res_lon) return post_east, post_north
def test_auxil(tmpdir, testdata): dir = str(tmpdir) with Raster(testdata['tif']) as ras: bbox = os.path.join(dir, 'bbox.shp') ras.bbox(bbox) ogr2ogr(bbox, os.path.join(dir, 'bbox.gml'), {'format': 'GML'}) gdal_translate(ras.raster, os.path.join(dir, 'test'), {'format': 'ENVI'}) gdal_rasterize(bbox, os.path.join(dir, 'test2'), {'format': 'GTiff', 'xRes': 20, 'yRes': 20})
def clc_prep(clc, reference, outname): """ resample and crop the corine product to the resolution and extent of a reference image. Parameters ---------- clc: str the name of the CLC input file reference: str the name of the reference file outname: str the named of the output image Returns ------- """ with Raster(reference).bbox() as box_ras: with Raster(clc).bbox() as box_clc: if intersect(box_ras, box_clc) is None: print('no intersect') return if not os.path.isfile(outname): with Raster(reference) as ras: ref_crs = ras.projection xres, yres = ras.res with ras.bbox() as box: ref_ext = box.extent outputBounds = (ref_ext['xmin'], ref_ext['ymin'], ref_ext['xmax'], ref_ext['ymax']) gdalwarp_opt = { 'format': 'GTiff', 'outputBounds': outputBounds, 'multithread': True, 'xRes': xres, 'yRes': yres, 'dstSRS': ref_crs, 'resampleAlg': 'mode' } gdalwarp(src=clc, dst=outname, options=gdalwarp_opt) else: print('outfile already exists')
def __buildvrt(archives, vrtfile, pattern, vsi, extent, nodata=None, srs=None): locals = [vsi + x for x in dissolve([finder(x, [pattern]) for x in archives])] if nodata is None: with Raster(locals[0]) as ras: nodata = ras.nodata opts = {'outputBounds': (extent['xmin'], extent['ymin'], extent['xmax'], extent['ymax']), 'srcNodata': nodata} if srs is not None: opts['outputSRS'] = crsConvert(srs, 'wkt') gdalbuildvrt(src=locals, dst=vrtfile, options=opts)
def test_Raster_subset(testdata): with Raster(testdata['tif']) as ras: ext = ras.bbox().extent xres, yres = ras.res ext['xmin'] += xres ext['xmax'] -= xres ext['ymin'] += yres ext['ymax'] -= yres with bbox(ext, ras.proj4) as vec: with ras[vec] as sub: xres, yres = ras.res assert sub.geo['xmin'] - ras.geo['xmin'] == xres assert ras.geo['xmax'] - sub.geo['xmax'] == xres assert sub.geo['ymin'] - ras.geo['ymin'] == xres assert ras.geo['ymax'] - sub.geo['ymax'] == xres
def test_stack(tmpdir, testdata): name = testdata['tif'] outname = os.path.join(str(tmpdir), 'test') tr = (30, 30) with pytest.raises(IOError): stack(srcfiles=[], resampling='near', targetres=tr, srcnodata=-99, dstnodata=-99, dstfile=outname) with pytest.raises(IOError): stack(srcfiles=[name, name], resampling='near', targetres=tr, srcnodata=-99, dstnodata=-99, dstfile=outname, layernames=['a']) with pytest.raises(RuntimeError): stack(srcfiles=[name, name], resampling='near', targetres=30, srcnodata=-99, dstnodata=-99, dstfile=outname) with pytest.raises(IOError): stack(srcfiles=[name], resampling='near', targetres=tr, overwrite=True, srcnodata=-99, dstnodata=-99, dstfile=outname) with pytest.raises(RuntimeError): stack(srcfiles=[name, name], resampling='near', targetres=(30, 30, 30), srcnodata=-99, dstnodata=-99, dstfile=outname) with pytest.raises(IOError): stack(srcfiles=[name, name], resampling='foobar', targetres=tr, srcnodata=-99, dstnodata=-99, dstfile=outname) with pytest.raises(OSError): stack(srcfiles=[name, 'foobar'], resampling='near', targetres=tr, srcnodata=-99, dstnodata=-99, dstfile=outname) stack(srcfiles=[name, name], resampling='near', targetres=tr, overwrite=True, srcnodata=-99, dstnodata=-99, dstfile=outname) outdir = os.path.join(str(tmpdir), 'subdir') stack(srcfiles=[name, name], resampling='near', targetres=tr, overwrite=True, layernames=['test1', 'test2'], srcnodata=-99, dstnodata=-99, dstfile=outdir, separate=True, compress=True) with Raster(outname) as ras: assert ras.bands == 2 # Raster.rescale currently only supports one band with pytest.raises(ValueError): ras.rescale(lambda x: x * 10)
def test_rasterize(tmpdir, testdata): outname = os.path.join(str(tmpdir), 'test.shp') with Raster(testdata['tif']) as ras: vec = ras.bbox() # test length mismatch between burn_values and expressions with pytest.raises(RuntimeError): rasterize(vec, reference=ras, outname=outname, burn_values=[1], expressions=['foo', 'bar']) # test a faulty expression with pytest.raises(RuntimeError): rasterize(vec, reference=ras, outname=outname, burn_values=[1], expressions=['foo']) # test default parametrization rasterize(vec, reference=ras, outname=outname) assert os.path.isfile(outname) # test appending to existing file with valid expression rasterize(vec, reference=ras, outname=outname, append=True, burn_values=[1], expressions=['id=1']) # test wrong input type for reference with pytest.raises(RuntimeError): rasterize(vec, reference='foobar', outname=outname)
def geocode(infile, outdir, t_srs=4326, tr=20, polarizations='all', shapefile=None, scaling='dB', geocoding_type='Range-Doppler', removeS1BorderNoise=True, removeS1BorderNoiseMethod='pyroSAR', removeS1ThermalNoise=True, offset=None, allow_RES_OSV=False, demName='SRTM 1Sec HGT', externalDEMFile=None, externalDEMNoDataValue=None, externalDEMApplyEGM=True, terrainFlattening=True, basename_extensions=None, test=False, export_extra=None, groupsize=1, cleanup=True, tmpdir=None, gpt_exceptions=None, gpt_args=None, returnWF=False, nodataValueAtSea=True, demResamplingMethod='BILINEAR_INTERPOLATION', imgResamplingMethod='BILINEAR_INTERPOLATION', alignToStandardGrid=False, standardGridOriginX=0, standardGridOriginY=0, speckleFilter=False, refarea='gamma0'): """ wrapper function for geocoding SAR images using ESA SNAP Parameters ---------- infile: str or ~pyroSAR.drivers.ID or list the SAR scene(s) to be processed; multiple scenes are treated as consecutive acquisitions, which will be mosaicked with SNAP's SliceAssembly operator outdir: str The directory to write the final files to. 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: int or float, optional The target resolution in meters. Default is 20 polarizations: list or str The polarizations to be processed; can be a string for a single polarization, e.g. 'VV', or a list of several polarizations, e.g. ['VV', 'VH']. With the special value 'all' (default) all available polarizations are processed. shapefile: str or :py:class:`~spatialist.vector.Vector` or dict or None A vector geometry for subsetting the SAR scene to a test site. scaling: {'dB', 'db', 'linear'}, optional Should the output be in linear or decibel scaling? Default is 'dB'. geocoding_type: {'Range-Doppler', 'SAR simulation cross correlation'}, optional The type of geocoding applied; can be either 'Range-Doppler' (default) or 'SAR simulation cross correlation' removeS1BorderNoise: bool, optional Enables removal of S1 GRD border noise (default). removeS1BorderNoiseMethod: str the border noise removal method to be applied, See :func:`pyroSAR.S1.removeGRDBorderNoise` for details; one of the following: - 'ESA': the pure implementation as described by ESA - 'pyroSAR': the ESA method plus the custom pyroSAR refinement removeS1ThermalNoise: bool, optional Enables removal of S1 thermal noise (default). offset: tuple, optional A tuple defining offsets for left, right, top and bottom in pixels, e.g. (100, 100, 0, 0); this variable is overridden if a shapefile is defined. Default is None. allow_RES_OSV: bool (only applies to Sentinel-1) Also allow the less accurate RES orbit files to be used? The function first tries to download a POE file for the scene. If this fails and RES files are allowed, it will download the RES file. The selected OSV type is written to the workflow XML file. Processing is aborted if the correction fails (Apply-Orbit-File parameter continueOnFail set to false). demName: str the name of the auto-download DEM. Default is 'SRTM 1Sec HGT'. Is ignored when `externalDEMFile` is not None. externalDEMFile: str or None, optional The absolute path to an external DEM file. Default is None. Overrides `demName`. externalDEMNoDataValue: int, float or None, optional The no data value of the external DEM. If not specified (default) the function will try to read it from the specified external DEM. externalDEMApplyEGM: bool, optional Apply Earth Gravitational Model to external DEM? Default is True. terrainFlattening: bool apply topographic normalization on the data? basename_extensions: list of str or None names of additional parameters to append to the basename, e.g. ['orbitNumber_rel'] test: bool, optional If set to True the workflow xml file is only written and not executed. Default is False. export_extra: list or None a list of image file IDs to be exported to outdir. The following IDs are currently supported: - incidenceAngleFromEllipsoid - localIncidenceAngle - projectedLocalIncidenceAngle - DEM - layoverShadowMask - scatteringArea groupsize: int the number of workers executed together in one gpt call cleanup: bool should all files written to the temporary directory during function execution be deleted after processing? tmpdir: str or None path of custom temporary directory, useful to separate output folder and temp folder. If `None`, the `outdir` location will be used. The created subdirectory will be deleted after processing. gpt_exceptions: dict or None a dictionary to override the configured GPT executable for certain operators; each (sub-)workflow containing this operator will be executed with the define executable; - e.g. ``{'Terrain-Flattening': '/home/user/snap/bin/gpt'}`` gpt_args: list or None a list of additional arguments to be passed to the gpt call - e.g. ``['-x', '-c', '2048M']`` for increased tile cache size and intermediate clearing returnWF: bool return the full name of the written workflow XML file? nodataValueAtSea: bool mask pixels acquired over sea? The sea mask depends on the selected DEM. demResamplingMethod: str one of the following: - 'NEAREST_NEIGHBOUR' - 'BILINEAR_INTERPOLATION' - 'CUBIC_CONVOLUTION' - 'BISINC_5_POINT_INTERPOLATION' - 'BISINC_11_POINT_INTERPOLATION' - 'BISINC_21_POINT_INTERPOLATION' - 'BICUBIC_INTERPOLATION' imgResamplingMethod: str the resampling method for geocoding the SAR image; the options are identical to demResamplingMethod speckleFilter: str one of the following: - 'Boxcar' - 'Median' - 'Frost' - 'Gamma Map' - 'Refined Lee' - 'Lee' - 'Lee Sigma' refarea: str or list 'sigma0', 'gamma0' or a list of both alignToStandardGrid: bool align all processed images to a common grid? standardGridOriginX: int or float the x origin value for grid alignment standardGridOriginY: int or float the y origin value for grid alignment Returns ------- str or None either the name of the workflow file if `returnWF == True` or None otherwise Note ---- If only one polarization is selected and not extra products are defined the results are directly written to GeoTiff. Otherwise the results are first written to a folder containing ENVI files and then transformed to GeoTiff files (one for each polarization/extra product). If GeoTiff would directly be selected as output format for multiple polarizations then a multilayer GeoTiff is written by SNAP which is considered an unfavorable format .. figure:: figures/snap_geocode.png :scale: 25% :align: center Workflow diagram for function geocode for processing a Sentinel-1 Ground Range Detected (GRD) scene to radiometrically terrain corrected (RTC) backscatter. An additional Subset node might be inserted in case a vector geometry is provided. Examples -------- geocode a Sentinel-1 scene and export the local incidence angle map with it >>> from pyroSAR.snap import geocode >>> filename = 'S1A_IW_GRDH_1SDV_20180829T170656_20180829T170721_023464_028DE0_F7BD.zip' >>> geocode(infile=filename, outdir='outdir', tr=20, scaling='dB', >>> export_extra=['DEM', 'localIncidenceAngle'], t_srs=4326) See Also -------- :class:`pyroSAR.drivers.ID`, :class:`spatialist.vector.Vector`, :func:`spatialist.auxil.crsConvert()` """ if isinstance(infile, pyroSAR.ID): id = infile elif isinstance(infile, str): id = pyroSAR.identify(infile) elif isinstance(infile, list): ids = pyroSAR.identify_many(infile, verbose=False, sortkey='start') id = ids[0] else: raise TypeError("'infile' must be of type str, list or pyroSAR.ID") if id.is_processed(outdir): print('scene {} already processed'.format(id.outname_base())) return # print(os.path.basename(id.scene)) if not os.path.isdir(outdir): os.makedirs(outdir) ############################################ # general setup if id.sensor in ['ASAR', 'ERS1', 'ERS2']: formatName = 'ENVISAT' elif id.sensor in ['S1A', 'S1B']: if id.product == 'SLC': raise RuntimeError('Sentinel-1 SLC data is not supported yet') formatName = 'SENTINEL-1' else: raise RuntimeError('sensor not supported (yet)') # several options like resampling are modified globally for the whole workflow at the end of this function # this list gathers IDs of nodes for which this should not be done because they are configured individually resampling_exceptions = [] ###################### # print('- assessing polarization selection') if isinstance(polarizations, str): if polarizations == 'all': polarizations = id.polarizations else: if polarizations in id.polarizations: polarizations = [polarizations] else: raise RuntimeError( 'polarization {} does not exists in the source product'. format(polarizations)) elif isinstance(polarizations, list): polarizations = [x for x in polarizations if x in id.polarizations] else: raise RuntimeError('polarizations must be of type str or list') bandnames = dict() bandnames['int'] = ['Intensity_' + x for x in polarizations] bandnames['beta0'] = ['Beta0_' + x for x in polarizations] bandnames['gamma0'] = ['Gamma0_' + x for x in polarizations] bandnames['sigma0'] = ['Sigma0_' + x for x in polarizations] ############################################ ############################################ # parse base workflow # print('- parsing base workflow') workflow = parse_recipe('base') ############################################ # Read node configuration # print('-- configuring Read Node') read = workflow['Read'] read.parameters['file'] = id.scene read.parameters['formatName'] = formatName readers = [read.id] if isinstance(infile, list): for i in range(1, len(infile)): readn = parse_node('Read') readn.parameters['file'] = ids[i].scene readn.parameters['formatName'] = formatName workflow.insert_node(readn, before=read.id, resetSuccessorSource=False) readers.append(readn.id) sliceAssembly = parse_node('SliceAssembly') sliceAssembly.parameters['selectedPolarisations'] = polarizations workflow.insert_node(sliceAssembly, before=readers) read = sliceAssembly ############################################ # Remove-GRD-Border-Noise node configuration # print('-- configuring Remove-GRD-Border-Noise Node') if id.sensor in ['S1A', 'S1B'] and removeS1BorderNoise: bn = parse_node('Remove-GRD-Border-Noise') workflow.insert_node(bn, before=read.id) bn.parameters['selectedPolarisations'] = polarizations ############################################ # ThermalNoiseRemoval node configuration # print('-- configuring ThermalNoiseRemoval Node') if id.sensor in ['S1A', 'S1B'] and removeS1ThermalNoise: for reader in readers: tn = parse_node('ThermalNoiseRemoval') workflow.insert_node(tn, before=reader) tn.parameters['selectedPolarisations'] = polarizations ############################################ # orbit file application node configuration # print('-- configuring Apply-Orbit-File Node') orbit_lookup = { 'ENVISAT': 'DELFT Precise (ENVISAT, ERS1&2) (Auto Download)', 'SENTINEL-1': 'Sentinel Precise (Auto Download)' } orbitType = orbit_lookup[formatName] if formatName == 'ENVISAT' and id.acquisition_mode == 'WSM': orbitType = 'DORIS Precise VOR (ENVISAT) (Auto Download)' if formatName == 'SENTINEL-1': match = id.getOSV(osvType='POE', returnMatch=True) if match is None and allow_RES_OSV: id.getOSV(osvType='RES') orbitType = 'Sentinel Restituted (Auto Download)' orb = workflow['Apply-Orbit-File'] orb.parameters['orbitType'] = orbitType orb.parameters['continueOnFail'] = False ############################################ # calibration node configuration # print('-- configuring Calibration Node') cal = workflow['Calibration'] cal.parameters['selectedPolarisations'] = polarizations cal.parameters['sourceBands'] = bandnames['int'] if isinstance(refarea, str): refarea = [refarea] if terrainFlattening: if 'gamma0' not in refarea: raise RuntimeError( 'if terrain flattening is applied refarea must be gamma0') cal.parameters['outputBetaBand'] = True if 'sigma0' in refarea: cal.parameters['outputSigmaBand'] = True else: refarea_options = ['sigma0', 'gamma0'] for opt in refarea: if opt not in refarea_options: message = '{0} must be one of the following:\n- {1}' raise ValueError( message.format('refarea', '\n- '.join(refarea_options))) cal.parameters['output{}Band'.format(opt[:-1].capitalize())] = True last = cal.id ############################################ # terrain flattening node configuration # print('-- configuring Terrain-Flattening Node') if terrainFlattening: tf = parse_node('Terrain-Flattening') workflow.insert_node(tf, before=last) if id.sensor in ['ERS1', 'ERS2'] or (id.sensor == 'ASAR' and id.acquisition_mode != 'APP'): tf.parameters['sourceBands'] = 'Beta0' else: tf.parameters['sourceBands'] = bandnames['beta0'] if 'reGridMethod' in tf.parameters.keys(): if externalDEMFile is None: tf.parameters['reGridMethod'] = True else: tf.parameters['reGridMethod'] = False last = tf.id ############################################ # speckle filtering node configuration speckleFilter_options = [ 'Boxcar', 'Median', 'Frost', 'Gamma Map', 'Refined Lee', 'Lee', 'Lee Sigma' ] if speckleFilter: message = '{0} must be one of the following:\n- {1}' if speckleFilter not in speckleFilter_options: raise ValueError( message.format('speckleFilter', '\n- '.join(speckleFilter_options))) sf = parse_node('Speckle-Filter') workflow.insert_node(sf, before=last) sf.parameters['sourceBands'] = None sf.parameters['filter'] = speckleFilter last = sf.id ############################################ # configuration of node sequence for specific geocoding approaches bands = dissolve([bandnames[opt] for opt in refarea]) if geocoding_type == 'Range-Doppler': tc = parse_node('Terrain-Correction') workflow.insert_node(tc, before=last) tc.parameters['sourceBands'] = bands elif geocoding_type == 'SAR simulation cross correlation': sarsim = parse_node('SAR-Simulation') workflow.insert_node(sarsim, before=last) sarsim.parameters['sourceBands'] = bands workflow.insert_node(parse_node('Cross-Correlation'), before='SAR-Simulation') tc = parse_node('SARSim-Terrain-Correction') workflow.insert_node(tc, before='Cross-Correlation') else: raise RuntimeError('geocode_type not recognized') tc.parameters['alignToStandardGrid'] = alignToStandardGrid tc.parameters['standardGridOriginX'] = standardGridOriginX tc.parameters['standardGridOriginY'] = standardGridOriginY ############################################ # Multilook node configuration try: image_geometry = id.meta['image_geometry'] incidence = id.meta['incidence'] except KeyError: raise RuntimeError( 'This function does not yet support sensor {}'.format(id.sensor)) rlks, azlks = multilook_factors(sp_rg=id.spacing[0], sp_az=id.spacing[1], tr_rg=tr, tr_az=tr, geometry=image_geometry, incidence=incidence) if azlks > 1 or rlks > 1: workflow.insert_node(parse_node('Multilook'), before='Calibration') ml = workflow['Multilook'] ml.parameters['nAzLooks'] = azlks ml.parameters['nRgLooks'] = rlks ml.parameters['sourceBands'] = None ############################################ # merge sigma0 and gamma0 bands to pass them to Terrain-Correction if len(refarea) > 1 and terrainFlattening: bm = parse_node('BandMerge') workflow.insert_node(bm, before=[tf.source, tf.id]) sources = bm.source gamma_index = sources.index('Terrain-Flattening') sigma_index = abs(gamma_index - 1) s1_id = os.path.basename(os.path.splitext(id.scene)[0]) bands_long = [] for band in bands: comp = [band + '::'] if shapefile is not None: comp.append('Subset_') comp.append(s1_id) if band.startswith('Gamma'): comp.append('_' + workflow.suffix(stop=sources[gamma_index])) else: comp.append('_' + workflow.suffix(stop=sources[sigma_index])) bands_long.append(''.join(comp)) bm.parameters['sourceBands'] = bands_long bm.parameters['geographicError'] = 0.0 ############################################ # specify spatial resolution and coordinate reference system of the output dataset # print('-- configuring CRS') tc.parameters['pixelSpacingInMeter'] = tr try: t_srs = crsConvert(t_srs, 'epsg') except TypeError: raise RuntimeError("format of parameter 't_srs' not recognized") # the EPSG code 4326 is not supported by SNAP and thus the WKT string has to be defined; # in all other cases defining EPSG:{code} will do if t_srs == 4326: t_srs = 'GEOGCS["WGS84(DD)",' \ 'DATUM["WGS84",' \ 'SPHEROID["WGS84", 6378137.0, 298.257223563]],' \ 'PRIMEM["Greenwich", 0.0],' \ 'UNIT["degree", 0.017453292519943295],' \ 'AXIS["Geodetic longitude", EAST],' \ 'AXIS["Geodetic latitude", NORTH]]' else: t_srs = 'EPSG:{}'.format(t_srs) tc.parameters['mapProjection'] = t_srs ############################################ # (optionally) add node for conversion from linear to db scaling # print('-- configuring LinearToFromdB Node') if scaling not in ['dB', 'db', 'linear']: raise RuntimeError( 'scaling must be a string of either "dB", "db" or "linear"') if scaling in ['dB', 'db']: lin2db = parse_node('LinearToFromdB') workflow.insert_node(lin2db, before=tc.id) lin2db.parameters['sourceBands'] = bands ############################################ # (optionally) add subset node and add bounding box coordinates of defined shapefile # print('-- configuring Subset Node') if shapefile: # print('--- read') if isinstance(shapefile, dict): ext = shapefile else: if isinstance(shapefile, Vector): shp = shapefile.clone() elif isinstance(shapefile, str): shp = Vector(shapefile) else: raise TypeError( "argument 'shapefile' must be either a dictionary, a Vector object or a string." ) # reproject the geometry to WGS 84 latlon shp.reproject(4326) ext = shp.extent shp.close() # add an extra buffer of 0.01 degrees buffer = 0.01 ext['xmin'] -= buffer ext['ymin'] -= buffer ext['xmax'] += buffer ext['ymax'] += buffer # print('--- create bbox') with bbox(ext, 4326) as bounds: # print('--- intersect') inter = intersect(id.bbox(), bounds) if not inter: raise RuntimeError( 'no bounding box intersection between shapefile and scene') inter.close() wkt = bounds.convert2wkt()[0] subset = parse_node('Subset') workflow.insert_node(subset, before=read.id) subset.parameters['region'] = [0, 0, id.samples, id.lines] subset.parameters['geoRegion'] = wkt subset.parameters['copyMetadata'] = True ############################################ # (optionally) configure subset node for pixel offsets if offset and not shapefile: subset = parse_node('Subset') workflow.insert_node(subset, before=read.id) # left, right, top and bottom offset in pixels l, r, t, b = offset subset_values = [l, t, id.samples - l - r, id.lines - t - b] subset.parameters['region'] = subset_values subset.parameters['geoRegion'] = '' ############################################ # parametrize write node # print('-- configuring Write Node') # create a suffix for the output file to identify processing steps performed in the workflow suffix = workflow.suffix() if tmpdir is None: tmpdir = outdir basename = os.path.join(tmpdir, id.outname_base(basename_extensions)) outname = basename + '_' + suffix write = workflow['Write'] write.parameters['file'] = outname write.parameters['formatName'] = 'ENVI' ############################################ ############################################ if export_extra is not None: tc_options = [ 'incidenceAngleFromEllipsoid', 'localIncidenceAngle', 'projectedLocalIncidenceAngle', 'DEM' ] tc_write = None tc_selection = [] for item in export_extra: if item in tc_options: if tc_write is None: tc_write = parse_node('Write') workflow.insert_node(tc_write, before=tc.id, resetSuccessorSource=False) tc_write.parameters['file'] = outname tc_write.parameters['formatName'] = 'ENVI' key = 'save{}{}'.format(item[0].upper(), item[1:]) tc.parameters[key] = True tc_selection.append(item) elif item == 'layoverShadowMask': sarsim = parse_node('SAR-Simulation') sarsim.parameters['saveLayoverShadowMask'] = True workflow.insert_node(sarsim, after=tc.id, resetSuccessorSource=False) sarsim_select = parse_node('BandSelect') sarsim_select.parameters['sourceBands'] = 'layover_shadow_mask' workflow.insert_node(sarsim_select, before=sarsim.id, resetSuccessorSource=False) sarsim_tc = parse_node('Terrain-Correction') workflow.insert_node(sarsim_tc, before=sarsim_select.id) sarsim_tc.parameters[ 'alignToStandardGrid'] = alignToStandardGrid sarsim_tc.parameters[ 'standardGridOriginX'] = standardGridOriginX sarsim_tc.parameters[ 'standardGridOriginY'] = standardGridOriginY sarsim_tc.parameters[ 'imgResamplingMethod'] = 'NEAREST_NEIGHBOUR' resampling_exceptions.append(sarsim_tc.id) sarsim_write = parse_node('Write') sarsim_write.parameters['file'] = outname sarsim_write.parameters['formatName'] = 'ENVI' workflow.insert_node(sarsim_write, before=sarsim_tc.id, resetSuccessorSource=False) elif item == 'scatteringArea': area_select = parse_node('BandSelect') workflow.insert_node(area_select, before=tf.source, resetSuccessorSource=False) area_select.parameters['sourceBands'] = bandnames['beta0'] area_merge1 = parse_node('BandMerge') workflow.insert_node(area_merge1, before=[tf.id, area_select.id], resetSuccessorSource=False) math = parse_node('BandMaths') math.element.attrib[ 'class'] = '"com.bc.ceres.binding.dom.XppDomElement"' workflow.insert_node(math, before=area_merge1.id, resetSuccessorSource=False) # math = parse_node('BandMaths') # workflow.insert_node(math, before=[tf.id, area_select.id], resetSuccessorSource=False) math.parameters.clear_variables() exp = math.parameters['targetBands'][0] exp['name'] = 'scatteringArea_VV' exp['type'] = 'float32' exp['expression'] = 'Beta0_VV / Gamma0_VV' exp['noDataValue'] = 0.0 area_merge2 = parse_node('BandMerge') workflow.insert_node(area_merge2, before=[tf.id, math.id], resetSuccessorSource=False) tc.source = area_merge2.id # modify Terrain-Correction source bands tc_bands = tc.parameters['sourceBands'] + ',scatteringArea_VV' tc.parameters['sourceBands'] = tc_bands # add scattering Area to list of band directly written from Terrain-Correction tc_selection.append('scatteringArea_VV') else: raise RuntimeError( "ID '{}' not valid for argument 'export_extra'".format( item)) if len(tc_selection) > 0: tc_select = parse_node('BandSelect') workflow.insert_node(tc_select, after=tc_write.id) tc_select.parameters['sourceBands'] = tc_selection ############################################ ############################################ # select DEM type # print('-- configuring DEM') dempar = { 'externalDEMFile': externalDEMFile, 'externalDEMApplyEGM': externalDEMApplyEGM } if externalDEMFile is not None: if os.path.isfile(externalDEMFile): if externalDEMNoDataValue is None: with Raster(externalDEMFile) as dem: dempar['externalDEMNoDataValue'] = dem.nodata if dempar['externalDEMNoDataValue'] is None: raise RuntimeError( 'Cannot read NoData value from DEM file. ' 'Please specify externalDEMNoDataValue') else: dempar['externalDEMNoDataValue'] = externalDEMNoDataValue dempar['reGridMethod'] = False else: raise RuntimeError('specified externalDEMFile does not exist') dempar['demName'] = 'External DEM' else: dempar['demName'] = demName dempar['externalDEMFile'] = None dempar['externalDEMNoDataValue'] = 0 for key, value in dempar.items(): workflow.set_par(key, value) # download the EGM lookup table if necessary if dempar['externalDEMApplyEGM']: get_egm96_lookup() ############################################ ############################################ # configure the resampling methods options = [ 'NEAREST_NEIGHBOUR', 'BILINEAR_INTERPOLATION', 'CUBIC_CONVOLUTION', 'BISINC_5_POINT_INTERPOLATION', 'BISINC_11_POINT_INTERPOLATION', 'BISINC_21_POINT_INTERPOLATION', 'BICUBIC_INTERPOLATION' ] message = '{0} must be one of the following:\n- {1}' if demResamplingMethod not in options: raise ValueError( message.format('demResamplingMethod', '\n- '.join(options))) if imgResamplingMethod not in options: raise ValueError( message.format('imgResamplingMethod', '\n- '.join(options))) workflow.set_par('demResamplingMethod', demResamplingMethod) workflow.set_par('imgResamplingMethod', imgResamplingMethod, exceptions=resampling_exceptions) ############################################ ############################################ # additional parameter settings applied to the whole workflow workflow.set_par('nodataValueAtSea', nodataValueAtSea) ############################################ ############################################ # write workflow to file and optionally execute it # print('- writing workflow to file') wf_name = outname.replace(tmpdir, outdir) + '_proc.xml' workflow.write(wf_name) # execute the newly written workflow if not test: try: groups = groupbyWorkers(wf_name, groupsize) gpt(wf_name, groups=groups, cleanup=cleanup, gpt_exceptions=gpt_exceptions, gpt_args=gpt_args, removeS1BorderNoiseMethod=removeS1BorderNoiseMethod, outdir=outdir) except RuntimeError as e: print(str(e)) with open(wf_name.replace('_proc.xml', '_error.log'), 'w') as log: log.write(str(e)) if returnWF: return wf_name
def geocode(infile, outdir, t_srs=4326, tr=20, polarizations='all', shapefile=None, scaling='dB', geocoding_type='Range-Doppler', removeS1BorderNoise=True, removeS1ThermalNoise=True, offset=None, externalDEMFile=None, externalDEMNoDataValue=None, externalDEMApplyEGM=True, terrainFlattening=True, basename_extensions=None, test=False, export_extra=None, groupsize=2, cleanup=True, gpt_exceptions=None, returnWF=False, demResamplingMethod='BILINEAR_INTERPOLATION', imgResamplingMethod='BILINEAR_INTERPOLATION', speckleFilter=False, refarea='gamma0'): """ wrapper function for geocoding SAR images using ESA SNAP Parameters ---------- infile: str or ~pyroSAR.drivers.ID the SAR scene to be processed outdir: str The directory to write the final files to. 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: int or float, optional The target resolution in meters. Default is 20 polarizations: list or {'VV', 'HH', 'VH', 'HV', 'all'}, optional The polarizations to be processed; can be a string for a single polarization e.g. 'VV' or a list of several polarizations e.g. ['VV', 'VH']. Default is 'all'. shapefile: str or :py:class:`~spatialist.vector.Vector`, optional A vector geometry for subsetting the SAR scene to a test site. Default is None. scaling: {'dB', 'db', 'linear'}, optional Should the output be in linear or decibel scaling? Default is 'dB'. geocoding_type: {'Range-Doppler', 'SAR simulation cross correlation'}, optional The type of geocoding applied; can be either 'Range-Doppler' (default) or 'SAR simulation cross correlation' removeS1BorderNoise: bool, optional Enables removal of S1 GRD border noise (default). removeS1ThermalNoise: bool, optional Enables removal of S1 thermal noise (default). offset: tuple, optional A tuple defining offsets for left, right, top and bottom in pixels, e.g. (100, 100, 0, 0); this variable is overridden if a shapefile is defined. Default is None. externalDEMFile: str or None, optional The absolute path to an external DEM file. Default is None. externalDEMNoDataValue: int, float or None, optional The no data value of the external DEM. If not specified (default) the function will try to read it from the specified external DEM. externalDEMApplyEGM: bool, optional Apply Earth Gravitational Model to external DEM? Default is True. terainFlattening: bool apply topographic normalization on the data? basename_extensions: list of str names of additional parameters to append to the basename, e.g. ['orbitNumber_rel'] test: bool, optional If set to True the workflow xml file is only written and not executed. Default is False. export_extra: list or None a list of image file IDs to be exported to outdir. The following IDs are currently supported: - incidenceAngleFromEllipsoid - localIncidenceAngle - projectedLocalIncidenceAngle - DEM groupsize: int the number of workers executed together in one gpt call cleanup: bool should all files written to the temporary directory during function execution be deleted after processing? gpt_exceptions: dict a dictionary to override the configured GPT executable for certain operators; each (sub-)workflow containing this operator will be executed with the define executable; - e.g. ``{'Terrain-Flattening': '/home/user/snap/bin/gpt'}`` returnWF: bool return the full name of the written workflow XML file? demResamplingMethod: str one of the following: - 'NEAREST_NEIGHBOUR' - 'BILINEAR_INTERPOLATION' - 'CUBIC_CONVOLUTION' - 'BISINC_5_POINT_INTERPOLATION' - 'BISINC_11_POINT_INTERPOLATION' - 'BISINC_21_POINT_INTERPOLATION' - 'BICUBIC_INTERPOLATION' imgResamplingMethod: str the resampling method for geocoding the SAR image; the options are identical to demResamplingMethod speckleFilter: str one of the following: - 'Boxcar' - 'Median' - 'Frost' - 'Gamma Map' - 'Refined Lee' - 'Lee' - 'Lee Sigma' refarea: str one of the following: - 'beta0' - 'gamma0' - 'sigma0' Returns ------- str or None either the name of the workflow file if `returnWF == True` or None otherwise Note ---- If only one polarization is selected and not extra products are defined the results are directly written to GeoTiff. Otherwise the results are first written to a folder containing ENVI files and then transformed to GeoTiff files (one for each polarization/extra product). If GeoTiff would directly be selected as output format for multiple polarizations then a multilayer GeoTiff is written by SNAP which is considered an unfavorable format .. figure:: figures/snap_geocode.png :scale: 25% :align: center Workflow diagram for function geocode for processing a Sentinel-1 Ground Range Detected (GRD) scene to radiometrically terrain corrected (RTC) backscatter. An additional Subset node might be inserted in case a vector geometry is provided. Examples -------- geocode a Sentinel-1 scene and export the local incidence angle map with it >>> from pyroSAR.snap import geocode >>> filename = 'S1A_IW_GRDH_1SDV_20180829T170656_20180829T170721_023464_028DE0_F7BD.zip' >>> geocode(infile=filename, outdir='outdir', tr=20, scaling='dB', >>> export_extra=['DEM', 'localIncidenceAngle'], t_srs=4326) See Also -------- :class:`pyroSAR.drivers.ID`, :class:`spatialist.vector.Vector`, :func:`spatialist.auxil.crsConvert()` """ id = infile if isinstance(infile, pyroSAR.ID) else pyroSAR.identify(infile) if id.is_processed(outdir): print('scene {} already processed'.format(id.outname_base())) return # print(os.path.basename(id.scene)) if not os.path.isdir(outdir): os.makedirs(outdir) ############################################ # general setup if id.sensor in ['ASAR', 'ERS1', 'ERS2']: formatName = 'ENVISAT' elif id.sensor in ['S1A', 'S1B']: if id.product == 'SLC': raise RuntimeError('Sentinel-1 SLC data is not supported yet') formatName = 'SENTINEL-1' else: raise RuntimeError('sensor not supported (yet)') ###################### # print('- assessing polarization selection') if isinstance(polarizations, str): if polarizations == 'all': polarizations = id.polarizations else: if polarizations in id.polarizations: polarizations = [polarizations] else: raise RuntimeError('polarization {} does not exists in the source product'.format(polarizations)) elif isinstance(polarizations, list): polarizations = [x for x in polarizations if x in id.polarizations] else: raise RuntimeError('polarizations must be of type str or list') format = 'GeoTiff-BigTIFF' if len(polarizations) == 1 and export_extra is None else 'ENVI' # print(polarizations) # print(format) bandnames = dict() bandnames['int'] = ['Intensity_' + x for x in polarizations] bandnames['beta0'] = ['Beta0_' + x for x in polarizations] bandnames['gamma0'] = ['Gamma0_' + x for x in polarizations] bandnames['sigma0'] = ['Sigma0_' + x for x in polarizations] ############################################ ############################################ # parse base workflow # print('- parsing base workflow') workflow = parse_recipe('base') ############################################ # Read node configuration # print('-- configuring Read Node') read = workflow['Read'] read.parameters['file'] = id.scene read.parameters['formatName'] = formatName ############################################ # Remove-GRD-Border-Noise node configuration # print('-- configuring Remove-GRD-Border-Noise Node') if id.sensor in ['S1A', 'S1B'] and removeS1BorderNoise: bn = parse_node('Remove-GRD-Border-Noise') workflow.insert_node(bn, before='Read') bn.parameters['selectedPolarisations'] = polarizations ############################################ # ThermalNoiseRemoval node configuration # print('-- configuring ThermalNoiseRemoval Node') if id.sensor in ['S1A', 'S1B'] and removeS1ThermalNoise: tn = parse_node('ThermalNoiseRemoval') workflow.insert_node(tn, before='Read') tn.parameters['selectedPolarisations'] = polarizations ############################################ # orbit file application node configuration # print('-- configuring Apply-Orbit-File Node') orbit_lookup = {'ENVISAT': 'DELFT Precise (ENVISAT, ERS1&2) (Auto Download)', 'SENTINEL-1': 'Sentinel Precise (Auto Download)'} orbitType = orbit_lookup[formatName] if formatName == 'ENVISAT' and id.acquisition_mode == 'WSM': orbitType = 'DORIS Precise VOR (ENVISAT) (Auto Download)' orb = workflow['Apply-Orbit-File'] orb.parameters['orbitType'] = orbitType ############################################ # calibration node configuration # print('-- configuring Calibration Node') cal = workflow['Calibration'] cal.parameters['selectedPolarisations'] = polarizations cal.parameters['sourceBands'] = bandnames['int'] if terrainFlattening: if refarea != 'gamma0': raise RuntimeError('if terrain flattening is applied refarea must be gamma0') cal.parameters['outputBetaBand'] = True else: refarea_options = ['sigma0', 'beta0', 'gamma0'] if refarea not in refarea_options: message = '{0} must be one of the following:\n- {1}' raise ValueError(message.format('refarea', '\n- '.join(refarea_options))) cal.parameters['output{}Band'.format(refarea[:-1].capitalize())] = True last = cal.id ############################################ # terrain flattening node configuration # print('-- configuring Terrain-Flattening Node') if terrainFlattening: tf = parse_node('Terrain-Flattening') workflow.insert_node(tf, before=last) if id.sensor in ['ERS1', 'ERS2'] or (id.sensor == 'ASAR' and id.acquisition_mode != 'APP'): tf.parameters['sourceBands'] = 'Beta0' else: tf.parameters['sourceBands'] = bandnames['beta0'] if externalDEMFile is None: tf.parameters['reGridMethod'] = True else: tf.parameters['reGridMethod'] = False last = tf.id ############################################ # speckle filtering node configuration speckleFilter_options = ['Boxcar', 'Median', 'Frost', 'Gamma Map', 'Refined Lee', 'Lee', 'Lee Sigma'] if speckleFilter: message = '{0} must be one of the following:\n- {1}' if speckleFilter not in speckleFilter_options: raise ValueError(message.format('speckleFilter', '\n- '.join(speckleFilter_options))) sf = parse_node('Speckle-Filter') workflow.insert_node(sf, before=last) sf.parameters['sourceBands'] = bandnames[refarea] sf.parameters['filter'] = speckleFilter last = sf.id ############################################ # configuration of node sequence for specific geocoding approaches # print('-- configuring geocoding approach Nodes') if geocoding_type == 'Range-Doppler': tc = parse_node('Terrain-Correction') workflow.insert_node(tc, before=last) tc.parameters['sourceBands'] = bandnames[refarea] elif geocoding_type == 'SAR simulation cross correlation': sarsim = parse_node('SAR-Simulation') workflow.insert_node(sarsim, before=last) sarsim.parameters['sourceBands'] = bandnames[refarea] workflow.insert_node(parse_node('Cross-Correlation'), before='SAR-Simulation') tc = parse_node('SARSim-Terrain-Correction') workflow.insert_node(tc, before='Cross-Correlation') else: raise RuntimeError('geocode_type not recognized') ############################################ # Multilook node configuration try: image_geometry = id.meta['image_geometry'] incidence = id.meta['incidence'] except KeyError: raise RuntimeError('This function does not yet support sensor {}'.format(id.sensor)) rlks, azlks = multilook_factors(sp_rg=id.spacing[0], sp_az=id.spacing[1], tr_rg=tr, tr_az=tr, geometry=image_geometry, incidence=incidence) if azlks > 1 or rlks > 1: workflow.insert_node(parse_node('Multilook'), before='Calibration') ml = workflow['Multilook'] ml.parameters['nAzLooks'] = azlks ml.parameters['nRgLooks'] = rlks ml.parameters['sourceBands'] = None # if cal.parameters['outputBetaBand']: # ml.parameters['sourceBands'] = bandnames['beta0'] # elif cal.parameters['outputGammaBand']: # ml.parameters['sourceBands'] = bandnames['gamma0'] # elif cal.parameters['outputSigmaBand']: # ml.parameters['sourceBands'] = bandnames['sigma0'] ############################################ # specify spatial resolution and coordinate reference system of the output dataset # print('-- configuring CRS') tc.parameters['pixelSpacingInMeter'] = tr try: t_srs = crsConvert(t_srs, 'epsg') except TypeError: raise RuntimeError("format of parameter 't_srs' not recognized") # the EPSG code 4326 is not supported by SNAP and thus the WKT string has to be defined; # in all other cases defining EPSG:{code} will do if t_srs == 4326: t_srs = 'GEOGCS["WGS84(DD)",' \ 'DATUM["WGS84",' \ 'SPHEROID["WGS84", 6378137.0, 298.257223563]],' \ 'PRIMEM["Greenwich", 0.0],' \ 'UNIT["degree", 0.017453292519943295],' \ 'AXIS["Geodetic longitude", EAST],' \ 'AXIS["Geodetic latitude", NORTH]]' else: t_srs = 'EPSG:{}'.format(t_srs) tc.parameters['mapProjection'] = t_srs ############################################ # (optionally) add node for conversion from linear to db scaling # print('-- configuring LinearToFromdB Node') if scaling not in ['dB', 'db', 'linear']: raise RuntimeError('scaling must be a string of either "dB", "db" or "linear"') if scaling in ['dB', 'db']: lin2db = parse_node('lin2db') workflow.insert_node(lin2db, before=tc.id) lin2db.parameters['sourceBands'] = bandnames[refarea] ############################################ # (optionally) add subset node and add bounding box coordinates of defined shapefile # print('-- configuring Subset Node') if shapefile: # print('--- read') shp = shapefile.clone() if isinstance(shapefile, Vector) else Vector(shapefile) # reproject the geometry to WGS 84 latlon # print('--- reproject') shp.reproject(4326) ext = shp.extent shp.close() # add an extra buffer of 0.01 degrees buffer = 0.01 ext['xmin'] -= buffer ext['ymin'] -= buffer ext['xmax'] += buffer ext['ymax'] += buffer # print('--- create bbox') with bbox(ext, 4326) as bounds: # print('--- intersect') inter = intersect(id.bbox(), bounds) if not inter: raise RuntimeError('no bounding box intersection between shapefile and scene') # print('--- close intersect') inter.close() # print('--- get wkt') wkt = bounds.convert2wkt()[0] # print('--- parse XML node') subset = parse_node('Subset') # print('--- insert node') workflow.insert_node(subset, before='Read') subset.parameters['region'] = [0, 0, id.samples, id.lines] subset.parameters['geoRegion'] = wkt ############################################ # (optionally) configure subset node for pixel offsets if offset and not shapefile: subset = parse_node('Subset') workflow.insert_node(subset, before='Read') # left, right, top and bottom offset in pixels l, r, t, b = offset subset_values = [l, t, id.samples - l - r, id.lines - t - b] subset.parameters['region'] = subset_values subset.parameters['geoRegion'] = '' ############################################ # parametrize write node # print('-- configuring Write Node') # create a suffix for the output file to identify processing steps performed in the workflow suffix = workflow.suffix basename = os.path.join(outdir, id.outname_base(basename_extensions)) extension = suffix if format == 'ENVI' else polarizations[0] + '_' + suffix outname = basename + '_' + extension write = workflow['Write'] write.parameters['file'] = outname write.parameters['formatName'] = format ############################################ ############################################ if export_extra is not None: options = ['incidenceAngleFromEllipsoid', 'localIncidenceAngle', 'projectedLocalIncidenceAngle', 'DEM'] write = parse_node('Write') workflow.insert_node(write, before=tc.id, resetSuccessorSource=False) write.parameters['file'] = outname write.parameters['formatName'] = format for item in export_extra: if item not in options: raise RuntimeError("ID '{}' not valid for argument 'export_extra'".format(item)) key = 'save{}{}'.format(item[0].upper(), item[1:]) tc.parameters[key] = True ############################################ ############################################ # select DEM type # print('-- configuring DEM') dempar = {'externalDEMFile': externalDEMFile, 'externalDEMApplyEGM': externalDEMApplyEGM} if externalDEMFile is not None: if os.path.isfile(externalDEMFile): if externalDEMNoDataValue is None: with Raster(externalDEMFile) as dem: dempar['externalDEMNoDataValue'] = dem.nodata if dempar['externalDEMNoDataValue'] is None: raise RuntimeError('Cannot read NoData value from DEM file. ' 'Please specify externalDEMNoDataValue') else: dempar['externalDEMNoDataValue'] = externalDEMNoDataValue dempar['reGridMethod'] = False else: raise RuntimeError('specified externalDEMFile does not exist') dempar['demName'] = 'External DEM' else: # SRTM 1arcsec is only available between -58 and +60 degrees. # If the scene exceeds those latitudes SRTM 3arcsec is selected. if id.getCorners()['ymax'] > 60 or id.getCorners()['ymin'] < -58: dempar['demName'] = 'SRTM 3Sec' else: dempar['demName'] = 'SRTM 1Sec HGT' dempar['externalDEMFile'] = None dempar['externalDEMNoDataValue'] = 0 for key, value in dempar.items(): workflow.set_par(key, value) ############################################ ############################################ # configure the resampling methods options = ['NEAREST_NEIGHBOUR', 'BILINEAR_INTERPOLATION', 'CUBIC_CONVOLUTION', 'BISINC_5_POINT_INTERPOLATION', 'BISINC_11_POINT_INTERPOLATION', 'BISINC_21_POINT_INTERPOLATION', 'BICUBIC_INTERPOLATION'] message = '{0} must be one of the following:\n- {1}' if demResamplingMethod not in options: raise ValueError(message.format('demResamplingMethod', '\n- '.join(options))) if imgResamplingMethod not in options: raise ValueError(message.format('imgResamplingMethod', '\n- '.join(options))) workflow.set_par('demResamplingMethod', demResamplingMethod) workflow.set_par('imgResamplingMethod', imgResamplingMethod) ############################################ ############################################ # write workflow to file and optionally execute it # print('- writing workflow to file') workflow.write(outname + '_proc') # execute the newly written workflow if not test: try: groups = groupbyWorkers(outname + '_proc.xml', groupsize) gpt(outname + '_proc.xml', groups=groups, cleanup=cleanup, gpt_exceptions=gpt_exceptions) except RuntimeError as e: if cleanup: if os.path.isdir(outname): shutil.rmtree(outname) elif os.path.isfile(outname): os.remove(outname) os.remove(outname + '_proc.xml') with open(outname + '_error.log', 'w') as log: log.write(str(e)) if returnWF: return outname + '_proc.xml'
def geocode(infile, outdir, t_srs=4326, tr=20, polarizations='all', shapefile=None, scaling='dB', geocoding_type='Range-Doppler', removeS1BoderNoise=True, offset=None, externalDEMFile=None, externalDEMNoDataValue=None, externalDEMApplyEGM=True, basename_extensions=None, test=False): """ wrapper function for geocoding SAR images using ESA SNAP Parameters ---------- infile: str or ~pyroSAR.drivers.ID the SAR scene to be processed outdir: str The directory to write the final files to. 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: int or float, optional The target resolution in meters. Default is 20 polarizations: list or {'VV', 'HH', 'VH', 'HV', 'all'}, optional The polarizations to be processed; can be a string for a single polarization e.g. 'VV' or a list of several polarizations e.g. ['VV', 'VH']. Default is 'all'. shapefile: str or :py:class:`~spatialist.vector.Vector`, optional A vector geometry for subsetting the SAR scene to a test site. Default is None. scaling: {'dB', 'db', 'linear'}, optional Should the output be in linear or decibel scaling? Default is 'dB'. geocoding_type: {'Range-Doppler', 'SAR simulation cross correlation'}, optional The type of geocoding applied; can be either 'Range-Doppler' (default) or 'SAR simulation cross correlation' removeS1BoderNoise: bool, optional Enables removal of S1 GRD border noise (default). offset: tuple, optional A tuple defining offsets for left, right, top and bottom in pixels, e.g. (100, 100, 0, 0); this variable is overridden if a shapefile is defined. Default is None. externalDEMFile: str or None, optional The absolute path to an external DEM file. Default is None. externalDEMNoDataValue: int, float or None, optional The no data value of the external DEM. If not specified (default) the function will try to read it from the specified external DEM. externalDEMApplyEGM: bool, optional Apply Earth Gravitational Model to external DEM? Default is True. basename_extensions: list of str names of additional parameters to append to the basename, e.g. ['orbitNumber_rel'] test: bool, optional If set to True the workflow xml file is only written and not executed. Default is False. Note ---- If only one polarization is selected the results are directly written to GeoTiff. Otherwise the results are first written to a folder containing ENVI files and then transformed to GeoTiff files (one for each polarization). If GeoTiff would directly be selected as output format for multiple polarizations then a multilayer GeoTiff is written by SNAP which is considered an unfavorable format See Also -------- :class:`pyroSAR.drivers.ID`, :class:`spatialist.vector.Vector`, :func:`spatialist.auxil.crsConvert()` """ id = infile if isinstance(infile, pyroSAR.ID) else pyroSAR.identify(infile) if id.is_processed(outdir): print('scene {} already processed'.format(id.outname_base())) return # print(os.path.basename(id.scene)) if not os.path.isdir(outdir): os.makedirs(outdir) ############################################ # general setup if id.sensor in ['ASAR', 'ERS1', 'ERS2']: formatName = 'ENVISAT' elif id.sensor in ['S1A', 'S1B']: if id.product == 'SLC': raise RuntimeError('Sentinel-1 SLC data is not supported yet') formatName = 'SENTINEL-1' else: raise RuntimeError('sensor not supported (yet)') ###################### # print('- assessing polarization selection') if isinstance(polarizations, str): if polarizations == 'all': polarizations = id.polarizations else: if polarizations in id.polarizations: polarizations = [polarizations] else: raise RuntimeError('polarization {} does not exists in the source product'.format(polarizations)) elif isinstance(polarizations, list): polarizations = [x for x in polarizations if x in id.polarizations] else: raise RuntimeError('polarizations must be of type str or list') format = 'GeoTiff-BigTIFF' if len(polarizations) == 1 else 'ENVI' # print(polarizations) # print(format) bands_int = ','.join(['Intensity_' + x for x in polarizations]) bands_beta = ','.join(['Beta0_' + x for x in polarizations]) bands_gamma = ','.join(['Gamma0_' + x for x in polarizations]) ############################################ ############################################ # parse base workflow # print('- parsing base workflow') workflow = parse_recipe('geocode') ############################################ # Read node configuration # print('-- configuring Read Node') read = workflow.find('.//node[@id="Read"]') read.find('.//parameters/file').text = id.scene read.find('.//parameters/formatName').text = formatName ############################################ # Remove-GRD-Border-Noise node configuration # print('-- configuring Remove-GRD-Border-Noise Node') if id.sensor in ['S1A', 'S1B'] and removeS1BoderNoise: insert_node(workflow, parse_node('Remove-GRD-Border-Noise'), before='Read') bn = workflow.find('.//node[@id="Remove-GRD-Border-Noise"]') bn.find('.//parameters/selectedPolarisations').text = ','.join(polarizations) ############################################ # orbit file application node configuration # print('-- configuring Apply-Orbit-File Node') orbit_lookup = {'ENVISAT': 'DELFT Precise (ENVISAT, ERS1&2) (Auto Download)', 'SENTINEL-1': 'Sentinel Precise (Auto Download)'} orbitType = orbit_lookup[formatName] if formatName == 'ENVISAT' and id.acquisition_mode == 'WSM': orbitType = 'DORIS Precise VOR (ENVISAT) (Auto Download)' orb = workflow.find('.//node[@id="Apply-Orbit-File"]') orb.find('.//parameters/orbitType').text = orbitType ############################################ # calibration node configuration # print('-- configuring Calibration Node') cal = workflow.find('.//node[@id="Calibration"]') cal.find('.//parameters/selectedPolarisations').text = ','.join(polarizations) cal.find('.//parameters/sourceBands').text = bands_int ############################################ # terrain flattening node configuration # print('-- configuring Terrain-Flattening Node') tf = workflow.find('.//node[@id="Terrain-Flattening"]') if id.sensor in ['ERS1', 'ERS2'] or (id.sensor == 'ASAR' and id.acquisition_mode != 'APP'): tf.find('.//parameters/sourceBands').text = 'Beta0' else: tf.find('.//parameters/sourceBands').text = bands_beta ############################################ # configuration of node sequence for specific geocoding approaches # print('-- configuring geocoding approach Nodes') if geocoding_type == 'Range-Doppler': insert_node(workflow, parse_node('Terrain-Correction'), before='Terrain-Flattening') tc = workflow.find('.//node[@id="Terrain-Correction"]') tc.find('.//parameters/sourceBands').text = bands_gamma elif geocoding_type == 'SAR simulation cross correlation': insert_node(workflow, parse_node('SAR-Simulation'), before='Terrain-Flattening') insert_node(workflow, parse_node('Cross-Correlation'), before='SAR-Simulation') insert_node(workflow, parse_node('SARSim-Terrain-Correction'), before='Cross-Correlation') tc = workflow.find('.//node[@id="SARSim-Terrain-Correction"]') sarsim = workflow.find('.//node[@id="SAR-Simulation"]') sarsim.find('.//parameters/sourceBands').text = bands_gamma else: raise RuntimeError('geocode_type not recognized') ############################################ # specify spatial resolution and coordinate reference system of the output dataset # print('-- configuring CRS') tc.find('.//parameters/pixelSpacingInMeter').text = str(tr) try: t_srs = crsConvert(t_srs, 'epsg') except TypeError: raise RuntimeError("format of parameter 't_srs' not recognized") # the EPSG code 4326 is not supported by SNAP and thus the WKT string has to be defined; # in all other cases defining EPSG:{code} will do if t_srs == 4326: t_srs = 'GEOGCS["WGS84(DD)",' \ 'DATUM["WGS84",' \ 'SPHEROID["WGS84", 6378137.0, 298.257223563]],' \ 'PRIMEM["Greenwich", 0.0],' \ 'UNIT["degree", 0.017453292519943295],' \ 'AXIS["Geodetic longitude", EAST],' \ 'AXIS["Geodetic latitude", NORTH]]' else: t_srs = 'EPSG:{}'.format(t_srs) tc.find('.//parameters/mapProjection').text = t_srs ############################################ # (optionally) add node for conversion from linear to db scaling # print('-- configuring LinearToFromdB Node') if scaling not in ['dB', 'db', 'linear']: raise RuntimeError('scaling must be a string of either "dB", "db" or "linear"') if scaling in ['dB', 'db']: lin2db = parse_node('lin2db') sourceNode = 'Terrain-Correction' if geocoding_type == 'Range-Doppler' else 'SARSim-Terrain-Correction' insert_node(workflow, lin2db, before=sourceNode) lin2db = workflow.find('.//node[@id="LinearToFromdB"]') lin2db.find('.//parameters/sourceBands').text = bands_gamma ############################################ # (optionally) add subset node and add bounding box coordinates of defined shapefile # print('-- configuring Subset Node') if shapefile: # print('--- read') shp = shapefile if isinstance(shapefile, Vector) else Vector(shapefile) # reproject the geometry to WGS 84 latlon # print('--- reproject') shp.reproject(4326) ext = shp.extent # add an extra buffer of 0.01 degrees buffer = 0.01 ext['xmin'] -= buffer ext['ymin'] -= buffer ext['xmax'] += buffer ext['ymax'] += buffer #print('--- create bbox') with bbox(ext, shp.srs) as bounds: # print('--- intersect') print(shapefile.srs) inter = intersect(id.bbox(), bounds) if not inter: raise RuntimeError('no bounding box intersection between shapefile and scene') # print('--- close intersect') inter.close() # print('--- get wkt') wkt = bounds.convert2wkt()[0] if isinstance(shapefile, str): shp.close() # print('--- parse XML node') subset = parse_node('Subset') # print('--- insert node') insert_node(workflow, subset, before='Read') subset = workflow.find('.//node[@id="Subset"]') subset.find('.//parameters/region').text = ','.join(map(str, [0, 0, id.samples, id.lines])) subset.find('.//parameters/geoRegion').text = wkt ############################################ # (optionally) configure subset node for pixel offsets if offset and not shapefile: subset = parse_node('Subset') insert_node(workflow, subset, before='Read') # left, right, top and bottom offset in pixels l, r, t, b = offset subset = workflow.find('.//node[@id="Subset"]') subset_values = ','.join(map(str, [l, t, id.samples - l - r, id.lines - t - b])) subset.find('.//parameters/region').text = subset_values subset.find('.//parameters/geoRegion').text = '' ############################################ # parametrize write node # print('-- configuring Write Node') # create a suffix for the output file to identify processing steps performed in the workflow suffix = parse_suffix(workflow) basename = os.path.join(outdir, id.outname_base(basename_extensions)) extension = suffix if format == 'ENVI' else polarizations[0] + '_' + suffix outname = basename + '_' + extension write = workflow.find('.//node[@id="Write"]') write.find('.//parameters/file').text = outname write.find('.//parameters/formatName').text = format ############################################ ############################################ # select DEM type # print('-- configuring DEM') if externalDEMFile is not None: if os.path.isfile(externalDEMFile): if externalDEMNoDataValue is None: with Raster(externalDEMFile) as dem: externalDEMNoDataValue = dem.nodata if externalDEMNoDataValue is None: raise RuntimeError('Cannot read NoData value from DEM file. ' 'Please specify externalDEMNoDataValue') else: raise RuntimeError('specified externalDEMFile does not exist') demname = 'External DEM' else: # SRTM 1arcsec is only available between -58 and +60 degrees. # If the scene exceeds those latitudes SRTM 3arcsec is selected. if id.getCorners()['ymax'] > 60 or id.getCorners()['ymin'] < -58: demname = 'SRTM 3Sec' else: demname = 'SRTM 1Sec HGT' externalDEMFile = None externalDEMNoDataValue = 0 for demName in workflow.findall('.//parameters/demName'): demName.text = demname for externalDEM in workflow.findall('.//parameters/externalDEMFile'): externalDEM.text = externalDEMFile for demNodata in workflow.findall('.//parameters/externalDEMNoDataValue'): demNodata.text = str(externalDEMNoDataValue) for egm in workflow.findall('.//parameters/externalDEMApplyEGM'): egm.text = str(externalDEMApplyEGM).lower() ############################################ ############################################ # write workflow to file and optionally execute it # print('- writing workflow to file') write_recipe(workflow, outname + '_proc') # execute the newly written workflow if not test: try: gpt(outname + '_proc.xml') except RuntimeError: os.remove(outname + '_proc.xml')
def test_stack(tmpdir, testdata): name = testdata['tif'] outname = os.path.join(str(tmpdir), 'test') tr = (30, 30) # no input files provided with pytest.raises(RuntimeError): stack(srcfiles=[], resampling='near', targetres=tr, srcnodata=-99, dstnodata=-99, dstfile=outname) # two files, but only one layer name with pytest.raises(RuntimeError): stack(srcfiles=[name, name], resampling='near', targetres=tr, srcnodata=-99, dstnodata=-99, dstfile=outname, layernames=['a']) # targetres must be a two-entry tuple/list with pytest.raises(RuntimeError): stack(srcfiles=[name, name], resampling='near', targetres=30, srcnodata=-99, dstnodata=-99, dstfile=outname) # only one file specified with pytest.raises(RuntimeError): stack(srcfiles=[name], resampling='near', targetres=tr, overwrite=True, srcnodata=-99, dstnodata=-99, dstfile=outname) # targetres must contain two values with pytest.raises(RuntimeError): stack(srcfiles=[name, name], resampling='near', targetres=(30, 30, 30), srcnodata=-99, dstnodata=-99, dstfile=outname) # unknown resampling method with pytest.raises(RuntimeError): stack(srcfiles=[name, name], resampling='foobar', targetres=tr, srcnodata=-99, dstnodata=-99, dstfile=outname) # non-existing files with pytest.raises(RuntimeError): stack(srcfiles=['foo', 'bar'], resampling='near', targetres=tr, srcnodata=-99, dstnodata=-99, dstfile=outname) # create a multi-band stack stack(srcfiles=[name, name], resampling='near', targetres=tr, overwrite=True, srcnodata=-99, dstnodata=-99, dstfile=outname, layernames=['test1', 'test2']) with Raster(outname) as ras: assert ras.bands == 2 # Raster.rescale currently only supports one band with pytest.raises(ValueError): ras.rescale(lambda x: x * 10) # outname exists and overwrite is False with pytest.raises(RuntimeError): stack(srcfiles=[name, name], resampling='near', targetres=tr, overwrite=False, srcnodata=-99, dstnodata=-99, dstfile=outname, layernames=['test1', 'test2']) # pass shapefile outname = os.path.join(str(tmpdir), 'test2') with Raster(name).bbox() as box: stack(srcfiles=[name, name], resampling='near', targetres=tr, overwrite=True, srcnodata=-99, dstnodata=-99, dstfile=outname, shapefile=box, layernames=['test1', 'test2']) with Raster(outname) as ras: assert ras.bands == 2 # pass shapefile and do mosaicing outname = os.path.join(str(tmpdir), 'test3') with Raster(name).bbox() as box: stack(srcfiles=[[name, name]], resampling='near', targetres=tr, overwrite=True, srcnodata=-99, dstnodata=-99, dstfile=outname, shapefile=box) with Raster(outname + '.tif') as ras: assert ras.bands == 1 assert ras.format == 'GTiff' # projection mismatch name2 = os.path.join(str(tmpdir), os.path.basename(name)) outname = os.path.join(str(tmpdir), 'test4') gdalwarp(name, name2, options={'dstSRS': crsConvert(4326, 'wkt')}) with pytest.raises(RuntimeError): stack(srcfiles=[name, name2], resampling='near', targetres=tr, overwrite=True, srcnodata=-99, dstnodata=-99, dstfile=outname) # no projection found outname = os.path.join(str(tmpdir), 'test5') gdal_translate(name, name2, {'options': ['-co', 'PROFILE=BASELINE']}) with Raster(name2) as ras: print(ras.projection) with pytest.raises(RuntimeError): stack(srcfiles=[name2, name2], resampling='near', targetres=tr, overwrite=True, srcnodata=-99, dstnodata=-99, dstfile=outname) # create separate GeoTiffs outdir = os.path.join(str(tmpdir), 'subdir') stack(srcfiles=[name, name], resampling='near', targetres=tr, overwrite=True, layernames=['test1', 'test2'], srcnodata=-99, dstnodata=-99, dstfile=outdir, separate=True, compress=True) # repeat with overwrite disabled (no error raised, just a print message) stack(srcfiles=[name, name], resampling='near', targetres=tr, overwrite=False, layernames=['test1', 'test2'], srcnodata=-99, dstnodata=-99, dstfile=outdir, separate=True, compress=True) # repeat without layernames but sortfun # bandnames not unique outdir = os.path.join(str(tmpdir), 'subdir2') with pytest.raises(RuntimeError): stack(srcfiles=[name, name], resampling='near', targetres=tr, overwrite=True, sortfun=os.path.basename, srcnodata=-99, dstnodata=-99, dstfile=outdir, separate=True, compress=True) # repeat without layernames but sortfun name2 = os.path.join(str(tmpdir), os.path.basename(name).replace('VV', 'XX')) shutil.copyfile(name, name2) outdir = os.path.join(str(tmpdir), 'subdir2') stack(srcfiles=[name, name2], resampling='near', targetres=tr, overwrite=True, sortfun=os.path.basename, srcnodata=-99, dstnodata=-99, dstfile=outdir, separate=True, compress=True) # shapefile filtering outdir = os.path.join(str(tmpdir), 'subdir3') files = [testdata['tif'], testdata['tif2'], testdata['tif3']] with Raster(files[0]).bbox() as box: stack(srcfiles=files, resampling='near', targetres=(30, 30), overwrite=False, layernames=['test1', 'test2', 'test3'], srcnodata=-99, dstnodata=-99, dstfile=outdir, separate=True, compress=True, shapefile=box) # repeated run with different scene selection and only one scene after spatial filtering stack(srcfiles=files[1:], resampling='near', targetres=(30, 30), overwrite=True, layernames=['test2', 'test3'], srcnodata=-99, dstnodata=-99, dstfile=outdir, separate=True, compress=True, shapefile=box)
def inc_stack(small, gamma, snap, outdir, prefix=''): outnames_base = ['small', 'gamma', 'snap'] outnames = [ os.path.join(outdir, prefix + x) + '.tif' for x in outnames_base ] if all([os.path.isfile(x) for x in outnames]): return outnames # set SMALL product nodata GeoTiff value with Raster(small)[0:100, 0:100] as ras: if ras.nodata is None: print('setting nodata value for SMALL product') mat = ras.matrix() nodata = float(mat[0, 0]) ras2 = gdal.Open(small, GA_Update) ras2.GetRasterBand(1).SetNoDataValue(nodata) ras2 = None tmpdir = os.path.join(outdir, 'tmp') if not os.path.isdir(tmpdir): os.makedirs(tmpdir) small_edit = os.path.join( tmpdir, os.path.basename(small).replace('.tif', '_edit.tif')) if not os.path.isfile(small_edit): print('reducing resolution of SMALL product') gdal_translate(small, small_edit, options={ 'xRes': 90, 'yRes': 90, 'resampleAlg': 'average', 'format': 'GTiff' }) # subtract 90 degrees from SMALL product small_out = outnames[0] if not os.path.isfile(small_out): print('subtracting 90 degrees from SMALL product') with Raster(small_edit) as ras: mat = ras.matrix() - 90 ras.assign(mat, 0) print('creating {}'.format(small_out)) ras.write(small_out, format='GTiff', nodata=-99) # set GAMMA product nodata value with Raster(gamma) as ras: if ras.nodata != 0: print('setting nodata value of GAMMA product') ras2 = gdal.Open(gamma, GA_Update) ras2.GetRasterBand(1).SetNoDataValue(0) ras2 = None # convert GAMMA product from radians to degrees gamma_deg = os.path.join( tmpdir, os.path.basename(gamma).replace('.tif', '_deg.tif')) if not os.path.isfile(gamma_deg): print('converting GAMMA product from radians to degrees') with Raster(gamma) as ras: mat = np.rad2deg(ras.matrix()) ras.assign(mat, 0) ras.write(gamma_deg, format='GTiff') gamma = gamma_deg # use extent of SMALL product as reference ext = Raster(small_out).bbox().extent # create new directory for the stacked files if not os.path.isdir(outdir): os.makedirs(outdir) # warp the products to their common extent warp_opts = { 'options': ['-q'], 'format': 'GTiff', 'multithread': True, 'outputBounds': (ext['xmin'], ext['ymin'], ext['xmax'], ext['ymax']), 'dstNodata': -99, 'xRes': 90, 'yRes': 90, 'resampleAlg': 'bilinear', 'dstSRS': 'EPSG:32632' } for i, item in enumerate([gamma, snap]): outfile = outnames[i + 1] if not os.path.isfile(outfile): print('creating {}'.format(outfile)) gdalwarp(src=item, dst=outfile, options=warp_opts) shutil.rmtree(tmpdir) return outnames
def dem_create(src, dst, t_srs=None, tr=None, resampling_method='bilinear', geoid_convert=False, geoid='EGM96'): """ create a new DEM GeoTiff file and optionally convert heights from geoid to ellipsoid Parameters ---------- src: str the input dataset, e.g. a VRT from function :func:`dem_autoload` dst: str the output dataset t_srs: None, 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 (None): use the crs of ``src``. tr: None or tuple the target resolution as (xres, yres) resampling_method: str the gdalwarp resampling method; See `here <https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r>`_ for options. geoid_convert: bool convert geoid heights? geoid: str the geoid model to be corrected, only used if ``geoid_convert == True``; current options: * 'EGM96' Returns ------- """ with Raster(src) as ras: nodata = ras.nodata epsg_in = ras.epsg if t_srs is None: epsg_out = epsg_in else: epsg_out = crsConvert(t_srs, 'epsg') gdalwarp_args = { 'format': 'GTiff', 'multithread': True, 'srcNodata': nodata, 'dstNodata': nodata, 'srcSRS': 'EPSG:{}'.format(epsg_in), 'dstSRS': 'EPSG:{}'.format(epsg_out), 'resampleAlg': resampling_method } if tr is not None: gdalwarp_args.update({'xRes': tr[0], 'yRes': tr[1]}) if geoid_convert: if gdal.__version__ < '2.2': raise RuntimeError('geoid conversion requires GDAL >= 2.2;' 'see documentation of gdalwarp') if geoid == 'EGM96': gdalwarp_args['srcSRS'] += '+5773' else: raise RuntimeError('geoid model not yet supported') try: message = 'creating mosaic' crs = gdalwarp_args['dstSRS'] if crs != 'EPSG:4326': message += ' and reprojecting to {}'.format(crs) print(message) gdalwarp(src, dst, gdalwarp_args) except RuntimeError as e: if os.path.isfile(dst): os.remove(dst) errstr = str(e) if 'Cannot open egm96_15.gtx' in errstr: addition = '\nplease refer to the following site for instructions ' \ 'on how to use the file egm96_15.gtx (requires proj.4 >= 5.0.0):\n' \ 'https://gis.stackexchange.com/questions/258532/' \ 'noaa-vdatum-gdal-variable-paths-for-linux-ubuntu' raise RuntimeError(errstr + addition) else: raise e
def test_Raster(tmpdir, testdata): with pytest.raises(RuntimeError): with Raster(1) as ras: print(ras) with Raster(testdata['tif']) as ras: print(ras) assert ras.bands == 1 assert ras.proj4.strip( ) == '+proj=utm +zone=31 +datum=WGS84 +units=m +no_defs' assert ras.cols == 268 assert ras.rows == 217 assert ras.dim == (217, 268, 1) assert ras.dtype == 'Float32' assert ras.epsg == 32631 assert ras.format == 'GTiff' assert ras.geo == { 'ymax': 4830114.70107, 'rotation_y': 0.0, 'rotation_x': 0.0, 'xmax': 625408.241204, 'xres': 20.0, 'xmin': 620048.241204, 'ymin': 4825774.70107, 'yres': -20.0 } assert ras.geogcs == 'WGS 84' assert ras.is_valid() is True assert ras.proj4args == { 'units': 'm', 'no_defs': None, 'datum': 'WGS84', 'proj': 'utm', 'zone': '31' } assert ras.allstats() == [{ 'min': -26.65471076965332, 'max': 1.4325850009918213, 'mean': -12.124929534450377, 'sdev': 4.738273594738293 }] assert ras.bbox().getArea() == 23262400.0 assert len(ras.layers()) == 1 assert ras.projcs == 'WGS 84 / UTM zone 31N' assert ras.res == (20.0, 20.0) # test writing a subset with no original data in memory outname = os.path.join(str(tmpdir), 'test_sub.tif') with ras[0:200, 0:100] as sub: sub.write(outname, format='GTiff') with Raster(outname) as ras2: assert ras2.cols == 100 assert ras2.rows == 200 ras.load() mat = ras.matrix() assert isinstance(mat, np.ndarray) ras.assign(mat, band=0) # ras.reduce() ras.rescale(lambda x: 10 * x) # test writing data with original data in memory ras.write(os.path.join(str(tmpdir), 'test'), format='GTiff', compress_tif=True) with pytest.raises(RuntimeError): ras.write(os.path.join(str(tmpdir), 'test.tif'), format='GTiff')