PyGeoFetch 1.9.0__tar.gz → 1.9.2__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-1.9.0 → pygeofetch-1.9.2}/CHANGELOG.md +2 -2
- {pygeofetch-1.9.0/pygeofetch.egg-info → pygeofetch-1.9.2}/PKG-INFO +3 -3
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/__init__.py +1 -1
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/core/engine.py +32 -8
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/__init__.py +4 -1
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/annotation.py +33 -2
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/atmosphere.py +32 -7
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/coregister.py +83 -6
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/extraction.py +182 -4
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/geolocation.py +55 -1
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/interferogram.py +69 -11
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/unwrap.py +120 -1
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/meta.yaml +1 -1
- {pygeofetch-1.9.0 → pygeofetch-1.9.2/pygeofetch.egg-info}/PKG-INFO +3 -3
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch.egg-info/requires.txt +2 -2
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pyproject.toml +4 -4
- pygeofetch-1.9.2/tests/test_insar.py +578 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/tests/test_pgv_integration_contract.py +2 -2
- pygeofetch-1.9.0/tests/test_insar.py +0 -465
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/LICENSE +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/MANIFEST.in +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/README.md +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/auth_commands.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/config_commands.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/download_commands.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/index_commands.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/main.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/pipeline_process_commands.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/postprocess_commands.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/preprocess_commands.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/sar_commands.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/cli/search_commands.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/config/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/config/defaults.yaml +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/config/settings.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/core/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/core/authenticator.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/core/cache_manager.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/core/downloader.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/core/logging.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/core/orbits.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/core/scheduler.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/core/searcher.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/gpu.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/timeseries.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/validate.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/insar/visualize.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/models/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/models/download_task.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/models/satellite_data.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/models/search_query.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/models/user_auth.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processing/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processing/base.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processing/batch.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processing/indices.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processing/pipeline.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processing/postprocessor.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processing/preprocessor.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processing/sar.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processor/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processor/indices.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processor/landsat.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processor/loader.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processor/stacker.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/processor/timeseries.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/airbus_oneatlas.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/aws_earth.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/base.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/copernicus.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/digitalglobe.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/earth_explorer_additional.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/element84.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/eodag_provider.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/esa_scihub.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/geoserver_generic.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/google_earth_engine.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/inpe_cbers.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/isro_bhuvan.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/jaxa_earth.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/maxar_gbdx.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/nasa_earthdata.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/noaa_big_data.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/opentopography.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/planet.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/planetary_computer.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/sentinel_hub.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/terrabotics.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/providers/usgs.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/sar/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/sar/_native.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/sar/_ost.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/sar/_sarxarray.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/sar/extraction.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/sar/processor.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/utils/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/utils/file_utils.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/utils/gcp_recovery.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/utils/geo_utils.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/utils/logging_setup.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/utils/retry_handler.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/utils/validators.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/viz/__init__.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/viz/map.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch/viz/plot.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch.egg-info/SOURCES.txt +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch.egg-info/dependency_links.txt +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch.egg-info/entry_points.txt +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch.egg-info/not-zip-safe +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/pygeofetch.egg-info/top_level.txt +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/setup.cfg +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/tests/test_extraction.py +0 -0
- {pygeofetch-1.9.0 → pygeofetch-1.9.2}/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/1.9.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.9.2/).
|
|
5
5
|
|
|
6
|
-
## [1.9.
|
|
6
|
+
## [1.9.2] — 2026-07-12
|
|
7
7
|
|
|
8
8
|
### Added
|
|
9
9
|
- Federated search across 22+ satellite data providers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygeofetch
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.2
|
|
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
|
|
@@ -94,8 +94,8 @@ Requires-Dist: plotly>=5.14.0; extra == "viz"
|
|
|
94
94
|
Requires-Dist: whitebox>=2.3.0; extra == "viz"
|
|
95
95
|
Requires-Dist: whiteboxgui>=2.3.0; extra == "viz"
|
|
96
96
|
Requires-Dist: geemap>=0.28.0; extra == "viz"
|
|
97
|
-
Requires-Dist: osmnx>=1.9.
|
|
98
|
-
Requires-Dist: contextily>=1.9.
|
|
97
|
+
Requires-Dist: osmnx>=1.9.2; extra == "viz"
|
|
98
|
+
Requires-Dist: contextily>=1.9.2; extra == "viz"
|
|
99
99
|
Requires-Dist: xyzservices>=2023.10.0; extra == "viz"
|
|
100
100
|
Requires-Dist: mapclassify>=2.5.0; extra == "viz"
|
|
101
101
|
Requires-Dist: geoplot>=0.5.0; extra == "viz"
|
|
@@ -176,7 +176,7 @@ class PyGeoFetch:
|
|
|
176
176
|
if _product.upper() == "SLC" and _effective:
|
|
177
177
|
_effective = self._route_slc_providers(_effective)
|
|
178
178
|
results = self.searcher.search(query, providers=_effective, use_cache=use_cache)
|
|
179
|
-
self._warn_if_outdated_constellation(results)
|
|
179
|
+
self._warn_if_outdated_constellation(results, query)
|
|
180
180
|
return results
|
|
181
181
|
|
|
182
182
|
def search_and_save(
|
|
@@ -217,19 +217,43 @@ class PyGeoFetch:
|
|
|
217
217
|
}
|
|
218
218
|
_GRD_ONLY = {"planetary_computer", "aws_earth", "element84"}
|
|
219
219
|
|
|
220
|
-
def _warn_if_outdated_constellation(self, results: list) -> None:
|
|
221
|
-
"""Log a warning if results contain only decommissioned satellites
|
|
222
|
-
|
|
220
|
+
def _warn_if_outdated_constellation(self, results: list, query) -> None:
|
|
221
|
+
"""Log a warning if results contain only decommissioned satellites
|
|
222
|
+
for a search whose OWN requested date range extends past S1A's
|
|
223
|
+
real decommissioning date — not just because today happens to be
|
|
224
|
+
past it. A search for genuine historical data (e.g. November
|
|
225
|
+
2024, well before S1A's actual 1 July 2026 end of mission) must
|
|
226
|
+
never trigger this, regardless of what today's date is; S1A
|
|
227
|
+
results from before decommissioning are completely legitimate."""
|
|
228
|
+
from datetime import date, datetime
|
|
223
229
|
|
|
224
230
|
S1A_DECOM = date(2026, 7, 1)
|
|
225
|
-
|
|
231
|
+
|
|
232
|
+
query_end = getattr(query, "end_date", None)
|
|
233
|
+
if query_end is None:
|
|
234
|
+
# No end date specified -- fall back to whether the search
|
|
235
|
+
# was actually run after decommissioning (the old behaviour,
|
|
236
|
+
# but only as a last resort when there's no query date to
|
|
237
|
+
# check against).
|
|
238
|
+
query_end_date = date.today()
|
|
239
|
+
elif isinstance(query_end, str):
|
|
240
|
+
query_end_date = datetime.fromisoformat(query_end.replace("Z", "")).date()
|
|
241
|
+
elif isinstance(query_end, datetime):
|
|
242
|
+
query_end_date = query_end.date()
|
|
243
|
+
elif isinstance(query_end, date):
|
|
244
|
+
query_end_date = query_end
|
|
245
|
+
else:
|
|
246
|
+
query_end_date = date.today()
|
|
247
|
+
|
|
248
|
+
if query_end_date >= S1A_DECOM:
|
|
226
249
|
platforms = {getattr(r, "satellite", "") or "" for r in results}
|
|
227
250
|
normalised = {_normalise_satellite_name(p) for p in platforms}
|
|
228
251
|
if any("S1" in p for p in normalised): # only check if S1 results
|
|
229
252
|
if not any(p in {"S1C", "S1D"} for p in normalised):
|
|
230
253
|
logger.warning(
|
|
231
|
-
"Search returned only S1A/S1B results
|
|
232
|
-
"
|
|
254
|
+
"Search returned only S1A/S1B results for a date range "
|
|
255
|
+
"extending past Sentinel-1A's real decommissioning date "
|
|
256
|
+
"(1 July 2026). This may indicate a provider collection "
|
|
233
257
|
"routing issue. Check platform filter settings."
|
|
234
258
|
)
|
|
235
259
|
|
|
@@ -460,4 +484,4 @@ class PyGeoFetch:
|
|
|
460
484
|
|
|
461
485
|
def __repr__(self) -> str:
|
|
462
486
|
authed = [item["provider"] for item in self.auth.list()]
|
|
463
|
-
return f"PyGeoFetch(authenticated={authed}, cache_entries={len(self.searcher.cache._cache)})"
|
|
487
|
+
return f"PyGeoFetch(authenticated={authed}, cache_entries={len(self.searcher.cache._cache)})"
|
|
@@ -55,7 +55,7 @@ from pygeofetch.insar.extraction import SLCExtractor
|
|
|
55
55
|
from pygeofetch.insar.gpu import gpu_available
|
|
56
56
|
from pygeofetch.insar.interferogram import InterferogramGenerator, InterferogramResult
|
|
57
57
|
from pygeofetch.insar.timeseries import SBASTimeSeries
|
|
58
|
-
from pygeofetch.insar.unwrap import PhaseUnwrapper
|
|
58
|
+
from pygeofetch.insar.unwrap import PhaseUnwrapper, multilook
|
|
59
59
|
from pygeofetch.insar.validate import DataValidator, ValidationResult
|
|
60
60
|
from pygeofetch.insar.visualize import (
|
|
61
61
|
visualize_interferogram,
|
|
@@ -79,6 +79,7 @@ from pygeofetch.insar.geolocation import (
|
|
|
79
79
|
find_zero_doppler_time,
|
|
80
80
|
parse_orbit_file,
|
|
81
81
|
interpolate_orbit_state,
|
|
82
|
+
los_to_vertical_displacement,
|
|
82
83
|
)
|
|
83
84
|
from pygeofetch.insar.coregister import (
|
|
84
85
|
compute_offset_field_from_dem,
|
|
@@ -90,6 +91,7 @@ __all__ = [
|
|
|
90
91
|
"InterferogramGenerator",
|
|
91
92
|
"InterferogramResult",
|
|
92
93
|
"PhaseUnwrapper",
|
|
94
|
+
"multilook",
|
|
93
95
|
"SBASTimeSeries",
|
|
94
96
|
"AtmosphericCorrector",
|
|
95
97
|
"SLCExtractor",
|
|
@@ -105,6 +107,7 @@ __all__ = [
|
|
|
105
107
|
"find_zero_doppler_time",
|
|
106
108
|
"parse_orbit_file",
|
|
107
109
|
"interpolate_orbit_state",
|
|
110
|
+
"los_to_vertical_displacement",
|
|
108
111
|
"compute_offset_field_from_dem",
|
|
109
112
|
"fit_offset_polynomial",
|
|
110
113
|
"resample_with_offset_field",
|
|
@@ -15,6 +15,19 @@ MPC-0392):
|
|
|
15
15
|
/product/imageAnnotation/imageInformation/azimuthTimeInterval
|
|
16
16
|
/product/imageAnnotation/imageInformation/slantRangeTime
|
|
17
17
|
/product/generalAnnotation/productInformation/rangeSamplingRate
|
|
18
|
+
|
|
19
|
+
Dimension field paths confirmed separately, against ESA's Sentinel-1
|
|
20
|
+
Product Specification (MPC-0240), which documents renaming
|
|
21
|
+
numberOfAzimuthLines -> numberOfLines and numberOfRangeSamples ->
|
|
22
|
+
numberOfSamples:
|
|
23
|
+
|
|
24
|
+
/product/imageAnnotation/imageInformation/numberOfLines
|
|
25
|
+
/product/imageAnnotation/imageInformation/numberOfSamples
|
|
26
|
+
|
|
27
|
+
("numberOfColumns" is not a real field in this schema at all -- an
|
|
28
|
+
earlier, unverified assumption here that only surfaced against real
|
|
29
|
+
Sentinel-1 data, since synthetic tests built on the same assumption
|
|
30
|
+
couldn't have caught their own premise being wrong.)
|
|
18
31
|
"""
|
|
19
32
|
|
|
20
33
|
from __future__ import annotations
|
|
@@ -103,6 +116,17 @@ def parse_slc_geometry(
|
|
|
103
116
|
n for n in zf.namelist()
|
|
104
117
|
if "/annotation/" in n and n.lower().endswith(".xml")
|
|
105
118
|
and "/calibration/" not in n.lower()
|
|
119
|
+
# Exclude RFI (radio frequency interference) report files --
|
|
120
|
+
# confirmed real, present in Sentinel-1 products since IPF
|
|
121
|
+
# 3.40, living in this same annotation tree. Their filenames
|
|
122
|
+
# are prefixed "rfi-" (not the satellite-platform prefix
|
|
123
|
+
# "s1a-"/"s1b-" real per-swath annotation files use), and
|
|
124
|
+
# critically can ALSO contain "-slc-" for SLC products
|
|
125
|
+
# (confirmed by testing: an earlier "-slc-" based filter was
|
|
126
|
+
# insufficient for exactly this reason), so the prefix is
|
|
127
|
+
# the only reliable differentiator, not substring content.
|
|
128
|
+
and not Path(n).name.lower().startswith("rfi-")
|
|
129
|
+
and "/rfi/" not in n.lower()
|
|
106
130
|
]
|
|
107
131
|
if member_hint:
|
|
108
132
|
filtered = [n for n in candidates if member_hint.lower() in n.lower()]
|
|
@@ -142,7 +166,14 @@ def parse_slc_geometry(
|
|
|
142
166
|
get_text(".//generalAnnotation/productInformation/rangeSamplingRate")
|
|
143
167
|
),
|
|
144
168
|
n_lines=int(get_text(".//imageAnnotation/imageInformation/numberOfLines")),
|
|
145
|
-
|
|
169
|
+
# Real field name confirmed against ESA's own Product
|
|
170
|
+
# Specification (MPC-0240): "Renamed numberOfRangeSamples to
|
|
171
|
+
# numberOfSamples". "numberOfColumns" was never a real field in
|
|
172
|
+
# this schema at all -- an incorrect assumption in earlier
|
|
173
|
+
# research here, caught only against real Sentinel-1 data,
|
|
174
|
+
# since the synthetic tests built on that same assumption
|
|
175
|
+
# couldn't have caught their own premise being wrong.
|
|
176
|
+
n_columns=int(get_text(".//imageAnnotation/imageInformation/numberOfSamples")),
|
|
146
177
|
)
|
|
147
178
|
|
|
148
179
|
logger.info(
|
|
@@ -150,4 +181,4 @@ def parse_slc_geometry(
|
|
|
150
181
|
Path(safe_zip_path).name, geometry.n_lines, geometry.n_columns,
|
|
151
182
|
geometry.first_line_time,
|
|
152
183
|
)
|
|
153
|
-
return geometry
|
|
184
|
+
return geometry
|
|
@@ -74,6 +74,7 @@ class AtmosphericCorrector:
|
|
|
74
74
|
dem: Union[str, Path],
|
|
75
75
|
acquisition_datetime: Optional[str] = None,
|
|
76
76
|
incidence_angle_deg: float = 38.0,
|
|
77
|
+
return_metadata: bool = False,
|
|
77
78
|
) -> Any:
|
|
78
79
|
"""
|
|
79
80
|
Remove the tropospheric delay component from wrapped or unwrapped phase.
|
|
@@ -88,15 +89,33 @@ class AtmosphericCorrector:
|
|
|
88
89
|
incidence_angle_deg: Radar incidence angle for LOS projection
|
|
89
90
|
of zenith delay (Sentinel-1 IW ≈ 30-46°,
|
|
90
91
|
default 38° is the mid-swath average).
|
|
92
|
+
return_metadata: If True, returns (phase, metadata) instead
|
|
93
|
+
of just phase. metadata includes
|
|
94
|
+
"correction_applied" (bool) and, for the
|
|
95
|
+
elevation method, "r_squared" (float) --
|
|
96
|
+
this is the only reliable way to know
|
|
97
|
+
whether a correction actually happened,
|
|
98
|
+
since the elevation method's internal R²
|
|
99
|
+
gate can legitimately skip correction
|
|
100
|
+
(returning the input unchanged) without
|
|
101
|
+
raising or otherwise signalling that in
|
|
102
|
+
the plain return value. Default False
|
|
103
|
+
preserves the original return type for
|
|
104
|
+
existing callers.
|
|
91
105
|
|
|
92
106
|
Returns:
|
|
93
|
-
Corrected phase array, same shape and units as input
|
|
107
|
+
Corrected phase array, same shape and units as input (or a
|
|
108
|
+
(phase, metadata) tuple if return_metadata=True).
|
|
94
109
|
"""
|
|
95
110
|
if self._method == "era5":
|
|
96
|
-
|
|
111
|
+
result = self._correct_era5(
|
|
97
112
|
phase, dem, acquisition_datetime, incidence_angle_deg
|
|
98
113
|
)
|
|
99
|
-
|
|
114
|
+
metadata = {"correction_applied": True}
|
|
115
|
+
else:
|
|
116
|
+
result, metadata = self._correct_elevation(phase, dem)
|
|
117
|
+
|
|
118
|
+
return (result, metadata) if return_metadata else result
|
|
100
119
|
|
|
101
120
|
# ── native elevation-correlated correction ────────────────────────────────
|
|
102
121
|
|
|
@@ -121,6 +140,12 @@ class AtmosphericCorrector:
|
|
|
121
140
|
the phase variance (R² > 0.5); otherwise it is skipped and logged,
|
|
122
141
|
since removing a low-confidence trend risks deleting real
|
|
123
142
|
deformation signal rather than atmospheric noise.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
(phase_or_corrected, metadata) -- metadata always includes
|
|
146
|
+
"correction_applied" (bool) and, whenever the R² gate was
|
|
147
|
+
actually evaluated, "r_squared" (float), so callers can tell
|
|
148
|
+
a genuine correction apart from a same-shaped pass-through.
|
|
124
149
|
"""
|
|
125
150
|
np = self._np()
|
|
126
151
|
try:
|
|
@@ -145,7 +170,7 @@ class AtmosphericCorrector:
|
|
|
145
170
|
logger.warning(
|
|
146
171
|
"Insufficient valid pixels for elevation correction — returning uncorrected"
|
|
147
172
|
)
|
|
148
|
-
return phase
|
|
173
|
+
return phase, {"correction_applied": False, "reason": "insufficient_valid_pixels"}
|
|
149
174
|
|
|
150
175
|
dem_v = dem_data[valid]
|
|
151
176
|
phase_v = phase[valid]
|
|
@@ -164,7 +189,7 @@ class AtmosphericCorrector:
|
|
|
164
189
|
"correction to avoid absorbing real deformation signal.",
|
|
165
190
|
r_squared,
|
|
166
191
|
)
|
|
167
|
-
return phase
|
|
192
|
+
return phase, {"correction_applied": False, "r_squared": float(r_squared)}
|
|
168
193
|
|
|
169
194
|
tropo_phase = slope_rad_per_m * dem_data
|
|
170
195
|
corrected = phase - tropo_phase
|
|
@@ -174,7 +199,7 @@ class AtmosphericCorrector:
|
|
|
174
199
|
slope_rad_per_m,
|
|
175
200
|
int(valid.sum()),
|
|
176
201
|
)
|
|
177
|
-
return corrected.astype(np.float32)
|
|
202
|
+
return corrected.astype(np.float32), {"correction_applied": True, "r_squared": float(r_squared)}
|
|
178
203
|
|
|
179
204
|
# ── ERA5/PyAPS-based correction ───────────────────────────────────────────
|
|
180
205
|
|
|
@@ -280,4 +305,4 @@ class AtmosphericCorrector:
|
|
|
280
305
|
def _np(self):
|
|
281
306
|
import numpy as np
|
|
282
307
|
|
|
283
|
-
return np
|
|
308
|
+
return np
|
|
@@ -21,11 +21,49 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import logging
|
|
23
23
|
from pathlib import Path
|
|
24
|
-
from typing import Tuple, Union
|
|
24
|
+
from typing import Optional, Tuple, Union
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger("pygeofetch.insar.coregister")
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def read_matched_swath(path: Union[str, Path]) -> Optional[str]:
|
|
30
|
+
"""
|
|
31
|
+
Read back the matched sub-swath label (e.g. "iw3") recorded by
|
|
32
|
+
SLCExtractor.extract_scene(). Returns None if not present, so
|
|
33
|
+
callers can fall back to parse_slc_geometry()'s default (arbitrary
|
|
34
|
+
first-match) behaviour rather than fail outright.
|
|
35
|
+
"""
|
|
36
|
+
import rasterio
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
with rasterio.open(path) as src:
|
|
40
|
+
return src.tags().get("matched_swath") or None
|
|
41
|
+
except Exception:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_crop_offset(path: Union[str, Path]) -> Tuple[float, float]:
|
|
46
|
+
"""
|
|
47
|
+
Read back the crop offset recorded by SLCExtractor._crop_to_aoi() -
|
|
48
|
+
the local array's origin in the original, uncropped scene's pixel
|
|
49
|
+
coordinates. Returns (0.0, 0.0) if the file wasn't cropped (either
|
|
50
|
+
it's the raw fallback extraction, or a file this metadata predates),
|
|
51
|
+
which is the correct default: no crop means local and full-scene
|
|
52
|
+
coordinates are already the same thing.
|
|
53
|
+
"""
|
|
54
|
+
import rasterio
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with rasterio.open(path) as src:
|
|
58
|
+
tags = src.tags()
|
|
59
|
+
return (
|
|
60
|
+
float(tags.get("crop_row_off", 0.0)),
|
|
61
|
+
float(tags.get("crop_col_off", 0.0)),
|
|
62
|
+
)
|
|
63
|
+
except Exception:
|
|
64
|
+
return (0.0, 0.0)
|
|
65
|
+
|
|
66
|
+
|
|
29
67
|
def compute_offset_field_from_dem(
|
|
30
68
|
ref_geometry,
|
|
31
69
|
ref_orbit,
|
|
@@ -294,7 +332,11 @@ def fit_offset_polynomial(grid_rows, grid_cols, offsets, degree: int = 1):
|
|
|
294
332
|
return evaluate
|
|
295
333
|
|
|
296
334
|
|
|
297
|
-
def resample_with_offset_field(
|
|
335
|
+
def resample_with_offset_field(
|
|
336
|
+
data, row_offset_fn, col_offset_fn,
|
|
337
|
+
ref_row_offset: float = 0.0, ref_col_offset: float = 0.0,
|
|
338
|
+
sec_row_offset: float = 0.0, sec_col_offset: float = 0.0,
|
|
339
|
+
):
|
|
298
340
|
"""
|
|
299
341
|
Resample a complex array using a real, per-pixel offset field
|
|
300
342
|
(from fit_offset_polynomial), rather than a naive shape-only zoom.
|
|
@@ -303,7 +345,24 @@ def resample_with_offset_field(data, row_offset_fn, col_offset_fn):
|
|
|
303
345
|
data: Complex array to resample (the secondary SLC).
|
|
304
346
|
row_offset_fn, col_offset_fn: Callables from fit_offset_polynomial(),
|
|
305
347
|
giving the real (secondary - reference) offset
|
|
306
|
-
at any (row, col) in
|
|
348
|
+
at any (row, col) expressed in REFERENCE
|
|
349
|
+
full-scene coordinates (that's what
|
|
350
|
+
compute_offset_field_from_dem() fits them on).
|
|
351
|
+
ref_row_offset, ref_col_offset: The reference array's origin in
|
|
352
|
+
full-scene coordinates (from read_crop_offset()
|
|
353
|
+
on the reference file, if cropped) -- needed to
|
|
354
|
+
correctly evaluate the offset functions, since
|
|
355
|
+
they were fit on full-scene coordinates, not the
|
|
356
|
+
reference crop's local 0-based ones.
|
|
357
|
+
sec_row_offset, sec_col_offset: The secondary array's (this
|
|
358
|
+
function's `data`) origin in full-scene
|
|
359
|
+
coordinates -- needed to convert the computed
|
|
360
|
+
full-scene secondary sample position back into
|
|
361
|
+
`data`'s own local coordinates for indexing,
|
|
362
|
+
since the secondary crop can have a different
|
|
363
|
+
offset than the reference crop.
|
|
364
|
+
All four default to 0 for already-full-scene
|
|
365
|
+
(uncropped) data.
|
|
307
366
|
|
|
308
367
|
Returns:
|
|
309
368
|
The resampled complex array, on the reference image's grid.
|
|
@@ -314,8 +373,26 @@ def resample_with_offset_field(data, row_offset_fn, col_offset_fn):
|
|
|
314
373
|
h, w = data.shape
|
|
315
374
|
row_idx, col_idx = np.mgrid[0:h, 0:w].astype(np.float64)
|
|
316
375
|
|
|
317
|
-
|
|
318
|
-
|
|
376
|
+
# row_idx/col_idx are the reference grid's local (0-based) coordinates.
|
|
377
|
+
# Convert to full-scene coordinates to correctly evaluate the fitted
|
|
378
|
+
# offset functions, which were built on real annotation-derived
|
|
379
|
+
# full-scene coordinates, not any particular crop's local ones.
|
|
380
|
+
ref_global_row = row_idx + ref_row_offset
|
|
381
|
+
ref_global_col = col_idx + ref_col_offset
|
|
382
|
+
|
|
383
|
+
offset_row = row_offset_fn(ref_global_row, ref_global_col)
|
|
384
|
+
offset_col = col_offset_fn(ref_global_row, ref_global_col)
|
|
385
|
+
|
|
386
|
+
# Where this pixel's secondary counterpart is, in full-scene coordinates
|
|
387
|
+
sec_global_row = ref_global_row + offset_row
|
|
388
|
+
sec_global_col = ref_global_col + offset_col
|
|
389
|
+
|
|
390
|
+
# Convert to the SECONDARY array's own local coordinates for actually
|
|
391
|
+
# indexing into `data` -- the secondary crop can have a different
|
|
392
|
+
# offset than the reference crop, so this is not the same subtraction
|
|
393
|
+
# as ref_row_offset/ref_col_offset above.
|
|
394
|
+
sample_rows = sec_global_row - sec_row_offset
|
|
395
|
+
sample_cols = sec_global_col - sec_col_offset
|
|
319
396
|
|
|
320
397
|
real = map_coordinates(data.real, [sample_rows, sample_cols], order=1, mode="constant", cval=0.0)
|
|
321
398
|
imag = map_coordinates(data.imag, [sample_rows, sample_cols], order=1, mode="constant", cval=0.0)
|
|
@@ -336,4 +413,4 @@ def _linspace(start, stop, n):
|
|
|
336
413
|
if n == 1:
|
|
337
414
|
return [start]
|
|
338
415
|
step = (stop - start) / (n - 1)
|
|
339
|
-
return [start + i * step for i in range(n)]
|
|
416
|
+
return [start + i * step for i in range(n)]
|
|
@@ -189,15 +189,193 @@ class SLCExtractor:
|
|
|
189
189
|
else output_dir / f"{zip_path.stem}_{self._pol}.tif"
|
|
190
190
|
)
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
cropped = self._crop_to_aoi(zip_path, matched_member, aoi_tuple, out_path)
|
|
193
|
+
if cropped is None:
|
|
194
|
+
logger.warning(
|
|
195
|
+
"Cropping %s to AOI failed — falling back to extracting the "
|
|
196
|
+
"full, uncropped sub-swath (this will be much larger and "
|
|
197
|
+
"slower for everything downstream).",
|
|
198
|
+
self._swath_label(matched_member),
|
|
199
|
+
)
|
|
200
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
201
|
+
with zf.open(matched_member) as src_f, open(out_path, "wb") as dst_f:
|
|
202
|
+
dst_f.write(src_f.read())
|
|
203
|
+
self._tag_matched_swath(out_path, matched_member)
|
|
195
204
|
|
|
196
205
|
logger.info(
|
|
197
206
|
"Extracted %s -> %s", self._swath_label(matched_member), out_path.name
|
|
198
207
|
)
|
|
199
208
|
return out_path
|
|
200
209
|
|
|
210
|
+
def _tag_matched_swath(self, out_path: Path, member_name: str) -> None:
|
|
211
|
+
"""Tag an already-written GeoTIFF with its matched sub-swath label,
|
|
212
|
+
for the fallback (raw-copy) extraction path, which doesn't go
|
|
213
|
+
through rasterio for its initial write the way _crop_to_aoi()
|
|
214
|
+
does. Failure here is non-fatal -- the file itself is still
|
|
215
|
+
correct, just missing this one piece of discoverable metadata,
|
|
216
|
+
so downstream code falls back to arbitrary-first-match behaviour
|
|
217
|
+
rather than losing the extraction entirely."""
|
|
218
|
+
try:
|
|
219
|
+
import rasterio
|
|
220
|
+
|
|
221
|
+
with rasterio.open(out_path, "r+") as dst:
|
|
222
|
+
dst.update_tags(matched_swath=self._swath_label(member_name).lower())
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
logger.warning(
|
|
225
|
+
"Could not tag %s with its matched sub-swath: %s", out_path.name, exc
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def _crop_to_aoi(
|
|
229
|
+
self,
|
|
230
|
+
zip_path: Path,
|
|
231
|
+
member_name: str,
|
|
232
|
+
aoi_tuple: Tuple[float, float, float, float],
|
|
233
|
+
out_path: Path,
|
|
234
|
+
margin_frac: float = 0.15,
|
|
235
|
+
) -> Optional[Path]:
|
|
236
|
+
"""
|
|
237
|
+
Crop a zipped Sentinel-1 measurement TIFF to the AOI, reading
|
|
238
|
+
directly from inside the zip (no full-file extraction) and
|
|
239
|
+
writing only the windowed AOI region.
|
|
240
|
+
|
|
241
|
+
Sentinel-1 SLC TIFFs are GCP-georeferenced, not a simple affine
|
|
242
|
+
transform, so an exact pixel window can't be computed directly.
|
|
243
|
+
This fits an approximate affine transform from the real embedded
|
|
244
|
+
GCPs (rasterio.transform.from_gcps — the standard technique for
|
|
245
|
+
this), then adds a safety margin around the computed window to
|
|
246
|
+
protect against that approximation's error rather than risk
|
|
247
|
+
cutting off the real AOI.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
margin_frac: Fractional padding added to the computed window
|
|
251
|
+
on each side (0.15 = 15%) — generous on purpose,
|
|
252
|
+
since padding too much only costs a little extra
|
|
253
|
+
size, while padding too little risks silently
|
|
254
|
+
losing part of the real area of interest.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
out_path on success, None if cropping wasn't possible for
|
|
258
|
+
any reason (missing GCPs, unreadable TIFF, degenerate
|
|
259
|
+
transform) — callers should fall back to full extraction
|
|
260
|
+
rather than fail outright.
|
|
261
|
+
"""
|
|
262
|
+
import rasterio
|
|
263
|
+
from rasterio.transform import from_gcps
|
|
264
|
+
from rasterio.windows import Window
|
|
265
|
+
from rasterio.crs import CRS
|
|
266
|
+
|
|
267
|
+
vsi_path = f"/vsizip/{zip_path}/{member_name}"
|
|
268
|
+
try:
|
|
269
|
+
with rasterio.open(vsi_path) as src:
|
|
270
|
+
gcps, _gcp_crs = src.gcps
|
|
271
|
+
if not gcps or len(gcps) < 4:
|
|
272
|
+
logger.warning(
|
|
273
|
+
"%s: too few GCPs (%d) to fit a reliable crop transform",
|
|
274
|
+
member_name, len(gcps) if gcps else 0,
|
|
275
|
+
)
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
approx_transform = from_gcps(gcps)
|
|
279
|
+
|
|
280
|
+
min_lon, min_lat, max_lon, max_lat = aoi_tuple
|
|
281
|
+
# Inverse-transform all 4 AOI corners and take the bounding
|
|
282
|
+
# box of the results, rather than rasterio.windows.from_bounds()
|
|
283
|
+
# -- that function assumes a standard north-up transform
|
|
284
|
+
# (row increases as latitude decreases) and raises "Bounds
|
|
285
|
+
# and transform are inconsistent" when it doesn't, which is
|
|
286
|
+
# exactly the orientation real ascending-pass Sentinel-1
|
|
287
|
+
# data has (confirmed directly against real product
|
|
288
|
+
# footprints). This corner-based approach makes no
|
|
289
|
+
# assumption about orientation at all.
|
|
290
|
+
inv_transform = ~approx_transform
|
|
291
|
+
corners = [
|
|
292
|
+
(min_lon, min_lat), (min_lon, max_lat),
|
|
293
|
+
(max_lon, min_lat), (max_lon, max_lat),
|
|
294
|
+
]
|
|
295
|
+
cols, rows = [], []
|
|
296
|
+
for lon, lat in corners:
|
|
297
|
+
col, row = inv_transform * (lon, lat)
|
|
298
|
+
cols.append(col)
|
|
299
|
+
rows.append(row)
|
|
300
|
+
window = Window(
|
|
301
|
+
col_off=min(cols), row_off=min(rows),
|
|
302
|
+
width=max(cols) - min(cols), height=max(rows) - min(rows),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Safety margin: the GCP-fitted transform is only an
|
|
306
|
+
# approximation, so pad the window generously rather than
|
|
307
|
+
# risk cropping out part of the real AOI.
|
|
308
|
+
pad_w = max(window.width * margin_frac, 50)
|
|
309
|
+
pad_h = max(window.height * margin_frac, 50)
|
|
310
|
+
col_off = window.col_off - pad_w
|
|
311
|
+
row_off = window.row_off - pad_h
|
|
312
|
+
width = window.width + 2 * pad_w
|
|
313
|
+
height = window.height + 2 * pad_h
|
|
314
|
+
|
|
315
|
+
# Clamp to the actual raster extent -- the padded window
|
|
316
|
+
# can legitimately extend past the real data bounds.
|
|
317
|
+
col_off_clamped = max(0, col_off)
|
|
318
|
+
row_off_clamped = max(0, row_off)
|
|
319
|
+
width_clamped = min(width - (col_off_clamped - col_off), src.width - col_off_clamped)
|
|
320
|
+
height_clamped = min(height - (row_off_clamped - row_off), src.height - row_off_clamped)
|
|
321
|
+
|
|
322
|
+
if width_clamped <= 0 or height_clamped <= 0:
|
|
323
|
+
logger.warning(
|
|
324
|
+
"%s: computed crop window falls entirely outside the "
|
|
325
|
+
"raster bounds -- GCP transform may be unreliable",
|
|
326
|
+
member_name,
|
|
327
|
+
)
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
read_window = Window(
|
|
331
|
+
col_off_clamped, row_off_clamped, width_clamped, height_clamped
|
|
332
|
+
)
|
|
333
|
+
data = src.read(1, window=read_window)
|
|
334
|
+
|
|
335
|
+
if data.size == 0:
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
# Real, locally-accurate transform for the cropped region,
|
|
339
|
+
# derived from the same GCP-fit transform used to locate it.
|
|
340
|
+
crop_transform = rasterio.windows.transform(read_window, approx_transform)
|
|
341
|
+
|
|
342
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
343
|
+
with rasterio.open(
|
|
344
|
+
out_path, "w", driver="GTiff",
|
|
345
|
+
dtype=data.dtype, count=1,
|
|
346
|
+
width=data.shape[1], height=data.shape[0],
|
|
347
|
+
crs=CRS.from_epsg(4326), transform=crop_transform,
|
|
348
|
+
compress="deflate",
|
|
349
|
+
) as dst:
|
|
350
|
+
dst.write(data, 1)
|
|
351
|
+
# Record the crop's origin in the FULL, UNCROPPED
|
|
352
|
+
# scene's pixel coordinates. Real orbit-based
|
|
353
|
+
# coregistration computes offset fields using
|
|
354
|
+
# annotation.py's real acquisition timing, which
|
|
355
|
+
# describes the full, uncropped sub-swath -- without
|
|
356
|
+
# this offset recorded, downstream code has no way to
|
|
357
|
+
# know that local row/col 0,0 in this cropped file is
|
|
358
|
+
# actually row/col crop_row_off,crop_col_off in the
|
|
359
|
+
# scene the orbit math is expressed in, and would
|
|
360
|
+
# silently evaluate the fitted offset polynomial at
|
|
361
|
+
# the wrong coordinates.
|
|
362
|
+
dst.update_tags(
|
|
363
|
+
crop_row_off=str(row_off_clamped),
|
|
364
|
+
crop_col_off=str(col_off_clamped),
|
|
365
|
+
matched_swath=self._swath_label(member_name).lower(),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
logger.info(
|
|
369
|
+
"Cropped %s: %dx%d -> %dx%d (%.1fx smaller)",
|
|
370
|
+
self._swath_label(member_name), src.width, src.height,
|
|
371
|
+
data.shape[1], data.shape[0],
|
|
372
|
+
(src.width * src.height) / max(data.size, 1),
|
|
373
|
+
)
|
|
374
|
+
return out_path
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
logger.warning("Could not crop %s to AOI: %s", member_name, exc)
|
|
377
|
+
return None
|
|
378
|
+
|
|
201
379
|
def list_subswaths(self, zip_path: Union[str, Path]) -> List[str]:
|
|
202
380
|
"""
|
|
203
381
|
List the measurement TIFF entries for the configured polarisation
|
|
@@ -295,4 +473,4 @@ class SLCExtractor:
|
|
|
295
473
|
for swath in ("iw1", "iw2", "iw3", "ew1", "ew2", "ew3", "ew4", "ew5"):
|
|
296
474
|
if swath in lower:
|
|
297
475
|
return swath.upper()
|
|
298
|
-
return "?"
|
|
476
|
+
return "?"
|