Skip to content

Commit 2e2a551

Browse files
B1ueber2yPhil26ATsarlinpe
authored
Adapt to the new interface for pycolmap 3.12 (#467)
* Adapt to the new pycolmap inteface * update * inliers to inlier_mask * specify minimum pycolmap version * fix long-term broken interface * reconstruction.reg_image_ids() becomes set rather than list * adapt to the new pycolmap localization interface * format * Fix naming error: image_list -> image_names * Fix 3D viz and demo * Improve logging --> progressbar for reconstuction * Fix inloc pose estimation * Add frames and rigs to shutil.move after reconstruction * Fix formatting * Fix geometric verification in triangulation * Require pycolmap version 3.12 * Fix visualization * Reduce size of demo notebook and switch to ALIKED as default * Create cameras using kwargs instead of dicts * Add write_poses to utils/io and use it in localize_sfm and localize_inloc * Wrap incremental_mapping in OutputCapture * Remove unused verbose argument * tag pycolmap >=3.12.3. --------- Co-authored-by: Phil26AT <phil.lindenberger@gmail.com> Co-authored-by: Paul-Edouard Sarlin <15985472+sarlinpe@users.noreply.github.com>
1 parent 3bdf494 commit 2e2a551

12 files changed

Lines changed: 320 additions & 115 deletions

File tree

demo.ipynb

Lines changed: 184 additions & 45 deletions
Large diffs are not rendered by default.

hloc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
except ImportError:
2222
logger.warning("pycolmap is not installed, some features may not work.")
2323
else:
24-
min_version = version.parse("0.6.0")
24+
min_version = version.parse("3.12.3")
2525
found_version = pycolmap.__version__
2626
if found_version != "dev":
2727
version = version.parse(found_version)

hloc/localize_inloc.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from tqdm import tqdm
1212

1313
from . import logger
14+
from .utils.io import write_poses
1415
from .utils.parsers import names_to_pair, parse_retrieval
1516

1617

@@ -110,7 +111,11 @@ def pose_from_cluster(dataset_dir, q, retrieved, feature_file, match_file, skip=
110111
"height": height,
111112
"params": [focal_length, cx, cy],
112113
}
113-
ret = pycolmap.absolute_pose_estimation(all_mkpq, all_mkp3d, cfg, 48.00)
114+
estimation_options = pycolmap.AbsolutePoseEstimationOptions()
115+
estimation_options.ransac.max_error = 48
116+
ret = pycolmap.estimate_and_refine_absolute_pose(
117+
all_mkpq, all_mkp3d, cfg, estimation_options
118+
)
114119
ret["cfg"] = cfg
115120
return ret, all_mkpq, all_mkpr, all_mkp3d, all_indices, num_matches
116121

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

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

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

163162
logs_path = f"{results}_logs.pkl"
164163
logger.info(f"Writing logs to {logs_path}...")

hloc/localize_sfm.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from tqdm import tqdm
1010

1111
from . import logger
12-
from .utils.io import get_keypoints, get_matches
12+
from .utils.io import get_keypoints, get_matches, write_poses
1313
from .utils.parsers import parse_image_lists, parse_retrieval
1414

1515

@@ -58,7 +58,7 @@ def __init__(self, reconstruction, config=None):
5858
def localize(self, points2D_all, points2D_idxs, points3D_id, query_camera):
5959
points2D = points2D_all[points2D_idxs]
6060
points3D = [self.reconstruction.points3D[j].xyz for j in points3D_id]
61-
ret = pycolmap.absolute_pose_estimation(
61+
ret = pycolmap.estimate_and_refine_absolute_pose(
6262
points2D,
6363
points3D,
6464
query_camera,
@@ -208,14 +208,7 @@ def main(
208208

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

220213
logs_path = f"{results}_logs.pkl"
221214
logger.info(f"Writing logs to {logs_path}...")

hloc/pipelines/7Scenes/create_gt_sfm.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111

1212
def scene_coordinates(p2D, R_w2c, t_w2c, depth, camera):
1313
assert len(depth) == len(p2D)
14-
p2D_norm = np.stack(pycolmap.Camera(camera._asdict()).image_to_world(p2D))
14+
pycolmap_camera = pycolmap.Camera(
15+
camera_id=camera.id,
16+
model=camera.model,
17+
width=camera.width,
18+
height=camera.height,
19+
params=camera.params,
20+
)
21+
p2D_norm = pycolmap_camera.cam_from_img(p2D)
1522
p2D_h = np.concatenate([p2D_norm, np.ones_like(p2D_norm[:, :1])], 1)
1623
p3D_c = p2D_h * depth[:, None]
1724
p3D_w = (p3D_c - t_w2c) @ R_w2c
@@ -52,7 +59,14 @@ def project_to_image(p3D, R, t, camera, eps: float = 1e-4, pad: int = 1):
5259
p3D = (p3D @ R.T) + t
5360
visible = p3D[:, -1] >= eps # keep points in front of the camera
5461
p2D_norm = p3D[:, :-1] / p3D[:, -1:].clip(min=eps)
55-
p2D = np.stack(pycolmap.Camera(camera._asdict()).world_to_image(p2D_norm))
62+
pycolmap_camera = pycolmap.Camera(
63+
camera_id=camera.id,
64+
model=camera.model,
65+
width=camera.width,
66+
height=camera.height,
67+
params=camera.params,
68+
)
69+
p2D = pycolmap_camera.img_from_cam(p2D_norm)
5670
size = np.array([camera.width - pad - 1, camera.height - pad - 1])
5771
valid = np.all((p2D >= pad) & (p2D <= size), -1)
5872
valid &= visible

hloc/reconstruction.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any, Dict, List, Optional
66

77
import pycolmap
8+
import tqdm
89

910
from . import logger
1011
from .triangulation import (
@@ -46,7 +47,7 @@ def import_images(
4647
database_path,
4748
image_dir,
4849
camera_mode,
49-
image_list=image_list or [],
50+
image_names=image_list or [],
5051
options=options,
5152
)
5253

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

6263

64+
def incremental_mapping(
65+
database_path: Path,
66+
image_dir: Path,
67+
sfm_path: Path,
68+
options: Optional[Dict[str, Any]] = None,
69+
) -> dict[int, pycolmap.Reconstruction]:
70+
num_images = pycolmap.Database(database_path).num_images
71+
pbars = []
72+
73+
def restart_progress_bar():
74+
if len(pbars) > 0:
75+
pbars[-1].close()
76+
pbars.append(
77+
tqdm.tqdm(
78+
total=num_images,
79+
desc=f"Reconstruction {len(pbars)}",
80+
unit="images",
81+
postfix="registered",
82+
)
83+
)
84+
pbars[-1].update(2)
85+
86+
reconstructions = pycolmap.incremental_mapping(
87+
database_path,
88+
image_dir,
89+
sfm_path,
90+
options=options or {},
91+
initial_image_pair_callback=restart_progress_bar,
92+
next_image_callback=lambda: pbars[-1].update(1),
93+
)
94+
95+
return reconstructions
96+
97+
6398
def run_reconstruction(
6499
sfm_dir: Path,
65100
database_path: Path,
@@ -73,11 +108,11 @@ def run_reconstruction(
73108
if options is None:
74109
options = {}
75110
options = {"num_threads": min(multiprocessing.cpu_count(), 16), **options}
111+
76112
with OutputCapture(verbose):
77-
with pycolmap.ostream():
78-
reconstructions = pycolmap.incremental_mapping(
79-
database_path, image_dir, models_path, options=options
80-
)
113+
reconstructions = incremental_mapping(
114+
database_path, image_dir, models_path, options=options
115+
)
81116

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

99-
for filename in ["images.bin", "cameras.bin", "points3D.bin"]:
134+
for filename in [
135+
"images.bin",
136+
"cameras.bin",
137+
"points3D.bin",
138+
"frames.bin",
139+
"rigs.bin",
140+
]:
100141
if (sfm_dir / filename).exists():
101142
(sfm_dir / filename).unlink()
102143
shutil.move(str(models_path / str(largest_index) / filename), str(sfm_dir))
@@ -124,6 +165,9 @@ def main(
124165
sfm_dir.mkdir(parents=True, exist_ok=True)
125166
database = sfm_dir / "database.db"
126167

168+
logger.info(f"Writing COLMAP logs to {sfm_dir / 'colmap.LOG.*'}")
169+
pycolmap.logging.set_log_destination(pycolmap.logging.INFO, sfm_dir / "colmap.LOG.")
170+
127171
create_empty_db(database)
128172
import_images(image_dir, database, camera_mode, image_list, image_options)
129173
image_ids = get_image_ids(database)

hloc/triangulation.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import argparse
2-
import contextlib
3-
import io
4-
import sys
52
from pathlib import Path
63
from typing import Any, Dict, List, Optional
74

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

2320
def __enter__(self):
2421
if not self.verbose:
25-
self.capture = contextlib.redirect_stdout(io.StringIO())
26-
self.out = self.capture.__enter__()
22+
pycolmap.logging.alsologtostderr = False
2723

2824
def __exit__(self, exc_type, *args):
2925
if not self.verbose:
30-
self.capture.__exit__(exc_type, *args)
31-
if exc_type is not None:
32-
logger.error("Failed with output:\n%s", self.out.getvalue())
33-
sys.stdout.flush()
26+
pycolmap.logging.alsologtostderr = True
3427

3528

3629
def create_db_from_model(
@@ -114,12 +107,11 @@ def estimation_and_geometric_verification(
114107
):
115108
logger.info("Performing geometric verification of the matches...")
116109
with OutputCapture(verbose):
117-
with pycolmap.ostream():
118-
pycolmap.verify_matches(
119-
database_path,
120-
pairs_path,
121-
options=dict(ransac=dict(max_num_trials=20000, min_inlier_ratio=0.1)),
122-
)
110+
pycolmap.verify_matches(
111+
database_path,
112+
pairs_path,
113+
options=dict(ransac=dict(max_num_trials=20000, min_inlier_ratio=0.1)),
114+
)
123115

124116

125117
def geometric_verification(
@@ -170,7 +162,7 @@ def geometric_verification(
170162
db.add_two_view_geometry(id0, id1, matches)
171163
continue
172164

173-
cam1_from_cam0 = image1.cam_from_world * image0.cam_from_world.inverse()
165+
cam1_from_cam0 = image1.cam_from_world() * image0.cam_from_world().inverse()
174166
errors0, errors1 = compute_epipolar_errors(
175167
cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]]
176168
)
@@ -207,10 +199,9 @@ def run_triangulation(
207199
if options is None:
208200
options = {}
209201
with OutputCapture(verbose):
210-
with pycolmap.ostream():
211-
reconstruction = pycolmap.triangulate_points(
212-
reference_model, database_path, image_dir, model_path, options=options
213-
)
202+
reconstruction = pycolmap.triangulate_points(
203+
reference_model, database_path, image_dir, model_path, options=options
204+
)
214205
return reconstruction
215206

216207

hloc/utils/geometry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def to_homogeneous(p):
77

88

99
def compute_epipolar_errors(j_from_i: pycolmap.Rigid3d, p2d_i, p2d_j):
10-
j_E_i = j_from_i.essential_matrix()
10+
j_E_i = pycolmap.essential_matrix_from_pose(j_from_i)
1111
l2d_j = to_homogeneous(p2d_i) @ j_E_i.T
1212
l2d_i = to_homogeneous(p2d_j) @ j_E_i
1313
dist = np.abs(np.sum(to_homogeneous(p2d_i) * l2d_i, axis=1))

hloc/utils/io.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from pathlib import Path
2-
from typing import Tuple
2+
from typing import Mapping, Tuple
33

44
import cv2
55
import h5py
66
import numpy as np
7+
import pycolmap
78

89
from .parsers import names_to_pair, names_to_pair_old
910

@@ -76,3 +77,16 @@ def get_matches(path: Path, name0: str, name1: str) -> Tuple[np.ndarray]:
7677
matches = np.flip(matches, -1)
7778
scores = scores[idx]
7879
return matches, scores
80+
81+
82+
def write_poses(
83+
poses: Mapping[str, pycolmap.Rigid3d], path: str, prepend_camera_name: bool
84+
):
85+
with open(path, "w") as f:
86+
for query, t in poses.items():
87+
qvec = " ".join(map(str, t.rotation.quat[[3, 0, 1, 2]]))
88+
tvec = " ".join(map(str, t.translation))
89+
name = query.split("/")[-1]
90+
if prepend_camera_name:
91+
name = query.split("/")[-2] + "/" + name
92+
f.write(f"{name} {qvec} {tvec}\n")

hloc/utils/viz_3d.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def plot_camera(
116116
legendgroup=legendgroup,
117117
name=name,
118118
showlegend=False,
119-
hovertemplate=text.replace("\n", "<br>"),
119+
hovertemplate=text.replace("\n", "<br>") if text else None,
120120
)
121121
fig.add_trace(pyramid)
122122

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

141141

142142
def plot_camera_colmap(
143-
fig: go.Figure,
144-
image: pycolmap.Image,
145-
camera: pycolmap.Camera,
146-
name: Optional[str] = None,
147-
**kwargs
143+
fig: go.Figure, cam_from_world: pycolmap.Rigid3d, camera: pycolmap.Camera, **kwargs
148144
):
149145
"""Plot a camera frustum from PyCOLMAP objects"""
150-
world_t_camera = image.cam_from_world.inverse()
146+
world_t_camera = cam_from_world.inverse()
151147
plot_camera(
152148
fig,
153149
world_t_camera.rotation.matrix(),
154150
world_t_camera.translation,
155151
camera.calibration_matrix(),
152+
**kwargs
153+
)
154+
155+
156+
def plot_image_colmap(
157+
fig: go.Figure,
158+
image: pycolmap.Image,
159+
camera: pycolmap.Camera,
160+
name: Optional[str] = None,
161+
**kwargs
162+
):
163+
"""Plot a camera frustum from a PyCOLMAP image."""
164+
plot_camera_colmap(
165+
fig,
166+
image.cam_from_world(),
167+
camera,
156168
name=name or str(image.image_id),
157169
text=str(image),
158170
**kwargs
@@ -162,9 +174,7 @@ def plot_camera_colmap(
162174
def plot_cameras(fig: go.Figure, reconstruction: pycolmap.Reconstruction, **kwargs):
163175
"""Plot a camera as a cone with camera frustum."""
164176
for image_id, image in reconstruction.images.items():
165-
plot_camera_colmap(
166-
fig, image, reconstruction.cameras[image.camera_id], **kwargs
167-
)
177+
plot_image_colmap(fig, image, reconstruction.cameras[image.camera_id], **kwargs)
168178

169179

170180
def plot_reconstruction(
@@ -186,8 +196,7 @@ def plot_reconstruction(
186196
p3D
187197
for _, p3D in rec.points3D.items()
188198
if (
189-
(p3D.xyz >= bbs[0]).all()
190-
and (p3D.xyz <= bbs[1]).all()
199+
bbs.contains_point(p3D.xyz)
191200
and p3D.error <= max_reproj_error
192201
and p3D.track.length() >= min_track_length
193202
)

0 commit comments

Comments
 (0)