diff --git a/annotation_gui_gcp/GUI.py b/annotation_gui_gcp/GUI.py index 62a4fd4ce..051bdfa66 100644 --- a/annotation_gui_gcp/GUI.py +++ b/annotation_gui_gcp/GUI.py @@ -11,8 +11,8 @@ matplotlib.use("TkAgg") -from .image_sequence_view import ImageSequenceView -from .orthophoto_view import OrthoPhotoView +from image_sequence_view import ImageSequenceView +from orthophoto_view import OrthoPhotoView FONT = "TkFixedFont" @@ -34,7 +34,7 @@ def __init__( master.bind_all("z", lambda event: self.toggle_zoom_all_views()) master.bind_all("x", lambda event: self.toggle_sticky_zoom()) master.bind_all("a", lambda event: self.go_to_current_gcp()) - self.get_reconstruction_options() + self.reconstruction_options = self.get_reconstruction_options() self.create_ui(ortho_paths) master.lift() @@ -47,7 +47,7 @@ def get_reconstruction_options(self): p_recs = self.path + "/reconstruction.json" print(p_recs) if not os.path.exists(p_recs): - return {} + return ["NONE", "NONE"] data = dataset.DataSet(self.path) recs = data.load_reconstruction() options = [] @@ -60,7 +60,7 @@ def get_reconstruction_options(self): ) options.append(str_repr) options.append("None (3d-to-2d)") - self.reconstruction_options = options + return options def create_ui(self, ortho_paths): tools_frame = tk.Frame(self.master) @@ -392,7 +392,7 @@ def go_to_worst_gcp(self): if len(self.gcp_manager.gcp_reprojections) == 0: print("No GCP reprojections available. Can't jump to worst GCP") return - worst_gcp = self.gcp_manager.compute_gcp_errors()[0] + worst_gcp = self.gcp_manager.get_worst_gcp() if worst_gcp is None: return diff --git a/annotation_gui_gcp/__init__.py b/annotation_gui_gcp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/annotation_gui_gcp/gcp_manager.py b/annotation_gui_gcp/gcp_manager.py index ca2cb7ca5..1ef940cf3 100644 --- a/annotation_gui_gcp/gcp_manager.py +++ b/annotation_gui_gcp/gcp_manager.py @@ -38,8 +38,6 @@ def load_from_file(self, file_path): self.points[point["id"]] = point["observations"] latlon = point.get("position") if latlon: - if "altitude" in latlon: - raise NotImplementedError("Not supported: altitude in GCPs") self.latlons[point["id"]] = latlon def write_to_file(self, filename): @@ -87,26 +85,24 @@ def add_point_observation(self, point_id, shot_id, projection, latlon=None): } ) - def compute_gcp_errors(self): - error_avg = {} + def get_worst_gcp(self): worst_gcp_error = 0 worst_gcp = None - shot_worst_gcp = None - for gcp_id in self.points: - error_avg[gcp_id] = 0 for gcp_id in self.gcp_reprojections: if gcp_id not in self.points: continue for shot_id in self.gcp_reprojections[gcp_id]: err = self.gcp_reprojections[gcp_id][shot_id]["error"] - error_avg[gcp_id] += err if err > worst_gcp_error: worst_gcp_error = err - shot_worst_gcp = shot_id worst_gcp = gcp_id - error_avg[gcp_id] /= len(self.gcp_reprojections[gcp_id]) - return worst_gcp, shot_worst_gcp, worst_gcp_error, error_avg + errors_worst_gcp = [ + x["error"] for x in self.gcp_reprojections[worst_gcp].values() + ] + n = len(errors_worst_gcp) + print(f"Worst GCP: {worst_gcp} unconfirmed in {n} images") + return worst_gcp def shot_with_max_gcp_error(self, image_keys, gcp): # Return they key with most reprojection error for this GCP diff --git a/annotation_gui_gcp/image_manager.py b/annotation_gui_gcp/image_manager.py index 82ed25a9b..5012e9e1b 100644 --- a/annotation_gui_gcp/image_manager.py +++ b/annotation_gui_gcp/image_manager.py @@ -17,6 +17,9 @@ def load_image(path): new_w = int(round(rgb.size[0] / scale)) new_h = int(round(rgb.size[1] / scale)) rgb = rgb.resize((new_w, new_h), resample=Image.BILINEAR) + + if rgb.mode == 'L': + rgb = rgb.convert("RGB") # Matplotlib will transform to rgba when plotting return _rgb_to_rgba(np.asarray(rgb)) diff --git a/annotation_gui_gcp/image_sequence_view.py b/annotation_gui_gcp/image_sequence_view.py index 312805221..7988a4a3c 100644 --- a/annotation_gui_gcp/image_sequence_view.py +++ b/annotation_gui_gcp/image_sequence_view.py @@ -4,8 +4,8 @@ import numpy as np from matplotlib import pyplot as plt -from .geometry import get_all_track_observations, get_tracks_visible_in_image -from .view import View +from geometry import get_all_track_observations, get_tracks_visible_in_image +from view import View class ImageSequenceView(View): diff --git a/annotation_gui_gcp/main.py b/annotation_gui_gcp/main.py index 65700f2d0..5be9cd9c4 100644 --- a/annotation_gui_gcp/main.py +++ b/annotation_gui_gcp/main.py @@ -5,9 +5,9 @@ from opensfm import dataset, io -from . import GUI -from .gcp_manager import GroundControlPointManager -from .image_manager import ImageManager +import GUI +from gcp_manager import GroundControlPointManager +from image_manager import ImageManager def parse_args(): diff --git a/annotation_gui_gcp/orthophoto_view.py b/annotation_gui_gcp/orthophoto_view.py index 4a3594fd5..4b7e213b5 100644 --- a/annotation_gui_gcp/orthophoto_view.py +++ b/annotation_gui_gcp/orthophoto_view.py @@ -4,8 +4,8 @@ import rasterio.warp from opensfm import features -from .orthophoto_manager import OrthoPhotoManager -from .view import View +from orthophoto_manager import OrthoPhotoManager +from view import View class OrthoPhotoView(View): diff --git a/annotation_gui_gcp/requirements.txt b/annotation_gui_gcp/requirements.txt index 6ccafc3f9..796e83d84 100644 --- a/annotation_gui_gcp/requirements.txt +++ b/annotation_gui_gcp/requirements.txt @@ -1 +1,2 @@ matplotlib +rasterio diff --git a/bin/opensfm b/bin/opensfm index b5ee4b15d..62a50af95 100755 --- a/bin/opensfm +++ b/bin/opensfm @@ -9,4 +9,5 @@ else PYTHON=python fi +echo "${PYTHON} ${DIR}/opensfm_main.py $@" "$PYTHON" "$DIR"/opensfm_main.py "$@" diff --git a/bin/opensfm_run_all b/bin/opensfm_run_all index b2c6cb22e..52508c65b 100755 --- a/bin/opensfm_run_all +++ b/bin/opensfm_run_all @@ -9,6 +9,8 @@ DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) "$DIR"/opensfm match_features "$1" "$DIR"/opensfm create_tracks "$1" "$DIR"/opensfm reconstruct "$1" -"$DIR"/opensfm mesh "$1" -"$DIR"/opensfm undistort "$1" -"$DIR"/opensfm compute_depthmaps "$1" +# "$DIR"/opensfm mesh "$1" +# "$DIR"/opensfm undistort "$1" +# "$DIR"/opensfm compute_depthmaps "$1" +"$DIR"/opensfm compute_statistics "$1" +"$DIR"/opensfm export_report "$1" \ No newline at end of file diff --git a/opensfm/actions/compute_statistics.py b/opensfm/actions/compute_statistics.py index d2c4c3e24..f4b906f84 100644 --- a/opensfm/actions/compute_statistics.py +++ b/opensfm/actions/compute_statistics.py @@ -22,6 +22,7 @@ def run_dataset(data): stats_dict = stats.compute_all_statistics(data, tracks_manager, reconstructions) + stats.save_sequencegraph(data, reconstructions, output_path) stats.save_residual_grids(data, tracks_manager, reconstructions, output_path) stats.save_matchgraph(data, tracks_manager, reconstructions, output_path) stats.save_heatmap(data, tracks_manager, reconstructions, output_path) diff --git a/opensfm/dataset.py b/opensfm/dataset.py index f5a0a2fc8..797b78651 100644 --- a/opensfm/dataset.py +++ b/opensfm/dataset.py @@ -488,8 +488,8 @@ def invent_reference_lla(self, images=None): if not wlat and not wlon: for gcp in self.load_ground_control_points_impl(None): - lat += gcp.lla["latitude"] - lon += gcp.lla["longitude"] + lat += gcp.lla.get("latitude", 0.) + lon += gcp.lla.get("longitude", 0.) wlat += 1 wlon += 1 diff --git a/opensfm/stats.py b/opensfm/stats.py index da6d6a1fe..32392160e 100644 --- a/opensfm/stats.py +++ b/opensfm/stats.py @@ -2,7 +2,7 @@ import math import os import statistics -from collections import defaultdict +from collections import defaultdict, OrderedDict from functools import lru_cache import matplotlib.cm as cm @@ -380,6 +380,57 @@ def save_matchgraph(data, tracks_manager, reconstructions, output_path): ) +def load_sequence_database_from_file(data_path, fname="sequence_database.json"): + """ + Simply loads a sequence file and returns it. + This doesn't require an existing SfM reconstruction + """ + p_json = os.path.join(data_path, fname) + if not os.path.isfile(p_json): + return None + seq_dict = OrderedDict(io.json_load(open(p_json, "r"))) + + return seq_dict + + +def save_sequencegraph(data, reconstructions, output_path): + shot_sequences = {} + data_path = data.data_path + seq_dict = load_sequence_database_from_file(data_path) + sequences = list(seq_dict.keys()) + image_xyz_per_sequences = {seq: [] for seq in sequences} + + for i, rec in enumerate(reconstructions): + for shot_name, shot in rec.shots.items(): + shot_sequence = None + shot_sequences[shot_name] = None + for seq, shot_list in seq_dict.items(): + if shot_name in shot_list: + shot_sequence = seq + break + xyz = shot.pose.get_origin() + image_xyz_per_sequences[shot_sequence].append(xyz) + + plt.clf() + cmap = cm.get_cmap("rainbow") + + for seq, image_xyz in image_xyz_per_sequences.items(): + xyz = np.array(image_xyz) + c = sequences.index(seq) / len(sequences) + plt.scatter(xyz[:, 1], xyz[:, 2], color=cmap(c), label=seq, s=2) + plt.legend() + plt.grid() + plt.xlabel('Y[m]') + plt.ylabel('Z[m]') + plt.axis('equal') + + plt.savefig( + os.path.join(output_path, "sequencegraph.png"), + dpi=300, + bbox_inches="tight", + ) + + def save_topview(data, tracks_manager, reconstructions, output_path): points = [] colors = [] @@ -410,6 +461,8 @@ def save_topview(data, tracks_manager, reconstructions, output_path): if not shot.metadata.gps_position.has_value: continue gps = shot.metadata.gps_position.value + if gps[0] == gps[1] == gps[2] == 0.: + continue all_x.append(gps[0]) all_y.append(gps[1]) @@ -506,6 +559,9 @@ def save_topview(data, tracks_manager, reconstructions, output_path): if not shot.metadata.gps_position.has_value: continue gps = shot.metadata.gps_position.value + if gps[0] == gps[1] == gps[2] == 0.: + continue + gps_x, gps_y = int((gps[0] - low_x) / size_x * im_size_x), int( (gps[1] - low_y) / size_y * im_size_y ) diff --git a/requirements.txt b/requirements.txt index 9cf56efc7..14d46be02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,9 +3,9 @@ exifread==2.1.2 fpdf2==2.1.0 joblib==0.14.1 matplotlib -networkx==1.11 +networkx==2.5.0 numpy -Pillow==7.1.0 +Pillow pyproj>=1.9.5.1 pytest==3.0.7 python-dateutil==2.6.0