Skip to main content

pyTheia - A Python Structure-from-Motion and Geometric Vision Swiss Knife

pyTheia is based on TheiaSfM. It contains Python bindings for most of the functionalities of TheiaSfM and more.

Documentation: https://urbste.github.io/pyTheiaSfM/ (MkDocs; build locally with pip install -r docs/requirements.txt and mkdocs serve -f docs/mkdocs.yml).

The library is still in active development and the interfaces are not yet all fixed

With pyTheia you have access to a variety of different camera models, structure-from-motion pipelines and geometric vision algorithms.

Differences to the original library TheiaSfM

pyTheia does not aim at being an end-to-end SfM library. For example, building robust feature detection and matching pipelines is usually application and data specific (e.g. image resolution, runtime, pose priors, invariances, ...). This includes image pre- and postprocessing.

pyTheia is rather a "swiss knife" for quickly prototyping SfM related reconstruction applications without sacrificing perfomance. For example SOTA feature detection & matching, place recognition algorithms are based on deep learning, and easily usable from Python. However, using these algorithms from a C++ library is not always straighforward and especially quick testing and prototyping is cumbersome.

Dependency changes

Compared to the original TheiaSfM:

  • SuiteSparse: Optional for Ceres; GPL-dependent code was removed in src/math/matrix/sparse_cholesky_llt.cc (cholmod -> Eigen::SimplicialLDLT), which may be slower for very large problems and slightly less stable numerically.
  • RapidJSON: No separate dependency; RapidJSON is vendored via cereal headers.
  • OpenImageIO / theia/image: Not used. Raster images and EXIF are handled in Python (OpenCV, Pillow, etc.); C++ focuses on geometry, matching structures, and SfM pipelines once correspondences exist.

Changes to the original TheiaSfM library

  • Global SfM algorithms:
    • LiGT position solver
    • Lagrange Dual rotation estimator
    • Hybrid rotation estimator
    • Possibility to fix multiple views in Robust_L1L2 solver
    • Nonlinear translation solver can fix multiple view or estimate all remaining views in reconstruction
  • Camera models
    • Double Sphere
    • Extended Unified
    • Orthographic
  • Bundle adjustment
    • Using a homogeneous representation for scene points
    • Extracting covariance information
    • Possibility to add a depth prior to 3D points
    • Position prior for camera poses (e.g. for GPS or known positions)
  • General
    • Added timestamp, position_prior_, position_prior_sqrt_information_ variables to View class Eigen::Matrix3d position_prior_sqrt_information_;
    • Added inverse_depth_, reference_descriptor, reference_bearing_ variables to Track class
    • Added covariance_, depth_prior_, depth_prior_variance_ to Feature class
  • Absolute Pose solvers
    • SQPnP
    • UncalibratedPlanarOrthographic Pose
  • Transformations / cross-run alignment
    • BA relative pose edges (BundleAdjustReconstructionWithRelativePoseEdges, scale-invariant odometry via RelativePoseConstraint) — see Cross-run alignment
    • Cross-reconstruction Sim(3) pose graph (AlignReconstructionsWithPoseGraph, CrossReconstructionSim3PoseGraphOptimizer, …) — see Transformations → pose graph and Cross-run alignment

Usage Examples

Full reconstruction example: Global, Hybrid or Incremental SfM using OpenCV feature detection and matching

Have a look at the short example: sfm_pipeline.py. Download the south_building dataset from here. Extract it somewhere and run:

python pytests/sfm_pipeline.py --image_path /path/to/south-building/images/

Creating a camera

The following example show you how to create a camera in pyTheia. You can construct it from a pt.sfm.CameraIntrinsicsPrior() or set all parameters using respective functions from pt.sfm.Camera() class.

import pytheia as pt
prior = pt.sfm.CameraIntrinsicsPrior()
prior.focal_length.value = [1000.]
prior.aspect_ratio.value = [1.]
prior.principal_point.value = [500., 500.]
prior.radial_distortion.value = [0., 0., 0., 0]
prior.tangential_distortion.value = [0., 0.]
prior.skew.value = [0]
prior.camera_intrinsics_model_type = 'PINHOLE' 
#'PINHOLE', 'DOUBLE_SPHERE', 'EXTENDED_UNIFIED', 'FISHEYE', 'FOV', 'DIVISION_UNDISTORTION'
camera = pt.sfm.Camera()
camera.SetFromCameraIntrinsicsPriors(prior)

