asp-plot 1.5.0__tar.gz → 1.6.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-1.5.0 → asp_plot-1.6.0}/.claude/settings.local.json +11 -1
- {asp_plot-1.5.0 → asp_plot-1.6.0}/CHANGELOG.md +19 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/PKG-INFO +2 -2
- {asp_plot-1.5.0 → asp_plot-1.6.0}/README.md +1 -1
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/cli/asp_plot.py +174 -52
- asp_plot-1.6.0/asp_plot/report.py +296 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/utils.py +0 -94
- {asp_plot-1.5.0 → asp_plot-1.6.0}/environment.yml +1 -2
- {asp_plot-1.5.0 → asp_plot-1.6.0}/pyproject.toml +1 -1
- {asp_plot-1.5.0 → asp_plot-1.6.0}/.flake8 +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/.github/workflows/release.yml +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/.github/workflows/run-tests.yml +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/.gitignore +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/.pre-commit-config.yaml +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/LICENSE +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/__init__.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/alignment.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/altimetry.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/bundle_adjust.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/cli/__init__.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/cli/csm_camera_plot.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/cli/stereo_geom.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/csm_camera.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/processing_parameters.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/scenes.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/stereo.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/stereo_geometry.py +0 -0
- {asp_plot-1.5.0 → asp_plot-1.6.0}/asp_plot/stereopair_metadata_parser.py +0 -0
|
@@ -16,7 +16,17 @@
|
|
|
16
16
|
"WebFetch(domain:dg-cms-uploads-production.s3.amazonaws.com)",
|
|
17
17
|
"WebFetch(domain:engineering.purdue.edu)",
|
|
18
18
|
"Bash(gh api:*)",
|
|
19
|
-
"Bash(gh run view:*)"
|
|
19
|
+
"Bash(gh run view:*)",
|
|
20
|
+
"Bash(gh issue view:*)",
|
|
21
|
+
"WebFetch(domain:github.com)",
|
|
22
|
+
"WebFetch(domain:pypi.org)",
|
|
23
|
+
"Bash(find:*)",
|
|
24
|
+
"WebFetch(domain:py-pdf.github.io)",
|
|
25
|
+
"Bash(conda install:*)",
|
|
26
|
+
"Bash(mamba install:*)",
|
|
27
|
+
"Bash(python:*)",
|
|
28
|
+
"Bash(pip install:*)",
|
|
29
|
+
"Bash(pre-commit run:*)"
|
|
20
30
|
],
|
|
21
31
|
"deny": [],
|
|
22
32
|
"ask": []
|
|
@@ -5,6 +5,25 @@ 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.6.0] - 2026-02-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Structured PDF report generation with title page, section headings, figure captions, DEM metadata summary table, and runtime summary table
|
|
12
|
+
- New `report.py` module containing `ReportSection` and `ReportMetadata` dataclasses, `ASPReportPDF` class, and `compile_report()` function
|
|
13
|
+
- DEM metadata (dimensions, GSD, CRS, nodata %, elevation range) automatically collected and displayed on the report title page
|
|
14
|
+
- Figure captions describing each plot in the generated PDF report
|
|
15
|
+
- Page headers (report title) and footers (page numbers) throughout the report
|
|
16
|
+
- Tests for report dataclasses and PDF compilation (8 new tests)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Replaced `markdown-pdf` dependency with `fpdf2` (available on conda-forge, enabling conda-only installation)
|
|
20
|
+
- Reordered report sections: Input Scenes and Stereo Geometry now appear before DEM results, matching the logical processing flow
|
|
21
|
+
- Report generation moved from `utils.py` to dedicated `report.py` module
|
|
22
|
+
- PNG images are now embedded directly in the PDF (eliminated intermediate PNG-to-JPEG conversion step)
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
- Dependency on `markdown-pdf` (pip-only package that blocked conda-forge packaging)
|
|
26
|
+
|
|
8
27
|
## [1.5.0] - 2026-02-13
|
|
9
28
|
|
|
10
29
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asp_plot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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: Issues, https://github.com/uw-cryo/asp_plot/issues
|
|
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
Scripts and notebooks to visualize output from the [NASA Ames Stereo Pipeline (ASP)](https://github.com/NeoGeographyToolkit/StereoPipeline).
|
|
21
21
|
|
|
22
22
|
> [!IMPORTANT]
|
|
23
|
-
> [View an example WorldView
|
|
23
|
+
> [View an example WorldView report here](notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
|
|
24
24
|
>
|
|
25
25
|
|
|
26
26
|
## Motivation
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Scripts and notebooks to visualize output from the [NASA Ames Stereo Pipeline (ASP)](https://github.com/NeoGeographyToolkit/StereoPipeline).
|
|
6
6
|
|
|
7
7
|
> [!IMPORTANT]
|
|
8
|
-
> [View an example WorldView
|
|
8
|
+
> [View an example WorldView report here](notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
|
|
9
9
|
>
|
|
10
10
|
|
|
11
11
|
## Motivation
|
|
@@ -9,10 +9,11 @@ import contextily as ctx
|
|
|
9
9
|
from asp_plot.altimetry import Altimetry
|
|
10
10
|
from asp_plot.bundle_adjust import PlotBundleAdjustFiles, ReadBundleAdjustFiles
|
|
11
11
|
from asp_plot.processing_parameters import ProcessingParameters
|
|
12
|
+
from asp_plot.report import ReportMetadata, ReportSection, compile_report
|
|
12
13
|
from asp_plot.scenes import ScenePlotter
|
|
13
14
|
from asp_plot.stereo import StereoPlotter
|
|
14
15
|
from asp_plot.stereo_geometry import StereoGeometryPlotter
|
|
15
|
-
from asp_plot.utils import Raster
|
|
16
|
+
from asp_plot.utils import Raster
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@click.command()
|
|
@@ -140,17 +141,21 @@ def main(
|
|
|
140
141
|
directory, os.path.join(stereo_directory, report_filename)
|
|
141
142
|
)
|
|
142
143
|
|
|
144
|
+
sections = []
|
|
143
145
|
figure_counter = count(0)
|
|
144
146
|
|
|
145
|
-
|
|
147
|
+
# Initialize StereoPlotter early (needed for DEM info and multiple plot types)
|
|
148
|
+
stereo_plotter = StereoPlotter(
|
|
146
149
|
directory,
|
|
147
150
|
stereo_directory,
|
|
148
151
|
reference_dem=reference_dem,
|
|
149
152
|
dem_fn=dem_filename,
|
|
150
153
|
dem_gsd=dem_gsd,
|
|
151
|
-
)
|
|
154
|
+
)
|
|
155
|
+
asp_dem = stereo_plotter.dem_fn
|
|
152
156
|
|
|
153
|
-
# Set map CRS from output DEM
|
|
157
|
+
# Set map CRS from output DEM and collect DEM metadata
|
|
158
|
+
report_metadata = None
|
|
154
159
|
if map_crs is None:
|
|
155
160
|
if asp_dem and os.path.exists(asp_dem):
|
|
156
161
|
try:
|
|
@@ -164,6 +169,30 @@ def main(
|
|
|
164
169
|
)
|
|
165
170
|
map_crs = "EPSG:4326"
|
|
166
171
|
|
|
172
|
+
# Collect DEM metadata for the report title page
|
|
173
|
+
if asp_dem and os.path.exists(asp_dem):
|
|
174
|
+
try:
|
|
175
|
+
dem_raster = Raster(asp_dem)
|
|
176
|
+
dem_data = dem_raster.read_array()
|
|
177
|
+
total_pixels = dem_data.size
|
|
178
|
+
nodata_pixels = dem_data.mask.sum() if hasattr(dem_data.mask, "sum") else 0
|
|
179
|
+
nodata_pct = (nodata_pixels / total_pixels * 100) if total_pixels else 0.0
|
|
180
|
+
valid = dem_data.compressed()
|
|
181
|
+
elev_range = (
|
|
182
|
+
(float(valid.min()), float(valid.max())) if valid.size else (0, 0)
|
|
183
|
+
)
|
|
184
|
+
report_metadata = ReportMetadata(
|
|
185
|
+
dem_dimensions=(dem_raster.ds.width, dem_raster.ds.height),
|
|
186
|
+
dem_gsd_m=dem_raster.get_gsd(),
|
|
187
|
+
dem_crs=map_crs or "",
|
|
188
|
+
dem_nodata_percent=nodata_pct,
|
|
189
|
+
dem_elevation_range=elev_range,
|
|
190
|
+
dem_filename=os.path.basename(asp_dem),
|
|
191
|
+
reference_dem=reference_dem or "",
|
|
192
|
+
)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
print(f"\nCould not collect DEM metadata: {e}\n")
|
|
195
|
+
|
|
167
196
|
# TODO: Centralize this in plotting utils, should not need ctx import in the CLI wrapper
|
|
168
197
|
if add_basemap:
|
|
169
198
|
ctx_kwargs = {
|
|
@@ -175,56 +204,84 @@ def main(
|
|
|
175
204
|
else:
|
|
176
205
|
ctx_kwargs = {}
|
|
177
206
|
|
|
178
|
-
#
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
207
|
+
# ---- Section 1: Input Scenes ----
|
|
208
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
209
|
+
scene_plotter = ScenePlotter(directory, stereo_directory, title="Input Scenes")
|
|
210
|
+
scene_plotter.plot_scenes(save_dir=plots_directory, fig_fn=fig_fn)
|
|
211
|
+
sections.append(
|
|
212
|
+
ReportSection(
|
|
213
|
+
title="Input Scenes",
|
|
214
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
215
|
+
caption="Left and right input scenes used for stereo processing.",
|
|
216
|
+
)
|
|
186
217
|
)
|
|
187
218
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
219
|
+
# ---- Section 2: Stereo Geometry (conditional) ----
|
|
220
|
+
if plot_geometry:
|
|
221
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
222
|
+
geom_plotter = StereoGeometryPlotter(directory, add_basemap=add_basemap)
|
|
223
|
+
geom_plotter.dg_geom_plot(save_dir=plots_directory, fig_fn=fig_fn)
|
|
224
|
+
sections.append(
|
|
225
|
+
ReportSection(
|
|
226
|
+
title="Stereo Geometry",
|
|
227
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
228
|
+
caption="Stereo acquisition geometry skyplot and map view showing satellite viewing angles and scene footprints.",
|
|
229
|
+
)
|
|
230
|
+
)
|
|
193
231
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
232
|
+
# ---- Section 3: Match Points ----
|
|
233
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
234
|
+
stereo_plotter.title = "Stereo Match Points"
|
|
235
|
+
stereo_plotter.plot_match_points(save_dir=plots_directory, fig_fn=fig_fn)
|
|
236
|
+
sections.append(
|
|
237
|
+
ReportSection(
|
|
238
|
+
title="Match Points",
|
|
239
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
240
|
+
caption="Interest point matches between left and right images identified during stereo correlation.",
|
|
241
|
+
)
|
|
198
242
|
)
|
|
199
243
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
save_dir=plots_directory,
|
|
205
|
-
fig_fn=f"{next(figure_counter):02}.png",
|
|
244
|
+
# ---- Section 4: Detailed Hillshade ----
|
|
245
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
246
|
+
stereo_plotter.title = "Hillshade with details"
|
|
247
|
+
stereo_plotter.plot_detailed_hillshade(
|
|
248
|
+
subset_km=subset_km, save_dir=plots_directory, fig_fn=fig_fn
|
|
206
249
|
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
250
|
+
sections.append(
|
|
251
|
+
ReportSection(
|
|
252
|
+
title="Detailed Hillshade",
|
|
253
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
254
|
+
caption=f"DEM hillshade with {subset_km} km detail subset in second row. If available, corresponding mapprojected ortho image subsets are displayed in the bottom row.",
|
|
255
|
+
)
|
|
212
256
|
)
|
|
213
257
|
|
|
214
|
-
#
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
258
|
+
# ---- Section 5: DEM Results ----
|
|
259
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
260
|
+
stereo_plotter.title = "Stereo DEM Results"
|
|
261
|
+
stereo_plotter.plot_dem_results(save_dir=plots_directory, fig_fn=fig_fn)
|
|
262
|
+
sections.append(
|
|
263
|
+
ReportSection(
|
|
264
|
+
title="DEM Results",
|
|
265
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
266
|
+
caption="Output DEM with intersection error map and difference relative to the reference DEM used in processing.",
|
|
267
|
+
)
|
|
218
268
|
)
|
|
219
269
|
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
270
|
+
# ---- Section 6: Disparity ----
|
|
271
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
272
|
+
stereo_plotter.title = "Disparity (pixels)"
|
|
273
|
+
stereo_plotter.plot_disparity(
|
|
274
|
+
unit="pixels", quiver=True, save_dir=plots_directory, fig_fn=fig_fn
|
|
275
|
+
)
|
|
276
|
+
sections.append(
|
|
277
|
+
ReportSection(
|
|
278
|
+
title="Disparity",
|
|
279
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
280
|
+
caption="Horizontal and vertical disparity maps in pixels with quiver overlay.",
|
|
225
281
|
)
|
|
282
|
+
)
|
|
226
283
|
|
|
227
|
-
# ICESat-2
|
|
284
|
+
# ---- Sections 7-10: ICESat-2 (conditional) ----
|
|
228
285
|
if plot_icesat:
|
|
229
286
|
icesat = Altimetry(directory=directory, dem_fn=asp_dem)
|
|
230
287
|
|
|
@@ -242,37 +299,69 @@ def main(
|
|
|
242
299
|
|
|
243
300
|
icesat.predefined_temporal_filter_atl06sr(date=icesat_filter_date)
|
|
244
301
|
|
|
302
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
245
303
|
icesat.mapview_plot_atl06sr_to_dem(
|
|
246
304
|
key="all",
|
|
247
305
|
save_dir=plots_directory,
|
|
248
|
-
fig_fn=
|
|
306
|
+
fig_fn=fig_fn,
|
|
249
307
|
map_crs=map_crs,
|
|
250
308
|
**ctx_kwargs,
|
|
251
309
|
)
|
|
310
|
+
sections.append(
|
|
311
|
+
ReportSection(
|
|
312
|
+
title="ICESat-2 ATL06-SR Map (All)",
|
|
313
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
314
|
+
caption="ICESat-2 ATL06-SR elevation differences (all processing levels) vs. ASP DEM.",
|
|
315
|
+
)
|
|
316
|
+
)
|
|
252
317
|
|
|
318
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
253
319
|
icesat.histogram(
|
|
254
320
|
key="all",
|
|
255
321
|
plot_aligned=False,
|
|
256
322
|
save_dir=plots_directory,
|
|
257
|
-
fig_fn=
|
|
323
|
+
fig_fn=fig_fn,
|
|
324
|
+
)
|
|
325
|
+
sections.append(
|
|
326
|
+
ReportSection(
|
|
327
|
+
title="ICESat-2 ATL06-SR Histogram (All)",
|
|
328
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
329
|
+
caption="Distribution of elevation differences between ICESat-2 ATL06-SR (all) and ASP DEM.",
|
|
330
|
+
)
|
|
258
331
|
)
|
|
259
332
|
|
|
333
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
260
334
|
icesat.mapview_plot_atl06sr_to_dem(
|
|
261
335
|
key="ground_seasonal",
|
|
262
336
|
save_dir=plots_directory,
|
|
263
|
-
fig_fn=
|
|
337
|
+
fig_fn=fig_fn,
|
|
264
338
|
map_crs=map_crs,
|
|
265
339
|
**ctx_kwargs,
|
|
266
340
|
)
|
|
341
|
+
sections.append(
|
|
342
|
+
ReportSection(
|
|
343
|
+
title="ICESat-2 ATL06-SR Map (Ground, Seasonal)",
|
|
344
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
345
|
+
caption="ICESat-2 ATL06-SR elevation differences (ground, seasonally filtered) vs. ASP DEM.",
|
|
346
|
+
)
|
|
347
|
+
)
|
|
267
348
|
|
|
349
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
268
350
|
icesat.histogram(
|
|
269
351
|
key="ground_seasonal",
|
|
270
352
|
plot_aligned=False,
|
|
271
353
|
save_dir=plots_directory,
|
|
272
|
-
fig_fn=
|
|
354
|
+
fig_fn=fig_fn,
|
|
355
|
+
)
|
|
356
|
+
sections.append(
|
|
357
|
+
ReportSection(
|
|
358
|
+
title="ICESat-2 ATL06-SR Histogram (Ground, Seasonal)",
|
|
359
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
360
|
+
caption="Distribution of elevation differences between ICESat-2 ATL06-SR (ground, seasonal) and ASP DEM.",
|
|
361
|
+
)
|
|
273
362
|
)
|
|
274
363
|
|
|
275
|
-
# Bundle
|
|
364
|
+
# ---- Sections 11+: Bundle Adjustment (conditional) ----
|
|
276
365
|
if bundle_adjust_directory:
|
|
277
366
|
try:
|
|
278
367
|
ba_files = ReadBundleAdjustFiles(directory, bundle_adjust_directory)
|
|
@@ -286,27 +375,43 @@ def main(
|
|
|
286
375
|
title="Bundle Adjust Initial and Final Residuals (Log Scale)",
|
|
287
376
|
)
|
|
288
377
|
|
|
378
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
289
379
|
plotter.plot_n_gdfs(
|
|
290
380
|
column_name="mean_residual",
|
|
291
381
|
cbar_label="Mean residual (px)",
|
|
292
382
|
map_crs=map_crs,
|
|
293
383
|
save_dir=plots_directory,
|
|
294
|
-
fig_fn=
|
|
384
|
+
fig_fn=fig_fn,
|
|
295
385
|
**ctx_kwargs,
|
|
296
386
|
)
|
|
387
|
+
sections.append(
|
|
388
|
+
ReportSection(
|
|
389
|
+
title="Bundle Adjust Residuals (Log Scale)",
|
|
390
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
391
|
+
caption="Initial and final bundle adjustment residuals on a logarithmic scale.",
|
|
392
|
+
)
|
|
393
|
+
)
|
|
297
394
|
|
|
298
395
|
plotter.lognorm = False
|
|
299
396
|
plotter.title = "Bundle Adjust Initial and Final Residuals (Linear Scale)"
|
|
300
397
|
|
|
398
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
301
399
|
plotter.plot_n_gdfs(
|
|
302
400
|
column_name="mean_residual",
|
|
303
401
|
cbar_label="Mean residual (px)",
|
|
304
402
|
common_clim=False,
|
|
305
403
|
map_crs=map_crs,
|
|
306
404
|
save_dir=plots_directory,
|
|
307
|
-
fig_fn=
|
|
405
|
+
fig_fn=fig_fn,
|
|
308
406
|
**ctx_kwargs,
|
|
309
407
|
)
|
|
408
|
+
sections.append(
|
|
409
|
+
ReportSection(
|
|
410
|
+
title="Bundle Adjust Residuals (Linear Scale)",
|
|
411
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
412
|
+
caption="Initial and final bundle adjustment residuals on a linear scale.",
|
|
413
|
+
)
|
|
414
|
+
)
|
|
310
415
|
|
|
311
416
|
# Map-projected residuals (requires reference DEM in bundle_adjust)
|
|
312
417
|
try:
|
|
@@ -317,14 +422,22 @@ def main(
|
|
|
317
422
|
title="Bundle Adjust Midpoint distance between\nfinal interest points projected onto reference DEM",
|
|
318
423
|
)
|
|
319
424
|
|
|
425
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
320
426
|
plotter.plot_n_gdfs(
|
|
321
427
|
column_name="mapproj_ip_dist_meters",
|
|
322
428
|
cbar_label="Interest point distance (m)",
|
|
323
429
|
map_crs=map_crs,
|
|
324
430
|
save_dir=plots_directory,
|
|
325
|
-
fig_fn=
|
|
431
|
+
fig_fn=fig_fn,
|
|
326
432
|
**ctx_kwargs,
|
|
327
433
|
)
|
|
434
|
+
sections.append(
|
|
435
|
+
ReportSection(
|
|
436
|
+
title="Map-Projected Residuals",
|
|
437
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
438
|
+
caption="Midpoint distance between final interest points projected onto the reference DEM used in processing.",
|
|
439
|
+
)
|
|
440
|
+
)
|
|
328
441
|
except ValueError as e:
|
|
329
442
|
print(f"\n\nSkipping map-projected residuals plot: {e}\n\n")
|
|
330
443
|
|
|
@@ -340,6 +453,7 @@ def main(
|
|
|
340
453
|
title="Bundle Adjust Initial and Final Geodiff vs. Reference DEM",
|
|
341
454
|
)
|
|
342
455
|
|
|
456
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
343
457
|
plotter.plot_n_gdfs(
|
|
344
458
|
column_name="height_diff_meters",
|
|
345
459
|
cbar_label="Height difference (m)",
|
|
@@ -347,9 +461,16 @@ def main(
|
|
|
347
461
|
cmap="RdBu",
|
|
348
462
|
symm_clim=True,
|
|
349
463
|
save_dir=plots_directory,
|
|
350
|
-
fig_fn=
|
|
464
|
+
fig_fn=fig_fn,
|
|
351
465
|
**ctx_kwargs,
|
|
352
466
|
)
|
|
467
|
+
sections.append(
|
|
468
|
+
ReportSection(
|
|
469
|
+
title="Geodiff vs. Reference DEM",
|
|
470
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
471
|
+
caption="Initial and final geodiff height differences compared to the reference DEM used in processing.",
|
|
472
|
+
)
|
|
473
|
+
)
|
|
353
474
|
except ValueError as e:
|
|
354
475
|
print(
|
|
355
476
|
f"\n\nSkipping geodiff plots (requires --mapproj-dem flag in bundle_adjust): {e}\n\n"
|
|
@@ -369,10 +490,11 @@ def main(
|
|
|
369
490
|
processing_parameters_dict = processing_parameters.from_log_files()
|
|
370
491
|
|
|
371
492
|
compile_report(
|
|
372
|
-
|
|
493
|
+
sections,
|
|
373
494
|
processing_parameters_dict,
|
|
374
495
|
report_pdf_path,
|
|
375
496
|
report_title=report_title,
|
|
497
|
+
report_metadata=report_metadata,
|
|
376
498
|
)
|
|
377
499
|
|
|
378
500
|
shutil.rmtree(plots_directory)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import textwrap
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from fpdf import FPDF
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ReportSection:
|
|
13
|
+
"""A section of the PDF report containing a figure with title and caption.
|
|
14
|
+
|
|
15
|
+
Attributes
|
|
16
|
+
----------
|
|
17
|
+
title : str
|
|
18
|
+
Section heading displayed above the figure.
|
|
19
|
+
image_path : str
|
|
20
|
+
Absolute path to the PNG image file.
|
|
21
|
+
caption : str
|
|
22
|
+
Caption text displayed below the figure.
|
|
23
|
+
figure_number : int
|
|
24
|
+
Auto-assigned by compile_report().
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
title: str
|
|
28
|
+
image_path: str
|
|
29
|
+
caption: str = ""
|
|
30
|
+
figure_number: int = 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ReportMetadata:
|
|
35
|
+
"""Metadata about the output DEM for the report title page.
|
|
36
|
+
|
|
37
|
+
Attributes
|
|
38
|
+
----------
|
|
39
|
+
dem_dimensions : tuple
|
|
40
|
+
(width, height) in pixels.
|
|
41
|
+
dem_gsd_m : float
|
|
42
|
+
Ground sample distance in meters.
|
|
43
|
+
dem_crs : str
|
|
44
|
+
Coordinate reference system string (e.g. "EPSG:32616").
|
|
45
|
+
dem_nodata_percent : float
|
|
46
|
+
Percentage of nodata pixels.
|
|
47
|
+
dem_elevation_range : tuple
|
|
48
|
+
(min, max) elevation in meters.
|
|
49
|
+
dem_filename : str
|
|
50
|
+
DEM filename.
|
|
51
|
+
reference_dem : str
|
|
52
|
+
Reference DEM path or description.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
dem_dimensions: tuple = (0, 0)
|
|
56
|
+
dem_gsd_m: float = 0.0
|
|
57
|
+
dem_crs: str = ""
|
|
58
|
+
dem_nodata_percent: float = 0.0
|
|
59
|
+
dem_elevation_range: tuple = (0, 0)
|
|
60
|
+
dem_filename: str = ""
|
|
61
|
+
reference_dem: str = ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ASPReportPDF(FPDF):
|
|
65
|
+
"""FPDF subclass with custom header and footer for ASP reports."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, report_title="ASP Output Quality Report"):
|
|
68
|
+
super().__init__(orientation="P", unit="mm", format="Letter")
|
|
69
|
+
self.report_title = report_title
|
|
70
|
+
self.set_auto_page_break(auto=True, margin=20)
|
|
71
|
+
self.set_margins(15, 15, 15)
|
|
72
|
+
|
|
73
|
+
def header(self):
|
|
74
|
+
if self.page_no() > 1:
|
|
75
|
+
self.set_font("Helvetica", "I", 8)
|
|
76
|
+
self.set_text_color(128, 128, 128)
|
|
77
|
+
self.cell(0, 6, self.report_title, align="L")
|
|
78
|
+
self.ln(2)
|
|
79
|
+
self.set_draw_color(200, 200, 200)
|
|
80
|
+
self.line(15, self.get_y(), self.w - 15, self.get_y())
|
|
81
|
+
self.ln(4)
|
|
82
|
+
self.set_text_color(0, 0, 0)
|
|
83
|
+
|
|
84
|
+
def footer(self):
|
|
85
|
+
self.set_y(-15)
|
|
86
|
+
self.set_font("Helvetica", "I", 8)
|
|
87
|
+
self.set_text_color(128, 128, 128)
|
|
88
|
+
self.cell(0, 10, f"Page {self.page_no()} of {{nb}}", align="C")
|
|
89
|
+
self.set_text_color(0, 0, 0)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def compile_report(
|
|
93
|
+
sections,
|
|
94
|
+
processing_parameters_dict,
|
|
95
|
+
report_pdf_path,
|
|
96
|
+
report_title="ASP Output Quality Report",
|
|
97
|
+
report_metadata=None,
|
|
98
|
+
):
|
|
99
|
+
"""
|
|
100
|
+
Compile a PDF report with ASP processing results and plots.
|
|
101
|
+
|
|
102
|
+
Creates a structured PDF report with a title page, figure sections
|
|
103
|
+
with captions, and a processing parameters appendix.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
sections : list of ReportSection
|
|
108
|
+
Ordered list of report sections, each containing a title,
|
|
109
|
+
image path, and optional caption.
|
|
110
|
+
processing_parameters_dict : dict
|
|
111
|
+
Dictionary containing processing parameters from ASP logs.
|
|
112
|
+
report_pdf_path : str
|
|
113
|
+
Output path for the PDF report.
|
|
114
|
+
report_title : str, optional
|
|
115
|
+
Title for the report. Default is "ASP Output Quality Report".
|
|
116
|
+
report_metadata : ReportMetadata, optional
|
|
117
|
+
DEM metadata for the title page summary table. Default is None.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
None
|
|
122
|
+
Generates a PDF report at the specified path.
|
|
123
|
+
|
|
124
|
+
Notes
|
|
125
|
+
-----
|
|
126
|
+
Required keys in processing_parameters_dict:
|
|
127
|
+
- processing_timestamp: When the processing was performed
|
|
128
|
+
- reference_dem: Path to reference DEM used
|
|
129
|
+
- bundle_adjust: Bundle adjustment command
|
|
130
|
+
- bundle_adjust_run_time: Time to run bundle adjustment
|
|
131
|
+
- stereo: Stereo command
|
|
132
|
+
- stereo_run_time: Time to run stereo
|
|
133
|
+
- point2dem: Point2dem command
|
|
134
|
+
- point2dem_run_time: Time to run point2dem
|
|
135
|
+
"""
|
|
136
|
+
pdf = ASPReportPDF(report_title=report_title)
|
|
137
|
+
pdf.alias_nb_pages()
|
|
138
|
+
|
|
139
|
+
# ---- Title page ----
|
|
140
|
+
pdf.add_page()
|
|
141
|
+
pdf.ln(30)
|
|
142
|
+
pdf.set_font("Helvetica", "B", 22)
|
|
143
|
+
pdf.multi_cell(0, 12, report_title, align="C")
|
|
144
|
+
pdf.ln(8)
|
|
145
|
+
pdf.set_font("Helvetica", "", 11)
|
|
146
|
+
processing_date = processing_parameters_dict.get("processing_timestamp", "")
|
|
147
|
+
pdf.cell(
|
|
148
|
+
0,
|
|
149
|
+
8,
|
|
150
|
+
f"Processed on: {processing_date}",
|
|
151
|
+
align="C",
|
|
152
|
+
new_x="LMARGIN",
|
|
153
|
+
new_y="NEXT",
|
|
154
|
+
)
|
|
155
|
+
pdf.ln(10)
|
|
156
|
+
|
|
157
|
+
if report_metadata is not None:
|
|
158
|
+
_add_metadata_table(pdf, report_metadata)
|
|
159
|
+
|
|
160
|
+
# ---- Figure sections ----
|
|
161
|
+
for i, section in enumerate(sections, start=1):
|
|
162
|
+
section.figure_number = i
|
|
163
|
+
if not os.path.exists(section.image_path):
|
|
164
|
+
logger.warning(f"Image not found, skipping: {section.image_path}")
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
pdf.add_page()
|
|
168
|
+
pdf.set_font("Helvetica", "B", 14)
|
|
169
|
+
pdf.cell(0, 10, section.title, new_x="LMARGIN", new_y="NEXT")
|
|
170
|
+
pdf.ln(2)
|
|
171
|
+
|
|
172
|
+
usable_width = pdf.w - pdf.l_margin - pdf.r_margin
|
|
173
|
+
pdf.image(section.image_path, x=pdf.l_margin, w=usable_width)
|
|
174
|
+
|
|
175
|
+
if section.caption:
|
|
176
|
+
pdf.ln(3)
|
|
177
|
+
pdf.set_font("Helvetica", "I", 9)
|
|
178
|
+
pdf.multi_cell(0, 5, f"Figure {section.figure_number}: {section.caption}")
|
|
179
|
+
|
|
180
|
+
# ---- Processing Parameters page ----
|
|
181
|
+
pdf.add_page()
|
|
182
|
+
pdf.set_font("Helvetica", "B", 16)
|
|
183
|
+
pdf.cell(0, 10, "Processing Parameters", new_x="LMARGIN", new_y="NEXT")
|
|
184
|
+
pdf.ln(4)
|
|
185
|
+
|
|
186
|
+
_add_runtime_table(pdf, processing_parameters_dict)
|
|
187
|
+
pdf.ln(6)
|
|
188
|
+
|
|
189
|
+
ref_dem = processing_parameters_dict.get("reference_dem", "")
|
|
190
|
+
if ref_dem:
|
|
191
|
+
pdf.set_font("Helvetica", "B", 10)
|
|
192
|
+
pdf.cell(0, 7, "Reference DEM:", new_x="LMARGIN", new_y="NEXT")
|
|
193
|
+
pdf.set_font("Courier", "", 7)
|
|
194
|
+
pdf.multi_cell(0, 4, ref_dem)
|
|
195
|
+
pdf.ln(4)
|
|
196
|
+
|
|
197
|
+
for key, label in [
|
|
198
|
+
("bundle_adjust", "Bundle Adjust"),
|
|
199
|
+
("stereo", "Stereo"),
|
|
200
|
+
("point2dem", "point2dem"),
|
|
201
|
+
]:
|
|
202
|
+
cmd = processing_parameters_dict.get(key, "")
|
|
203
|
+
if cmd:
|
|
204
|
+
pdf.set_font("Helvetica", "B", 10)
|
|
205
|
+
pdf.cell(0, 7, f"{label} Command:", new_x="LMARGIN", new_y="NEXT")
|
|
206
|
+
pdf.set_font("Courier", "", 7)
|
|
207
|
+
wrapped = textwrap.fill(cmd, width=120)
|
|
208
|
+
pdf.multi_cell(0, 4, wrapped)
|
|
209
|
+
pdf.ln(4)
|
|
210
|
+
|
|
211
|
+
pdf.output(report_pdf_path)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _add_metadata_table(pdf, metadata):
|
|
215
|
+
"""Add DEM metadata summary table to the PDF.
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
pdf : ASPReportPDF
|
|
220
|
+
The PDF document.
|
|
221
|
+
metadata : ReportMetadata
|
|
222
|
+
DEM metadata to display.
|
|
223
|
+
"""
|
|
224
|
+
pdf.set_font("Helvetica", "B", 12)
|
|
225
|
+
pdf.cell(0, 10, "DEM Summary", align="C", new_x="LMARGIN", new_y="NEXT")
|
|
226
|
+
pdf.ln(2)
|
|
227
|
+
|
|
228
|
+
rows = []
|
|
229
|
+
if metadata.dem_filename:
|
|
230
|
+
rows.append(("DEM File", metadata.dem_filename))
|
|
231
|
+
if metadata.dem_dimensions != (0, 0):
|
|
232
|
+
w, h = metadata.dem_dimensions
|
|
233
|
+
rows.append(("Dimensions (px)", f"{w} x {h}"))
|
|
234
|
+
if metadata.dem_gsd_m:
|
|
235
|
+
rows.append(("GSD (m)", f"{metadata.dem_gsd_m:.2f}"))
|
|
236
|
+
if metadata.dem_crs:
|
|
237
|
+
rows.append(("CRS", metadata.dem_crs))
|
|
238
|
+
if metadata.dem_nodata_percent:
|
|
239
|
+
rows.append(("Nodata (%)", f"{metadata.dem_nodata_percent:.1f}"))
|
|
240
|
+
if metadata.dem_elevation_range != (0, 0):
|
|
241
|
+
lo, hi = metadata.dem_elevation_range
|
|
242
|
+
rows.append(("Elevation Range (m)", f"{lo:.1f} to {hi:.1f}"))
|
|
243
|
+
if metadata.reference_dem:
|
|
244
|
+
rows.append(("Reference DEM", metadata.reference_dem))
|
|
245
|
+
|
|
246
|
+
if not rows:
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
col_w = (pdf.w - pdf.l_margin - pdf.r_margin) / 2
|
|
250
|
+
table_x = pdf.l_margin
|
|
251
|
+
|
|
252
|
+
pdf.set_font("Helvetica", "B", 9)
|
|
253
|
+
pdf.set_fill_color(220, 220, 220)
|
|
254
|
+
pdf.set_x(table_x)
|
|
255
|
+
pdf.cell(col_w, 7, "Property", border=1, fill=True)
|
|
256
|
+
pdf.cell(col_w, 7, "Value", border=1, fill=True, new_x="LMARGIN", new_y="NEXT")
|
|
257
|
+
|
|
258
|
+
pdf.set_font("Helvetica", "", 9)
|
|
259
|
+
for prop, val in rows:
|
|
260
|
+
pdf.set_x(table_x)
|
|
261
|
+
pdf.cell(col_w, 7, prop, border=1)
|
|
262
|
+
pdf.cell(col_w, 7, str(val), border=1, new_x="LMARGIN", new_y="NEXT")
|
|
263
|
+
|
|
264
|
+
pdf.ln(4)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _add_runtime_table(pdf, params):
|
|
268
|
+
"""Add runtime summary table to the PDF.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
pdf : ASPReportPDF
|
|
273
|
+
The PDF document.
|
|
274
|
+
params : dict
|
|
275
|
+
Processing parameters dictionary.
|
|
276
|
+
"""
|
|
277
|
+
pdf.set_font("Helvetica", "B", 11)
|
|
278
|
+
pdf.cell(0, 8, "Runtime Summary", new_x="LMARGIN", new_y="NEXT")
|
|
279
|
+
pdf.ln(2)
|
|
280
|
+
|
|
281
|
+
col_w = (pdf.w - pdf.l_margin - pdf.r_margin) / 2
|
|
282
|
+
|
|
283
|
+
pdf.set_font("Helvetica", "B", 9)
|
|
284
|
+
pdf.set_fill_color(220, 220, 220)
|
|
285
|
+
pdf.cell(col_w, 7, "Step", border=1, fill=True)
|
|
286
|
+
pdf.cell(col_w, 7, "Runtime", border=1, fill=True, new_x="LMARGIN", new_y="NEXT")
|
|
287
|
+
|
|
288
|
+
pdf.set_font("Helvetica", "", 9)
|
|
289
|
+
for key, label in [
|
|
290
|
+
("bundle_adjust_run_time", "Bundle Adjust"),
|
|
291
|
+
("stereo_run_time", "Stereo"),
|
|
292
|
+
("point2dem_run_time", "point2dem"),
|
|
293
|
+
]:
|
|
294
|
+
runtime = params.get(key, "N/A")
|
|
295
|
+
pdf.cell(col_w, 7, label, border=1)
|
|
296
|
+
pdf.cell(col_w, 7, str(runtime), border=1, new_x="LMARGIN", new_y="NEXT")
|
|
@@ -11,7 +11,6 @@ import matplotlib.pyplot as plt
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import rasterio as rio
|
|
13
13
|
import rioxarray
|
|
14
|
-
from markdown_pdf import MarkdownPdf, Section
|
|
15
14
|
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
16
15
|
from osgeo import gdal
|
|
17
16
|
from rasterio.errors import NotGeoreferencedWarning
|
|
@@ -134,99 +133,6 @@ def save_figure(fig, save_dir=None, fig_fn=None, dpi=150):
|
|
|
134
133
|
raise ValueError("\n\nPlease provide a save directory and figure filename.\n\n")
|
|
135
134
|
|
|
136
135
|
|
|
137
|
-
def compile_report(
|
|
138
|
-
plots_directory, processing_parameters_dict, report_pdf_path, report_title=None
|
|
139
|
-
):
|
|
140
|
-
"""
|
|
141
|
-
Compile a PDF report with ASP processing results and plots.
|
|
142
|
-
|
|
143
|
-
Creates a structured PDF report containing processing parameters and
|
|
144
|
-
generated plots from ASP processing. The plots are converted from PNG
|
|
145
|
-
to JPEG for better compression in the PDF.
|
|
146
|
-
|
|
147
|
-
Parameters
|
|
148
|
-
----------
|
|
149
|
-
plots_directory : str
|
|
150
|
-
Directory containing plot files (PNG format)
|
|
151
|
-
processing_parameters_dict : dict
|
|
152
|
-
Dictionary containing processing parameters from ASP logs
|
|
153
|
-
report_pdf_path : str
|
|
154
|
-
Output path for the PDF report
|
|
155
|
-
report_title : str, optional
|
|
156
|
-
Title for the report. If None, uses the parent directory name
|
|
157
|
-
|
|
158
|
-
Returns
|
|
159
|
-
-------
|
|
160
|
-
None
|
|
161
|
-
Generates a PDF report at the specified path
|
|
162
|
-
|
|
163
|
-
Notes
|
|
164
|
-
-----
|
|
165
|
-
Required keys in processing_parameters_dict:
|
|
166
|
-
- processing_timestamp: When the processing was performed
|
|
167
|
-
- reference_dem: Path to reference DEM used
|
|
168
|
-
- bundle_adjust: Bundle adjustment command
|
|
169
|
-
- bundle_adjust_run_time: Time to run bundle adjustment
|
|
170
|
-
- stereo: Stereo command
|
|
171
|
-
- stereo_run_time: Time to run stereo
|
|
172
|
-
- point2dem: Point2dem command
|
|
173
|
-
- point2dem_run_time: Time to run point2dem
|
|
174
|
-
|
|
175
|
-
The function converts PNG files to temporary JPG files for the report,
|
|
176
|
-
then deletes the temporary files afterward.
|
|
177
|
-
"""
|
|
178
|
-
from PIL import Image
|
|
179
|
-
|
|
180
|
-
files = [f for f in os.listdir(plots_directory) if f.endswith(".png")]
|
|
181
|
-
files.sort()
|
|
182
|
-
|
|
183
|
-
# Convert .png files to .jpg with 95% quality
|
|
184
|
-
compressed_files = []
|
|
185
|
-
for file in files:
|
|
186
|
-
png_path = os.path.join(plots_directory, file)
|
|
187
|
-
jpg_file = file.replace(".png", ".jpg")
|
|
188
|
-
jpg_path = os.path.join(plots_directory, jpg_file)
|
|
189
|
-
|
|
190
|
-
with Image.open(png_path) as img:
|
|
191
|
-
img = img.convert("RGB")
|
|
192
|
-
img.save(jpg_path, "JPEG", quality=95)
|
|
193
|
-
|
|
194
|
-
compressed_files.append(jpg_file)
|
|
195
|
-
|
|
196
|
-
processing_date = processing_parameters_dict["processing_timestamp"]
|
|
197
|
-
|
|
198
|
-
if report_title is None:
|
|
199
|
-
report_title = os.path.basename(os.path.dirname(report_pdf_path))
|
|
200
|
-
|
|
201
|
-
report_title = (
|
|
202
|
-
f"# ASP Report\n\n## {report_title:}\n\nProcessed on: {processing_date:}"
|
|
203
|
-
)
|
|
204
|
-
reference_dem_string = (
|
|
205
|
-
f"### Reference DEM:\n\n`{processing_parameters_dict['reference_dem']:}`"
|
|
206
|
-
)
|
|
207
|
-
ba_string = f"### Bundle Adjust ({processing_parameters_dict['bundle_adjust_run_time']:}):\n\n`{processing_parameters_dict['bundle_adjust']:}`"
|
|
208
|
-
stereo_string = f"### Stereo ({processing_parameters_dict['stereo_run_time']:}):\n\n`{processing_parameters_dict['stereo']:}`"
|
|
209
|
-
point2dem_string = f"### point2dem ({processing_parameters_dict['point2dem_run_time']}):\n\n`{processing_parameters_dict['point2dem']:}`"
|
|
210
|
-
|
|
211
|
-
pdf = MarkdownPdf()
|
|
212
|
-
|
|
213
|
-
pdf.add_section(Section(f"{report_title:}\n\n"))
|
|
214
|
-
pdf.add_section(
|
|
215
|
-
Section(
|
|
216
|
-
f"## Processing Parameters\n\n{reference_dem_string:}\n\n{ba_string:}\n\n{stereo_string}\n\n{point2dem_string}\n\n"
|
|
217
|
-
)
|
|
218
|
-
)
|
|
219
|
-
plots = "".join([f"\n\n" for file in compressed_files])
|
|
220
|
-
pdf.add_section(Section(f"## Plots\n\n{plots:}", root=plots_directory))
|
|
221
|
-
|
|
222
|
-
pdf.save(report_pdf_path)
|
|
223
|
-
|
|
224
|
-
# cleanup temporary JPEG files
|
|
225
|
-
for file in compressed_files:
|
|
226
|
-
jpg_path = os.path.join(plots_directory, file)
|
|
227
|
-
os.remove(jpg_path)
|
|
228
|
-
|
|
229
|
-
|
|
230
136
|
def get_xml_tag(xml, tag, all=False):
|
|
231
137
|
"""
|
|
232
138
|
Extract value(s) from XML tag(s).
|
|
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
|