asp-plot 3.2.0__py3-none-any.whl → 3.4.0__py3-none-any.whl
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/alignment.py +13 -2
- asp_plot/altimetry_source.py +30 -6
- asp_plot/cli/asp_report.py +11 -0
- asp_plot/cli/dem_benchmark.py +181 -0
- asp_plot/dem_benchmark.py +1376 -0
- asp_plot/icesat2_source.py +77 -39
- asp_plot/planetary_source.py +11 -2
- asp_plot/report.py +72 -6
- asp_plot/report_captions.py +2 -2
- asp_plot/report_pipeline.py +5 -0
- asp_plot/stereo.py +110 -39
- {asp_plot-3.2.0.dist-info → asp_plot-3.4.0.dist-info}/METADATA +1 -1
- {asp_plot-3.2.0.dist-info → asp_plot-3.4.0.dist-info}/RECORD +16 -14
- {asp_plot-3.2.0.dist-info → asp_plot-3.4.0.dist-info}/WHEEL +1 -1
- {asp_plot-3.2.0.dist-info → asp_plot-3.4.0.dist-info}/entry_points.txt +1 -0
- {asp_plot-3.2.0.dist-info → asp_plot-3.4.0.dist-info}/licenses/LICENSE +0 -0
asp_plot/alignment.py
CHANGED
|
@@ -386,7 +386,7 @@ class Alignment:
|
|
|
386
386
|
|
|
387
387
|
return report
|
|
388
388
|
|
|
389
|
-
def apply_dem_translation(self, output_prefix="pc_align/pc_align"):
|
|
389
|
+
def apply_dem_translation(self, output_prefix="pc_align/pc_align", output_fn=None):
|
|
390
390
|
"""
|
|
391
391
|
Apply the pc_align translation to the DEM.
|
|
392
392
|
|
|
@@ -398,6 +398,13 @@ class Alignment:
|
|
|
398
398
|
----------
|
|
399
399
|
output_prefix : str, optional
|
|
400
400
|
Prefix for pc_align output files, default is "pc_align/pc_align"
|
|
401
|
+
output_fn : str, optional
|
|
402
|
+
Where to write the translated DEM. Default is None, which writes
|
|
403
|
+
``<dem_fn without .tif>_pc_align_translated.tif`` next to the
|
|
404
|
+
source DEM (the report convention). Pass an explicit path to
|
|
405
|
+
keep the translated copy elsewhere, e.g. when many candidate
|
|
406
|
+
DEMs are aligned side by side and their folders should stay
|
|
407
|
+
untouched (:class:`asp_plot.dem_benchmark.DEMBenchmark`).
|
|
401
408
|
|
|
402
409
|
Returns
|
|
403
410
|
-------
|
|
@@ -479,7 +486,11 @@ class Alignment:
|
|
|
479
486
|
t_srs.ImportFromWkt(src.ds.crs.to_wkt())
|
|
480
487
|
proj_shift = self.get_proj_shift(src_c, src_shift, s_srs, t_srs, inv_trans=True)
|
|
481
488
|
|
|
482
|
-
|
|
489
|
+
if output_fn is None:
|
|
490
|
+
aligned_dem_fn = self.dem_fn.replace(".tif", "_pc_align_translated.tif")
|
|
491
|
+
else:
|
|
492
|
+
aligned_dem_fn = output_fn
|
|
493
|
+
os.makedirs(os.path.dirname(os.path.abspath(aligned_dem_fn)), exist_ok=True)
|
|
483
494
|
print(f"\nWriting out: {aligned_dem_fn}\n")
|
|
484
495
|
|
|
485
496
|
gdal_opt = ["COMPRESS=LZW", "TILED=YES", "PREDICTOR=3", "BIGTIFF=IF_SAFER"]
|
asp_plot/altimetry_source.py
CHANGED
|
@@ -20,6 +20,11 @@ import numpy as np
|
|
|
20
20
|
import rioxarray
|
|
21
21
|
import xarray as xr
|
|
22
22
|
|
|
23
|
+
# A residual this many NMADs from the median is not a DEM error but a blunder in
|
|
24
|
+
# the altimetry (cloud returns, a bad fit); dropped before the n_sigma × std cut
|
|
25
|
+
# so that a large cluster of them cannot inflate the std and defeat it.
|
|
26
|
+
GROSS_OUTLIER_NMAD = 30
|
|
27
|
+
|
|
23
28
|
|
|
24
29
|
class AltimetrySource:
|
|
25
30
|
"""Base for ICESat-2 and planetary altimetry sources.
|
|
@@ -66,13 +71,26 @@ class AltimetrySource:
|
|
|
66
71
|
return rioxarray.open_rasterio(dem_fn, masked=True).squeeze()
|
|
67
72
|
|
|
68
73
|
@staticmethod
|
|
69
|
-
def
|
|
70
|
-
"""Boolean mask keeping dh values within ``n_sigma`` × std of the mean
|
|
74
|
+
def _outlier_mask(dh, n_sigma):
|
|
75
|
+
"""Boolean mask keeping dh values within ``n_sigma`` × std of the mean,
|
|
76
|
+
after dropping gross outliers.
|
|
77
|
+
|
|
78
|
+
Two cuts. First, anything farther than ``GROSS_OUTLIER_NMAD`` (30)
|
|
79
|
+
normalized median absolute deviations from the median is dropped: no
|
|
80
|
+
DEM error puts a point 30 NMADs out, but a cloud return does — one
|
|
81
|
+
ICESat-2 pass of marine-layer cloud 150–200 m above the ground was a
|
|
82
|
+
fifth of the sample over a coastal site, and a plain mean/std cut,
|
|
83
|
+
its std inflated to ~70 m by that cluster, removed nothing and left
|
|
84
|
+
every DEM with an RMSE near 85 m. Then the usual ``n_sigma`` × std cut
|
|
85
|
+
about the mean of what survives, which is the cut every report has
|
|
86
|
+
always applied and, absent gross contamination, gives the same result
|
|
87
|
+
as before (the gross cut touches at most a few dozen points of the
|
|
88
|
+
~7000 over Atlanta).
|
|
71
89
|
|
|
72
90
|
Rows whose ``dh`` is NaN are kept (they carry no difference yet and
|
|
73
91
|
must not be dropped). Returns ``None`` — signalling that no filtering
|
|
74
92
|
should occur — when there are no finite values or the spread is
|
|
75
|
-
degenerate (zero or non-finite
|
|
93
|
+
degenerate (zero or non-finite scale).
|
|
76
94
|
|
|
77
95
|
Parameters
|
|
78
96
|
----------
|
|
@@ -84,11 +102,17 @@ class AltimetrySource:
|
|
|
84
102
|
valid = dh.dropna().values
|
|
85
103
|
if valid.size == 0:
|
|
86
104
|
return None
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
median_val = np.median(valid)
|
|
106
|
+
nmad_val = 1.4826 * np.median(np.abs(valid - median_val))
|
|
107
|
+
if nmad_val == 0 or np.isnan(nmad_val):
|
|
108
|
+
return None
|
|
109
|
+
gross = (dh - median_val).abs() <= GROSS_OUTLIER_NMAD * nmad_val
|
|
110
|
+
core = dh[gross].dropna().values
|
|
111
|
+
mean_val = np.mean(core)
|
|
112
|
+
std_val = np.std(core)
|
|
89
113
|
if std_val == 0 or np.isnan(std_val):
|
|
90
114
|
return None
|
|
91
|
-
mask = (dh - mean_val).abs() <= n_sigma * std_val
|
|
115
|
+
mask = gross & ((dh - mean_val).abs() <= n_sigma * std_val)
|
|
92
116
|
return mask | dh.isna()
|
|
93
117
|
|
|
94
118
|
def _write_csv_to_directory(self, df, filename):
|
asp_plot/cli/asp_report.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
|
+
from asp_plot.report import FIGURE_MAX_DPI
|
|
3
4
|
from asp_plot.report_pipeline import ReportConfig, run_report
|
|
4
5
|
|
|
5
6
|
|
|
@@ -128,6 +129,14 @@ def _reconstruct_command():
|
|
|
128
129
|
default=None,
|
|
129
130
|
help="Title for the report. Default: Directory name of ASP processing.",
|
|
130
131
|
)
|
|
132
|
+
@click.option(
|
|
133
|
+
"--figure-max-dpi",
|
|
134
|
+
prompt=False,
|
|
135
|
+
default=FIGURE_MAX_DPI,
|
|
136
|
+
show_default=True,
|
|
137
|
+
type=int,
|
|
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
|
+
)
|
|
131
140
|
def main(
|
|
132
141
|
directory,
|
|
133
142
|
bundle_adjust_prefix,
|
|
@@ -146,6 +155,7 @@ def main(
|
|
|
146
155
|
reuse_selections,
|
|
147
156
|
report_filename,
|
|
148
157
|
report_title,
|
|
158
|
+
figure_max_dpi,
|
|
149
159
|
):
|
|
150
160
|
"""
|
|
151
161
|
Generate a comprehensive report of ASP processing results.
|
|
@@ -172,6 +182,7 @@ def main(
|
|
|
172
182
|
reuse_selections=reuse_selections,
|
|
173
183
|
report_filename=report_filename,
|
|
174
184
|
report_title=report_title,
|
|
185
|
+
figure_max_dpi=figure_max_dpi,
|
|
175
186
|
report_command=_reconstruct_command(),
|
|
176
187
|
)
|
|
177
188
|
run_report(config)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from asp_plot.dem_benchmark import DEMBenchmark
|
|
6
|
+
from asp_plot.icesat2_source import WORLDCOVER_GROUPS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command()
|
|
10
|
+
@click.argument("dems", nargs=-1, required=True)
|
|
11
|
+
@click.option(
|
|
12
|
+
"--parquet",
|
|
13
|
+
prompt=False,
|
|
14
|
+
default=None,
|
|
15
|
+
help="ICESat-2 ATL06-SR parquet cache to score Earth DEMs against (the "
|
|
16
|
+
"atl06sr_all.parquet a previous asp_report run wrote next to its report, "
|
|
17
|
+
"or from Altimetry.request_atl06sr_multi_processing(save_to_parquet=True)). "
|
|
18
|
+
"The same points are replayed for every DEM; no SlideRule request is made.",
|
|
19
|
+
)
|
|
20
|
+
@click.option(
|
|
21
|
+
"--altimetry-csv",
|
|
22
|
+
prompt=False,
|
|
23
|
+
default=None,
|
|
24
|
+
help="LOLA/MOLA CSV to score Moon/Mars DEMs against (see "
|
|
25
|
+
"request_planetary_altimetry). Use instead of --parquet for planetary DEMs.",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--directory",
|
|
29
|
+
prompt=False,
|
|
30
|
+
default="./",
|
|
31
|
+
help="Working directory. pc_align products and the translated DEM copies go "
|
|
32
|
+
"under <directory>/dem_benchmark/<label>/, never into the DEMs' own folders. "
|
|
33
|
+
"Default: current directory.",
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--reference",
|
|
37
|
+
prompt=False,
|
|
38
|
+
default=None,
|
|
39
|
+
help="Label of one of the DEMs to difference the others against (vs_ref "
|
|
40
|
+
"columns of the stats table). Default: none.",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--no-pc-align",
|
|
44
|
+
is_flag=True,
|
|
45
|
+
default=False,
|
|
46
|
+
help="Skip the per-DEM pc_align translation; report pre-alignment residuals only.",
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--own-extent",
|
|
50
|
+
is_flag=True,
|
|
51
|
+
default=False,
|
|
52
|
+
help="Compute coverage and triangulation-error statistics over each DEM's "
|
|
53
|
+
"own extent instead of the intersection of all DEM footprints.",
|
|
54
|
+
)
|
|
55
|
+
@click.option(
|
|
56
|
+
"--filter-out",
|
|
57
|
+
prompt=False,
|
|
58
|
+
multiple=True,
|
|
59
|
+
default=("water",),
|
|
60
|
+
type=click.Choice(sorted(WORLDCOVER_GROUPS) + ["none"]),
|
|
61
|
+
help="ESA WorldCover group to drop from the ICESat-2 points before "
|
|
62
|
+
"differencing; repeat the option for several. Giving the option replaces "
|
|
63
|
+
"the default, so name water as well to keep dropping it: '--filter-out "
|
|
64
|
+
"water --filter-out trees' is the stable-terrain choice when years "
|
|
65
|
+
"separate the imagery from the ICESat-2 passes. 'none' keeps every return "
|
|
66
|
+
"and cannot be combined with a group. Earth DEMs only. Default: water.",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--n-bootstrap",
|
|
70
|
+
prompt=False,
|
|
71
|
+
default=1000,
|
|
72
|
+
type=int,
|
|
73
|
+
help="Replicates of the paired block bootstrap that puts 95 % intervals on "
|
|
74
|
+
"the shared-point median and NMAD of every DEM and on its NMAD difference "
|
|
75
|
+
"to the best DEM (whole ICESat-2 beam tracks, or MOLA orbits, are "
|
|
76
|
+
"resampled). 0 skips it. Default: 1000.",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--title",
|
|
80
|
+
prompt=False,
|
|
81
|
+
default=None,
|
|
82
|
+
help="Figure title. Default: none.",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--output-directory",
|
|
86
|
+
prompt=False,
|
|
87
|
+
default=None,
|
|
88
|
+
help="Directory for the figure and stats CSV. Default: --directory.",
|
|
89
|
+
)
|
|
90
|
+
@click.option(
|
|
91
|
+
"--output-filename",
|
|
92
|
+
prompt=False,
|
|
93
|
+
default="dem_benchmark.png",
|
|
94
|
+
help="Figure filename; the stats CSV takes the same name with a .csv "
|
|
95
|
+
"extension, and the residual histogram figure a _histogram suffix. "
|
|
96
|
+
"Default: dem_benchmark.png.",
|
|
97
|
+
)
|
|
98
|
+
def main(
|
|
99
|
+
dems,
|
|
100
|
+
parquet,
|
|
101
|
+
altimetry_csv,
|
|
102
|
+
directory,
|
|
103
|
+
reference,
|
|
104
|
+
no_pc_align,
|
|
105
|
+
own_extent,
|
|
106
|
+
filter_out,
|
|
107
|
+
n_bootstrap,
|
|
108
|
+
title,
|
|
109
|
+
output_directory,
|
|
110
|
+
output_filename,
|
|
111
|
+
):
|
|
112
|
+
"""
|
|
113
|
+
Score many DEMs against one altimetry sample.
|
|
114
|
+
|
|
115
|
+
DEMS are paths, optionally labelled as LABEL=PATH (e.g.
|
|
116
|
+
"MVS=stereo_mvs3/run-DEM.tif"); an unlabelled ASP run-DEM.tif is labelled
|
|
117
|
+
by its folder. Every DEM gets: coverage inside the common footprint, the
|
|
118
|
+
median triangulation error from its IntersectionErr raster when present
|
|
119
|
+
(table only; it is a consistency check, not a quality ranking), and the
|
|
120
|
+
altimetry-minus-DEM median / NMAD / RMSE before and (unless
|
|
121
|
+
--no-pc-align) after a pc_align translation, on its own points and on the
|
|
122
|
+
points valid in every DEM, with bootstrap intervals on the latter. Writes
|
|
123
|
+
a one-row-per-DEM summary figure, an overlaid residual histogram, and the
|
|
124
|
+
stats table as CSV.
|
|
125
|
+
"""
|
|
126
|
+
if "none" in filter_out and len(set(filter_out)) > 1:
|
|
127
|
+
raise click.UsageError(
|
|
128
|
+
"--filter-out none keeps every return and cannot be combined with "
|
|
129
|
+
"a land-cover group."
|
|
130
|
+
)
|
|
131
|
+
directory = os.path.expanduser(directory)
|
|
132
|
+
if output_directory is None:
|
|
133
|
+
output_directory = directory
|
|
134
|
+
output_directory = os.path.expanduser(output_directory)
|
|
135
|
+
|
|
136
|
+
bench = DEMBenchmark(
|
|
137
|
+
directory=directory,
|
|
138
|
+
dems=list(dems),
|
|
139
|
+
parquet=parquet,
|
|
140
|
+
altimetry_csv=altimetry_csv,
|
|
141
|
+
reference=reference,
|
|
142
|
+
aoi=None if own_extent else "intersection",
|
|
143
|
+
filter_out=[g for g in filter_out if g != "none"] or None,
|
|
144
|
+
n_bootstrap=n_bootstrap,
|
|
145
|
+
title=title,
|
|
146
|
+
)
|
|
147
|
+
stats = bench.run(pc_align=not no_pc_align)
|
|
148
|
+
|
|
149
|
+
stem = os.path.splitext(output_filename)[0]
|
|
150
|
+
csv_fn = bench.save_stats(os.path.join(output_directory, f"{stem}.csv"))
|
|
151
|
+
bench.summary_plot(save_dir=output_directory, fig_fn=output_filename)
|
|
152
|
+
bench.histogram_plot(save_dir=output_directory, fig_fn=f"{stem}_histogram.png")
|
|
153
|
+
|
|
154
|
+
# The headline columns; the CSV has every column of STATS_COLUMNS.
|
|
155
|
+
shown = stats[
|
|
156
|
+
[
|
|
157
|
+
"label",
|
|
158
|
+
"valid_pct",
|
|
159
|
+
"ie_median_m",
|
|
160
|
+
"n_points",
|
|
161
|
+
"n_shared",
|
|
162
|
+
"dh_shared_median_m",
|
|
163
|
+
"dh_shared_nmad_m",
|
|
164
|
+
"dh_aligned_shared_median_m",
|
|
165
|
+
"dh_aligned_shared_nmad_m",
|
|
166
|
+
"nmad_ci_low_m",
|
|
167
|
+
"nmad_ci_high_m",
|
|
168
|
+
"p_best",
|
|
169
|
+
]
|
|
170
|
+
]
|
|
171
|
+
shown = shown.dropna(axis=1, how="all")
|
|
172
|
+
print("\n" + shown.to_string(index=False, float_format=lambda v: f"{v:.2f}"))
|
|
173
|
+
print(
|
|
174
|
+
f"\nSummary figure: {os.path.join(output_directory, output_filename)}"
|
|
175
|
+
f"\nHistogram: {os.path.join(output_directory, stem + '_histogram.png')}"
|
|
176
|
+
f"\nStats table: {csv_fn}\n"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
main()
|