-
Notifications
You must be signed in to change notification settings - Fork 28
Goldstein filtering step for unwrapping workflow #247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1bfb61c
9a15535
cc3493e
ab4fa61
e5a6212
deb239e
9653901
0e17a87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,3 +5,4 @@ | |
| from ._log import * | ||
| from ._show_versions import * | ||
| from ._types import * | ||
| from .goldstein import goldstein | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,97 @@ | ||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||
| from numpy.typing import ArrayLike | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def goldstein(phase: ArrayLike, alpha: float, psize: int = 32) -> np.ndarray: | ||||||||||||||||||||
| """Apply the Goldstein adaptive filter to the given data. | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a reference to the Goldstein/Werner 1998 paper here. @scottstanie has made it really easy to cite references in docstrings in this repo-- You could add a bibtex entry like this to this file: docs/references.bib Then you could cite the paper in a docstring like this:
Suggested change
|
||||||||||||||||||||
| Parameters | ||||||||||||||||||||
| ---------- | ||||||||||||||||||||
| phase : np.ndarray | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The annotation above says this argument is of type
Suggested change
where "array_like" is a keyword in the NumPy glossary: https://numpy.org/doc/stable/glossary.html#term-array_like |
||||||||||||||||||||
| 2D complex array containing the data to be filtered. | ||||||||||||||||||||
| alpha : float | ||||||||||||||||||||
| Filtering parameter for Goldstein algorithm | ||||||||||||||||||||
| Must be between 0 (no filtering) and 1 (maximum filtering) | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Should we enforce this in the function body? E.g. something like this: if (alpha < 0.0) or (alpha > 1.0):
raise ValueError(f"alpha must be between 0 and 1, instead got {alpha}")There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly, you could consider adding a check that I think |
||||||||||||||||||||
| psize : int, optional | ||||||||||||||||||||
| edge length of square patch | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This description might not mean much to a user who's unfamiliar with the details of the implementation. We could maybe make the intention of """Apply the Goldstein adaptive filter to the given data.
+
+ Applies an adaptive spectral filter to fixed-size overlapping square patches
+ of the input data. The contributions from multiple overlapping patches are
+ blended using a triangular weighting of the filtered data in order to
+ attenuate boundary artifacts. |
||||||||||||||||||||
| Default = 32 | ||||||||||||||||||||
|
Comment on lines
+13
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer to use full sentences in docstring descriptions (with periods and proper capitalization), since I think it looks more professional.
Suggested change
|
||||||||||||||||||||
| Returns | ||||||||||||||||||||
| ------- | ||||||||||||||||||||
| 2D numpy array of filtered data. | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should include the type of the return value here as well to be consistent with NumPy docstring conventions
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use proper capitalization of other projects' names when referring to them in our docstrings. "numpy" is officially stylized as "NumPy"
Suggested change
Or alternatively you could just remove the "numpy" part and say "2D array"
Suggested change
|
||||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def apply_pspec(data: ArrayLike): | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
| # NaN is allowed value | ||||||||||||||||||||
| assert not (alpha < 0), f"Invalid parameter value {alpha} < 0" | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is this comment relevant to the code that it's attached to? Are we saying that (I think the behavior I had in mind was that the interferogram array could contain NaNs, but that alpha should always be finite-valued. I'm not sure that there'd be any reason to support NaN-valued alpha.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is an idiomatic use of By intention and common practice, assertions should be used for validating the programmer's own logic. They're sort of a tool for debugging and internally documenting assumptions. They shouldn't be used for validating external inputs to public functions. Instead, we should raise an exception of the appropriate type (like a |
||||||||||||||||||||
| wgt = np.power(np.abs(data) ** 2, alpha / 2) | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we care about the amplitude values of the filtered data at all? Maybe a question for @scottstanie. Currently, the filter coefficients and block weights are not normalized, so the processing is effectively applying a scaling factor to the output. It shouldn't affect the phase, but the amplitude values will not be comparable to the input amplitudes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't believe so- There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I believe you're right about SNAPHU. The ICU algorithm does care of about interferogram amplitudes a bit (for computing "intensity neutrons"), but I don't think we're seriously considering ICU in our comparison of unwrappers. I'm not aware of other unwrapping algorithms that use amplitude information, so it seems reasonably safe to not worry about normalizing amplitudes. |
||||||||||||||||||||
| data = wgt * data | ||||||||||||||||||||
| return data | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def make_wgt(nxp: int, nyp: int) -> np.ndarray: | ||||||||||||||||||||
| # Create arrays of horizontal and vertical weights | ||||||||||||||||||||
| wx = 1.0 - np.abs(np.arange(nxp // 2) - (nxp / 2.0 - 1.0)) / (nxp / 2.0 - 1.0) | ||||||||||||||||||||
| wy = 1.0 - np.abs(np.arange(nyp // 2) - (nyp / 2.0 - 1.0)) / (nyp / 2.0 - 1.0) | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we intend to support odd-valued patch sizes (i.e. Note that the array of weights returned by this function won't have the same shape as each corresponding data block if I personally think it'd be fine to not support odd-valued patch sizes, but IMO we should document this in the docstring and add a check that ensures
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case where So this should be functionally equivalent to the above (unless we want to support odd-valued
Suggested change
|
||||||||||||||||||||
| # Compute the outer product of wx and wy to create | ||||||||||||||||||||
| # the top-left quadrant of the weight matrix | ||||||||||||||||||||
| quadrant = np.outer(wy, wx) | ||||||||||||||||||||
| # Create a full weight matrix by mirroring the quadrant along both axes | ||||||||||||||||||||
| wgt = np.block( | ||||||||||||||||||||
| [ | ||||||||||||||||||||
| [quadrant, np.flip(quadrant, axis=1)], | ||||||||||||||||||||
| [np.flip(quadrant, axis=0), np.flip(np.flip(quadrant, axis=0), axis=1)], | ||||||||||||||||||||
| ] | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| return wgt | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def patch_goldstein_filter( | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a compelling reason why we've defined this function inside the body of the In some circumstances, I think this is a useful idiom for defining private functions, but in this case I think it might be fruitful to pull this function out. In particular, it would be useful to be able to test this function in isolation (without all of the blocking logic), but we currently can't do that since it's inaccessible to the unit test suite. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No unit tests If you make this a standalone function (so that its importable from the test suite), it should be pretty trivial to implement a test that checks that the phase values are unchanged by filtering with Another good test would be to simulate a signal with a known magnitude spectrum (e.g. by constructing the signal in the frequency domain and then taking the IFFT) and then applying It might also be worthwhile to exercise the blocking logic in a test. Common issues in block processing include off-by-one indexing errors and mishandling the remainder samples past the end of the last full-sized block. I think you could guard against most of these common issues in a test by passing an array of ones to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. related to the future nan change #261 , we could check that the number of input nodata values are similar/the same as the output nodata values. |
||||||||||||||||||||
| data: ArrayLike, wgt: ArrayLike, psize: int | ||||||||||||||||||||
| ) -> np.ndarray: | ||||||||||||||||||||
| """Apply the filter to a single patch of data. | ||||||||||||||||||||
| Parameters | ||||||||||||||||||||
| ---------- | ||||||||||||||||||||
| data : np.ndarray | ||||||||||||||||||||
| 2D complex array containing the data to be filtered. | ||||||||||||||||||||
| wgt : np.ndarray | ||||||||||||||||||||
|
Comment on lines
+55
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a side note, while "np.ndarray" --> "numpy.ndarray" |
||||||||||||||||||||
| weight matrix for summing neighboring data | ||||||||||||||||||||
| psize : int | ||||||||||||||||||||
| edge length of square FFT area | ||||||||||||||||||||
| Returns | ||||||||||||||||||||
| ------- | ||||||||||||||||||||
| 2D numpy array of filtered data. | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
| """ | ||||||||||||||||||||
| # Calculate alpha | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This comment doesn't seem to be related to the code block that it's attached to. |
||||||||||||||||||||
| data = np.fft.fft2(data, s=(psize, psize)) | ||||||||||||||||||||
| data = apply_pspec(data) | ||||||||||||||||||||
| data = np.fft.ifft2(data, s=(psize, psize)) | ||||||||||||||||||||
| return wgt * data | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
From the perspective of breaking up the algorithm into functions that each perform logically independent steps, it seems to me that this function shouldn't know anything about If I understand what's going on correctly, Instead, I would suggest multiplying |
||||||||||||||||||||
|
|
||||||||||||||||||||
| def apply_goldstein_filter(data: ArrayLike) -> np.ndarray: | ||||||||||||||||||||
| # Create an empty array for the output | ||||||||||||||||||||
| out = np.zeros(data.shape, dtype=np.complex64) | ||||||||||||||||||||
| # ignore processing for empty chunks | ||||||||||||||||||||
| if np.all(np.isnan(data)): | ||||||||||||||||||||
| return data | ||||||||||||||||||||
|
Comment on lines
+76
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need any special handling for this case? Let's say hypothetically we remove lines 77 & 78. In that case, if the input But should we really be optimizing for the case where the input data is all NaN-valued? Note that this is (slightly) pessimizing the usual case where the input contains at least one non-NaN value, since I would suggest removing these lines since I think encountering all-NaN-valued datasets seems unlikely in normal operation so, on net, this is probably a pessimization.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be a good thing to test- For the stitched, geocoded interferograms, The tilt means it's about 30% nodata. For single bursts, it's about 60% nodata (which is why adding a check like this speeds up the wrapped phase part by ~2x). On the other hand, having this hardcoded is very geocoded-input-centric. An RIFG has no need for this and only gets it slowed down There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah will remove this. I think this is a remnant from when I was nanning out zero values, which can't be done with tophu at the moment anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right now, the check for all NaN values is outside the loop over patches. So this shortcut will only trigger if the dataset contains 100% nodata. IIUC, there shouldn't be any speed up for interferograms that contain 60% nodata. Maybe we should consider adding a similar check inside the loop over patches? If an individual patch contains all NaNs, there's no need to compute & apply the filter there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh right, yes this should be removed then, the comment |
||||||||||||||||||||
| # Create the weight matrix | ||||||||||||||||||||
| wgt_matrix = make_wgt(psize, psize) | ||||||||||||||||||||
| # Iterate over windows of the data | ||||||||||||||||||||
| for i in range(0, data.shape[0] - psize, psize // 2): | ||||||||||||||||||||
| for j in range(0, data.shape[1] - psize, psize // 2): | ||||||||||||||||||||
|
Comment on lines
+82
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noting another potential issue with odd-valued If But if |
||||||||||||||||||||
| # Create proocessing windows | ||||||||||||||||||||
| data_window = data[i : i + psize, j : j + psize] | ||||||||||||||||||||
| wgt_window = wgt_matrix[: data_window.shape[0], : data_window.shape[1]] | ||||||||||||||||||||
| # Apply the filter to the window | ||||||||||||||||||||
| filtered_window = patch_goldstein_filter(data_window, wgt_window, psize) | ||||||||||||||||||||
| # Add the result to the output array | ||||||||||||||||||||
| slice_i = slice(i, min(i + psize, out.shape[0])) | ||||||||||||||||||||
| slice_j = slice(j, min(j + psize, out.shape[1])) | ||||||||||||||||||||
| out[slice_i, slice_j] += filtered_window[ | ||||||||||||||||||||
| : slice_i.stop - slice_i.start, : slice_j.stop - slice_j.start | ||||||||||||||||||||
| ] | ||||||||||||||||||||
| return out | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return apply_goldstein_filter(phase) | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,10 @@ | |
| from pathlib import Path | ||
| from typing import Optional, Sequence, Union | ||
|
|
||
| import numpy as np | ||
| from tqdm.auto import tqdm | ||
|
|
||
| from dolphin import io | ||
| from dolphin import goldstein, io | ||
| from dolphin._log import get_log, log_runtime | ||
| from dolphin._types import Filename | ||
| from dolphin.utils import DummyProcessPoolExecutor, full_suffix | ||
|
|
@@ -18,7 +19,6 @@ | |
| DEFAULT_UNW_NODATA, | ||
| UNW_SUFFIX, | ||
| ) | ||
| from ._snaphu_py import unwrap_snaphu_py | ||
| from ._tophu import multiscale_unwrap | ||
| from ._utils import create_combined_mask, set_nodata_values | ||
|
|
||
|
|
@@ -47,6 +47,8 @@ def run( | |
| ccl_nodata: int | None = DEFAULT_CCL_NODATA, | ||
| scratchdir: Optional[Filename] = None, | ||
| overwrite: bool = False, | ||
| run_goldstein: bool = False, | ||
| alpha: float = 0.5, | ||
| ) -> tuple[list[Path], list[Path]]: | ||
| """Run snaphu on all interferograms in a directory. | ||
|
|
||
|
|
@@ -97,6 +99,10 @@ def run( | |
| If None, uses `tophu`'s `/tmp/...` default. | ||
| overwrite : bool, optional, default = False | ||
| Overwrite existing unwrapped files. | ||
| run_goldstein : bool, optional, default = False | ||
| Whether to run Goldstein filtering on interferogram | ||
| alpha : float, optional, default = 0.5 | ||
| Alpha parameter for Goldstein filtering | ||
|
|
||
| Returns | ||
| ------- | ||
|
|
@@ -159,6 +165,8 @@ def run( | |
| unw_nodata=unw_nodata, | ||
| ccl_nodata=ccl_nodata, | ||
| scratchdir=scratchdir, | ||
| run_goldstein=run_goldstein, | ||
| alpha=alpha, | ||
| ) | ||
| for ifg_file, out_file, cor_file in zip(in_files, out_files, cor_filenames) | ||
| ] | ||
|
|
@@ -190,6 +198,8 @@ def unwrap( | |
| unw_nodata: float | None = DEFAULT_UNW_NODATA, | ||
| ccl_nodata: int | None = DEFAULT_CCL_NODATA, | ||
| scratchdir: Optional[Filename] = None, | ||
| run_goldstein: bool = False, | ||
| alpha: float = 0.5, | ||
| ) -> tuple[Path, Path]: | ||
| """Unwrap a single interferogram using snaphu, isce3, or tophu. | ||
|
|
||
|
|
@@ -244,6 +254,10 @@ def unwrap( | |
| scratchdir : Filename, optional | ||
| Path to scratch directory to hold intermediate files. | ||
| If None, uses `tophu`'s `/tmp/...` default. | ||
| run_goldstein : bool, optional, default = False | ||
| Whether to run Goldstein filtering on interferogram | ||
| alpha : float, optional, default = 0.5 | ||
| Alpha parameter for Goldstein filtering | ||
|
|
||
| Returns | ||
| ------- | ||
|
|
@@ -272,12 +286,47 @@ def unwrap( | |
| output_filename=combined_mask_file, | ||
| ) | ||
|
|
||
| if run_goldstein: | ||
| suf = Path(unw_filename).suffix | ||
| if suf == ".tif": | ||
| driver = "GTiff" | ||
| opts = list(io.DEFAULT_TIFF_OPTIONS) | ||
| else: | ||
| driver = "ENVI" | ||
| opts = list(io.DEFAULT_ENVI_OPTIONS) | ||
|
Comment on lines
+291
to
+296
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this is baking in the assumption that we will only ever want to process GeoTiff files or ENVI files. It'd be nice if dolphin had a function like def get_raster_driver(filename: str | os.PathLike[str]) -> str:
...that abstracted the logic for inferring the driver name of a particular raster, so that any future changes to this logic could be contained in a single function. Similarly, it'd be nice to see a function like def get_default_driver_options(driver: str) -> list[str]:
...as the single source of truth for getting our default driver-specific raster creation options. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also meant to comment here- If we do want to accomodate more than GTiff, that would probably make more sense as a larger refactor to pass around |
||
|
|
||
| # If we're running Goldstein filtering, the intermediate | ||
| # filtered/unwrapped rasters are temporary rasters in the scratch dir. | ||
| filt_ifg_filename = ( | ||
| Path(scratchdir or ".") / Path(ifg_filename).with_suffix(".filt" + suf).name | ||
| ) | ||
| scratch_unw_filename = Path(unw_filename).with_suffix(".filt.unw" + suf) | ||
|
|
||
| ifg = io.load_gdal(ifg_filename) | ||
| logger.info(f"Goldstein filtering {ifg_filename} -> {filt_ifg_filename}") | ||
| filt_ifg = goldstein(ifg, alpha=alpha) | ||
| logger.info(f"Writing filtered output to {filt_ifg_filename}") | ||
| io.write_arr( | ||
| arr=filt_ifg, | ||
| output_name=filt_ifg_filename, | ||
| like_filename=ifg_filename, | ||
| driver=driver, | ||
| options=opts, | ||
| ) | ||
| unwrapper_ifg_filename = filt_ifg_filename | ||
| unwrapper_unw_filename = scratch_unw_filename | ||
| else: | ||
| unwrapper_ifg_filename = Path(ifg_filename) | ||
| unwrapper_unw_filename = Path(unw_filename) | ||
|
Comment on lines
+316
to
+320
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note for @mirzaees and @taliboliver - this is where it could make sense to add the interpolation logic. |
||
|
|
||
| if unwrap_method == UnwrapMethod.SNAPHU: | ||
| from ._snaphu_py import unwrap_snaphu_py | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was it causing any problems to have this imported at the top of the file? Or was this a mistake to move the import? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, yeah I moved this here because I was testing in an environment without snaphu. It was very convenient to not have to install snaphu simply by moving the import to where it's needed, but I can definitely move this back if that's an antipattern. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose we can leave it for now if someone really want's to use another unwrapper that's not snaphu py, we're not currently putting |
||
|
|
||
| # Pass everything to snaphu-py | ||
| unw_path, conncomp_path = unwrap_snaphu_py( | ||
| ifg_filename, | ||
| unwrapper_ifg_filename, | ||
| corr_filename, | ||
| unw_filename, | ||
| unwrapper_unw_filename, | ||
| nlooks, | ||
| ntiles=ntiles, | ||
| tile_overlap=tile_overlap, | ||
|
|
@@ -292,9 +341,9 @@ def unwrap( | |
| ) | ||
| else: | ||
| unw_path, conncomp_path = multiscale_unwrap( | ||
| ifg_filename, | ||
| unwrapper_ifg_filename, | ||
| corr_filename, | ||
| unw_filename, | ||
| unwrapper_unw_filename, | ||
| downsample_factor, | ||
| ntiles=ntiles, | ||
| nlooks=nlooks, | ||
|
|
@@ -321,4 +370,22 @@ def unwrap( | |
| filename=conncomp_path, output_nodata=ccl_nodata, like_filename=ifg_filename | ||
| ) | ||
|
|
||
| # Transfer ambiguity numbers from filtered unwrapped interferogram | ||
| # back to original interferogram | ||
| if run_goldstein: | ||
| logger.info( | ||
| f"Transferring ambiguity numbers from filtered ifg {scratch_unw_filename}" | ||
| ) | ||
| unw_arr = io.load_gdal(scratch_unw_filename) | ||
|
|
||
| final_arr = np.angle(ifg) + (unw_arr - np.angle(filt_ifg)) | ||
|
|
||
| io.write_arr( | ||
| arr=final_arr, | ||
| output_name=unw_filename, | ||
| dtype=np.float32, | ||
| driver=driver, | ||
| options=opts, | ||
| ) | ||
|
|
||
| return unw_path, conncomp_path | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -164,6 +164,12 @@ class UnwrapOptions(BaseModel, extra="forbid"): | |
| "Whether to run the unwrapping step after wrapped phase estimation." | ||
| ), | ||
| ) | ||
| run_goldstein: bool = Field( | ||
| False, | ||
| description=( | ||
| "Whether to run Goldstein filtering step on wrapped interferogram." | ||
| ), | ||
| ) | ||
| _directory: Path = PrivateAttr(Path("unwrapped")) | ||
| unwrap_method: UnwrapMethod = UnwrapMethod.SNAPHU | ||
| n_parallel_jobs: int = Field( | ||
|
|
@@ -205,6 +211,13 @@ class UnwrapOptions(BaseModel, extra="forbid"): | |
| description="Statistical cost mode method for SNAPHU.", | ||
| ) | ||
|
|
||
| alpha: float = Field( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we might end up changing this name slightly to avoid confusion with the SHP alpha parameter but for now it's fine and different |
||
| 0.5, | ||
| description=( | ||
| "(for Goldstein filtering) Power parameter for Goldstein algorithm." | ||
| ), | ||
| ) | ||
|
|
||
| @field_validator("ntiles", "downsample_factor", mode="before") | ||
| @classmethod | ||
| def _to_tuple(cls, v): | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming the input argument "phase" might be a little misleading. The Goldstein filter is intended to operate on complex-valued interferogram data -- not on the wrapped phase directly.
I typically name this parameter "igram".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah I lost this same comment in a closed window- let's definitely change it for the next PR because it confused me several times