Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7115a57
add interpolation option for unwrapping
Mar 12, 2024
1012e7c
add reference, convert float weights once
scottstanie Mar 13, 2024
9724606
remove the extra all check
scottstanie Mar 13, 2024
1102da1
Remove pymp reference
scottstanie Mar 13, 2024
1ef74c5
remove todo
scottstanie Mar 13, 2024
ab26f93
Pass through the correlation threshold for interpolation
scottstanie Mar 13, 2024
5ba1c51
Merge pull request #1 from scottstanie/interpolation_for_unwrap_scott
mirzaees Mar 13, 2024
46aea75
support to have both interpolation and filtering
Mar 14, 2024
1f09151
change the values of all zero sample interferogram
Mar 18, 2024
04454ea
add test for interpolation loop
Mar 18, 2024
bdb7fbd
Merge branch 'main' into interpolation_for_unwrap
mirzaees Mar 18, 2024
3eac61a
adjust the equality tolerance
Mar 18, 2024
41acf59
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 18, 2024
4efc8a0
change docstring to match datatype
Mar 18, 2024
190558a
Merge branch 'interpolation_for_unwrap' of https://github.com/mirzaee…
Mar 18, 2024
b242ca0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 18, 2024
ee7795a
simpler interferogram for unit test
Mar 18, 2024
713a687
Merge branch 'interpolation_for_unwrap' of https://github.com/mirzaee…
Mar 18, 2024
e92240e
change to more transparent interpolation check
Mar 19, 2024
c9d6a91
add filter and interpolation options
Mar 26, 2024
4f8d15f
add grow conncomp
Mar 26, 2024
fd1743c
change typing
Mar 26, 2024
e3a682f
select reference point with a general condition file
Mar 28, 2024
ad89747
Merge branch 'main' into grow_conncomp
mirzaees Mar 29, 2024
8fdb94d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 29, 2024
66dbc2a
Update src/dolphin/timeseries.py
mirzaees Apr 2, 2024
d136a32
use a more generic form of Callable
Apr 2, 2024
653706c
use with to open files
Apr 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/dolphin/_cli_unwrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ def get_parser(subparser=None, subcommand_name="unwrap") -> argparse.ArgumentPar
default=UnwrapMethod.SNAPHU.value,
help="Choice of unwrapping algorithm to use.",
)
algorithm_opts.add_argument(
"--run-goldstein",
action="store_true",
help="Run Goldstein filter before unwrapping.",
)
algorithm_opts.add_argument(
"--run-interpolation",
action="store_true",
help="Run interpolation before unwrapping.",
)

tophu_opts = parser.add_argument_group("Tophu options")
# Add ability for downsampling/tiling with tophu
Expand Down
45 changes: 35 additions & 10 deletions src/dolphin/timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ class ReferencePointError(ValueError):
pass


def argmin_index(arr: ArrayLike) -> tuple[int, ...]:
"""Get the index tuple of the minimum value of the array.

If multiple occurrences of the minimum value exist, returns
the index of the first such occurrence in the flattened array.

Parameters
----------
arr : array_like
The input array.

Returns
-------
tuple of int
The index of the minimum value.

"""
return np.unravel_index(np.argmin(arr), np.shape(arr))


