PyGeoFetch 2.0.0__tar.gz → 2.0.1__tar.gz
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-2.0.0 → pygeofetch-2.0.1}/CHANGELOG.md +6 -6
- {pygeofetch-2.0.0/pygeofetch.egg-info → pygeofetch-2.0.1}/PKG-INFO +2 -2
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/__init__.py +2 -2
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/main.py +2 -2
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/coregister.py +140 -52
- pygeofetch-2.0.1/pygeofetch/insar/esd.py +361 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/interferogram.py +117 -31
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/meta.yaml +1 -1
- {pygeofetch-2.0.0 → pygeofetch-2.0.1/pygeofetch.egg-info}/PKG-INFO +2 -2
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch.egg-info/requires.txt +1 -1
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pyproject.toml +2 -2
- pygeofetch-2.0.0/pygeofetch/insar/esd.py +0 -333
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/LICENSE +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/MANIFEST.in +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/README.md +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/auth_commands.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/config_commands.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/download_commands.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/index_commands.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/pipeline_process_commands.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/postprocess_commands.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/preprocess_commands.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/sar_commands.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/cli/search_commands.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/config/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/config/defaults.yaml +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/config/settings.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/core/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/core/authenticator.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/core/cache_manager.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/core/downloader.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/core/engine.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/core/logging.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/core/orbits.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/core/scheduler.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/core/searcher.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/annotation.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/atmosphere.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/deburst.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/extraction.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/flatearth.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/geolocation.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/gpu.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/ionosphere.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/stack_selection.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/synthetic.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/timeseries.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/unwrap.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/validate.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/visualize.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/insar/workflow.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/models/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/models/download_task.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/models/satellite_data.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/models/search_query.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/models/user_auth.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processing/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processing/base.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processing/batch.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processing/indices.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processing/pipeline.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processing/postprocessor.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processing/preprocessor.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processing/sar.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processor/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processor/indices.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processor/landsat.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processor/loader.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processor/stacker.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/processor/timeseries.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/airbus_oneatlas.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/aws_earth.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/base.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/copernicus.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/digitalglobe.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/earth_explorer_additional.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/element84.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/eodag_provider.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/esa_scihub.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/geoserver_generic.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/google_earth_engine.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/inpe_cbers.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/isro_bhuvan.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/jaxa_earth.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/maxar_gbdx.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/nasa_earthdata.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/noaa_big_data.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/opentopography.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/planet.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/planetary_computer.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/sentinel_hub.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/terrabotics.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/providers/usgs.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/sar/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/sar/_native.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/sar/_ost.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/sar/_sarxarray.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/sar/extraction.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/sar/processor.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/utils/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/utils/file_utils.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/utils/gcp_recovery.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/utils/geo_utils.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/utils/logging_setup.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/utils/retry_handler.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/utils/validators.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/viz/__init__.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/viz/map.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch/viz/plot.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch.egg-info/SOURCES.txt +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch.egg-info/dependency_links.txt +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch.egg-info/entry_points.txt +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch.egg-info/not-zip-safe +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/pygeofetch.egg-info/top_level.txt +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/setup.cfg +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_atmospheric_circular_regression (1).py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_atmospheric_circular_regression.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_build_sbas_network.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_burst_metadata.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_burst_processing_integration.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_coregister_integration.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_coregister_upgrades.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_era5_atmospheric_correction.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_extraction.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_flat_earth_phase.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_gcp_reprojection.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_insar.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_ionosphere.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_models.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_pgv_integration_contract.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_provider_geometry_audit (1).py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_provider_geometry_audit.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_provider_scene_disambiguation.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_providers.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_raster_collocation_integration.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_search_download.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_splitspectrum_diagnostic.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_utils.py +0 -0
- {pygeofetch-2.0.0 → pygeofetch-2.0.1}/tests/test_viz_plot.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to PyGeoFetch are documented here.
|
|
4
|
-
Format follows [Keep a Changelog](https://keepachangelog.com/en/2.0.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/2.0.1/).
|
|
5
5
|
|
|
6
|
-
## [2.0.
|
|
6
|
+
## [2.0.1] — 2026-07-29
|
|
7
7
|
|
|
8
8
|
### Added
|
|
9
9
|
- Major SNAPHU unwrapping improvements for urban InSAR applications
|
|
@@ -51,7 +51,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/2.0.0/).
|
|
|
51
51
|
|
|
52
52
|
### Migration Guide
|
|
53
53
|
```python
|
|
54
|
-
# Before (v2.0.
|
|
54
|
+
# Before (v2.0.1)
|
|
55
55
|
unwrapper = PhaseUnwrapper(cost_mode="defo", init_method="mcf")
|
|
56
56
|
unwrapped = unwrapper.unwrap(phase, coherence, nlooks=2.0)
|
|
57
57
|
|
|
@@ -72,7 +72,7 @@ unwrapped = unwrapper.unwrap(
|
|
|
72
72
|
|
|
73
73
|
---
|
|
74
74
|
|
|
75
|
-
## [2.0.
|
|
75
|
+
## [2.0.1] — 2026-07-12
|
|
76
76
|
|
|
77
77
|
### Added
|
|
78
78
|
- Federated search across 22+ satellite data providers
|
|
@@ -188,8 +188,8 @@ unwrapped = unwrapper.unwrap(
|
|
|
188
188
|
|
|
189
189
|
| Version | Date | Key Changes |
|
|
190
190
|
|---------|------------|-------------|
|
|
191
|
-
| **2.0.
|
|
192
|
-
| **2.0.
|
|
191
|
+
| **2.0.1** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
|
|
192
|
+
| **2.0.1** | 2026-07-12 | 22 providers, 17 indices, Sentinel-1C/D |
|
|
193
193
|
| **1.9.2** | 2026-06-15 | Batch processing, parallel downloads |
|
|
194
194
|
| **1.9.1** | 2026-05-20 | 3D terrain, SNAPHU integration, time series |
|
|
195
195
|
| **1.9.0** | 2026-04-25 | Initial public release |
|
|
@@ -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,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
|
|
|
@@ -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
|
|
|
@@ -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):
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real per-burst-overlap Enhanced Spectral Diversity (ESD), replacing the
|
|
3
|
+
previous whole-image approximation.
|
|
4
|
+
|
|
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).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from typing import List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from pygeofetch.insar.annotation import SwathTiming
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("pygeofetch.insar.esd")
|
|
27
|
+
|
|
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
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def compute_overlap_row_ranges(
|
|
35
|
+
swath_timing: SwathTiming,
|
|
36
|
+
azimuth_time_interval_s: float,
|
|
37
|
+
) -> List[Tuple[Tuple[int, int], Tuple[int, int]]]:
|
|
38
|
+
"""
|
|
39
|
+
Compute the row ranges for each burst-overlap region.
|
|
40
|
+
|
|
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).
|
|
44
|
+
"""
|
|
45
|
+
overlaps = []
|
|
46
|
+
lines_per_burst = swath_timing.lines_per_burst
|
|
47
|
+
bursts = swath_timing.bursts
|
|
48
|
+
|
|
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
|
|
53
|
+
|
|
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
|
|
57
|
+
|
|
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
|
|
62
|
+
)
|
|
63
|
+
burst_next_start_time = bursts[i + 1].azimuth_time.timestamp()
|
|
64
|
+
|
|
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
|
|
77
|
+
|
|
78
|
+
n_bw = bw_row_end - bw_row_start + 1
|
|
79
|
+
n_fw = fw_row_end - fw_row_start + 1
|
|
80
|
+
|
|
81
|
+
if n_bw != n_fw:
|
|
82
|
+
logger.warning(
|
|
83
|
+
"Overlap %d<->%d: backward-view row count (%d) does not "
|
|
84
|
+
"match forward-view row count (%d) -- unusual burst "
|
|
85
|
+
"timing; using the shorter of the two.",
|
|
86
|
+
i, i + 1, n_bw, n_fw,
|
|
87
|
+
)
|
|
88
|
+
n_common = min(n_bw, n_fw)
|
|
89
|
+
bw_row_start = bw_row_end - n_common + 1
|
|
90
|
+
fw_row_end = fw_row_start + n_common - 1
|
|
91
|
+
|
|
92
|
+
overlaps.append(((bw_row_start, bw_row_end), (fw_row_start, fw_row_end)))
|
|
93
|
+
|
|
94
|
+
return overlaps
|
|
95
|
+
|
|
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
|
+
|
|
208
|
+
def estimate_esd_shift_per_burst_overlap(
|
|
209
|
+
ref_complex,
|
|
210
|
+
sec_complex,
|
|
211
|
+
swath_timing: SwathTiming,
|
|
212
|
+
azimuth_time_interval_s: float,
|
|
213
|
+
row_offset: int = 0,
|
|
214
|
+
delta_f_ovl_hz: float = SENTINEL1_IW_DELTA_F_OVL_HZ,
|
|
215
|
+
coherence_threshold: float = 0.3,
|
|
216
|
+
) -> Tuple[Optional[float], List[Optional[float]]]:
|
|
217
|
+
"""
|
|
218
|
+
Real per-burst-overlap ESD: estimate azimuth misregistration from
|
|
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.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
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().
|
|
237
|
+
azimuth_time_interval_s: Real per-line time spacing.
|
|
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.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
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).
|
|
252
|
+
"""
|
|
253
|
+
import numpy as np
|
|
254
|
+
|
|
255
|
+
if not isinstance(swath_timing, SwathTiming):
|
|
256
|
+
raise TypeError(
|
|
257
|
+
f"estimate_esd_shift_per_burst_overlap expects a SwathTiming "
|
|
258
|
+
f"object, got {type(swath_timing).__name__}. Pass the result of "
|
|
259
|
+
f"parse_burst_info(), not a raw bursts list."
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
overlaps = compute_overlap_row_ranges(swath_timing, azimuth_time_interval_s)
|
|
263
|
+
per_overlap_shifts: List[Optional[float]] = []
|
|
264
|
+
|
|
265
|
+
n_outside = 0
|
|
266
|
+
n_below_coh = 0
|
|
267
|
+
n_usable = 0
|
|
268
|
+
|
|
269
|
+
h, w = ref_complex.shape
|
|
270
|
+
|
|
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
|
|
282
|
+
per_overlap_shifts.append(None)
|
|
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
|
+
)
|
|
289
|
+
continue
|
|
290
|
+
|
|
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,
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
n_usable += 1
|
|
313
|
+
per_overlap_shifts.append(shift_s)
|
|
314
|
+
logger.debug(
|
|
315
|
+
"Overlap %d<->%d: shift=%.6f ms, median_coh=%.3f",
|
|
316
|
+
i, i + 1, shift_s * 1000, median_coh,
|
|
317
|
+
)
|
|
318
|
+
|
|
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,
|
|
328
|
+
)
|
|
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
|
|
348
|
+
|
|
349
|
+
# Combine valid shifts via median (robust to outliers)
|
|
350
|
+
valid_shifts = [s for s in per_overlap_shifts if s is not None]
|
|
351
|
+
combined_shift = float(np.median(valid_shifts))
|
|
352
|
+
|
|
353
|
+
logger.info(
|
|
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,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return combined_shift, per_overlap_shifts
|