PyGeoFetch 2.0.8__py3-none-any.whl → 2.1.0__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 +102 -422
- pygeofetch/meta.yaml +1 -1
- {pygeofetch-2.0.8.dist-info → pygeofetch-2.1.0.dist-info}/METADATA +1 -1
- {pygeofetch-2.0.8.dist-info → pygeofetch-2.1.0.dist-info}/RECORD +10 -10
- {pygeofetch-2.0.8.dist-info → pygeofetch-2.1.0.dist-info}/WHEEL +0 -0
- {pygeofetch-2.0.8.dist-info → pygeofetch-2.1.0.dist-info}/entry_points.txt +0 -0
- {pygeofetch-2.0.8.dist-info → pygeofetch-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {pygeofetch-2.0.8.dist-info → pygeofetch-2.1.0.dist-info}/top_level.txt +0 -0
pygeofetch/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
PyGeoFetch v2.0
|
|
2
|
+
PyGeoFetch v2.1.0 — 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.1.0"
|
|
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.1.0 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.1.0 — Universal Satellite Data Pipeline
|
|
75
75
|
===========================================================
|
|
76
76
|
Unified access to 22+ satellite data providers.
|
|
77
77
|
|
pygeofetch/insar/unwrap.py
CHANGED
|
@@ -29,6 +29,7 @@ import zipfile
|
|
|
29
29
|
import shutil
|
|
30
30
|
import json
|
|
31
31
|
import tempfile
|
|
32
|
+
import io
|
|
32
33
|
from dataclasses import dataclass, field
|
|
33
34
|
from pathlib import Path
|
|
34
35
|
from typing import Any, Dict, Optional, Tuple, Union
|
|
@@ -40,17 +41,11 @@ logger = logging.getLogger("pygeofetch.insar.unwrap")
|
|
|
40
41
|
class UnwrapResult:
|
|
41
42
|
"""
|
|
42
43
|
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
44
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
profile: Dict[str, Any] # rasterio-style profile for georeferencing
|
|
45
|
+
unwrapped_phase: Any
|
|
46
|
+
conncomp: Any
|
|
47
|
+
coherence: Any
|
|
48
|
+
profile: Dict[str, Any]
|
|
54
49
|
reference_date: Optional[str] = None
|
|
55
50
|
secondary_date: Optional[str] = None
|
|
56
51
|
nlooks: float = 1.0
|
|
@@ -65,29 +60,7 @@ class UnwrapResult:
|
|
|
65
60
|
save_png: bool = False,
|
|
66
61
|
mask_unreliable: bool = True,
|
|
67
62
|
) -> 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
63
|
import numpy as np
|
|
90
|
-
|
|
91
64
|
try:
|
|
92
65
|
import rasterio
|
|
93
66
|
except ImportError:
|
|
@@ -110,7 +83,6 @@ class UnwrapResult:
|
|
|
110
83
|
"blockysize": 256,
|
|
111
84
|
}
|
|
112
85
|
|
|
113
|
-
# ── Unwrapped phase ──
|
|
114
86
|
unw_to_save = self.unwrapped_phase.copy()
|
|
115
87
|
if mask_unreliable:
|
|
116
88
|
unw_to_save[self.conncomp == 0] = -9999.0
|
|
@@ -121,89 +93,58 @@ class UnwrapResult:
|
|
|
121
93
|
dst.write(unw_to_save.astype(np.float32)[np.newaxis])
|
|
122
94
|
paths["unwrapped_phase"] = phase_path
|
|
123
95
|
|
|
124
|
-
# ── Connected components ──
|
|
125
96
|
cc_path = out_dir / "conncomp.tif"
|
|
126
97
|
cc_profile = dict(base_profile, dtype="int32", nodata=0)
|
|
127
98
|
with rasterio.open(cc_path, "w", **cc_profile) as dst:
|
|
128
99
|
dst.write(self.conncomp.astype(np.int32)[np.newaxis])
|
|
129
100
|
paths["conncomp"] = cc_path
|
|
130
101
|
|
|
131
|
-
# ── Coherence ──
|
|
132
102
|
coh_path = out_dir / "coherence.tif"
|
|
133
103
|
coh_profile = dict(base_profile, dtype="float32", nodata=-1.0)
|
|
134
104
|
with rasterio.open(coh_path, "w", **coh_profile) as dst:
|
|
135
105
|
dst.write(self.coherence.astype(np.float32)[np.newaxis])
|
|
136
106
|
paths["coherence"] = coh_path
|
|
137
107
|
|
|
138
|
-
logger.info(
|
|
139
|
-
"Unwrap products saved → %s (reliable=%.1f%%)",
|
|
140
|
-
out_dir, self.reliable_fraction * 100,
|
|
141
|
-
)
|
|
108
|
+
logger.info("Unwrap products saved → %s (reliable=%.1f%%)", out_dir, self.reliable_fraction * 100)
|
|
142
109
|
|
|
143
|
-
# ── Optional PNG visualizations ──
|
|
144
110
|
if save_png:
|
|
145
|
-
self._save_pngs(out_dir
|
|
111
|
+
self._save_pngs(out_dir)
|
|
146
112
|
|
|
147
113
|
return paths
|
|
148
114
|
|
|
149
|
-
def _save_pngs(self, out_dir: Path
|
|
150
|
-
"""Save PNG visualizations with robust percentile scaling."""
|
|
115
|
+
def _save_pngs(self, out_dir: Path) -> None:
|
|
151
116
|
import numpy as np
|
|
152
117
|
import matplotlib
|
|
153
|
-
matplotlib.use("Agg")
|
|
118
|
+
matplotlib.use("Agg")
|
|
154
119
|
import matplotlib.pyplot as plt
|
|
155
120
|
|
|
156
|
-
# ── Unwrapped phase PNG ──
|
|
157
121
|
fig, ax = plt.subplots(1, 1, figsize=(14, 8))
|
|
158
122
|
masked_unw = np.ma.masked_where(self.conncomp == 0, self.unwrapped_phase)
|
|
159
123
|
finite_unw = self.unwrapped_phase[self.conncomp > 0]
|
|
160
124
|
if finite_unw.size > 0:
|
|
161
|
-
vmin = float(np.percentile(finite_unw, 2))
|
|
162
|
-
vmax = float(np.percentile(finite_unw, 98))
|
|
125
|
+
vmin, vmax = float(np.percentile(finite_unw, 2)), float(np.percentile(finite_unw, 98))
|
|
163
126
|
else:
|
|
164
127
|
vmin, vmax = -np.pi, np.pi
|
|
165
128
|
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
|
-
)
|
|
129
|
+
ax.set_title(f"Unwrapped Phase (rad)\n{self.reference_date} → {self.secondary_date} | reliable={self.reliable_fraction*100:.1f}%")
|
|
171
130
|
fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
|
172
|
-
|
|
173
|
-
fig.savefig(png_path, dpi=150, bbox_inches="tight")
|
|
131
|
+
fig.savefig(out_dir / "unwrapped_phase.png", dpi=150, bbox_inches="tight")
|
|
174
132
|
plt.close(fig)
|
|
175
133
|
|
|
176
|
-
# ── Connected components PNG ──
|
|
177
134
|
fig, ax = plt.subplots(1, 1, figsize=(14, 8))
|
|
178
135
|
masked_cc = np.ma.masked_where(self.conncomp == 0, self.conncomp)
|
|
179
136
|
ax.imshow(masked_cc, cmap="tab20")
|
|
180
|
-
ax.set_title(
|
|
181
|
-
|
|
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")
|
|
137
|
+
ax.set_title(f"Connected Components\n{self.reference_date} → {self.secondary_date}")
|
|
138
|
+
fig.savefig(out_dir / "conncomp.png", dpi=150, bbox_inches="tight")
|
|
187
139
|
plt.close(fig)
|
|
188
140
|
|
|
189
|
-
logger.info("Unwrap PNGs saved → %s", out_dir)
|
|
190
|
-
|
|
191
141
|
|
|
192
142
|
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
143
|
exe_name = "snaphu.exe" if platform.system() == "Windows" else "snaphu"
|
|
200
|
-
|
|
201
|
-
# 1. System PATH
|
|
202
144
|
path = shutil.which(exe_name)
|
|
203
145
|
if path:
|
|
204
146
|
return path
|
|
205
147
|
|
|
206
|
-
# 2. Local cache
|
|
207
148
|
bin_dir = Path.home() / ".pygeofetch" / "bin"
|
|
208
149
|
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
209
150
|
local_exe = bin_dir / exe_name
|
|
@@ -211,12 +152,8 @@ def _ensure_snaphu_cli() -> str:
|
|
|
211
152
|
if local_exe.exists():
|
|
212
153
|
return str(local_exe)
|
|
213
154
|
|
|
214
|
-
# 3. Download if on Windows
|
|
215
155
|
if platform.system() == "Windows":
|
|
216
156
|
logger.info("SNAPHU executable not found. Downloading pre-compiled binary for Windows...")
|
|
217
|
-
|
|
218
|
-
# The official isce-framework/snaphu-py does not provide Windows wheels/binaries.
|
|
219
|
-
# We use marsfan/SNAPHU-win which reliably hosts pre-compiled Windows binaries.
|
|
220
157
|
api_url = "https://api.github.com/repos/marsfan/SNAPHU-win/releases/latest"
|
|
221
158
|
asset_url = None
|
|
222
159
|
|
|
@@ -224,17 +161,14 @@ def _ensure_snaphu_cli() -> str:
|
|
|
224
161
|
req = urllib.request.Request(api_url, headers={'User-Agent': 'PyGeoFetch'})
|
|
225
162
|
with urllib.request.urlopen(req, timeout=15) as response:
|
|
226
163
|
data = json.loads(response.read().decode())
|
|
227
|
-
|
|
228
164
|
for asset in data.get("assets", []):
|
|
229
|
-
name
|
|
230
|
-
if name.endswith(".zip") and "snaphu" in name:
|
|
165
|
+
if asset["name"].lower().endswith(".zip") and "snaphu" in asset["name"].lower():
|
|
231
166
|
asset_url = asset["browser_download_url"]
|
|
232
167
|
break
|
|
233
168
|
except Exception as e:
|
|
234
169
|
logger.warning(f"Could not query GitHub API for SNAPHU binary: {e}")
|
|
235
170
|
|
|
236
171
|
if not asset_url:
|
|
237
|
-
# Hardcoded fallback to the known working 2.0.7 release
|
|
238
172
|
asset_url = "https://github.com/marsfan/SNAPHU-win/releases/download/2.0.7/snaphu-v2.0.7.zip"
|
|
239
173
|
|
|
240
174
|
logger.info(f"Downloading SNAPHU from {asset_url}...")
|
|
@@ -242,126 +176,82 @@ def _ensure_snaphu_cli() -> str:
|
|
|
242
176
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
243
177
|
zip_path = Path(tmp_dir) / "snaphu.zip"
|
|
244
178
|
urllib.request.urlretrieve(asset_url, str(zip_path))
|
|
245
|
-
|
|
246
179
|
with zipfile.ZipFile(zip_path, 'r') as zf:
|
|
247
|
-
# Look for snaphu.exe inside the zip (might be in a subfolder)
|
|
248
180
|
exe_files = [f for f in zf.namelist() if f.lower().endswith(".exe") and "snaphu" in f.lower()]
|
|
249
181
|
if not exe_files:
|
|
250
182
|
exe_files = [f for f in zf.namelist() if f.lower().endswith(".exe")]
|
|
251
|
-
if not exe_files:
|
|
252
|
-
raise RuntimeError("Downloaded zip did not contain an .exe file.")
|
|
253
|
-
|
|
254
183
|
with zf.open(exe_files[0]) as src, open(local_exe, "wb") as dst:
|
|
255
184
|
shutil.copyfileobj(src, dst)
|
|
256
|
-
|
|
257
185
|
logger.info(f"SNAPHU downloaded successfully to {local_exe}")
|
|
258
186
|
return str(local_exe)
|
|
259
187
|
except Exception as e:
|
|
260
188
|
raise RuntimeError(f"Failed to download/extract SNAPHU executable: {e}")
|
|
261
189
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def goldstein_filter(
|
|
272
|
-
interferogram,
|
|
273
|
-
alpha: float = 0.5,
|
|
274
|
-
tile_size: int = 32,
|
|
275
|
-
overlap: float = 0.5,
|
|
276
|
-
):
|
|
277
|
-
"""
|
|
278
|
-
Adaptive Goldstein phase filter.
|
|
279
|
-
"""
|
|
280
|
-
import numpy as np
|
|
190
|
+
raise RuntimeError(
|
|
191
|
+
"SNAPHU executable not found in PATH.\n"
|
|
192
|
+
"Please install it using your system package manager:\n"
|
|
193
|
+
" Ubuntu/Debian: sudo apt-get install snaphu\n"
|
|
194
|
+
" macOS (Homebrew): brew install snaphu\n"
|
|
195
|
+
"Or install the Python bindings: pip install snaphu"
|
|
196
|
+
)
|
|
197
|
+
|
|
281
198
|
|
|
199
|
+
def goldstein_filter(interferogram, alpha: float = 0.5, tile_size: int = 32, overlap: float = 0.5):
|
|
200
|
+
import numpy as np
|
|
282
201
|
interferogram = np.asarray(interferogram, dtype=np.complex64)
|
|
283
202
|
h, w = interferogram.shape
|
|
284
203
|
step = max(1, int(tile_size * (1 - overlap)))
|
|
285
204
|
window_1d = np.hanning(tile_size)
|
|
286
205
|
window_2d = np.outer(window_1d, window_1d).astype(np.float32)
|
|
287
|
-
|
|
288
206
|
output = np.zeros((h, w), dtype=np.complex64)
|
|
289
207
|
weight_sum = np.zeros((h, w), dtype=np.float32)
|
|
290
|
-
|
|
291
208
|
row_starts = list(range(0, max(1, h - tile_size + 1), step))
|
|
292
209
|
col_starts = list(range(0, max(1, w - tile_size + 1), step))
|
|
293
|
-
if not row_starts or row_starts[-1] + tile_size < h:
|
|
294
|
-
|
|
295
|
-
if not col_starts or col_starts[-1] + tile_size < w:
|
|
296
|
-
col_starts.append(max(0, w - tile_size))
|
|
297
|
-
|
|
210
|
+
if not row_starts or row_starts[-1] + tile_size < h: row_starts.append(max(0, h - tile_size))
|
|
211
|
+
if not col_starts or col_starts[-1] + tile_size < w: col_starts.append(max(0, w - tile_size))
|
|
298
212
|
for row_start in row_starts:
|
|
299
213
|
for col_start in col_starts:
|
|
300
214
|
r_end = min(row_start + tile_size, h)
|
|
301
215
|
c_end = min(col_start + tile_size, w)
|
|
302
216
|
tile = interferogram[row_start:r_end, col_start:c_end]
|
|
303
217
|
th, tw = tile.shape
|
|
304
|
-
if th < 2 or tw < 2:
|
|
305
|
-
continue
|
|
306
|
-
|
|
218
|
+
if th < 2 or tw < 2: continue
|
|
307
219
|
spectrum = np.fft.fft2(tile)
|
|
308
220
|
magnitude = np.abs(spectrum)
|
|
309
221
|
peak = magnitude.max()
|
|
310
|
-
if peak > 0:
|
|
311
|
-
|
|
312
|
-
else:
|
|
313
|
-
filtered_spectrum = spectrum
|
|
222
|
+
if peak > 0: filtered_spectrum = spectrum * (magnitude ** alpha) / (peak ** alpha)
|
|
223
|
+
else: filtered_spectrum = spectrum
|
|
314
224
|
filtered_tile = np.fft.ifft2(filtered_spectrum)
|
|
315
|
-
|
|
316
225
|
tile_window = window_2d[:th, :tw]
|
|
317
226
|
output[row_start:r_end, col_start:c_end] += filtered_tile * tile_window
|
|
318
227
|
weight_sum[row_start:r_end, col_start:c_end] += tile_window
|
|
319
|
-
|
|
320
228
|
valid = weight_sum > 0
|
|
321
229
|
output[valid] /= weight_sum[valid]
|
|
322
230
|
output[~valid] = interferogram[~valid]
|
|
323
231
|
return output.astype(np.complex64)
|
|
324
232
|
|
|
325
233
|
|
|
326
|
-
def bridge_unwrap_regions(
|
|
327
|
-
unwrapped_phase,
|
|
328
|
-
conncomp,
|
|
329
|
-
bridge_radius: int = 50,
|
|
330
|
-
min_region_size: int = 100,
|
|
331
|
-
reference_pixel=None,
|
|
332
|
-
):
|
|
333
|
-
"""
|
|
334
|
-
Bridging unwrapping-error correction.
|
|
335
|
-
"""
|
|
234
|
+
def bridge_unwrap_regions(unwrapped_phase, conncomp, bridge_radius: int = 50, min_region_size: int = 100, reference_pixel=None):
|
|
336
235
|
import numpy as np
|
|
337
236
|
from scipy.spatial import cKDTree
|
|
338
|
-
|
|
339
237
|
labels = np.unique(conncomp)
|
|
340
238
|
labels = labels[labels != 0]
|
|
341
239
|
region_sizes = {lbl: int(np.sum(conncomp == lbl)) for lbl in labels}
|
|
342
240
|
valid_labels = [lbl for lbl in labels if region_sizes[lbl] >= min_region_size]
|
|
343
|
-
|
|
344
|
-
if not valid_labels:
|
|
345
|
-
return unwrapped_phase.copy(), {}
|
|
346
|
-
|
|
241
|
+
if not valid_labels: return unwrapped_phase.copy(), {}
|
|
347
242
|
valid_labels.sort(key=lambda lbl: region_sizes[lbl], reverse=True)
|
|
348
243
|
if reference_pixel is not None:
|
|
349
244
|
rp_row, rp_col = reference_pixel
|
|
350
245
|
rp_label = int(conncomp[rp_row, rp_col])
|
|
351
246
|
if rp_label == 0 or rp_label not in valid_labels:
|
|
352
|
-
raise ValueError(
|
|
353
|
-
f"reference_pixel {reference_pixel} is not part of any "
|
|
354
|
-
f"valid (reliable, large-enough) region."
|
|
355
|
-
)
|
|
247
|
+
raise ValueError(f"reference_pixel {reference_pixel} is not part of any valid region.")
|
|
356
248
|
reference_label = rp_label
|
|
357
249
|
else:
|
|
358
250
|
reference_label = valid_labels[0]
|
|
359
|
-
|
|
360
251
|
corrected = np.array(unwrapped_phase, dtype=np.float64, copy=True)
|
|
361
252
|
resolved_points = {reference_label: np.column_stack(np.where(conncomp == reference_label))}
|
|
362
253
|
offsets_applied = {reference_label: 0.0}
|
|
363
254
|
remaining = set(valid_labels) - {reference_label}
|
|
364
|
-
|
|
365
255
|
while remaining:
|
|
366
256
|
best = None
|
|
367
257
|
for lbl in remaining:
|
|
@@ -372,11 +262,8 @@ def bridge_unwrap_regions(
|
|
|
372
262
|
min_i = int(np.argmin(dist))
|
|
373
263
|
if best is None or dist[min_i] < best[0]:
|
|
374
264
|
best = (dist[min_i], lbl, resolved_lbl, lbl_points[min_i], ref_points[idx[min_i]])
|
|
375
|
-
|
|
376
|
-
if best is None:
|
|
377
|
-
break
|
|
265
|
+
if best is None: break
|
|
378
266
|
_, lbl, target_lbl, point_a, point_b = best
|
|
379
|
-
|
|
380
267
|
def _local_median(point, region_mask):
|
|
381
268
|
r, c = point
|
|
382
269
|
r0, r1 = max(0, r - bridge_radius), r + bridge_radius + 1
|
|
@@ -385,47 +272,28 @@ def bridge_unwrap_regions(
|
|
|
385
272
|
window_mask = region_mask[r0:r1, c0:c1]
|
|
386
273
|
values = window_phase[window_mask]
|
|
387
274
|
return float(np.median(values)) if len(values) > 0 else float(corrected[r, c])
|
|
388
|
-
|
|
389
275
|
median_a = _local_median(point_a, conncomp == lbl)
|
|
390
276
|
median_b = _local_median(point_b, conncomp == target_lbl)
|
|
391
277
|
raw_offset = median_b - median_a
|
|
392
278
|
integer_offset = 2 * np.pi * np.round(raw_offset / (2 * np.pi))
|
|
393
|
-
|
|
394
279
|
corrected[conncomp == lbl] += integer_offset
|
|
395
280
|
offsets_applied[lbl] = integer_offset
|
|
396
281
|
resolved_points[lbl] = np.column_stack(np.where(conncomp == lbl))
|
|
397
282
|
remaining.remove(lbl)
|
|
398
|
-
|
|
399
283
|
return corrected.astype(np.float32), offsets_applied
|
|
400
284
|
|
|
401
285
|
|
|
402
|
-
def multilook(
|
|
403
|
-
data: Any,
|
|
404
|
-
looks_azimuth: int = 4,
|
|
405
|
-
looks_range: int = 1,
|
|
406
|
-
wrapped_phase: Optional[bool] = None,
|
|
407
|
-
) -> Any:
|
|
408
|
-
"""
|
|
409
|
-
Average an array down by the given factor in each dimension.
|
|
410
|
-
"""
|
|
286
|
+
def multilook(data: Any, looks_azimuth: int = 4, looks_range: int = 1, wrapped_phase: Optional[bool] = None) -> Any:
|
|
411
287
|
import numpy as np
|
|
412
|
-
|
|
413
288
|
h, w = data.shape
|
|
414
289
|
h_ml = (h // looks_azimuth) * looks_azimuth
|
|
415
290
|
w_ml = (w // looks_range) * looks_range
|
|
416
291
|
trimmed = data[:h_ml, :w_ml]
|
|
417
292
|
reshaped_shape = (h_ml // looks_azimuth, looks_azimuth, w_ml // looks_range, looks_range)
|
|
418
|
-
|
|
419
293
|
if np.iscomplexobj(trimmed):
|
|
420
294
|
return trimmed.reshape(reshaped_shape).mean(axis=(1, 3))
|
|
421
|
-
|
|
422
295
|
if wrapped_phase is None:
|
|
423
|
-
raise ValueError(
|
|
424
|
-
"multilook() received real-valued input but wrapped_phase was "
|
|
425
|
-
"not specified. Pass wrapped_phase=True for wrapped phase, "
|
|
426
|
-
"or wrapped_phase=False for coherence/unwrapped phase."
|
|
427
|
-
)
|
|
428
|
-
|
|
296
|
+
raise ValueError("multilook() received real-valued input but wrapped_phase was not specified.")
|
|
429
297
|
if wrapped_phase:
|
|
430
298
|
reshaped = np.exp(1j * trimmed).reshape(reshaped_shape)
|
|
431
299
|
return np.angle(reshaped.mean(axis=(1, 3)))
|
|
@@ -434,27 +302,16 @@ def multilook(
|
|
|
434
302
|
|
|
435
303
|
|
|
436
304
|
class PhaseUnwrapper:
|
|
437
|
-
"""
|
|
438
|
-
Phase unwrapping via SNAPHU.
|
|
439
|
-
"""
|
|
440
|
-
|
|
441
305
|
def __init__(self, cost_mode: str = "defo", init_method: str = "mcf") -> None:
|
|
442
306
|
valid_costs = ("topo", "defo", "smooth", "nostatcosts")
|
|
443
|
-
if cost_mode not in valid_costs:
|
|
444
|
-
raise ValueError(f"cost_mode must be one of {valid_costs}, got {cost_mode!r}")
|
|
307
|
+
if cost_mode not in valid_costs: raise ValueError(f"cost_mode must be one of {valid_costs}")
|
|
445
308
|
valid_inits = ("mcf", "mst")
|
|
446
|
-
if init_method not in valid_inits:
|
|
447
|
-
raise ValueError(f"init_method must be one of {valid_inits}, got {init_method!r}")
|
|
309
|
+
if init_method not in valid_inits: raise ValueError(f"init_method must be one of {valid_inits}")
|
|
448
310
|
self._cost_mode = cost_mode
|
|
449
311
|
self._init_method = init_method
|
|
450
312
|
|
|
451
313
|
def _unwrap_cli(self, igram, corr, nlooks, min_conncomp_frac, min_region_size):
|
|
452
|
-
"""
|
|
453
|
-
Fallback unwrapping using the SNAPHU CLI executable.
|
|
454
|
-
Includes auto-repair for the infamous 0xC0000135 (STATUS_DLL_NOT_FOUND) error.
|
|
455
|
-
"""
|
|
456
314
|
import numpy as np
|
|
457
|
-
|
|
458
315
|
cli_path = _ensure_snaphu_cli()
|
|
459
316
|
height, width = igram.shape
|
|
460
317
|
bin_dir = Path(cli_path).parent
|
|
@@ -466,27 +323,24 @@ class PhaseUnwrapper:
|
|
|
466
323
|
unw_path = tmp / "unw.unw"
|
|
467
324
|
cc_path = tmp / "conncomp.cc"
|
|
468
325
|
conf_path = tmp / "snaphu.conf"
|
|
469
|
-
|
|
470
326
|
igram.tofile(str(igram_path))
|
|
471
327
|
corr.tofile(str(corr_path))
|
|
472
|
-
|
|
473
328
|
nlooks_int = max(1, int(nlooks))
|
|
474
329
|
conf_text = f"""INFILE igram.cpx
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
330
|
+
INFILEFORMAT COMPLEX_DATA
|
|
331
|
+
CORRFILE corr.cor
|
|
332
|
+
CORRFILEFORMAT FLOAT_DATA
|
|
333
|
+
OUTFILE unw.unw
|
|
334
|
+
OUTFILEFORMAT FLOAT_DATA
|
|
335
|
+
CONNCOMPFILE conncomp.cc
|
|
336
|
+
CONNCOMPOUTFORMAT UINT_DATA
|
|
337
|
+
LINELENGTH {width}
|
|
338
|
+
STATCOSTMODE {self._cost_mode.upper()}
|
|
339
|
+
INITMETHOD {self._init_method.upper()}
|
|
340
|
+
NLOOKSRANGE {nlooks_int}
|
|
341
|
+
NLOOKSAZ {nlooks_int}
|
|
342
|
+
"""
|
|
488
343
|
conf_path.write_text(conf_text)
|
|
489
|
-
|
|
490
344
|
cmd = [cli_path, "-f", "snaphu.conf"]
|
|
491
345
|
result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(tmp), timeout=3600)
|
|
492
346
|
return result, unw_path, cc_path
|
|
@@ -496,12 +350,8 @@ class PhaseUnwrapper:
|
|
|
496
350
|
|
|
497
351
|
# AUTO-REPAIR: Catch Windows STATUS_DLL_NOT_FOUND (0xC0000135)
|
|
498
352
|
if result.returncode in (3221225781, -1073741515):
|
|
499
|
-
logger.warning(
|
|
500
|
-
"SNAPHU crashed with STATUS_DLL_NOT_FOUND (0xC0000135). "
|
|
501
|
-
"Auto-bundling MSVC runtime DLLs..."
|
|
502
|
-
)
|
|
353
|
+
logger.warning("SNAPHU crashed with STATUS_DLL_NOT_FOUND (0xC0000135). Auto-bundling MSVC runtime DLLs...")
|
|
503
354
|
self._ensure_msvc_runtime(bin_dir)
|
|
504
|
-
|
|
505
355
|
logger.info("Retrying SNAPHU with bundled DLLs...")
|
|
506
356
|
result, unw_path, cc_path = _run_snaphu(tmp_dir)
|
|
507
357
|
|
|
@@ -514,191 +364,72 @@ class PhaseUnwrapper:
|
|
|
514
364
|
total_pixels = height * width
|
|
515
365
|
labels, counts = np.unique(conncomp, return_counts=True)
|
|
516
366
|
for lbl, count in zip(labels, counts):
|
|
517
|
-
if lbl == 0:
|
|
518
|
-
continue
|
|
367
|
+
if lbl == 0: continue
|
|
519
368
|
if count < min_region_size or (count / total_pixels) < min_conncomp_frac:
|
|
520
369
|
conncomp[conncomp == lbl] = 0
|
|
521
|
-
|
|
522
370
|
return unw, conncomp.astype(np.int32)
|
|
523
371
|
|
|
524
372
|
def _ensure_msvc_runtime(self, bin_dir: Path):
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
already present in the user's Python environment (e.g., from
|
|
528
|
-
numpy/scipy wheels) and copying them next to snaphu.exe.
|
|
529
|
-
"""
|
|
530
|
-
import sys
|
|
531
|
-
import shutil
|
|
532
|
-
|
|
533
|
-
required_dlls = ["vcruntime140.dll", "msvcp140.dll"]
|
|
373
|
+
import shutil, zipfile, io, urllib.request
|
|
374
|
+
required_dlls = ["vcruntime140.dll", "msvcp140.dll", "vcruntime140_1.dll"]
|
|
534
375
|
missing = [dll for dll in required_dlls if not (bin_dir / dll).exists()]
|
|
376
|
+
if not missing: return
|
|
535
377
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
logger.info(f"SNAPHU requires {missing}. Searching local Python environment...")
|
|
378
|
+
logger.info(f"SNAPHU requires {missing}. Downloading directly from NuGet (no admin required)...")
|
|
379
|
+
nuget_url = "https://www.nuget.org/api/v2/package/VCRuntime.140"
|
|
540
380
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
for
|
|
548
|
-
if
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if len(found) == len(missing):
|
|
566
|
-
logger.info("Found MSVC DLLs in local Python environment. Copying to SNAPHU bin...")
|
|
567
|
-
for dll, src in found.items():
|
|
568
|
-
shutil.copy2(src, bin_dir / dll)
|
|
569
|
-
return
|
|
570
|
-
|
|
571
|
-
raise RuntimeError(
|
|
572
|
-
f"SNAPHU requires the Microsoft Visual C++ Redistributable DLLs ({missing}), "
|
|
573
|
-
"but they could not be found in your Python environment.\n"
|
|
574
|
-
"Please install the Microsoft Visual C++ Redistributable manually from:\n"
|
|
575
|
-
"https://aka.ms/vs/17/release/vc_redist.x64.exe\n"
|
|
576
|
-
"Then restart your Jupyter kernel and try again."
|
|
577
|
-
)
|
|
578
|
-
|
|
579
|
-
def unwrap(
|
|
580
|
-
self,
|
|
581
|
-
interferogram: Any,
|
|
582
|
-
coherence: Any,
|
|
583
|
-
nlooks: float = 1.0,
|
|
584
|
-
mask: Optional[Any] = None,
|
|
585
|
-
min_conncomp_frac: float = 0.01,
|
|
586
|
-
min_region_size: int = 100,
|
|
587
|
-
) -> Tuple[Any, Any]:
|
|
588
|
-
np = self._np()
|
|
589
|
-
|
|
590
|
-
if np.iscomplexobj(interferogram):
|
|
591
|
-
igram = interferogram.astype(np.complex64)
|
|
592
|
-
else:
|
|
593
|
-
igram = np.exp(1j * interferogram).astype(np.complex64)
|
|
381
|
+
try:
|
|
382
|
+
req = urllib.request.Request(nuget_url, headers={'User-Agent': 'PyGeoFetch'})
|
|
383
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
384
|
+
nupkg_data = response.read()
|
|
385
|
+
extracted = 0
|
|
386
|
+
with zipfile.ZipFile(io.BytesIO(nupkg_data)) as zf:
|
|
387
|
+
for name in zf.namelist():
|
|
388
|
+
if "win-x64/native/" in name and name.lower().endswith(".dll"):
|
|
389
|
+
dll_name = Path(name).name
|
|
390
|
+
if dll_name.lower() in [m.lower() for m in missing]:
|
|
391
|
+
with zf.open(name) as src, open(bin_dir / dll_name, "wb") as dst:
|
|
392
|
+
shutil.copyfileobj(src, dst)
|
|
393
|
+
extracted += 1
|
|
394
|
+
if extracted > 0:
|
|
395
|
+
logger.info(f"Successfully extracted {extracted} MSVC DLLs to {bin_dir}. Retrying SNAPHU...")
|
|
396
|
+
return
|
|
397
|
+
else:
|
|
398
|
+
raise RuntimeError("Could not find required DLLs in the NuGet package.")
|
|
399
|
+
except Exception as e:
|
|
400
|
+
raise RuntimeError(
|
|
401
|
+
f"Failed to download/extract MSVC runtime DLLs from NuGet: {e}\n"
|
|
402
|
+
"As a last resort, please install the Microsoft Visual C++ Redistributable manually from:\n"
|
|
403
|
+
"https://aka.ms/vs/17/release/vc_redist.x64.exe\nThen restart your Jupyter kernel."
|
|
404
|
+
)
|
|
594
405
|
|
|
406
|
+
def unwrap(self, interferogram: Any, coherence: Any, nlooks: float = 1.0, mask: Optional[Any] = None, min_conncomp_frac: float = 0.01, min_region_size: int = 100) -> Tuple[Any, Any]:
|
|
407
|
+
np = self._np()
|
|
408
|
+
if np.iscomplexobj(interferogram): igram = interferogram.astype(np.complex64)
|
|
409
|
+
else: igram = np.exp(1j * interferogram).astype(np.complex64)
|
|
595
410
|
corr = np.clip(coherence, 0.0, 1.0).astype(np.float32)
|
|
596
|
-
|
|
597
|
-
if mask is not None:
|
|
598
|
-
corr = np.where(mask, corr, 0.0).astype(np.float32)
|
|
599
|
-
|
|
411
|
+
if mask is not None: corr = np.where(mask, corr, 0.0).astype(np.float32)
|
|
600
412
|
corr = np.nan_to_num(corr, nan=0.0, posinf=0.0, neginf=0.0)
|
|
601
|
-
|
|
602
|
-
logger.info(
|
|
603
|
-
"Unwrapping %s pixels (cost=%s, init=%s, nlooks=%.1f)",
|
|
604
|
-
f"{igram.shape[0]}x{igram.shape[1]}",
|
|
605
|
-
self._cost_mode,
|
|
606
|
-
self._init_method,
|
|
607
|
-
nlooks,
|
|
608
|
-
)
|
|
609
|
-
|
|
413
|
+
logger.info("Unwrapping %s pixels (cost=%s, init=%s, nlooks=%.1f)", f"{igram.shape[0]}x{igram.shape[1]}", self._cost_mode, self._init_method, nlooks)
|
|
610
414
|
try:
|
|
611
|
-
# Try Python API first (fastest)
|
|
612
415
|
import snaphu
|
|
613
|
-
unwrapped, conncomp = snaphu.unwrap(
|
|
614
|
-
igram,
|
|
615
|
-
corr,
|
|
616
|
-
nlooks=nlooks,
|
|
617
|
-
cost=self._cost_mode,
|
|
618
|
-
init=self._init_method,
|
|
619
|
-
min_conncomp_frac=min_conncomp_frac,
|
|
620
|
-
min_region_size=min_region_size,
|
|
621
|
-
)
|
|
416
|
+
unwrapped, conncomp = snaphu.unwrap(igram, corr, nlooks=nlooks, cost=self._cost_mode, init=self._init_method, min_conncomp_frac=min_conncomp_frac, min_region_size=min_region_size)
|
|
622
417
|
except ImportError:
|
|
623
|
-
# Fallback to CLI (solves Windows installation nightmares)
|
|
624
418
|
logger.info("snaphu Python package not found. Falling back to SNAPHU CLI executable...")
|
|
625
|
-
unwrapped, conncomp = self._unwrap_cli(
|
|
626
|
-
igram, corr, nlooks, min_conncomp_frac, min_region_size
|
|
627
|
-
)
|
|
419
|
+
unwrapped, conncomp = self._unwrap_cli(igram, corr, nlooks, min_conncomp_frac, min_region_size)
|
|
628
420
|
except Exception as exc:
|
|
629
|
-
raise RuntimeError(
|
|
630
|
-
f"SNAPHU unwrapping failed: {exc}\n"
|
|
631
|
-
"Common causes: incompatible array shapes, all-zero coherence, "
|
|
632
|
-
"or insufficient memory for large scenes."
|
|
633
|
-
) from exc
|
|
421
|
+
raise RuntimeError(f"SNAPHU unwrapping failed: {exc}") from exc
|
|
634
422
|
|
|
635
423
|
n_unreliable = int(np.sum(conncomp == 0))
|
|
636
424
|
pct = 100 * n_unreliable / conncomp.size
|
|
637
|
-
if pct > 30:
|
|
638
|
-
|
|
639
|
-
"%.1f%% of pixels are in the unreliable connected component "
|
|
640
|
-
"(conncomp==0). Consider improving coherence via multilooking.",
|
|
641
|
-
pct,
|
|
642
|
-
)
|
|
643
|
-
else:
|
|
644
|
-
logger.info("Unwrapping complete — %.1f%% unreliable pixels", pct)
|
|
645
|
-
|
|
425
|
+
if pct > 30: logger.warning("%.1f%% of pixels are in the unreliable connected component (conncomp==0).", pct)
|
|
426
|
+
else: logger.info("Unwrapping complete — %.1f%% unreliable pixels", pct)
|
|
646
427
|
return unwrapped.astype(np.float32), conncomp
|
|
647
428
|
|
|
648
|
-
def unwrap_pair(
|
|
649
|
-
self,
|
|
650
|
-
interferogram: Any,
|
|
651
|
-
coherence: Any,
|
|
652
|
-
profile: Dict[str, Any],
|
|
653
|
-
reference_date: Optional[str] = None,
|
|
654
|
-
secondary_date: Optional[str] = None,
|
|
655
|
-
nlooks: float = 1.0,
|
|
656
|
-
looks_azimuth: int = 1,
|
|
657
|
-
looks_range: int = 1,
|
|
658
|
-
mask: Optional[Any] = None,
|
|
659
|
-
min_conncomp_frac: float = 0.001,
|
|
660
|
-
min_region_size: int = 100,
|
|
661
|
-
) -> UnwrapResult:
|
|
662
|
-
"""
|
|
663
|
-
Unwrap a single interferogram pair and return an UnwrapResult.
|
|
664
|
-
|
|
665
|
-
This is the high-level method that combines unwrap() + result
|
|
666
|
-
packaging. Use this in notebook cells for clean orchestration:
|
|
667
|
-
|
|
668
|
-
result = unwrapper.unwrap_pair(phase_ml, coh_ml, profile,
|
|
669
|
-
looks_azimuth=UNWRAP_LOOKS_AZ,
|
|
670
|
-
looks_range=UNWRAP_LOOKS_RG)
|
|
671
|
-
result.save(output_dir / f"{d1}_{d2}", save_png=True)
|
|
672
|
-
|
|
673
|
-
Args:
|
|
674
|
-
interferogram: Complex interferogram or wrapped phase (radians).
|
|
675
|
-
coherence: Coherence array (0-1), same shape.
|
|
676
|
-
profile: Rasterio-style profile for georeferencing.
|
|
677
|
-
reference_date, secondary_date: ISO date strings for labeling.
|
|
678
|
-
nlooks: Effective number of looks.
|
|
679
|
-
looks_azimuth, looks_range: Multilook factors used to produce the
|
|
680
|
-
input arrays. Used to correctly scale
|
|
681
|
-
the georeferencing transform.
|
|
682
|
-
mask: Optional boolean mask (True=valid).
|
|
683
|
-
min_conncomp_frac: Minimum connected component fraction.
|
|
684
|
-
min_region_size: Minimum region size in pixels.
|
|
685
|
-
|
|
686
|
-
Returns:
|
|
687
|
-
UnwrapResult with all data packaged and ready to save.
|
|
688
|
-
"""
|
|
429
|
+
def unwrap_pair(self, interferogram: Any, coherence: Any, profile: Dict[str, Any], reference_date: Optional[str] = None, secondary_date: Optional[str] = None, nlooks: float = 1.0, looks_azimuth: int = 1, looks_range: int = 1, mask: Optional[Any] = None, min_conncomp_frac: float = 0.001, min_region_size: int = 100) -> UnwrapResult:
|
|
689
430
|
import numpy as np
|
|
690
|
-
|
|
691
|
-
unwrapped, conncomp = self.unwrap(
|
|
692
|
-
interferogram, coherence,
|
|
693
|
-
nlooks=nlooks,
|
|
694
|
-
mask=mask,
|
|
695
|
-
min_conncomp_frac=min_conncomp_frac,
|
|
696
|
-
min_region_size=min_region_size,
|
|
697
|
-
)
|
|
698
|
-
|
|
431
|
+
unwrapped, conncomp = self.unwrap(interferogram, coherence, nlooks=nlooks, mask=mask, min_conncomp_frac=min_conncomp_frac, min_region_size=min_region_size)
|
|
699
432
|
reliable_fraction = float(np.mean(conncomp > 0))
|
|
700
|
-
|
|
701
|
-
# Scale the profile transform to match the multilooked arrays
|
|
702
433
|
scaled_profile = dict(profile)
|
|
703
434
|
if looks_azimuth > 1 or looks_range > 1:
|
|
704
435
|
if scaled_profile.get("transform") is not None:
|
|
@@ -706,83 +437,32 @@ class PhaseUnwrapper:
|
|
|
706
437
|
scaled_profile["transform"] = scaled_profile["transform"] * Affine.scale(looks_range, looks_azimuth)
|
|
707
438
|
scaled_profile["height"] = unwrapped.shape[0]
|
|
708
439
|
scaled_profile["width"] = unwrapped.shape[1]
|
|
440
|
+
return UnwrapResult(unwrapped_phase=unwrapped, conncomp=conncomp, coherence=coherence, profile=scaled_profile, reference_date=reference_date, secondary_date=secondary_date, nlooks=nlooks, cost_mode=self._cost_mode, init_method=self._init_method, reliable_fraction=reliable_fraction)
|
|
709
441
|
|
|
710
|
-
|
|
711
|
-
unwrapped_phase=unwrapped,
|
|
712
|
-
conncomp=conncomp,
|
|
713
|
-
coherence=coherence,
|
|
714
|
-
profile=scaled_profile,
|
|
715
|
-
reference_date=reference_date,
|
|
716
|
-
secondary_date=secondary_date,
|
|
717
|
-
nlooks=nlooks,
|
|
718
|
-
cost_mode=self._cost_mode,
|
|
719
|
-
init_method=self._init_method,
|
|
720
|
-
reliable_fraction=reliable_fraction,
|
|
721
|
-
metadata={
|
|
722
|
-
"cost_mode": self._cost_mode,
|
|
723
|
-
"init_method": self._init_method,
|
|
724
|
-
"nlooks": nlooks,
|
|
725
|
-
"reliable_fraction": reliable_fraction,
|
|
726
|
-
},
|
|
727
|
-
)
|
|
728
|
-
|
|
729
|
-
def unwrap_files(
|
|
730
|
-
self,
|
|
731
|
-
interferogram_path: Union[str, Path],
|
|
732
|
-
coherence_path: Union[str, Path],
|
|
733
|
-
output_path: Union[str, Path],
|
|
734
|
-
nlooks: float = 1.0,
|
|
735
|
-
mask_path: Optional[Union[str, Path]] = None,
|
|
736
|
-
) -> Path:
|
|
442
|
+
def unwrap_files(self, interferogram_path: Union[str, Path], coherence_path: Union[str, Path], output_path: Union[str, Path], nlooks: float = 1.0, mask_path: Optional[Union[str, Path]] = None) -> Path:
|
|
737
443
|
np = self._np()
|
|
738
|
-
try:
|
|
739
|
-
|
|
740
|
-
except ImportError:
|
|
741
|
-
raise ImportError('rasterio required: pip install "pygeofetch[geo]"')
|
|
742
|
-
|
|
444
|
+
try: import rasterio
|
|
445
|
+
except ImportError: raise ImportError('rasterio required: pip install "pygeofetch[geo]"')
|
|
743
446
|
with rasterio.open(interferogram_path) as src:
|
|
744
447
|
profile = src.profile.copy()
|
|
745
448
|
phase = src.read(1).astype(np.float32)
|
|
746
|
-
|
|
747
449
|
with rasterio.open(coherence_path) as src:
|
|
748
450
|
coherence = src.read(1).astype(np.float32)
|
|
749
|
-
|
|
750
451
|
mask = None
|
|
751
452
|
if mask_path:
|
|
752
|
-
with rasterio.open(mask_path) as src:
|
|
753
|
-
mask = src.read(1).astype(bool)
|
|
754
|
-
|
|
453
|
+
with rasterio.open(mask_path) as src: mask = src.read(1).astype(bool)
|
|
755
454
|
unwrapped, conncomp = self.unwrap(phase, coherence, nlooks=nlooks, mask=mask)
|
|
756
|
-
|
|
757
455
|
out_path = Path(output_path)
|
|
758
456
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
759
|
-
|
|
760
|
-
out_profile = {
|
|
761
|
-
"driver": "GTiff",
|
|
762
|
-
"dtype": "float32",
|
|
763
|
-
"count": 1,
|
|
764
|
-
"height": unwrapped.shape[0],
|
|
765
|
-
"width": unwrapped.shape[1],
|
|
766
|
-
"crs": profile.get("crs"),
|
|
767
|
-
"transform": profile.get("transform"),
|
|
768
|
-
"nodata": -9999.0,
|
|
769
|
-
"compress": "deflate",
|
|
770
|
-
"tiled": True,
|
|
771
|
-
"blockxsize": 256,
|
|
772
|
-
"blockysize": 256,
|
|
773
|
-
}
|
|
457
|
+
out_profile = {"driver": "GTiff", "dtype": "float32", "count": 1, "height": unwrapped.shape[0], "width": unwrapped.shape[1], "crs": profile.get("crs"), "transform": profile.get("transform"), "nodata": -9999.0, "compress": "deflate", "tiled": True, "blockxsize": 256, "blockysize": 256}
|
|
774
458
|
with rasterio.open(out_path, "w", **out_profile) as dst:
|
|
775
459
|
dst.write(unwrapped[np.newaxis])
|
|
776
460
|
dst.update_tags(1, description="unwrapped_phase_radians")
|
|
777
|
-
|
|
778
461
|
conncomp_path = out_path.parent / f"{out_path.stem}_conncomp.tif"
|
|
779
462
|
cc_profile = dict(out_profile, dtype="int32", nodata=0)
|
|
780
463
|
with rasterio.open(conncomp_path, "w", **cc_profile) as dst:
|
|
781
464
|
dst.write(conncomp.astype(np.int32)[np.newaxis])
|
|
782
|
-
|
|
783
|
-
logger.info(
|
|
784
|
-
"Unwrapped phase → %s (conncomp → %s)", out_path.name, conncomp_path.name
|
|
785
|
-
)
|
|
465
|
+
logger.info("Unwrapped phase → %s (conncomp → %s)", out_path.name, conncomp_path.name)
|
|
786
466
|
return out_path
|
|
787
467
|
|
|
788
468
|
def _np(self):
|
pygeofetch/meta.yaml
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
pygeofetch/__init__.py,sha256=
|
|
2
|
-
pygeofetch/meta.yaml,sha256=
|
|
1
|
+
pygeofetch/__init__.py,sha256=L8N1Q9HeHj-WQl0W1f8EFZJ2OjkBFszL4pR5p8sSo5k,1990
|
|
2
|
+
pygeofetch/meta.yaml,sha256=ymM1Zwx7Acs5nqHhu8wSkS-5_FHZvedmax-CdxbXAjI,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=OuNJCr8oWa3Gtk1ECDsOhriQfSIl2l7SL5DAdl6CCVc,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=FqvvqpxlmWZ7WfkCQa05vers8LaFO-a8o1QMW3YszKU,22832
|
|
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.1.0.dist-info/licenses/LICENSE,sha256=zavMyhODD6NvZTmmKFEtATrngwY4CU_NEtY7lhjxGnI,1075
|
|
108
|
+
pygeofetch-2.1.0.dist-info/METADATA,sha256=YIRKj3ve8llHulK2xAeCY27rzphU2aPOa26dBldgeaM,43254
|
|
109
|
+
pygeofetch-2.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
110
|
+
pygeofetch-2.1.0.dist-info/entry_points.txt,sha256=dHiUXLF5xNJ8vgtXZoUQNleEMQUeDPZiKQOE1tW8SDY,55
|
|
111
|
+
pygeofetch-2.1.0.dist-info/top_level.txt,sha256=rOmnZargIfNk99fwVOqt6OJyJvpSTBBAYO5HU37yhHk,11
|
|
112
|
+
pygeofetch-2.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|