7  Phase Unwrapping

The goal of this notebook is to read an interferogram image (i.e., 2-D array of phase values) and unwrap it. Phase unwrapping is a critical process in interferometry, which involves recovering unambiguous phase data from the interferogram.

A SAR interferogram represents the phase difference between two radar acquisitions (i.e., two SLC images). The phase difference is usually wrapped within a range of 0 to 2π, because the phase is inherently cyclical. When the true phase difference exceeds 2π, it gets “wrapped” into this range, creating a discontinuous phase signal. Phase unwrapping refers to the process of reconstructing the continuous phase field from the wrapped phase data.

Unwrapping an interferogram is essential for extracting correct information contained in the phase such as surface topography and earth surface deformations.

There are many approaches that tried to solve the unwrapping problem, tackling challenging scenarios involving noise or large phase discontinuities. Here we present the Network-flow Algorithm for phase unwrapping (C. W. Chen and H. A. Zebker, 2000), which is implemented in the snaphu package.

7.1 Loading Data

The data is stored on the Jupyterhub server, so we need to load it using their respective paths. In this notebook we will use the resulting wrapped interferogram from notebook “Interferograms”, but we need to process it in the radar geometry in order to unwrap it (while in notebook “Interferograms” we end the whole process by performing the geocoding, just for better visualization purposes).

import cmcrameri as cmc  # noqa: F401
import intake
import numpy as np
import seaborn as sns
import snaphu  # noqa: F401
import xarray as xr

from mrs.catalog import get_intake_url
from mrs.plot import (
    plot_coarsened_image,
    plot_compare_coherence_mask_presence,
    plot_compare_wrapped_unwrapped_completewrapped,
    plot_different_coherence_thresholds,
    plot_displacement_map,
    plot_interferogram_map,
    plot_summary,
)
from mrs.processing import subsetting, unwrap_array
url = get_intake_url()
catalog = intake.open_catalog(url)
ds = catalog.complex_handout.read().compute()
ds["cmplx"] = ds["real"] + ds["imag"] * 1j
https://git.geo.tuwien.ac.at/public_projects/microwave-remote-sensing/-/raw/dev-exs/microwave-remote-sensing.yml
# Set cyclic and linear colormaps
cmap_cyc = sns.color_palette("hls", as_cmap=True)
cmap_lin = "cmc.roma_r"
cmap_disp = "cmc.vik"

# Create a mask for the areas which have no data
mask = ds.phase.where(ds.phase == 0, other=True, drop=False).astype(bool)

Let’s start by displaying the interferogram that needs to be unwrapped. Recall that due to the Slant Range geometry and the satellite acquisition pass (ascending, in our case), the image appears north/south flipped (with respect to the geocoded image)!

# Plot Phase Interferogram Image
plot_interferogram_map(ds=ds, mask=mask, cmap_cyc=cmap_cyc)

7.2 Phase Unwrapping

As we will be doing the unwrapping multiple times in this notebook let’s create a function that does the unwrapping for us on xarray DataArray objects. The actual core function where the unwrapping is happening is snaphu.unwrap_phase from the snaphu package. This function needs a 2D numpy array as input, where each pixel value is a complex number. Therefore, we have to convert the xarray DataArray to a 2D numpy array with complex values. We do that by combining the phase and intensity bands to a complex array. The actual unwrapping is essentially an addition of the phase values, such that the values are continuous and not between \(-\pi\) and \(\pi\).

Figure 1: Illustration of how the unwrapping of the phase works. (Source: ESA).

7.2.1 Unwrapping on a Subset

As the original image is too large to unwrap in a reasonable time, we will unwrap a subset of the image. In this case, we will unwrap an area of 500x500 pixels.

# Select a subset of the data
dx, dy = 500, 500
x0, y0 = 2800, 1700


# Subsetting the data arrays
subset = subsetting(ds.where(mask), x0, y0, dx, dy)

# Unwrap the subset
subset = unwrap_array(subset, complex_var="cmplx", ouput_var="unwrapped")

Now let’s compare the wrapped and unwrapped phase images.

plot_compare_wrapped_unwrapped_completewrapped(
    subset=subset,
    cmap_cyc=cmap_cyc,
    ds=ds,
    mask=mask,
    p0=(x0, y0),
    dxy=(dx, dy),
)

