PyGeoFetch 2.0.3__py3-none-any.whl → 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygeofetch/__init__.py +2 -2
- pygeofetch/cli/main.py +2 -2
- pygeofetch/insar/unwrap.py +412 -303
- pygeofetch/meta.yaml +1 -1
- {pygeofetch-2.0.3.dist-info → pygeofetch-2.0.4.dist-info}/METADATA +1 -1
- {pygeofetch-2.0.3.dist-info → pygeofetch-2.0.4.dist-info}/RECORD +10 -10
- {pygeofetch-2.0.3.dist-info → pygeofetch-2.0.4.dist-info}/WHEEL +0 -0
- {pygeofetch-2.0.3.dist-info → pygeofetch-2.0.4.dist-info}/entry_points.txt +0 -0
- {pygeofetch-2.0.3.dist-info → pygeofetch-2.0.4.dist-info}/licenses/LICENSE +0 -0
- {pygeofetch-2.0.3.dist-info → pygeofetch-2.0.4.dist-info}/top_level.txt +0 -0
pygeofetch/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch v2.0.
|
|
2
|
+
PyGeoFetch v2.0.4 — Universal Satellite Data Pipeline.
|
|
3
3
|
|
|
4
4
|
Unified access to 22+ satellite data providers with a consistent
|
|
5
5
|
Python API and CLI. Supports authentication management, federated search,
|
|
@@ -30,7 +30,7 @@ CLI::
|
|
|
30
30
|
pygeofetch doctor
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
__version__ = "2.0.
|
|
33
|
+
__version__ = "2.0.4"
|
|
34
34
|
__author__ = "PyGeoFetch Contributors"
|
|
35
35
|
__license__ = "MIT"
|
|
36
36
|
|
pygeofetch/cli/main.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch CLI v2.0.
|
|
2
|
+
PyGeoFetch CLI v2.0.4 main entry point.
|
|
3
3
|
|
|
4
4
|
Full command reference:
|
|
5
5
|
pygeofetch auth — credential management
|
|
@@ -71,7 +71,7 @@ def cli(
|
|
|
71
71
|
) -> None:
|
|
72
72
|
"""
|
|
73
73
|
\b
|
|
74
|
-
PyGeoFetch v2.0.
|
|
74
|
+
PyGeoFetch v2.0.4 — Universal Satellite Data Pipeline
|
|
75
75
|
===========================================================
|
|
76
76
|
Unified access to 22+ satellite data providers.
|
|
77
77
|
|
pygeofetch/insar/unwrap.py
CHANGED
|
@@ -22,25 +22,258 @@ Install: pip install "pygeofetch[insar]" (installs snaphu, scipy)
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
import logging
|
|
25
|
+
import platform
|
|
26
|
+
import subprocess
|
|
27
|
+
import urllib.request
|
|
28
|
+
import zipfile
|
|
29
|
+
import shutil
|
|
30
|
+
import json
|
|
31
|
+
import tempfile
|
|
32
|
+
from dataclasses import dataclass, field
|
|
25
33
|
from pathlib import Path
|
|
26
|
-
from typing import Any, Optional, Tuple, Union
|
|
34
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
27
35
|
|
|
28
36
|
logger = logging.getLogger("pygeofetch.insar.unwrap")
|
|
29
37
|
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
@dataclass
|
|
40
|
+
class UnwrapResult:
|
|
41
|
+
"""
|
|
42
|
+
Result of phase unwrapping for one interferogram pair.
|
|
43
|
+
|
|
44
|
+
Mirrors the InterferogramResult.save() pattern: the notebook cell
|
|
45
|
+
stays focused on orchestration (which pairs, what parameters), while
|
|
46
|
+
this class handles all file I/O, georeferencing, nodata masking,
|
|
47
|
+
and optional visualization.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
unwrapped_phase: Any # float32 array, radians, continuous
|
|
51
|
+
conncomp: Any # int32 array, 0 = unreliable/masked
|
|
52
|
+
coherence: Any # float32 array, 0-1 (multilooked coherence used)
|
|
53
|
+
profile: Dict[str, Any] # rasterio-style profile for georeferencing
|
|
54
|
+
reference_date: Optional[str] = None
|
|
55
|
+
secondary_date: Optional[str] = None
|
|
56
|
+
nlooks: float = 1.0
|
|
57
|
+
cost_mode: str = "defo"
|
|
58
|
+
init_method: str = "mcf"
|
|
59
|
+
reliable_fraction: float = 0.0
|
|
60
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
def save(
|
|
63
|
+
self,
|
|
64
|
+
output_dir: Union[str, Path],
|
|
65
|
+
save_png: bool = False,
|
|
66
|
+
mask_unreliable: bool = True,
|
|
67
|
+
) -> Dict[str, Path]:
|
|
68
|
+
"""
|
|
69
|
+
Save unwrapped phase and connected components as GeoTIFFs.
|
|
70
|
+
|
|
71
|
+
Writes:
|
|
72
|
+
unwrapped_phase.tif (float32, radians, nodata=-9999)
|
|
73
|
+
conncomp.tif (int32, 0=unreliable)
|
|
74
|
+
coherence.tif (float32, 0-1)
|
|
75
|
+
[optional] unwrapped_phase.png, conncomp.png
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
output_dir: Directory to save into.
|
|
79
|
+
save_png: If True, also save PNG visualizations.
|
|
80
|
+
mask_unreliable: If True (default), pixels with conncomp==0
|
|
81
|
+
are set to nodata in the unwrapped phase
|
|
82
|
+
GeoTIFF. This is baked in automatically so
|
|
83
|
+
every user gets correct nodata handling
|
|
84
|
+
without having to remember to do it manually.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Dict mapping product name to output path.
|
|
88
|
+
"""
|
|
89
|
+
import numpy as np
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
import rasterio
|
|
93
|
+
except ImportError:
|
|
94
|
+
raise ImportError('rasterio required: pip install "pygeofetch[geo]"')
|
|
95
|
+
|
|
96
|
+
out_dir = Path(output_dir)
|
|
97
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
paths = {}
|
|
99
|
+
|
|
100
|
+
base_profile = {
|
|
101
|
+
"driver": "GTiff",
|
|
102
|
+
"count": 1,
|
|
103
|
+
"height": self.unwrapped_phase.shape[0],
|
|
104
|
+
"width": self.unwrapped_phase.shape[1],
|
|
105
|
+
"crs": self.profile.get("crs"),
|
|
106
|
+
"transform": self.profile.get("transform"),
|
|
107
|
+
"compress": "deflate",
|
|
108
|
+
"tiled": True,
|
|
109
|
+
"blockxsize": 256,
|
|
110
|
+
"blockysize": 256,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ── Unwrapped phase ──
|
|
114
|
+
unw_to_save = self.unwrapped_phase.copy()
|
|
115
|
+
if mask_unreliable:
|
|
116
|
+
unw_to_save[self.conncomp == 0] = -9999.0
|
|
117
|
+
|
|
118
|
+
phase_path = out_dir / "unwrapped_phase.tif"
|
|
119
|
+
phase_profile = dict(base_profile, dtype="float32", nodata=-9999.0)
|
|
120
|
+
with rasterio.open(phase_path, "w", **phase_profile) as dst:
|
|
121
|
+
dst.write(unw_to_save.astype(np.float32)[np.newaxis])
|
|
122
|
+
paths["unwrapped_phase"] = phase_path
|
|
123
|
+
|
|
124
|
+
# ── Connected components ──
|
|
125
|
+
cc_path = out_dir / "conncomp.tif"
|
|
126
|
+
cc_profile = dict(base_profile, dtype="int32", nodata=0)
|
|
127
|
+
with rasterio.open(cc_path, "w", **cc_profile) as dst:
|
|
128
|
+
dst.write(self.conncomp.astype(np.int32)[np.newaxis])
|
|
129
|
+
paths["conncomp"] = cc_path
|
|
130
|
+
|
|
131
|
+
# ── Coherence ──
|
|
132
|
+
coh_path = out_dir / "coherence.tif"
|
|
133
|
+
coh_profile = dict(base_profile, dtype="float32", nodata=-1.0)
|
|
134
|
+
with rasterio.open(coh_path, "w", **coh_profile) as dst:
|
|
135
|
+
dst.write(self.coherence.astype(np.float32)[np.newaxis])
|
|
136
|
+
paths["coherence"] = coh_path
|
|
34
137
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
138
|
+
logger.info(
|
|
139
|
+
"Unwrap products saved → %s (reliable=%.1f%%)",
|
|
140
|
+
out_dir, self.reliable_fraction * 100,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# ── Optional PNG visualizations ──
|
|
144
|
+
if save_png:
|
|
145
|
+
self._save_pngs(out_dir, unw_to_save, paths)
|
|
146
|
+
|
|
147
|
+
return paths
|
|
148
|
+
|
|
149
|
+
def _save_pngs(self, out_dir: Path, unw_masked, paths: Dict[str, Path]) -> None:
|
|
150
|
+
"""Save PNG visualizations with robust percentile scaling."""
|
|
151
|
+
import numpy as np
|
|
152
|
+
import matplotlib
|
|
153
|
+
matplotlib.use("Agg") # non-interactive backend for headless saving
|
|
154
|
+
import matplotlib.pyplot as plt
|
|
155
|
+
|
|
156
|
+
# ── Unwrapped phase PNG ──
|
|
157
|
+
fig, ax = plt.subplots(1, 1, figsize=(14, 8))
|
|
158
|
+
masked_unw = np.ma.masked_where(self.conncomp == 0, self.unwrapped_phase)
|
|
159
|
+
finite_unw = self.unwrapped_phase[self.conncomp > 0]
|
|
160
|
+
if finite_unw.size > 0:
|
|
161
|
+
vmin = float(np.percentile(finite_unw, 2))
|
|
162
|
+
vmax = float(np.percentile(finite_unw, 98))
|
|
163
|
+
else:
|
|
164
|
+
vmin, vmax = -np.pi, np.pi
|
|
165
|
+
im = ax.imshow(masked_unw, cmap="jet", vmin=vmin, vmax=vmax)
|
|
166
|
+
ax.set_title(
|
|
167
|
+
f"Unwrapped Phase (rad)\n"
|
|
168
|
+
f"{self.reference_date} → {self.secondary_date} | "
|
|
169
|
+
f"reliable={self.reliable_fraction*100:.1f}%"
|
|
170
|
+
)
|
|
171
|
+
fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
|
172
|
+
png_path = out_dir / "unwrapped_phase.png"
|
|
173
|
+
fig.savefig(png_path, dpi=150, bbox_inches="tight")
|
|
174
|
+
plt.close(fig)
|
|
175
|
+
|
|
176
|
+
# ── Connected components PNG ──
|
|
177
|
+
fig, ax = plt.subplots(1, 1, figsize=(14, 8))
|
|
178
|
+
masked_cc = np.ma.masked_where(self.conncomp == 0, self.conncomp)
|
|
179
|
+
ax.imshow(masked_cc, cmap="tab20")
|
|
180
|
+
ax.set_title(
|
|
181
|
+
f"Connected Components\n"
|
|
182
|
+
f"{self.reference_date} → {self.secondary_date} | "
|
|
183
|
+
f"reliable={self.reliable_fraction*100:.1f}%"
|
|
184
|
+
)
|
|
185
|
+
png_path = out_dir / "conncomp.png"
|
|
186
|
+
fig.savefig(png_path, dpi=150, bbox_inches="tight")
|
|
187
|
+
plt.close(fig)
|
|
188
|
+
|
|
189
|
+
logger.info("Unwrap PNGs saved → %s", out_dir)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _ensure_snaphu_cli() -> str:
|
|
193
|
+
"""
|
|
194
|
+
Ensures the SNAPHU CLI executable is available.
|
|
195
|
+
1. Checks system PATH.
|
|
196
|
+
2. Checks ~/.pygeofetch/bin/snaphu(.exe).
|
|
197
|
+
3. If on Windows and missing, downloads the pre-compiled binary from GitHub.
|
|
198
|
+
"""
|
|
199
|
+
exe_name = "snaphu.exe" if platform.system() == "Windows" else "snaphu"
|
|
200
|
+
|
|
201
|
+
# 1. System PATH
|
|
202
|
+
path = shutil.which(exe_name)
|
|
203
|
+
if path:
|
|
204
|
+
return path
|
|
205
|
+
|
|
206
|
+
# 2. Local cache
|
|
207
|
+
bin_dir = Path.home() / ".pygeofetch" / "bin"
|
|
208
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
local_exe = bin_dir / exe_name
|
|
210
|
+
|
|
211
|
+
if local_exe.exists():
|
|
212
|
+
return str(local_exe)
|
|
213
|
+
|
|
214
|
+
# 3. Download if on Windows
|
|
215
|
+
if platform.system() == "Windows":
|
|
216
|
+
logger.info("SNAPHU executable not found. Downloading pre-compiled binary for Windows...")
|
|
217
|
+
api_url = "https://api.github.com/repos/isce-framework/snaphu-py/releases/latest"
|
|
218
|
+
asset_url = None
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
req = urllib.request.Request(api_url, headers={'User-Agent': 'PyGeoFetch'})
|
|
222
|
+
with urllib.request.urlopen(req, timeout=15) as response:
|
|
223
|
+
data = json.loads(response.read().decode())
|
|
224
|
+
|
|
225
|
+
for asset in data.get("assets", []):
|
|
226
|
+
name = asset["name"].lower()
|
|
227
|
+
if "win" in name and name.endswith(".zip"):
|
|
228
|
+
asset_url = asset["browser_download_url"]
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
if not asset_url:
|
|
232
|
+
# Fallback to scottstanie's repo which often hosts the windows binaries
|
|
233
|
+
api_url2 = "https://api.github.com/repos/scottstanie/snaphu-py/releases/latest"
|
|
234
|
+
req2 = urllib.request.Request(api_url2, headers={'User-Agent': 'PyGeoFetch'})
|
|
235
|
+
with urllib.request.urlopen(req2, timeout=15) as response2:
|
|
236
|
+
data2 = json.loads(response2.read().decode())
|
|
237
|
+
for asset in data2.get("assets", []):
|
|
238
|
+
if "win" in asset["name"].lower() and asset["name"].endswith(".zip"):
|
|
239
|
+
asset_url = asset["browser_download_url"]
|
|
240
|
+
break
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.warning(f"Could not query GitHub API for SNAPHU binary: {e}")
|
|
243
|
+
|
|
244
|
+
if not asset_url:
|
|
245
|
+
raise RuntimeError(
|
|
246
|
+
"Could not find a pre-compiled SNAPHU executable for Windows.\n"
|
|
247
|
+
"Please install it manually and add it to your PATH, or install via pip:\n"
|
|
248
|
+
" pip install snaphu"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
logger.info(f"Downloading SNAPHU from {asset_url}...")
|
|
252
|
+
try:
|
|
253
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
254
|
+
zip_path = Path(tmp_dir) / "snaphu.zip"
|
|
255
|
+
urllib.request.urlretrieve(asset_url, str(zip_path))
|
|
256
|
+
|
|
257
|
+
with zipfile.ZipFile(zip_path, 'r') as zf:
|
|
258
|
+
exe_files = [f for f in zf.namelist() if f.lower().endswith(".exe")]
|
|
259
|
+
if not exe_files:
|
|
260
|
+
raise RuntimeError("Downloaded zip did not contain an .exe file.")
|
|
261
|
+
|
|
262
|
+
with zf.open(exe_files[0]) as src, open(local_exe, "wb") as dst:
|
|
263
|
+
shutil.copyfileobj(src, dst)
|
|
264
|
+
|
|
265
|
+
logger.info(f"SNAPHU downloaded successfully to {local_exe}")
|
|
266
|
+
return str(local_exe)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
raise RuntimeError(f"Failed to download/extract SNAPHU executable: {e}")
|
|
269
|
+
|
|
270
|
+
else:
|
|
271
|
+
raise RuntimeError(
|
|
272
|
+
"SNAPHU executable not found in PATH.\n"
|
|
273
|
+
"Please install it using your system package manager:\n"
|
|
274
|
+
" Ubuntu/Debian: sudo apt-get install snaphu\n"
|
|
275
|
+
" macOS (Homebrew): brew install snaphu\n"
|
|
276
|
+
"Or install the Python bindings: pip install snaphu"
|
|
44
277
|
)
|
|
45
278
|
|
|
46
279
|
|
|
@@ -52,49 +285,6 @@ def goldstein_filter(
|
|
|
52
285
|
):
|
|
53
286
|
"""
|
|
54
287
|
Adaptive Goldstein phase filter.
|
|
55
|
-
|
|
56
|
-
Suppresses interferometric phase noise in the frequency domain,
|
|
57
|
-
per-tile, weighting each spatial frequency by its own local power.
|
|
58
|
-
This is fundamentally different from multilook() -- multilooking
|
|
59
|
-
averages blindly in the spatial domain regardless of what's in the
|
|
60
|
-
data; Goldstein filtering adaptively preserves real fringe patterns
|
|
61
|
-
while suppressing noise, since noise and signal separate cleanly in
|
|
62
|
-
the frequency domain but not in the spatial domain. The two are
|
|
63
|
-
complementary, not substitutes -- multilook first to reduce array
|
|
64
|
-
size and get an honest coherence estimate, then Goldstein filter
|
|
65
|
-
the (still noisy) phase before unwrapping.
|
|
66
|
-
|
|
67
|
-
Verified before use, not assumed: on synthetic data with known
|
|
68
|
-
ground truth (real fringe pattern, coherence=0.5, matching this
|
|
69
|
-
project's real Mexico City coherence level), filtering reduced mean
|
|
70
|
-
phase error from 0.96 rad to 0.36 rad against the true phase (62%
|
|
71
|
-
reduction). More directly: on synthetic data at coherence=0.55 that
|
|
72
|
-
reproducibly failed to unwrap (0% reliable, matching every real
|
|
73
|
-
failure seen across this project's Obuasi, Accra, and Mexico City
|
|
74
|
-
InSAR work), the SAME data with Goldstein filtering applied first
|
|
75
|
-
unwrapped at 94.8% reliable.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
interferogram: Complex interferogram (ref * conj(sec)), any
|
|
79
|
-
shape. Real, non-complex wrapped phase should be
|
|
80
|
-
converted first via np.exp(1j * phase).
|
|
81
|
-
alpha: Filter strength, 0.0 (no filtering, returns the
|
|
82
|
-
input essentially unchanged) to 1.0 (aggressive,
|
|
83
|
-
risks distorting real signal in low-SNR tiles).
|
|
84
|
-
0.5-0.7 is a reasonable starting range; higher
|
|
85
|
-
values trade more noise suppression for more
|
|
86
|
-
risk of altering genuine phase structure.
|
|
87
|
-
tile_size: FFT tile size in pixels. Smaller tiles adapt to
|
|
88
|
-
local noise better but have less frequency
|
|
89
|
-
resolution; larger tiles are the opposite.
|
|
90
|
-
32-64 is standard practice.
|
|
91
|
-
overlap: Fraction of tile overlap (0-1), blended with a
|
|
92
|
-
Hann window to avoid tile-boundary artifacts.
|
|
93
|
-
0.5 (50%) is standard and verified here.
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
Filtered complex interferogram, same shape and dtype
|
|
97
|
-
(complex64) as the input.
|
|
98
288
|
"""
|
|
99
289
|
import numpy as np
|
|
100
290
|
|
|
@@ -138,7 +328,7 @@ def goldstein_filter(
|
|
|
138
328
|
|
|
139
329
|
valid = weight_sum > 0
|
|
140
330
|
output[valid] /= weight_sum[valid]
|
|
141
|
-
output[~valid] = interferogram[~valid]
|
|
331
|
+
output[~valid] = interferogram[~valid]
|
|
142
332
|
return output.astype(np.complex64)
|
|
143
333
|
|
|
144
334
|
|
|
@@ -150,75 +340,7 @@ def bridge_unwrap_regions(
|
|
|
150
340
|
reference_pixel=None,
|
|
151
341
|
):
|
|
152
342
|
"""
|
|
153
|
-
Bridging unwrapping-error correction
|
|
154
|
-
Computers & Geosciences 133, 104331 -- the method MintPy uses,
|
|
155
|
-
ported and adapted natively here rather than depending on MintPy).
|
|
156
|
-
|
|
157
|
-
SNAPHU unwraps each connected component (conncomp) internally
|
|
158
|
-
consistently, but has no way to relate physically DISCONNECTED
|
|
159
|
-
regions to each other -- each island's phase could be off from the
|
|
160
|
-
others by an unknown INTEGER multiple of 2*pi, since SNAPHU's
|
|
161
|
-
network-flow solver never sees a path connecting them. Bridging
|
|
162
|
-
finds the closest point-pair between regions and estimates that
|
|
163
|
-
integer offset from the real, local median phase difference there
|
|
164
|
-
(robust to per-pixel noise), then corrects the whole region by that
|
|
165
|
-
exact multiple of 2*pi.
|
|
166
|
-
|
|
167
|
-
Real, important, honest limitation, not a flaw in this
|
|
168
|
-
implementation specifically: bridging can only recover RELATIVE
|
|
169
|
-
consistency between regions, never absolute truth. The largest
|
|
170
|
-
region is used as the reference (offset 0); if THAT region itself
|
|
171
|
-
carries a real, undetected cycle error, every other region gets
|
|
172
|
-
correctly bridged relative to it but the whole scene stays offset
|
|
173
|
-
from ground truth by that same amount. This is the same fundamental
|
|
174
|
-
limitation that's why SBASTimeSeries needs an explicit reference
|
|
175
|
-
pixel -- no phase-based method alone can anchor to an absolute
|
|
176
|
-
value without external ground truth (a GPS station, a known-stable
|
|
177
|
-
reference point, etc.).
|
|
178
|
-
|
|
179
|
-
Verified before use: on synthetic data with THREE disconnected
|
|
180
|
-
regions and different, deliberate, known integer-cycle errors
|
|
181
|
-
introduced in each (including realistic per-pixel phase noise at
|
|
182
|
-
coherence=0.6), every pairwise region-to-region consistency check
|
|
183
|
-
came back under 0.02 rad after correction -- confirming the method
|
|
184
|
-
correctly recovers mutual consistency, the real, valid claim this
|
|
185
|
-
technique can make (verified NOT to claim absolute-truth recovery,
|
|
186
|
-
which no such method can deliver without external reference data).
|
|
187
|
-
|
|
188
|
-
Args:
|
|
189
|
-
unwrapped_phase: 2D array of unwrapped phase (radians), as
|
|
190
|
-
produced by PhaseUnwrapper.unwrap().
|
|
191
|
-
conncomp: 2D array of connected-component labels from the
|
|
192
|
-
same unwrap() call, same shape. 0 = unreliable
|
|
193
|
-
(not part of any real region).
|
|
194
|
-
bridge_radius: Half-size of the window around each bridge
|
|
195
|
-
endpoint used to compute the robust median phase
|
|
196
|
-
difference. Matches MintPy's own default (50).
|
|
197
|
-
min_region_size: Regions smaller than this (in pixels) are
|
|
198
|
-
excluded from bridging -- too small to trust a
|
|
199
|
-
median estimate from, left as unreliable.
|
|
200
|
-
reference_pixel: Optional (row, col). If given, the region
|
|
201
|
-
CONTAINING this pixel is used as the reference
|
|
202
|
-
(offset 0) instead of always defaulting to the
|
|
203
|
-
largest region. Matters for multi-pair use (e.g.
|
|
204
|
-
SBAS): bridging each pair independently with the
|
|
205
|
-
default (largest-region) reference can anchor
|
|
206
|
-
different pairs to genuinely different real
|
|
207
|
-
locations, since "largest region" isn't
|
|
208
|
-
necessarily the same region pair to pair —
|
|
209
|
-
confirmed directly: the same pixel's bridged
|
|
210
|
-
value varied by tens of radians across pairs
|
|
211
|
-
without this, corrupting the downstream joint
|
|
212
|
-
inversion even though each pair was individually
|
|
213
|
-
correctly bridged. Pass the SAME reference_pixel
|
|
214
|
-
used for SBAS's own referencing to fix this.
|
|
215
|
-
|
|
216
|
-
Returns:
|
|
217
|
-
(corrected_phase, offsets_applied) -- corrected_phase is the
|
|
218
|
-
same shape as unwrapped_phase, with each region's integer-cycle
|
|
219
|
-
offset applied. offsets_applied is a dict mapping region label
|
|
220
|
-
to the real, applied correction in radians (0 for the
|
|
221
|
-
reference region, by definition).
|
|
343
|
+
Bridging unwrapping-error correction.
|
|
222
344
|
"""
|
|
223
345
|
import numpy as np
|
|
224
346
|
from scipy.spatial import cKDTree
|
|
@@ -238,9 +360,7 @@ def bridge_unwrap_regions(
|
|
|
238
360
|
if rp_label == 0 or rp_label not in valid_labels:
|
|
239
361
|
raise ValueError(
|
|
240
362
|
f"reference_pixel {reference_pixel} is not part of any "
|
|
241
|
-
f"valid (reliable, large-enough) region
|
|
242
|
-
f"bridging there. Pick a pixel with conncomp != 0 in a "
|
|
243
|
-
f"region at least min_region_size={min_region_size} pixels."
|
|
363
|
+
f"valid (reliable, large-enough) region."
|
|
244
364
|
)
|
|
245
365
|
reference_label = rp_label
|
|
246
366
|
else:
|
|
@@ -252,7 +372,7 @@ def bridge_unwrap_regions(
|
|
|
252
372
|
remaining = set(valid_labels) - {reference_label}
|
|
253
373
|
|
|
254
374
|
while remaining:
|
|
255
|
-
best = None
|
|
375
|
+
best = None
|
|
256
376
|
for lbl in remaining:
|
|
257
377
|
lbl_points = np.column_stack(np.where(conncomp == lbl))
|
|
258
378
|
for resolved_lbl, ref_points in resolved_points.items():
|
|
@@ -295,64 +415,7 @@ def multilook(
|
|
|
295
415
|
wrapped_phase: Optional[bool] = None,
|
|
296
416
|
) -> Any:
|
|
297
417
|
"""
|
|
298
|
-
Average an array down by the given factor in each dimension
|
|
299
|
-
unwrapping — reduces both pixel count and phase noise, the standard
|
|
300
|
-
technique for improving unwrapping reliability on low-coherence data
|
|
301
|
-
(Eineder 1999; cited directly in ESA's InSAR processing tutorial,
|
|
302
|
-
TM-19: "In cases of low coherence (say 0.1), the number of looks to
|
|
303
|
-
be averaged should increase up to 100").
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
data: 2D array to multilook. Complex (interferogram),
|
|
307
|
-
or real-valued (coherence, wrapped phase,
|
|
308
|
-
unwrapped phase, or any other raster).
|
|
309
|
-
looks_azimuth: Averaging factor along axis 0.
|
|
310
|
-
looks_range: Averaging factor along axis 1.
|
|
311
|
-
wrapped_phase: Required for real-valued input, not optional or
|
|
312
|
-
defaulted — dtype alone cannot tell wrapped phase
|
|
313
|
-
apart from coherence or unwrapped phase, and
|
|
314
|
-
guessing wrong is not a small error:
|
|
315
|
-
|
|
316
|
-
- wrapped_phase=True: circular (complex-exponential)
|
|
317
|
-
averaging. Correct for phase bounded to
|
|
318
|
-
[-pi, pi) — plain arithmetic averaging would be
|
|
319
|
-
biased wherever a look-window straddles the
|
|
320
|
-
wrap boundary (e.g. averaging -3.1 and +3.1 rad
|
|
321
|
-
arithmetically gives ~0, when the true circular
|
|
322
|
-
average is near +-pi).
|
|
323
|
-
- wrapped_phase=False: plain arithmetic averaging.
|
|
324
|
-
Correct for coherence (bounded [0,1], not an
|
|
325
|
-
angle at all) and for unwrapped phase (an
|
|
326
|
-
unbounded real value). Verified directly:
|
|
327
|
-
circular-averaging real unwrapped phase of
|
|
328
|
-
~45 rad silently collapses it to ~1.3 rad,
|
|
329
|
-
destroying the very thing unwrapping produced.
|
|
330
|
-
|
|
331
|
-
Complex input (interferograms) ignores this
|
|
332
|
-
parameter entirely and always averages directly
|
|
333
|
-
as complex numbers — unambiguous, no real/wrapped
|
|
334
|
-
distinction applies.
|
|
335
|
-
|
|
336
|
-
Returns:
|
|
337
|
-
The multilooked array, same dtype family as the input.
|
|
338
|
-
|
|
339
|
-
Raises:
|
|
340
|
-
ValueError: if `data` is real-valued and `wrapped_phase` was not
|
|
341
|
-
explicitly specified — a real, deliberate refusal to guess,
|
|
342
|
-
not an oversight. Silent dtype-based guessing was the
|
|
343
|
-
original design of this function before this exact failure
|
|
344
|
-
mode was found and fixed.
|
|
345
|
-
|
|
346
|
-
Example::
|
|
347
|
-
|
|
348
|
-
# A real interferogram (complex) -- unambiguous
|
|
349
|
-
igram_ml = multilook(interferogram, looks_azimuth=4, looks_range=1)
|
|
350
|
-
|
|
351
|
-
# Wrapped phase -- must say so explicitly
|
|
352
|
-
phase_ml = multilook(wrapped_phase_rad, 4, 1, wrapped_phase=True)
|
|
353
|
-
|
|
354
|
-
# Coherence -- must say so explicitly (plain averaging)
|
|
355
|
-
coherence_ml = multilook(coherence, 4, 1, wrapped_phase=False)
|
|
418
|
+
Average an array down by the given factor in each dimension.
|
|
356
419
|
"""
|
|
357
420
|
import numpy as np
|
|
358
421
|
|
|
@@ -368,13 +431,8 @@ def multilook(
|
|
|
368
431
|
if wrapped_phase is None:
|
|
369
432
|
raise ValueError(
|
|
370
433
|
"multilook() received real-valued input but wrapped_phase was "
|
|
371
|
-
"not specified.
|
|
372
|
-
"
|
|
373
|
-
"from coherence or unwrapped phase (needs plain arithmetic "
|
|
374
|
-
"averaging), and guessing wrong silently corrupts the data "
|
|
375
|
-
"rather than raising an error. Pass wrapped_phase=True for "
|
|
376
|
-
"wrapped phase in radians, or wrapped_phase=False for "
|
|
377
|
-
"coherence, unwrapped phase, or any other real-valued raster."
|
|
434
|
+
"not specified. Pass wrapped_phase=True for wrapped phase, "
|
|
435
|
+
"or wrapped_phase=False for coherence/unwrapped phase."
|
|
378
436
|
)
|
|
379
437
|
|
|
380
438
|
if wrapped_phase:
|
|
@@ -386,50 +444,83 @@ def multilook(
|
|
|
386
444
|
|
|
387
445
|
class PhaseUnwrapper:
|
|
388
446
|
"""
|
|
389
|
-
Phase unwrapping via SNAPHU
|
|
390
|
-
|
|
391
|
-
Args:
|
|
392
|
-
cost_mode: SNAPHU cost function — ``"topo"`` (default, for terrain),
|
|
393
|
-
``"defo"`` (for deformation — less smoothing bias),
|
|
394
|
-
``"smooth"`` (generic smoothness prior),
|
|
395
|
-
``"nostatcosts"`` (uniform cost, fastest, least accurate).
|
|
396
|
-
init_method: ``"mcf"`` (default — Minimum Cost Flow, matches ASF/GAMMA)
|
|
397
|
-
or ``"mst"`` (Minimum Spanning Tree — faster, less optimal).
|
|
398
|
-
|
|
399
|
-
Example::
|
|
400
|
-
|
|
401
|
-
from pygeofetch.insar import InterferogramGenerator, PhaseUnwrapper
|
|
402
|
-
|
|
403
|
-
gen = InterferogramGenerator()
|
|
404
|
-
result = gen.process_pair("ref.tif", "sec.tif", dem="dem.tif")
|
|
405
|
-
|
|
406
|
-
unwrapper = PhaseUnwrapper(cost_mode="defo")
|
|
407
|
-
unwrapped, conncomp = unwrapper.unwrap(
|
|
408
|
-
result.interferogram, result.coherence
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
Choosing cost_mode:
|
|
412
|
-
- Use ``"topo"`` for DEM generation / topographic mapping tasks
|
|
413
|
-
where phase gradients follow terrain.
|
|
414
|
-
- Use ``"defo"`` for deformation monitoring (subsidence, volcanic,
|
|
415
|
-
earthquake) where phase gradients follow displacement, not
|
|
416
|
-
terrain — this is the standard choice for MintPy/SBAS workflows.
|
|
447
|
+
Phase unwrapping via SNAPHU.
|
|
417
448
|
"""
|
|
418
449
|
|
|
419
450
|
def __init__(self, cost_mode: str = "defo", init_method: str = "mcf") -> None:
|
|
420
451
|
valid_costs = ("topo", "defo", "smooth", "nostatcosts")
|
|
421
452
|
if cost_mode not in valid_costs:
|
|
422
|
-
raise ValueError(
|
|
423
|
-
f"cost_mode must be one of {valid_costs}, got {cost_mode!r}"
|
|
424
|
-
)
|
|
453
|
+
raise ValueError(f"cost_mode must be one of {valid_costs}, got {cost_mode!r}")
|
|
425
454
|
valid_inits = ("mcf", "mst")
|
|
426
455
|
if init_method not in valid_inits:
|
|
427
|
-
raise ValueError(
|
|
428
|
-
f"init_method must be one of {valid_inits}, got {init_method!r}"
|
|
429
|
-
)
|
|
456
|
+
raise ValueError(f"init_method must be one of {valid_inits}, got {init_method!r}")
|
|
430
457
|
self._cost_mode = cost_mode
|
|
431
458
|
self._init_method = init_method
|
|
432
459
|
|
|
460
|
+
def _unwrap_cli(self, igram, corr, nlooks, min_conncomp_frac, min_region_size):
|
|
461
|
+
"""
|
|
462
|
+
Fallback unwrapping using the SNAPHU CLI executable.
|
|
463
|
+
Bypasses Python C-extension compilation issues on Windows entirely.
|
|
464
|
+
"""
|
|
465
|
+
import numpy as np
|
|
466
|
+
|
|
467
|
+
cli_path = _ensure_snaphu_cli()
|
|
468
|
+
height, width = igram.shape
|
|
469
|
+
|
|
470
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
471
|
+
tmp = Path(tmp_dir)
|
|
472
|
+
igram_path = tmp / "igram.cpx"
|
|
473
|
+
corr_path = tmp / "corr.cor"
|
|
474
|
+
unw_path = tmp / "unw.unw"
|
|
475
|
+
cc_path = tmp / "conncomp.cc"
|
|
476
|
+
conf_path = tmp / "snaphu.conf"
|
|
477
|
+
|
|
478
|
+
# Write binary files
|
|
479
|
+
igram.tofile(str(igram_path))
|
|
480
|
+
corr.tofile(str(corr_path))
|
|
481
|
+
|
|
482
|
+
# Write config file using relative paths to avoid Windows space issues
|
|
483
|
+
nlooks_int = max(1, int(nlooks))
|
|
484
|
+
conf_text = f"""INFILE igram.cpx
|
|
485
|
+
INFILEFORMAT COMPLEX_DATA
|
|
486
|
+
CORRFILE corr.cor
|
|
487
|
+
CORRFILEFORMAT FLOAT_DATA
|
|
488
|
+
OUTFILE unw.unw
|
|
489
|
+
OUTFILEFORMAT FLOAT_DATA
|
|
490
|
+
CONNCOMPFILE conncomp.cc
|
|
491
|
+
CONNCOMPOUTFORMAT UINT_DATA
|
|
492
|
+
LINELENGTH {width}
|
|
493
|
+
STATCOSTMODE {self._cost_mode.upper()}
|
|
494
|
+
INITMETHOD {self._init_method.upper()}
|
|
495
|
+
NLOOKSRANGE {nlooks_int}
|
|
496
|
+
NLOOKSAZ {nlooks_int}
|
|
497
|
+
"""
|
|
498
|
+
conf_path.write_text(conf_text)
|
|
499
|
+
|
|
500
|
+
# Run SNAPHU from within the temp directory
|
|
501
|
+
try:
|
|
502
|
+
cmd = [cli_path, "-f", "snaphu.conf"]
|
|
503
|
+
result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(tmp), timeout=3600)
|
|
504
|
+
if result.returncode != 0:
|
|
505
|
+
raise RuntimeError(f"SNAPHU CLI failed with code {result.returncode}: {result.stderr}")
|
|
506
|
+
except Exception as e:
|
|
507
|
+
raise RuntimeError(f"SNAPHU CLI execution failed: {e}")
|
|
508
|
+
|
|
509
|
+
# Read outputs
|
|
510
|
+
unw = np.fromfile(str(unw_path), dtype=np.float32).reshape((height, width))
|
|
511
|
+
conncomp = np.fromfile(str(cc_path), dtype=np.uint32).reshape((height, width))
|
|
512
|
+
|
|
513
|
+
# Apply reliability filtering to match Python API behavior
|
|
514
|
+
total_pixels = height * width
|
|
515
|
+
labels, counts = np.unique(conncomp, return_counts=True)
|
|
516
|
+
for lbl, count in zip(labels, counts):
|
|
517
|
+
if lbl == 0:
|
|
518
|
+
continue
|
|
519
|
+
if count < min_region_size or (count / total_pixels) < min_conncomp_frac:
|
|
520
|
+
conncomp[conncomp == lbl] = 0
|
|
521
|
+
|
|
522
|
+
return unw, conncomp.astype(np.int32)
|
|
523
|
+
|
|
433
524
|
def unwrap(
|
|
434
525
|
self,
|
|
435
526
|
interferogram: Any,
|
|
@@ -439,56 +530,11 @@ class PhaseUnwrapper:
|
|
|
439
530
|
min_conncomp_frac: float = 0.01,
|
|
440
531
|
min_region_size: int = 100,
|
|
441
532
|
) -> Tuple[Any, Any]:
|
|
442
|
-
"""
|
|
443
|
-
Unwrap a wrapped interferogram phase.
|
|
444
|
-
|
|
445
|
-
Args:
|
|
446
|
-
interferogram: Complex64 array (wrapped interferogram) OR
|
|
447
|
-
float32 array of wrapped phase in radians.
|
|
448
|
-
coherence: Float32 coherence array, 0-1, same shape.
|
|
449
|
-
nlooks: Effective number of looks (affects statistical
|
|
450
|
-
cost weighting). Use the multilook factor applied
|
|
451
|
-
during interferogram formation.
|
|
452
|
-
mask: Optional boolean array — True = valid, False = masked out
|
|
453
|
-
(e.g. water bodies, layover/shadow regions).
|
|
454
|
-
min_conncomp_frac: Minimum size of a connected component, as a
|
|
455
|
-
fraction of total pixels, for SNAPHU to mark it
|
|
456
|
-
reliable (conncomp != 0). snaphu-py's own
|
|
457
|
-
default (0.01 = 1% of the whole scene) can
|
|
458
|
-
discard genuinely valid, internally-consistent
|
|
459
|
-
regions when the real coherent area is small
|
|
460
|
-
relative to the full scene — confirmed
|
|
461
|
-
directly: a real low-coherence scene with a
|
|
462
|
-
small but real coherent patch reported 100%
|
|
463
|
-
unreliable at the default, purely because that
|
|
464
|
-
real patch fell under the 1%-of-scene pixel
|
|
465
|
-
threshold, not because nothing was actually
|
|
466
|
-
unwrappable. Lower this (e.g. 0.001-0.005) for
|
|
467
|
-
scenes with small AOIs or patchy real coherence.
|
|
468
|
-
min_region_size: Minimum absolute pixel count for the same
|
|
469
|
-
reliability decision — snaphu-py's own default
|
|
470
|
-
is 100. Works alongside min_conncomp_frac (the
|
|
471
|
-
less restrictive of the two typically governs
|
|
472
|
-
on a small scene).
|
|
473
|
-
|
|
474
|
-
Returns:
|
|
475
|
-
(unwrapped_phase, conncomp) — both same shape as input.
|
|
476
|
-
unwrapped_phase: float32 radians, continuous (not wrapped to [-pi, pi))
|
|
477
|
-
conncomp: int32 connected-component labels (0 = unreliable/masked)
|
|
478
|
-
|
|
479
|
-
Example::
|
|
480
|
-
|
|
481
|
-
unwrapped, conncomp = unwrapper.unwrap(igram, coherence, nlooks=4.0)
|
|
482
|
-
# conncomp == 0 marks pixels SNAPHU could not confidently unwrap
|
|
483
|
-
reliable = conncomp > 0
|
|
484
|
-
"""
|
|
485
533
|
np = self._np()
|
|
486
|
-
sx = _require_snaphu()
|
|
487
534
|
|
|
488
535
|
if np.iscomplexobj(interferogram):
|
|
489
536
|
igram = interferogram.astype(np.complex64)
|
|
490
537
|
else:
|
|
491
|
-
# Treat as wrapped phase in radians — convert to unit-magnitude complex
|
|
492
538
|
igram = np.exp(1j * interferogram).astype(np.complex64)
|
|
493
539
|
|
|
494
540
|
corr = np.clip(coherence, 0.0, 1.0).astype(np.float32)
|
|
@@ -496,9 +542,6 @@ class PhaseUnwrapper:
|
|
|
496
542
|
if mask is not None:
|
|
497
543
|
corr = np.where(mask, corr, 0.0).astype(np.float32)
|
|
498
544
|
|
|
499
|
-
# Real, worthwhile defensive check: NaN/Inf coherence (e.g. from
|
|
500
|
-
# an upstream division-by-zero on a fully decorrelated pixel)
|
|
501
|
-
# would otherwise reach SNAPHU as invalid input.
|
|
502
545
|
corr = np.nan_to_num(corr, nan=0.0, posinf=0.0, neginf=0.0)
|
|
503
546
|
|
|
504
547
|
logger.info(
|
|
@@ -510,7 +553,9 @@ class PhaseUnwrapper:
|
|
|
510
553
|
)
|
|
511
554
|
|
|
512
555
|
try:
|
|
513
|
-
|
|
556
|
+
# Try Python API first (fastest)
|
|
557
|
+
import snaphu
|
|
558
|
+
unwrapped, conncomp = snaphu.unwrap(
|
|
514
559
|
igram,
|
|
515
560
|
corr,
|
|
516
561
|
nlooks=nlooks,
|
|
@@ -519,12 +564,17 @@ class PhaseUnwrapper:
|
|
|
519
564
|
min_conncomp_frac=min_conncomp_frac,
|
|
520
565
|
min_region_size=min_region_size,
|
|
521
566
|
)
|
|
567
|
+
except ImportError:
|
|
568
|
+
# Fallback to CLI (solves Windows installation nightmares)
|
|
569
|
+
logger.info("snaphu Python package not found. Falling back to SNAPHU CLI executable...")
|
|
570
|
+
unwrapped, conncomp = self._unwrap_cli(
|
|
571
|
+
igram, corr, nlooks, min_conncomp_frac, min_region_size
|
|
572
|
+
)
|
|
522
573
|
except Exception as exc:
|
|
523
574
|
raise RuntimeError(
|
|
524
575
|
f"SNAPHU unwrapping failed: {exc}\n"
|
|
525
576
|
"Common causes: incompatible array shapes, all-zero coherence, "
|
|
526
|
-
"or insufficient memory for large scenes.
|
|
527
|
-
"the interferogram first to reduce pixel count."
|
|
577
|
+
"or insufficient memory for large scenes."
|
|
528
578
|
) from exc
|
|
529
579
|
|
|
530
580
|
n_unreliable = int(np.sum(conncomp == 0))
|
|
@@ -532,8 +582,7 @@ class PhaseUnwrapper:
|
|
|
532
582
|
if pct > 30:
|
|
533
583
|
logger.warning(
|
|
534
584
|
"%.1f%% of pixels are in the unreliable connected component "
|
|
535
|
-
"(conncomp==0). Consider improving coherence via multilooking
|
|
536
|
-
"or filtering, or check for large decorrelated areas.",
|
|
585
|
+
"(conncomp==0). Consider improving coherence via multilooking.",
|
|
537
586
|
pct,
|
|
538
587
|
)
|
|
539
588
|
else:
|
|
@@ -541,6 +590,87 @@ class PhaseUnwrapper:
|
|
|
541
590
|
|
|
542
591
|
return unwrapped.astype(np.float32), conncomp
|
|
543
592
|
|
|
593
|
+
def unwrap_pair(
|
|
594
|
+
self,
|
|
595
|
+
interferogram: Any,
|
|
596
|
+
coherence: Any,
|
|
597
|
+
profile: Dict[str, Any],
|
|
598
|
+
reference_date: Optional[str] = None,
|
|
599
|
+
secondary_date: Optional[str] = None,
|
|
600
|
+
nlooks: float = 1.0,
|
|
601
|
+
looks_azimuth: int = 1,
|
|
602
|
+
looks_range: int = 1,
|
|
603
|
+
mask: Optional[Any] = None,
|
|
604
|
+
min_conncomp_frac: float = 0.001,
|
|
605
|
+
min_region_size: int = 100,
|
|
606
|
+
) -> UnwrapResult:
|
|
607
|
+
"""
|
|
608
|
+
Unwrap a single interferogram pair and return an UnwrapResult.
|
|
609
|
+
|
|
610
|
+
This is the high-level method that combines unwrap() + result
|
|
611
|
+
packaging. Use this in notebook cells for clean orchestration:
|
|
612
|
+
|
|
613
|
+
result = unwrapper.unwrap_pair(phase_ml, coh_ml, profile,
|
|
614
|
+
looks_azimuth=UNWRAP_LOOKS_AZ,
|
|
615
|
+
looks_range=UNWRAP_LOOKS_RG)
|
|
616
|
+
result.save(output_dir / f"{d1}_{d2}", save_png=True)
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
interferogram: Complex interferogram or wrapped phase (radians).
|
|
620
|
+
coherence: Coherence array (0-1), same shape.
|
|
621
|
+
profile: Rasterio-style profile for georeferencing.
|
|
622
|
+
reference_date, secondary_date: ISO date strings for labeling.
|
|
623
|
+
nlooks: Effective number of looks.
|
|
624
|
+
looks_azimuth, looks_range: Multilook factors used to produce the
|
|
625
|
+
input arrays. Used to correctly scale
|
|
626
|
+
the georeferencing transform.
|
|
627
|
+
mask: Optional boolean mask (True=valid).
|
|
628
|
+
min_conncomp_frac: Minimum connected component fraction.
|
|
629
|
+
min_region_size: Minimum region size in pixels.
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
UnwrapResult with all data packaged and ready to save.
|
|
633
|
+
"""
|
|
634
|
+
import numpy as np
|
|
635
|
+
|
|
636
|
+
unwrapped, conncomp = self.unwrap(
|
|
637
|
+
interferogram, coherence,
|
|
638
|
+
nlooks=nlooks,
|
|
639
|
+
mask=mask,
|
|
640
|
+
min_conncomp_frac=min_conncomp_frac,
|
|
641
|
+
min_region_size=min_region_size,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
reliable_fraction = float(np.mean(conncomp > 0))
|
|
645
|
+
|
|
646
|
+
# Scale the profile transform to match the multilooked arrays
|
|
647
|
+
scaled_profile = dict(profile)
|
|
648
|
+
if looks_azimuth > 1 or looks_range > 1:
|
|
649
|
+
if scaled_profile.get("transform") is not None:
|
|
650
|
+
from rasterio.transform import Affine
|
|
651
|
+
scaled_profile["transform"] = scaled_profile["transform"] * Affine.scale(looks_range, looks_azimuth)
|
|
652
|
+
scaled_profile["height"] = unwrapped.shape[0]
|
|
653
|
+
scaled_profile["width"] = unwrapped.shape[1]
|
|
654
|
+
|
|
655
|
+
return UnwrapResult(
|
|
656
|
+
unwrapped_phase=unwrapped,
|
|
657
|
+
conncomp=conncomp,
|
|
658
|
+
coherence=coherence,
|
|
659
|
+
profile=scaled_profile,
|
|
660
|
+
reference_date=reference_date,
|
|
661
|
+
secondary_date=secondary_date,
|
|
662
|
+
nlooks=nlooks,
|
|
663
|
+
cost_mode=self._cost_mode,
|
|
664
|
+
init_method=self._init_method,
|
|
665
|
+
reliable_fraction=reliable_fraction,
|
|
666
|
+
metadata={
|
|
667
|
+
"cost_mode": self._cost_mode,
|
|
668
|
+
"init_method": self._init_method,
|
|
669
|
+
"nlooks": nlooks,
|
|
670
|
+
"reliable_fraction": reliable_fraction,
|
|
671
|
+
},
|
|
672
|
+
)
|
|
673
|
+
|
|
544
674
|
def unwrap_files(
|
|
545
675
|
self,
|
|
546
676
|
interferogram_path: Union[str, Path],
|
|
@@ -549,26 +679,6 @@ class PhaseUnwrapper:
|
|
|
549
679
|
nlooks: float = 1.0,
|
|
550
680
|
mask_path: Optional[Union[str, Path]] = None,
|
|
551
681
|
) -> Path:
|
|
552
|
-
"""
|
|
553
|
-
Unwrap directly from/to GeoTIFF files, preserving georeferencing.
|
|
554
|
-
|
|
555
|
-
Args:
|
|
556
|
-
interferogram_path: Wrapped phase or complex interferogram GeoTIFF.
|
|
557
|
-
coherence_path: Coherence GeoTIFF (0-1).
|
|
558
|
-
output_path: Output path for the unwrapped phase GeoTIFF.
|
|
559
|
-
nlooks: Effective number of looks.
|
|
560
|
-
mask_path: Optional binary mask GeoTIFF (1=valid, 0=masked).
|
|
561
|
-
|
|
562
|
-
Returns:
|
|
563
|
-
Path to the unwrapped phase GeoTIFF.
|
|
564
|
-
|
|
565
|
-
Example::
|
|
566
|
-
|
|
567
|
-
unwrapper.unwrap_files(
|
|
568
|
-
"wrapped_phase.tif", "coherence.tif",
|
|
569
|
-
output_path="unwrapped.tif", nlooks=4.0,
|
|
570
|
-
)
|
|
571
|
-
"""
|
|
572
682
|
np = self._np()
|
|
573
683
|
try:
|
|
574
684
|
import rasterio
|
|
@@ -622,5 +732,4 @@ class PhaseUnwrapper:
|
|
|
622
732
|
|
|
623
733
|
def _np(self):
|
|
624
734
|
import numpy as np
|
|
625
|
-
|
|
626
735
|
return np
|
pygeofetch/meta.yaml
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
pygeofetch/__init__.py,sha256=
|
|
2
|
-
pygeofetch/meta.yaml,sha256=
|
|
1
|
+
pygeofetch/__init__.py,sha256=BKesKaBedlzLMOOF5DKJ5qFl2_HT0ND_1DzMXYQuSLc,1990
|
|
2
|
+
pygeofetch/meta.yaml,sha256=jLWw6eKXkoNNHhqAMfC8viFsNaPLFS6cVVXBnzFq5u0,1280
|
|
3
3
|
pygeofetch/cli/__init__.py,sha256=QZMIIHEgmjGFieucqq9fZ5uephxP9-jdu1s8Q4qqw8o,30
|
|
4
4
|
pygeofetch/cli/auth_commands.py,sha256=YqbrN_BYcAYCaUIYLpSJ_VIw35tKFUiD9Kc55SWbDWg,7742
|
|
5
5
|
pygeofetch/cli/config_commands.py,sha256=1Pps0HLCLPODMlM6SFV8ru3IKCDpexSTz8uKIXvIIF8,3918
|
|
6
6
|
pygeofetch/cli/download_commands.py,sha256=reQ9vmA50mj1zqechjLHAOe_cjBfa1miqfWa8UiIlxU,11741
|
|
7
7
|
pygeofetch/cli/index_commands.py,sha256=IoLGrsSU-lTcBitFFsg8EtucqSKZtNL-0ODj_I8Twew,10020
|
|
8
|
-
pygeofetch/cli/main.py,sha256=
|
|
8
|
+
pygeofetch/cli/main.py,sha256=wJG74Rbfsolf4xgUTq4S8UTZ0aVrX1IxyRSJzUA1ncA,30335
|
|
9
9
|
pygeofetch/cli/pipeline_process_commands.py,sha256=OvuouT35zZlrC-7hWAJRfaY_aUNk4ScK0Fia9WVB_Gw,6904
|
|
10
10
|
pygeofetch/cli/postprocess_commands.py,sha256=kof_f-kjjvYl5eqPqrcNHSOUM_qVkGdSh6-N4vcx2y4,5825
|
|
11
11
|
pygeofetch/cli/preprocess_commands.py,sha256=cFp1VR03Q1pmCwiBWYmS5lXF3NOnzVAq3XgrZh3rBdU,8696
|
|
@@ -39,7 +39,7 @@ pygeofetch/insar/splitspectrum_diagnostic.py,sha256=_IlfBs__BW7Yz2RwdmWxfrOspe67
|
|
|
39
39
|
pygeofetch/insar/stack_selection.py,sha256=ebFwO063cCT9kn3zLPlkYu6fpnfx-SRC90Ch18E7qJk,13157
|
|
40
40
|
pygeofetch/insar/synthetic.py,sha256=FazdaG_u3GR1qk8022T92W2uxDJPg7OJaUSGY0wR0pw,16248
|
|
41
41
|
pygeofetch/insar/timeseries.py,sha256=EtLJiPySMaUWwyNxI2GyfBdkqB4W-uvL-_E_T_POHz4,32063
|
|
42
|
-
pygeofetch/insar/unwrap.py,sha256=
|
|
42
|
+
pygeofetch/insar/unwrap.py,sha256=3cAEI80KkY98lZ8rHNWlqzr-TXt_6RVWT3auslTi4FA,28218
|
|
43
43
|
pygeofetch/insar/validate.py,sha256=BZaVvaPNllfDzoPwat0vDA4do6H5xh5Ju_g4r5YNogE,10110
|
|
44
44
|
pygeofetch/insar/visualize.py,sha256=D_KlWkpuOvfjke6ThHcMBkZTRwWn1KdBFNL1TBcy6kI,7543
|
|
45
45
|
pygeofetch/insar/workflow.py,sha256=8IxGgabdz9fDLoGAVrJRCdw16GP3sSDSoFPXyv1RXcg,11713
|
|
@@ -104,9 +104,9 @@ pygeofetch/utils/validators.py,sha256=f-k3mGFP4byjimyDymhPEdsz5I16jZacx1HSnwrARj
|
|
|
104
104
|
pygeofetch/viz/__init__.py,sha256=xf2C9IL4n3bv3oxk8RYkogq7nkuNyt18V-urAoXWRQY,449
|
|
105
105
|
pygeofetch/viz/map.py,sha256=vYO99NvTumCAVXVtXcEzMx_cpmH-FvSVNWl8X0PNSgE,15494
|
|
106
106
|
pygeofetch/viz/plot.py,sha256=I16rdOWb80gcjYDsDvTMzuyek7QHgUNOQxY2RejBAPs,53675
|
|
107
|
-
pygeofetch-2.0.
|
|
108
|
-
pygeofetch-2.0.
|
|
109
|
-
pygeofetch-2.0.
|
|
110
|
-
pygeofetch-2.0.
|
|
111
|
-
pygeofetch-2.0.
|
|
112
|
-
pygeofetch-2.0.
|
|
107
|
+
pygeofetch-2.0.4.dist-info/licenses/LICENSE,sha256=zavMyhODD6NvZTmmKFEtATrngwY4CU_NEtY7lhjxGnI,1075
|
|
108
|
+
pygeofetch-2.0.4.dist-info/METADATA,sha256=RtGCL4jStxgOt8TpMJb0WHGaCPdQPpoByRXVqGpKeAo,43254
|
|
109
|
+
pygeofetch-2.0.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
110
|
+
pygeofetch-2.0.4.dist-info/entry_points.txt,sha256=dHiUXLF5xNJ8vgtXZoUQNleEMQUeDPZiKQOE1tW8SDY,55
|
|
111
|
+
pygeofetch-2.0.4.dist-info/top_level.txt,sha256=rOmnZargIfNk99fwVOqt6OJyJvpSTBBAYO5HU37yhHk,11
|
|
112
|
+
pygeofetch-2.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|