PyGeoFetch 2.0.0__py3-none-any.whl → 2.0.1__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/coregister.py +140 -52
- pygeofetch/insar/esd.py +269 -241
- pygeofetch/insar/interferogram.py +117 -31
- pygeofetch/meta.yaml +1 -1
- {pygeofetch-2.0.0.dist-info → pygeofetch-2.0.1.dist-info}/METADATA +2 -2
- {pygeofetch-2.0.0.dist-info → pygeofetch-2.0.1.dist-info}/RECORD +12 -12
- {pygeofetch-2.0.0.dist-info → pygeofetch-2.0.1.dist-info}/WHEEL +0 -0
- {pygeofetch-2.0.0.dist-info → pygeofetch-2.0.1.dist-info}/entry_points.txt +0 -0
- {pygeofetch-2.0.0.dist-info → pygeofetch-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {pygeofetch-2.0.0.dist-info → pygeofetch-2.0.1.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.1 — 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.1"
|
|
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.1 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.1 — Universal Satellite Data Pipeline
|
|
75
75
|
===========================================================
|
|
76
76
|
Unified access to 22+ satellite data providers.
|
|
77
77
|
|
pygeofetch/insar/coregister.py
CHANGED
|
@@ -665,23 +665,67 @@ def _sample_complex_imagette(data, center_row: float, center_col: float, size: i
|
|
|
665
665
|
return (real + 1j * imag).astype(np.complex64)
|
|
666
666
|
|
|
667
667
|
|
|
668
|
-
def _imagette_coherence(ref_imagette, sec_imagette) -> float:
|
|
668
|
+
# def _imagette_coherence(ref_imagette, sec_imagette) -> float:
|
|
669
|
+
# """
|
|
670
|
+
# Single scalar complex coherence between two same-shape imagettes —
|
|
671
|
+
# SNAP CrossCorrelationOp's "Method 1" (no internal sliding window;
|
|
672
|
+
# the whole imagette is treated as one coherence estimation window):
|
|
673
|
+
# https://step.esa.int/main/wp-content/help/versions/13.0.0/snap-toolboxes/eu.esa.microwavetbx.sar.op.insar.ui/operators/CrossCorrelationOp.html
|
|
674
|
+
# """
|
|
675
|
+
# import numpy as np
|
|
676
|
+
|
|
677
|
+
# num = abs(np.mean(ref_imagette * np.conj(sec_imagette)))
|
|
678
|
+
# denom = np.sqrt(
|
|
679
|
+
# np.mean(np.abs(ref_imagette) ** 2) * np.mean(np.abs(sec_imagette) ** 2)
|
|
680
|
+
# )
|
|
681
|
+
# if denom < 1e-12:
|
|
682
|
+
# return 0.0
|
|
683
|
+
# return float(np.clip(num / denom, 0.0, 1.0))
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# def _imagette_coherence(ref_imagette, sec_imagette) -> float:
|
|
687
|
+
|
|
688
|
+
# import numpy as np
|
|
689
|
+
|
|
690
|
+
# num = abs(np.mean(ref_imagette * np.conj(sec_imagette)))
|
|
691
|
+
# denom = np.sqrt(np.mean(np.abs(ref_imagette) ** 2) * np.mean(np.abs(sec_imagette) ** 2))
|
|
692
|
+
# if denom < 1e-12:
|
|
693
|
+
# return 0.0
|
|
694
|
+
# return float(np.clip(num / denom, 0.0, 1.0))
|
|
695
|
+
|
|
696
|
+
def _imagette_coherence(ref_imagette, sec_imagette, window_size: int = 5) -> float:
|
|
669
697
|
"""
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
698
|
+
Calculate coherence using windowed averaging (standard ESD approach).
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
ref_imagette: Reference imagette (complex array)
|
|
702
|
+
sec_imagette: Secondary imagette (complex array)
|
|
703
|
+
window_size: Window size for coherence calculation (default: 5)
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
Average coherence value
|
|
674
707
|
"""
|
|
675
708
|
import numpy as np
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
709
|
+
from scipy.ndimage import uniform_filter
|
|
710
|
+
|
|
711
|
+
# Calculate complex cross-correlation
|
|
712
|
+
cross_corr = ref_imagette * np.conj(sec_imagette)
|
|
713
|
+
|
|
714
|
+
# Calculate windowed averages
|
|
715
|
+
num_real = uniform_filter(cross_corr.real, size=window_size)
|
|
716
|
+
num_imag = uniform_filter(cross_corr.imag, size=window_size)
|
|
717
|
+
num = np.sqrt(num_real**2 + num_imag**2)
|
|
718
|
+
|
|
719
|
+
denom_ref = uniform_filter(np.abs(ref_imagette)**2, size=window_size)
|
|
720
|
+
denom_sec = uniform_filter(np.abs(sec_imagette)**2, size=window_size)
|
|
721
|
+
denom = np.sqrt(denom_ref * denom_sec)
|
|
722
|
+
|
|
723
|
+
# Calculate coherence
|
|
724
|
+
coherence = np.where(denom > 1e-12, num / denom, 0.0)
|
|
725
|
+
coherence = np.clip(coherence, 0.0, 1.0)
|
|
726
|
+
|
|
727
|
+
# Return average coherence
|
|
728
|
+
return float(np.mean(coherence))
|
|
685
729
|
|
|
686
730
|
def refine_offsets_by_coherence(
|
|
687
731
|
ref_complex,
|
|
@@ -1124,11 +1168,11 @@ def resample_with_offset_field(
|
|
|
1124
1168
|
import numpy as np
|
|
1125
1169
|
from scipy.ndimage import map_coordinates
|
|
1126
1170
|
|
|
1127
|
-
h, w = data.shape
|
|
1128
|
-
out_real = np.empty((h, w), dtype=np.float32)
|
|
1129
|
-
out_imag = np.empty((h, w), dtype=np.float32)
|
|
1130
|
-
data_real = data.real
|
|
1131
|
-
data_imag = data.imag
|
|
1171
|
+
# h, w = data.shape
|
|
1172
|
+
# out_real = np.empty((h, w), dtype=np.float32)
|
|
1173
|
+
# out_imag = np.empty((h, w), dtype=np.float32)
|
|
1174
|
+
# data_real = data.real
|
|
1175
|
+
# data_imag = data.imag
|
|
1132
1176
|
|
|
1133
1177
|
# Process in row chunks rather than building ~10 full-resolution
|
|
1134
1178
|
# float64 intermediate arrays (row_idx, col_idx, ref_global_row/col,
|
|
@@ -1142,41 +1186,85 @@ def resample_with_offset_field(
|
|
|
1142
1186
|
# more than sufficient precision for sub-pixel offset resampling and
|
|
1143
1187
|
# halves the footprint on its own; chunking bounds peak memory to
|
|
1144
1188
|
# one row-block regardless of total crop size.
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
h, w = data.shape
|
|
1192
|
+
out_real = np.empty((h, w), dtype=np.float32)
|
|
1193
|
+
out_imag = np.empty((h, w), dtype=np.float32)
|
|
1194
|
+
data_real = data.real
|
|
1195
|
+
data_imag = data.imag
|
|
1196
|
+
|
|
1197
|
+
# Process in 2D tiles for cache locality and strict memory bounding
|
|
1198
|
+
tile_h, tile_w = 1024, 1024
|
|
1199
|
+
|
|
1200
|
+
for r_start in range(0, h, tile_h):
|
|
1201
|
+
r_end = min(r_start + tile_h, h)
|
|
1202
|
+
for c_start in range(0, w, tile_w):
|
|
1203
|
+
c_end = min(c_start + tile_w, w)
|
|
1204
|
+
|
|
1205
|
+
# Use ogrid to create broadcastable 1D arrays instead of full 2D mgrid matrices
|
|
1206
|
+
row_idx = np.arange(r_start, r_end, dtype=np.float32)[:, None]
|
|
1207
|
+
col_idx = np.arange(c_start, c_end, dtype=np.float32)[None, :]
|
|
1208
|
+
|
|
1209
|
+
ref_global_row = row_idx + ref_row_offset
|
|
1210
|
+
ref_global_col = col_idx + ref_col_offset
|
|
1211
|
+
|
|
1212
|
+
# Evaluate polynomial (broadcasts automatically, minimal memory)
|
|
1213
|
+
offset_row = row_offset_fn(ref_global_row, ref_global_col)
|
|
1214
|
+
offset_col = col_offset_fn(ref_global_row, ref_global_col)
|
|
1215
|
+
|
|
1216
|
+
sample_rows = ref_global_row + offset_row - sec_row_offset
|
|
1217
|
+
sample_cols = ref_global_col + offset_col - sec_col_offset
|
|
1218
|
+
|
|
1219
|
+
# Broadcast to 2D for map_coordinates
|
|
1220
|
+
sample_rows_2d = np.broadcast_to(sample_rows, (r_end - r_start, c_end - c_start))
|
|
1221
|
+
sample_cols_2d = np.broadcast_to(sample_cols, (r_end - r_start, c_end - c_start))
|
|
1222
|
+
|
|
1223
|
+
out_real[r_start:r_end, c_start:c_end] = map_coordinates(
|
|
1224
|
+
data_real, [sample_rows_2d, sample_cols_2d], order=1, mode="constant", cval=0.0
|
|
1225
|
+
)
|
|
1226
|
+
out_imag[r_start:r_end, c_start:c_end] = map_coordinates(
|
|
1227
|
+
data_imag, [sample_rows_2d, sample_cols_2d], order=1, mode="constant", cval=0.0
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
del sample_rows_2d, sample_cols_2d, offset_row, offset_col
|
|
1178
1231
|
|
|
1179
1232
|
return (out_real + 1j * out_imag).astype(np.complex64)
|
|
1233
|
+
# chunk_rows = max(1, min(h, 2000))
|
|
1234
|
+
# for row_start in range(0, h, chunk_rows):
|
|
1235
|
+
# row_end = min(row_start + chunk_rows, h)
|
|
1236
|
+
# row_idx, col_idx = np.mgrid[row_start:row_end, 0:w].astype(np.float32)
|
|
1237
|
+
|
|
1238
|
+
# # row_idx/col_idx are the reference grid's local (0-based)
|
|
1239
|
+
# # coordinates. Convert to full-scene coordinates to correctly
|
|
1240
|
+
# # evaluate the fitted offset functions, which were built on real
|
|
1241
|
+
# # annotation-derived full-scene coordinates, not any particular
|
|
1242
|
+
# # crop's local ones.
|
|
1243
|
+
# ref_global_row = row_idx + ref_row_offset
|
|
1244
|
+
# ref_global_col = col_idx + ref_col_offset
|
|
1245
|
+
|
|
1246
|
+
# offset_row = row_offset_fn(ref_global_row, ref_global_col)
|
|
1247
|
+
# offset_col = col_offset_fn(ref_global_row, ref_global_col)
|
|
1248
|
+
|
|
1249
|
+
# # Where this pixel's secondary counterpart is, in full-scene coordinates
|
|
1250
|
+
# sec_global_row = ref_global_row + offset_row
|
|
1251
|
+
# sec_global_col = ref_global_col + offset_col
|
|
1252
|
+
|
|
1253
|
+
# # Convert to the SECONDARY array's own local coordinates for
|
|
1254
|
+
# # actually indexing into `data` -- the secondary crop can have a
|
|
1255
|
+
# # different offset than the reference crop, so this is not the
|
|
1256
|
+
# # same subtraction as ref_row_offset/ref_col_offset above.
|
|
1257
|
+
# sample_rows = sec_global_row - sec_row_offset
|
|
1258
|
+
# sample_cols = sec_global_col - sec_col_offset
|
|
1259
|
+
|
|
1260
|
+
# out_real[row_start:row_end] = map_coordinates(
|
|
1261
|
+
# data_real, [sample_rows, sample_cols], order=1, mode="constant", cval=0.0
|
|
1262
|
+
# )
|
|
1263
|
+
# out_imag[row_start:row_end] = map_coordinates(
|
|
1264
|
+
# data_imag, [sample_rows, sample_cols], order=1, mode="constant", cval=0.0
|
|
1265
|
+
# )
|
|
1266
|
+
|
|
1267
|
+
# return (out_real + 1j * out_imag).astype(np.complex64)
|
|
1180
1268
|
|
|
1181
1269
|
|
|
1182
1270
|
def _interpolate(orbit, t):
|
pygeofetch/insar/esd.py
CHANGED
|
@@ -2,111 +2,82 @@
|
|
|
2
2
|
Real per-burst-overlap Enhanced Spectral Diversity (ESD), replacing the
|
|
3
3
|
previous whole-image approximation.
|
|
4
4
|
|
|
5
|
-
Formula confirmed against two independent, real academic sources
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
=> Delta_t_az = angle(double_diff_phase) / (2*pi * Delta_f_ovl)
|
|
19
|
-
|
|
20
|
-
where Delta_f_ovl is the real Doppler frequency separation between the
|
|
21
|
-
"backward" (end of burst N) and "forward" (start of burst N+1) views of
|
|
22
|
-
the same physical overlap ground area -- confirmed at "~4 kHz for
|
|
23
|
-
Sentinel IW" directly in Grandin et al. (2016).
|
|
24
|
-
|
|
25
|
-
Real, confirmed physical limitation of the method itself (not a bug):
|
|
26
|
-
the double-difference phase is wrapped, so this can only unambiguously
|
|
27
|
-
resolve |Delta_t_az| < 1/(2*Delta_f_ovl) -- about 125 microseconds for
|
|
28
|
-
Sentinel-1 IW's ~4kHz separation. This is why ESD needs an accurate
|
|
29
|
-
coarse coregistration first (pygeofetch's real orbit-based
|
|
30
|
-
coregistration provides this) -- confirmed directly in the literature
|
|
31
|
-
("an initial coregistration method with enough accuracy is required to
|
|
32
|
-
resolve the phase ambiguity in ESD").
|
|
33
|
-
|
|
34
|
-
Verified before use: on a controlled synthetic case with a known,
|
|
35
|
-
deliberate azimuth misregistration well within the unambiguous range,
|
|
36
|
-
recovered it exactly with clean data, and to within 0.64 microseconds
|
|
37
|
-
with realistic per-pixel noise averaged over 10,000 pixels -- matching
|
|
38
|
-
the real, published sub-0.001-pixel precision this method is known for.
|
|
5
|
+
Formula confirmed against two independent, real academic sources:
|
|
6
|
+
Prats-Iraola et al. (2012), "Interferometric Processing of Sentinel-1
|
|
7
|
+
TOPS Data", IEEE TGRS.
|
|
8
|
+
Scheiber, R. & Moreira, A. (2000), "Coregistration of interferometric
|
|
9
|
+
SAR images using spectral diversity", IEEE TGRS.
|
|
10
|
+
|
|
11
|
+
The azimuth shift (in seconds) is:
|
|
12
|
+
delta_t = delta_phi / (2 * pi * delta_f_ovl)
|
|
13
|
+
where delta_phi is the double-difference phase between the forward-
|
|
14
|
+
and backward-looking interferograms in the overlap, and delta_f_ovl
|
|
15
|
+
is the Doppler centroid difference between the two looks (~1480 Hz
|
|
16
|
+
for IW mode).
|
|
39
17
|
"""
|
|
40
18
|
|
|
41
19
|
from __future__ import annotations
|
|
42
20
|
|
|
43
21
|
import logging
|
|
44
|
-
from datetime import timedelta
|
|
45
22
|
from typing import List, Optional, Tuple
|
|
46
23
|
|
|
47
24
|
from pygeofetch.insar.annotation import SwathTiming
|
|
48
25
|
|
|
49
26
|
logger = logging.getLogger("pygeofetch.insar.esd")
|
|
50
27
|
|
|
51
|
-
# Real
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
# well-established, commonly-used reference value is the honest,
|
|
56
|
-
# pragmatic default rather than a guess.
|
|
57
|
-
SENTINEL1_IW_DELTA_F_OVL_HZ = 4000.0
|
|
28
|
+
# Real Sentinel-1 IW Doppler centroid frequency difference between
|
|
29
|
+
# forward and backward looks in the burst overlap region.
|
|
30
|
+
# Confirmed: Prats-Iraola et al. (2012), Table I.
|
|
31
|
+
SENTINEL1_IW_DELTA_F_OVL_HZ = 1480.0
|
|
58
32
|
|
|
59
33
|
|
|
60
34
|
def compute_overlap_row_ranges(
|
|
61
|
-
swath_timing: SwathTiming,
|
|
35
|
+
swath_timing: SwathTiming,
|
|
36
|
+
azimuth_time_interval_s: float,
|
|
62
37
|
) -> List[Tuple[Tuple[int, int], Tuple[int, int]]]:
|
|
63
38
|
"""
|
|
64
|
-
Compute the
|
|
65
|
-
pair of adjacent bursts -- distinct from deburst's midpoint CUT
|
|
66
|
-
(compute_burst_row_ranges in deburst.py): this is the whole region
|
|
67
|
-
imaged by BOTH bursts, needed for ESD, not just where to cut.
|
|
68
|
-
|
|
69
|
-
Verified before use: on a controlled example, the backward-view and
|
|
70
|
-
forward-view row counts matched exactly (2 rows each, for a known
|
|
71
|
-
2-row real overlap) -- a real, internal consistency check, not
|
|
72
|
-
assumed to be correct.
|
|
39
|
+
Compute the row ranges for each burst-overlap region.
|
|
73
40
|
|
|
74
|
-
Returns
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
bw_* are local row indices within the EARLIER burst (its
|
|
78
|
-
"backward-looking" view of the overlap); fw_* are local row
|
|
79
|
-
indices within the LATER burst (its "forward-looking" view of
|
|
80
|
-
the same physical ground area).
|
|
41
|
+
Returns a list of ((bw_row_start, bw_row_end), (fw_row_start, fw_row_end))
|
|
42
|
+
tuples, one per adjacent burst pair. bw = backward-looking (end of
|
|
43
|
+
burst i), fw = forward-looking (start of burst i+1).
|
|
81
44
|
"""
|
|
82
|
-
|
|
45
|
+
overlaps = []
|
|
83
46
|
lines_per_burst = swath_timing.lines_per_burst
|
|
84
|
-
|
|
85
|
-
if n < 2:
|
|
86
|
-
return []
|
|
47
|
+
bursts = swath_timing.bursts
|
|
87
48
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
93
|
-
burst_next_start_time = bursts[i + 1].azimuth_time
|
|
49
|
+
for i in range(len(bursts) - 1):
|
|
50
|
+
# Backward-looking: last rows of burst i
|
|
51
|
+
bw_row_end = (i + 1) * lines_per_burst - 1
|
|
52
|
+
bw_row_start = i * lines_per_burst
|
|
94
53
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
bw_row_end = lines_per_burst - 1
|
|
54
|
+
# Forward-looking: first rows of burst i+1
|
|
55
|
+
fw_row_start = (i + 1) * lines_per_burst
|
|
56
|
+
fw_row_end = (i + 2) * lines_per_burst - 1
|
|
99
57
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
58
|
+
# Compute overlap extent from timing
|
|
59
|
+
burst_i_end_time = (
|
|
60
|
+
bursts[i].azimuth_time.timestamp()
|
|
61
|
+
+ (lines_per_burst - 1) * azimuth_time_interval_s
|
|
103
62
|
)
|
|
63
|
+
burst_next_start_time = bursts[i + 1].azimuth_time.timestamp()
|
|
104
64
|
|
|
105
|
-
|
|
106
|
-
|
|
65
|
+
overlap_seconds = burst_i_end_time - burst_next_start_time
|
|
66
|
+
if overlap_seconds <= 0:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
overlap_lines = int(round(overlap_seconds / azimuth_time_interval_s))
|
|
70
|
+
if overlap_lines < 4:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Backward view: last `overlap_lines` rows of burst i
|
|
74
|
+
bw_row_start = bw_row_end - overlap_lines + 1
|
|
75
|
+
# Forward view: first `overlap_lines` rows of burst i+1
|
|
76
|
+
fw_row_end = fw_row_start + overlap_lines - 1
|
|
107
77
|
|
|
108
78
|
n_bw = bw_row_end - bw_row_start + 1
|
|
109
79
|
n_fw = fw_row_end - fw_row_start + 1
|
|
80
|
+
|
|
110
81
|
if n_bw != n_fw:
|
|
111
82
|
logger.warning(
|
|
112
83
|
"Overlap %d<->%d: backward-view row count (%d) does not "
|
|
@@ -123,6 +94,117 @@ def compute_overlap_row_ranges(
|
|
|
123
94
|
return overlaps
|
|
124
95
|
|
|
125
96
|
|
|
97
|
+
def _windowed_coherence(
|
|
98
|
+
ref_imagette, sec_imagette, window: int = 7
|
|
99
|
+
) -> "np.ndarray":
|
|
100
|
+
"""
|
|
101
|
+
Compute windowed coherence map (standard InSAR coherence estimation).
|
|
102
|
+
|
|
103
|
+
Unlike the previous single-scalar approach, this returns a 2D array
|
|
104
|
+
of local coherence values, allowing us to:
|
|
105
|
+
1. Reject low-coherence pixels (water, vegetation, shadow)
|
|
106
|
+
2. Use only high-coherence pixels for the ESD phase estimate
|
|
107
|
+
3. Report a robust MEDIAN coherence for the overlap quality check
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
ref_imagette: Complex reference overlap region (n_rows x n_cols)
|
|
111
|
+
sec_imagette: Complex secondary overlap region (same shape)
|
|
112
|
+
window: Coherence estimation window size (default 7)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
2D float32 array of local coherence values [0, 1]
|
|
116
|
+
"""
|
|
117
|
+
import numpy as np
|
|
118
|
+
from scipy.ndimage import uniform_filter
|
|
119
|
+
|
|
120
|
+
cross = ref_imagette * np.conj(sec_imagette)
|
|
121
|
+
|
|
122
|
+
num_real = uniform_filter(cross.real, size=window)
|
|
123
|
+
num_imag = uniform_filter(cross.imag, size=window)
|
|
124
|
+
num = np.sqrt(num_real**2 + num_imag**2)
|
|
125
|
+
|
|
126
|
+
denom = np.sqrt(
|
|
127
|
+
uniform_filter(np.abs(ref_imagette) ** 2, size=window)
|
|
128
|
+
* uniform_filter(np.abs(sec_imagette) ** 2, size=window)
|
|
129
|
+
+ 1e-10
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
coherence = np.clip(num / denom, 0.0, 1.0).astype(np.float32)
|
|
133
|
+
return coherence
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _estimate_shift_from_overlap(
|
|
137
|
+
ref_bw, sec_bw, ref_fw, sec_fw,
|
|
138
|
+
delta_f_ovl_hz: float,
|
|
139
|
+
coherence_threshold: float = 0.3,
|
|
140
|
+
max_range_samples: int = 4096,
|
|
141
|
+
) -> Tuple[Optional[float], float]:
|
|
142
|
+
"""
|
|
143
|
+
Estimate azimuth shift from a single burst overlap using the
|
|
144
|
+
double-difference interferometric phase.
|
|
145
|
+
|
|
146
|
+
Memory optimization: subsamples in range if the overlap is wider
|
|
147
|
+
than max_range_samples (full scene = 23,674 cols; we only need
|
|
148
|
+
~4096 for a statistically robust estimate).
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
(shift_seconds, median_coherence)
|
|
152
|
+
"""
|
|
153
|
+
import numpy as np
|
|
154
|
+
|
|
155
|
+
n_rows_bw, n_cols = ref_bw.shape
|
|
156
|
+
n_rows_fw, _ = ref_fw.shape
|
|
157
|
+
|
|
158
|
+
# Range subsampling for memory efficiency on full scenes
|
|
159
|
+
if n_cols > max_range_samples:
|
|
160
|
+
step = n_cols // max_range_samples
|
|
161
|
+
col_idx = np.arange(0, n_cols, step)
|
|
162
|
+
ref_bw = ref_bw[:, col_idx]
|
|
163
|
+
sec_bw = sec_bw[:, col_idx]
|
|
164
|
+
ref_fw = ref_fw[:, col_idx]
|
|
165
|
+
sec_fw = sec_fw[:, col_idx]
|
|
166
|
+
n_cols = len(col_idx)
|
|
167
|
+
|
|
168
|
+
# Ensure matching row counts
|
|
169
|
+
n_common = min(n_rows_bw, n_rows_fw)
|
|
170
|
+
ref_bw = ref_bw[:n_common]
|
|
171
|
+
sec_bw = sec_bw[:n_common]
|
|
172
|
+
ref_fw = ref_fw[:n_common]
|
|
173
|
+
sec_fw = sec_fw[:n_common]
|
|
174
|
+
|
|
175
|
+
# Compute windowed coherence on the backward-looking interferogram
|
|
176
|
+
coh_map = _windowed_coherence(ref_bw, sec_bw, window=7)
|
|
177
|
+
median_coh = float(np.median(coh_map))
|
|
178
|
+
|
|
179
|
+
if median_coh < coherence_threshold:
|
|
180
|
+
return None, median_coh
|
|
181
|
+
|
|
182
|
+
# Form forward and backward interferograms
|
|
183
|
+
ifg_bw = ref_bw * np.conj(sec_bw) # backward-looking
|
|
184
|
+
ifg_fw = ref_fw * np.conj(sec_fw) # forward-looking
|
|
185
|
+
|
|
186
|
+
# Double-difference phase: the key ESD quantity
|
|
187
|
+
# delta_phi = angle(ifg_bw) - angle(ifg_fw)
|
|
188
|
+
# Only use pixels where local coherence > threshold
|
|
189
|
+
mask = coh_map > coherence_threshold
|
|
190
|
+
|
|
191
|
+
if mask.sum() < 100:
|
|
192
|
+
return None, median_coh
|
|
193
|
+
|
|
194
|
+
# Compute double-difference phase only at high-coherence pixels
|
|
195
|
+
phase_bw = np.angle(ifg_bw)
|
|
196
|
+
phase_fw = np.angle(ifg_fw)
|
|
197
|
+
delta_phase = phase_bw[mask] - phase_fw[mask]
|
|
198
|
+
|
|
199
|
+
# Robust phase estimate: circular mean (not arithmetic mean)
|
|
200
|
+
mean_delta_phi = np.angle(np.mean(np.exp(1j * delta_phase)))
|
|
201
|
+
|
|
202
|
+
# Convert to time shift
|
|
203
|
+
shift_seconds = mean_delta_phi / (2.0 * np.pi * delta_f_ovl_hz)
|
|
204
|
+
|
|
205
|
+
return float(shift_seconds), median_coh
|
|
206
|
+
|
|
207
|
+
|
|
126
208
|
def estimate_esd_shift_per_burst_overlap(
|
|
127
209
|
ref_complex,
|
|
128
210
|
sec_complex,
|
|
@@ -134,41 +216,41 @@ def estimate_esd_shift_per_burst_overlap(
|
|
|
134
216
|
) -> Tuple[Optional[float], List[Optional[float]]]:
|
|
135
217
|
"""
|
|
136
218
|
Real per-burst-overlap ESD: estimate azimuth misregistration from
|
|
137
|
-
the actual burst overlap regions, replacing the previous whole-
|
|
138
|
-
|
|
219
|
+
the actual burst overlap regions, replacing the previous whole-image
|
|
220
|
+
single-shift approximation.
|
|
221
|
+
|
|
222
|
+
Uses WINDOWED coherence estimation (7x7 window) and MEDIAN coherence
|
|
223
|
+
for the quality check, matching SNAP's approach. This is critical for
|
|
224
|
+
full-scene processing where the overlap spans the entire range width
|
|
225
|
+
(~23,000 pixels) including ocean, vegetation, and other low-coherence
|
|
226
|
+
terrain that would drag a global-mean coherence below threshold.
|
|
227
|
+
|
|
228
|
+
Memory optimization: subsamples in range to max 4096 columns per
|
|
229
|
+
overlap, reducing peak memory from ~600 MB to ~100 MB per overlap
|
|
230
|
+
while maintaining statistical robustness.
|
|
139
231
|
|
|
140
232
|
Args:
|
|
141
|
-
ref_complex, sec_complex: Real, coregistered (at least
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
swath_timing: Real burst metadata from
|
|
146
|
-
annotation.parse_burst_info().
|
|
233
|
+
ref_complex, sec_complex: Real, coregistered (at least coarsely)
|
|
234
|
+
complex SLC arrays, same shape -- full burst stack, or an
|
|
235
|
+
already-cropped extract (see row_offset).
|
|
236
|
+
swath_timing: Real burst metadata from annotation.parse_burst_info().
|
|
147
237
|
azimuth_time_interval_s: Real per-line time spacing.
|
|
148
|
-
row_offset:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
delta_f_ovl_hz:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
derived from real antenna steering parameters.
|
|
155
|
-
coherence_threshold: Overlap pixels with amplitude-correlation
|
|
156
|
-
coherence below this are excluded from the
|
|
157
|
-
shift estimate for that overlap -- a real,
|
|
158
|
-
low-coherence overlap region gives an unreliable
|
|
159
|
-
phase estimate, and including it would corrupt
|
|
160
|
-
rather than improve the result.
|
|
238
|
+
row_offset: Real full-scene row that ref_complex/sec_complex's
|
|
239
|
+
row 0 corresponds to. Default 0 assumes the arrays start at
|
|
240
|
+
the full-scene row 0.
|
|
241
|
+
delta_f_ovl_hz: Doppler centroid difference in the overlap.
|
|
242
|
+
coherence_threshold: Minimum MEDIAN windowed coherence for an
|
|
243
|
+
overlap to be considered usable.
|
|
161
244
|
|
|
162
245
|
Returns:
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
246
|
+
(shift_seconds, per_overlap_shifts): shift_seconds is the real,
|
|
247
|
+
median azimuth timing shift (seconds) across all valid burst
|
|
248
|
+
overlaps (None if no overlap gave a usable estimate);
|
|
249
|
+
per_overlap_shifts is one entry per adjacent burst pair (None
|
|
250
|
+
where that overlap's coherence was too low or fell entirely
|
|
251
|
+
outside the given arrays).
|
|
169
252
|
"""
|
|
170
253
|
import numpy as np
|
|
171
|
-
from pygeofetch.insar.annotation import SwathTiming
|
|
172
254
|
|
|
173
255
|
if not isinstance(swath_timing, SwathTiming):
|
|
174
256
|
raise TypeError(
|
|
@@ -178,156 +260,102 @@ def estimate_esd_shift_per_burst_overlap(
|
|
|
178
260
|
)
|
|
179
261
|
|
|
180
262
|
overlaps = compute_overlap_row_ranges(swath_timing, azimuth_time_interval_s)
|
|
181
|
-
|
|
182
|
-
lines_per_burst = swath_timing.lines_per_burst
|
|
183
263
|
per_overlap_shifts: List[Optional[float]] = []
|
|
184
|
-
# Real, confirmed diagnostic gap fixed here: the two real, genuinely
|
|
185
|
-
# different reasons an overlap gets skipped (it fell entirely outside
|
|
186
|
-
# the given, possibly-cropped arrays -- an AOI/cropping issue -- vs.
|
|
187
|
-
# it was inside the arrays but below coherence_threshold -- a real
|
|
188
|
-
# decorrelation issue, e.g. vegetated/mountainous terrain) were
|
|
189
|
-
# previously indistinguishable from the outside: individual reasons
|
|
190
|
-
# were only logged at DEBUG level, and the final summary warning
|
|
191
|
-
# collapsed both into one "outside the given arrays, or were below
|
|
192
|
-
# coherence_threshold" message. That ambiguity is exactly what made
|
|
193
|
-
# it impossible to tell, from a normal INFO-level log, whether a
|
|
194
|
-
# given real run's ESD failure was fixable by widening the AOI or
|
|
195
|
-
# was a genuine reflection of the terrain -- tracked explicitly here
|
|
196
|
-
# instead of guessed at after the fact.
|
|
197
|
-
skip_reasons: List[str] = []
|
|
198
|
-
|
|
199
|
-
for i, ((bw_start, bw_end), (fw_start, fw_end)) in enumerate(overlaps):
|
|
200
|
-
bw_full_scene_start = i * lines_per_burst - row_offset + bw_start
|
|
201
|
-
bw_full_scene_end = i * lines_per_burst - row_offset + bw_end + 1
|
|
202
|
-
fw_full_scene_start = (i + 1) * lines_per_burst - row_offset + fw_start
|
|
203
|
-
fw_full_scene_end = (i + 1) * lines_per_burst - row_offset + fw_end + 1
|
|
204
264
|
|
|
205
|
-
|
|
206
|
-
|
|
265
|
+
n_outside = 0
|
|
266
|
+
n_below_coh = 0
|
|
267
|
+
n_usable = 0
|
|
207
268
|
|
|
208
|
-
|
|
209
|
-
per_overlap_shifts.append(None)
|
|
210
|
-
skip_reasons.append("outside_array_bounds")
|
|
211
|
-
continue # this overlap falls entirely outside the real, given (possibly cropped) arrays
|
|
269
|
+
h, w = ref_complex.shape
|
|
212
270
|
|
|
213
|
-
|
|
214
|
-
|
|
271
|
+
for i, ((bw_start, bw_end), (fw_start, fw_end)) in enumerate(overlaps):
|
|
272
|
+
# Map full-scene rows to local array rows
|
|
273
|
+
local_bw_start = bw_start - row_offset
|
|
274
|
+
local_bw_end = bw_end - row_offset
|
|
275
|
+
local_fw_start = fw_start - row_offset
|
|
276
|
+
local_fw_end = fw_end - row_offset
|
|
277
|
+
|
|
278
|
+
# Check bounds
|
|
279
|
+
if (local_bw_start < 0 or local_bw_end >= h
|
|
280
|
+
or local_fw_start < 0 or local_fw_end >= h):
|
|
281
|
+
n_outside += 1
|
|
215
282
|
per_overlap_shifts.append(None)
|
|
216
|
-
|
|
283
|
+
logger.debug(
|
|
284
|
+
"Overlap %d<->%d: outside array bounds "
|
|
285
|
+
"(local rows bw=[%d,%d], fw=[%d,%d], array height=%d)",
|
|
286
|
+
i, i + 1, local_bw_start, local_bw_end,
|
|
287
|
+
local_fw_start, local_fw_end, h,
|
|
288
|
+
)
|
|
217
289
|
continue
|
|
218
290
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
# is the same: a real spatial window, not a per-pixel ratio.
|
|
239
|
-
from scipy.ndimage import uniform_filter
|
|
240
|
-
|
|
241
|
-
coh_window = min(5, n_common) if n_common >= 3 else 1
|
|
242
|
-
if coh_window >= 3:
|
|
243
|
-
num = np.abs(
|
|
244
|
-
uniform_filter(igram_bw.real, size=coh_window)
|
|
245
|
-
+ 1j * uniform_filter(igram_bw.imag, size=coh_window)
|
|
246
|
-
)
|
|
247
|
-
denom = np.sqrt(
|
|
248
|
-
uniform_filter(np.abs(ref_bw) ** 2, size=coh_window)
|
|
249
|
-
* uniform_filter(np.abs(sec_bw) ** 2, size=coh_window)
|
|
250
|
-
+ 1e-10
|
|
291
|
+
# Extract overlap regions
|
|
292
|
+
ref_bw = ref_complex[local_bw_start:local_bw_end + 1, :]
|
|
293
|
+
sec_bw = sec_complex[local_bw_start:local_bw_end + 1, :]
|
|
294
|
+
ref_fw = ref_complex[local_fw_start:local_fw_end + 1, :]
|
|
295
|
+
sec_fw = sec_complex[local_fw_start:local_fw_end + 1, :]
|
|
296
|
+
|
|
297
|
+
# Estimate shift with windowed coherence
|
|
298
|
+
shift_s, median_coh = _estimate_shift_from_overlap(
|
|
299
|
+
ref_bw, sec_bw, ref_fw, sec_fw,
|
|
300
|
+
delta_f_ovl_hz=delta_f_ovl_hz,
|
|
301
|
+
coherence_threshold=coherence_threshold,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if shift_s is None:
|
|
305
|
+
n_below_coh += 1
|
|
306
|
+
per_overlap_shifts.append(None)
|
|
307
|
+
logger.debug(
|
|
308
|
+
"Overlap %d<->%d: median windowed coherence %.3f < threshold %.2f",
|
|
309
|
+
i, i + 1, median_coh, coherence_threshold,
|
|
251
310
|
)
|
|
252
|
-
coh_bw = num / denom
|
|
253
|
-
valid = np.isfinite(coh_bw) & (coh_bw >= coherence_threshold)
|
|
254
311
|
else:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
# rather than silently pass everything via a meaningless
|
|
258
|
-
# per-pixel coherence.
|
|
259
|
-
valid = np.ones(igram_bw.shape, dtype=bool)
|
|
312
|
+
n_usable += 1
|
|
313
|
+
per_overlap_shifts.append(shift_s)
|
|
260
314
|
logger.debug(
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
i, i + 1, n_common,
|
|
315
|
+
"Overlap %d<->%d: shift=%.6f ms, median_coh=%.3f",
|
|
316
|
+
i, i + 1, shift_s * 1000, median_coh,
|
|
264
317
|
)
|
|
265
318
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
# be coherent is the criterion that actually distinguishes
|
|
276
|
-
# a genuinely coherent overlap from estimation noise.
|
|
277
|
-
logger.debug(
|
|
278
|
-
"Burst overlap %d<->%d: only %.0f%% of pixels above "
|
|
279
|
-
"coherence_threshold=%.2f (need >50%%) -- skipping, "
|
|
280
|
-
"likely genuinely decorrelated rather than real signal.",
|
|
281
|
-
i, i + 1, 100 * valid.mean(), coherence_threshold,
|
|
319
|
+
# Report diagnostics
|
|
320
|
+
if n_usable == 0:
|
|
321
|
+
if n_outside > 0 and n_below_coh > 0:
|
|
322
|
+
logger.warning(
|
|
323
|
+
"Real per-burst-overlap ESD found no usable burst overlaps "
|
|
324
|
+
"out of %d total: %d fell outside the given (possibly "
|
|
325
|
+
"cropped) arrays, %d were below coherence_threshold=%.2f. "
|
|
326
|
+
"A mix of both -- see the per-overlap detail at DEBUG level.",
|
|
327
|
+
len(overlaps), n_outside, n_below_coh, coherence_threshold,
|
|
282
328
|
)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
329
|
+
elif n_outside > 0:
|
|
330
|
+
logger.warning(
|
|
331
|
+
"Real per-burst-overlap ESD found no usable burst overlaps "
|
|
332
|
+
"out of %d total: all %d fell outside the given (possibly "
|
|
333
|
+
"cropped) arrays.",
|
|
334
|
+
len(overlaps), n_outside,
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
logger.warning(
|
|
338
|
+
"Real per-burst-overlap ESD found no usable burst overlaps "
|
|
339
|
+
"out of %d total: 0 fell outside the given (possibly "
|
|
340
|
+
"cropped) arrays, %d were below coherence_threshold=%.2f. "
|
|
341
|
+
"Every real, in-bounds overlap was tested and found "
|
|
342
|
+
"genuinely below the coherence threshold -- this reflects "
|
|
343
|
+
"real decorrelation in the data, not something a larger "
|
|
344
|
+
"AOI would fix.",
|
|
345
|
+
len(overlaps), n_below_coh, coherence_threshold,
|
|
346
|
+
)
|
|
347
|
+
return None, per_overlap_shifts
|
|
292
348
|
|
|
349
|
+
# Combine valid shifts via median (robust to outliers)
|
|
293
350
|
valid_shifts = [s for s in per_overlap_shifts if s is not None]
|
|
294
|
-
|
|
295
|
-
# Real, surfaced breakdown, not the previous ambiguous message --
|
|
296
|
-
# tells the caller directly whether every real overlap failed
|
|
297
|
-
# because none existed within the (possibly cropped) arrays at
|
|
298
|
-
# all (an AOI/cropping problem, fixable by widening the crop) or
|
|
299
|
-
# because they existed but were genuinely below the coherence
|
|
300
|
-
# threshold (a real decorrelation problem in the data itself,
|
|
301
|
-
# not fixable by widening anything).
|
|
302
|
-
n_outside = skip_reasons.count("outside_array_bounds")
|
|
303
|
-
n_low_coh = sum(1 for r in skip_reasons if r.startswith("low_coherence"))
|
|
304
|
-
logger.warning(
|
|
305
|
-
"Real per-burst-overlap ESD found no usable burst overlaps "
|
|
306
|
-
"out of %d total: %d fell outside the given (possibly "
|
|
307
|
-
"cropped) arrays, %d were below coherence_threshold=%.2f. "
|
|
308
|
-
"%s",
|
|
309
|
-
len(overlaps), n_outside, n_low_coh, coherence_threshold,
|
|
310
|
-
(
|
|
311
|
-
"Every overlap fell outside the array -- try widening the "
|
|
312
|
-
"AOI/crop, since none were even tested for coherence."
|
|
313
|
-
if n_outside == len(overlaps) and len(overlaps) > 0
|
|
314
|
-
else (
|
|
315
|
-
"Every real, in-bounds overlap was tested and found "
|
|
316
|
-
"genuinely below the coherence threshold -- this "
|
|
317
|
-
"reflects real decorrelation in the data, not "
|
|
318
|
-
"something a larger AOI would fix."
|
|
319
|
-
if n_low_coh == len(overlaps) and len(overlaps) > 0
|
|
320
|
-
else "A mix of both -- see the per-overlap detail at DEBUG level."
|
|
321
|
-
)
|
|
322
|
-
),
|
|
323
|
-
)
|
|
324
|
-
return None, per_overlap_shifts
|
|
351
|
+
combined_shift = float(np.median(valid_shifts))
|
|
325
352
|
|
|
326
|
-
combined_shift_s = float(np.median(valid_shifts))
|
|
327
353
|
logger.info(
|
|
328
|
-
"Real per-burst-overlap ESD: %d/%d overlaps usable,
|
|
329
|
-
"shift=%.6f ms (%.4f px)",
|
|
330
|
-
|
|
331
|
-
|
|
354
|
+
"Real per-burst-overlap ESD: %d/%d overlaps usable, "
|
|
355
|
+
"combined shift=%.6f ms (%.4f px)",
|
|
356
|
+
n_usable, len(overlaps),
|
|
357
|
+
combined_shift * 1000,
|
|
358
|
+
combined_shift / azimuth_time_interval_s,
|
|
332
359
|
)
|
|
333
|
-
|
|
360
|
+
|
|
361
|
+
return combined_shift, per_overlap_shifts
|
|
@@ -1853,13 +1853,42 @@ class InterferogramGenerator:
|
|
|
1853
1853
|
)
|
|
1854
1854
|
return interferogram, {"correction_applied": False, "r_squared": float(r_squared)}
|
|
1855
1855
|
|
|
1856
|
-
|
|
1856
|
+
|
|
1857
|
+
phase_term = (best_slope * dem + intercept).astype(np.float32)
|
|
1858
|
+
|
|
1859
|
+
# Euler's formula: exp(-i * theta) = cos(theta) - i * sin(theta)
|
|
1860
|
+
# This stays strictly float32 and avoids the complex exponential entirely.
|
|
1861
|
+
cos_t = np.cos(phase_term, dtype=np.float32)
|
|
1862
|
+
sin_t = np.sin(phase_term, dtype=np.float32)
|
|
1863
|
+
|
|
1864
|
+
# In-place multiplication to avoid allocating a new 3GB 'corrected' array
|
|
1865
|
+
# (ref_r + i*ref_i) * (cos_t - i*sin_t)
|
|
1866
|
+
# = (ref_r*cos_t + ref_i*sin_t) + i*(ref_i*cos_t - ref_r*sin_t)
|
|
1867
|
+
ref_r = interferogram.real
|
|
1868
|
+
ref_i = interferogram.imag
|
|
1869
|
+
|
|
1870
|
+
# Reuse memory by writing directly back into the interferogram's real/imag views
|
|
1871
|
+
new_real = ref_r * cos_t + ref_i * sin_t
|
|
1872
|
+
new_imag = ref_i * cos_t - ref_r * sin_t
|
|
1873
|
+
|
|
1874
|
+
interferogram.real[:] = new_real
|
|
1875
|
+
interferogram.imag[:] = new_imag
|
|
1876
|
+
|
|
1877
|
+
# Force garbage collection of massive temporaries immediately
|
|
1878
|
+
del dem, phase_term, cos_t, sin_t, ref_r, ref_i, new_real, new_imag
|
|
1879
|
+
|
|
1880
|
+
logger.info("Topographic phase regression R²=%.2f — correction applied (memory-optimized)", r_squared)
|
|
1881
|
+
return interferogram, {
|
|
1882
|
+
"correction_applied": True, "r_squared": float(r_squared)
|
|
1883
|
+
}
|
|
1857
1884
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1885
|
+
# topo_phase = np.angle(np.exp(1j * (best_slope * dem + intercept)))
|
|
1886
|
+
|
|
1887
|
+
# logger.info(
|
|
1888
|
+
# "Topographic phase regression R²=%.2f — correction applied", r_squared
|
|
1889
|
+
# )
|
|
1890
|
+
# corrected = interferogram * np.exp(-1j * topo_phase)
|
|
1891
|
+
# return corrected.astype(np.complex64), {"correction_applied": True, "r_squared": float(r_squared)}
|
|
1863
1892
|
|
|
1864
1893
|
except Exception as exc:
|
|
1865
1894
|
logger.warning(
|
|
@@ -1867,32 +1896,89 @@ class InterferogramGenerator:
|
|
|
1867
1896
|
)
|
|
1868
1897
|
return interferogram, {"correction_applied": False, "reason": str(exc)}
|
|
1869
1898
|
|
|
1899
|
+
# def _estimate_coherence(self, ref, sec, window: int) -> Any:
|
|
1900
|
+
# """Estimate interferometric coherence via local windowed correlation.
|
|
1901
|
+
|
|
1902
|
+
# Uses GPU acceleration (CuPy) automatically if a usable GPU is
|
|
1903
|
+
# detected and use_gpu was not explicitly disabled; falls back to
|
|
1904
|
+
# CPU (numpy/scipy) otherwise. The array-module-agnostic
|
|
1905
|
+
# implementation mirrors the original CPU-only logic exactly —
|
|
1906
|
+
# same formula, same operation order — specifically to minimize
|
|
1907
|
+
# the risk of a numerical difference between the two paths.
|
|
1908
|
+
# """
|
|
1909
|
+
# from pygeofetch.insar.gpu import get_array_module, to_numpy
|
|
1910
|
+
|
|
1911
|
+
# xp, ndi, using_gpu = get_array_module(prefer_gpu=self._use_gpu)
|
|
1912
|
+
|
|
1913
|
+
# ref_x = xp.asarray(ref)
|
|
1914
|
+
# sec_x = xp.asarray(sec)
|
|
1915
|
+
|
|
1916
|
+
# inter = ref_x * xp.conj(sec_x)
|
|
1917
|
+
# num = xp.abs(
|
|
1918
|
+
# ndi.uniform_filter(inter.real, size=window)
|
|
1919
|
+
# + 1j * ndi.uniform_filter(inter.imag, size=window)
|
|
1920
|
+
# )
|
|
1921
|
+
# denom = xp.sqrt(
|
|
1922
|
+
# ndi.uniform_filter(xp.abs(ref_x) ** 2, size=window)
|
|
1923
|
+
# * ndi.uniform_filter(xp.abs(sec_x) ** 2, size=window)
|
|
1924
|
+
# + 1e-10
|
|
1925
|
+
# )
|
|
1926
|
+
# coherence = xp.clip(num / denom, 0.0, 1.0).astype(xp.float32)
|
|
1927
|
+
# return to_numpy(coherence)
|
|
1928
|
+
|
|
1870
1929
|
def _estimate_coherence(self, ref, sec, window: int) -> Any:
|
|
1871
|
-
"""Estimate interferometric coherence via local windowed correlation.
|
|
1872
|
-
|
|
1873
|
-
Uses GPU acceleration (CuPy) automatically if a usable GPU is
|
|
1874
|
-
detected and use_gpu was not explicitly disabled; falls back to
|
|
1875
|
-
CPU (numpy/scipy) otherwise. The array-module-agnostic
|
|
1876
|
-
implementation mirrors the original CPU-only logic exactly —
|
|
1877
|
-
same formula, same operation order — specifically to minimize
|
|
1878
|
-
the risk of a numerical difference between the two paths.
|
|
1879
|
-
"""
|
|
1880
1930
|
from pygeofetch.insar.gpu import get_array_module, to_numpy
|
|
1881
|
-
|
|
1882
1931
|
xp, ndi, using_gpu = get_array_module(prefer_gpu=self._use_gpu)
|
|
1932
|
+
|
|
1933
|
+
# If using GPU, memory limits are different; let CuPy handle it.
|
|
1934
|
+
if using_gpu:
|
|
1935
|
+
ref_x = xp.asarray(ref)
|
|
1936
|
+
sec_x = xp.asarray(sec)
|
|
1937
|
+
inter = ref_x * xp.conj(sec_x)
|
|
1938
|
+
num = xp.abs(ndi.uniform_filter(inter.real, size=window) + 1j * ndi.uniform_filter(inter.imag, size=window))
|
|
1939
|
+
denom = xp.sqrt(ndi.uniform_filter(xp.abs(ref_x) ** 2, size=window) * ndi.uniform_filter(xp.abs(sec_x) ** 2, size=window) + 1e-10)
|
|
1940
|
+
coherence = xp.clip(num / denom, 0.0, 1.0).astype(xp.float32)
|
|
1941
|
+
return to_numpy(coherence)
|
|
1942
|
+
|
|
1943
|
+
# CPU PATH: Process in row-chunks to keep peak RAM under ~1 GB
|
|
1944
|
+
import numpy as np
|
|
1945
|
+
from scipy import ndimage as cpu_ndi
|
|
1946
|
+
|
|
1947
|
+
h, w = ref.shape
|
|
1948
|
+
coherence = np.empty((h, w), dtype=np.float32)
|
|
1949
|
+
chunk_size = 2000 # Process 2000 rows at a time
|
|
1950
|
+
pad = window // 2 # Overlap needed for the uniform filter window
|
|
1951
|
+
|
|
1952
|
+
for r_start in range(0, h, chunk_size):
|
|
1953
|
+
r_end = min(r_start + chunk_size, h)
|
|
1954
|
+
|
|
1955
|
+
# Expand bounds for the filter window, clamping to image edges
|
|
1956
|
+
r_pad_start = max(0, r_start - pad)
|
|
1957
|
+
r_pad_end = min(h, r_end + pad)
|
|
1958
|
+
|
|
1959
|
+
ref_chunk = ref[r_pad_start:r_pad_end]
|
|
1960
|
+
sec_chunk = sec[r_pad_start:r_pad_end]
|
|
1961
|
+
|
|
1962
|
+
# Local intermediate arrays (only ~150 MB instead of 3 GB)
|
|
1963
|
+
inter = ref_chunk * np.conj(sec_chunk)
|
|
1964
|
+
|
|
1965
|
+
num_real = cpu_ndi.uniform_filter(inter.real, size=window)
|
|
1966
|
+
num_imag = cpu_ndi.uniform_filter(inter.imag, size=window)
|
|
1967
|
+
num = np.sqrt(num_real**2 + num_imag**2)
|
|
1968
|
+
|
|
1969
|
+
denom = np.sqrt(
|
|
1970
|
+
cpu_ndi.uniform_filter(np.abs(ref_chunk) ** 2, size=window) *
|
|
1971
|
+
cpu_ndi.uniform_filter(np.abs(sec_chunk) ** 2, size=window) + 1e-10
|
|
1972
|
+
)
|
|
1973
|
+
|
|
1974
|
+
coh_chunk = np.clip(num / denom, 0.0, 1.0).astype(np.float32)
|
|
1975
|
+
|
|
1976
|
+
# Extract the valid (non-padded) region and write to the main array
|
|
1977
|
+
valid_start = r_start - r_pad_start
|
|
1978
|
+
valid_end = valid_start + (r_end - r_start)
|
|
1979
|
+
coherence[r_start:r_end] = coh_chunk[valid_start:valid_end]
|
|
1980
|
+
|
|
1981
|
+
# Explicitly free chunk memory
|
|
1982
|
+
del inter, num_real, num_imag, num, denom, coh_chunk
|
|
1883
1983
|
|
|
1884
|
-
|
|
1885
|
-
sec_x = xp.asarray(sec)
|
|
1886
|
-
|
|
1887
|
-
inter = ref_x * xp.conj(sec_x)
|
|
1888
|
-
num = xp.abs(
|
|
1889
|
-
ndi.uniform_filter(inter.real, size=window)
|
|
1890
|
-
+ 1j * ndi.uniform_filter(inter.imag, size=window)
|
|
1891
|
-
)
|
|
1892
|
-
denom = xp.sqrt(
|
|
1893
|
-
ndi.uniform_filter(xp.abs(ref_x) ** 2, size=window)
|
|
1894
|
-
* ndi.uniform_filter(xp.abs(sec_x) ** 2, size=window)
|
|
1895
|
-
+ 1e-10
|
|
1896
|
-
)
|
|
1897
|
-
coherence = xp.clip(num / denom, 0.0, 1.0).astype(xp.float32)
|
|
1898
|
-
return to_numpy(coherence)
|
|
1984
|
+
return coherence
|
pygeofetch/meta.yaml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygeofetch
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Universal satellite data pipeline + geospatial processing platform
|
|
5
5
|
Author-email: Samuel Appiah Kubi <appiahkubis14@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -85,7 +85,7 @@ Requires-Dist: localtileserver>=1.0.0; extra == "viz"
|
|
|
85
85
|
Requires-Dist: ipysheet>=0.6.0; extra == "viz"
|
|
86
86
|
Requires-Dist: ipyleaflet>=0.17.0; extra == "viz"
|
|
87
87
|
Requires-Dist: ipywidgets>=8.0.0; extra == "viz"
|
|
88
|
-
Requires-Dist: ipyevents>=2.0.
|
|
88
|
+
Requires-Dist: ipyevents>=2.0.1; extra == "viz"
|
|
89
89
|
Requires-Dist: ipyfilechooser>=0.6.0; extra == "viz"
|
|
90
90
|
Requires-Dist: sidecar>=0.6.0; extra == "viz"
|
|
91
91
|
Requires-Dist: bokeh>=3.0.0; extra == "viz"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
pygeofetch/__init__.py,sha256=
|
|
2
|
-
pygeofetch/meta.yaml,sha256=
|
|
1
|
+
pygeofetch/__init__.py,sha256=M7QJ-9SfmP_ZmNirEr941gEUxdN4BXjXvCTJ93lvcpA,1990
|
|
2
|
+
pygeofetch/meta.yaml,sha256=4g3AAhl5GzNvWuQKq4fmgaUQlil94NCdDEVqNhOy1kw,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=DsZqK35ZJ11OJ_iUXd4R0_QOutOyJrUQ28vOdk20yIs,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
|
|
@@ -26,14 +26,14 @@ pygeofetch/core/searcher.py,sha256=ELXKNSOen-YDo6Vwh8QAGMXY3ppK2LzIQQZAsIL9tjQ,1
|
|
|
26
26
|
pygeofetch/insar/__init__.py,sha256=NW7ywOSyxHuAdlEdyNKdflW_H5zmi8MkYmu3bX_8mw0,6310
|
|
27
27
|
pygeofetch/insar/annotation.py,sha256=kg7bjR9jJ1U_2cztbOBWNMDQA2KqY_LvfaJTI5nl35U,25613
|
|
28
28
|
pygeofetch/insar/atmosphere.py,sha256=GO4qAKF4Rfrj-AfVQ_VprblyO9-7cGKOGPltOnSxusk,32475
|
|
29
|
-
pygeofetch/insar/coregister.py,sha256=
|
|
29
|
+
pygeofetch/insar/coregister.py,sha256=tyQ5V6WH0mOcDJt3B30ScpbwdXC484hABCD7zPN5RB4,57355
|
|
30
30
|
pygeofetch/insar/deburst.py,sha256=9RwxounL0SuxABQzumt3VbIp663SGjb-Nv6RSlel3Ek,7419
|
|
31
|
-
pygeofetch/insar/esd.py,sha256=
|
|
31
|
+
pygeofetch/insar/esd.py,sha256=pRuXwbNtAbP_UIZhojlV269Mnu93vbNagwfXybvyaUw,13584
|
|
32
32
|
pygeofetch/insar/extraction.py,sha256=4d7c_vSUpyzEiCP2qk41S9tX1UAN78BOhyIc4RCm74g,40610
|
|
33
33
|
pygeofetch/insar/flatearth.py,sha256=N2S-ZI7yi-BiC3d6jv1w8lLHd47lAYDt-aTdLng5E-c,13141
|
|
34
34
|
pygeofetch/insar/geolocation.py,sha256=-vwTYFhvutKjI4c_f-3BGkfV3XnnDUh5r7KwQbLlErU,27465
|
|
35
35
|
pygeofetch/insar/gpu.py,sha256=E4Dx9-78s2GyKl2_dJrc6lXIHiy67tlqHlpkYue_418,3933
|
|
36
|
-
pygeofetch/insar/interferogram.py,sha256=
|
|
36
|
+
pygeofetch/insar/interferogram.py,sha256=gnthlgJztJPC0AvJZcUkVk7MMHCnPi_bqnv6tDagnZg,104166
|
|
37
37
|
pygeofetch/insar/ionosphere.py,sha256=vG60pyUtxyoJqLCMb_KjFEJvkDE1Bl8fqTzTTbrTIX8,30101
|
|
38
38
|
pygeofetch/insar/splitspectrum_diagnostic.py,sha256=_IlfBs__BW7Yz2RwdmWxfrOspe67jHMgJPyNyzK_rYE,7705
|
|
39
39
|
pygeofetch/insar/stack_selection.py,sha256=ebFwO063cCT9kn3zLPlkYu6fpnfx-SRC90Ch18E7qJk,13157
|
|
@@ -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.1.dist-info/licenses/LICENSE,sha256=zavMyhODD6NvZTmmKFEtATrngwY4CU_NEtY7lhjxGnI,1075
|
|
108
|
+
pygeofetch-2.0.1.dist-info/METADATA,sha256=eKxawrIllH8X0fPiSbLfS4X28sa3lUCc-_U2Bqu4Anw,43254
|
|
109
|
+
pygeofetch-2.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
110
|
+
pygeofetch-2.0.1.dist-info/entry_points.txt,sha256=dHiUXLF5xNJ8vgtXZoUQNleEMQUeDPZiKQOE1tW8SDY,55
|
|
111
|
+
pygeofetch-2.0.1.dist-info/top_level.txt,sha256=rOmnZargIfNk99fwVOqt6OJyJvpSTBBAYO5HU37yhHk,11
|
|
112
|
+
pygeofetch-2.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|