Skip to content

Commit 398fe61

Browse files
YanNounfacebook-github-bot
authored andcommitted
feat: angular residual errors
Summary: This Diff add angular errors to OpenSfM quality report. Reviewed By: paulinus Differential Revision: D28475730 fbshipit-source-id: 7f22e6b7fd7c461739a4363c699ebc8ecabbccbe
1 parent d75ea71 commit 398fe61

7 files changed

Lines changed: 98 additions & 25 deletions

File tree

opensfm/pymap.pyi

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,20 @@ class Map:
162162
def update_rig_instance(self, arg0) -> RigInstance: ...
163163
def update_shot(self, arg0) -> Shot: ...
164164

165+
class ErrorType:
166+
Pixel: Any = ...
167+
Normalized: Any = ...
168+
Angular: Any = ...
169+
def __init__(self, arg0: int) -> None: ...
170+
def __eq__(self, arg0: ErrorType) -> bool: ...
171+
def __getstate__(self) -> tuple: ...
172+
def __hash__(self) -> int: ...
173+
def __int__(self) -> int: ...
174+
def __ne__(self, arg0: ErrorType) -> bool: ...
175+
def __setstate__(self, arg0: tuple) -> None: ...
176+
@property
177+
def __members__(self) -> dict: ...
178+
165179
class PanoShotView:
166180
def __init__(self, arg0: Map) -> None: ...
167181
def get(self, arg0: str) -> Shot: ...

opensfm/report.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,11 @@ def make_reconstruction_details(self):
319319

