Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
395aa01
Add optional view-graph calibration to ClusterVGGTWithFrontend
kathirgounder Apr 28, 2026
213d463
Wire post-BA retri into ClusterVGGT(WithFrontend)
kathirgounder Apr 28, 2026
75581c8
Merge pull request #1112 from kathirgounder/vggt-with-frontend-calibr…
akshay-krishnan Apr 28, 2026
faecee5
some options, list to dict
akshay-krishnan Apr 28, 2026
6ada58f
documentation
akshay-krishnan May 4, 2026
0cef355
option to include all edges in partition
akshay-krishnan May 19, 2026
aeba561
Add verified-viewgraph pipeline: verified-graph partition + post-merg…
kathirgounder Jun 23, 2026
170dc61
Fix stale BA field + tune Brussels phototourism configs (baseline + v…
kathirgounder Jun 23, 2026
77796d2
Adopt gp-glomap-parity peak frontend: PoseLibVerifier + 8192 SIFT + 3…
kathirgounder Jun 23, 2026
e08be7c
Enable scipy view-graph focal calibration (use_view_graph_calibration…
kathirgounder Jun 23, 2026
1bc7319
Cluster BA: reuse global SIFT tracks + triangulate structure (verifie…
kathirgounder Jun 23, 2026
86a5a3c
Fix worker OOM in verified pipeline: scatter global frontend without …
kathirgounder Jun 23, 2026
20adfc4
Fix view-graph calibration: return intrinsics dict (not keys) when no…
kathirgounder Jun 23, 2026
1c5ef76
Drop global-track scatter; per-cluster frontend + flag-gated triangul…
kathirgounder Jun 24, 2026
7b64162
Focal flow: global Fetzer + two-tier focal anchoring through cluster …
kathirgounder Jun 25, 2026
ede5a9b
Revert per-cluster triangulated structure to VGGT depth (keep focal-f…
kathirgounder Jun 25, 2026
1dc132b
Run cluster frontend inline; drop worker_client() nested submission
kathirgounder Jun 25, 2026
bbd79ff
Recover track-less cameras: VGGT depth no longer vetoes verified SIFT…
kathirgounder Jun 26, 2026
8d6948b
Reuse global verified correspondences per cluster (skip the per-clust…
kathirgounder Jun 26, 2026
1af1e43
Recover good-pose / no-depth cameras via the post-merge retriangulation
kathirgounder Jun 27, 2026
dc9824a
Integrate VGGT-Omega geometry into the verified pipeline (Brussels A/B)
kathirgounder Jun 27, 2026
7a7c51c
Dispatch image preprocessing on the geometry transformer (fix omega run)
kathirgounder Jun 28, 2026
652e62b
Add REVIEW.md: reviewer guide for the verified-pipeline PR
kathirgounder Jun 28, 2026
ac86c70
VGGT-depth build: drop out-of-bounds depth anchors instead of clamping
kathirgounder Jun 30, 2026
fad3cad
COLMAP-DB frontend: run the verified VGGT pipeline straight from a da…
kathirgounder Jun 30, 2026
01053e5
Lean gather: reduce two-view results to v_corr_idxs on the worker
kathirgounder Jul 1, 2026
b6db814
Stream global two-view: keep only v_corr_idxs on the worker
kathirgounder Jul 1, 2026
1e1cc46
Run global verified frontend inline in the main process (drop the das…
kathirgounder Jul 1, 2026
93d883b
Stabilize dask comms for large single-node scenes (image-load storm +…
kathirgounder Jul 1, 2026
0f82d9c
Add progress logging to the in-process frontend serial loops
kathirgounder Jul 1, 2026
ba559cb
Add ColmapDBMegaLoc retriever: sparsify the COLMAP view graph by Mega…
kathirgounder Jul 1, 2026
cd83f54
Fetzer: pass jac_sparsity to least_squares (fixes hang at scale)
kathirgounder Jul 1, 2026
ce1932a
ColmapDBMegaLoc: per-image top-K over COLMAP neighbors (fix strict-in…
kathirgounder Jul 1, 2026
07b8d05
Make ColmapCorrespondenceGenerator picklable (drop pycolmap.Database …
kathirgounder Jul 1, 2026
7d2d71b
Parallelize the global verified frontend across Dask workers (keep in…
kathirgounder Jul 2, 2026
818f7b4
Cap COLMAP SIFT to num_threads=1 (fix worker oversubscription hang)
kathirgounder Jul 2, 2026
eaca349
Skip per-cluster pre-BA reproj filter (let robust BA keep VGGT-Omega …
kathirgounder Jul 3, 2026
fa0b6f6
Turn off trackless-camera recovery in post-merge retri (paired with p…
kathirgounder Jul 3, 2026
9f7c701
./viz Babylon viewer: fix gimbal lock + big perf/UX upgrade
kathirgounder Jul 3, 2026
7faabee
Revert "Turn off trackless-camera recovery in post-merge retri (paire…
kathirgounder Jul 3, 2026
eda5725
Revert "Skip per-cluster pre-BA reproj filter (let robust BA keep VGG…
kathirgounder Jul 3, 2026
b703e43
Turn off trackless-camera recovery (audit: 55/79 injected poses were …
kathirgounder Jul 3, 2026
8e25870
Switch per-cluster 3D init to SIFT-triangulated structure (VGGT poses…
kathirgounder Jul 3, 2026
9858024
./viz: make frustum/dot sizing outlier-robust + drop garbage cameras
kathirgounder Jul 3, 2026
d87e756
Anchor merges by GLOBAL TRACK IDENTITY (gid-index sidecar) + one-rule…
kathirgounder Jul 4, 2026
e56e630
Refine merge guard from gid-run evidence: overlap escape + 0-track dr…
kathirgounder Jul 4, 2026
7f4e0b5
Weight Sim3 point correspondences to scene scale (they were cosmetic …
kathirgounder Jul 4, 2026
7af6437
RANSAC-prefilter Sim3 point pairs + soften sigma + escape structure f…
kathirgounder Jul 4, 2026
59d147d
RESET to R3-baseline merge semantics; gate gid anchoring + post-merge…
kathirgounder Jul 4, 2026
d11429a
Offline-replay telemetry: dump global tracks, cluster tree, verified …
kathirgounder Jul 4, 2026
2c93fe5
Port three offline-validated fixes: majority-vote gid identity, hones…
kathirgounder Jul 4, 2026
8e43ed6
Config: retrieval 0.10/200 (C_9 bridge edges), boundary+gid off for p…
kathirgounder Jul 4, 2026
0b4a702
Retrieval 120/0.15: validated live — K=60 floater block seats at K=120
kathirgounder Jul 5, 2026
bd70af6
Add export-time triangulation-angle track filter (merged_anglefiltered/)
kathirgounder Jul 5, 2026
3935f53
Add run_per_cluster_ba flag; OFF in verified config (BA census: degra…
kathirgounder Jul 5, 2026
12b13be
EXIF focal passthrough (calibration_source=exif) + tighten merge foca…
kathirgounder Jul 6, 2026
1d09611
Revert to golden-run machinery + EXIF focals: cluster BA back ON, mer…
kathirgounder Jul 6, 2026
1a51bf8
./viz: port pipeline-viz scene-up alignment (fix the gimbal-lock feel)
kathirgounder Jul 6, 2026
03f8327
Fix ratio-test crash on <2 knnMatch candidates (near-featureless images)
kathirgounder Jul 6, 2026
b4e85be
Precomputed-frontend injection: consume 1DSfM's released view graph +…
kathirgounder Jul 7, 2026
6f03453
Precomputed frontend: EXIF-rotation dims guard + max_resolution assert
kathirgounder Jul 7, 2026
b574e9c
Robust gated Fetzer calibration (borglab#1115 recipe): PoseLib F + pl…
kathirgounder Jul 7, 2026
32c98f5
Add precomputed_pairs_path: run our full frontend on a fixed pair lis…
kathirgounder Jul 7, 2026
a0d7d27
Pin OpenCV to 1 thread per matcher task (N-workers x all-cores oversu…
kathirgounder Jul 7, 2026
104ea5b
Add TorchTwoWayMatcher: GPU/torch mutual-NN + bidirectional ratio mat…
kathirgounder Jul 7, 2026
b5e568d
Make MERGE_GUARD Sim3 scale band configurable; widen to [0.02, 50] in…
kathirgounder Jul 8, 2026
9e3a5fd
Remove Sim3 scale band by default (band = [0, inf))
kathirgounder Jul 8, 2026
e99859f
Configurable cache root (GTSFM_CACHE_ROOT) + atomic cache writes
kathirgounder Jul 8, 2026
862f270
Fetzer gtsam solver: accept FetzerFactor/SelfCalibrationFactor names …
kathirgounder Jul 8, 2026
c482561
Expose cam_pose3_prior_noise_sigma in BundleAdjustmentOptions
kathirgounder Jul 8, 2026
9f25fb2
Add retri_free_ba + retri_iterations to post-merge retriangulation
kathirgounder Jul 8, 2026
3f68251
Expose retri_free_ba/retri_iterations keys in the verified omega config
kathirgounder Jul 8, 2026
17c5b96
retri_free_ba: drop priors only; inherit robustness from ba_options
kathirgounder Jul 8, 2026
8947430
./viz: paper-figure frustums + H to hide UI for clean shots
kathirgounder Jul 9, 2026
1bfa349
Save model-predicted intrinsics as per-cluster telemetry
kathirgounder Jul 9, 2026
4ed605d
Add modern Babylon.js reconstruction viewer (./pipeline-viz) as an op…
kathirgounder Jul 10, 2026
562e0ca
Sync colmapdb config to the peak verified config
kathirgounder Jul 15, 2026
7eaa80a
Merge pull request #1117 from kathirgounder/vggt-verified-pipeline
akshay-krishnan Jul 15, 2026
fb96002
Revert "[skip benchmarks]Vggt verified pipeline"
kathirgounder Jul 23, 2026
c33c412
Merge pull request #1119 from borglab/revert-1117-vggt-verified-pipeline
kathirgounder Jul 23, 2026
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
25 changes: 8 additions & 17 deletions gtsfm/bundle/bundle_adjustment.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ class BundleAdjustmentOptions:
min_tracks_per_camera: int = 15
compute_pose_covariances: bool = False
optimizer_relative_cost_tol: float = 1e-5
use_multi_view_retriangulation: bool = False
mv_retri_min_track_length: int = 3
mv_retri_reproj_error_thresh: float = 10.0
mv_retri_max_num_hypotheses: int = 100

def to_optimizer(self, **overrides) -> "BundleAdjustmentOptimizer":
"""Construct a :class:`BundleAdjustmentOptimizer` from these options.
Expand All @@ -195,6 +199,10 @@ def to_optimizer(self, **overrides) -> "BundleAdjustmentOptimizer":
min_tracks_per_camera=self.min_tracks_per_camera,
compute_pose_covariances=self.compute_pose_covariances,
optimizer_relative_cost_tol=self.optimizer_relative_cost_tol,
use_multi_view_retriangulation=self.use_multi_view_retriangulation,
mv_retri_min_track_length=self.mv_retri_min_track_length,
mv_retri_reproj_error_thresh=self.mv_retri_reproj_error_thresh,
mv_retri_max_num_hypotheses=self.mv_retri_max_num_hypotheses,
)
kwargs.update(overrides)
return BundleAdjustmentOptimizer(**kwargs)
Expand Down Expand Up @@ -238,13 +246,6 @@ def __init__(
min_tracks_per_camera: int = 15,
compute_pose_covariances: bool = False,
optimizer_relative_cost_tol: float = 1e-5,
# ── Optional post-BA multi-view retriangulation (opt-in) ──
# When `use_multi_view_retriangulation=True`: after the existing BA loop
# converges, re-triangulate the union-find 2D tracks against the post-BA
# cameras (recovers tracks dropped between union-find and BA's filter
# passes) and run a final BA on the augmented set. Requires `tracks_2d` to
# be passed to `create_computation_graph` / `_run_ba_and_evaluate`. The
# final BA reuses the existing `reproj_error_thresholds[-1]` for filtering.
use_multi_view_retriangulation: bool = False,
mv_retri_min_track_length: int = 3,
mv_retri_reproj_error_thresh: float = 10.0,
Expand Down Expand Up @@ -952,12 +953,6 @@ def _run_ba_and_evaluate(
)
total_time = time.time() - start_time

# ── Optional post-BA multi-view retriangulation stage ──
# Re-triangulate union-find tracks against the post-BA cameras (recovers
# tracks dropped between union-find and BA's filter passes), then run a
# final BA on the augmented set. The final BA reuses the existing tightest
# `reproj_error_thresholds[-1]` for inline filtering — same mechanism as
# the upstream BA loop.
if self._use_multi_view_retriangulation:
if tracks_2d is None:
logger.warning(
Expand All @@ -984,10 +979,6 @@ def _run_ba_and_evaluate(
min_track_length=self._mv_retri_min_track_length,
)
if retri_data.number_tracks() > 0:
# Final BA on the retri'd track set. No inline filter — pose AUC
# is set by BA's converged cameras and is independent of any
# downstream track filtering. Callers can filter the returned
# GtsfmData themselves if they want.
(optimized_data, filtered_result, valid_mask, _) = self.run_ba_stage_with_filtering(
initial_data=retri_data,
absolute_pose_priors=absolute_pose_priors,
Expand Down
4 changes: 4 additions & 0 deletions gtsfm/cluster_optimizer/cluster_mvo.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ def _run_two_view_estimation(
all_two_view_results = cast(AnnotatedGraph[TwoViewResult], gathered_tve_futures)
valid_two_view_results = {edge: result for edge, result in all_two_view_results.items() if result.valid()}

n_total = len(all_two_view_results)
n_valid = len(valid_two_view_results)
logger.info("Two-view estimation: %d/%d pairs valid, %d rejected.", n_valid, n_total, n_total - n_valid)

if len(valid_two_view_results) == 0:
logger.warning("🔵 ClusterMVO: Skipping cluster as it has no valid two-view results.")

Expand Down
26 changes: 25 additions & 1 deletion gtsfm/cluster_optimizer/cluster_vggt.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@

import gtsfm.common.types as gtsfm_types
import gtsfm.utils.metrics as metrics_utils
from gtsfm.bundle.bundle_adjustment import BundleAdjustmentOptions
from gtsfm.bundle.bundle_adjustment import BundleAdjustmentOptions, multi_view_retriangulate_from_2d_tracks
from gtsfm.cluster_optimizer.cluster_optimizer_base import ClusterComputationGraph, ClusterContext, ClusterOptimizerBase
from gtsfm.common.gtsfm_data import GtsfmData
from gtsfm.common.sfm_track import SfmTrack2d
from gtsfm.evaluation.metrics import GtsfmMetric, GtsfmMetricsGroup
from gtsfm.frontend.multi_view_tracker import MultiViewTracker
from gtsfm.frontend.vggt_geometry_transformer import (
Expand Down Expand Up @@ -50,11 +51,20 @@ def _run_cluster_ba(
drop_camera_with_no_track: bool = False,
min_track_length: int = 2,
cluster_label: Optional[str] = None,
tracks_2d: Optional[list[SfmTrack2d]] = None,
use_multi_view_retriangulation: bool = False,
) -> tuple[GtsfmData, GtsfmData]:
"""Run cluster-level BA on a GtsfmData result.

This is a module-level function so it can be used with ``dask.delayed``.

Args:
tracks_2d: (optional) Union-find 2D tracks. Required when
``use_multi_view_retriangulation=True``.
use_multi_view_retriangulation: When True, after the initial BA, re-triangulate
``tracks_2d`` against the post-BA cameras (recovers tracks dropped earlier
in the pipeline) and run a second BA on the augmented track set.

Returns:
Tuple of (post_ba_result, pre_ba_result).
"""
Expand Down Expand Up @@ -90,6 +100,20 @@ def _run_cluster_ba(
post_ba_max_reproj_error
)

# Optional retri stage: re-triangulate union-find tracks against the post-BA
# cameras and run another BA on the augmented set. Recovers tracks dropped
# earlier in the pipeline; mirrors the retri stage in
# BundleAdjustmentOptimizer._run_ba_and_evaluate.
if use_multi_view_retriangulation and tracks_2d is not None:
retri_data = multi_view_retriangulate_from_2d_tracks(
gtsfm_data_with_ba, tracks_2d, min_track_length=min_track_length,
)
if retri_data.number_tracks() > 0:
gtsfm_data_with_ba, _ = optimizer.run_simple_ba(retri_data)
gtsfm_data_with_ba = gtsfm_data_with_ba.filter_landmark_measurements(
post_ba_max_reproj_error
)

logger.info(
"%s🔍 #valid tracks after BA: %d out of %d",
f"[{cluster_label}] " if cluster_label else "",
Expand Down
68 changes: 63 additions & 5 deletions gtsfm/cluster_optimizer/cluster_vggt_with_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from gtsfm.products.visibility_graph import visibility_graph_keys
from gtsfm.two_view_estimator import TwoViewEstimator
from gtsfm.ui.gtsfm_process import UiMetadata
from gtsfm.view_graph_estimator.view_graph_calibration import calibrate_view_graph
import gtsam

from gtsfm.utils import torch as torch_utils
Expand Down Expand Up @@ -132,6 +133,7 @@ def _build_gtsfm_data_from_vggt_depth(
image_indices: tuple[int, ...],
num_images: int,
min_track_length: int = 2,
refined_intrinsics: Optional[dict[int, gtsfm_types.CALIBRATION_TYPE]] = None,
) -> GtsfmData:
"""Build GtsfmData using VGGT cameras (rescaled to original resolution) and frontend 2D tracks.

Expand Down Expand Up @@ -159,14 +161,19 @@ def _build_gtsfm_data_from_vggt_depth(
_, H_vggt, W_vggt = dense_points.shape[:3]
global_to_local = {gidx: lidx for lidx, gidx in enumerate(image_indices)}

# Register cameras with intrinsics rescaled to original image resolution.
# Register cameras with intrinsics in original image resolution. If
# `refined_intrinsics` is supplied (from view-graph calibration), use those
# directly; otherwise rescale VGGT's predicted intrinsics from VGGT pixel space.
gtsfm_data = GtsfmData(number_images=num_images)
for global_idx, camera in cameras.items():
if global_idx in image_shapes and global_idx in global_to_local:
_, orig_W = image_shapes[global_idx]
local_idx = global_to_local[global_idx]
scaled_W = float(original_coords[local_idx, 4])
camera = _scale_camera_intrinsics(camera, scale=orig_W / scaled_W)
if refined_intrinsics is not None:
camera = type(camera)(camera.pose(), refined_intrinsics[global_idx])
else:
_, orig_W = image_shapes[global_idx]
local_idx = global_to_local[global_idx]
scaled_W = float(original_coords[local_idx, 4])
camera = _scale_camera_intrinsics(camera, scale=orig_W / scaled_W)
gtsfm_data.add_camera(global_idx, camera)

for track_2d in tracks_2d:
Expand Down Expand Up @@ -220,6 +227,38 @@ def _build_gtsfm_data_from_vggt_depth(
return gtsfm_data


def _refine_vggt_intrinsics_via_view_graph(
vggt_result: VggtGeometryResult,
v_corr_idxs: dict,
keypoints_list: list,
image_shapes: dict[int, tuple[int, int]],
image_indices: tuple[int, ...],
) -> dict[int, gtsfm_types.CALIBRATION_TYPE]:
"""Refine focal lengths via Fetzer joint optimization over F-matrix edges.

Returns intrinsics in ORIGINAL image coordinates (suitable for use with the
frontend keypoints). VGGT's predicted intrinsics are rescaled to original
image coords first, then handed to `calibrate_view_graph` as the initial
estimate. VGGT's predicted poses are unchanged — only intrinsics are refined.
"""
initial_intrinsics: dict[int, gtsfm_types.CALIBRATION_TYPE] = {}
for local_idx, global_idx in enumerate(image_indices):
if global_idx in vggt_result.cameras and global_idx in image_shapes:
_, orig_W = image_shapes[global_idx]
scaled_W = float(vggt_result.original_coords[local_idx, 4])
scale = orig_W / scaled_W if scaled_W > 0 else 1.0
scaled_cam = _scale_camera_intrinsics(vggt_result.cameras[global_idx], scale=scale)
initial_intrinsics[global_idx] = scaled_cam.calibration()

keypoints = {gidx: keypoints_list[gidx] for gidx in image_indices}
refined, _edges_to_remove = calibrate_view_graph(
v_corr_idxs_dict=v_corr_idxs,
keypoints=keypoints,
initial_intrinsics=initial_intrinsics,
)
return refined


class ClusterVGGTWithFrontend(ClusterMVO):
"""Cluster optimizer that combines a traditional MVO frontend with VGGT poses.

Expand Down Expand Up @@ -250,6 +289,8 @@ def __init__(
save_two_view_viz: bool = False,
pose_angular_error_thresh: float = 3,
output_worker: Optional[str] = None,
use_view_graph_calibration: bool = False,
use_multi_view_retriangulation: bool = False,
) -> None:
super().__init__(
correspondence_generator=correspondence_generator,
Expand All @@ -268,6 +309,8 @@ def __init__(
self._metric_constructed_only = metric_constructed_only
self._input_mode = input_mode
self._seed = seed
self._use_view_graph_calibration = use_view_graph_calibration
self._use_multi_view_retriangulation = use_multi_view_retriangulation

self._weights_path = Path(weights_path) if weights_path is not None else None
self._loader_kwargs: dict[str, Any] = {}
Expand Down Expand Up @@ -343,6 +386,18 @@ def create_computation_graph(self, context: ClusterContext) -> ClusterComputatio
# 4. Original image shapes (needed to map frontend pixel coords → VGGT pixel coords).
image_shapes_graph = delayed(_get_image_shapes)(context.loader, global_indices)

# 4b. Optional: refine VGGT's predicted intrinsics via Fetzer joint
# optimization over the frontend's F-matrices (keeps VGGT's predicted poses).
refined_intrinsics_graph = None
if self._use_view_graph_calibration:
refined_intrinsics_graph = delayed(_refine_vggt_intrinsics_via_view_graph)(
vggt_result_graph,
v_corr_idxs_graph,
frontend_graphs.padded_keypoints,
image_shapes_graph,
global_indices,
)

# 5. Build GtsfmData: lift 2D tracks to 3D using VGGT depth map.
ba_input_graph = delayed(_build_gtsfm_data_from_vggt_depth)(
vggt_result_graph,
Expand All @@ -351,6 +406,7 @@ def create_computation_graph(self, context: ClusterContext) -> ClusterComputatio
image_indices=global_indices,
num_images=context.num_images,
min_track_length=self._min_track_length,
refined_intrinsics=refined_intrinsics_graph,
)

# 6. Cluster-level BA.
Expand All @@ -362,6 +418,8 @@ def create_computation_graph(self, context: ClusterContext) -> ClusterComputatio
drop_camera_with_no_track=self._drop_camera_with_no_track,
min_track_length=self._min_track_length,
cluster_label=context.label,
tracks_2d=tracks_2d_graph,
use_multi_view_retriangulation=self._use_multi_view_retriangulation,
)

# 7. Metrics + I/O.
Expand Down
4 changes: 4 additions & 0 deletions gtsfm/configs/vggt_sift_frontend_megaloc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ cluster_optimizer:
input_mode: crop
seed: 42
model_cache_key: null
# Refine VGGT predicted intrinsics via Fetzer joint optimization on F-matrices.
use_view_graph_calibration: false
# Re-triangulate union-find 2D tracks against post-BA cameras and run another BA.
use_multi_view_retriangulation: false

# --- Merging options ---
merging_options:
Expand Down
4 changes: 4 additions & 0 deletions gtsfm/configs/vggt_sift_frontend_megaloc_phototourism.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ cluster_optimizer:
input_mode: crop
seed: 42
model_cache_key: null
# Refine VGGT predicted intrinsics via Fetzer joint optimization on F-matrices.
use_view_graph_calibration: false
# Re-triangulate union-find 2D tracks against post-BA cameras and run another BA.
use_multi_view_retriangulation: false

# --- Merging options ---
merging_options:
Expand Down
4 changes: 4 additions & 0 deletions gtsfm/configs/vggt_unified_frontend_megaloc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ cluster_optimizer:
input_mode: crop
seed: 42
model_cache_key: null
# Refine VGGT predicted intrinsics via Fetzer joint optimization on F-matrices.
use_view_graph_calibration: false
# Re-triangulate union-find 2D tracks against post-BA cameras and run another BA.
use_multi_view_retriangulation: false

# --- Merging options ---
merging_options:
Expand Down
14 changes: 9 additions & 5 deletions gtsfm/graph_partitioner/metis_partitioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def __init__(
min_child_overlap_for_split: int = 2,
min_parent_overlap_for_split: int = 2,
split_oversized_nodes: bool = False,
include_all_edges_in_cluster: bool = False,
) -> None:
super().__init__(process_name="MetisPartitioner")
if min_cameras_to_partition is not None and min_cameras_to_partition < 1:
Expand All @@ -55,6 +56,7 @@ def __init__(
self._min_child_overlap_for_split = min_child_overlap_for_split
self._min_parent_overlap_for_split = min_parent_overlap_for_split
self._split_oversized_nodes = split_oversized_nodes
self._include_all_edges_in_cluster = include_all_edges_in_cluster

@staticmethod
def _is_connected(graph: VisibilityGraph) -> bool:
Expand Down Expand Up @@ -684,11 +686,13 @@ def _cluster_from_clique(self, clique: SymbolicBayesTreeClique, graph: Visibilit
child_results = self._merge_small_children_at_level(child_results, graph)
descendant_edges = set.union(*(result.edges for result in child_results)) if child_results else set()

# Only keep edges that touch at least one frontal variable from this clique.
candidate_edges = {
(i, j) for i, j in graph if i in keys and j in keys and (i in frontals or j in frontals or not frontals)
}
current_edges = candidate_edges - descendant_edges
if self._include_all_edges_in_cluster:
current_edges = {(i, j) for i, j in graph if i in keys and j in keys}
else:
candidate_edges = {
(i, j) for i, j in graph if i in keys and j in keys and (i in frontals or j in frontals or not frontals)
}
current_edges = candidate_edges - descendant_edges

def sorted_edges(edges: set[tuple[int, int]]) -> list[tuple[int, int]]:
return sorted(edges)
Expand Down
20 changes: 18 additions & 2 deletions gtsfm/multi_view_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ def create_computation_graph(

# View graph calibration: refine focal lengths from F-matrices
if self._run_view_graph_calibration:
all_intrinsics, edges_to_remove = delayed(view_graph_calibration.calibrate_view_graph, nout=2)(
viewgraph_v_corr_idxs_graph, keypoints_graph, all_intrinsics, num_images
all_intrinsics, edges_to_remove = delayed(_calibrate_view_graph_from_lists, nout=2)(
viewgraph_v_corr_idxs_graph, keypoints_graph, all_intrinsics
)
# Remove edges with high calibration error
viewgraph_i2Ri1_graph, viewgraph_i2Ui1_graph, viewgraph_v_corr_idxs_graph = delayed(_filter_edges, nout=3)(
Expand Down Expand Up @@ -336,6 +336,22 @@ def _sync_two_view_reports_after_calibration(
return synced_reports


def _calibrate_view_graph_from_lists(
v_corr_idxs_dict,
keypoints_list: list[Keypoints],
intrinsics_list: list[gtsfm_types.CALIBRATION_TYPE],
**kwargs,
):
"""List-interface wrapper around calibrate_view_graph for use in multi_view_optimizer."""
kp_dict = {i: kp for i, kp in enumerate(keypoints_list)}
intr_dict = {i: cal for i, cal in enumerate(intrinsics_list)}
refined_dict, edges_to_remove = view_graph_calibration.calibrate_view_graph(
v_corr_idxs_dict, kp_dict, intr_dict, **kwargs
)
refined_list = [refined_dict.get(i, intrinsics_list[i]) for i in range(len(intrinsics_list))]
return refined_list, edges_to_remove


def reestimate_relative_poses(
i2Ri1_dict: Dict[Tuple[int, int], Rot3],
i2Ui1_dict: Dict[Tuple[int, int], Unit3],
Expand Down
Loading