@jit
def weighted_lstsq_single(
A: ArrayLike,
Expand Down Expand Up @@ -607,28 +627,33 @@ def correlation_to_variance(correlation: ArrayLike, nlooks: int) -> Array:

def select_reference_point(
ccl_file_list: Sequence[PathOrStr],
amp_dispersion_file: PathOrStr,
condition_file: PathOrStr,
output_dir: Path,
condition_func: Callable[[ArrayLike], tuple[int, ...]] = argmin_index,
block_shape: tuple[int, int] = (512, 512),
num_threads: int = 5,
) -> ReferencePoint:
"""Automatically select a reference point for a stack of unwrapped interferograms.

Uses the amplitude dispersion and connected component labels, the point is selected
Uses the condition file and connected component labels, the point is selected
which

1. is within intersection of all nonzero connected component labels (always valid)
2. has the lowest amplitude dispersion
2. has the condition applied to condition file. for example: has the lowest
amplitude dispersion

Parameters
----------
ccl_file_list : Sequence[PathOrStr]
List of connected component label phase files.
amp_dispersion_file: PathOrStr
Amplitude dispersion, stitched and multilooked to the same size each raster
in `ccl_file_list`
condition_file: PathOrStr
A file with the same size as each raster, like amplitude dispersion or
temporal coherence in `ccl_file_list`
output_dir: Path
Path to store the computed "conncomp_intersection.tif" raster
condition_func: Callable[[ArrayLike, ]]
The function to apply to the condition file,
for example numpy.argmin which finds the pixel with lowest value
block_shape: tuple[int, int]
Size of blocks to read from while processing `ccl_file_list`
Default = (512, 512)
Expand Down Expand Up @@ -693,12 +718,12 @@ def intersect_conncomp(arr: np.ma.MaskedArray, axis: int) -> np.ndarray:
# Create a mask of pixels with this label
isin_largest_conncomp = label == largest_idx

amp_dispersion = io.load_gdal(amp_dispersion_file, masked=True)
condition_file_values = io.load_gdal(condition_file, masked=True)
# Mask out where the conncomps aren't equal to the largest
amp_dispersion.mask = amp_dispersion.mask | (~isin_largest_conncomp)
condition_file_values.mask = condition_file_values.mask | (~isin_largest_conncomp)

# Pick the (unmasked) point with the lowest amplitude dispersion
ref_row, ref_col = np.unravel_index(np.argmin(amp_dispersion), amp_dispersion.shape)
# Pick the (unmasked) point with the condition applied to condition file
ref_row, ref_col = condition_func(condition_file_values)

# Cast to `int` to avoid having `np.int64` types
return ReferencePoint(int(ref_row), int(ref_col))
82 changes: 78 additions & 4 deletions src/dolphin/unwrap/_snaphu_py.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

from pathlib import Path
from typing import Optional

from numpy.typing import ArrayLike

from dolphin._log import get_log
from dolphin._types import Filename
Expand All @@ -21,13 +24,13 @@ def unwrap_snaphu_py(
ntiles: tuple[int, int] = (1, 1),
tile_overlap: tuple[int, int] = (0, 0),
nproc: int = 1,
mask_file: Filename | None = None,
mask_file: Optional[Filename] = None,
zero_where_masked: bool = False,
unw_nodata: float | None = DEFAULT_UNW_NODATA,
ccl_nodata: int | None = DEFAULT_CCL_NODATA,
unw_nodata: Optional[float] = DEFAULT_UNW_NODATA,
ccl_nodata: Optional[int] = DEFAULT_CCL_NODATA,
init_method: str = "mst",
cost: str = "smooth",
scratchdir: Filename | None = None,
scratchdir: Optional[Filename] = None,
) -> tuple[Path, Path]:
"""Unwrap an interferogram using at multiple scales using `tophu`.

Expand Down Expand Up @@ -151,3 +154,74 @@ def unwrap_snaphu_py(
return _zero_from_mask(unw_filename, cc_filename, mask_file)

return Path(unw_filename), Path(cc_filename)


def grow_conncomp_snaphu(
unw_filename: Filename,
corr_filename: Filename,
nlooks: float,
mask: Optional[ArrayLike] = None,
ccl_nodata: Optional[int] = DEFAULT_CCL_NODATA,
cost: str = "smooth",
min_conncomp_frac: float = 0.0001,
scratchdir: Optional[Filename] = None,
) -> Path:
"""Compute connected component labels using SNAPHU.

Parameters
----------
unw_filename : Filename
Path to output unwrapped phase file.
corr_filename : Filename
Path to input correlation file.
nlooks : float
Effective number of looks used to form the input correlation data.
mask : Array, optional
binary byte mask array, by default None.
Assumes that 1s are valid pixels and 0s are invalid.
ccl_nodata : float, optional
Nodata value for the connected component labels.
cost : str
Statistical cost mode.
Default = "smooth"
min_conncomp_frac : float, optional
Minimum size of a single connected component, as a fraction of the total number
of pixels in the array. Defaults to 0.0001.
scratchdir : Filename, optional
If provided, uses a scratch directory to save the intermediate files
during unwrapping.

Returns
-------
conncomp_path : Filename
Path to output connected component label file.

"""
import snaphu

unw_suffix = full_suffix(unw_filename)
cc_filename = str(unw_filename).replace(unw_suffix, CONNCOMP_SUFFIX)

with (
snaphu.io.Raster(unw_filename) as unw,
snaphu.io.Raster(corr_filename) as corr,
snaphu.io.Raster.create(
cc_filename,
like=unw,
nodata=ccl_nodata,
dtype="u2",
**DEFAULT_TIFF_OPTIONS_RIO,
) as conncomp,
):
snaphu.grow_conncomps(
unw=unw,
corr=corr,
nlooks=nlooks,
mask=mask,
cost=cost,
min_conncomp_frac=min_conncomp_frac,
scratchdir=scratchdir,
conncomp=conncomp,
)

return Path(cc_filename)
45 changes: 31 additions & 14 deletions src/dolphin/unwrap/_unwrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
UNW_SUFFIX,
UNW_SUFFIX_ZEROED,
)
from ._snaphu_py import unwrap_snaphu_py
from ._snaphu_py import grow_conncomp_snaphu, unwrap_snaphu_py
from ._tophu import multiscale_unwrap
from ._utils import create_combined_mask, set_nodata_values

Expand All @@ -37,7 +37,7 @@ def run(
output_path: Filename,
*,
nlooks: float = 5,
mask_file: Optional[Filename] = None,
mask_filename: Optional[Filename] = None,
zero_where_masked: bool = False,
unwrap_method: UnwrapMethod = UnwrapMethod.SNAPHU,
init_method: str = "mst",
Expand Down Expand Up @@ -69,7 +69,7 @@ def run(
Path to output directory.
nlooks : int, optional
Effective number of looks used to form the input correlation data.
mask_file : Filename, optional
mask_filename : Filename, optional
Path to binary byte mask file, by default None.
Assumes that 1s are valid pixels and 0s are invalid.
zero_where_masked : bool, optional
Expand Down Expand Up @@ -160,8 +160,8 @@ def run(
out_files.append(outf)
logger.info(f"{len(out_files)} left to unwrap")

if mask_file:
mask_file = Path(mask_file).resolve()
if mask_filename:
mask_filename = Path(mask_filename).resolve()

# This keeps it from spawning a new process for a single job.
Executor = ThreadPoolExecutor if max_jobs > 1 else DummyProcessPoolExecutor
Expand All @@ -176,7 +176,7 @@ def run(
init_method=init_method,
cost=cost,
unwrap_method=unwrap_method,
mask_file=mask_file,
mask_filename=mask_filename,
zero_where_masked=zero_where_masked,
downsample_factor=downsample_factor,
ntiles=ntiles,
Expand All @@ -197,7 +197,7 @@ def run(
# We're not passing all the unw files in, so we need to tally up below
_unw_path, _cc_path = fut.result()

if zero_where_masked and mask_file is not None:
if zero_where_masked and mask_filename is not None:
all_out_files = [
Path(str(outf).replace(UNW_SUFFIX, UNW_SUFFIX_ZEROED))
for outf in all_out_files
Expand All @@ -219,7 +219,7 @@ def unwrap(
corr_filename: Filename,
unw_filename: Filename,
nlooks: float,
mask_file: Optional[Filename] = None,
mask_filename: Optional[Filename] = None,
zero_where_masked: bool = False,
ntiles: Union[int, tuple[int, int]] = 1,
tile_overlap: tuple[int, int] = (0, 0),
Expand Down Expand Up @@ -250,7 +250,7 @@ def unwrap(
Path to output unwrapped phase file.
nlooks : float
Effective number of looks used to form the input correlation data.
mask_file : Filename, optional
mask_filename : Filename, optional
Path to binary byte mask file, by default None.
Assumes that 1s are valid pixels and 0s are invalid.
zero_where_masked : bool, optional
Expand Down Expand Up @@ -306,9 +306,9 @@ def unwrap(

Returns
-------
unw_path : Path
unw_path : Filename
Path to output unwrapped phase file.
conncomp_path : Path
conncomp_path : Filename
Path to output connected component label file.

"""
Expand All @@ -320,13 +320,13 @@ def unwrap(
unwrap_method = UnwrapMethod(unwrap_method)

# Check for a nodata mask
if io.get_raster_nodata(ifg_filename) is None or mask_file is None:
if io.get_raster_nodata(ifg_filename) is None or mask_filename is None:
# With no marked `nodata`, just use the passed in mask
combined_mask_file = mask_file
combined_mask_file = mask_filename
else:
combined_mask_file = Path(ifg_filename).with_suffix(".mask.tif")
create_combined_mask(
mask_filename=mask_file,
mask_filename=mask_filename,
image_filename=ifg_filename,
output_filename=combined_mask_file,
)
Expand Down Expand Up @@ -395,13 +395,15 @@ def unwrap(
f"Masking pixels with correlation below {interpolation_cor_threshold}"
)
coherent_pixel_mask = corr[:] >= interpolation_cor_threshold

logger.info(f"Interpolating {pre_interp_ifg_filename} -> {interp_ifg_filename}")
modified_ifg = interpolate(
ifg=ifg,
weights=coherent_pixel_mask,
weight_cutoff=interpolation_cor_threshold,
max_radius=max_radius,
)

logger.info(f"Writing interpolated output to {interp_ifg_filename}")
io.write_arr(
arr=modified_ifg,
Expand Down Expand Up @@ -481,4 +483,19 @@ def unwrap(
options=opts,
)

# Regrow connected components after phase modification
corr = io.load_gdal(corr_filename)
mask = corr[:] > 0
# TODO decide whether we want to have the
# 'min_conncomp_frac' option in the config
conncomp_path = grow_conncomp_snaphu(
unw_filename=unw_filename,
corr_filename=corr_filename,
nlooks=nlooks,
mask=mask,
ccl_nodata=ccl_nodata,
cost=cost,
scratchdir=scratchdir,
)

return unw_path, conncomp_path
14 changes: 14 additions & 0 deletions src/dolphin/workflows/_cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def create_config(
no_unwrap: bool = False,
no_inversion: bool = False,
n_parallel_unwrap: int = 1,
run_goldstein: bool = False,
run_interpolation: bool = False,
unwrap_method: UnwrapMethod = UnwrapMethod.SNAPHU,
troposphere_files: Optional[list[str]] = None,
tropo_date_fmt: str = "%Y%m%d",
Expand Down Expand Up @@ -104,6 +106,8 @@ def create_config(
"n_parallel_jobs": n_parallel_unwrap,
"run_unwrap": not no_unwrap,
"zero_where_masked": zero_where_masked,
"run_goldstein": run_goldstein,
"run_interpolation": run_interpolation,
},
timeseries_options={
"run_inversion": not no_inversion,
Expand Down Expand Up @@ -274,6 +278,16 @@ def get_parser(subparser=None, subcommand_name="run"):
default=1,
help="Number of interferograms to unwrap in parallel.",
)
unwrap_group.add_argument(
"--run-goldstein",
action="store_true",
help="Run Goldstein filter before unwrapping.",
)
unwrap_group.add_argument(
"--run-interpolation",
action="store_true",
help="Run interpolation before unwrapping.",
)

# Correction options
correction_group = parser.add_argument_group("Correction options")
Expand Down
2 changes: 1 addition & 1 deletion src/dolphin/workflows/unwrapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def run(
cor_filenames=cor_file_list,
output_path=output_path,
nlooks=nlooks,
mask_file=output_mask,
mask_filename=output_mask,
zero_where_masked=unwrap_options.zero_where_masked,
max_jobs=unwrap_options.n_parallel_jobs,
ntiles=unwrap_options.ntiles,
Expand Down