7.2.2 Unwrapping with coherence mask

Additionally, can we try to calculate the unwrapped image by excluding pixels which the coherence values are lower than a certain threshold. This is done by masking the coherence image with the threshold value and then unwrapping the phase image with the masked coherence image.

threshold1 = 0.3

subset_unwrapped_coherence = unwrap_array(
    subset,
    coherence=subset.coh,
    coh_low_threshold=threshold1,
    complex_var="cmplx",
    ouput_var="unwrapped_coh",
)

Let’s compare the unwrapped image with and without the coherence mask.

plot_compare_coherence_mask_presence(
    subset=subset_unwrapped_coherence,
    cmap_cyc=cmap_cyc,
    threshold=threshold1,
)

Let’s see if another threshold value for the coherence mask gives better results.

threshold2 = 0.5
subset_unwrapped_coherence_threshold2 = unwrap_array(
    subset,
    coherence=subset.coh,
    coh_low_threshold=threshold2,
    complex_var="cmplx",
    ouput_var="unwrapped_coh",
)


plot_different_coherence_thresholds(
    ds_coh=subset_unwrapped_coherence,
    ds_coh_2=subset_unwrapped_coherence_threshold2,
    cmap_cyc=cmap_cyc,
)

A higher coherence threshold means that only pixels with a coherence value greater than 0.5 will be used for phase unwrapping. This would result in an unwrapping process that is likely more stable, with reduced noise (invalid phase information in the proximity of the earthquake faults is discarded). However, an excessive coherence threshold might have significant gaps or missing information, especially in areas where motion or surface changes have occurred. The choice of a coherence threshold depends on the balance you want to strike between the accuracy and coverage of the output unwrapped image.

Keep in mind that in case of large displacements, such as the Ridgecrest earthquake, phase unwrapping can be problematic and lead to poor results: when the displacement is large, the phase difference becomes wrapped multiple times, leading to phase aliasing. In this case, the phase values become ambiguous, we cannot distinguish between multiple phase wraps, thus leading to incorrect results.

7.3 Applying an Equation for the Displacement Map

From the unwrapped phase image (we will use the phase masked with a coherence threshold of 0.3) we can calculate the displacement map using the following equation:

$ d = - _d $

where: - \(\lambda = 0.056\) for Sentinel-1 - \(\Delta \phi_d\) is the unwrapped image

This operation can be very useful for monitoring ground deformation.

def displacement(unw: xr.DataArray, lambda_val: float = 0.056) -> xr.DataArray:
    """Compute the displacement given the unwrapped phase."""
    return unw * -lambda_val / (4 * np.pi)


# Calculate the displacement
disp_subset = displacement(subset.unwrapped_coh)
# Plot the displacement map
plot_displacement_map(
    subset=disp_subset,
    cmap_disp=cmap_disp,
    title="Displacement Map of the Subset",
)

7.4 Coarsen Approach

As the whole data is too large and the processing time already exceeds 20 minutes when using an image with 4000x4000 pixels, we can coarsen the image so that we can unwrap and compute the displacement for the whole scene.

kernel_size = 3

lowres = ds.coarsen(x=kernel_size, y=kernel_size, boundary="trim").median()
lowres = unwrap_array(
    lowres,
    tile_overlap=10,
    coherence=lowres.coh,
    coh_low_threshold=0.3,
    ntiles=(20, 30),
)

We can now plot the unwrapped image of the low resolution image.

# Plot the unwrapped phase
plot_coarsened_image(lowres=lowres, cmap_cyc=cmap_cyc)

We can also now calculate the displacement map and compare them.

lowres_disp = displacement(lowres.unwrapped)

# Plot the displacement map
plot_displacement_map(
    subset=lowres_disp,
    cmap_disp=cmap_disp,
    title="Displacement Map entire scene (coarse resolution)",
)

Plot a summary of the previous plots:

# Plot summary of previous plots
plot_summary(
    subset=subset,
    subset_disp=disp_subset,
    lowres=lowres,
    lowres_disp=lowres_disp,
    cmap_cyc=cmap_cyc,
    cmap_disp=cmap_disp,
)

In the following animation, we can capture the 3D displacement caused by the Ridgecrest quake by observing the after and before elevation model.

Credits: NASA