Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9f5155b
Adapt to the new pycolmap inteface
B1ueber2y Dec 12, 2024
fb39463
update
B1ueber2y Dec 12, 2024
1d14d71
inliers to inlier_mask
B1ueber2y Dec 12, 2024
f91076b
specify minimum pycolmap version
B1ueber2y Dec 13, 2024
dfe106a
fix long-term broken interface
B1ueber2y Dec 16, 2024
13cfc88
reconstruction.reg_image_ids() becomes set rather than list
B1ueber2y Dec 25, 2024
44db557
adapt to the new pycolmap localization interface
B1ueber2y Feb 20, 2025
aa646ff
Merge branch 'master' into features/fix_pycolmap
B1ueber2y Jul 4, 2025
e65f2d3
format
B1ueber2y Jul 4, 2025
ca93b9f
Fix naming error: image_list -> image_names
Phil26AT Jul 4, 2025
375f04a
Fix 3D viz and demo
Phil26AT Jul 4, 2025
33880e0
Improve logging --> progressbar for reconstuction
Phil26AT Jul 4, 2025
d14f7e0
Fix inloc pose estimation
Phil26AT Jul 4, 2025
6ee245b
Add frames and rigs to shutil.move after reconstruction
Phil26AT Jul 4, 2025
cc2b7d5
Fix formatting
Phil26AT Jul 4, 2025
71353ec
Fix geometric verification in triangulation
Phil26AT Jul 4, 2025
9161721
Require pycolmap version 3.12
Phil26AT Jul 4, 2025
26f6ea9
Fix visualization
Phil26AT Jul 4, 2025
00b09c4
Reduce size of demo notebook and switch to ALIKED as default
Phil26AT Jul 21, 2025
8508f66
Create cameras using kwargs instead of dicts
Phil26AT Jul 21, 2025
b772d56
Add write_poses to utils/io and use it in localize_sfm and localize_i…
Phil26AT Jul 21, 2025
c4d7244
Wrap incremental_mapping in OutputCapture
Phil26AT Jul 21, 2025
eb64ebd
Remove unused verbose argument
Phil26AT Jul 21, 2025
9bd3821
Merge branch 'master' into features/fix_pycolmap
sarlinpe Jul 21, 2025
6bd402d
tag pycolmap >=3.12.3.
B1ueber2y Jul 22, 2025
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
229 changes: 184 additions & 45 deletions demo.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion hloc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
except ImportError:
logger.warning("pycolmap is not installed, some features may not work.")
else:
min_version = version.parse("0.6.0")
min_version = version.parse("3.12.0")
found_version = pycolmap.__version__
if found_version != "dev":
version = version.parse(found_version)
Expand Down
17 changes: 8 additions & 9 deletions hloc/localize_inloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from tqdm import tqdm

from . import logger
from .utils.io import write_poses
from .utils.parsers import names_to_pair, parse_retrieval


Expand Down Expand Up @@ -110,7 +111,11 @@ def pose_from_cluster(dataset_dir, q, retrieved, feature_file, match_file, skip=
"height": height,
"params": [focal_length, cx, cy],
}
ret = pycolmap.absolute_pose_estimation(all_mkpq, all_mkp3d, cfg, 48.00)
estimation_options = pycolmap.AbsolutePoseEstimationOptions()
estimation_options.ransac.max_error = 48
ret = pycolmap.estimate_and_refine_absolute_pose(
all_mkpq, all_mkp3d, cfg, estimation_options
)
ret["cfg"] = cfg
return ret, all_mkpq, all_mkpr, all_mkp3d, all_indices, num_matches

Expand Down Expand Up @@ -140,7 +145,7 @@ def main(dataset_dir, retrieval, features, matches, results, skip_matches=None):
dataset_dir, q, db, feature_file, match_file, skip_matches
)

poses[q] = (ret["qvec"], ret["tvec"])
poses[q] = ret["cam_from_world"]
logs["loc"][q] = {
"db": db,
"PnP_ret": ret,
Expand All @@ -152,13 +157,7 @@ def main(dataset_dir, retrieval, features, matches, results, skip_matches=None):
}