# the camera object also carries extrinsics information
camera.SetPosition([0,0,-2])
camera.SetOrientationFromAngleAxis([0,0,0.1])

# project with intrinsics image to camera coordinates
camera_intrinsics = camera.CameraIntrinsics()
pt2 = [100.,100.]
pt3 = camera_intrinsics.ImageToCameraCoordinates(pt2)
pt2 = camera_intrinsics.CameraToImageCoordinates(pt3)

# project with camera extrinsics
pt3_h = [1,1,2,1] # homogeneous 3d point
depth, pt2 = camera.ProjectPoint(pt3_h)
# get a ray from camera to 3d point in the world frame
ray = camera.PixelToUnitDepthRay(pt2)
pt3_h_ = ray*depth + camera.GetPosition() # == pt3_h[:3]

Solve for absolute or relative camera pose

pyTheia integrates a lot of performant geometric vision algorithms. Have a look at the tests

import pytheia as pt

# absolute pose
pose = pt.sfm.PoseFromThreePoints(pts2D, pts3D) # Kneip
pose = pt.sfm.FourPointsPoseFocalLengthRadialDistortion(pts2D, pts3D)
pose = pt.sfm.FourPointPoseAndFocalLength(pts2D, pts3D)
pose = pt.sfm.DlsPnp(pts2D, pts3D)
... and more

# relative pose
pose = pt.sfm.NormalizedEightPointFundamentalMatrix(pts2D, pts2D)
pose = pt.sfm.FourPointHomography(pts2D, pts2D)
pose = pt.sfm.FivePointRelativePose(pts2D, pts2D)
pose = pt.sfm.SevenPointFundamentalMatrix(pts2D, pts2D)
... and more

# ransac estimation
params = pt.solvers.RansacParameters()
params.error_thresh = 0.1
params.max_iterations = 100
params.failure_probability = 0.01

# absolute pose ransac
correspondences2D3D = pt.matching.FeatureCorrespondence2D3D(
  pt.sfm.Feature(point1), pt.sfm.Feature(point2))

pnp_type =  pt.sfm.PnPType.DLS #  pt.sfm.PnPType.SQPnP,  pt.sfm.PnPType.KNEIP
success, abs_ori, summary = pt.sfm.EstimateCalibratedAbsolutePose(
  params, pt.sfm.RansacType(0), pnp_type, correspondences2D3D)

success, abs_ori, summary = pt.sfm.EstimateAbsolutePoseWithKnownOrientation(
  params, pt.sfm.RansacType(0), correspondences2D3D)
... and more
# relative pose ransac
correspondences2D2D = pt.matching.FeatureCorrespondence(
            pt.sfm.Feature(point1), pt.sfm.Feature(point2))

success, rel_ori, summary = pt.sfm.EstimateRelativePose(
        params, pt.sfm.RansacType(0), correspondences2D2D)

success, rad_homog, summary = pt.sfm.EstimateRadialHomographyMatrix(
        params, pt.sfm.RansacType(0), correspondences2D2D)  

success, rad_homog, summary = pt.sfm.EstimateFundamentalMatrix(
        params, pt.sfm.RansacType(0), correspondences2D2D)  
... and more

Bundle Adjustment of views or points

import pytheia as pt
recon = pt.sfm.Reconstruction()
# add some views and points
view_id = recon.AddView() 
...
track_id = recon.AddTrack()
...
covariance = np.eye(2) * 0.5**2
point = [200,200]
recon.AddObservation(track_id, view_id, pt.sfm.Feature(point, covariance))

# robust BA
opts = pt.sfm.BundleAdjustmentOptions()
opts.robust_loss_width = 1.345
opts.loss_function_type = pt.sfm.LossFunctionType.HUBER

res = pt.sfm.BundleAdjustReconstruction(opts, recon)
res = pt.sfm.BundleAdjustPartialReconstruction(opts, {view_ids}, {track_ids}, recon)
res = pt.sfm.BundleAdjustPartialViewsConstant(opts, {var_view_ids}, {const_view_ids}, recon)

