Performance¶
This page describes practical performance considerations for pyTheia (the Python fork). Historical Strecha / 1DSfM benchmark tables from upstream TheiaSfM (2016) are not representative of this fork's Python-first API and are omitted here.
Threading¶
ReconstructionEstimatorOptions.num_threadsdefaults to 1. Raise it for incremental, hybrid, or global reconstruction when running large scenes on multi-core hosts.- Some internal paths (e.g. nested bundle adjustment inside incremental estimation) may still cap parallelism for numerical stability.
- OpenMP pragmas exist in parts of the C++ tree, but OpenMP is not wired in CMake by default; do not assume multi-threaded OpenMP without a custom build.
Linear algebra and Ceres backends¶
Bundle adjustment and many solvers use Ceres Solver. Defaults favor portable CPU backends (EIGEN dense, EIGEN_SPARSE sparse).
- Pre-built wheels link a CPU Ceres build. For GPU linear algebra, build Ceres locally with CUDA (and optionally cuDSS for sparse CUDA), then build pyTheia against that Ceres. See Building and Bundle adjustment.
- SuiteSparse / CHOLMOD: this fork removed GPL-dependent SuiteSparse code from
sparse_cholesky_llt.ccand uses Eigen::SimplicialLDLT instead. That improves portability but can be slower or slightly less stable on very large sparse problems than a SuiteSparse-enabled Ceres build.
Python / pybind11 boundary¶
Most bound functions hold the Python GIL for the full C++ call. Long-running work (full reconstruction, bundle adjustment, single-pair two-view estimation) blocks other Python threads.
Exception: BulkEstimateTwoViewInfo releases the GIL while processing many view pairs in C++. Prefer batch APIs where available when driving pyTheia from multi-threaded Python or async code. From 1.1.0, pass optional depth_i / depth_j and read scales so monodepth two-view stays on the bulk path (RANSAC).
Passing large correspondences or descriptors through Python lists incurs conversion overhead. For hot loops, keep data in NumPy arrays and batch work on the C++ side when bindings support it.
Local build tuning¶
| Setting | Effect |
|---|---|
BUILD_MARCH_NATIVE=1 (env) / -DBUILD_WITH_MARCH_NATIVE=ON |
Enables -march=native -mtune=native for local max performance; off for portable wheels |
| CUDA-enabled Ceres | Speeds large BA when BundleAdjustmentOptions selects GPU dense/sparse backends |
Higher num_threads on estimators |
Better CPU utilization for reconstruction pipelines |
Two-view relative pose estimation¶
EstimateTwoViewInfo / EstimateRelativePose were optimized along independent, composable axes (see RANSAC and robust estimation and Pose — relative pose for the API):
- RANSAC-loop optimizations (always on): Sampson-only scoring with a single final essential-matrix decomposition instead of one per candidate model, vectorized Sampson residuals (
SquaredSampsonDistances), hoisted per-iteration allocations, count-only inlier scoring, andPartialPivLUin the 10×10 elimination. use_sturm_5pt(defaulttrue): swaps in a Sturm-sequence-based 5-point minimal solver (FivePointRelativePoseSturm, adapted from PoseLib) that avoids the eigendecomposition theia's original Stewénius-style solver performs per RANSAC iteration.use_monodepth(defaultfalse, requires per-featuredepth_prior): drops the minimal sample size from 5 (or 8, uncalibrated) to 3, which sharply reduces the number of RANSAC iterations needed for a given inlier ratio/confidence — the effect compounds with the outlier ratio.- Dense LM local optimization when
use_lo=true:RefineModelfor calibrated relative pose, monodepth relative pose, and calibrated absolute pose uses a small analytic LM (theia/math/lmlsq) instead of constructing a Ceres problem (or a one-viewReconstruction) on every LO call. See RANSAC — local optimization.
Representative numbers from dev/benchmark_two_view_estimation.py (2000 correspondences, synthetic scene, use_lo=false; classic = use_sturm_5pt=False):
| outlier ratio | classic 5pt | Sturm 5pt | monodepth 3pt |
|---|---|---|---|
| 10% | 1.51 ms | 0.96 ms | 0.56 ms |
| 30% | 3.26 ms | 2.86 ms | 0.68 ms |
| 50% | 15.69 ms | 13.46 ms | 1.05 ms |
Absolute numbers depend heavily on machine, correspondence count, and outlier ratio — re-run the script on your own hardware/dataset before relying on these for capacity planning. With use_lo=true, wall time rises with the number of LO calls (RansacSummary.num_lo_iterations); the dense LM path keeps that overhead much smaller than the previous Ceres-backed LO.
Measuring performance¶
For two-view relative pose estimation, run dev/benchmark_two_view_estimation.py (see above). For other paths, there is no general in-repo microbenchmark suite; see wall-clock patterns in pyexamples/ and examples/vismatch_sfm/. When comparing changes, use the same Ceres build, thread count, and dataset.