Browse by type
We introduce a framework that improves the accuracy of Structure-from-Motion (SfM) and visual localization by refining keypoints, camera poses, and 3D points using the direct alignment of deep features. It is presented in our paper: - Pixel-Perfect Structure-from-Motion with Featuremetric Refinement - Authors: Philipp Lindenberger*, Paul-Edouard Sarlin*, Viktor Larsson, and Marc Pollefeys - Website: psarlin.com/pixsfm (videos, slides, poster)
Here we provide pixsfm, a Python package that can be readily used with COLMAP and our toolbox hloc. This makes it easy to refine an existing COLMAP model or reconstruct a new dataset with state-of-the-art image matching. Our framework also improves visual localization in challenging conditions.
The refinement is composed of 2 steps:
In each step, we optimize the consistency of dense deep features over multiple views by minimizing a featuremetric cost. These features are extracted beforehand from the images using a pre-trained CNN.
With pixsfm, you can:
Our implementation scales to large scenes by carefully managing the memory and leveraging parallelism and SIMD vectorization when possible.
pixsfm requires Python >=3.6, GCC >=6.1, and COLMAP installed from source at the latest commit. The core optimization is implemented in C++ with Ceres >= 2.1 but we provide Python bindings with high granularity. The code is written for UNIX and has not been tested on Windows. The remaining dependencies are listed in requirements.txt and include PyTorch >=1.7 and pycolmap + pyceres built from source:
# install COLMAP following colmap.github.io/install.html#build-from-source
sudo apt-get install libhdf5-dev
git clone https://github.com/cvg/pixel-perfect-sfm --recursive
cd pixel-perfect-sfm
pip install -r requirements.txt
To use other local features besides SIFT via COLMAP, we also require hloc:
git clone --recursive https://github.com/cvg/Hierarchical-Localization/
cd Hierarchical-Localization/
pip install -e .
Finally build and install the pixsfm package:
pip install -e . # install pixsfm in develop mode
We highly recommend to use pixsfm with a working GPU for the dense feature extraction. All other steps can only run on the CPU. Having issues with compilation errors or runtime crashes? Want to use the codebase as a C++ library? Check our FAQ.
The Jupyter notebook demo.ipynb demonstrates a minimal usage example. It shows how to run Structure-from-Motion and the refinement, how to align and compare different 3D models, and how to localize and refine additional query images.
Visualizing mapping and localization results in the demo.
Given keypoints and matches computed with hloc and stored in HDF5 files, we can run Pixel-Perfect SfM from a Python script:
from pixsfm.refine_hloc import PixSfM
refiner = PixSfM()
model, debug_outputs = refiner.reconstruction(
path_to_working_directory,
path_to_image_dir,
path_to_list_of_image_pairs,
path_to_keypoints.h5,
path_to_matches.h5,
)
# model is a pycolmap.Reconstruction 3D model
or from the command line:
python -m pixsfm.refine_hloc reconstructor \
--sfm_dir path_to_working_directory \
--image_dir path_to_image_dir \
--pairs_path path_to_list_of_image_pairs \
--features_path path_to_keypoints.h5 \
--matches_path path_to_matches.h5
Note that:
path_to_working_directory in either case.debug_outputs contains the dense features and optimization statistics.We have fine-grained control over all hyperparameters via OmegaConf configurations, which have sensible default values defined in PixSfM.default_conf. See Detailed configuration for a description of the main configuration entries and their defaults.
[Click to see some examples]
For example, dense features are stored in memory by default. If we reconstruct a large scene or have limited RAM, we should instead write them to a cache file that is loaded on-demand. With the Python API, we can pass a configuration update:
refiner = PixSfM(conf={"dense_features": {"use_cache": True}})
or equivalently with the command line using a dotlist:
python -m pixsfm.refine_hloc reconstructor [...] dense_features.use_cache=true
We also provide ready-to-use configuration templates in pixsfm/configs/ covering the main use cases. For example, pixsfm/configs/low_memory.yaml reduces the memory consumption to scale to large scene and can be used as follow:
refiner = PixSfM(conf="low_memory")
# or
python -m pixsfm.refine_hloc reconstructor [...] --config low_memory
[Click to expand]
If camera poses are available, we can simply triangulate a 3D point cloud from an existing reference COLMAP model with:
model, _ = refiner.triangulation(..., path_to_reference_model, ...)
or
python -m pixsfm.refine_hloc triangulator [...] \
--reference_sfm_model path_to_reference_model
By default, camera poses and intrinsics are optimized by the bundle adjustment. To keep them fixed, we can simply overwrite the corresponding options as:
conf = {"BA": {"optimizer": {
"refine_focal_length": False,
"refine_extra_params": False, # distortion parameters
"refine_extrinsics": False, # camera poses
}}}
refiner = PixSfM(conf=conf)
refiner.triangulation(...)
or equivalently
python -m pixsfm.refine_hloc triangulator [...] \
'BA.optimizer={refine_focal_length: false, refine_extra_params: false, refine_extrinsics: false}'
The first step of the refinement is the keypoint adjustment (KA). It refines the keypoints from tentative matches only, before SfM. Here we show how to run this step separately.
[Click to expand]
To refine keypoints stored in an hloc HDF5 feature file:
from pixsfm.refine_hloc import PixSfM
refiner = PixSfM()
keypoints, _, _ = refiner.refine_keypoints(
path_to_output_keypoints.h5,
path_to_input_keypoints.h5,
path_to_list_of_image_pairs,
path_to_matches.h5,
path_to_image_dir,
)
To refine keypoints stored in a COLMAP database:
from pixsfm.refine_colmap import PixSfM
refiner = PixSfM()
keypoints, _, _ = refiner.refine_keypoints_from_db(
path_to_output_database, # pass path_to_input_database for in-place refinement
path_to_input_database,
path_to_image_dir,
)
In either case, there is an equivalent command line interface.
The second contribution of the refinement is the bundle adjustment (BA). Here we show how to run it separately to refine an existing COLMAP 3D model.
[Click to expand]
To refine a 3D model stored on file:
from pixsfm.refine_colmap import PixSfM
refiner = PixSfM()
model, _, _, = refiner.refine_reconstruction(
path_to_input_model,
path_to_output_model,
path_to_image_dir,
)
Using the command line interface:
python -m pixsfm.refine_colmap bundle_adjuster \
--input_path path_to_input_model \
--output_path path_to_output_model \
--image_dir path_to_image_dir
When estimating the camera pose of a single image, we can also run the keypoint and bundle adjustments before and after PnP+RANSAC. This requires reference features attached to each observation of the reference model. They can be computed in several ways.
[Click to learn how to localize a single image]
from pixsfm.localization import QueryLocalizer
localizer = QueryLocalizer(
reference_model, # pycolmap.Reconstruction 3D model
image_dir=path_to_reference_image_dir,
dense_features=cache_path, # optional: cache to file for later reuse
)
pose_dict = localizer.localize(
pnp_points2D # keypoints with valid 3D correspondence (N, 2)
pnp_point3D_ids, # IDs of corresponding 3D points in the reconstruction
query_camera, # pycolmap.Camera
image_path=path_to_query_image,
)
if pose_dict["success"]:
# quaternion and translation of the query, from world to camera
qvec, tvec = pose_dict["qvec"], pose_dict["tvec"]
The default localization configuration can be accessed with QueryLocalizer.default_conf.
refiner = PixSfM()
model, outputs = refiner.reconstruction(...)
features = outputs["feature_manager"]
# or load the features manually
features = pixsfm.extract.load_features_from_cache(
refiner.resolve_cache_path(output_dir=path_to_output_sfm)
)
localizer = QueryLocalizer(
reference_model, # pycolmap.Reconstruction 3D model
dense_features=features,
)
We can also batch-localize multiple queries equivalently to hloc.localize_sfm:
pixsfm.localize.main(
dense_features, # FeatureManager or path to cache file
reference_model, # pycolmap.Reconstruction 3D model
path_to_query_list,
path_to_image_dir,
path_to_image_pairs,
path_to_keypoints,
path_to_matches,
path_to_output_results,
config=config, # optional dict
)
We now show how to run the featuremetric pipeline on the Aachen Day-Night v1.1 dataset. First, download the dataset by following the instructions described here. Then run python examples/sfm+loc_aachen.py, which will perform mapping and localization with SuperPoint+SuperGlue. As the scene is large, with over 7k images, we cache the dense feature patches and therefore require about 350GB of free disk space. Expect the sparse feature matching to take a few hours on a recent GPU. We also show in examples/refine_sift_aachen.py how to start from an existing COLMAP database.
We can evaluate the accuracy of the pixel-perfect SfM and of camera pose estimation on the ETH3D dataset. Refer to the paper for more details.
First, we download the dataset with python -m pixsfm.eval.eth3d.download, by default to ./datasets/ETH3D/.
[Click to expand]
We first need to install the ETH3D multi-view evaluation tool:
sudo apt install libpcl-dev # linux only
git clone git@github.com:ETH3D/multi-view-evaluation.git
cd multi-view-evaluation && mkdir build && cd build
cmake .. && make -j
We can then evaluate the accuracy of the sparse 3D point cloud triangulated with Pixel-Perfect SfM, for example on the courtyard scene with SuperPoint keypoints:
python -m pixsfm.eval.eth3d.triangulation \
--scenes courtyard \
--methods superpoint \
--tag pixsfm
--scenes and --methods to run all scenes with all feature detectors../outputs/ETH3D/ by default--tag some_run_name to distinguish different runs--config norefine to turn off any refinement or use the dotlist KA.apply=false BA.apply=false --config photometric to run the photometric BA (no KA)To aggregate the results and compare different runs, for example with and without refinement, we run:
python -m pixsfm.eval.eth3d.plot_triangulation \
--scenes courtyard \
--methods superpoint \
--tags pixsfm raw
Running on all scenes and all detectors should yield the following results (±1%):
``` ----scene---- -keypoints- -tag-- -accuracy @ X cm- completeness @ X cm 1.0 2.
$ claude mcp add pixel-perfect-sfm \
-- python -m otcore.mcp_server <graph>