# optimize absolute pose on normalized 2D 3D correspondences
res = pt.sfm.OptimizeAbsolutePoseOnNormFeatures(
  [pt.sfm.FeatureCorrespondence2D3D], R_init, p_init, opts)

# bundle camera adjust pose only
res = pt.sfm.BundleAdjustView(recon, opts, view_id)
res = pt.sfm.BundleAdjustViewWithCov(recon, view_id)
res = pt.sfm.BundleAdjustViewsWithCov(recon, opts, [view_id1,view_id2])

# optimize structure only
res = pt.sfm.BundleAdjustTrack(recon, opts, trackid)
res = pt.sfm.BundleAdjustTrackWithCov(recon, opts, [view_id1,view_id2])
res = pt.sfm.BundleAdjustTracksWithCov(recon, opts, [view_id1,trackid])

# two view optimization
res = pt.sfm.BundleAdjustTwoViewsAngular(recon, [pt.sfm.FeatureCorrespondence], pt.sfm.TwoViewInfo())

Export to Nerfstudio and SDFStudio

You can export a pt.sfm.Reconstruction to Nerfstudio or SDFStudio formats directly from Python:

import pytheia as pt
# Nerfstudio (writes transforms.json)
pt.io.WriteNerfStudio("/path/to/images", recon, 16, "/path/to/out/transforms.json")
# SDFStudio (all images must be undistorted)
pt.io.WriteSdfStudio("/path/to/images", recon, (2.0, 6.0), 1.0)

More complete examples are in pyexamples/io/nerfstudio_export_reconstruction.py and pyexamples/io/sdfstudio_export_reconstruction.py.

Building

This section describes how to build on Ubuntu locally or on WSL2 (with sudo where noted).

Core dependency: Ceres Solver (non-linear least squares for bundle adjustment and many solvers). A normal Ceres install also pulls in Eigen, glog, and gflags (or your distro equivalents).

  • Use a current Ceres 2.x release (see Ceres installation). Older 2.1.x is still fine for CPU-only builds.
  • Optional — GPU solvers in Ceres: build Ceres with USE_CUDA=ON for CUDA dense linear algebra. For CUDA sparse (CUDA_SPARSE in Ceres), build Ceres with NVIDIA cuDSS support so the library includes the cuDSS component (details in upstream docs). When you configure pyTheia, CMake detects dense CUDA (compile check) and sparse CUDA (CERES_COMPILED_COMPONENTS); you can override with -DTHEIA_CERES_USE_CUDA / -DTHEIA_CERES_USE_CUDA_SPARSE if needed. Runtime selection of GPU backends is via BundleAdjustmentOptions (dense_linear_algebra_library_type, sparse_linear_algebra_library_type); see the MkDocs chapter Bundle adjustment. Pre-built manylinux wheels typically link a CPU Ceres — use a local build against GPU-enabled Ceres for CUDA backends.

Example: system install (sudo)

sudo apt install cmake build-essential libgflags-dev libgoogle-glog-dev libatlas-base-dev

mkdir LIBS && cd LIBS

# Eigen (example 3.4.x)
git clone https://gitlab.com/libeigen/eigen.git
cd eigen && git checkout 3.4.0
mkdir -p build && cd build && cmake .. && sudo cmake --install .

# Ceres — latest 2.x tag; add -DUSE_CUDA=ON / cuDSS CMake variables per Ceres docs for GPU
cd ../..
git clone https://github.com/ceres-solver/ceres-solver.git
cd ceres-solver && git fetch --tags && git checkout "$(git tag -l '2.*' | sort -V | tail -1)"
mkdir build && cd build
cmake .. -DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF -DBUILD_BENCHMARKS=OFF
cmake --build . -j"$(nproc)"
sudo cmake --install .

Local build without sudo

Prefer Ceres EXPORT_BUILD_DIR=ON so find_package(Ceres) can use the build tree. You still need development packages for gflags/glog/atlas (or ask your admin).

mkdir /home/LIBS && cd /home/LIBS

