asp-plot 1.17.0__tar.gz → 1.18.0__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.
Files changed (46) hide show
  1. {asp_plot-1.17.0 → asp_plot-1.18.0}/AGENTS.md +8 -3
  2. {asp_plot-1.17.0 → asp_plot-1.18.0}/CHANGELOG.md +5 -0
  3. {asp_plot-1.17.0 → asp_plot-1.18.0}/PKG-INFO +1 -1
  4. asp_plot-1.18.0/asp_plot/mapproject.py +248 -0
  5. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/processing_parameters.py +36 -0
  6. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/report.py +47 -12
  7. {asp_plot-1.17.0 → asp_plot-1.18.0}/pyproject.toml +1 -1
  8. {asp_plot-1.17.0 → asp_plot-1.18.0}/.flake8 +0 -0
  9. {asp_plot-1.17.0 → asp_plot-1.18.0}/.github/workflows/release.yml +0 -0
  10. {asp_plot-1.17.0 → asp_plot-1.18.0}/.github/workflows/run-tests.yml +0 -0
  11. {asp_plot-1.17.0 → asp_plot-1.18.0}/.gitignore +0 -0
  12. {asp_plot-1.17.0 → asp_plot-1.18.0}/.pre-commit-config.yaml +0 -0
  13. {asp_plot-1.17.0 → asp_plot-1.18.0}/.readthedocs.yaml +0 -0
  14. {asp_plot-1.17.0 → asp_plot-1.18.0}/LICENSE +0 -0
  15. {asp_plot-1.17.0 → asp_plot-1.18.0}/README.md +0 -0
  16. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/__init__.py +0 -0
  17. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/alignment.py +0 -0
  18. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/altimetry.py +0 -0
  19. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/altimetry_plots.py +0 -0
  20. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/altimetry_source.py +0 -0
  21. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/asp_log.py +0 -0
  22. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/bodies.py +0 -0
  23. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/bundle_adjust.py +0 -0
  24. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/cli/__init__.py +0 -0
  25. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/cli/asp_plot.py +0 -0
  26. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/cli/csm_camera_plot.py +0 -0
  27. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/cli/gallery.py +0 -0
  28. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/cli/request_planetary_altimetry.py +0 -0
  29. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/cli/stereo_geom.py +0 -0
  30. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/csm_analysis.py +0 -0
  31. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/csm_camera.py +0 -0
  32. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/csm_io.py +0 -0
  33. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/gallery.py +0 -0
  34. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/icesat2_source.py +0 -0
  35. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/planetary_source.py +0 -0
  36. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/report_captions.py +0 -0
  37. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/report_pipeline.py +0 -0
  38. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/scenes.py +0 -0
  39. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/selections.py +0 -0
  40. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/sensors.py +0 -0
  41. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/stereo.py +0 -0
  42. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/stereo_geometry.py +0 -0
  43. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/stereopair_metadata_parser.py +0 -0
  44. {asp_plot-1.17.0 → asp_plot-1.18.0}/asp_plot/utils.py +0 -0
  45. {asp_plot-1.17.0 → asp_plot-1.18.0}/conda-forge-recipe/meta.yaml +0 -0
  46. {asp_plot-1.17.0 → asp_plot-1.18.0}/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.17.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.
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
@@ -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,11 @@ 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.0] - 2026-06-25
9
+
10
+ ### Added
11
+ - **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.
12
+
8
13
  ## [1.17.0] - 2026-06-24
9
14
 
10
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asp_plot
3
- Version: 1.17.0
3
+ Version: 1.18.0
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
- for key, label in [
464
- ("bundle_adjust", "Bundle Adjust"),
465
- ("stereo", "Stereo"),
466
- ("point2dem", "point2dem"),
467
- ]:
468
- cmd = params.get(key, "")
469
- if cmd:
470
- pdf.set_font("Helvetica", "B", 10)
471
- pdf.cell(0, 7, f"{label} Command:", new_x="LMARGIN", new_y="NEXT")
472
- pdf.set_font("Courier", "", 7)
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(4)
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)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "asp_plot"
7
- version = "1.17.0"
7
+ version = "1.18.0"
8
8
  license = {text = "BSD-3-Clause"}
9
9
  authors = [
10
10
  { name="Ben Purinton", email="purinton@uw.edu" },
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