asp-plot 1.17.0__tar.gz → 1.18.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.
- {asp_plot-1.17.0 → asp_plot-1.18.1}/AGENTS.md +10 -5
- {asp_plot-1.17.0 → asp_plot-1.18.1}/CHANGELOG.md +10 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/PKG-INFO +1 -1
- asp_plot-1.18.1/asp_plot/mapproject.py +248 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/processing_parameters.py +36 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/report.py +47 -12
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/scenes.py +2 -1
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/sensors.py +6 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/stereo.py +2 -1
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/utils.py +28 -8
- {asp_plot-1.17.0 → asp_plot-1.18.1}/pyproject.toml +1 -1
- {asp_plot-1.17.0 → asp_plot-1.18.1}/.flake8 +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/.github/workflows/release.yml +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/.github/workflows/run-tests.yml +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/.gitignore +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/.pre-commit-config.yaml +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/.readthedocs.yaml +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/LICENSE +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/README.md +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/__init__.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/alignment.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/altimetry.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/altimetry_plots.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/altimetry_source.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/asp_log.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/bodies.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/bundle_adjust.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/cli/__init__.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/cli/asp_plot.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/cli/csm_camera_plot.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/cli/gallery.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/cli/request_planetary_altimetry.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/cli/stereo_geom.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/csm_analysis.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/csm_camera.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/csm_io.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/gallery.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/icesat2_source.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/planetary_source.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/report_captions.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/report_pipeline.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/selections.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/stereo_geometry.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/asp_plot/stereopair_metadata_parser.py +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/conda-forge-recipe/meta.yaml +0 -0
- {asp_plot-1.17.0 → asp_plot-1.18.1}/environment.yml +0 -0
|
@@ -4,7 +4,7 @@ This file provides guidance to AI coding agents (Claude Code and others) when wo
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
`asp_plot` (current version: 1.
|
|
7
|
+
`asp_plot` (current version: 1.18.0) is a Python package for visualizing output from the NASA Ames Stereo Pipeline (ASP). It processes stereo satellite imagery results (both terrestrial and planetary), generates diagnostic plots, and creates comprehensive PDF reports similar to those from commercial SfM software like Agisoft Metashape. Requires Python >= 3.11. Published on PyPI and conda-forge.
|
|
8
8
|
|
|
9
9
|
The package supports:
|
|
10
10
|
- Stereo DEM processing visualization
|
|
@@ -110,8 +110,8 @@ The package is organized by functionality, with each module focused on a specifi
|
|
|
110
110
|
- Coordinate utilities: `get_utm_epsg()` for determining UTM EPSG from lon/lat, `get_planetary_bounds()` for DEM bounds in planetocentric 0-360 lon/lat
|
|
111
111
|
- Planetary body detection: `detect_planetary_body(dem_fn)` returns `"earth"`, `"moon"`, or `"mars"` by inspecting CRS WKT DATUM/ELLIPSOID fields
|
|
112
112
|
- Subprocess utilities: `run_subprocess_command()`
|
|
113
|
-
- Vantor/copyright utilities:
|
|
114
|
-
- `detect_vantor_satellite(directory)`:
|
|
113
|
+
- Vantor/copyright utilities (an **attribution** concern — named for the rights-holder — kept deliberately distinct from sensor/reader **identity**, the WorldView-named abstraction in `sensors.py`; #137):
|
|
114
|
+
- `detect_vantor_satellite(directory)`: True when an XML camera file's `SATID` matches `VANTOR_SATID_PREFIXES` — any DigitalGlobe→Maxar→Vantor-owned satellite, i.e. the WorldView family (`WV*`, incl. Legion `WVLG`), GeoEye (`GE*`), QuickBird (`QB*`), IKONOS (`IK*`) — not just WorldView. Gates the `© Vantor` overlay via `Plotter.is_vantor`
|
|
115
115
|
- `add_copyright_overlay(ax)`: Adds "© Vantor {year}" text overlay to bottom-right of matplotlib axes
|
|
116
116
|
- Scene metadata: `get_acquisition_dates(directory, extra_dirs=None)` reads `FIRSTLINETIME` from WorldView/Maxar XMLs and parses the capture timestamp from `AST_L1A_...` file/directory names. Returns a sorted, deduplicated list of `"YYYY-MM-DD HH:MM:SS UTC"` strings; empty if nothing is found. Used by the CLI to populate `ReportMetadata.acquisition_dates`.
|
|
117
117
|
|
|
@@ -129,7 +129,7 @@ The package is organized by functionality, with each module focused on a specifi
|
|
|
129
129
|
- `ReportMetadata`: Dataclass for DEM metadata displayed on the title page (dimensions, GSD, CRS, nodata %, elevation range, DEM filename, reference DEM, acquisition dates). The "Acquisition Date(s)" row is added to the summary table only when `acquisition_dates` is non-empty.
|
|
130
130
|
- `ASPReportPDF`: FPDF subclass with custom header/footer and page numbers
|
|
131
131
|
- `compile_report()`: Assembles title page, Processing Parameters (page 2), figure sections with captions, and any trailing alignment pages into a PDF. Accepts optional `report_command` string to record the CLI invocation. Figures are automatically scaled to fit page dimensions, preventing overflow/cutoff. `sections` is a mixed list of `ReportSection | AlignmentReportPage`; dispatch is by `isinstance` check.
|
|
132
|
-
- `_add_processing_parameters_page()`: helper that renders the runtime summary table plus the bundle_adjust / stereo / point2dem / report commands on page 2 (moved from the trailing page as of v1.13.0).
|
|
132
|
+
- `_add_processing_parameters_page()`: helper that renders the runtime summary table plus the bundle_adjust / stereo / point2dem / report commands on page 2 (moved from the trailing page as of v1.13.0). Also renders the reconstructed `mapproject` command(s) (from the optional `mapproject` list key; see `mapproject.py`) with a "reconstructed from output metadata" note.
|
|
133
133
|
- `_fmt_sig()`: formats a number compactly — 2 decimals for |x| < 10, 1 decimal for 10 ≤ |x| < 100, 0 decimals above, "n/a" for non-finite. Used for alignment stats.
|
|
134
134
|
- Title page displays: processing date, ASP version (from logs), asp_plot version (from package metadata)
|
|
135
135
|
- Page order: title + DEM summary → Processing Parameters → diagnostic figures → (if `--pc_align` ran) alignment report page + aligned-DEM figures.
|
|
@@ -151,9 +151,14 @@ The package is organized by functionality, with each module focused on a specifi
|
|
|
151
151
|
|
|
152
152
|
**`processing_parameters.py`** - `ProcessingParameters` class
|
|
153
153
|
- Delegates all ASP-log parsing to `asp_log.AspLog` (no more inline string surgery; the bare `except:` clauses became directory guards)
|
|
154
|
-
- Extracts command lines, run times, and processing parameters; `from_log_files()` returns a dict including the `asp_version` key
|
|
154
|
+
- Extracts command lines, run times, and processing parameters; `from_log_files()` returns a dict including the `asp_version` key and a `mapproject` key (list of reconstructed mapproject commands; see `mapproject.py`)
|
|
155
155
|
- Used by report generation to document processing settings
|
|
156
156
|
|
|
157
|
+
**`mapproject.py`** - Reconstruct `mapproject` commands from output GeoTIFF metadata (issue #96)
|
|
158
|
+
- ASP's `mapproject` writes **no log file** (unlike `bundle_adjust`/`stereo`/`point2dem`), so `asp_log.py` has nothing to parse for the mapprojection step. Instead of requiring a new ASP `--log` flag, the command is reconstructed **from the output data alone**: ASP stamps `INPUT_IMAGE_FILE` / `CAMERA_FILE` / `DEM_FILE` / `CAMERA_MODEL_TYPE` / `BUNDLE_ADJUST_PREFIX` into each mapprojected GeoTIFF header, and the raster's own CRS / resolution / bounds give `--t_srs` / `--tr` / `--t_projwin`
|
|
159
|
+
- `reconstruct_mapproject_command(raster_path)`: returns the `mapproject ...` string, or `None` if the ASP mapproject tag signature (`REQUIRED_TAGS` = `INPUT_IMAGE_FILE` + `CAMERA_FILE` + `DEM_FILE`, all read back during reconstruction) is absent. Reuses the `utils.Raster` wrapper (free `NotGeoreferencedWarning` suppression + `get_epsg_code()` with the compound-CRS 2D fallback + `get_gsd()`/bounds) rather than re-opening with raw rasterio. `--t_srs` is `EPSG:XXXX` when an EPSG code exists (incl. the 2D fallback), else the quoted PROJ string (custom planetary/local frames, e.g. jitter stereographic); a malformed CRS returns `None` and logs instead of crashing the report. `_format_coord` renders coordinates to 12 significant figures, positionally — clean for large UTM northings *and* full-precision for degree-scale geographic `--tr`, with no scientific notation or float-repr noise. The reconstruction is faithful but **not byte-for-byte re-runnable** (session is the resolved `-t`, an input `--mpp` shows as the resolved `--tr`, output reads the actual filename) — the report flags this with a one-line note
|
|
160
|
+
- `find_mapproject_commands(directories, stereo_command=None)`: scans dirs (processing root, BA dir, stereo dir) for all `*.tif`/`*.tiff` and keeps those carrying the tag signature — **identity is decided by the file's own metadata, never by filename**, so there is no naming-convention dependency (reading a GeoTIFF header is cheap; the `NotGeoreferencedWarning` from raw non-georef inputs is silenced). Dedupes by the reconstructed command string (identical scene reached via two dirs collapses; distinct left/right both show). When `stereo_command` is given, a discovered output is kept only if its filename appears in that command — this scopes the result to the run being reported, so a non-mapprojected run sharing a parent dir with mapprojected scenes (the `stereo/` + `stereo_no_mapproj/` layout) does **not** spuriously list a mapproject step. It's a whole-token basename membership test (the output basename must equal one of the stereo command's argument basenames — not a raw substring, so `run.tif` can't match `prun.tif`), not positional parsing. `ProcessingParameters.get_mapproject_commands(stereo_command)` passes the parsed stereo command; `report.py` renders the results under "Mapproject Command(s)" on the Processing Parameters page (via the module-level `_render_command_block` helper, shared with the bundle_adjust/stereo/point2dem commands)
|
|
161
|
+
|
|
157
162
|
**`sensors.py`** - Sensor-specific scene metadata readers (issue #25)
|
|
158
163
|
- `SensorMetadata` ABC defining the reader interface (`detect` + `get_scene_dicts`) and the sensor-agnostic scene-dict schema, mirroring the `bodies.py` registry pattern so adding ASTER/HiRISE/etc. is a new subclass with no change to the geometry code
|
|
159
164
|
- `WorldViewMetadata(SensorMetadata)`: the WorldView/Maxar XML logic (file discovery, `dg_mosaic` tiling, per-scene extraction, ephemeris/attitude/footprint) moved verbatim out of the parser. Named after the satellite family rather than the (twice-renamed: DigitalGlobe → Maxar → Vantor) company; the same format also covers GeoEye-1/QuickBird/IKONOS
|
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.18.1] - 2026-06-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`© Vantor` attribution now covers all Vantor-owned satellites, not just WorldView** ([#137](https://github.com/uw-cryo/asp_plot/issues/137)). The copyright-overlay check (`detect_vantor_satellite`) matched only `SATID` values starting with `WV`, so GeoEye-1 (`GE01`), QuickBird (`QB02`), and IKONOS scenes — all owned by the same rights-holder (DigitalGlobe → Maxar → Vantor) — were silently left un-attributed. Detection now matches a `VANTOR_SATID_PREFIXES` whitelist (`WV` incl. WorldView Legion `WVLG`, `GE`, `QB`, `IK`). This clarifies that `is_vantor` / `detect_vantor_satellite` are an **attribution** concern (named for the company), intentionally distinct from sensor/reader **identity** (the WorldView-named abstraction in `sensors.py`); the two names are documented as deliberately different so they aren't reconciled into one.
|
|
12
|
+
|
|
13
|
+
## [1.18.0] - 2026-06-25
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **Reconstruct `mapproject` commands in the PDF report** ([#96](https://github.com/uw-cryo/asp_plot/issues/96)). ASP's `mapproject` does not write a log file the way `bundle_adjust` / `stereo` / `point2dem` do, so the processing-parameters page never documented the mapprojection step. Rather than depend on a new ASP `--log` flag, the new `asp_plot/mapproject.py` reconstructs the command **from the output GeoTIFF metadata alone**: ASP stamps `INPUT_IMAGE_FILE` / `CAMERA_FILE` / `DEM_FILE` / `CAMERA_MODEL_TYPE` / `BUNDLE_ADJUST_PREFIX` into each mapprojected output, and combined with the raster's CRS (`--t_srs`), resolution (`--tr`), and bounds (`--t_projwin`) that is enough to rebuild the invocation. `ProcessingParameters.from_log_files()` now adds a `mapproject` key (a list — one command per mapprojected input scene found), and the report's "Processing Parameters" page renders them under a "Mapproject Command(s)" heading with a note that the values are reconstructed (resolved session/grid). Works across ASTER, WorldView/RPC, and CSM (jitter) sessions, including custom projections without an EPSG code (falls back to the PROJ string). The command(s) are rendered in ASP pipeline order (between bundle_adjust and stereo), and scoped to the run being reported via the stereo command — a non-mapprojected run that shares a parent directory with mapprojected scenes (the `stereo/` + `stereo_no_mapproj/` layout) does not spuriously list a mapproject step.
|
|
17
|
+
|
|
8
18
|
## [1.17.0] - 2026-06-24
|
|
9
19
|
|
|
10
20
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asp_plot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.1
|
|
4
4
|
Summary: Package for plotting outputs Ames Stereo Pipeline processing
|
|
5
5
|
Project-URL: Homepage, https://github.com/uw-cryo/asp_plot
|
|
6
6
|
Project-URL: Documentation, https://asp-plot.readthedocs.io
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Reconstruct ``mapproject`` commands from ASP-mapprojected GeoTIFF metadata.
|
|
2
|
+
|
|
3
|
+
ASP's ``mapproject`` tool does not write a log file the way ``bundle_adjust`` /
|
|
4
|
+
``stereo`` / ``point2dem`` do, so :mod:`asp_plot.asp_log` (which parses those
|
|
5
|
+
logs) has nothing to read for the mapprojection step (issue #96).
|
|
6
|
+
|
|
7
|
+
Fortunately ``mapproject`` stamps everything we need into the *output* GeoTIFF
|
|
8
|
+
header, so the command can be reconstructed from the output data alone -- no new
|
|
9
|
+
ASP ``--log`` flag required. The fields written by ASP are:
|
|
10
|
+
|
|
11
|
+
- ``INPUT_IMAGE_FILE`` -- the image that was mapprojected
|
|
12
|
+
- ``CAMERA_FILE`` -- the camera model
|
|
13
|
+
- ``DEM_FILE`` -- the DEM used as the projection surface
|
|
14
|
+
- ``CAMERA_MODEL_TYPE`` -- the resolved ``--session-type`` / ``-t``
|
|
15
|
+
- ``BUNDLE_ADJUST_PREFIX`` -- the ``--bundle-adjust-prefix`` (``"NONE"`` if unset)
|
|
16
|
+
|
|
17
|
+
combined with the raster's own CRS (``--t_srs``), resolution (``--tr``), and
|
|
18
|
+
bounds (``--t_projwin``).
|
|
19
|
+
|
|
20
|
+
The reconstruction is faithful but *not* byte-for-byte re-runnable: the session
|
|
21
|
+
type is the resolved value (not necessarily what the user typed), an input
|
|
22
|
+
``--mpp`` shows up resolved as ``--tr``, and the output name is read from the
|
|
23
|
+
file itself. Callers that surface this to users (e.g. the PDF report) should say
|
|
24
|
+
so. See ``reconstruct_mapproject_command`` for the exact argv order.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import glob
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
import rasterio
|
|
33
|
+
|
|
34
|
+
from asp_plot.utils import Raster
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
# GeoTIFF metadata tags ASP's mapproject writes into every output. This trio is
|
|
39
|
+
# the signature we require to treat a raster as a mapproject output -- and the
|
|
40
|
+
# *only* thing we rely on to identify mapprojected files. We deliberately do not
|
|
41
|
+
# match on filename conventions (``*_map.tif`` etc.): the tags are written by ASP
|
|
42
|
+
# itself, are self-validating, and survive a rename, whereas a filename glob
|
|
43
|
+
# would force an external naming convention and miss anything that deviates. All
|
|
44
|
+
# three are read back during reconstruction, so requiring them here also guards
|
|
45
|
+
# the ``tags[...]`` lookups below.
|
|
46
|
+
REQUIRED_TAGS = ("INPUT_IMAGE_FILE", "CAMERA_FILE", "DEM_FILE")
|
|
47
|
+
|
|
48
|
+
# Raster extensions scanned when discovering mapproject outputs in a directory.
|
|
49
|
+
RASTER_EXTENSIONS = ("*.tif", "*.tiff")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _format_coord(value, sig_figs=12):
|
|
53
|
+
"""Format a projwin/resolution coordinate for the CLI.
|
|
54
|
+
|
|
55
|
+
Renders to ``sig_figs`` significant figures, positionally (never scientific
|
|
56
|
+
notation), trimming trailing zeros. Significant figures -- not fixed decimal
|
|
57
|
+
places -- because the two ends of the range matter: GeoTIFF bounds can be
|
|
58
|
+
large UTM northings (~5.2e6) while a geographic-CRS ``--tr`` can be ~5e-6
|
|
59
|
+
degrees. A fixed ``%.6f`` would coarsen the latter to ~1 significant figure;
|
|
60
|
+
plain shortest-repr would surface float noise on the former
|
|
61
|
+
(``3722294.979`` computed as ``3722294.9790000003``). Twelve significant
|
|
62
|
+
figures is well clear of double-precision noise (~16 figures) yet keeps
|
|
63
|
+
clean grid values clean (``0.481`` -> ``0.481``, ``15.0`` -> ``15``).
|
|
64
|
+
"""
|
|
65
|
+
if not np.isfinite(value) or value == 0:
|
|
66
|
+
return "0" if value == 0 else repr(value)
|
|
67
|
+
decimals = sig_figs - 1 - int(np.floor(np.log10(abs(value))))
|
|
68
|
+
return np.format_float_positional(round(value, decimals), trim="-")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def reconstruct_mapproject_command(raster_path):
|
|
72
|
+
"""Reconstruct the ``mapproject`` command for an ASP-mapprojected GeoTIFF.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
raster_path : str
|
|
77
|
+
Path to a GeoTIFF that may be an ASP ``mapproject`` output.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
str or None
|
|
82
|
+
The reconstructed ``mapproject ...`` command line, or ``None`` if the
|
|
83
|
+
file is missing the ASP mapproject metadata signature (i.e. it is not a
|
|
84
|
+
mapproject output, or was written by a tool that did not stamp the
|
|
85
|
+
header).
|
|
86
|
+
|
|
87
|
+
Notes
|
|
88
|
+
-----
|
|
89
|
+
The reconstructed argv order mirrors the ASP CLI::
|
|
90
|
+
|
|
91
|
+
mapproject [-t SESSION] [--t_srs SRS] --tr TR \\
|
|
92
|
+
--t_projwin XMIN YMIN XMAX YMAX \\
|
|
93
|
+
[--bundle-adjust-prefix PREFIX] \\
|
|
94
|
+
DEM_FILE INPUT_IMAGE CAMERA OUTPUT
|
|
95
|
+
|
|
96
|
+
``--t_srs`` is emitted as ``EPSG:XXXX`` when the CRS has an exact EPSG code,
|
|
97
|
+
otherwise as the PROJ string (quoted), so custom planetary/local projections
|
|
98
|
+
(e.g. the stereographic frames used in jitter solving) still round-trip.
|
|
99
|
+
"""
|
|
100
|
+
# Reuse the package Raster wrapper: it suppresses the NotGeoreferencedWarning
|
|
101
|
+
# that the raw (non-georef) input scenes raise during discovery, and it owns
|
|
102
|
+
# the EPSG/GSD/bounds derivation (incl. the compound-CRS EPSG fallback) that
|
|
103
|
+
# _t_srs_token relies on.
|
|
104
|
+
try:
|
|
105
|
+
raster = Raster(raster_path)
|
|
106
|
+
except rasterio.errors.RasterioIOError as e:
|
|
107
|
+
logger.warning("Could not read %s for mapproject metadata: %s", raster_path, e)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
with raster.ds:
|
|
111
|
+
tags = raster.ds.tags()
|
|
112
|
+
if not all(tags.get(k) for k in REQUIRED_TAGS):
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
parts = ["mapproject"]
|
|
116
|
+
|
|
117
|
+
session = tags.get("CAMERA_MODEL_TYPE")
|
|
118
|
+
if session:
|
|
119
|
+
parts += ["-t", session]
|
|
120
|
+
|
|
121
|
+
t_srs = _t_srs_token(raster)
|
|
122
|
+
if t_srs:
|
|
123
|
+
parts += ["--t_srs", t_srs]
|
|
124
|
+
|
|
125
|
+
parts += ["--tr", _format_coord(raster.get_gsd())]
|
|
126
|
+
bounds = raster.ds.bounds
|
|
127
|
+
parts += [
|
|
128
|
+
"--t_projwin",
|
|
129
|
+
_format_coord(bounds.left),
|
|
130
|
+
_format_coord(bounds.bottom),
|
|
131
|
+
_format_coord(bounds.right),
|
|
132
|
+
_format_coord(bounds.top),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
ba_prefix = tags.get("BUNDLE_ADJUST_PREFIX")
|
|
136
|
+
if ba_prefix and ba_prefix != "NONE":
|
|
137
|
+
parts += ["--bundle-adjust-prefix", ba_prefix]
|
|
138
|
+
|
|
139
|
+
parts += [
|
|
140
|
+
tags["DEM_FILE"],
|
|
141
|
+
tags["INPUT_IMAGE_FILE"],
|
|
142
|
+
tags["CAMERA_FILE"],
|
|
143
|
+
os.path.basename(raster_path),
|
|
144
|
+
]
|
|
145
|
+
return " ".join(parts)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _t_srs_token(raster):
|
|
149
|
+
"""Return the ``--t_srs`` token for a :class:`~asp_plot.utils.Raster`.
|
|
150
|
+
|
|
151
|
+
Prefers a compact ``EPSG:XXXX`` via ``Raster.get_epsg_code()`` (which already
|
|
152
|
+
falls back to the horizontal 2D component for compound / 3D-promoted CRSs);
|
|
153
|
+
failing that, emits the PROJ string (quoted, since it contains spaces) so
|
|
154
|
+
custom projections without an EPSG code -- e.g. the stereographic frames used
|
|
155
|
+
in jitter solving -- still round-trip. A missing or malformed CRS returns
|
|
156
|
+
``None`` (logged) rather than raising, so one odd raster cannot abort report
|
|
157
|
+
generation.
|
|
158
|
+
"""
|
|
159
|
+
crs = raster.ds.crs
|
|
160
|
+
if crs is None:
|
|
161
|
+
return None
|
|
162
|
+
try:
|
|
163
|
+
epsg = raster.get_epsg_code()
|
|
164
|
+
if epsg:
|
|
165
|
+
return f"EPSG:{epsg}"
|
|
166
|
+
proj4 = crs.to_proj4()
|
|
167
|
+
if proj4:
|
|
168
|
+
return f'"{proj4}"'
|
|
169
|
+
except Exception as e: # malformed/exotic CRS -- don't crash the report
|
|
170
|
+
logger.warning("Could not derive --t_srs from CRS: %s", e)
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def find_mapproject_commands(directories, stereo_command=None):
|
|
175
|
+
"""Find ASP-mapprojected outputs across ``directories`` and reconstruct them.
|
|
176
|
+
|
|
177
|
+
Scans each directory (non-recursively) for GeoTIFFs and keeps those carrying
|
|
178
|
+
the ASP mapproject metadata signature (see ``REQUIRED_TAGS``) -- identity is
|
|
179
|
+
decided entirely by the file's own metadata, not by its name, so the result
|
|
180
|
+
is robust to ASP/asp_plot filename conventions. Each kept output is
|
|
181
|
+
reconstructed into a command; results are deduplicated (the same scene can be
|
|
182
|
+
reached via more than one of the passed directories) and returned sorted for
|
|
183
|
+
stable report output.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
directories : iterable of str or None
|
|
188
|
+
Directories to scan (e.g. the processing root, stereo dir, BA dir).
|
|
189
|
+
``None`` entries are skipped.
|
|
190
|
+
stereo_command : str, optional
|
|
191
|
+
The stereo command line for the run being reported. When given, a
|
|
192
|
+
mapprojected output is kept only if its filename appears among the
|
|
193
|
+
stereo inputs (i.e. this run actually consumed it). This scopes the
|
|
194
|
+
result to the run at hand: a non-mapprojected stereo run that shares a
|
|
195
|
+
directory with leftover mapprojected scenes (e.g. ``stereo/`` and
|
|
196
|
+
``stereo_no_mapproj/`` under one parent) no longer picks them up. A
|
|
197
|
+
mapprojected run's ``stereo`` command lists the ``*_map.tif`` inputs; a
|
|
198
|
+
non-mapprojected run lists the raw images instead, so the gate is a
|
|
199
|
+
plain filename-membership test -- no fragile positional parsing. When
|
|
200
|
+
``None`` (or empty), no gating is applied and every discovered output is
|
|
201
|
+
returned.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
list of str
|
|
206
|
+
Reconstructed ``mapproject`` command lines, sorted; empty if none found.
|
|
207
|
+
"""
|
|
208
|
+
# Scope to what this stereo run actually used: a mapprojected output is an
|
|
209
|
+
# input to mapprojected stereo, so its basename matches one of the stereo
|
|
210
|
+
# command's argument basenames. Compare against the set of whole-token
|
|
211
|
+
# basenames (not a raw substring of the command) so a short output name like
|
|
212
|
+
# "run.tif" can't spuriously match "prun.tif" or a path fragment.
|
|
213
|
+
stereo_input_basenames = (
|
|
214
|
+
{os.path.basename(token) for token in stereo_command.split()}
|
|
215
|
+
if stereo_command
|
|
216
|
+
else None
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
seen = set()
|
|
220
|
+
commands = []
|
|
221
|
+
scanned = set()
|
|
222
|
+
for directory in directories:
|
|
223
|
+
if not directory:
|
|
224
|
+
continue
|
|
225
|
+
directory = os.path.expanduser(directory)
|
|
226
|
+
if not os.path.isdir(directory):
|
|
227
|
+
continue
|
|
228
|
+
candidates = set()
|
|
229
|
+
for ext in RASTER_EXTENSIONS:
|
|
230
|
+
candidates.update(glob.glob(os.path.join(directory, ext)))
|
|
231
|
+
for path in sorted(candidates):
|
|
232
|
+
real = os.path.realpath(path)
|
|
233
|
+
if real in scanned:
|
|
234
|
+
continue
|
|
235
|
+
scanned.add(real)
|
|
236
|
+
if (
|
|
237
|
+
stereo_input_basenames is not None
|
|
238
|
+
and os.path.basename(path) not in stereo_input_basenames
|
|
239
|
+
):
|
|
240
|
+
continue
|
|
241
|
+
command = reconstruct_mapproject_command(path)
|
|
242
|
+
# Dedupe on the reconstructed command: distinct left/right scenes
|
|
243
|
+
# differ (different input image + output name), but the same scene
|
|
244
|
+
# copied into two scanned directories yields an identical command.
|
|
245
|
+
if command and command not in seen:
|
|
246
|
+
seen.add(command)
|
|
247
|
+
commands.append(command)
|
|
248
|
+
return sorted(commands)
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
5
|
from asp_plot.asp_log import STEREO_STEP_ORDER, AspLog
|
|
6
|
+
from asp_plot.mapproject import find_mapproject_commands
|
|
6
7
|
from asp_plot.utils import glob_file
|
|
7
8
|
|
|
8
9
|
logging.basicConfig(level=logging.WARNING)
|
|
@@ -213,10 +214,45 @@ class ProcessingParameters:
|
|
|
213
214
|
"stereo_run_time": stereo_run_time,
|
|
214
215
|
"point2dem": point2dem_params,
|
|
215
216
|
"point2dem_run_time": point2dem_run_time,
|
|
217
|
+
"mapproject": self.get_mapproject_commands(stereo_params),
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
return self.processing_parameters_dict
|
|
219
221
|
|
|
222
|
+
def get_mapproject_commands(self, stereo_command=None):
|
|
223
|
+
"""Reconstruct ``mapproject`` commands from mapprojected scene metadata.
|
|
224
|
+
|
|
225
|
+
ASP's ``mapproject`` does not write a log file, but it stamps the
|
|
226
|
+
reconstruction inputs into each output GeoTIFF header
|
|
227
|
+
(issue #96). The mapprojected scenes live alongside the raw inputs in
|
|
228
|
+
the processing root and/or the bundle-adjust directory, so we scan those
|
|
229
|
+
plus the stereo directory.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
stereo_command : str, optional
|
|
234
|
+
The stereo command line for this run. When supplied, only
|
|
235
|
+
mapprojected outputs that the stereo run actually consumed (their
|
|
236
|
+
filename appears in the command) are reported -- so a
|
|
237
|
+
non-mapprojected run that shares a directory with mapprojected
|
|
238
|
+
scenes does not spuriously list a mapproject step.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
list of str
|
|
243
|
+
Reconstructed ``mapproject`` command lines (one per mapprojected
|
|
244
|
+
input image found), sorted; empty if the run was not mapprojected or
|
|
245
|
+
the outputs carry no ASP metadata.
|
|
246
|
+
"""
|
|
247
|
+
return find_mapproject_commands(
|
|
248
|
+
[
|
|
249
|
+
self.processing_directory,
|
|
250
|
+
self.full_ba_directory,
|
|
251
|
+
self.full_stereo_directory,
|
|
252
|
+
],
|
|
253
|
+
stereo_command=stereo_command,
|
|
254
|
+
)
|
|
255
|
+
|
|
220
256
|
def from_bundle_adjust_log(self):
|
|
221
257
|
"""
|
|
222
258
|
Extract parameters from the bundle adjustment log file.
|
|
@@ -190,6 +190,11 @@ def compile_report(
|
|
|
190
190
|
- stereo_run_time: Time to run stereo
|
|
191
191
|
- point2dem: Point2dem command
|
|
192
192
|
- point2dem_run_time: Time to run point2dem
|
|
193
|
+
|
|
194
|
+
Optional keys:
|
|
195
|
+
- mapproject: list of reconstructed mapproject command strings (one per
|
|
196
|
+
mapprojected input scene); rendered on the Processing Parameters page
|
|
197
|
+
when present and non-empty.
|
|
193
198
|
"""
|
|
194
199
|
pdf = ASPReportPDF(report_title=report_title)
|
|
195
200
|
pdf.alias_nb_pages()
|
|
@@ -432,6 +437,16 @@ def _add_alignment_stats_row_table(pdf, stats_row):
|
|
|
432
437
|
pdf.ln(6)
|
|
433
438
|
|
|
434
439
|
|
|
440
|
+
def _render_command_block(pdf, label, cmd):
|
|
441
|
+
"""Render a single bold-labelled, monospace-wrapped command on the PDF."""
|
|
442
|
+
pdf.set_font("Helvetica", "B", 10)
|
|
443
|
+
pdf.cell(0, 7, f"{label} Command:", new_x="LMARGIN", new_y="NEXT")
|
|
444
|
+
pdf.set_font("Courier", "", 7)
|
|
445
|
+
wrapped = textwrap.fill(cmd, width=120)
|
|
446
|
+
pdf.multi_cell(0, 4, wrapped, new_x="LMARGIN", new_y="NEXT")
|
|
447
|
+
pdf.ln(4)
|
|
448
|
+
|
|
449
|
+
|
|
435
450
|
def _add_processing_parameters_page(pdf, params, report_command):
|
|
436
451
|
"""Add the Processing Parameters page (runtime table + commands).
|
|
437
452
|
|
|
@@ -460,19 +475,39 @@ def _add_processing_parameters_page(pdf, params, report_command):
|
|
|
460
475
|
pdf.multi_cell(0, 4, ref_dem)
|
|
461
476
|
pdf.ln(4)
|
|
462
477
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
478
|
+
# Render commands in ASP pipeline order: bundle_adjust -> mapproject ->
|
|
479
|
+
# stereo -> point2dem. mapproject is reconstructed from output metadata
|
|
480
|
+
# (it writes no log), so it carries an explanatory note and may be a list.
|
|
481
|
+
if params.get("bundle_adjust"):
|
|
482
|
+
_render_command_block(pdf, "Bundle Adjust", params["bundle_adjust"])
|
|
483
|
+
|
|
484
|
+
mapproject_cmds = params.get("mapproject") or []
|
|
485
|
+
if mapproject_cmds:
|
|
486
|
+
label = (
|
|
487
|
+
"Mapproject Command" if len(mapproject_cmds) == 1 else "Mapproject Commands"
|
|
488
|
+
)
|
|
489
|
+
pdf.set_font("Helvetica", "B", 10)
|
|
490
|
+
pdf.cell(0, 7, f"{label}:", new_x="LMARGIN", new_y="NEXT")
|
|
491
|
+
pdf.set_font("Helvetica", "I", 7)
|
|
492
|
+
pdf.multi_cell(
|
|
493
|
+
0,
|
|
494
|
+
4,
|
|
495
|
+
"Reconstructed from output GeoTIFF metadata (mapproject writes no "
|
|
496
|
+
"log file); session type and grid size are the resolved values.",
|
|
497
|
+
new_x="LMARGIN",
|
|
498
|
+
new_y="NEXT",
|
|
499
|
+
)
|
|
500
|
+
pdf.set_font("Courier", "", 7)
|
|
501
|
+
for cmd in mapproject_cmds:
|
|
473
502
|
wrapped = textwrap.fill(cmd, width=120)
|
|
474
|
-
pdf.multi_cell(0, 4, wrapped)
|
|
475
|
-
pdf.ln(
|
|
503
|
+
pdf.multi_cell(0, 4, wrapped, new_x="LMARGIN", new_y="NEXT")
|
|
504
|
+
pdf.ln(1)
|
|
505
|
+
pdf.ln(3)
|
|
506
|
+
|
|
507
|
+
if params.get("stereo"):
|
|
508
|
+
_render_command_block(pdf, "Stereo", params["stereo"])
|
|
509
|
+
if params.get("point2dem"):
|
|
510
|
+
_render_command_block(pdf, "point2dem", params["point2dem"])
|
|
476
511
|
|
|
477
512
|
if report_command:
|
|
478
513
|
pdf.set_font("Helvetica", "B", 10)
|
|
@@ -26,7 +26,8 @@ class SceneFiles:
|
|
|
26
26
|
full_stereo_directory : str
|
|
27
27
|
Full path to the stereo directory.
|
|
28
28
|
is_vantor : bool
|
|
29
|
-
Whether the source imagery is from a Vantor (WorldView
|
|
29
|
+
Whether the source imagery is from a Vantor-owned satellite (WorldView
|
|
30
|
+
family, GeoEye, QuickBird, etc.); gates the "© Vantor" copyright overlay.
|
|
30
31
|
left_scene_sub_fn, right_scene_sub_fn : str or None
|
|
31
32
|
Paths to the left/right sub-sampled scene files.
|
|
32
33
|
"""
|
|
@@ -106,6 +106,12 @@ class WorldViewMetadata(SensorMetadata):
|
|
|
106
106
|
extract per-scene metadata, handling both single XML files and multiple XML
|
|
107
107
|
tiles per scene (mosaicked with ``dg_mosaic``).
|
|
108
108
|
|
|
109
|
+
This class is named for the *sensor family* (the stable WorldView name) and
|
|
110
|
+
governs which reader parses the XML. It is intentionally distinct from the
|
|
111
|
+
*attribution* check :func:`asp_plot.utils.detect_vantor_satellite`, which is
|
|
112
|
+
named for the rights-holder (Vantor) and decides whether the "© Vantor"
|
|
113
|
+
overlay applies. The two concerns use different names on purpose; see #137.
|
|
114
|
+
|
|
109
115
|
Attributes
|
|
110
116
|
----------
|
|
111
117
|
directory : str
|
|
@@ -39,7 +39,8 @@ class StereoFiles:
|
|
|
39
39
|
full_directory : str
|
|
40
40
|
Full path to the stereo directory.
|
|
41
41
|
is_vantor : bool
|
|
42
|
-
Whether the source imagery is from a Vantor (WorldView
|
|
42
|
+
Whether the source imagery is from a Vantor-owned satellite (WorldView
|
|
43
|
+
family, GeoEye, QuickBird, etc.); gates the "© Vantor" copyright overlay.
|
|
43
44
|
reference_dem : str or None
|
|
44
45
|
Path to the reference DEM (supplied or recovered from the stereo log).
|
|
45
46
|
left_image_fn, left_image_sub_fn, right_image_sub_fn : str or None
|
|
@@ -1101,9 +1101,10 @@ class Plotter:
|
|
|
1101
1101
|
title : str, optional
|
|
1102
1102
|
Plot title, default is None
|
|
1103
1103
|
is_vantor : bool, optional
|
|
1104
|
-
Whether the source imagery is from a Vantor (
|
|
1105
|
-
When True, ``plot_array``
|
|
1106
|
-
|
|
1104
|
+
Whether the source imagery is from a Vantor-owned satellite (the
|
|
1105
|
+
WorldView family, GeoEye, QuickBird, etc.). When True, ``plot_array``
|
|
1106
|
+
adds a "© Vantor" copyright overlay for panels that request it via
|
|
1107
|
+
``copyright=True``. Default is False.
|
|
1107
1108
|
"""
|
|
1108
1109
|
self.clim_perc = clim_perc
|
|
1109
1110
|
self.lognorm = lognorm
|
|
@@ -1196,8 +1197,8 @@ class Plotter:
|
|
|
1196
1197
|
Whether to add a satellite-imagery copyright overlay, default is
|
|
1197
1198
|
False. The overlay is only drawn when this is True *and* the Plotter
|
|
1198
1199
|
was created with ``is_vantor=True`` (i.e. the source imagery is from
|
|
1199
|
-
a Vantor
|
|
1200
|
-
|
|
1200
|
+
a Vantor-owned satellite). Set on the panels that display the optical
|
|
1201
|
+
scenes.
|
|
1201
1202
|
|
|
1202
1203
|
Returns
|
|
1203
1204
|
-------
|
|
@@ -1297,8 +1298,26 @@ class Plotter:
|
|
|
1297
1298
|
ctx.add_basemap(ax=ax, **ctx_kwargs)
|
|
1298
1299
|
|
|
1299
1300
|
|
|
1301
|
+
# SATID prefixes for the DigitalGlobe -> Maxar -> Vantor satellite heritage, all
|
|
1302
|
+
# of which carry "© Vantor" attribution. Covers the WorldView family ("WV", incl.
|
|
1303
|
+
# WorldView Legion "WVLG"), GeoEye ("GE"), QuickBird ("QB"), and IKONOS ("IK").
|
|
1304
|
+
VANTOR_SATID_PREFIXES = ("WV", "GE", "QB", "IK")
|
|
1305
|
+
|
|
1306
|
+
|
|
1300
1307
|
def detect_vantor_satellite(directory):
|
|
1301
|
-
"""Check if XML files in directory indicate a Vantor
|
|
1308
|
+
"""Check if XML files in directory indicate a Vantor-owned satellite.
|
|
1309
|
+
|
|
1310
|
+
This is an *attribution* check, distinct from sensor/reader identity: it
|
|
1311
|
+
decides whether the "© Vantor" copyright overlay applies (see
|
|
1312
|
+
:func:`add_copyright_overlay` and :class:`Plotter`). It is named for the
|
|
1313
|
+
rights-holder (Vantor, formerly Maxar/DigitalGlobe) and matches *any* Vantor
|
|
1314
|
+
satellite, not just WorldView. Sensor *identity* — which reader parses the
|
|
1315
|
+
XML — is a separate concern handled by the WorldView-named abstraction in
|
|
1316
|
+
:mod:`asp_plot.sensors`; the two names intentionally differ and should not be
|
|
1317
|
+
"reconciled" into one.
|
|
1318
|
+
|
|
1319
|
+
A scene is considered Vantor-owned when an XML camera file carries a SATID
|
|
1320
|
+
matching one of :data:`VANTOR_SATID_PREFIXES`.
|
|
1302
1321
|
|
|
1303
1322
|
Parameters
|
|
1304
1323
|
----------
|
|
@@ -1308,7 +1327,8 @@ def detect_vantor_satellite(directory):
|
|
|
1308
1327
|
Returns
|
|
1309
1328
|
-------
|
|
1310
1329
|
bool
|
|
1311
|
-
True if any XML file contains a
|
|
1330
|
+
True if any XML file contains a Vantor-heritage SATID (e.g. WV01-WV04,
|
|
1331
|
+
WVLG, GE01, QB02).
|
|
1312
1332
|
"""
|
|
1313
1333
|
try:
|
|
1314
1334
|
xml_files = glob_file(directory, "*.[Xx][Mm][Ll]", all_files=True)
|
|
@@ -1320,7 +1340,7 @@ def detect_vantor_satellite(directory):
|
|
|
1320
1340
|
for xml_file in xml_files:
|
|
1321
1341
|
try:
|
|
1322
1342
|
satid = get_xml_tag(xml_file, "SATID")
|
|
1323
|
-
if satid.startswith(
|
|
1343
|
+
if satid.startswith(VANTOR_SATID_PREFIXES):
|
|
1324
1344
|
return True
|
|
1325
1345
|
except (ValueError, Exception):
|
|
1326
1346
|
continue
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|