Skip to content

Commit 92ed268

Browse files
author
Yann N.
committed
Better and unified GCP thresholds handling :
- Use a single threshold from triangulation to stats - Only use GCPs for linear alignment at the end since GPS and GCP have no garantee of consistency (bias)
1 parent 1b30a0e commit 92ed268

File tree

9 files changed

+60
-38
lines changed

9 files changed

+60
-38
lines changed

opensfm/align.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,13 @@ def alignment_constraints(
127127
X, Xp = [], []
128128
# Get Ground Control Point correspondences
129129
if gcp and config["bundle_use_gcp"]:
130-
triangulated, measured = triangulate_all_gcp(reconstruction, gcp)
130+
triangulated, measured = triangulate_all_gcp(
131+
reconstruction, gcp, config["gcp_reprojection_error_threshold"]
132+
)
131133
X.extend(triangulated)
132134
Xp.extend(measured)
133135
# Get camera center correspondences
134-
if use_gps and config["bundle_use_gps"]:
136+
elif use_gps and config["bundle_use_gps"]:
135137
for rig_instance in reconstruction.rig_instances.values():
136138
gpses = [
137139
shot.metadata.gps_position.value
@@ -433,14 +435,17 @@ def get_horizontal_and_vertical_directions(
433435

434436

435437
def triangulate_all_gcp(
436-
reconstruction: types.Reconstruction, gcp: List[pymap.GroundControlPoint]
438+
reconstruction: types.Reconstruction,
439+
gcp: List[pymap.GroundControlPoint],
440+
threshold: float,
437441
) -> Tuple[List[NDArray], List[NDArray]]:
438442
"""Group and triangulate Ground Control Points seen in 2+ images."""
439443
triangulated, measured = [], []
440444
for point in gcp:
441445
x = multiview.triangulate_gcp(
442446
point,
443447
reconstruction.shots,
448+
threshold,
444449
)
445450
if x is not None and len(point.lla):
446451
point_enu = np.array(

opensfm/config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ class OpenSfMConfig:
267267
# The default vertical standard deviation of the GCPs (in meters)
268268
gcp_vertical_sd: float = 0.1
269269
# Global weight for GCPs, expressed a ratio of the sum of (# projections) + (# shots) + (# relative motions)
270-
gcp_global_weight: float = 0.01
270+
gcp_global_weight: float = 0.03
271271
# The standard deviation of the rig translation
272272
rig_translation_sd: float = 0.1
273273
# The standard deviation of the rig rotation
@@ -313,7 +313,7 @@ class OpenSfMConfig:
313313
save_partial_reconstructions: bool = False
314314

315315
##################################
316-
# Params for GPS alignment
316+
# Params for GPS/GCP alignment
317317
##################################
318318
# Use or ignore EXIF altitude tag
319319
use_altitude_tag: bool = True
@@ -327,6 +327,8 @@ class OpenSfMConfig:
327327
bundle_use_gcp: bool = True
328328
# Compensate GPS with a per-camera similarity transform
329329
bundle_compensate_gps_bias: bool = False
330+
# Thrershold for the reprojection error of GCPs to be considered outliers
331+
gcp_reprojection_error_threshold: float = 0.05
330332

331333
##################################
332334
# Params for rigs

opensfm/multiview.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ def triangulate_gcp(
533533
reproj_threshold: float = 0.02,
534534
min_ray_angle_degrees: float = 1.0,
535535
min_depth: float = 0.001,
536+
iterations: int = 10,
536537
) -> Optional[NDArray]:
537538
"""Compute the reconstructed position of a GCP from observations."""
538539

@@ -550,13 +551,17 @@ def triangulate_gcp(
550551

551552
if len(os) >= 2:
552553
thresholds = len(os) * [reproj_threshold]
554+
os = np.asarray(os)
555+
bs = np.asarray(bs)
553556
valid_triangulation, X = pygeometry.triangulate_bearings_midpoint(
554-
np.asarray(os),
555-
np.asarray(bs),
557+
os,
558+
bs,
556559
thresholds,
557560
np.radians(min_ray_angle_degrees),
558561
min_depth,
559562
)
563+
560564
if valid_triangulation:
565+
X = pygeometry.point_refinement(os, bs, X, iterations)
561566
return X
562567
return None

opensfm/reconstruction.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,7 +1511,7 @@ def grow_reconstruction(
15111511
rig_camera_priors = data.load_rig_cameras()
15121512

15131513
paint_reconstruction(data, tracks_manager, reconstruction)
1514-
align_reconstruction(reconstruction, gcp, config)
1514+
align_reconstruction(reconstruction, [], config)
15151515

15161516
bundle(reconstruction, camera_priors, rig_camera_priors, None, 0, config)
15171517
remove_outliers(reconstruction, config)
@@ -1598,7 +1598,7 @@ def grow_reconstruction(
15981598

15991599
if should_retriangulate.should():
16001600
logger.info("Re-triangulating")
1601-
align_reconstruction(reconstruction, gcp, config)
1601+
align_reconstruction(reconstruction, [], config)
16021602
b1rep = bundle(
16031603
reconstruction, camera_priors, rig_camera_priors, None, local_ba_grid, config
16041604
)
@@ -1614,7 +1614,7 @@ def grow_reconstruction(
16141614
should_retriangulate.done()
16151615
should_bundle.done()
16161616
elif should_bundle.should():
1617-
align_reconstruction(reconstruction, gcp, config)
1617+
align_reconstruction(reconstruction, [], config)
16181618
brep = bundle(
16191619
reconstruction, camera_priors, rig_camera_priors, None, local_ba_grid, config
16201620
)
@@ -1711,7 +1711,7 @@ def triangulation_reconstruction(
17111711

17121712
step = {}
17131713
logger.info(f"Triangulation SfM. Inner iteration {j}, running bundle ...")
1714-
align_reconstruction(reconstruction, gcp, config_override)
1714+
align_reconstruction(reconstruction, [], config_override)
17151715
b1rep = bundle(
17161716
reconstruction, camera_priors, rig_camera_priors, None, config_override
17171717
)

opensfm/src/bundle/src/bundle_adjuster.cc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -809,8 +809,7 @@ void BundleAdjuster::Run() {
809809
projection_type, use_analytic_, observation, projection_loss, &problem);
810810

811811
// Add relative depth error blocks
812-
geometry::Dispatch<AddRelativeDepthError>(projection_type, observation,
813-
projection_loss, &problem);
812+
geometry::Dispatch<AddRelativeDepthError>(projection_type, observation, projection_loss, &problem);
814813
}
815814

816815
// Add relative motion errors

opensfm/src/sfm/ba_helpers.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class BAHelpers {
7272
static bool TriangulateGCP(
7373
const map::GroundControlPoint& point,
7474
const std::unordered_map<map::ShotId, map::Shot>& shots,
75+
float reproj_threshold,
7576
Vec3d& coordinates);
7677

7778
static void AlignmentConstraints(

opensfm/src/sfm/src/ba_helpers.cc

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -351,8 +351,8 @@ py::tuple BAHelpers::BundleLocal(
351351
bool BAHelpers::TriangulateGCP(
352352
const map::GroundControlPoint& point,
353353
const std::unordered_map<map::ShotId, map::Shot>& shots,
354+
float reproj_threshold,
354355
Vec3d& coordinates) {
355-
constexpr auto reproj_threshold{1.0};
356356
constexpr auto min_ray_angle = 0.1 * M_PI / 180.0;
357357
constexpr auto min_depth = 1e-3; // Assume GCPs 1mm+ away from the camera
358358
MatX3d os, bs;
@@ -390,52 +390,58 @@ size_t BAHelpers::AddGCPToBundle(
390390
const auto& reference = map.GetTopocentricConverter();
391391
const auto& shots = map.GetShots();
392392

393-
const auto dominant_terms = ba.GetRigInstances().size() +
394-
ba.GetProjectionsCount() +
395-
ba.GetRelativeMotionsCount();
396393

397-
size_t total_terms = 0;
398-
for (const auto& point : gcp) {
399-
Vec3d coordinates;
400-
if (TriangulateGCP(point, shots, coordinates) || !point.lla_.empty()) {
401-
++total_terms;
402-
}
403-
for (const auto& obs : point.observations_) {
404-
total_terms += (shots.count(obs.shot_id_) > 0);
405-
}
406-
}
407-
408-
double global_weight = config["gcp_global_weight"].cast<double>() *
409-
dominant_terms / std::max<size_t>(1, total_terms);
394+
const float reproj_threshold =
395+
config["gcp_reprojection_error_threshold"].cast<float>();
410396

411397
size_t added_gcp_observations = 0;
412398
for (const auto& point : gcp) {
413399
const auto point_id = "gcp-" + point.id_;
414400
Vec3d coordinates;
415-
if (!TriangulateGCP(point, shots, coordinates)) {
401+
if (!TriangulateGCP(point, shots, reproj_threshold, coordinates)) {
416402
if (!point.lla_.empty()) {
417403
coordinates = reference.ToTopocentric(point.GetLlaVec3d());
418404
} else {
419405
continue;
420406
}
421407
}
408+
409+
double avg_observations = 0.;
410+
int valid_shots = 0;
411+
for (const auto& obs : point.observations_) {
412+
const auto shot_it = shots.find(obs.shot_id_);
413+
if (shot_it != shots.end()) {
414+
const auto& shot = (shot_it->second);
415+
avg_observations += shot.GetLandmarkObservations().size();
416+
++valid_shots;
417+
}
418+
}
419+
420+
if(!valid_shots) {
421+
continue;
422+
}
423+
avg_observations /= valid_shots;
424+
425+
426+
const double prior_weight = config["gcp_global_weight"].cast<double>() * avg_observations;
422427
constexpr auto point_constant{false};
423428
ba.AddPoint(point_id, coordinates, point_constant);
424429
if (!point.lla_.empty()) {
425430
const auto point_std = Vec3d(config["gcp_horizontal_sd"].cast<double>(),
426431
config["gcp_horizontal_sd"].cast<double>(),
427432
config["gcp_vertical_sd"].cast<double>());
428433
ba.AddPointPrior(point_id, reference.ToTopocentric(point.GetLlaVec3d()),
429-
point_std / global_weight, point.has_altitude_);
434+
point_std / prior_weight, point.has_altitude_);
430435
}
431436

432437
// Now iterate through the observations
438+
const double obs_weight = config["gcp_global_weight"].cast<double>() * avg_observations;
433439
for (const auto& obs : point.observations_) {
434440
const auto& shot_id = obs.shot_id_;
435441
if (shots.count(shot_id) > 0) {
436442
constexpr double scale{0.001};
437443
ba.AddPointProjectionObservation(shot_id, point_id, obs.projection_,
438-
scale / global_weight);
444+
scale / obs_weight);
439445
++added_gcp_observations;
440446
}
441447
}
@@ -829,7 +835,7 @@ py::dict BAHelpers::Bundle(
829835
AddGCPToBundle(ba, map, gcp, config);
830836
}
831837

832-
if (config["bundle_compensate_gps_bias"].cast<bool>()) {
838+
if (config["bundle_compensate_gps_bias"].cast<bool>() && !gcp.empty()) {
833839
const auto& biases = map.GetBiases();
834840
for (const auto& camera : map.GetCameras()) {
835841
ba.SetCameraBias(camera.first, biases.at(camera.first));
@@ -989,7 +995,7 @@ void BAHelpers::AlignmentConstraints(
989995
continue;
990996
}
991997
Vec3d coordinates;
992-
if (TriangulateGCP(point, shots, coordinates)) {
998+
if (TriangulateGCP(point, shots, config["gcp_reprojection_error_threshold"].cast<float>(), coordinates)) {
993999
Xp.row(idx) = topocentricConverter.ToTopocentric(point.GetLlaVec3d());
9941000
X.row(idx) = coordinates;
9951001
++idx;

opensfm/stats.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ def gcp_errors(
9494

9595
triangulated = None
9696
for rec in reconstructions:
97-
triangulated = multiview.triangulate_gcp(gcp, rec.shots, 1.0, 0.1)
97+
triangulated = multiview.triangulate_gcp(
98+
gcp, rec.shots, data.config["gcp_reprojection_error_threshold"]
99+
)
98100
if triangulated is None:
99101
continue
100102
else:
@@ -580,7 +582,9 @@ def save_residual_histogram(
580582
n,
581583
_,
582584
p_angular,
583-
) = axs[2].hist(b_angular[:-1], b_angular, weights=h_angular)
585+
) = axs[
586+
2
587+
].hist(b_angular[:-1], b_angular, weights=h_angular)
584588
n = n.astype("int")
585589
for i in range(len(p_angular)):
586590
p_angular[i].set_facecolor(plt.cm.viridis(n[i] / max(n)))

opensfm/test/test_reconstruction_triangulation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_reconstruction_triangulation(
4040
assert 0.01 < errors["absolute_gps_rmse"] < 0.1
4141

4242
# Sanity check that GCP error is similar to the generated gcp_noise
43-
assert 0.01 < errors["absolute_gcp_rmse_horizontal"] < 0.05
43+
assert 0.01 < errors["absolute_gcp_rmse_horizontal"] < 0.03
4444
assert 0.005 < errors["absolute_gcp_rmse_vertical"] < 0.04
4545

4646
# Check that the GPS bias (only translation) is recovered

0 commit comments

Comments
 (0)