|
| 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) |
0 commit comments