git clone https://gitlab.com/libeigen/eigen.git
cd eigen && git checkout 3.4.0
mkdir -p build && cd build && cmake .. -DCMAKE_INSTALL_PREFIX=/home/LIBS/eigen/build && cmake --build . -j"$(nproc)" && cmake --install .

cd /home/LIBS
git clone https://github.com/ceres-solver/ceres-solver.git
cd ceres-solver && git fetch --tags && git checkout "$(git tag -l '2.*' | sort -V | tail -1)"
mkdir build && cd build
cmake .. -DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF -DBUILD_BENCHMARKS=OFF -DEXPORT_BUILD_DIR=ON
cmake --build . -j"$(nproc)"

cd /path/to/pyTheiaSfM && mkdir build && cd build
cmake -DEigen3_DIR=/home/LIBS/eigen/build/share/eigen3/cmake/ -DCeres_DIR=/home/LIBS/ceres-solver/build ../
cmake --build . -j"$(nproc)"

Full narrative (system deps, CUDA notes, docs build): docs/content/building.md (also rendered as Building on the project docs site).

How to build Python wheels

Local build with sudo installed ceres-solver and Eigen

Tested on Ubuntu. In your Python >= 3.8 environment of choice run:

sh build_and_install.sh

If you have problems like /lib/libstdc++.so.6: version `GLIBCXX_3.4.30' not found on Ubuntu 22.04 in an Anaconda environment try:

conda install -c conda-forge libstdcxx-ng

Another solution is to check the GLIBCXX versions. If the version that the library requires is installed, then we can create a symbolic link into the conda environment.

strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX
# if the GLIBCXX version is available then do:
ln -sf /usr/lib/x86_64-linux-gnu/libstdc++.so.6 ${CONDA_PREFIX}/lib/libstdc++.so.6

With Docker

The docker build will actually build manylinux wheels for Linux (Python 3.8-3.13). There are two ways to do that. One will clutter the source directory, but you will have the wheel file directly available (./wheelhouse/). Another drawback of this approach is that the files will have been created with docker sudo rights and are diffcult to delete:

# e.g. for python 3.9
docker run --rm -e PYTHON_VERSION="cp39-cp39" -v `pwd`:/home urbste/pytheia_base:1.4.0 /home/pypackage/build-wheel-linux.sh

The other one is cleaner but you will have to copy the wheels out of the docker container afterwards:

docker build -t pytheia:1.0 .
docker run -it pytheia:1.0

Then all the wheels will be inside the container in the folder /home/wheelhouse. Open a second terminal and run

docker ps # this will give you a list of running containers to find the correct CONTAINER_ID
docker cp CONTAINER_ID:/home/wheelhouse /path/to/result/folder/pytheia_wheels

Typing and editor stubs

To get full function/argument lists and IntelliSense in editors for the native extension:

  • Stubs ship with the package under src/pytheia/ (pytheia.pyi, pytheia/*.pyi, and py.typed). After pip install, editors such as VS Code/Pylance pick them up from the installed package.

  • Regenerate stubs locally (requires a built extension and pybind11-stubgen):

    pip install pybind11-stubgen
    dev/generate_stubs.sh
    

    This refreshes .pyi files under src/pytheia/ (same layout as the wheel build).

  • When building wheels via setup.py, stubs are generated automatically by default. To skip:

    GENERATE_STUBS=0 python setup.py bdist_wheel --plat-name=...
    
  • The package ships a PEP 561 marker (py.typed) so downstream type checkers can consume the bundled stubs.

Acknowledgements

pyTheiaSfM includes minimal relative-pose solvers and dense local-optimization refiners adapted (not vendored as a dependency) from PoseLib by Viktor Larsson and contributors, distributed under the BSD-3-Clause license (see docs/licenses/POSELIB_LICENSE.txt). The adapted algorithms are described in:

  • D. Nistér, "An Efficient Solution to the Five-Point Relative Pose Problem", IEEE TPAMI, 2004. (Sturm-sequence-based 5-point solver)
  • Y. Ding, V. Larsson, et al., "RePoseD: Efficient Relative Pose Estimation With Known Depth Information", ICCV 2025. (Monocular-depth 3-point solvers)

RANSAC RefineModel for calibrated relative pose, monodepth relative pose, and calibrated absolute pose uses PoseLib-style analytic LM (src/theia/math/lmlsq/, src/theia/sfm/pose/refine_*.h) inside Theia’s own sample-consensus loop.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pytheia-1.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (11.6 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

pytheia-1.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (11.6 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

pytheia-1.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (11.5 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

pytheia-1.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (11.5 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

pytheia-1.2.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (11.5 MB view details)

Uploaded CPython 3.9manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

pytheia-1.2.1-cp38-cp38-manylinux_2_28_x86_64.whl (11.5 MB view details)

Uploaded CPython 3.8manylinux: glibc 2.28+ x86-64

File details

Details for the file pytheia-1.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pytheia-1.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 50d32e4f02c99b41ff0178eeb5f000da96ac7ba447be41e8e45609ffd556a57d
MD5 bb2acaf2c357c9ce76be695f5f61bbf5
BLAKE2b-256 5404b77cfdf34cb988eed04cb2bbb4b9a526c83f8e39a1a4235fb9874951a0cb

See more details on using hashes here.

File details

Details for the file pytheia-1.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pytheia-1.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 e4e04540bfe75ecf7ab17883875314698680212390f95f4eaaadcd34ba057deb
MD5 514199874f3dd468c85315dc29fecc66
BLAKE2b-256 e4e16e4a4d21302eac97dcf8c78263d087eaef29581833a76ed5d80ad339a22b

See more details on using hashes here.

File details

Details for the file pytheia-1.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pytheia-1.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 1e6127ab349db3624051048bcff4a068aa6884694d58374207adaa98985d239d
MD5 e2e06a13444ad48ca70d75a6746877f9
BLAKE2b-256 ace20a240674b98381f8da9002e65851b1c6948233a0a8de57f4dc7991e0b186

See more details on using hashes here.

File details

Details for the file pytheia-1.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pytheia-1.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 fbd4ed48530b8d0d7bdcbb4552cb344f9d5bfdd65860f9667514c4c8d5759acb
MD5 ef1fb5f09d3190f47f94df100456b2e8
BLAKE2b-256 0b6b66c1ef0659b5da8bddfc35c0d712139c8b90d4b07a6a83ddf20a80d047b8

See more details on using hashes here.

File details

Details for the file pytheia-1.2.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pytheia-1.2.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 9fdc5a9bb0e62305c3e748fbb3701143f57bd139aa8b754454751d5e1158bd82
MD5 e04173c122c96463385b3045a4a887d1
BLAKE2b-256 db856be3a02080ad65c449e17049b9ade908611e9673b3f94965259b49f84054

See more details on using hashes here.

File details

Details for the file pytheia-1.2.1-cp38-cp38-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pytheia-1.2.1-cp38-cp38-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 8cf6d58c73ef0e490736fe216f9846f6ed0c88dadba9646642094b3883d293b3
MD5 4b916931b4cfcf45540260dcb8dfac7d
BLAKE2b-256 fdb23b43885c5a3eca823d6eaa400b0a0a54f9a9055fcecf56c9f82952adb6b5

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.2.1 This release

6 files

1.2.0

6 files

1.0.8

6 files

1.0.7

6 files

1.0.6

6 files

1.0.5

6 files

1.0.4

6 files

1.0.3

6 files

1.0.2

6 files

1.0.1

6 files

1.0.0

6 files

0.5.0

6 files

0.4.6

6 files

0.4.5

6 files

0.4.4

6 files

0.4.3

6 files

0.4.2

6 files

0.4.1

5 files

0.4.0

12 files

0.3.0

12 files

0.2.9

12 files

0.2.7

15 files

0.2.6

12 files

0.2.5

12 files

0.2.4

12 files

0.2.3

12 files

0.2.2

12 files

0.2.1

12 files

0.2.0

12 files

0.1.27

12 files

0.1.26

12 files

0.1.25

12 files

0.1.24

5 files

0.1.22

5 files

0.1.21

5 files

0.1.20

5 files

0.1.19

5 files

0.1.18

5 files

0.1.17

5 files

0.1.16

5 files

0.1.15

5 files

0.1.14

5 files

0.1.13

5 files

0.1.9

5 files

0.1.8

5 files

0.1.7

5 files

0.1.6

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page