Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete rewrite into modular package with CLI #3

Merged
merged 28 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
43fb5a4
refactoring into package
bpurinton Sep 11, 2023
58cc631
add percentiles to ba plots
bpurinton May 10, 2024
07b7aaf
class based inheritence of Plotter
bpurinton May 11, 2024
e732edb
format with black
bpurinton May 11, 2024
ec8d129
wip - finished ba plots, fixed scenes to add name and gsd
bpurinton May 12, 2024
841fd87
refactor processing_parameters to only take directory args
bpurinton May 12, 2024
2c1037d
updated tests, all green
bpurinton May 12, 2024
f313bd7
wip - stereo plots
bpurinton May 13, 2024
763f72c
match point plots lookin good
bpurinton May 13, 2024
6469b15
disparity plots lookin good
bpurinton May 13, 2024
3f6f85a
wip - scale disparity correctly
bpurinton May 13, 2024
2833366
fixes to stereo plots
bpurinton May 13, 2024
121c6b4
plots fixed, need to update specs
bpurinton May 13, 2024
65ef18d
specs passing, need to add for stereo
bpurinton May 13, 2024
3e11514
added tests for stereo, some bug fixes
bpurinton May 15, 2024
d443404
tested with WV03, works, couple more bug fixes
bpurinton May 15, 2024
731a7b9
only pass directory to ba, not the prefix
bpurinton May 15, 2024
4cd96c9
format
bpurinton May 16, 2024
9e71073
fixed some math
bpurinton May 16, 2024
9ff110e
add clim options to results plot
bpurinton May 16, 2024
9413c37
save figure working
bpurinton May 16, 2024
d3c9a5c
saving all figs out to report dir
bpurinton May 16, 2024
234fa5e
pdf report generated
bpurinton May 16, 2024
96744c4
better report
bpurinton May 19, 2024
4f942ae
CLI working
bpurinton May 21, 2024
062268c
small typo
bpurinton Jun 4, 2024
8c1e2b8
update README
bpurinton Jun 5, 2024
44ce0aa
update author email
bpurinton Jun 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ venv/
ENV/
env.bak/
venv.bak/
.vscode/

# Spyder project settings
.spyderproject
Expand Down
57 changes: 55 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,61 @@
# asp_plot

