asp-plot 0.1.0__tar.gz → 0.2.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-0.1.0 → asp_plot-0.2.0}/PKG-INFO +7 -1
- {asp_plot-0.1.0 → asp_plot-0.2.0}/README.md +6 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/__init__.py +1 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/bundle_adjust.py +51 -18
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/cli/asp_plot.py +47 -29
- asp_plot-0.2.0/asp_plot/scenes.py +216 -0
- asp_plot-0.2.0/asp_plot/stereopair_metadata_parser.py +330 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/utils.py +20 -13
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/PKG-INFO +7 -1
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/SOURCES.txt +1 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/pyproject.toml +1 -1
- {asp_plot-0.1.0 → asp_plot-0.2.0}/setup.py +1 -1
- {asp_plot-0.1.0 → asp_plot-0.2.0}/tests/__init__.py +1 -0
- asp_plot-0.2.0/tests/test_bundle_adjust.py +40 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/tests/test_imports.py +4 -2
- {asp_plot-0.1.0 → asp_plot-0.2.0}/tests/test_scenes.py +14 -1
- asp_plot-0.1.0/asp_plot/scenes.py +0 -68
- asp_plot-0.1.0/tests/test_bundle_adjust.py +0 -35
- {asp_plot-0.1.0 → asp_plot-0.2.0}/LICENSE +0 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/cli/__init__.py +0 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/processing_parameters.py +0 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/stereo.py +0 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/dependency_links.txt +0 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/entry_points.txt +0 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/top_level.txt +0 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/setup.cfg +0 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/tests/test_processing_parameters.py +0 -0
- {asp_plot-0.1.0 → asp_plot-0.2.0}/tests/test_stereo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: asp_plot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Package for plotting outputs Ames Stereo Pipeline processing
|
|
5
5
|
Author: Ben Purinton
|
|
6
6
|
Author-email: Ben Purinton <purinton@uw.edu>
|
|
@@ -54,6 +54,12 @@ $ pytest
|
|
|
54
54
|
|
|
55
55
|
### Package and upload
|
|
56
56
|
|
|
57
|
+
```
|
|
58
|
+
$ rm -rf dist/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then update version in `pyproject.toml` and `setup.py`, then:
|
|
62
|
+
|
|
57
63
|
```
|
|
58
64
|
$ python3 -m pip install --upgrade build
|
|
59
65
|
$ python3 -m build
|
|
@@ -8,30 +8,31 @@ import contextily as ctx
|
|
|
8
8
|
from asp_plot.utils import ColorBar, Plotter, save_figure
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class
|
|
11
|
+
class ReadBundleAdjustFiles:
|
|
12
12
|
def __init__(self, directory, bundle_adjust_directory):
|
|
13
13
|
self.directory = directory
|
|
14
14
|
self.bundle_adjust_directory = bundle_adjust_directory
|
|
15
15
|
|
|
16
|
-
def
|
|
16
|
+
def get_csv_paths(self, geodiff_files=False):
|
|
17
17
|
filenames = [
|
|
18
18
|
"*-initial_residuals_pointmap.csv",
|
|
19
19
|
"*-final_residuals_pointmap.csv",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
+
if geodiff_files:
|
|
23
|
+
filenames = [f.replace(".csv", "-diff.csv") for f in filenames]
|
|
24
|
+
|
|
22
25
|
paths = [
|
|
23
|
-
glob.glob(
|
|
24
|
-
|
|
25
|
-
)[0]
|
|
26
|
-
for filename in filenames
|
|
26
|
+
glob.glob(os.path.join(self.directory, self.bundle_adjust_directory, f))[0]
|
|
27
|
+
for f in filenames
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
for path in paths:
|
|
30
31
|
if not os.path.isfile(path):
|
|
31
|
-
raise ValueError(f"
|
|
32
|
+
raise ValueError(f"CSV file not found: {path}")
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
return
|
|
34
|
+
initial, final = paths
|
|
35
|
+
return initial, final
|
|
35
36
|
|
|
36
37
|
def get_residuals_gdf(self, csv_path):
|
|
37
38
|
cols = [
|
|
@@ -66,11 +67,38 @@ class ReadResiduals:
|
|
|
66
67
|
resid_gdf.filename = os.path.basename(csv_path)
|
|
67
68
|
return resid_gdf
|
|
68
69
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
def get_geodiff_gdf(self, csv_path):
|
|
71
|
+
cols = [
|
|
72
|
+
"lon",
|
|
73
|
+
"lat",
|
|
74
|
+
"height_diff_meters",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
geodiff_df = pd.read_csv(csv_path, skiprows=7, names=cols)
|
|
78
|
+
|
|
79
|
+
geodiff_gdf = gpd.GeoDataFrame(
|
|
80
|
+
geodiff_df,
|
|
81
|
+
geometry=gpd.points_from_xy(
|
|
82
|
+
geodiff_df["lon"], geodiff_df["lat"], crs="EPSG:4326"
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
geodiff_gdf.filename = os.path.basename(csv_path)
|
|
87
|
+
return geodiff_gdf
|
|
88
|
+
|
|
89
|
+
def get_initial_final_residuals_gdfs(self):
|
|
90
|
+
resid_initial_path, resid_final_path = self.get_csv_paths()
|
|
91
|
+
resid_initial_gdf = self.get_residuals_gdf(resid_initial_path)
|
|
72
92
|
resid_final_gdf = self.get_residuals_gdf(resid_final_path)
|
|
73
|
-
return
|
|
93
|
+
return resid_initial_gdf, resid_final_gdf
|
|
94
|
+
|
|
95
|
+
def get_initial_final_geodiff_gdfs(self):
|
|
96
|
+
geodiff_initial_path, geodiff_final_path = self.get_csv_paths(
|
|
97
|
+
geodiff_files=True
|
|
98
|
+
)
|
|
99
|
+
geodiff_initial_gdf = self.get_geodiff_gdf(geodiff_initial_path)
|
|
100
|
+
geodiff_final_gdf = self.get_geodiff_gdf(geodiff_final_path)
|
|
101
|
+
return geodiff_initial_gdf, geodiff_final_gdf
|
|
74
102
|
|
|
75
103
|
def get_mapproj_residuals_gdf(self):
|
|
76
104
|
path = glob.glob(
|
|
@@ -123,18 +151,18 @@ class ReadResiduals:
|
|
|
123
151
|
return resid_triangulation_uncert_df
|
|
124
152
|
|
|
125
153
|
|
|
126
|
-
class
|
|
154
|
+
class PlotBundleAdjustFiles(Plotter):
|
|
127
155
|
def __init__(self, geodataframes, **kwargs):
|
|
128
156
|
super().__init__(**kwargs)
|
|
129
157
|
if not isinstance(geodataframes, list):
|
|
130
158
|
raise ValueError("Input must be a list of GeoDataFrames")
|
|
131
159
|
self.geodataframes = geodataframes
|
|
132
160
|
|
|
133
|
-
def
|
|
161
|
+
def gdf_percentile_stats(self, gdf, column_name="mean_residual"):
|
|
134
162
|
stats = gdf[column_name].quantile([0.25, 0.50, 0.84, 0.95]).round(2).tolist()
|
|
135
163
|
return stats
|
|
136
164
|
|
|
137
|
-
def
|
|
165
|
+
def plot_n_gdfs(
|
|
138
166
|
self,
|
|
139
167
|
column_name="mean_residual",
|
|
140
168
|
cbar_label="Mean Residual (m)",
|
|
@@ -175,10 +203,15 @@ class PlotResiduals(Plotter):
|
|
|
175
203
|
clim=clim,
|
|
176
204
|
column_name=column_name,
|
|
177
205
|
cbar_label=cbar_label,
|
|
206
|
+
cmap=cmap,
|
|
178
207
|
)
|
|
179
208
|
else:
|
|
180
209
|
self.plot_geodataframe(
|
|
181
|
-
ax=axa[i],
|
|
210
|
+
ax=axa[i],
|
|
211
|
+
gdf=gdf,
|
|
212
|
+
column_name=column_name,
|
|
213
|
+
cbar_label=cbar_label,
|
|
214
|
+
cmap=cmap,
|
|
182
215
|
)
|
|
183
216
|
|
|
184
217
|
ctx.add_basemap(ax=axa[i], **ctx_kwargs)
|
|
@@ -187,7 +220,7 @@ class PlotResiduals(Plotter):
|
|
|
187
220
|
axa[i].autoscale(False)
|
|
188
221
|
|
|
189
222
|
# Show some statistics and information
|
|
190
|
-
stats = self.
|
|
223
|
+
stats = self.gdf_percentile_stats(gdf, column_name)
|
|
191
224
|
stats_text = f"(n={gdf.shape[0]})\n" + "\n".join(
|
|
192
225
|
f"{quantile*100:.0f}th: {stat}"
|
|
193
226
|
for quantile, stat in zip([0.25, 0.50, 0.84, 0.95], stats)
|
|
@@ -3,9 +3,10 @@ import glob
|
|
|
3
3
|
import subprocess
|
|
4
4
|
import click
|
|
5
5
|
import contextily as ctx
|
|
6
|
+
from itertools import count
|
|
6
7
|
from asp_plot.processing_parameters import ProcessingParameters
|
|
7
|
-
from asp_plot.scenes import ScenePlotter
|
|
8
|
-
from asp_plot.bundle_adjust import
|
|
8
|
+
from asp_plot.scenes import ScenePlotter, SceneGeometryPlotter
|
|
9
|
+
from asp_plot.bundle_adjust import ReadBundleAdjustFiles, PlotBundleAdjustFiles
|
|
9
10
|
from asp_plot.stereo import StereoPlotter
|
|
10
11
|
from asp_plot.utils import compile_report
|
|
11
12
|
|
|
@@ -44,7 +45,7 @@ from asp_plot.utils import compile_report
|
|
|
44
45
|
@click.option(
|
|
45
46
|
"--plots_directory",
|
|
46
47
|
prompt=True,
|
|
47
|
-
default="
|
|
48
|
+
default="asp_plots_for_report",
|
|
48
49
|
help="Directory to put output plots. Default: asp_plots",
|
|
49
50
|
)
|
|
50
51
|
@click.option(
|
|
@@ -68,27 +69,23 @@ def main(
|
|
|
68
69
|
os.makedirs(plots_directory, exist_ok=True)
|
|
69
70
|
report_pdf_path = os.path.join(directory, report_filename)
|
|
70
71
|
|
|
72
|
+
figure_counter = count(0)
|
|
73
|
+
|
|
71
74
|
# Geometry plot
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
f"mv {directory}/*stereo_geom.png {plots_directory}/00_stereo_geom.png",
|
|
76
|
-
shell=True,
|
|
77
|
-
)
|
|
78
|
-
except:
|
|
79
|
-
print(
|
|
80
|
-
"Could not generate stereo geometry plot, check your path for dg_geom_plot.py"
|
|
81
|
-
)
|
|
75
|
+
plotter = SceneGeometryPlotter(directory)
|
|
76
|
+
|
|
77
|
+
plotter.dg_geom_plot(save_dir=plots_directory, fig_fn=f"{next(figure_counter):02}.png")
|
|
82
78
|
|
|
83
79
|
# Scene plot
|
|
84
80
|
plotter = ScenePlotter(directory, stereo_directory, title="Mapprojected Scenes")
|
|
85
81
|
|
|
86
|
-
plotter.plot_orthos(save_dir=plots_directory, fig_fn="
|
|
82
|
+
plotter.plot_orthos(save_dir=plots_directory, fig_fn=f"{next(figure_counter):02}.png")
|
|
87
83
|
|
|
88
84
|
# Bundle adjustment plots
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
ba_files = ReadBundleAdjustFiles(directory, bundle_adjust_directory)
|
|
86
|
+
resid_initial_gdf, resid_final_gdf = ba_files.get_initial_final_residuals_gdfs()
|
|
87
|
+
geodiff_initial_gdf, geodiff_final_gdf = ba_files.get_initial_final_geodiff_gdfs()
|
|
88
|
+
resid_mapprojected_gdf = ba_files.get_mapproj_residuals_gdf()
|
|
92
89
|
|
|
93
90
|
ctx_kwargs = {
|
|
94
91
|
"crs": map_crs,
|
|
@@ -97,48 +94,69 @@ def main(
|
|
|
97
94
|
"alpha": 0.5,
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
plotter =
|
|
101
|
-
[
|
|
97
|
+
plotter = PlotBundleAdjustFiles(
|
|
98
|
+
[resid_initial_gdf, resid_final_gdf],
|
|
102
99
|
lognorm=True,
|
|
103
100
|
title="Bundle Adjust Initial and Final Residuals (Log Scale)",
|
|
104
101
|
)
|
|
105
102
|
|
|
106
|
-
plotter.
|
|
103
|
+
plotter.plot_n_gdfs(
|
|
107
104
|
column_name="mean_residual",
|
|
108
105
|
cbar_label="Mean Residual (m)",
|
|
109
106
|
map_crs=map_crs,
|
|
110
107
|
save_dir=plots_directory,
|
|
111
|
-
fig_fn="
|
|
108
|
+
fig_fn=f"{next(figure_counter):02}.png",
|
|
112
109
|
**ctx_kwargs,
|
|
113
110
|
)
|
|
114
111
|
|
|
115
112
|
plotter.lognorm = False
|
|
116
113
|
plotter.title = "Bundle Adjust Initial and Final Residuals (Linear Scale)"
|
|
117
114
|
|
|
118
|
-
plotter.
|
|
115
|
+
plotter.plot_n_gdfs(
|
|
119
116
|
column_name="mean_residual",
|
|
120
117
|
cbar_label="Mean Residual (m)",
|
|
121
118
|
common_clim=False,
|
|
122
119
|
map_crs=map_crs,
|
|
123
120
|
save_dir=plots_directory,
|
|
124
|
-
fig_fn="
|
|
121
|
+
fig_fn=f"{next(figure_counter):02}.png",
|
|
125
122
|
**ctx_kwargs,
|
|
126
123
|
)
|
|
127
124
|
|
|
128
|
-
plotter =
|
|
125
|
+
plotter = PlotBundleAdjustFiles(
|
|
129
126
|
[resid_mapprojected_gdf],
|
|
130
127
|
title="Bundle Adjust Midpoint distance between\nfinal interest points projected onto reference DEM",
|
|
131
128
|
)
|
|
132
129
|
|
|
133
|
-
plotter.
|
|
130
|
+
plotter.plot_n_gdfs(
|
|
134
131
|
column_name="mapproj_ip_dist_meters",
|
|
135
132
|
cbar_label="Interest point distance (m)",
|
|
136
133
|
map_crs=map_crs,
|
|
137
134
|
save_dir=plots_directory,
|
|
138
|
-
fig_fn="
|
|
135
|
+
fig_fn=f"{next(figure_counter):02}.png",
|
|
139
136
|
**ctx_kwargs,
|
|
140
137
|
)
|
|
141
138
|
|
|
139
|
+
plotter = PlotBundleAdjustFiles(
|
|
140
|
+
[geodiff_initial_gdf, geodiff_final_gdf],
|
|
141
|
+
lognorm=False,
|
|
142
|
+
title="Bundle Adjust Initial and Final Geodiff vs. Reference DEM"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
clim = (float(geodiff_initial_gdf["height_diff_meters"].quantile(0.05)), float(geodiff_initial_gdf["height_diff_meters"].quantile(0.95)))
|
|
146
|
+
abs_max = max(abs(clim[0]), abs(clim[1]))
|
|
147
|
+
clim = (-abs_max, abs_max)
|
|
148
|
+
|
|
149
|
+
plotter.plot_n_gdfs(
|
|
150
|
+
column_name="height_diff_meters",
|
|
151
|
+
cbar_label="Height difference (m)",
|
|
152
|
+
map_crs=map_crs,
|
|
153
|
+
cmap="RdBu",
|
|
154
|
+
clim=clim,
|
|
155
|
+
save_dir=plots_directory,
|
|
156
|
+
fig_fn=f"{next(figure_counter):02}.png",
|
|
157
|
+
**ctx_kwargs
|
|
158
|
+
)
|
|
159
|
+
|
|
142
160
|
# Stereo plots
|
|
143
161
|
plotter = StereoPlotter(
|
|
144
162
|
directory,
|
|
@@ -150,7 +168,7 @@ def main(
|
|
|
150
168
|
|
|
151
169
|
plotter.plot_match_points(
|
|
152
170
|
save_dir=plots_directory,
|
|
153
|
-
fig_fn="
|
|
171
|
+
fig_fn=f"{next(figure_counter):02}.png",
|
|
154
172
|
)
|
|
155
173
|
|
|
156
174
|
plotter.title = "Disparity (pixels)"
|
|
@@ -159,14 +177,14 @@ def main(
|
|
|
159
177
|
unit="pixels",
|
|
160
178
|
quiver=True,
|
|
161
179
|
save_dir=plots_directory,
|
|
162
|
-
fig_fn="
|
|
180
|
+
fig_fn=f"{next(figure_counter):02}.png",
|
|
163
181
|
)
|
|
164
182
|
|
|
165
183
|
plotter.title = "Stereo DEM Results"
|
|
166
184
|
|
|
167
185
|
plotter.plot_dem_results(
|
|
168
186
|
save_dir=plots_directory,
|
|
169
|
-
fig_fn="
|
|
187
|
+
fig_fn=f"{next(figure_counter):02}.png",
|
|
170
188
|
)
|
|
171
189
|
|
|
172
190
|
# Compile report
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import matplotlib.gridspec as gridspec
|
|
6
|
+
from shapely import wkt
|
|
7
|
+
from asp_plot.utils import Raster, Plotter, save_figure
|
|
8
|
+
from asp_plot.stereopair_metadata_parser import StereopairMetadataParser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SceneGeometryPlotter(StereopairMetadataParser):
|
|
12
|
+
def __init__(self, directory, **kwargs):
|
|
13
|
+
super().__init__(directory=directory, **kwargs)
|
|
14
|
+
|
|
15
|
+
def get_scene_string(self, p, key="id1_dict"):
|
|
16
|
+
scene_string = (
|
|
17
|
+
"\nID:%s, GSD:%0.2f, off:%0.1f, az:%0.1f, el:%0.1f, it:%0.1f, ct:%0.1f, scan:%s, tdi:%i"
|
|
18
|
+
% (
|
|
19
|
+
p[key]["id"],
|
|
20
|
+
p[key]["meanproductgsd"],
|
|
21
|
+
p[key]["meanoffnadirviewangle"],
|
|
22
|
+
p[key]["meansataz"],
|
|
23
|
+
(90 - p[key]["meansatel"]),
|
|
24
|
+
p[key]["meanintrackviewangle"],
|
|
25
|
+
(p[key]["meancrosstrackviewangle"]),
|
|
26
|
+
p[key]["scandir"],
|
|
27
|
+
p[key]["tdi"],
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
return scene_string
|
|
31
|
+
|
|
32
|
+
def get_title(self, p):
|
|
33
|
+
title = p["pairname"]
|
|
34
|
+
title += "\nCenter datetime: %s" % p["cdate"]
|
|
35
|
+
title += "\nTime offset: %s" % str(p["dt"])
|
|
36
|
+
title += "\nConv. angle: %0.2f, B:H ratio: %0.2f, Int. area: %0.2f km2" % (
|
|
37
|
+
p["conv_ang"],
|
|
38
|
+
p["bh"],
|
|
39
|
+
p["intersection_area"],
|
|
40
|
+
)
|
|
41
|
+
title += self.get_scene_string(p, "id1_dict")
|
|
42
|
+
title += self.get_scene_string(p, "id2_dict")
|
|
43
|
+
return title
|
|
44
|
+
|
|
45
|
+
def skyplot(self, ax, p, title=True, tight_layout=True):
|
|
46
|
+
"""
|
|
47
|
+
Function to plot stereo geometry from dg xml
|
|
48
|
+
Parameters
|
|
49
|
+
-----------
|
|
50
|
+
p: pair dictionary
|
|
51
|
+
dictionary with xml info read from get_pair_dict function
|
|
52
|
+
ax: matplotlib.axes
|
|
53
|
+
polar axes object to plot the skyplot on
|
|
54
|
+
"""
|
|
55
|
+
ax.set_theta_direction(-1)
|
|
56
|
+
ax.set_theta_zero_location("N")
|
|
57
|
+
ax.grid(True)
|
|
58
|
+
|
|
59
|
+
plot_kw = {"marker": "o", "ls": "", "ms": 5}
|
|
60
|
+
|
|
61
|
+
ax.plot(0, 0, marker="o", color="k")
|
|
62
|
+
ax.plot(
|
|
63
|
+
np.radians(p["id1_dict"]["meansataz"]),
|
|
64
|
+
(90 - p["id1_dict"]["meansatel"]),
|
|
65
|
+
label=p["id1_dict"]["id"],
|
|
66
|
+
**plot_kw,
|
|
67
|
+
)
|
|
68
|
+
ax.plot(
|
|
69
|
+
np.radians(p["id2_dict"]["meansataz"]),
|
|
70
|
+
(90 - p["id2_dict"]["meansatel"]),
|
|
71
|
+
label=p["id2_dict"]["id"],
|
|
72
|
+
**plot_kw,
|
|
73
|
+
)
|
|
74
|
+
ax.plot(
|
|
75
|
+
[
|
|
76
|
+
np.radians(p["id1_dict"]["meansataz"]),
|
|
77
|
+
np.radians(p["id2_dict"]["meansataz"]),
|
|
78
|
+
],
|
|
79
|
+
[90 - p["id1_dict"]["meansatel"], 90 - p["id2_dict"]["meansatel"]],
|
|
80
|
+
color="k",
|
|
81
|
+
ls="--",
|
|
82
|
+
lw=0.5,
|
|
83
|
+
alpha=0.5,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
ax.legend(loc="lower left", fontsize="small")
|
|
87
|
+
|
|
88
|
+
ax.set_rmin(0)
|
|
89
|
+
ax.set_rmax(50)
|
|
90
|
+
if title:
|
|
91
|
+
ax.set_title(self.get_title(p), fontsize=8)
|
|
92
|
+
if tight_layout:
|
|
93
|
+
plt.tight_layout()
|
|
94
|
+
|
|
95
|
+
def map_plot(
|
|
96
|
+
self, ax, p, gdf_list, map_crs="EPSG:3857", title=True, tight_layout=True
|
|
97
|
+
):
|
|
98
|
+
"""
|
|
99
|
+
Plot satellite ephemeris and ground footprint for a DigitalGlobe stereo pair
|
|
100
|
+
# stitched together from David's notebook: https://github.com/dshean/dgtools/blob/master/notebooks/dg_pair_geom_eph_analysis.ipynb
|
|
101
|
+
Parameters
|
|
102
|
+
------------
|
|
103
|
+
ax: matplotlib sublot axes object
|
|
104
|
+
gdf_list: list of necessary GeoDataFrame objects
|
|
105
|
+
"""
|
|
106
|
+
import contextily as ctx
|
|
107
|
+
|
|
108
|
+
poly_kw = {"alpha": 0.5, "edgecolor": "k", "linewidth": 0.5}
|
|
109
|
+
eph_kw = {"markersize": 2}
|
|
110
|
+
|
|
111
|
+
fp1_gdf, fp2_gdf, eph1_gdf, eph2_gdf = gdf_list
|
|
112
|
+
|
|
113
|
+
c_list = ["blue", "orange"]
|
|
114
|
+
fp1_gdf.to_crs(map_crs).plot(ax=ax, color=c_list[0], **poly_kw)
|
|
115
|
+
fp2_gdf.to_crs(map_crs).plot(ax=ax, color=c_list[1], **poly_kw)
|
|
116
|
+
eph1_gdf.to_crs(map_crs).plot(
|
|
117
|
+
ax=ax, label=p["id1_dict"]["id"], color=c_list[0], **eph_kw
|
|
118
|
+
)
|
|
119
|
+
eph2_gdf.to_crs(map_crs).plot(
|
|
120
|
+
ax=ax, label=p["id2_dict"]["id"], color=c_list[1], **eph_kw
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
start_kw = {"markersize": 5, "facecolor": "w", "edgecolor": "k"}
|
|
124
|
+
eph1_gdf.iloc[0:2].to_crs(map_crs).plot(ax=ax, **start_kw)
|
|
125
|
+
eph2_gdf.iloc[0:2].to_crs(map_crs).plot(ax=ax, **start_kw)
|
|
126
|
+
|
|
127
|
+
ctx.add_basemap(ax, crs=map_crs, attribution=False)
|
|
128
|
+
|
|
129
|
+
ax.legend(loc="best", prop={"size": 6})
|
|
130
|
+
if title:
|
|
131
|
+
ax.set_title(self.get_title(p), fontsize=7.5)
|
|
132
|
+
if tight_layout:
|
|
133
|
+
plt.tight_layout()
|
|
134
|
+
|
|
135
|
+
def dg_geom_plot(self, save_dir=None, fig_fn=None):
|
|
136
|
+
# load pair information as dict
|
|
137
|
+
p = self.get_pair_dict()
|
|
138
|
+
|
|
139
|
+
# TODO: Should store xml names in the pairdict
|
|
140
|
+
# Use r100 outputs from dg_mosaic
|
|
141
|
+
xml_list = sorted(glob.glob(os.path.join(self.directory, "*r100.[Xx][Mm][Ll]")))
|
|
142
|
+
|
|
143
|
+
eph1_gdf, eph2_gdf = [self.getEphem_gdf(xml) for xml in xml_list]
|
|
144
|
+
fp1_gdf, fp2_gdf = [self.xml2gdf(xml) for xml in xml_list]
|
|
145
|
+
|
|
146
|
+
fig = plt.figure(figsize=(10, 7.5))
|
|
147
|
+
G = gridspec.GridSpec(nrows=1, ncols=2)
|
|
148
|
+
ax0 = fig.add_subplot(G[0, 0:1], polar=True)
|
|
149
|
+
ax1 = fig.add_subplot(G[0, 1:2])
|
|
150
|
+
|
|
151
|
+
self.skyplot(ax0, p, title=False, tight_layout=False)
|
|
152
|
+
|
|
153
|
+
# map_crs = 'EPSG:3857'
|
|
154
|
+
# Use local projection to minimize distortion
|
|
155
|
+
# Get Shapely polygon and compute centroid (for local projection def)
|
|
156
|
+
p_poly = wkt.loads(p["intersection"].ExportToWkt())
|
|
157
|
+
p_int_c = np.array(p_poly.centroid.coords.xy).ravel()
|
|
158
|
+
# map_crs = '+proj=ortho +lon_0={} +lat_0={}'.format(*p_int_c)
|
|
159
|
+
# Should be OK to use transverse mercator here, usually within ~2-3 deg
|
|
160
|
+
map_crs = "+proj=tmerc +lon_0={} +lat_0={}".format(*p_int_c)
|
|
161
|
+
|
|
162
|
+
self.map_plot(
|
|
163
|
+
ax1,
|
|
164
|
+
p,
|
|
165
|
+
[fp1_gdf, fp2_gdf, eph1_gdf, eph2_gdf],
|
|
166
|
+
map_crs=map_crs,
|
|
167
|
+
title=False,
|
|
168
|
+
tight_layout=False,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
plt.suptitle(self.get_title(p), fontsize=10)
|
|
172
|
+
|
|
173
|
+
if save_dir and fig_fn:
|
|
174
|
+
save_figure(fig, save_dir, fig_fn)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ScenePlotter(Plotter):
|
|
178
|
+
def __init__(self, directory, stereo_directory, **kwargs):
|
|
179
|
+
super().__init__(**kwargs)
|
|
180
|
+
self.directory = directory
|
|
181
|
+
self.stereo_directory = stereo_directory
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
self.left_ortho_sub_fn = glob.glob(
|
|
185
|
+
os.path.join(self.directory, self.stereo_directory, "*-L_sub.tif")
|
|
186
|
+
)[0]
|
|
187
|
+
self.right_ortho_sub_fn = glob.glob(
|
|
188
|
+
os.path.join(self.directory, self.stereo_directory, "*-R_sub.tif")
|
|
189
|
+
)[0]
|
|
190
|
+
except:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
"Could not find L-sub and R-sub images in stereo directory"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def plot_orthos(self, save_dir=None, fig_fn=None):
|
|
196
|
+
p = StereopairMetadataParser(self.directory).get_pair_dict()
|
|
197
|
+
|
|
198
|
+
fig, axa = plt.subplots(1, 2, figsize=(10, 5), dpi=300)
|
|
199
|
+
fig.suptitle(self.title, size=10)
|
|
200
|
+
axa = axa.ravel()
|
|
201
|
+
|
|
202
|
+
ortho_ma = Raster(self.left_ortho_sub_fn).read_array()
|
|
203
|
+
self.plot_array(ax=axa[0], array=ortho_ma, cmap="gray", add_cbar=False)
|
|
204
|
+
axa[0].set_title(
|
|
205
|
+
f"Left image\n{p['id1_dict']['id']}, {p['id1_dict']['meanproductgsd']:0.2f} m"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
ortho_ma = Raster(self.right_ortho_sub_fn).read_array()
|
|
209
|
+
self.plot_array(ax=axa[1], array=ortho_ma, cmap="gray", add_cbar=False)
|
|
210
|
+
axa[1].set_title(
|
|
211
|
+
f"Right image\n{p['id2_dict']['id']}, {p['id2_dict']['meanproductgsd']:0.2f} m"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
fig.tight_layout()
|
|
215
|
+
if save_dir and fig_fn:
|
|
216
|
+
save_figure(fig, save_dir, fig_fn)
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import geopandas as gpd
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from osgeo import ogr, osr, gdal
|
|
8
|
+
from shapely import wkt
|
|
9
|
+
from asp_plot.utils import get_xml_tag
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StereopairMetadataParser:
|
|
13
|
+
def __init__(self, directory):
|
|
14
|
+
self.directory = directory
|
|
15
|
+
|
|
16
|
+
def get_pair_dict(self):
|
|
17
|
+
ids = self.get_ids()
|
|
18
|
+
id1_dict = self.get_id_dict(ids[0])
|
|
19
|
+
id2_dict = self.get_id_dict(ids[1])
|
|
20
|
+
pairname = os.path.split(self.directory.rstrip("/\\"))[-1]
|
|
21
|
+
return self.pair_dict(id1_dict, id2_dict, pairname)
|
|
22
|
+
|
|
23
|
+
def get_ids(self):
|
|
24
|
+
|
|
25
|
+
def get_id(filename):
|
|
26
|
+
import re
|
|
27
|
+
|
|
28
|
+
ids = re.findall("10[123456][0-9a-fA-F]+00", filename)
|
|
29
|
+
return list(set(ids))
|
|
30
|
+
|
|
31
|
+
image_list = glob.glob(os.path.join(self.directory, "*.[Xx][Mm][Ll]"))
|
|
32
|
+
ids = [get_id(f) for f in image_list]
|
|
33
|
+
ids = sorted(set(item for sublist in ids if sublist for item in sublist))
|
|
34
|
+
return ids
|
|
35
|
+
|
|
36
|
+
def get_id_dict(self, id):
|
|
37
|
+
|
|
38
|
+
def list_average(list):
|
|
39
|
+
return np.round(pd.Series(list, dtype=float).dropna().mean(), 2)
|
|
40
|
+
|
|
41
|
+
def geom_union(geom_list):
|
|
42
|
+
union = geom_list[0]
|
|
43
|
+
for geom in geom_list[1:]:
|
|
44
|
+
union = union.Union(geom)
|
|
45
|
+
return union
|
|
46
|
+
|
|
47
|
+
xml_list = glob.glob(os.path.join(self.directory, f"*{id:}*.[Xx][Mm][Ll]"))
|
|
48
|
+
|
|
49
|
+
attributes = {
|
|
50
|
+
"MEANSATAZ": [],
|
|
51
|
+
"MEANSATEL": [],
|
|
52
|
+
"MEANOFFNADIRVIEWANGLE": [],
|
|
53
|
+
"MEANINTRACKVIEWANGLE": [],
|
|
54
|
+
"MEANCROSSTRACKVIEWANGLE": [],
|
|
55
|
+
"MEANPRODUCTGSD": [],
|
|
56
|
+
"MEANSUNAZ": [],
|
|
57
|
+
"MEANSUNEL": [],
|
|
58
|
+
"CLOUDCOVER": [],
|
|
59
|
+
"geom": [],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for xml in xml_list:
|
|
63
|
+
for tag, lst in attributes.items():
|
|
64
|
+
if tag != "geom":
|
|
65
|
+
lst.append(get_xml_tag(xml, tag))
|
|
66
|
+
else:
|
|
67
|
+
lst.append(self.xml2geom(xml))
|
|
68
|
+
|
|
69
|
+
d = {
|
|
70
|
+
"id": str(id),
|
|
71
|
+
"sensor": get_xml_tag(xml_list[0], "SATID"),
|
|
72
|
+
"date": datetime.strptime(
|
|
73
|
+
get_xml_tag(xml_list[0], "FIRSTLINETIME"), "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
74
|
+
),
|
|
75
|
+
"scandir": get_xml_tag(xml_list[0], "SCANDIRECTION"),
|
|
76
|
+
"tdi": int(get_xml_tag(xml_list[0], "TDILEVEL")),
|
|
77
|
+
"geom": geom_union(attributes["geom"]),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for tag, lst in attributes.items():
|
|
81
|
+
if tag != "geom":
|
|
82
|
+
d[tag.lower()] = list_average(lst)
|
|
83
|
+
|
|
84
|
+
return d
|
|
85
|
+
|
|
86
|
+
def getEphem(self, xml):
|
|
87
|
+
e = get_xml_tag(xml, "EPHEMLIST", all=True)
|
|
88
|
+
# Could get fancy with structured array here
|
|
89
|
+
# point_num, Xpos, Ypos, Zpos, Xvel, Yvel, Zvel, covariance matrix (6 elements)
|
|
90
|
+
# dtype=[('point', 'i4'), ('Xpos', 'f8'), ('Ypos', 'f8'), ('Zpos', 'f8'), ('Xvel', 'f8') ...]
|
|
91
|
+
# All coordinates are ECF, meters, meters/sec, m^2
|
|
92
|
+
return np.array([i.split() for i in e], dtype=np.float64)
|
|
93
|
+
|
|
94
|
+
def getEphem_gdf(self, xml):
|
|
95
|
+
names = [
|
|
96
|
+
"index",
|
|
97
|
+
]
|
|
98
|
+
names.extend(["x", "y", "z"])
|
|
99
|
+
names.extend(["dx", "dy", "dz"])
|
|
100
|
+
names.extend(["{}_cov".format(n) for n in names[1:7]])
|
|
101
|
+
e = self.getEphem(xml)
|
|
102
|
+
t0 = pd.to_datetime(get_xml_tag(xml, "STARTTIME"))
|
|
103
|
+
dt = pd.Timedelta(float(get_xml_tag(xml, "TIMEINTERVAL")), unit="s")
|
|
104
|
+
eph_df = pd.DataFrame(e, columns=names)
|
|
105
|
+
eph_df["time"] = t0 + eph_df.index * dt
|
|
106
|
+
eph_df.set_index("time", inplace=True)
|
|
107
|
+
eph_gdf = gpd.GeoDataFrame(
|
|
108
|
+
eph_df,
|
|
109
|
+
geometry=gpd.points_from_xy(eph_df["x"], eph_df["y"], eph_df["z"]),
|
|
110
|
+
crs="EPSG:4978",
|
|
111
|
+
)
|
|
112
|
+
return eph_gdf
|
|
113
|
+
|
|
114
|
+
def xml2wkt(self, xml):
|
|
115
|
+
tags = [
|
|
116
|
+
("ULLON", "ULLAT"),
|
|
117
|
+
("URLON", "URLAT"),
|
|
118
|
+
("LRLON", "LRLAT"),
|
|
119
|
+
("LLLON", "LLLAT"),
|
|
120
|
+
("ULLON", "ULLAT"),
|
|
121
|
+
]
|
|
122
|
+
coords = []
|
|
123
|
+
for lon_tag, lat_tag in tags:
|
|
124
|
+
lon = get_xml_tag(xml, lon_tag)
|
|
125
|
+
lat = get_xml_tag(xml, lat_tag)
|
|
126
|
+
if lon and lat:
|
|
127
|
+
coords.append(f"{lon} {lat}")
|
|
128
|
+
geom_wkt = f"POLYGON(({', '.join(coords)}))"
|
|
129
|
+
return geom_wkt
|
|
130
|
+
|
|
131
|
+
def xml2geom(self, xml):
|
|
132
|
+
geom_wkt = self.xml2wkt(xml)
|
|
133
|
+
geom = ogr.CreateGeometryFromWkt(geom_wkt)
|
|
134
|
+
wgs_srs = self.get_wgs_srs()
|
|
135
|
+
geom.AssignSpatialReference(wgs_srs)
|
|
136
|
+
return geom
|
|
137
|
+
|
|
138
|
+
def xml2gdf(self, xml, init_crs="EPSG:4326"):
|
|
139
|
+
poly = self.xml2poly(xml)
|
|
140
|
+
gdf = gpd.GeoDataFrame(
|
|
141
|
+
{"idx": [0], "geometry": poly}, geometry="geometry", crs=init_crs
|
|
142
|
+
)
|
|
143
|
+
return gdf
|
|
144
|
+
|
|
145
|
+
def xml2poly(self, xml):
|
|
146
|
+
geom_wkt = self.xml2wkt(xml)
|
|
147
|
+
return wkt.loads(geom_wkt)
|
|
148
|
+
|
|
149
|
+
def get_wgs_srs(self):
|
|
150
|
+
# Define WGS84 srs
|
|
151
|
+
# mpd = 111319.9
|
|
152
|
+
wgs_srs = osr.SpatialReference()
|
|
153
|
+
wgs_srs.SetWellKnownGeogCS("WGS84")
|
|
154
|
+
# GDAL3 hack
|
|
155
|
+
if int(gdal.__version__.split(".")[0]) >= 3:
|
|
156
|
+
wgs_srs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)
|
|
157
|
+
return wgs_srs
|
|
158
|
+
|
|
159
|
+
def pair_dict(self, id1_dict, id2_dict, pairname):
|
|
160
|
+
def center_date(dt_list):
|
|
161
|
+
dt_list_sort = sorted(dt_list)
|
|
162
|
+
dt_list_sort_rel = [dt - dt_list_sort[0] for dt in dt_list_sort]
|
|
163
|
+
avg_timedelta = sum(dt_list_sort_rel, timedelta()) / len(dt_list_sort_rel)
|
|
164
|
+
return dt_list_sort[0] + avg_timedelta
|
|
165
|
+
|
|
166
|
+
def get_conv(az1, el1, az2, el2):
|
|
167
|
+
conv_ang = np.rad2deg(
|
|
168
|
+
np.arccos(
|
|
169
|
+
np.sin(np.deg2rad(el1)) * np.sin(np.deg2rad(el2))
|
|
170
|
+
+ np.cos(np.deg2rad(el1))
|
|
171
|
+
* np.cos(np.deg2rad(el2))
|
|
172
|
+
* np.cos(np.deg2rad(az1 - az2))
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return np.round(conv_ang, 2)
|
|
176
|
+
|
|
177
|
+
def get_bh(conv_ang):
|
|
178
|
+
bh = 2 * np.tan(np.deg2rad(conv_ang / 2.0))
|
|
179
|
+
return np.round(bh, 2)
|
|
180
|
+
|
|
181
|
+
p = {}
|
|
182
|
+
p["id1_dict"] = id1_dict
|
|
183
|
+
p["id2_dict"] = id2_dict
|
|
184
|
+
p["pairname"] = pairname
|
|
185
|
+
|
|
186
|
+
self.get_pair_intersection(p)
|
|
187
|
+
|
|
188
|
+
cdate = center_date([p["id1_dict"]["date"], p["id2_dict"]["date"]])
|
|
189
|
+
p["cdate"] = cdate
|
|
190
|
+
dt1 = p["id1_dict"]["date"]
|
|
191
|
+
dt2 = p["id2_dict"]["date"]
|
|
192
|
+
dt = abs(dt1 - dt2)
|
|
193
|
+
p["dt"] = dt
|
|
194
|
+
|
|
195
|
+
p["conv_ang"] = get_conv(
|
|
196
|
+
p["id1_dict"]["meansataz"],
|
|
197
|
+
p["id1_dict"]["meansatel"],
|
|
198
|
+
p["id2_dict"]["meansataz"],
|
|
199
|
+
p["id2_dict"]["meansatel"],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
p["bh"] = get_bh(p["conv_ang"])
|
|
203
|
+
return p
|
|
204
|
+
|
|
205
|
+
def get_pair_intersection(self, p):
|
|
206
|
+
def geom_intersection(geom_list):
|
|
207
|
+
intsect = geom_list[0]
|
|
208
|
+
valid = False
|
|
209
|
+
for geom in geom_list[1:]:
|
|
210
|
+
if intsect.Intersects(geom):
|
|
211
|
+
valid = True
|
|
212
|
+
intsect = intsect.Intersection(geom)
|
|
213
|
+
if not valid:
|
|
214
|
+
intsect = None
|
|
215
|
+
return intsect
|
|
216
|
+
|
|
217
|
+
def geom2localortho(geom):
|
|
218
|
+
wgs_srs = self.get_wgs_srs()
|
|
219
|
+
cx, cy = geom.Centroid().GetPoint_2D()
|
|
220
|
+
lon, lat, z = self.coordinate_transformation_helper(
|
|
221
|
+
cx, cy, 0, geom.GetSpatialReference(), wgs_srs
|
|
222
|
+
)
|
|
223
|
+
local_srs = osr.SpatialReference()
|
|
224
|
+
local_proj = f"+proj=ortho +lat_0={lat:0.7f} +lon_0={lon:0.7f} +datum=WGS84 +units=m +no_defs "
|
|
225
|
+
local_srs.ImportFromProj4(local_proj)
|
|
226
|
+
local_geom = geom_dup(geom)
|
|
227
|
+
geom_transform(local_geom, local_srs)
|
|
228
|
+
return local_geom
|
|
229
|
+
|
|
230
|
+
def geom_dup(geom):
|
|
231
|
+
g = ogr.CreateGeometryFromWkt(geom.ExportToWkt())
|
|
232
|
+
g.AssignSpatialReference(geom.GetSpatialReference())
|
|
233
|
+
return g
|
|
234
|
+
|
|
235
|
+
def geom_transform(geom, t_srs):
|
|
236
|
+
s_srs = geom.GetSpatialReference()
|
|
237
|
+
if not s_srs.IsSame(t_srs):
|
|
238
|
+
ct = osr.CoordinateTransformation(s_srs, t_srs)
|
|
239
|
+
geom.Transform(ct)
|
|
240
|
+
geom.AssignSpatialReference(t_srs)
|
|
241
|
+
|
|
242
|
+
geom1 = p["id1_dict"]["geom"]
|
|
243
|
+
geom2 = p["id2_dict"]["geom"]
|
|
244
|
+
intersection = geom_intersection([geom1, geom2])
|
|
245
|
+
p["intersection"] = intersection
|
|
246
|
+
p["intersection_poly"] = wkt.loads(intersection.ExportToWkt())
|
|
247
|
+
intersection_local = geom2localortho(intersection)
|
|
248
|
+
local_srs = intersection_local.GetSpatialReference()
|
|
249
|
+
# This recomputes for local orthographic - important for width/height calculations
|
|
250
|
+
geom1_local = geom_dup(geom1)
|
|
251
|
+
geom_transform(geom1_local, local_srs)
|
|
252
|
+
geom2_local = geom_dup(geom2)
|
|
253
|
+
geom_transform(geom2_local, local_srs)
|
|
254
|
+
if intersection is not None:
|
|
255
|
+
# Area calc shouldn't matter too much
|
|
256
|
+
intersection_area = intersection_local.GetArea()
|
|
257
|
+
p["intersection_area"] = np.round(intersection_area / 1e6, 2)
|
|
258
|
+
perc = (
|
|
259
|
+
100.0 * intersection_area / geom1_local.GetArea(),
|
|
260
|
+
100 * intersection_area / geom2_local.GetArea(),
|
|
261
|
+
)
|
|
262
|
+
perc = (np.round(perc[0], 2), np.round(perc[1], 2))
|
|
263
|
+
p["intersection_area_perc"] = perc
|
|
264
|
+
else:
|
|
265
|
+
p["intersection_area"] = None
|
|
266
|
+
p["intersection_area_perc"] = None
|
|
267
|
+
|
|
268
|
+
def coordinate_transformation_helper(self, x, y, z, in_srs, out_srs):
|
|
269
|
+
def common_mask(ma_list, apply=False):
|
|
270
|
+
a = np.ma.array(ma_list, shrink=False)
|
|
271
|
+
mask = np.ma.getmaskarray(a).any(axis=0)
|
|
272
|
+
if apply:
|
|
273
|
+
return [np.ma.array(b, mask=mask) for b in ma_list]
|
|
274
|
+
else:
|
|
275
|
+
return mask
|
|
276
|
+
|
|
277
|
+
x = np.atleast_1d(x)
|
|
278
|
+
y = np.atleast_1d(y)
|
|
279
|
+
z = np.atleast_1d(z)
|
|
280
|
+
if x.shape != y.shape:
|
|
281
|
+
sys.exit("Inconsistent number of x and y points")
|
|
282
|
+
valid_idx = None
|
|
283
|
+
# Handle case where we have x array, y array, but a constant z (e.g., 0.0)
|
|
284
|
+
if z.shape != x.shape:
|
|
285
|
+
# If a constant elevation is provided
|
|
286
|
+
if z.shape[0] == 1:
|
|
287
|
+
orig_z = z
|
|
288
|
+
z = np.zeros_like(x)
|
|
289
|
+
z[:] = orig_z
|
|
290
|
+
if np.ma.is_masked(x):
|
|
291
|
+
z[np.ma.getmaskarray(x)] = np.ma.masked
|
|
292
|
+
else:
|
|
293
|
+
sys.exit("Inconsistent number of z and x/y points")
|
|
294
|
+
# If any of the inputs is masked, only transform points with all three coordinates available
|
|
295
|
+
if np.ma.is_masked(x) or np.ma.is_masked(y) or np.ma.is_masked(z):
|
|
296
|
+
x = np.ma.array(x)
|
|
297
|
+
y = np.ma.array(y)
|
|
298
|
+
z = np.ma.array(z)
|
|
299
|
+
valid_idx = ~(common_mask([x, y, z]))
|
|
300
|
+
# Prepare (x,y,z) tuples
|
|
301
|
+
xyz = np.array([x[valid_idx], y[valid_idx], z[valid_idx]]).T
|
|
302
|
+
else:
|
|
303
|
+
xyz = np.array([x.ravel(), y.ravel(), z.ravel()]).T
|
|
304
|
+
# Define coordinate transformation
|
|
305
|
+
coordinate_transformation = osr.CoordinateTransformation(in_srs, out_srs)
|
|
306
|
+
# Loop through each point
|
|
307
|
+
xyz2 = np.array(
|
|
308
|
+
[
|
|
309
|
+
coordinate_transformation.TransformPoint(xi, yi, zi)
|
|
310
|
+
for (xi, yi, zi) in xyz
|
|
311
|
+
]
|
|
312
|
+
).T
|
|
313
|
+
# If single input coordinate
|
|
314
|
+
if xyz2.shape[1] == 1:
|
|
315
|
+
xyz2 = xyz2.squeeze()
|
|
316
|
+
x2, y2, z2 = xyz2[0], xyz2[1], xyz2[2]
|
|
317
|
+
else:
|
|
318
|
+
# Fill in masked array
|
|
319
|
+
if valid_idx is not None:
|
|
320
|
+
x2 = np.zeros_like(x)
|
|
321
|
+
y2 = np.zeros_like(y)
|
|
322
|
+
z2 = np.zeros_like(z)
|
|
323
|
+
x2[valid_idx] = xyz2[0]
|
|
324
|
+
y2[valid_idx] = xyz2[1]
|
|
325
|
+
z2[valid_idx] = xyz2[2]
|
|
326
|
+
else:
|
|
327
|
+
x2 = xyz2[0].reshape(x.shape)
|
|
328
|
+
y2 = xyz2[1].reshape(y.shape)
|
|
329
|
+
z2 = xyz2[2].reshape(z.shape)
|
|
330
|
+
return x2, y2, z2
|
|
@@ -3,7 +3,7 @@ import numpy as np
|
|
|
3
3
|
import rasterio as rio
|
|
4
4
|
from rasterio.windows import Window
|
|
5
5
|
from osgeo import gdal
|
|
6
|
-
|
|
6
|
+
import geoutils as gu
|
|
7
7
|
import matplotlib.pyplot as plt
|
|
8
8
|
import matplotlib.colors
|
|
9
9
|
import matplotlib.image as mpimg
|
|
@@ -56,6 +56,20 @@ def compile_report(plots_directory, processing_parameters_dict, report_pdf_path)
|
|
|
56
56
|
pdf.save(report_pdf_path)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def get_xml_tag(xml, tag, all=False):
|
|
60
|
+
import xml.etree.ElementTree as ET
|
|
61
|
+
|
|
62
|
+
tree = ET.parse(xml)
|
|
63
|
+
if all:
|
|
64
|
+
elem = tree.findall(".//%s" % tag)
|
|
65
|
+
elem = [i.text for i in elem]
|
|
66
|
+
else:
|
|
67
|
+
elem = tree.find(".//%s" % tag)
|
|
68
|
+
elem = elem.text
|
|
69
|
+
|
|
70
|
+
return elem
|
|
71
|
+
|
|
72
|
+
|
|
59
73
|
class ColorBar:
|
|
60
74
|
def __init__(self, perc_range=(2, 98), symm=False):
|
|
61
75
|
self.perc_range = perc_range
|
|
@@ -134,8 +148,7 @@ class Raster:
|
|
|
134
148
|
hillshade = np.ma.masked_equal(hs_ds.ReadAsArray(), 0)
|
|
135
149
|
return hillshade
|
|
136
150
|
|
|
137
|
-
def compute_difference(self, second_fn
|
|
138
|
-
print(f"Computing difference map between:\n\n{self.fn}\n\nand\n\n{second_fn}")
|
|
151
|
+
def compute_difference(self, second_fn):
|
|
139
152
|
fn_list = [self.fn, second_fn]
|
|
140
153
|
outdir = os.path.dirname(os.path.abspath(self.fn))
|
|
141
154
|
|
|
@@ -145,17 +158,11 @@ class Raster:
|
|
|
145
158
|
+ os.path.splitext(os.path.split(second_fn)[1])[0]
|
|
146
159
|
)
|
|
147
160
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
r1_ds = ds_list[0]
|
|
152
|
-
r2_ds = ds_list[1]
|
|
153
|
-
r1 = iolib.ds_getma(r1_ds, 1)
|
|
154
|
-
r2 = iolib.ds_getma(r2_ds, 1)
|
|
155
|
-
diff = r2 - r1
|
|
161
|
+
rasters = gu.raster.load_multiple_rasters(fn_list, ref_grid=1)
|
|
162
|
+
diff = rasters[1] - rasters[0]
|
|
156
163
|
dst_fn = os.path.join(outdir, outprefix + "_diff.tif")
|
|
157
|
-
|
|
158
|
-
return diff
|
|
164
|
+
diff.save(dst_fn)
|
|
165
|
+
return diff.data
|
|
159
166
|
|
|
160
167
|
|
|
161
168
|
class Plotter:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: asp_plot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Package for plotting outputs Ames Stereo Pipeline processing
|
|
5
5
|
Author: Ben Purinton
|
|
6
6
|
Author-email: Ben Purinton <purinton@uw.edu>
|
|
@@ -54,6 +54,12 @@ $ pytest
|
|
|
54
54
|
|
|
55
55
|
### Package and upload
|
|
56
56
|
|
|
57
|
+
```
|
|
58
|
+
$ rm -rf dist/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then update version in `pyproject.toml` and `setup.py`, then:
|
|
62
|
+
|
|
57
63
|
```
|
|
58
64
|
$ python3 -m pip install --upgrade build
|
|
59
65
|
$ python3 -m build
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from asp_plot.bundle_adjust import ReadBundleAdjustFiles, PlotBundleAdjustFiles
|
|
3
|
+
import matplotlib
|
|
4
|
+
import geopandas as gpd
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
matplotlib.use("Agg")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestBundleAdjust:
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def ba_files(self):
|
|
13
|
+
directory = "tests/test_data"
|
|
14
|
+
ba_directory = "ba"
|
|
15
|
+
return ReadBundleAdjustFiles(directory, ba_directory)
|
|
16
|
+
|
|
17
|
+
def test_get_initial_final_residuals_gdfs(self, ba_files):
|
|
18
|
+
resid_initial, resid_final = ba_files.get_initial_final_residuals_gdfs()
|
|
19
|
+
assert isinstance(resid_initial, gpd.GeoDataFrame)
|
|
20
|
+
assert isinstance(resid_final, gpd.GeoDataFrame)
|
|
21
|
+
|
|
22
|
+
def test_get_initial_final_geodiff_gdfs(self, ba_files):
|
|
23
|
+
geodiff_initial, geodiff_final = ba_files.get_initial_final_geodiff_gdfs()
|
|
24
|
+
assert isinstance(geodiff_initial, gpd.GeoDataFrame)
|
|
25
|
+
assert isinstance(geodiff_final, gpd.GeoDataFrame)
|
|
26
|
+
|
|
27
|
+
def test_get_mapproj_residuals_gdf(self, ba_files):
|
|
28
|
+
resid_mapprojected_gdf = ba_files.get_mapproj_residuals_gdf()
|
|
29
|
+
assert isinstance(resid_mapprojected_gdf, gpd.GeoDataFrame)
|
|
30
|
+
|
|
31
|
+
def test_get_propagated_triangulation_uncert_df(self, ba_files):
|
|
32
|
+
resid_triangulation_uncert_df = ba_files.get_propagated_triangulation_uncert_df()
|
|
33
|
+
assert isinstance(resid_triangulation_uncert_df, pd.DataFrame)
|
|
34
|
+
|
|
35
|
+
def test_plot_n_gdfs(self, ba_files):
|
|
36
|
+
resid_initial, resid_final = ba_files.get_initial_final_residuals_gdfs()
|
|
37
|
+
try:
|
|
38
|
+
PlotBundleAdjustFiles([resid_initial, resid_final]).plot_n_gdfs(column_name="mean_residual")
|
|
39
|
+
except Exception as e:
|
|
40
|
+
pytest.fail(f"figure method raised an exception: {str(e)}")
|
|
@@ -7,6 +7,7 @@ class TestImports:
|
|
|
7
7
|
|
|
8
8
|
def test_import_asp_plot_modules(self):
|
|
9
9
|
import asp_plot.utils
|
|
10
|
+
import asp_plot.stereopair_metadata_parser
|
|
10
11
|
import asp_plot.scenes
|
|
11
12
|
import asp_plot.processing_parameters
|
|
12
13
|
import asp_plot.bundle_adjust
|
|
@@ -14,7 +15,8 @@ class TestImports:
|
|
|
14
15
|
|
|
15
16
|
def test_import_asp_plot_classes(self):
|
|
16
17
|
from asp_plot.utils import ColorBar, Raster, Plotter
|
|
17
|
-
from asp_plot.
|
|
18
|
+
from asp_plot.stereopair_metadata_parser import StereopairMetadataParser
|
|
19
|
+
from asp_plot.scenes import ScenePlotter, SceneGeometryPlotter
|
|
18
20
|
from asp_plot.processing_parameters import ProcessingParameters
|
|
19
|
-
from asp_plot.bundle_adjust import
|
|
21
|
+
from asp_plot.bundle_adjust import ReadBundleAdjustFiles, PlotBundleAdjustFiles
|
|
20
22
|
from asp_plot.stereo import StereoPlotter
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pytest
|
|
2
|
-
from asp_plot.scenes import ScenePlotter
|
|
2
|
+
from asp_plot.scenes import ScenePlotter, SceneGeometryPlotter
|
|
3
3
|
import matplotlib
|
|
4
4
|
|
|
5
5
|
matplotlib.use("Agg")
|
|
@@ -15,8 +15,21 @@ class TestScenePlotter:
|
|
|
15
15
|
)
|
|
16
16
|
return scene_plotter
|
|
17
17
|
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def scene_geometry_plotter(self):
|
|
20
|
+
scene_geometry_plotter = SceneGeometryPlotter(
|
|
21
|
+
directory="tests/test_data",
|
|
22
|
+
)
|
|
23
|
+
return scene_geometry_plotter
|
|
24
|
+
|
|
18
25
|
def test_plot_orthos(self, scene_plotter):
|
|
19
26
|
try:
|
|
20
27
|
scene_plotter.plot_orthos()
|
|
21
28
|
except Exception as e:
|
|
22
29
|
pytest.fail(f"figure method raised an exception: {str(e)}")
|
|
30
|
+
|
|
31
|
+
def test_dg_geom_plot(self, scene_geometry_plotter):
|
|
32
|
+
try:
|
|
33
|
+
scene_geometry_plotter.dg_geom_plot()
|
|
34
|
+
except Exception as e:
|
|
35
|
+
pytest.fail(f"figure method raised an exception: {str(e)}")
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import glob
|
|
3
|
-
import matplotlib.pyplot as plt
|
|
4
|
-
from dgtools.lib import dglib
|
|
5
|
-
from asp_plot.utils import Raster, Plotter, save_figure
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ScenePlotter(Plotter):
|
|
9
|
-
def __init__(self, directory, stereo_directory, **kwargs):
|
|
10
|
-
super().__init__(**kwargs)
|
|
11
|
-
self.directory = directory
|
|
12
|
-
self.stereo_directory = stereo_directory
|
|
13
|
-
|
|
14
|
-
try:
|
|
15
|
-
self.left_ortho_sub_fn = glob.glob(
|
|
16
|
-
os.path.join(self.directory, self.stereo_directory, "*-L_sub.tif")
|
|
17
|
-
)[0]
|
|
18
|
-
self.right_ortho_sub_fn = glob.glob(
|
|
19
|
-
os.path.join(self.directory, self.stereo_directory, "*-R_sub.tif")
|
|
20
|
-
)[0]
|
|
21
|
-
except:
|
|
22
|
-
raise ValueError(
|
|
23
|
-
"Could not find L-sub and R-sub images in stereo directory"
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
def get_names_and_gsd(self):
|
|
27
|
-
left_name, right_name = self.left_ortho_sub_fn.split("/")[-1].split("_")[2:4]
|
|
28
|
-
right_name = right_name.split("-")[0]
|
|
29
|
-
|
|
30
|
-
gsds = []
|
|
31
|
-
for image in [left_name, right_name]:
|
|
32
|
-
xml_fn = glob.glob(os.path.join(self.directory, f"{image}*.xml"))[0]
|
|
33
|
-
gsd = dglib.getTag(xml_fn, "MEANPRODUCTGSD")
|
|
34
|
-
if gsd is None:
|
|
35
|
-
gsd = dglib.getTag(xml_fn, "MEANCOLLECTEDGSD")
|
|
36
|
-
gsds.append(round(float(gsd), 2))
|
|
37
|
-
|
|
38
|
-
scene_dict = {
|
|
39
|
-
"left_name": left_name,
|
|
40
|
-
"right_name": right_name,
|
|
41
|
-
"left_gsd": gsds[0],
|
|
42
|
-
"right_gsd": gsds[1],
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return scene_dict
|
|
46
|
-
|
|
47
|
-
def plot_orthos(self, save_dir=None, fig_fn=None):
|
|
48
|
-
scene_dict = self.get_names_and_gsd()
|
|
49
|
-
|
|
50
|
-
fig, axa = plt.subplots(1, 2, figsize=(10, 5), dpi=300)
|
|
51
|
-
fig.suptitle(self.title, size=10)
|
|
52
|
-
axa = axa.ravel()
|
|
53
|
-
|
|
54
|
-
ortho_ma = Raster(self.left_ortho_sub_fn).read_array()
|
|
55
|
-
self.plot_array(ax=axa[0], array=ortho_ma, cmap="gray", add_cbar=False)
|
|
56
|
-
axa[0].set_title(
|
|
57
|
-
f"Left image\n{scene_dict['left_name']}, {scene_dict['left_gsd']:0.2f} m"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
ortho_ma = Raster(self.right_ortho_sub_fn).read_array()
|
|
61
|
-
self.plot_array(ax=axa[1], array=ortho_ma, cmap="gray", add_cbar=False)
|
|
62
|
-
axa[1].set_title(
|
|
63
|
-
f"Right image\n{scene_dict['right_name']}, {scene_dict['right_gsd']:0.2f} m"
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
fig.tight_layout()
|
|
67
|
-
if save_dir and fig_fn:
|
|
68
|
-
save_figure(fig, save_dir, fig_fn)
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from asp_plot.bundle_adjust import ReadResiduals, PlotResiduals
|
|
3
|
-
import matplotlib
|
|
4
|
-
import geopandas as gpd
|
|
5
|
-
import pandas as pd
|
|
6
|
-
|
|
7
|
-
matplotlib.use("Agg")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TestBundleAdjust:
|
|
11
|
-
@pytest.fixture
|
|
12
|
-
def residual_files(self):
|
|
13
|
-
directory = "tests/test_data"
|
|
14
|
-
ba_directory = "ba"
|
|
15
|
-
return ReadResiduals(directory, ba_directory)
|
|
16
|
-
|
|
17
|
-
def test_get_init_final_residuals_gdfs(self, residual_files):
|
|
18
|
-
resid_init, resid_final = residual_files.get_init_final_residuals_gdfs()
|
|
19
|
-
assert isinstance(resid_init, gpd.GeoDataFrame)
|
|
20
|
-
assert isinstance(resid_final, gpd.GeoDataFrame)
|
|
21
|
-
|
|
22
|
-
def test_get_mapproj_residuals_gdf(self, residual_files):
|
|
23
|
-
resid_mapprojected_gdf = residual_files.get_mapproj_residuals_gdf()
|
|
24
|
-
assert isinstance(resid_mapprojected_gdf, gpd.GeoDataFrame)
|
|
25
|
-
|
|
26
|
-
def test_get_propagated_triangulation_uncert_df(self, residual_files):
|
|
27
|
-
resid_triangulation_uncert_df = residual_files.get_propagated_triangulation_uncert_df()
|
|
28
|
-
assert isinstance(resid_triangulation_uncert_df, pd.DataFrame)
|
|
29
|
-
|
|
30
|
-
def test_plot_n_residuals(self, residual_files):
|
|
31
|
-
resid_init, resid_final = residual_files.get_init_final_residuals_gdfs()
|
|
32
|
-
try:
|
|
33
|
-
PlotResiduals([resid_init, resid_final]).plot_n_residuals(column_name="mean_residual")
|
|
34
|
-
except Exception as e:
|
|
35
|
-
pytest.fail(f"figure method raised an exception: {str(e)}")
|
|
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
|