logger.info(f"Writing poses to {results}...")
with open(results, "w") as f:
for q in queries:
qvec, tvec = poses[q]
qvec = " ".join(map(str, qvec))
tvec = " ".join(map(str, tvec))
name = q.split("/")[-1]
f.write(f"{name} {qvec} {tvec}\n")
write_poses(poses, results, prepend_camera_name=False)

logs_path = f"{results}_logs.pkl"
logger.info(f"Writing logs to {logs_path}...")
Expand Down
13 changes: 3 additions & 10 deletions hloc/localize_sfm.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from tqdm import tqdm

from . import logger
from .utils.io import get_keypoints, get_matches
from .utils.io import get_keypoints, get_matches, write_poses
from .utils.parsers import parse_image_lists, parse_retrieval


Expand Down Expand Up @@ -58,7 +58,7 @@ def __init__(self, reconstruction, config=None):
def localize(self, points2D_all, points2D_idxs, points3D_id, query_camera):
points2D = points2D_all[points2D_idxs]
points3D = [self.reconstruction.points3D[j].xyz for j in points3D_id]
ret = pycolmap.absolute_pose_estimation(
ret = pycolmap.estimate_and_refine_absolute_pose(
points2D,
points3D,
query_camera,
Expand Down Expand Up @@ -208,14 +208,7 @@ def main(

logger.info(f"Localized {len(cam_from_world)} / {len(queries)} images.")
logger.info(f"Writing poses to {results}...")
with open(results, "w") as f:
for query, t in cam_from_world.items():
qvec = " ".join(map(str, t.rotation.quat[[3, 0, 1, 2]]))
tvec = " ".join(map(str, t.translation))
name = query.split("/")[-1]
if prepend_camera_name:
name = query.split("/")[-2] + "/" + name
f.write(f"{name} {qvec} {tvec}\n")
write_poses(cam_from_world, results, prepend_camera_name=prepend_camera_name)

logs_path = f"{results}_logs.pkl"
logger.info(f"Writing logs to {logs_path}...")
Expand Down
18 changes: 16 additions & 2 deletions hloc/pipelines/7Scenes/create_gt_sfm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@

def scene_coordinates(p2D, R_w2c, t_w2c, depth, camera):
assert len(depth) == len(p2D)
p2D_norm = np.stack(pycolmap.Camera(camera._asdict()).image_to_world(p2D))
pycolmap_camera = pycolmap.Camera(
camera_id=camera.id,
model=camera.model,
width=camera.width,
height=camera.height,
params=camera.params,
)
p2D_norm = pycolmap_camera.cam_from_img(p2D)
p2D_h = np.concatenate([p2D_norm, np.ones_like(p2D_norm[:, :1])], 1)
p3D_c = p2D_h * depth[:, None]
p3D_w = (p3D_c - t_w2c) @ R_w2c
Expand Down Expand Up @@ -52,7 +59,14 @@ def project_to_image(p3D, R, t, camera, eps: float = 1e-4, pad: int = 1):
p3D = (p3D @ R.T) + t
visible = p3D[:, -1] >= eps # keep points in front of the camera
p2D_norm = p3D[:, :-1] / p3D[:, -1:].clip(min=eps)
p2D = np.stack(pycolmap.Camera(camera._asdict()).world_to_image(p2D_norm))
pycolmap_camera = pycolmap.Camera(
camera_id=camera.id,
model=camera.model,
width=camera.width,
height=camera.height,
params=camera.params,
)
p2D = pycolmap_camera.img_from_cam(p2D_norm)
size = np.array([camera.width - pad - 1, camera.height - pad - 1])
valid = np.all((p2D >= pad) & (p2D <= size), -1)
valid &= visible
Expand Down
56 changes: 50 additions & 6 deletions hloc/reconstruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, Dict, List, Optional

import pycolmap
import tqdm

from . import logger
from .triangulation import (
Expand Down Expand Up @@ -46,7 +47,7 @@ def import_images(
database_path,
image_dir,
camera_mode,
image_list=image_list or [],
image_names=image_list or [],
options=options,
)

Expand All @@ -60,6 +61,40 @@ def get_image_ids(database_path: Path) -> Dict[str, int]:
return images


def incremental_mapping(
database_path: Path,
image_dir: Path,
sfm_path: Path,
options: Optional[Dict[str, Any]] = None,
) -> dict[int, pycolmap.Reconstruction]:
num_images = pycolmap.Database(database_path).num_images
pbars = []

def restart_progress_bar():
if len(pbars) > 0:
pbars[-1].close()
pbars.append(
tqdm.tqdm(
total=num_images,
desc=f"Reconstruction {len(pbars)}",
unit="images",
postfix="registered",
)
)
pbars[-1].update(2)

reconstructions = pycolmap.incremental_mapping(
database_path,
image_dir,
sfm_path,
options=options or {},
initial_image_pair_callback=restart_progress_bar,
next_image_callback=lambda: pbars[-1].update(1),
)

return reconstructions


def run_reconstruction(
sfm_dir: Path,
database_path: Path,
Expand All @@ -73,11 +108,11 @@ def run_reconstruction(
if options is None:
options = {}
options = {"num_threads": min(multiprocessing.cpu_count(), 16), **options}

with OutputCapture(verbose):
with pycolmap.ostream():
reconstructions = pycolmap.incremental_mapping(
database_path, image_dir, models_path, options=options
)
reconstructions = incremental_mapping(
database_path, image_dir, models_path, options=options
)

if len(reconstructions) == 0:
logger.error("Could not reconstruct any model!")
Expand All @@ -96,7 +131,13 @@ def run_reconstruction(
f"Largest model is #{largest_index} " f"with {largest_num_images} images."
)

for filename in ["images.bin", "cameras.bin", "points3D.bin"]:
for filename in [
"images.bin",
"cameras.bin",
"points3D.bin",
"frames.bin",
"rigs.bin",
]:
if (sfm_dir / filename).exists():
(sfm_dir / filename).unlink()
shutil.move(str(models_path / str(largest_index) / filename), str(sfm_dir))
Expand Down Expand Up @@ -124,6 +165,9 @@ def main(
sfm_dir.mkdir(parents=True, exist_ok=True)
database = sfm_dir / "database.db"

logger.info(f"Writing COLMAP logs to {sfm_dir / 'colmap.LOG.*'}")
pycolmap.logging.set_log_destination(pycolmap.logging.INFO, sfm_dir / "colmap.LOG.")

create_empty_db(database)
import_images(image_dir, database, camera_mode, image_list, image_options)
image_ids = get_image_ids(database)
Expand Down
31 changes: 11 additions & 20 deletions hloc/triangulation.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import argparse
import contextlib
import io
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional

Expand All @@ -22,15 +19,11 @@ def __init__(self, verbose: bool):

def __enter__(self):
if not self.verbose:
self.capture = contextlib.redirect_stdout(io.StringIO())
self.out = self.capture.__enter__()
pycolmap.logging.alsologtostderr = False

def __exit__(self, exc_type, *args):
if not self.verbose:
self.capture.__exit__(exc_type, *args)
if exc_type is not None:
logger.error("Failed with output:\n%s", self.out.getvalue())
sys.stdout.flush()
pycolmap.logging.alsologtostderr = True


def create_db_from_model(
Expand Down Expand Up @@ -114,12 +107,11 @@ def estimation_and_geometric_verification(
):
logger.info("Performing geometric verification of the matches...")
with OutputCapture(verbose):
with pycolmap.ostream():
pycolmap.verify_matches(
database_path,
pairs_path,
options=dict(ransac=dict(max_num_trials=20000, min_inlier_ratio=0.1)),
)
pycolmap.verify_matches(
database_path,
pairs_path,
options=dict(ransac=dict(max_num_trials=20000, min_inlier_ratio=0.1)),
)


def geometric_verification(
Expand Down Expand Up @@ -170,7 +162,7 @@ def geometric_verification(
db.add_two_view_geometry(id0, id1, matches)
continue

cam1_from_cam0 = image1.cam_from_world * image0.cam_from_world.inverse()
cam1_from_cam0 = image1.cam_from_world() * image0.cam_from_world().inverse()
errors0, errors1 = compute_epipolar_errors(
cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]]
)
Expand Down Expand Up @@ -207,10 +199,9 @@ def run_triangulation(
if options is None:
options = {}
with OutputCapture(verbose):
with pycolmap.ostream():
reconstruction = pycolmap.triangulate_points(
reference_model, database_path, image_dir, model_path, options=options
)
reconstruction = pycolmap.triangulate_points(
reference_model, database_path, image_dir, model_path, options=options
)
return reconstruction


Expand Down
2 changes: 1 addition & 1 deletion hloc/utils/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def to_homogeneous(p):


def compute_epipolar_errors(j_from_i: pycolmap.Rigid3d, p2d_i, p2d_j):
j_E_i = j_from_i.essential_matrix()
j_E_i = pycolmap.essential_matrix_from_pose(j_from_i)
l2d_j = to_homogeneous(p2d_i) @ j_E_i.T
l2d_i = to_homogeneous(p2d_j) @ j_E_i
dist = np.abs(np.sum(to_homogeneous(p2d_i) * l2d_i, axis=1))
Expand Down
16 changes: 15 additions & 1 deletion hloc/utils/io.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from pathlib import Path
from typing import Tuple
from typing import Mapping, Tuple

import cv2
import h5py
import numpy as np
import pycolmap

from .parsers import names_to_pair, names_to_pair_old

Expand Down Expand Up @@ -76,3 +77,16 @@ def get_matches(path: Path, name0: str, name1: str) -> Tuple[np.ndarray]:
matches = np.flip(matches, -1)
scores = scores[idx]
return matches, scores


def write_poses(
poses: Mapping[str, pycolmap.Rigid3d], path: str, prepend_camera_name: bool
):
with open(path, "w") as f:
for query, t in poses.items():
qvec = " ".join(map(str, t.rotation.quat[[3, 0, 1, 2]]))
tvec = " ".join(map(str, t.translation))
name = query.split("/")[-1]
if prepend_camera_name:
name = query.split("/")[-2] + "/" + name
f.write(f"{name} {qvec} {tvec}\n")
35 changes: 22 additions & 13 deletions hloc/utils/viz_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def plot_camera(
legendgroup=legendgroup,
name=name,
showlegend=False,
hovertemplate=text.replace("\n", "<br>"),
hovertemplate=text.replace("\n", "<br>") if text else None,
)
fig.add_trace(pyramid)

Expand All @@ -134,25 +134,37 @@ def plot_camera(
name=name,
line=dict(color=color, width=1),
showlegend=False,
hovertemplate=text.replace("\n", "<br>"),
hovertemplate=text.replace("\n", "<br>") if text else None,
)
fig.add_trace(pyramid)


def plot_camera_colmap(
fig: go.Figure,
image: pycolmap.Image,
camera: pycolmap.Camera,
name: Optional[str] = None,
**kwargs
fig: go.Figure, cam_from_world: pycolmap.Rigid3d, camera: pycolmap.Camera, **kwargs
):
"""Plot a camera frustum from PyCOLMAP objects"""
world_t_camera = image.cam_from_world.inverse()
world_t_camera = cam_from_world.inverse()
plot_camera(
fig,
world_t_camera.rotation.matrix(),
world_t_camera.translation,
camera.calibration_matrix(),
**kwargs
)


def plot_image_colmap(
fig: go.Figure,
image: pycolmap.Image,
camera: pycolmap.Camera,
name: Optional[str] = None,
**kwargs
):
"""Plot a camera frustum from a PyCOLMAP image."""
plot_camera_colmap(
fig,
image.cam_from_world(),
camera,
name=name or str(image.image_id),
text=str(image),
**kwargs
Expand All @@ -162,9 +174,7 @@ def plot_camera_colmap(
def plot_cameras(fig: go.Figure, reconstruction: pycolmap.Reconstruction, **kwargs):
"""Plot a camera as a cone with camera frustum."""
for image_id, image in reconstruction.images.items():
plot_camera_colmap(
fig, image, reconstruction.cameras[image.camera_id], **kwargs
)
plot_image_colmap(fig, image, reconstruction.cameras[image.camera_id], **kwargs)


def plot_reconstruction(
Expand All @@ -186,8 +196,7 @@ def plot_reconstruction(
p3D
for _, p3D in rec.points3D.items()
if (
(p3D.xyz >= bbs[0]).all()
and (p3D.xyz <= bbs[1]).all()
bbs.contains_point(p3D.xyz)
and p3D.error <= max_reproj_error
and p3D.track.length() >= min_track_length
)
Expand Down
Loading