Scripts and notebooks to visualize output from the [NASA Ames Stereo Pipeline (ASP)](https://github.com/NeoGeographyToolkit/StereoPipeline).

## Motivation
Our objective is to release a set of standalone Python scripts that can be run automatically on an ASP output directory to prepare a set of standard diagnostic plots, publication-quality output figures, and a pdf report with relevant information, similar to the reports prepared by many commercial SfM software packages (e.g., Agisoft Metashape, Pix4DMapper).

Our objective is to release a modular Python package with a command-line interface (CLI) that can be run automatically on an ASP output directory to prepare a set of standard diagnostic plots, publication-quality output figures, and a pdf report with relevant information, similar to the reports prepared by many commercial SfM software packages (e.g., Agisoft Metashape, Pix4DMapper).


## Status
This is a work in progress, with initial notebooks compiled from recent projects using sample stereo images from the Maxar WorldView, Planet SkySat-C and BlackSky Global constellations. We plan to refine these tools in the coming year, and we welcome community contributions and input.

This is a work in progress.

The directory `original_code/` contains initial notebooks compiled from recent projects using sample stereo images from the Maxar WorldView, Planet SkySat-C and BlackSky Global constellations.

The functionality of these notebooks is being ported to the `asp_plot/` directory, which is the package `asp_plot`.


## Installing and testing the package

```
$ git clone [email protected]:uw-cryo/asp_plot.git
$ cd asp_plot
$ conda env create -f environment.yml
$ conda activate asp_plot
$ pip install -e .
$ python3 setup.py install
```

To ensure the install was successful, tests can be run with:

```
$ pytest
```

## Notebook example usage

Examples of the modular usage of the package can be found in the `notebooks/` directory.


## CLI usage

A full report and individual plots can be output via the command-line:

```
$ asp_plot --directory ./asp_processing \
--bundle_adjust_directory ba \
--stereo_directory stereo \
--map_crs EPSG:32604 \
--reference_dem ref_dem.tif \
--plots_directory asp_plots \
--report_filename asp_plot_report.pdf
```

Use:

```
$ asp_plot --help
```

for details (and defaults) of the command-line flags.
5 changes: 5 additions & 0 deletions asp_plot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import asp_plot.utils
import asp_plot.processing_parameters
import asp_plot.scenes
import asp_plot.bundle_adjust
import asp_plot.stereo
212 changes: 212 additions & 0 deletions asp_plot/bundle_adjust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import os
import glob
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
import contextily as ctx
from asp_plot.utils import ColorBar, Plotter, save_figure


class ReadResiduals:
def __init__(self, directory, bundle_adjust_directory):
self.directory = directory
self.bundle_adjust_directory = bundle_adjust_directory

def get_init_final_residuals_csvs(self):
filenames = [
"*-initial_residuals_pointmap.csv",
"*-final_residuals_pointmap.csv",
]

paths = [
glob.glob(
os.path.join(self.directory, self.bundle_adjust_directory, filename)
)[0]
for filename in filenames
]

for path in paths:
if not os.path.isfile(path):
raise ValueError(f"Residuals CSV file not found: {path}")

resid_init_path, resid_final_path = paths
return resid_init_path, resid_final_path

def get_residuals_gdf(self, csv_path):
cols = [
"lon",
"lat",
"height_above_datum",
"mean_residual",
"num_observations",
]

resid_df = pd.read_csv(csv_path, skiprows=2, names=cols)

# Need the astype('str') to handle cases where column has dtype of int (without the # from DEM appended to some rows)
resid_df["from_DEM"] = (
resid_df["num_observations"].astype("str").str.contains("# from DEM")
)

resid_df["num_observations"] = (
resid_df["num_observations"]
.astype("str")
.str.split("#", expand=True)[0]
.astype(int)
)

resid_gdf = gpd.GeoDataFrame(
resid_df,
geometry=gpd.points_from_xy(
resid_df["lon"], resid_df["lat"], crs="EPSG:4326"
),
)

resid_gdf.filename = os.path.basename(csv_path)
return resid_gdf

def get_init_final_residuals_gdfs(self):
resid_init_path, resid_final_path = self.get_init_final_residuals_csvs()
resid_init_gdf = self.get_residuals_gdf(resid_init_path)
resid_final_gdf = self.get_residuals_gdf(resid_final_path)
return resid_init_gdf, resid_final_gdf

def get_mapproj_residuals_gdf(self):
path = glob.glob(
os.path.join(
self.directory,
self.bundle_adjust_directory,
"*-mapproj_match_offsets.txt",
)
)[0]
if not os.path.isfile(path):
raise ValueError(f"MapProj Residuals TXT file not found: {path}")

cols = ["lon", "lat", "height_above_datum", "mapproj_ip_dist_meters"]
resid_mapprojected_df = pd.read_csv(path, skiprows=2, names=cols)
resid_mapprojected_gdf = gpd.GeoDataFrame(
resid_mapprojected_df,
geometry=gpd.points_from_xy(
resid_mapprojected_df["lon"],
resid_mapprojected_df["lat"],
crs="EPSG:4326",
),
)
return resid_mapprojected_gdf

def get_propagated_triangulation_uncert_df(self):
path = glob.glob(
os.path.join(
self.directory,
self.bundle_adjust_directory,
"*-triangulation_uncertainty.txt",
)
)[0]
if not os.path.isfile(path):
raise ValueError(f"Triangulation Uncertainty TXT file not found: {path}")

cols = [
"left_image",
"right_image",
"horiz_error_median",
"vert_error_median",
"horiz_error_mean",
"vert_error_mean",
"horiz_error_stddev",
"vert_error_stddev",
"num_meas",
]
resid_triangulation_uncert_df = pd.read_csv(
path, sep=" ", skiprows=2, names=cols
)
return resid_triangulation_uncert_df


class PlotResiduals(Plotter):
def __init__(self, geodataframes, **kwargs):
super().__init__(**kwargs)
if not isinstance(geodataframes, list):
raise ValueError("Input must be a list of GeoDataFrames")
self.geodataframes = geodataframes

def get_residual_stats(self, gdf, column_name="mean_residual"):
stats = gdf[column_name].quantile([0.25, 0.50, 0.84, 0.95]).round(2).tolist()
return stats

def plot_n_residuals(
self,
column_name="mean_residual",
cbar_label="Mean Residual (m)",
clip_final=True,
clim=None,
common_clim=True,
cmap="inferno",
map_crs="EPSG:4326",
save_dir=None,
fig_fn=None,
**ctx_kwargs,
):

# Get rows and columns and create subplots
n = len(self.geodataframes)
nrows = (n + 3) // 4
ncols = min(n, 4)
if n == 1:
fig, axa = plt.subplots(1, 1, figsize=(8, 6))
axa = [axa]
else:
fig, axa = plt.subplots(
nrows, ncols, figsize=(4 * ncols, 3 * nrows), sharex=True, sharey=True
)
axa = axa.flatten()

# Plot each GeoDataFrame
for i, gdf in enumerate(self.geodataframes):
gdf = gdf.sort_values(by=column_name).to_crs(map_crs)

if clim is None:
clim = ColorBar().get_clim(gdf[column_name])

if common_clim:
self.plot_geodataframe(
ax=axa[i],
gdf=gdf,
clim=clim,
column_name=column_name,
cbar_label=cbar_label,
)
else:
self.plot_geodataframe(
ax=axa[i], gdf=gdf, column_name=column_name, cbar_label=cbar_label
)

ctx.add_basemap(ax=axa[i], **ctx_kwargs)

if clip_final and i == n - 1:
axa[i].autoscale(False)

# Show some statistics and information
stats = self.get_residual_stats(gdf, column_name)
stats_text = f"(n={gdf.shape[0]})\n" + "\n".join(
f"{quantile*100:.0f}th: {stat}"
for quantile, stat in zip([0.25, 0.50, 0.84, 0.95], stats)
)
axa[i].text(
0.05,
0.95,
stats_text,
transform=axa[i].transAxes,
fontsize=8,
verticalalignment="top",
bbox=dict(boxstyle="round", facecolor="white", alpha=0.8),
)

# Clean up axes and tighten layout
for i in range(n, nrows * ncols):
fig.delaxes(axa[i])
fig.suptitle(self.title, size=10)
plt.subplots_adjust(wspace=0.2, hspace=0.4)
fig.tight_layout()
if save_dir and fig_fn:
save_figure(fig, save_dir, fig_fn)
Empty file added asp_plot/cli/__init__.py
Empty file.
Loading