asp-plot 3.4.0__tar.gz → 3.5.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.
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.gitignore +3 -1
- {asp_plot-3.4.0 → asp_plot-3.5.0}/AGENTS.md +30 -12
- {asp_plot-3.4.0 → asp_plot-3.5.0}/ARCHITECTURE.md +2 -1
- {asp_plot-3.4.0 → asp_plot-3.5.0}/CHANGELOG.md +22 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/PKG-INFO +1 -1
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/cli/asp_report.py +8 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/dem_benchmark.py +31 -6
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/report_pipeline.py +31 -1
- {asp_plot-3.4.0 → asp_plot-3.5.0}/pyproject.toml +1 -1
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.flake8 +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.github/ISSUE_TEMPLATE/problem-report.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.github/workflows/claude-code-review.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.github/workflows/claude.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.github/workflows/release.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.github/workflows/run-tests.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.pre-commit-config.yaml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/.readthedocs.yaml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/CLAUDE.md +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/LICENSE +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/README.md +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/__init__.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/alignment.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/altimetry.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/altimetry_plots.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/altimetry_source.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/asp_log.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/bodies.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/bundle_adjust.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/cli/__init__.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/cli/bundle_adjust_cameras.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/cli/csm_camera_plot.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/cli/dem_benchmark.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/cli/gallery.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/cli/request_planetary_altimetry.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/cli/stereo_geom.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/csm_analysis.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/csm_camera.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/csm_io.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/gallery.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/icesat2_source.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/mapproject.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/planetary_source.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/processing_parameters.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/report.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/report_captions.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/scenes.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/selections.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/sensors/__init__.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/sensors/aster.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/sensors/base.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/sensors/dimap.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/sensors/dimap_v1.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/sensors/rpc.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/sensors/worldview.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/stereo.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/stereo_geometry.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/stereopair_metadata_parser.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/asp_plot/utils.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/conda-forge-recipe/meta.yaml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.5.0}/environment.yml +0 -0
|
@@ -140,7 +140,9 @@ notebooks/**/*.parquet
|
|
|
140
140
|
notebooks/**/*.csv
|
|
141
141
|
CLAUDE/
|
|
142
142
|
.claude/**
|
|
143
|
-
scripts
|
|
143
|
+
# Study driver scripts live in a separate repo; scripts is often a symlink to it,
|
|
144
|
+
# which a trailing-slash pattern (directories only) does not match.
|
|
145
|
+
/scripts
|
|
144
146
|
/*.parquet
|
|
145
147
|
reports/regenerate_reports.sh
|
|
146
148
|
# Figure-selections sidecars written next to each report by `asp_report`. These are
|
|
@@ -52,15 +52,35 @@ To publish a regenerated set:
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
bash reports/regenerate_reports.sh # gitignored; needs ~/Desktop/asp-plot-examples
|
|
55
|
+
|
|
56
|
+
# Switch the Zenodo webhook off for the publish (needs repo admin; see below)
|
|
57
|
+
HOOK=$(gh api repos/uw-cryo/asp_plot/hooks \
|
|
58
|
+
--jq '.[] | select(.config.url | contains("zenodo.org")) | .id')
|
|
59
|
+
gh api -X PATCH repos/uw-cryo/asp_plot/hooks/$HOOK -F active=false --jq .active
|
|
60
|
+
|
|
55
61
|
gh release create reports-$(date +%F) \
|
|
56
62
|
reports/*.pdf reports/*_figure_selections.yml reports/regenerate_reports.sh \
|
|
57
63
|
--target main --latest=false \
|
|
58
64
|
--title "Example reports, $(date +%F)" --notes "What changed and why."
|
|
65
|
+
|
|
66
|
+
# Confirm nothing was delivered for the new release, then switch it back on
|
|
67
|
+
gh api "repos/uw-cryo/asp_plot/hooks/$HOOK/deliveries?per_page=3" \
|
|
68
|
+
--jq '.[] | "\(.delivered_at) \(.event) \(.action)"'
|
|
69
|
+
gh api -X PATCH repos/uw-cryo/asp_plot/hooks/$HOOK -F active=true --jq .active
|
|
59
70
|
# then bump REPORTS_RELEASE in docs/fetch_example_reports.sh and commit that one line
|
|
60
71
|
```
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
Five things matter here:
|
|
74
|
+
|
|
75
|
+
- **Keep the release off Zenodo.** The repository has a Zenodo webhook on
|
|
76
|
+
`release` events, and Zenodo archives every published release as a version of
|
|
77
|
+
the software: it cannot tell a reports release from a version release. The
|
|
78
|
+
`reports-2026-09-18` release was archived that way and, being the newest
|
|
79
|
+
record, became what the DOI badge in the README resolves to. Switch the
|
|
80
|
+
webhook off before publishing and back on afterwards, as above; GitHub does
|
|
81
|
+
not re-send the events it skipped. Always pass `--jq` when reading the hook,
|
|
82
|
+
because its URL embeds an access token that must not end up in a log or a
|
|
83
|
+
comment. A published Zenodo record can only be removed by Zenodo support.
|
|
64
84
|
- **Publish the release before pushing the commit** that points at it. The docs
|
|
65
85
|
build uses `curl -f`, so a tag that does not exist yet fails the build.
|
|
66
86
|
- **`--latest=false`**, or the reports release takes the "Latest" badge from the
|
|
@@ -69,13 +89,11 @@ Four things matter here:
|
|
|
69
89
|
hardcode absolute local paths) but they are what makes regeneration
|
|
70
90
|
figure-stable and offline (issue #121), so keeping them with the PDFs they
|
|
71
91
|
produced makes a release self-describing.
|
|
72
|
-
- **Attach `regenerate_reports.sh` as well, for the same reason
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
produced them can be reproduced by someone who has the example datasets and
|
|
78
|
-
nothing else.
|
|
92
|
+
- **Attach `regenerate_reports.sh` as well, for the same reason.** It is
|
|
93
|
+
gitignored too, so no clone of this repository contains it. Attaching it keeps
|
|
94
|
+
a copy with each set of reports, and a release that carries the reports, the
|
|
95
|
+
selections that shaped them, and the commands that produced them can be
|
|
96
|
+
reproduced by someone who has the example datasets and nothing else.
|
|
79
97
|
|
|
80
98
|
Always regenerate with `--reuse-selections` so pages stay figure-for-figure
|
|
81
99
|
comparable and no SlideRule request is made. Reports are capped at 200 effective
|
|
@@ -93,9 +111,9 @@ what keeps the assets small.
|
|
|
93
111
|
- **There is no "unadjusted" CSM state on disk after a `bundle_adjust` run** — it writes only `*.adjusted_state.json` for the *optimized* cameras, while the originals exist solely as vendor XML. To get the before-camera for `csm_camera_summary_plot()`, re-run with an identity transform: write a 4x4 identity to `identity.txt` and pass `--initial-transform identity.txt --apply-initial-transform-only` with the same images/XMLs into a separate `-o` prefix. That skips iterations, matching, and reports (seconds, not minutes), and the resulting `.adjust` files are exactly zero translation + identity quaternion, so you can verify the cameras really are untouched. Demonstrated in `notebooks/WorldView/worldview_spacenet_ucsd_stereo.ipynb`.
|
|
94
112
|
- **CSM camera *differences* deliberately diverge from ASP's `orbit_plot.py`** (#53): `csm_analysis.read_angles_common_frame()` puts both cameras in one satellite body frame estimated from the *original* ephemeris, instead of estimating a frame per camera from its own positions the way `csm_io.read_angles()` (the ASP mirror) does. A solver perturbs positions *and* resamples the ephemeris finer, and over the resulting ~140 m central-difference baseline a 2 m perturbation tilts a per-camera frame by ~0.8° — which is what made the example figure look full of outliers. Angle diffs are also wrapped to [-180, 180). Don't "re-sync" either back to `read_angles()`; `csm_io.py` stays a verbatim ASP mirror and is not where this belongs.
|
|
95
113
|
- **Airbus DIMAP quaternions are scalar-first** (`Q0` = scalar); they are reordered to the scalar-last `q1..q4` layout the roll/pitch/yaw code expects in `PleiadesMetadata.getAtt_df()`. Don't "fix" the reorder.
|
|
96
|
-
- **The altimetry outlier cut is a 30-NMAD gate plus 3σ; both values are deliberate.** A whole ICESat-2 pass can be cloud (over UCSD one pass was a marine-layer return 150–210 m above ground, 21 % of the sample), which defeats a plain mean ± 3σ cut by inflating σ; the gate removes it. Don't "simplify" the cut to median ± 3 NMAD: on the heavy-tailed residuals a DSM has against ICESat-2 in a city that removes a fifth of the real points and lowers every reported NMAD by a third (`_outlier_mask` in `altimetry_source.py
|
|
97
|
-
- **`asp_plot.sensors.rpc.read_rpc()` refuses any raster that carries a CRS**, on purpose: an RPC describes the raw image grid, and ASP's `mapproject` copies RPC metadata onto its outputs.
|
|
98
|
-
- **SpaceNet CORE3D NITF scenes are JPEG2000, and the ASP 3.8.0-alpha build ships GDAL's JP2OpenJPEG driver as a plugin it never loads**: `gdalinfo` and rasterio both fail with *No JPEG2000 capable driver is available*. `GDAL_DRIVER_PATH=<ASP root>/lib` for one `gdal_translate` to GeoTIFF is enough; the older 3.7.0 build had the driver built in, which is why earlier UCSD runs never hit this.
|
|
114
|
+
- **The altimetry outlier cut is a 30-NMAD gate plus 3σ; both values are deliberate.** A whole ICESat-2 pass can be cloud (over UCSD one pass was a marine-layer return 150–210 m above ground, 21 % of the sample), which defeats a plain mean ± 3σ cut by inflating σ; the gate removes it. Don't "simplify" the cut to median ± 3 NMAD: on the heavy-tailed residuals a DSM has against ICESat-2 in a city that removes a fifth of the real points and lowers every reported NMAD by a third (`_outlier_mask` in `altimetry_source.py`). If a benchmark or report shows an RMSE far above its NMAD, look for a cloud pass in the residual map before touching the DEM.
|
|
115
|
+
- **`asp_plot.sensors.rpc.read_rpc()` refuses any raster that carries a CRS**, on purpose: an RPC describes the raw image grid, and ASP's `mapproject` copies RPC metadata onto its outputs.
|
|
116
|
+
- **SpaceNet CORE3D NITF scenes are JPEG2000, and the ASP 3.8.0-alpha build ships GDAL's JP2OpenJPEG driver as a plugin it never loads**: `gdalinfo` and rasterio both fail with *No JPEG2000 capable driver is available*. `GDAL_DRIVER_PATH=<ASP root>/lib` for one `gdal_translate` to GeoTIFF is enough; the older 3.7.0 build had the driver built in, which is why earlier UCSD runs never hit this. The conda-forge `asp-3.7.0` env on NAS ships it as a plugin too, under `$ASP_ROOT/lib/gdalplugins`.
|
|
99
117
|
- **ASP multiview triangulation of mapprojected images needs `ISISROOT`** (observed with ASP 3.8.0-alpha, non-ISIS `-t pleiades` session): the joint triangulation aborts with an uncatchable `Isis::IException` (`$ISISROOT/IsisPreferences was not found`) that surfaces as a generic "Failed to run"/killed job. Workaround: `export ISISROOT=<ASP install root>` (the release bundles `IsisPreferences` there). Pair runs and raw-image multiview runs are unaffected; full write-up in a PR #155 comment.
|
|
100
118
|
|
|
101
119
|
## External Data Sources
|
|
@@ -62,6 +62,7 @@ The package is organized by functionality, with each module focused on a specifi
|
|
|
62
62
|
- A declarative section registry (`REPORT_SECTIONS`) of `ReportSpec`s replaces the old inline plot-and-append wall: each spec pairs an `enabled(ctx)` predicate with a `build(ctx)` function returning the sections to append. `--no-geometry` / `--no-altimetry` / `--no-pc-align` gating are predicates; figure numbering comes from a per-run counter on the shared `ReportContext`, so section order and numbering are data, not source-line position. The alignment "Page B/C/D" follow-ups are one spec emitting several sections
|
|
63
63
|
- Section builders: `_build_input_scenes`, `_build_stereo_geometry`, `_build_match_points`, `_build_bundle_adjust`, `_build_disparity`, `_build_dem_results`, `_build_detailed_hillshade`, `_build_altimetry` (→ `_build_altimetry_earth` / `_build_altimetry_planetary`)
|
|
64
64
|
- `_numbered_sections()`: shared helper turning a plotter's saved-filename list into one `ReportSection` per figure (first gets the caption, continuations are titled "... (continued)") — used by the scenes/geometry/match/disparity builders, whose plotters save one figure per pair on multi-view runs (issue #160)
|
|
65
|
+
- `_keep_figures()`: with `--figures-directory`, copies each section's PNG out of `tmp_asp_report_plots/` as `NN_<title-slug>.png` after `compile_report()` has numbered the sections, and before the plots directory is deleted. The title is in the name because figure numbers shift when a section is gated off
|
|
65
66
|
|
|
66
67
|
**`report_captions.py`** - Caption + description text for the report sections (issue #128)
|
|
67
68
|
- All section captions and the Earth/planetary alignment descriptions as module constants / small builder functions, moved out of the CLI as a data module (verified byte-identical to the pre-refactor strings via AST comparison)
|
|
@@ -366,7 +367,7 @@ Docs dependencies are in `pyproject.toml` under `[project.optional-dependencies]
|
|
|
366
367
|
## Example Notebooks
|
|
367
368
|
|
|
368
369
|
Example notebooks demonstrating modular usage are organized by sensor type:
|
|
369
|
-
- `notebooks/WorldView/` - WorldView examples (Earth-based). `worldview_spacenet_ucsd_stereo.ipynb` is the classic two-scene pair example; `worldview_utqiagvik_stereo.ipynb` (excluded from the docs build) and `worldview_uyuni_jitter_plots.ipynb` cover comprehensive modular usage and jitter plots
|
|
370
|
+
- `notebooks/WorldView/` - WorldView examples (Earth-based). `worldview_spacenet_ucsd_stereo.ipynb` is the classic two-scene pair example; `worldview_utqiagvik_stereo.ipynb` (excluded from the docs build) and `worldview_uyuni_jitter_plots.ipynb` cover comprehensive modular usage and jitter plots. The Atlanta example (`worldview_spacenet_atlanta_mvs.ipynb`) is a 3-scene same-pass multi-view run (`parallel_stereo --alignment-method affineepipolar` on the `wv_correct`ed `*_corr.tif` images, nadir13 reference) compared against the ASP-docs-recommended alternative: the three pairwise runs merged with `dem_mosaic` (coverage, DEM difference, ICESat-2 residuals). Two crop-window methods keep the ROIs matched: the reference crop window in mapprojected/ground coordinates converts to image pixels via GDAL's inverse RPC transform on the *original* `.tif` (the `wv_correct` `_corr.tif` carries no RPC metadata) — `gdal.Transformer(["METHOD=RPC", "RPC_HEIGHT=<m>"])` + `TransformPoint(1, lon, lat, h)` on the corners; and the `--left-image-crop-win` for a pair whose left image is *not* the reference is derived from the `bundle_adjust` clean match points that fall inside the reference crop window (no RPCs needed at all). Its benchmark cell, six DEMs scored with `DEMBenchmark` on one ICESat-2 sample, is the worked example of `DEMBenchmark`.
|
|
370
371
|
- `notebooks/Pleiades/` - Airbus Pléiades Neo tri-stereo example over Marseille (Earth-based): DIMAP stereo-geometry analysis, bundle adjustment of all three scenes, and a 3-image multi-view stereo run (nadir reference, `--left-image-crop-win` derived from the DIMAP `Dataset_Extent` vertices; the ASP docs discourage MVS in favor of pairwise + `dem_mosaic`, which the notebook cites)
|
|
371
372
|
- `notebooks/ASTER/` - ASTER examples with map-projection and jitter correction (Earth-based)
|
|
372
373
|
- `notebooks/LRO_NAC/` - Lunar Reconnaissance Orbiter Narrow Angle Camera examples (Lunar)
|
|
@@ -5,6 +5,28 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [3.5.0] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
In this release:
|
|
13
|
+
|
|
14
|
+
- `asp_report --figures-directory` keeps a copy of each report figure, named by figure number and title, for use in notebooks and slides.
|
|
15
|
+
- `DEMBenchmark` reuses a translated DEM newer than its inputs instead of rewriting it on every run.
|
|
16
|
+
- Three study notebooks are no longer in the examples.
|
|
17
|
+
- The example reports are regenerated under 3.4.0, and a reports release is kept off Zenodo.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`asp_report --figures-directory DIR` keeps a copy of each report figure** (`ReportConfig.figures_directory`). The report plots every figure to `tmp_asp_report_plots/` and deletes that directory once the PDF is written, so the only way to show a figure in a notebook or slides was to extract it from the PDF. With the option, each figure is also copied to `DIR` as `NN_<title>.png` (`05_dem_results.png`, `07_icesat_2_atl06_sr_map.png`), where `NN` is its figure number in the PDF. The title is in the name because the numbers shift when a section is skipped. Nothing is kept by default, so existing runs are unchanged.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **The example reports are regenerated under 3.4.0** and published as [`reports-2026-09-29`](https://github.com/uw-cryo/asp_plot/releases/tag/reports-2026-09-29); the docs build fetches that set. All seven carry the intersection error captions of 3.4.0. `WorldView_Atlanta_MVS` is rebuilt for the first time since the 200 dpi cap, which the fix for deleted `*-L.tif` files ([#202](https://github.com/uw-cryo/asp_plot/issues/202)) made possible, and goes from 22.4 MB to 12.4 MB. Every report was rebuilt with `--reuse-selections`, so each page shows the same altimetry points and clips as before.
|
|
24
|
+
- **A reports release is kept off Zenodo.** Zenodo archives every published GitHub release as a version of the software, and the first reports release had become what the DOI badge resolves to. `AGENTS.md` now has the step that prevents it: the Zenodo webhook is switched off while a reports release is published.
|
|
25
|
+
- **`DEMBenchmark` reuses a translated DEM that is newer than its DEM and its `pc_align` output** instead of rewriting it on every run, so re-scoring from cached `pc_align` runs no longer writes a full-size copy of every DEM. A new copy is written under a temporary name and renamed, so an interrupted run cannot leave a partial file that a later run would reuse.
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
- **Three study notebooks are no longer examples: `worldview_spacenet_benchmark.ipynb`, `worldview_spacenet_atlanta_stereo_scene_selection.ipynb` and `worldview_spacenet_ucsd_stereo_scene_selection.ipynb`.** Each answered a research question (which scene combination or pair gives the best DEM) rather than showing how to use a feature, and each re-execution added a full new copy of the notebook to the repository's history. `worldview_spacenet_atlanta_mvs.ipynb` remains the worked example of `DEMBenchmark`; `docs/cli/stereo_geom.md` covers assessing the geometry of every pair in a multi-scene delivery; and the UCSD stereo and Atlanta MVS notebooks now say in their text how their pairs were chosen and how the candidates scored (markdown edits only; neither notebook was re-executed).
|
|
29
|
+
|
|
8
30
|
## [3.4.0] - 2026-09-29
|
|
9
31
|
|
|
10
32
|
In this release:
|
|
@@ -137,6 +137,12 @@ def _reconstruct_command():
|
|
|
137
137
|
type=int,
|
|
138
138
|
help="Resolution ceiling for figures embedded in the PDF, applied at the size each figure is placed on the page. Figures are plotted at 220 dpi and then scaled down to the page, so wide multi-panel figures otherwise land at 400+ effective dpi and inflate the file for detail no screen or printer shows. Pass 0 to embed the full-resolution figures.",
|
|
139
139
|
)
|
|
140
|
+
@click.option(
|
|
141
|
+
"--figures-directory",
|
|
142
|
+
prompt=False,
|
|
143
|
+
default=None,
|
|
144
|
+
help="Directory to keep a copy of each report figure in, as NN_<title>.png (e.g. 05_dem_results.png), where NN is the figure number in the PDF. A relative path is taken from the current directory, not from --directory. Use this to show figures in a notebook or slides without extracting them from the PDF. Files of the same name are overwritten. Default: None, the figures are not kept.",
|
|
145
|
+
)
|
|
140
146
|
def main(
|
|
141
147
|
directory,
|
|
142
148
|
bundle_adjust_prefix,
|
|
@@ -156,6 +162,7 @@ def main(
|
|
|
156
162
|
report_filename,
|
|
157
163
|
report_title,
|
|
158
164
|
figure_max_dpi,
|
|
165
|
+
figures_directory,
|
|
159
166
|
):
|
|
160
167
|
"""
|
|
161
168
|
Generate a comprehensive report of ASP processing results.
|
|
@@ -183,6 +190,7 @@ def main(
|
|
|
183
190
|
report_filename=report_filename,
|
|
184
191
|
report_title=report_title,
|
|
185
192
|
figure_max_dpi=figure_max_dpi,
|
|
193
|
+
figures_directory=figures_directory,
|
|
186
194
|
report_command=_reconstruct_command(),
|
|
187
195
|
)
|
|
188
196
|
run_report(config)
|
|
@@ -54,6 +54,7 @@ Examples
|
|
|
54
54
|
>>> bench.summary_plot(save_dir="atlanta_mvs", fig_fn="dem_benchmark.png")
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
|
+
import glob
|
|
57
58
|
import logging
|
|
58
59
|
import os
|
|
59
60
|
import re
|
|
@@ -235,6 +236,14 @@ def _safe_dirname(label):
|
|
|
235
236
|
return re.sub(r"[^\w.-]+", "_", label).strip("_") or "dem"
|
|
236
237
|
|
|
237
238
|
|
|
239
|
+
def _is_newer_than(fn, sources):
|
|
240
|
+
"""True if ``fn`` exists and is at least as new as every existing file in ``sources``."""
|
|
241
|
+
if not os.path.exists(fn):
|
|
242
|
+
return False
|
|
243
|
+
mtime = os.path.getmtime(fn)
|
|
244
|
+
return all(os.path.getmtime(s) <= mtime for s in sources if os.path.exists(s))
|
|
245
|
+
|
|
246
|
+
|
|
238
247
|
def _dh_stats(dh, prefix):
|
|
239
248
|
"""median / NMAD / RMSE of a dh series as ``{prefix}_*_m`` keys (NaN when empty)."""
|
|
240
249
|
values = np.asarray(dh, dtype=float)
|
|
@@ -363,8 +372,10 @@ class DEMBenchmark:
|
|
|
363
372
|
directory : str
|
|
364
373
|
Working folder. pc_align products, the pc_align CSV and the translated
|
|
365
374
|
DEM copy for each candidate go under ``<directory>/dem_benchmark/<label>/``.
|
|
366
|
-
The translated copy is
|
|
367
|
-
|
|
375
|
+
The translated copy is reused while it is newer than its DEM and the
|
|
376
|
+
pc_align output, and otherwise rewritten (under a temporary name, then
|
|
377
|
+
renamed), so two benchmarks sharing a directory and a label must not
|
|
378
|
+
run at the same time.
|
|
368
379
|
dems : dict or list
|
|
369
380
|
``{label: dem_fn}`` (order preserved), or a list of paths /
|
|
370
381
|
``"label=path"`` strings (see :func:`parse_dem_specs`).
|
|
@@ -597,7 +608,8 @@ class DEMBenchmark:
|
|
|
597
608
|
Also run ``pc_align --compute-translation-only`` per DEM and
|
|
598
609
|
re-score against the translated copy. Default True. Existing
|
|
599
610
|
pc_align outputs under the benchmark folder are reused, so a
|
|
600
|
-
re-run makes no pc_align call and works offline
|
|
611
|
+
re-run makes no pc_align call and works offline; so are translated
|
|
612
|
+
DEMs newer than their DEM and pc_align output.
|
|
601
613
|
minimum_points : int, optional
|
|
602
614
|
Skip pc_align for a DEM with fewer valid dh points, default 500
|
|
603
615
|
(the report's threshold).
|
|
@@ -992,10 +1004,23 @@ class DEMBenchmark:
|
|
|
992
1004
|
)
|
|
993
1005
|
return None, None
|
|
994
1006
|
stem = os.path.splitext(os.path.basename(dem_fn))[0]
|
|
995
|
-
aligned_fn =
|
|
996
|
-
|
|
997
|
-
|
|
1007
|
+
aligned_fn = os.path.join(label_dir, f"{stem}_pc_align_translated.tif")
|
|
1008
|
+
if _is_newer_than(
|
|
1009
|
+
aligned_fn,
|
|
1010
|
+
[dem_fn]
|
|
1011
|
+
+ glob.glob(os.path.join(label_dir, "pc_align", f"{prefix_name}-*")),
|
|
1012
|
+
):
|
|
1013
|
+
print(f" Reusing translated DEM {aligned_fn}")
|
|
1014
|
+
return report, aligned_fn
|
|
1015
|
+
# Write under a temporary name and rename, so an interrupted run never
|
|
1016
|
+
# leaves a partial copy that the next run would take as current.
|
|
1017
|
+
partial_fn = os.path.join(label_dir, f"{stem}_pc_align_translated.partial.tif")
|
|
1018
|
+
written = alignment.apply_dem_translation(
|
|
1019
|
+
output_prefix=output_prefix, output_fn=partial_fn
|
|
998
1020
|
)
|
|
1021
|
+
if written != partial_fn:
|
|
1022
|
+
return report, written
|
|
1023
|
+
os.replace(partial_fn, aligned_fn)
|
|
999
1024
|
return report, aligned_fn
|
|
1000
1025
|
|
|
1001
1026
|
def _reference_stats(self, label, dem_fn):
|
|
@@ -14,6 +14,7 @@ Captions live in :mod:`asp_plot.report_captions`.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import os
|
|
17
|
+
import re
|
|
17
18
|
import shutil
|
|
18
19
|
from dataclasses import dataclass, field
|
|
19
20
|
from datetime import datetime, timezone
|
|
@@ -83,6 +84,9 @@ class ReportConfig:
|
|
|
83
84
|
# Resolution ceiling for figures embedded in the PDF, applied at the size
|
|
84
85
|
# each figure is placed on the page. See asp_plot.report.FIGURE_MAX_DPI.
|
|
85
86
|
figure_max_dpi: Optional[int] = FIGURE_MAX_DPI
|
|
87
|
+
# Directory to keep a copy of each report figure in, as NN_<title>.png.
|
|
88
|
+
# None keeps nothing: the figures are deleted with the plots directory.
|
|
89
|
+
figures_directory: Optional[str] = None
|
|
86
90
|
# Reconstructed "asp_report --flag ..." string recorded in the report. The
|
|
87
91
|
# CLI builds this from the Click context; non-CLI callers may leave it None.
|
|
88
92
|
report_command: Optional[str] = None
|
|
@@ -1052,12 +1056,35 @@ def _write_figure_selections(ctx: ReportContext) -> None:
|
|
|
1052
1056
|
print(f"\nCould not write figure selections: {e}\n")
|
|
1053
1057
|
|
|
1054
1058
|
|
|
1059
|
+
def _keep_figures(sections: List[object], figures_directory: str) -> None:
|
|
1060
|
+
"""Copy each report figure to ``figures_directory`` as ``NN_<title>.png``.
|
|
1061
|
+
|
|
1062
|
+
``NN`` is the figure number printed in the PDF and ``<title>`` the page
|
|
1063
|
+
heading in lowercase with runs of other characters replaced by ``_``
|
|
1064
|
+
(e.g. ``05_dem_results.png``), so a figure can be picked by name even when
|
|
1065
|
+
a section is skipped and the numbers shift. Files of the same name are
|
|
1066
|
+
overwritten; nothing else in the directory is touched.
|
|
1067
|
+
"""
|
|
1068
|
+
figures_directory = os.path.expanduser(figures_directory)
|
|
1069
|
+
os.makedirs(figures_directory, exist_ok=True)
|
|
1070
|
+
kept = 0
|
|
1071
|
+
for section in sections:
|
|
1072
|
+
if not section.image_path or not os.path.exists(section.image_path):
|
|
1073
|
+
continue
|
|
1074
|
+
slug = re.sub(r"[^a-z0-9]+", "_", section.title.lower()).strip("_")
|
|
1075
|
+
fn = f"{section.figure_number:02}_{slug}.png"
|
|
1076
|
+
shutil.copy2(section.image_path, os.path.join(figures_directory, fn))
|
|
1077
|
+
kept += 1
|
|
1078
|
+
print(f"\nKept {kept} report figures in {figures_directory}\n")
|
|
1079
|
+
|
|
1080
|
+
|
|
1055
1081
|
def run_report(config: ReportConfig) -> str:
|
|
1056
1082
|
"""Generate the ASP processing report described by ``config``.
|
|
1057
1083
|
|
|
1058
1084
|
Builds the runtime context, iterates the declarative
|
|
1059
1085
|
:data:`REPORT_SECTIONS` registry to assemble the report body, compiles the
|
|
1060
|
-
PDF, writes the figure-selections sidecar,
|
|
1086
|
+
PDF, writes the figure-selections sidecar, keeps a copy of the figures if
|
|
1087
|
+
``config.figures_directory`` is set, and cleans up. Returns the path
|
|
1061
1088
|
to the written PDF. Importable and callable without any Click context.
|
|
1062
1089
|
"""
|
|
1063
1090
|
ctx = _setup_context(config)
|
|
@@ -1088,6 +1115,9 @@ def run_report(config: ReportConfig) -> str:
|
|
|
1088
1115
|
|
|
1089
1116
|
_write_figure_selections(ctx)
|
|
1090
1117
|
|
|
1118
|
+
if config.figures_directory:
|
|
1119
|
+
_keep_figures(sections, config.figures_directory)
|
|
1120
|
+
|
|
1091
1121
|
shutil.rmtree(ctx.plots_directory)
|
|
1092
1122
|
|
|
1093
1123
|
print(f"\n\nReport saved to {ctx.report_pdf_path}\n\n")
|
|
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
|
|
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
|