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.
Files changed (28) hide show
  1. {asp_plot-0.1.0 → asp_plot-0.2.0}/PKG-INFO +7 -1
  2. {asp_plot-0.1.0 → asp_plot-0.2.0}/README.md +6 -0
  3. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/__init__.py +1 -0
  4. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/bundle_adjust.py +51 -18
  5. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/cli/asp_plot.py +47 -29
  6. asp_plot-0.2.0/asp_plot/scenes.py +216 -0
  7. asp_plot-0.2.0/asp_plot/stereopair_metadata_parser.py +330 -0
  8. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/utils.py +20 -13
  9. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/PKG-INFO +7 -1
  10. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/SOURCES.txt +1 -0
  11. {asp_plot-0.1.0 → asp_plot-0.2.0}/pyproject.toml +1 -1
  12. {asp_plot-0.1.0 → asp_plot-0.2.0}/setup.py +1 -1
  13. {asp_plot-0.1.0 → asp_plot-0.2.0}/tests/__init__.py +1 -0
  14. asp_plot-0.2.0/tests/test_bundle_adjust.py +40 -0
  15. {asp_plot-0.1.0 → asp_plot-0.2.0}/tests/test_imports.py +4 -2
  16. {asp_plot-0.1.0 → asp_plot-0.2.0}/tests/test_scenes.py +14 -1
  17. asp_plot-0.1.0/asp_plot/scenes.py +0 -68
  18. asp_plot-0.1.0/tests/test_bundle_adjust.py +0 -35
  19. {asp_plot-0.1.0 → asp_plot-0.2.0}/LICENSE +0 -0
  20. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/cli/__init__.py +0 -0
  21. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/processing_parameters.py +0 -0
  22. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot/stereo.py +0 -0
  23. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/dependency_links.txt +0 -0
  24. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/entry_points.txt +0 -0
  25. {asp_plot-0.1.0 → asp_plot-0.2.0}/asp_plot.egg-info/top_level.txt +0 -0
  26. {asp_plot-0.1.0 → asp_plot-0.2.0}/setup.cfg +0 -0
  27. {asp_plot-0.1.0 → asp_plot-0.2.0}/tests/test_processing_parameters.py +0 -0
  28. {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.1.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
@@ -39,6 +39,12 @@ $ pytest
39
39
 
40
40
  ### Package and upload
41
41
 
42
+ ```
43
+ $ rm -rf dist/
44
+ ```
45
+
46
+ Then update version in `pyproject.toml` and `setup.py`, then:
47
+
42
48
  ```
43
49
  $ python3 -m pip install --upgrade build
44
50
  $ python3 -m build
@@ -1,4 +1,5 @@
1
1
  import asp_plot.utils
2
+ import asp_plot.stereopair_metadata_parser
2
3
  import asp_plot.processing_parameters
3
4
  import asp_plot.scenes
4
5
  import asp_plot.bundle_adjust
@@ -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 ReadResiduals:
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 get_init_final_residuals_csvs(self):
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
- os.path.join(self.directory, self.bundle_adjust_directory, filename)
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"Residuals CSV file not found: {path}")
32
+ raise ValueError(f"CSV file not found: {path}")
32
33
 
33
- resid_init_path, resid_final_path = paths
34
- return resid_init_path, resid_final_path
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 get_init_final_residuals_gdfs(self):
70
- resid_init_path, resid_final_path = self.get_init_final_residuals_csvs()
71
- resid_init_gdf = self.get_residuals_gdf(resid_init_path)
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 resid_init_gdf, resid_final_gdf
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 PlotResiduals(Plotter):
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 get_residual_stats(self, gdf, column_name="mean_residual"):
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 plot_n_residuals(
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], gdf=gdf, column_name=column_name, cbar_label=cbar_label
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.get_residual_stats(gdf, column_name)
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 ReadResiduals, PlotResiduals
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="asp_plots",
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
- try:
73
- subprocess.run(["dg_geom_plot.py", directory])
74
- subprocess.run(
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="01_orthos.png")
82
+ plotter.plot_orthos(save_dir=plots_directory, fig_fn=f"{next(figure_counter):02}.png")
87
83
 
88
84
  # Bundle adjustment plots
89
- residuals = ReadResiduals(directory, bundle_adjust_directory)
90
- resid_init_gdf, resid_final_gdf = residuals.get_init_final_residuals_gdfs()
91
- resid_mapprojected_gdf = residuals.get_mapproj_residuals_gdf()
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 = PlotResiduals(
101
- [resid_init_gdf, resid_final_gdf],
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.plot_n_residuals(
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="02_ba_residuals_log.png",
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.plot_n_residuals(
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="03_ba_residuals_linear.png",
121
+ fig_fn=f"{next(figure_counter):02}.png",
125
122
  **ctx_kwargs,
126
123
  )
127
124
 
128
- plotter = PlotResiduals(
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.plot_n_residuals(
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="04_ba_residuals_mapproj_dist.png",
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="05_stereo_match_points.png",
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="06_disparity_pixels.png",
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="07_stereo_dem_results.png",
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
- from pygeotools.lib import iolib, warplib
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, outdir=None):
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
- ds_list = warplib.memwarp_multi_fn(
149
- fn_list, extent="intersection", res="max", t_srs="first", r="cubic"
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
- iolib.writeGTiff(diff, dst_fn, r1_ds, ndv=-9999)
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.1.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
@@ -7,6 +7,7 @@ asp_plot/bundle_adjust.py
7
7
  asp_plot/processing_parameters.py
8
8
  asp_plot/scenes.py
9
9
  asp_plot/stereo.py
10
+ asp_plot/stereopair_metadata_parser.py
10
11
  asp_plot/utils.py
11
12
  asp_plot.egg-info/PKG-INFO
12
13
  asp_plot.egg-info/SOURCES.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "asp_plot"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  authors = [
9
9
  { name="Ben Purinton", email="purinton@uw.edu" },
10
10
  ]
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
4
4
 
5
5
  setup(
6
6
  name="asp_plot",
7
- version="0.1.0",
7
+ version="0.2.0",
8
8
  description="Python library and client for plotting Ames Stereo Pipeline outputs",
9
9
  author="Ben Purinton",
10
10
  author_email="purinton@uw.edu",
@@ -1,4 +1,5 @@
1
1
  import asp_plot.utils
2
+ import asp_plot.stereopair_metadata_parser
2
3
  import asp_plot.processing_parameters
3
4
  import asp_plot.scenes
4
5
  import asp_plot.bundle_adjust
@@ -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.scenes import ScenePlotter
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 ReadResiduals, PlotResiduals
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