Skip to content

Commit 1939ccc

Browse files
Virginia Brancatovbrancat
authored andcommitted
Add PHASS unwrapper to InSAR workflow (#777)
* Update defaults and schemas to include both phass and icu unwrappers * Include unwrapper choice in InSAR workflow * Include unwrapper preferences in unwrap workflow * Allocate temporary wrapped phase raster * Using gdal pixel function to compute phase raster * Correct rifg variable name to rifg_h5 * Specifying that crossmul_path is required for stand-alone phase unwrapping usage * Resolve issue with merging develop (misplaced connected component) * Remove extra space, rename unwrap write attribute function, move code out of if statement * Improving comments on good_correlation * Update description of coherence in insar schema/defaults Co-authored-by: vbrancat <[email protected]>
1 parent 63bf31a commit 1939ccc

File tree

5 files changed

+215
-88
lines changed

5 files changed

+215
-88
lines changed

python/packages/pybind_nisar/workflows/insar_runconfig.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,10 @@ def yaml_check(self):
5555
else:
5656
self.cfg['processing']['crossmul']['flatten'] = None
5757

58-
# set to empty dict and default unwrap values will be used
59-
# if phase_unwrap fields not in yaml
60-
if 'phase_unwrap' not in self.cfg['processing']:
61-
self.cfg['processing']['phase_unwrap'] = {}
62-
63-
# if phase_unwrap fields not in yaml
64-
if self.cfg['processing']['phase_unwrap'] is None:
65-
self.cfg['processing']['phase_unwrap'] = {}
58+
# Create default unwrap cfg dict depending on unwrapping algorithm
59+
algorithm = self.cfg['processing']['phase_unwrap']['algorithm']
60+
if algorithm not in self.cfg['processing']['phase_unwrap']:
61+
self.cfg['processing']['phase_unwrap'][algorithm]={}
6662

6763
if 'interp_method' not in self.cfg['processing']['geocode']:
6864
self.cfg['processing']['geocode']['interp_method'] = 'BILINEAR'

python/packages/pybind_nisar/workflows/unwrap.py

Lines changed: 149 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
import h5py
1111
import journal
12+
import os
1213
import pybind_isce3 as isce3
14+
from osgeo import gdal
15+
import numpy as np
1316
from pybind_nisar.workflows import h5_prep
1417
from pybind_nisar.workflows.unwrap_runconfig import UnwrapRunConfig
1518
from pybind_nisar.workflows.yaml_argparse import YamlArgparse
@@ -22,6 +25,7 @@ def run(cfg: dict, input_hdf5: str, output_hdf5: str):
2225

2326
# pull parameters from dictionary
2427
freq_pols = cfg['processing']['input_subset']['list_of_frequencies']
28+
scratch_path = pathlib.Path(cfg['ProductPathGroup']['ScratchPath'])
2529
unwrap_args = cfg['processing']['phase_unwrap']
2630

2731
# Create error and info channels
@@ -36,11 +40,21 @@ def run(cfg: dict, input_hdf5: str, output_hdf5: str):
3640
error_channel.log(err_str)
3741
raise ValueError(err_str)
3842

39-
# ICU unwrapping (CPU only)
40-
unwrap_obj = isce3.unwrap.ICU()
43+
# Instantiate correct unwrap object
44+
if unwrap_args['algorithm'] == 'icu':
45+
unwrap_obj = isce3.unwrap.ICU()
46+
elif unwrap_args['algorithm'] == 'phass':
47+
unwrap_obj = isce3.unwrap.Phass()
48+
else:
49+
err_str = "Invalid unwrapping algorithm"
50+
error_channel.log(err_str)
51+
raise ValueError(err_str)
52+
53+
# Depending on unwrapper, set unwrap attributes
4154
unwrap_obj = set_unwrap_attributes(unwrap_obj, unwrap_args)
4255
info_channel.log("Running phase unwrapping with: ")
43-
info_channel = write_unwrap_attributes(unwrap_obj, info_channel)
56+
log_unwrap_attributes(unwrap_obj, info_channel,
57+
unwrap_args['algorithm'])
4458

4559
t_all = time.time()
4660

@@ -54,10 +68,9 @@ def run(cfg: dict, input_hdf5: str, output_hdf5: str):
5468
src_pol_group_path = f'{src_freq_group_path}/interferogram/{pol}'
5569
dst_pol_group_path = f'{dst_freq_group_path}/interferogram/{pol}'
5670

57-
# Prepare igram input raster
71+
# Interferogram filepath
5872
igram_path = f'HDF5:{crossmul_path}:/' \
5973
f'{src_pol_group_path}/wrappedInterferogram'
60-
igram_raster = isce3.io.Raster(igram_path)
6174

6275
# Prepare correlation input raster
6376
corr_path = f'HDF5:{crossmul_path}:/' \
@@ -75,6 +88,54 @@ def run(cfg: dict, input_hdf5: str, output_hdf5: str):
7588
conn_comp_dataset = dst_h5[conn_comp_path]
7689
conn_comp_raster = isce3.io.Raster(f"IH5:::ID={conn_comp_dataset.id.id}".encode("utf-8"),
7790
update=True)
91+
92+
# If unwrapping algorithm is ICU, run it with or without seed
93+
if unwrap_args['algorithm'] == 'icu':
94+
# Allocate interferogram as ISCE3 raster
95+
igram_raster = isce3.io.Raster(igram_path)
96+
if 'seed' in unwrap_args['icu']:
97+
unwrap_obj.unwrap(uigram_raster, conn_comp_raster,
98+
igram_raster,
99+
corr_raster, seed=unwrap_args['seed'])
100+
else:
101+
unwrap_obj.unwrap(uigram_raster, conn_comp_raster,
102+
igram_raster,
103+
corr_raster)
104+
else:
105+
# Unwrapping algorithm is PHASS which requires
106+
# the interferometric phase as input raster
107+
unwrap_scratch = scratch_path / f'unwrap/freq{freq}/{pol}'
108+
unwrap_scratch.mkdir(parents=True, exist_ok=True)
109+
110+
# Using GDAL pixel function to compute a wrapped phase VRT
111+
ds = gdal.Open(igram_path, gdal.GA_ReadOnly)
112+
vrttmpl = f'''
113+
<VRTDataset rasterXSize="{ds.RasterXSize}" rasterYSize="{ds.RasterYSize}">
114+
<VRTRasterBand dataType="Float32" band="1" subClass="VRTDerivedRasterBand">
115+
<Description>Phase</Description>
116+
<PixelFunctionType>phase</PixelFunctionType>
117+
<SimpleSource>
118+
<SourceFilename>{igram_path}</SourceFilename>
119+
</SimpleSource>
120+
</VRTRasterBand>
121+
</VRTDataset>'''
122+
ds = None
123+
with open(os.path.join(unwrap_scratch, 'wrapped_phase.vrt'),
124+
'w') as fid:
125+
fid.write(vrttmpl)
126+
127+
# Open phase_raster as ISCE3 raster
128+
phase_raster = isce3.io.Raster(os.path.join(unwrap_scratch, 'wrapped_phase.vrt'))
129+
130+
# Check if power raster has been allocated
131+
if 'power' in unwrap_args['phass']:
132+
power_raster = isce3.io.Raster(unwrap_args['phass']['power'])
133+
unwrap_obj.unwrap(phase_raster, power_raster,
134+
corr_raster, uigram_raster, conn_comp_raster)
135+
else:
136+
unwrap_obj.unwrap(phase_raster, corr_raster,
137+
uigram_raster, conn_comp_raster)
138+
78139
if 'seed' in unwrap_args:
79140
unwrap_obj.unwrap(uigram_raster, conn_comp_raster, igram_raster,
80141
corr_raster, seed=unwrap_args['seed'])
@@ -85,8 +146,7 @@ def run(cfg: dict, input_hdf5: str, output_hdf5: str):
85146
# Copy coherence magnitude and culled offsets to RIFG
86147
dataset_names = ['coherenceMagnitude', 'alongTrackOffset',
87148
'slantRangeOffset']
88-
group_names =['interferogram', 'pixelOffsets',
89-
'pixelOffsets']
149+
group_names = ['interferogram', 'pixelOffsets', 'pixelOffsets']
90150
for dataset_name, group_name in zip(dataset_names, group_names):
91151
dst_path = f'{dst_freq_group_path}/{group_name}/{pol}/{dataset_name}'
92152
src_path = f'{src_freq_group_path}/{group_name}/{pol}/{dataset_name}'
@@ -99,29 +159,36 @@ def run(cfg: dict, input_hdf5: str, output_hdf5: str):
99159
info_channel.log(f"Successfully ran phase unwrapping in {t_all_elapsed:.3f} seconds")
100160

101161

102-
def write_unwrap_attributes(unwrap, info):
162+
def log_unwrap_attributes(unwrap, info, algorithm):
103163
'''
104-
Write unwrap attributes to info channel
164+
Write unwrap attributes to info
165+
channel depending on unwrapping algorithm
105166
'''
106-
info.log(f"Number of buffer lines: {unwrap.buffer_lines}")
107-
info.log(f"Number of overlap lines: {unwrap.overlap_lines}")
108-
info.log(f"Use phase gradient neutron: {unwrap.use_phase_grad_neut}")
109-
info.log(f"Use intensity neutron: {unwrap.use_intensity_neut}")
110-
info.log(f"Phase gradient window size: {unwrap.phase_grad_win_size}")
111-
info.log(f"Phase gradient threshold: {unwrap.neut_phase_grad_thr}")
112-
info.log(f"Neutron intensity threshold: {unwrap.neut_intensity_thr}")
113-
info.log(
114-
f"Maximum intesity correlation threshold: {unwrap.neut_correlation_thr}")
115-
info.log(f"Number of trees: {unwrap.trees_number}")
116-
info.log(f"Maximum branch length: {unwrap.max_branch_length}")
117-
info.log(f"Pixel spacing ratio: {unwrap.ratio_dxdy}")
118-
info.log(f"Initial correlation threshold: {unwrap.init_corr_thr}")
119-
info.log(f"Maximum correlation threshold: {unwrap.max_corr_thr}")
120-
info.log(f"Correlation threshold increments: {unwrap.corr_incr_thr}")
121-
info.log(f"Minimum tile area fraction: {unwrap.min_cc_area}")
122-
info.log(f"Number of bootstraping lines: {unwrap.num_bs_lines}")
123-
info.log(f"Minimum overlapping area: {unwrap.min_overlap_area}")
124-
info.log(f"Phase variance threshold: {unwrap.phase_var_thr}")
167+
info.log(f"Unwrapping algorithm:{algorithm}")
168+
if algorithm == 'icu':
169+
info.log(f"Number of buffer lines: {unwrap.buffer_lines}")
170+
info.log(f"Number of overlap lines: {unwrap.overlap_lines}")
171+
info.log(f"Use phase gradient neutron: {unwrap.use_phase_grad_neut}")
172+
info.log(f"Use intensity neutron: {unwrap.use_intensity_neut}")
173+
info.log(f"Phase gradient window size: {unwrap.phase_grad_win_size}")
174+
info.log(f"Phase gradient threshold: {unwrap.neut_phase_grad_thr}")
175+
info.log(f"Neutron intensity threshold: {unwrap.neut_intensity_thr}")
176+
info.log(f"Maximum intensity correlation "
177+
f"threshold: {unwrap.neut_correlation_thr}")
178+
info.log(f"Number of trees: {unwrap.trees_number}")
179+
info.log(f"Maximum branch length: {unwrap.max_branch_length}")
180+
info.log(f"Pixel spacing ratio: {unwrap.ratio_dxdy}")
181+
info.log(f"Initial correlation threshold: {unwrap.init_corr_thr}")
182+
info.log(f"Maximum correlation threshold: {unwrap.max_corr_thr}")
183+
info.log(f"Correlation threshold increments: {unwrap.corr_incr_thr}")
184+
info.log(f"Minimum tile area fraction: {unwrap.min_cc_area}")
185+
info.log(f"Number of bootstraping lines: {unwrap.num_bs_lines}")
186+
info.log(f"Minimum overlapping area: {unwrap.min_overlap_area}")
187+
info.log(f"Phase variance threshold: {unwrap.phase_var_thr}")
188+
else:
189+
info.log(f"Good correlation: {unwrap.good_correlation}")
190+
info.log(
191+
f"Minimum size of an unwrapped region: {unwrap.min_pixels_region}")
125192

126193
return info
127194

@@ -131,43 +198,60 @@ def set_unwrap_attributes(unwrap, cfg: dict):
131198
Assign user-defined values in cfg to
132199
the unwrap object
133200
'''
201+
error_channel = journal.error('unwrap.set_unwrap_attributes')
134202

135-
if 'buffer_lines' in cfg:
136-
unwrap.buffer_lines = cfg['buffer_lines']
137-
if 'overlap_lines' in cfg:
138-
unwrap.overlap_lines = cfg['overlap_lines']
139-
if 'use_phase_gradient_neutron' in cfg:
140-
unwrap.use_phase_grad_neut = cfg['use_phase_gradient_neutron']
141-
if 'use_intensity_neutron' in cfg:
142-
unwrap.use_intensity_neut = cfg['use_intensity_neutron']
143-
if 'phase_gradient_window_size' in cfg:
144-
unwrap.phase_grad_win_size = cfg['phase_gradient_window_size']
145-
if 'neutron_phase_gradient_threshold' in cfg:
146-
unwrap.neut_phase_grad_thr = cfg['neutron_phase_gradient_threshold']
147-
if 'neutron_intensity_threshold' in cfg:
148-
unwrap.neut_intensity_thr = cfg['neutron_intensity_threshold']
149-
if 'max_intensity_correlation_threshold' in cfg:
150-
unwrap.neut_correlation_thr = cfg['max_intensity_correlation_threshold']
151-
if 'trees_number' in cfg:
152-
unwrap.trees_number = cfg['trees_number']
153-
if 'max_branch_length' in cfg:
154-
unwrap.max_branch_length = cfg['max_branch_length']
155-
if 'pixel_spacing_ratio' in cfg:
156-
unwrap.ratio_dxdy = cfg['pixel_spacing_ratio']
157-
if 'initial_correlation_threshold' in cfg:
158-
unwrap.init_corr_thr = cfg['initial_correlation_threshold']
159-
if 'max_correlation_threshold' in cfg:
160-
unwrap.max_corr_thr = cfg['max_correlation_threshold']
203+
algorithm = cfg['algorithm']
161204
if 'correlation_threshold_increments' in cfg:
162205
unwrap.corr_incr_thr = cfg['correlation_threshold_increments']
163-
if 'min_tile_area' in cfg:
164-
unwrap.min_cc_area = cfg['min_tile_area']
165-
if 'bootstrap_lines' in cfg:
166-
unwrap.num_bs_lines = cfg['bootstrap_lines']
167-
if 'min_overlap_area' in cfg:
168-
unwrap.min_overlap_area = cfg['min_overlap_area']
169-
if 'phase_variance_threshold' in cfg:
170-
unwrap.phase_var_thr = cfg['phase_variance_threshold']
206+
207+
if algorithm == 'icu':
208+
icu_cfg = cfg['icu']
209+
if 'buffer_lines' in icu_cfg:
210+
unwrap.buffer_lines = icu_cfg['buffer_lines']
211+
if 'overlap_lines' in icu_cfg:
212+
unwrap.overlap_lines = icu_cfg['overlap_lines']
213+
if 'use_phase_gradient_neutron' in icu_cfg:
214+
unwrap.use_phase_grad_neut = icu_cfg['use_phase_gradient_neutron']
215+
if 'use_intensity_neutron' in icu_cfg:
216+
unwrap.use_intensity_neut = icu_cfg['use_intensity_neutron']
217+
if 'phase_gradient_window_size' in icu_cfg:
218+
unwrap.phase_grad_win_size = icu_cfg['phase_gradient_window_size']
219+
if 'neutron_phase_gradient_threshold' in icu_cfg:
220+
unwrap.neut_phase_grad_thr = icu_cfg[
221+
'neutron_phase_gradient_threshold']
222+
if 'neutron_intensity_threshold' in icu_cfg:
223+
unwrap.neut_intensity_thr = icu_cfg['neutron_intensity_threshold']
224+
if 'max_intensity_correlation_threshold' in icu_cfg:
225+
unwrap.neut_correlation_thr = icu_cfg[
226+
'max_intensity_correlation_threshold']
227+
if 'trees_number' in icu_cfg:
228+
unwrap.trees_number = icu_cfg['trees_number']
229+
if 'max_branch_length' in icu_cfg:
230+
unwrap.max_branch_length = icu_cfg['max_branch_length']
231+
if 'pixel_spacing_ratio' in icu_cfg:
232+
unwrap.ratio_dxdy = icu_cfg['pixel_spacing_ratio']
233+
if 'initial_correlation_threshold' in icu_cfg:
234+
unwrap.init_corr_thr = icu_cfg['initial_correlation_threshold']
235+
if 'max_correlation_threshold' in icu_cfg:
236+
unwrap.max_corr_thr = icu_cfg['max_correlation_threshold']
237+
if 'min_tile_area' in icu_cfg:
238+
unwrap.min_cc_area = icu_cfg['min_tile_area']
239+
if 'bootstrap_lines' in icu_cfg:
240+
unwrap.num_bs_lines = icu_cfg['bootstrap_lines']
241+
if 'min_overlap_area' in icu_cfg:
242+
unwrap.min_overlap_area = icu_cfg['min_overlap_area']
243+
if 'phase_variance_threshold' in icu_cfg:
244+
unwrap.phase_var_thr = icu_cfg['phase_variance_threshold']
245+
elif algorithm == 'phass':
246+
phass_cfg = cfg['phass']
247+
if 'good_correlation' in phass_cfg:
248+
unwrap.good_correlation = phass_cfg['good_correlation']
249+
if 'min_unwrap_area' in phass_cfg:
250+
unwrap.min_pixels_region = phass_cfg['min_unwrap_area']
251+
else:
252+
err_str = "Not a valid unwrapping algorithm"
253+
error_channel.log(err_str)
254+
raise ValueError(err_str)
171255

172256
return unwrap
173257

@@ -187,13 +271,8 @@ def set_unwrap_attributes(unwrap, cfg: dict):
187271
# Prepare RUNW HDF5
188272
out_paths = h5_prep.run(unwrap_runconfig.cfg)
189273

190-
# out_paths['RIFG'] from h5_prep does not have actual interferogram data.
191-
# Use RIFG path from CLI or YAML
192-
if args.run_config_path is None:
193-
rifg = args.crossmul
194-
else:
195-
rifg = unwrap_runconfig.cfg['processing']['phase_unwrap'][
196-
'crossmul_path']
274+
# Use RIFG from crossmul_path
275+
rifg_h5 = unwrap_runconfig.cfg['processing']['phase_unwrap']['crossmul_path']
197276

198277
# Run phase unwrapping
199-
run(unwrap_runconfig.cfg, rifg, out_paths['RUNW'])
278+
run(unwrap_runconfig.cfg, rifg_h5, out_paths['RUNW'])

python/packages/pybind_nisar/workflows/unwrap_runconfig.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@ def yaml_check(self):
2020
'''
2121
error_channel = journal.error('CrossmulRunConfig.yaml_check')
2222

23-
if self.cfg['processing']['phase_unwrap'] is None:
24-
err_str = "'phase_unwrap' necessary for standalone execution with YAML"
25-
error_channel.log(err_str)
26-
raise ValueError(err_str)
27-
28-
if 'crossmul_path' not in self.cfg['processing']['phase_unwrap']:
23+
# Check if crossmul_path is provided (needed for stand-alone unwrapping)
24+
if self.cfg['processing']['phase_unwrap']['crossmul_path'] is None:
2925
err_str = "'crossmul_path' file path under `phase_unwrap' required for standalone execution with YAML"
3026
error_channel.log(err_str)
3127
raise ValueError(err_str)
3228

29+
# Allocate, if not present, cfg related to user-unwrapper choice
30+
algorithm = self.cfg['processing']['phase_unwrap']['algorithm']
31+
if algorithm not in self.cfg['processing']['phase_unwrap']:
32+
self.cfg['processing']['phase_unwrap'][algorithm] = {}
33+
3334
# Check if crossmul path is a directory or a file
3435
crossmul_path = self.cfg['processing']['phase_unwrap']['crossmul_path']
3536
if not os.path.isfile(crossmul_path):

share/nisar/defaults/insar.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,15 @@ runconfig:
271271
flatten: True
272272
oversample: 2
273273
rows_per_block: 8192
274+
275+
phase_unwrap:
276+
# Path to HDF5 file or directory containing the input interferogram
277+
# and coherence (normalized magnitude of complex correlation) rasters
278+
# Not required as we allow the use of intermediate outputs from
279+
# the previous InSAR module, which is not user-specified.
280+
# Required for running stand-alone phase unwrapping
281+
crossmul_path:
282+
# Increments to correlation threshold
283+
correlation_threshold_increments: 0.1
284+
# Unwrapping algorithm to use
285+
algorithm: icu

0 commit comments

Comments
 (0)