Skip to content

Commit 4c21956

Browse files
Virginia BrancatovbrancatLiang Yu
authored andcommitted
Workflow dense offsets (#733)
* Modify schemas and defaults to include dense_offsets parameters * Modify dense offset parameters according to updated schema in insar_test.yaml * Dense offsets argparse * dense offsets runconfig * dense offset estimation run * Add dense_offsets to InSAR workflow * Start unit test implementation for dense_offsets * Change allocation of dense offsets parameters in h5_prep * Allocate proper dimension for radar coordinates offset products * Correcting typos in naming object attributes * Correct typo in freq_pols assignment * Bypassing HDF5 file issue in assigning reference raster to PyCuAmpcor' * Adding capability of using varying Gross offset file * Completing dense offsets unit test * Add dense_offsets unit test dependency * Avoid insar unit test from running if CUDa is not available * Correct slant range and azimuth spacing for pixelOffsets * Avoid running CPU dense offsets and insar unit tests * Correct pep8 notation, isort imports, remove unused imports * avoid to load reference SLC in memory * Differentiating secondary raster path depending on its gdal or HDF5 nature * Update margin computation when range/azimuth gross offsets are set * Consistently change cross_correlation_algorithm to cross_correlation_domain * Correct error in memory mapping usage flag * Improving parameters description and option choices in dense_offsets_argparse * Correct amplitude to magnitude in dense_offsets_argarse * Add comment for the selection of the dense_offset unit test threshold * Allocate PyCuAmpcor offsets to ENVI rasters + headers * Abide pep8 notation * Remove check on coregistered_slc_path in dense_offsets.py * Rename set_check_offset_attributes to set_optional_attributes * Consistently change raw_oversampling_factor to slc_oversampling_factor * Remove extra space * Removing parenthesis import * Remove gpu enables from dense offset runconfig test * Format insar imports with isort * Format to pep8 notation * Add comment to raster generation in dense_offsets.py * Update comment in dense_offset unit test * Correct raw_ovs with slc_ovs * Add comment to merge_gross_offset option * Change format of reference SLC temp file from GTiff to ENVI * Add extra check for cross-correlation option * Format code to pep8 notation * Correct wrong spacing in h5_prep * Correct dense_offsets and insar unit test for dense_offset execution * Remove CLI option from dense offsets workflow * Replace setattrs with obj property assignment * Replacing if/else statements with ternary operator * Add line wrapping for long code lines * Removing additional check on runconfig in dense_offsets_runconfig * Add loud print statements on dense_offsets unit test * Revert changes to dense_offsets/insar unit test (test for CI system) * use CMake to make dense offset test CUDA only * separate CPU and CUDA insar workflow tests * Correct typo in cuda_insar unit test * Remove unused imports in dense_offsets unit test * Reshape gross offset file before allocating bands * Empty dense_offset test for CI testing * Add more dense_offsets unit test (no run, CI test) * Add dense_offsets.run in dense_offsets unit test (CI test) * dense offset unit test, comment out dense_offsets.run (CI test) * Make dense_offsets unit test dependent on cuda_insar (CI test) * Reintegrate validate dense offsets unit test (CI test) * Add missing imports to dense_offsets unit test * Reorganize and improve comments for dense offsets parameter in insar schema * Add all dense offsets parameters in defaults/insar.yaml for documentation * remove memory mapping flag and buffer size from insar schema/default * Add comment to not required coregistered_slc_path in insar schema * Handle case when coregistered_slc_path is None * Check if cfg dense offsets elements are not None (instead present) * Consider case when offset length/width is allocated by user in h5_prep * Use first available polarization to read SLC lines/columns * Modify margin computation based on range/azimuth chip size and search range * Remove error_channel from dense_offsets_runconfig initialization * Add more comment/context on the use of coregistered_slc_path in dense_offsets * Simplify directory/file check on coregistered_slc_path * Change offset object name to ampcor object * Implement suggested change to scratch path * Rename create_raster to copy_raster and make format a fnction parameter * Modified margin formula and expose format type in copy_raster function * Correct typos in margins definition * Remove ds=None from copy_raster function * Improve comments on reference raster copy * Add comments for output generation assumptions * Remove outlier masking from dense_offsets unit test * Use GDAL instead of isce3.io.Raster to create output Ampcor dataset * Assign value to memory mapping flag (not euser-exposed) * Take abs of gross offsets as in dense_offsets * Format code to pep8 notation * add #!/usr/bin/env python3 to insar.py * Add Ampcor parameter description to defaults/insar.yaml' Co-authored-by: vbrancat <[email protected]> Co-authored-by: Liang Yu <[email protected]>
1 parent 3c6d036 commit 4c21956

File tree

13 files changed

+781
-166
lines changed

13 files changed

+781
-166
lines changed
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
'''
2+
Wrapper for dense offsets
3+
'''
4+
5+
import pathlib
6+
import time
7+
8+
import journal
9+
import numpy as np
10+
import pybind_isce3 as isce3
11+
from osgeo import gdal
12+
from pybind_nisar.products.readers import SLC
13+
from pybind_nisar.workflows import gpu_check, h5_prep
14+
from pybind_nisar.workflows.yaml_argparse import YamlArgparse
15+
from pybind_nisar.workflows.dense_offsets_runconfig import \
16+
DenseOffsetsRunConfig
17+
18+
19+
def run(cfg: dict):
20+
'''
21+
Run dense offsets
22+
'''
23+
24+
# Pull parameters from cfg
25+
ref_hdf5 = cfg['InputFileGroup']['InputFilePath']
26+
sec_hdf5 = cfg['InputFileGroup']['SecondaryFilePath']
27+
scratch_path = pathlib.Path(cfg['ProductPathGroup']['ScratchPath'])
28+
freq_pols = cfg['processing']['input_subset']['list_of_frequencies']
29+
offset_params = cfg['processing']['dense_offsets']
30+
31+
# Initialize parameters shared between frequency A and B
32+
ref_slc = SLC(hdf5file=ref_hdf5)
33+
sec_slc = SLC(hdf5file=sec_hdf5)
34+
35+
# Get coregistered SLC path
36+
coregistered_slc_path = pathlib.Path(offset_params['coregistered_slc_path'])
37+
38+
error_channel = journal.error('dense_offsets.run')
39+
info_channel = journal.info('dense_offsets.run')
40+
info_channel.log('Start dense offsets estimation')
41+
42+
# Check GPU use
43+
use_gpu = gpu_check.use_gpu(cfg['worker']['gpu_enabled'],
44+
cfg['worker']['gpu_id'])
45+
46+
if use_gpu:
47+
# Set current CUDA device
48+
device = isce3.cuda.core.Device(cfg['worker']['gpu_id'])
49+
isce3.cuda.core.set_device(device)
50+
ampcor = isce3.cuda.matchtemplate.PyCuAmpcor()
51+
ampcor.deviceID = cfg['worker']['gpu_id']
52+
# Use memory mapping (not exposed to user but reference
53+
# and secondary raster are memory-mappable)
54+
ampcor.useMmap = 1
55+
else:
56+
err_str = "Currently, ISCE3 supports only GPU dense offsets"
57+
error_channel.log(err_str)
58+
raise NotImplementedError(err_str)
59+
60+
# Looping over frequencies and polarizations
61+
t_all = time.time()
62+
63+
for freq, pol_list in freq_pols.items():
64+
offset_scratch = scratch_path / f'dense_offsets/freq{freq}'
65+
66+
for pol in pol_list:
67+
# Set output directory and output filenames
68+
out_dir = offset_scratch / pol
69+
out_dir.mkdir(parents=True, exist_ok=True)
70+
71+
# Create a memory mappable copy of reference SLC
72+
ref_raster_str = f'HDF5:{ref_hdf5}:/{ref_slc.slcPath(freq, pol)}'
73+
copy_raster(ref_raster_str, str(out_dir / 'reference'), format='ENVI')
74+
ref_raster = isce3.io.Raster(ref_raster_str)
75+
ampcor.referenceImageName = str(out_dir / 'reference')
76+
ampcor.referenceImageHeight = ref_raster.length
77+
ampcor.referenceImageWidth = ref_raster.width
78+
79+
# If running insar.py, a memory mappable second raster has been
80+
# created in the previous step (resample slc). If secondary raster
81+
# is extracted from HDF5 file, needs to be made memory mappable
82+
if coregistered_slc_path.is_file():
83+
sec_raster_str = f'HDF5:{sec_hdf5}:/{sec_slc.slcPath(freq, pol)}'
84+
sec_raster_path = str(out_dir / 'secondary')
85+
copy_raster(sec_raster_str, sec_raster_path,
86+
format='ENVI')
87+
else:
88+
sec_raster_path = str(coregistered_slc_path /
89+
f'resample_slc/freq{freq}/{pol}/coregistered_secondary.slc')
90+
sec_raster = isce3.io.Raster(sec_raster_path)
91+
ampcor.secondaryImageName = sec_raster_path
92+
ampcor.secondaryImageHeight = sec_raster.length
93+
ampcor.secondaryImageWidth = sec_raster.width
94+
95+
# Setup other dense offsets parameters
96+
ampcor = set_optional_attributes(ampcor, offset_params,
97+
ref_raster.length,
98+
ref_raster.width)
99+
# Configure output filenames. It is assumed output are flat binaries
100+
# (e.g. ENVI files)
101+
ampcor.offsetImageName = str(out_dir / 'dense_offsets')
102+
ampcor.grossOffsetImageName = str(out_dir / 'gross_offset')
103+
ampcor.snrImageName = str(out_dir / 'snr')
104+
ampcor.covImageName = str(out_dir / 'covariance')
105+
106+
# Create empty ENVI datasets. PyCuAmpcor will overwrite the
107+
# binary files. Note, use gdal to pass interleave option
108+
create_empty_dataset(str(out_dir / 'dense_offsets'),
109+
ampcor.numberWindowAcross,
110+
ampcor.numberWindowDown, 2, gdal.GDT_Float32)
111+
create_empty_dataset(str(out_dir / 'gross_offsets'),
112+
ampcor.numberWindowAcross,
113+
ampcor.numberWindowDown, 2, gdal.GDT_Float32)
114+
create_empty_dataset(str(out_dir / 'snr'),
115+
ampcor.numberWindowAcross,
116+
ampcor.numberWindowDown, 1, gdal.GDT_Float32)
117+
create_empty_dataset(str(out_dir / 'covariance'),
118+
ampcor.numberWindowAcross,
119+
ampcor.numberWindowDown, 3, gdal.GDT_Float32)
120+
# Run dense offsets
121+
ampcor.runAmpcor()
122+
123+
t_all_elapsed = time.time() - t_all
124+
info_channel.log(
125+
f"Successfully ran dense_offsets in {t_all_elapsed:.3f} seconds")
126+
127+
128+
def set_optional_attributes(ampcor_obj, cfg, length, width):
129+
'''
130+
Set obj attributes to cfg values
131+
Check attributes validity
132+
'''
133+
134+
error_channel = journal.error('dense_offsets.run.set_optional_attribute')
135+
if cfg['window_range'] is not None:
136+
ampcor_obj.windowSizeWidth = cfg['window_range']
137+
138+
if cfg['window_azimuth'] is not None:
139+
ampcor_obj.windowSizeHeight = cfg['window_azimuth']
140+
141+
if cfg['half_search_range'] is not None:
142+
ampcor_obj.halfSearchRangeAcross = cfg['half_search_range']
143+
144+
if cfg['half_search_azimuth'] is not None:
145+
ampcor_obj.halfSearchRangeDown = cfg['half_search_azimuth']
146+
147+
if cfg['skip_range'] is not None:
148+
ampcor_obj.skipSampleAcross = cfg['skip_range']
149+
150+
if cfg['skip_azimuth'] is not None:
151+
ampcor_obj.skipSampleDown = cfg['skip_azimuth']
152+
153+
if cfg['margin'] is not None:
154+
margin = cfg['margin']
155+
else:
156+
margin = 0
157+
158+
# If gross offsets are set update margin
159+
if (cfg['gross_offset_range'] is not None) and (cfg['gross_offset_azimuth'] is not None):
160+
margin = max(margin, np.abs(cfg['gross_offset_range']),
161+
np.abs(cfg['gross_offset_azimuth']))
162+
163+
margin_rg = 2 * margin + 2*ampcor_obj.halfSearchRangeAcross + ampcor_obj.windowSizeWidth
164+
margin_az = 2 * margin + 2*ampcor_obj.halfSearchRangeDown + ampcor_obj.windowSizeHeight
165+
166+
ampcor_obj.referenceStartPixelAcrossStatic = cfg[
167+
'start_pixel_range'] if cfg['start_pixel_range'] is not None \
168+
else margin + ampcor_obj.halfSearchRangeAcross
169+
170+
ampcor_obj.referenceStartPixelDownStatic = cfg[
171+
'start_pixel_azimuth'] if cfg['start_pixel_range'] is not None \
172+
else margin + ampcor_obj.halfSearchRangeDown
173+
174+
if cfg['offset_width'] is not None:
175+
ampcor_obj.numberWindowAcross = cfg['offset_width']
176+
else:
177+
offset_width = (width - margin_rg) // ampcor_obj.skipSampleAcross
178+
ampcor_obj.numberWindowAcross = offset_width
179+
180+
if cfg['offset_length'] is not None:
181+
ampcor_obj.numberWindowDown = cfg['offset_length']
182+
else:
183+
offset_length = (length - margin_az) // ampcor_obj.skipSampleDown
184+
ampcor_obj.numberWindowDown = offset_length
185+
186+
if cfg['cross_correlation_domain'] is not None:
187+
algorithm = cfg['cross_correlation_domain']
188+
if algorithm == 'frequency':
189+
ampcor_obj.algorithm = 0
190+
elif algorithm == 'spatial':
191+
ampcor_obj.algorithm = 1
192+
else:
193+
err_str = f"{algorithm} is not a valid cross-correlation option"
194+
error_channel.log(err_str)
195+
raise ValueError(err_str)
196+
197+
if cfg['slc_oversampling_factor'] is not None:
198+
ampcor_obj.rawDataOversamplingFactor = cfg['slc_oversampling_factor']
199+
200+
if cfg['deramping_method'] is not None:
201+
deramp = cfg['deramping_method']
202+
ampcor_obj.derampMethod = 0 if deramp == "magnitude" else 1
203+
204+
if cfg['correlation_statistics_zoom'] is not None:
205+
ampcor_obj.corrStatWindowSize = cfg['correlation_statistics_zoom']
206+
207+
if cfg['correlation_surface_zoom'] is not None:
208+
ampcor_obj.corrSurfaceZoomInWindow = cfg['correlation_surface_zoom']
209+
210+
if cfg['correlation_surface_oversampling_factor'] is not None:
211+
ampcor_obj.corrSurfaceOverSamplingFactor = cfg[
212+
'correlation_surface_oversampling_factor']
213+
214+
if cfg['correlation_surface_oversampling_method'] is not None:
215+
method = cfg['correlation_surface_oversampling_method']
216+
ampcor_obj.corrSurfaceOverSamplingMethod = 0 if method == "fft" else 1
217+
218+
if cfg['windows_batch_range'] is not None:
219+
ampcor_obj.numberWindowAcrossInChunk = cfg['windows_batch_range']
220+
221+
if cfg['windows_batch_azimuth'] is not None:
222+
ampcor_obj.numberWindowDownInChunk = cfg['windows_batch_azimuth']
223+
224+
if cfg['cuda_streams'] is not None:
225+
ampcor_obj.nStreams = cfg['cuda_streams']
226+
227+
# Setup object parameters
228+
ampcor_obj.setupParams()
229+
if (cfg['use_gross_offsets'] is not None) and (
230+
cfg['gross_offset_range'] is not None) and \
231+
(cfg['gross_offset_azimuth'] is not None):
232+
ampcor_obj.setConstantGrossOffset(cfg['gross_offset_azimuth'],
233+
cfg['gross_offset_range'])
234+
235+
if cfg['gross_offset_filepath'] is not None:
236+
gross_offset = np.fromfile(cfg['gross_offset_filepath'], dtype=np.int32)
237+
windows_number = ampcor_obj.numberWindowAcross * ampcor_obj.numberWindowDown
238+
if gross_offset.size != 2 * windows_number:
239+
err_str = "The input gross offset does not match the offset width*offset length"
240+
error_channel.log(err_str)
241+
raise RuntimeError(err_str)
242+
gross_offset = gross_offset.reshape(window_number, 2)
243+
gross_azimuth = gross_offset[:, 0]
244+
gross_range = gross_offset[:, 1]
245+
ampcor_obj.setVaryingGrossOffset(gross_azimuth, gross_range)
246+
247+
# If True, add constant slant range/azimuth gross offsets to
248+
# estimated dense offsets (will be used to resample slc)
249+
if cfg['merge_gross_offset'] is not None:
250+
ampcor_obj.mergeGrossOffset = 1 if cfg['merge_gross_offset'] else 0
251+
252+
# Check pixel in image range
253+
ampcor_obj.checkPixelInImageRange()
254+
255+
return ampcor_obj
256+
257+
258+
def copy_raster(infile, outfile, format="ENVI"):
259+
ds = gdal.Open(infile, gdal.GA_ReadOnly)
260+
gdal_translate_opts = gdal.TranslateOptions(format=format)
261+
gdal.Translate(outfile, ds, options=gdal_translate_opts)
262+
263+
264+
def create_empty_dataset(filename, width, length,
265+
bands, dtype, interleave="bip", format="ENVI"):
266+
'''
267+
Create empty dataset with user-defined options
268+
'''
269+
driver = gdal.GetDriverByName(format)
270+
ds = driver.Create(filename, xsize=width, ysize=length,
271+
bands=bands, eType=dtype,
272+
options=[f"INTERLEAVE={interleave}"])
273+
274+
275+
if __name__ == "__main__":
276+
'''
277+
Run dense offsets estimation
278+
'''
279+
# Load command line args
280+
dense_offsets_parser = YamlArgparse()
281+
args = dense_offsets_parser.parse()
282+
# Get cfg dict from CLI args
283+
dense_offsets_runconfig = DenseOffsetsRunConfig(args)
284+
# Run dense offsets
285+
run(dense_offsets_runconfig.cfg)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import os
2+
3+
import journal
4+
import pybind_nisar.workflows.helpers as helpers
5+
from pybind_nisar.workflows.runconfig import RunConfig
6+
7+
8+
class DenseOffsetsRunConfig(RunConfig):
9+
10+
def __init__(self, args):
11+
super().__init__(args, 'insar')
12+
13+
if self.args.run_config_path is not None:
14+
self.load_geocode_yaml_to_dict()
15+
self.geocode_common_arg_load()
16+
self.yaml_check()
17+
18+
def yaml_check(self):
19+
'''
20+
Check dense offset specifics from YAML file
21+
'''
22+
23+
error_channel = journal.error('DenseOffsetsRunConfig.yaml_check')
24+
scratch_path = self.cfg['ProductPathGroup']['ScratchPath']
25+
26+
# If coregistered_slc_path is None, assume that we run dense_offsets
27+
# as part of insar.py. In this case, coregistered_slc_path comes
28+
# from the previous processing step via scratch_path
29+
if self.cfg['processing']['dense_offsets']['coregistered_slc_path'] is None:
30+
self.cfg['processing']['dense_offsets']['coregistered_slc_path'] = scratch_path
31+
32+
# Check if coregistered_slc_path is a path or directory
33+
coregistered_slc_path = self.cfg['processing']['dense_offsets']['coregistered_slc_path']
34+
if not os.path.exists(coregistered_slc_path):
35+
err_str = f"{coregistered_slc_path} invalid; must be a file or directory"
36+
error_channel.log(err_str)
37+
raise ValueError(err_str)
38+
39+
# Check if geometry-coregistered rasters
40+
# exists in directory or HDF5 file
41+
freq_pols = self.cfg['processing']['input_subset'][
42+
'list_of_frequencies']
43+
frequencies = freq_pols.keys()
44+
45+
if os.path.isdir(coregistered_slc_path):
46+
helpers.check_mode_directory_tree(coregistered_slc_path,
47+
'resample_slc',
48+
frequencies, freq_pols)
49+
else:
50+
helpers.check_hdf5_freq_pols(coregistered_slc_path, freq_pols)

0 commit comments

Comments
 (0)