320320
rows = [
321321
[
322-
"Average Reprojection Error (normalized / pixels)",
322+
"Average Reprojection Error (normalized / pixels / angular)",
323323
(
324324
f"{self.stats['reconstruction_statistics']['reprojection_error_normalized']:.2f} / "
325-
f"{self.stats['reconstruction_statistics']['reprojection_error_pixels']:.2f}"
325+
f"{self.stats['reconstruction_statistics']['reprojection_error_pixels']:.2f} / "
326+
f"{self.stats['reconstruction_statistics']['reprojection_error_angular']:.5f}"
326327
),
327328
],
328329
[

opensfm/src/map/map.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,10 @@ class Map {
169169
TracksManager ToTracksManager() const;
170170

171171
// Tracks manager x Reconstruction intersection functions
172+
enum ErrorType { Pixel = 0x0, Normalized = 0x1, Angular = 0x2 };
172173
std::unordered_map<ShotId, std::unordered_map<LandmarkId, Vec2d> >
173174
ComputeReprojectionErrors(const TracksManager& tracks_manager,
174-
bool scaled) const;
175+
const ErrorType& error_type) const;
175176
std::unordered_map<ShotId, std::unordered_map<LandmarkId, Observation> >
176177
GetValidObservations(const TracksManager& tracks_manager) const;
177178

opensfm/src/map/python/pybind.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ void DeclareShotMeasurement(py::module &m, const std::string &type_name) {
4545
}));
4646
}
4747
PYBIND11_MODULE(pymap, m) {
48+
py::enum_<map::Map::ErrorType>(m, "ErrorType")
49+
.value("Pixel", map::Map::Pixel)
50+
.value("Normalized", map::Map::Normalized)
51+
.value("Angular", map::Map::Angular)
52+
.export_values();
53+
4854
py::class_<map::Map>(m, "Map")
4955
.def(py::init())
5056
// Camera

opensfm/src/map/src/map.cc

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <map/rig.h>
55
#include <map/shot.h>
66

7+
#include <cmath>
78
#include <unordered_set>
89

910
namespace map {
@@ -364,7 +365,7 @@ RigInstance& Map::GetRigInstance(const RigInstanceId& instance_id) {
364365
return it->second;
365366
}
366367

367-
const RigInstance& Map::GetRigInstance(const RigInstanceId& instance_id) const{
368+
const RigInstance& Map::GetRigInstance(const RigInstanceId& instance_id) const {
368369
const auto& it = rig_instances_.find(instance_id);
369370
if (it == rig_instances_.end()) {
370371
throw std::runtime_error("Accessing invalid RigInstance index");
@@ -378,7 +379,7 @@ bool Map::HasRigInstance(const RigInstanceId& instance_id) const {
378379

379380
std::unordered_map<ShotId, std::unordered_map<LandmarkId, Vec2d> >
380381
Map::ComputeReprojectionErrors(const TracksManager& tracks_manager,
381-
bool scaled) const {
382+
const Map::ErrorType& error_type) const {
382383
std::unordered_map<ShotId, std::unordered_map<LandmarkId, Vec2d> > errors;
383384
for (const auto& shot_id : tracks_manager.GetShotIds()) {
384385
const auto find_shot = shots_.find(shot_id);
@@ -394,11 +395,27 @@ Map::ComputeReprojectionErrors(const TracksManager& tracks_manager,
394395
continue;
395396
}
396397

397-
const Vec2d error_2d =
398-
(track_n_obs.second.point -
399-
shot.Project(find_landmark->second.GetGlobalPos()));
400-
const auto scale = scaled ? track_n_obs.second.scale : 1.0;
401-
per_shot[track_n_obs.first] = error_2d / scale;
398+
if (error_type == Map::ErrorType::Pixel) {
399+
const Vec2d error_2d =
400+
(track_n_obs.second.point -
401+
shot.Project(find_landmark->second.GetGlobalPos()));
402+
per_shot[track_n_obs.first] = error_2d;
403+
}
404+
if (error_type == Map::ErrorType::Normalized) {
405+
const Vec2d error_2d =
406+
(track_n_obs.second.point -
407+
shot.Project(find_landmark->second.GetGlobalPos()));
408+
per_shot[track_n_obs.first] = error_2d / track_n_obs.second.scale;
409+
}
410+
if (error_type == Map::ErrorType::Angular) {
411+
const Vec3d point =
412+
(find_landmark->second.GetGlobalPos() - shot.GetPose()->GetOrigin())
413+
.normalized();
414+
const Vec3d bearing =
415+
shot.Bearing(track_n_obs.second.point).normalized();
416+
const double angle = std::acos(point.dot(bearing));
417+
per_shot[track_n_obs.first] = Vec2d::Constant(angle);
418+
}
402419
}
403420
}
404421
return errors;

opensfm/src/map/test/map_test.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ TEST_F(EmptyMapFixture, ConstructSmallProblem) {
245245
ASSERT_EQ(map.GetLandmarks().size(), n_points);
246246
}
247247

248-
TEST_F(OneCameraMapFixture, ComputeReprojectionError) {
248+
TEST_F(OneCameraMapFixture, ComputeReprojectionErrorNormalized) {
249249
const auto& shot = map.CreateShot("0", "0");
250250
Eigen::Vector3d pos = Eigen::Vector3d::Random();
251251
map.CreateLandmark("1", pos);
@@ -257,7 +257,8 @@ TEST_F(OneCameraMapFixture, ComputeReprojectionError) {
257257
const Observation o(0., 0., scale, 1, 1, 1, 1, 1, 1);
258258
manager.AddObservation("0", "1", o);
259259

260-
auto errors = map.ComputeReprojectionErrors(manager, true);
260+
auto errors =
261+
map.ComputeReprojectionErrors(manager, map::Map::ErrorType::Normalized);
261262
const auto computed = errors["0"]["1"];
262263
ASSERT_NEAR(expected[0] / scale, computed[0], 1e-8);
263264
ASSERT_NEAR(expected[1] / scale, computed[1], 1e-8);

opensfm/stats.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import matplotlib.colors as colors
1212
import matplotlib.pyplot as plt
1313
import numpy as np
14-
from opensfm import io, multiview, feature_loader
14+
from opensfm import io, multiview, feature_loader, pymap
1515
from opensfm.dataset import DataSet, DataSetBase
1616

1717
RESIDUAL_PIXEL_CUTOFF = 4
@@ -98,9 +98,10 @@ def gcp_errors(data: DataSetBase, reconstructions):
9898

9999
def _compute_errors(reconstructions, tracks_manager):
100100
@lru_cache(10)
101-
def _compute_errors_cached(index, normalized):
101+
def _compute_errors_cached(index, error_type):
102102
return reconstructions[index].map.compute_reprojection_errors(
103-
tracks_manager, normalized
103+
tracks_manager,
104+
error_type,
104105
)
105106

106107
return _compute_errors_cached
@@ -115,38 +116,52 @@ def _get_valid_observations_cached(index):
115116

116117

117118
def _projection_error(tracks_manager, reconstructions):
118-
all_errors_normalized, all_errors_pixels = [], []
119-
average_error_normalized, average_error_pixels = 0, 0
119+
all_errors_normalized, all_errors_pixels, all_errors_angular = [], [], []
120+
average_error_normalized, average_error_pixels, average_error_angular = 0, 0, 0
120121
for i in range(len(reconstructions)):
121-
errors_normalized = _compute_errors(reconstructions, tracks_manager)(i, True)
122-
errors_unnormalized = _compute_errors(reconstructions, tracks_manager)(i, False)
122+
errors_normalized = _compute_errors(reconstructions, tracks_manager)(
123+
i, pymap.ErrorType.Normalized
124+
)
125+
errors_unnormalized = _compute_errors(reconstructions, tracks_manager)(
126+
i, pymap.ErrorType.Pixel
127+
)
128+
errors_angular = _compute_errors(reconstructions, tracks_manager)(
129+
i, pymap.ErrorType.Angular
130+
)
123131

124132
for shot_id, shot_errors_normalized in errors_normalized.items():
125133
shot = reconstructions[i].get_shot(shot_id)
126134
normalizer = max(shot.camera.width, shot.camera.height)
127135

128-
for error_normalized, error_unnormalized in zip(
129-
shot_errors_normalized.values(), errors_unnormalized[shot_id].values()
136+
for error_normalized, error_unnormalized, error_angular in zip(
137+
shot_errors_normalized.values(),
138+
errors_unnormalized[shot_id].values(),
139+
errors_angular[shot_id].values(),
130140
):
131141
norm_pixels = _norm2d(error_unnormalized * normalizer)
132142
norm_normalized = _norm2d(error_normalized)
143+
norm_angle = error_angular[0]
133144
if norm_pixels > RESIDUAL_PIXEL_CUTOFF:
134145
continue
135146
average_error_normalized += norm_normalized
136147
average_error_pixels += norm_pixels
148+
average_error_angular += norm_angle
137149
all_errors_normalized.append(norm_normalized)
138150
all_errors_pixels.append(norm_pixels)
151+
all_errors_angular.append(norm_angle)
139152

140153
error_count = len(all_errors_normalized)
141154
if error_count == 0:
142-
return (-1.0, -1.0, ([], []), ([], []))
155+
return (-1.0, -1.0, -1.0, ([], []), ([], []), ([], []))
143156

144157
bins = 30
145158
return (
146159
average_error_normalized / error_count,
147160
average_error_pixels / error_count,
161+
average_error_angular / error_count,
148162
np.histogram(all_errors_normalized, bins),
149163
np.histogram(all_errors_pixels, bins),
164+
np.histogram(all_errors_angular, bins),
150165
)
151166

152167

@@ -203,11 +218,14 @@ def reconstruction_statistics(data: DataSetBase, tracks_manager, reconstructions
203218
(
204219
avg_normalized,
205220
avg_pixels,
221+
avg_angular,
206222
(hist_normalized, bins_normalized),
207223
(hist_pixels, bins_pixels),
224+
(hist_angular, bins_angular),
208225
) = _projection_error(tracks_manager, reconstructions)
209226
stats["reprojection_error_normalized"] = avg_normalized
210227
stats["reprojection_error_pixels"] = avg_pixels
228+
stats["reprojection_error_angular"] = avg_angular
211229
stats["reprojection_histogram_normalized"] = (
212230
list(map(float, hist_normalized)),
213231
list(map(float, bins_normalized)),
@@ -216,6 +234,10 @@ def reconstruction_statistics(data: DataSetBase, tracks_manager, reconstructions
216234
list(map(float, hist_pixels)),
217235
list(map(float, bins_pixels)),
218236
)
237+
stats["reprojection_histogram_angular"] = (
238+
list(map(float, hist_angular)),
239+
list(map(float, bins_angular)),
240+
)
219241

220242
return stats
221243

@@ -475,7 +497,7 @@ def save_residual_histogram(
475497
io_handler,
476498
):
477499
backup = dict(mpl.rcParams)
478-
fig, axs = plt.subplots(1, 2, tight_layout=True, figsize=(15, 3))
500+
fig, axs = plt.subplots(1, 3, tight_layout=True, figsize=(15, 3))
479501

480502
h_norm, b_norm = stats["reconstruction_statistics"][
481503
"reprojection_histogram_normalized"
@@ -493,8 +515,19 @@ def save_residual_histogram(
493515
for i in range(len(p_pixel)):
494516
p_pixel[i].set_facecolor(plt.cm.viridis(n[i] / max(n)))
495517

518+
h_angular, b_angular = stats["reconstruction_statistics"][
519+
"reprojection_histogram_angular"
520+
]
521+
n, _, p_angular, = axs[
522+
2
523+
].hist(b_angular[:-1], b_angular, weights=h_angular)
524+
n = n.astype("int")
525+
for i in range(len(p_angular)):
526+
p_angular[i].set_facecolor(plt.cm.viridis(n[i] / max(n)))
527+
496528
axs[0].set_title("Normalized Residual")
497529
axs[1].set_title("Pixel Residual")
530+
axs[2].set_title("Angular Residual")
498531

499532
with io_handler.open(
500533
os.path.join(output_path, "residual_histogram.png"), "wb"
@@ -768,8 +801,8 @@ def save_residual_grids(
768801

769802
for i in range(len(reconstructions)):
770803
valid_observations = _get_valid_observations(reconstructions, tracks_manager)(i)
771-
errors_scaled = _compute_errors(reconstructions, tracks_manager)(i, True)
772-
errors_unscaled = _compute_errors(reconstructions, tracks_manager)(i, False)
804+
errors_scaled = _compute_errors(reconstructions, tracks_manager)(i, pymap.ErrorType.Normalized)
805+
errors_unscaled = _compute_errors(reconstructions, tracks_manager)(i, pymap.ErrorType.Pixel)
773806

774807
for shot_id, shot_errors in errors_scaled.items():
775808
shot = reconstructions[i].get_shot(shot_id)

0 commit comments

Comments
 (0)