Skip to content

Commit 55e6cde

Browse files
committed
Use new pycolmap reconstruction and pipeline API
1 parent 9bad6b4 commit 55e6cde

File tree

9 files changed

+127
-243
lines changed

9 files changed

+127
-243
lines changed

hloc/pipelines/7Scenes/pipeline.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ def run_scene(images, gt_dir, retrieval, outputs, results, num_covis,
5353
images,
5454
sfm_pairs,
5555
features,
56-
sfm_matches,
57-
colmap_path='colmap')
56+
sfm_matches)
5857
if use_dense_depth:
5958
assert depth_dir is not None
6059
ref_sfm_fix = outputs / 'sfm_superpoint+superglue+depth'

hloc/pipelines/Aachen/pipeline.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@
5656
images,
5757
sfm_pairs,
5858
features,
59-
sfm_matches,
60-
colmap_path='colmap')
59+
sfm_matches)
6160

6261
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
6362
pairs_from_retrieval.main(

hloc/pipelines/Aachen_v1_1/pipeline.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@
5050
images,
5151
sfm_pairs,
5252
features,
53-
sfm_matches,
54-
colmap_path='colmap')
53+
sfm_matches)
5554

5655
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
5756
pairs_from_retrieval.main(

hloc/pipelines/CMU/pipeline.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ def run_slice(slice_, root, outputs, num_covis, num_loc):
5656
ref_images,
5757
sfm_pairs,
5858
features,
59-
sfm_matches,
60-
colmap_path='colmap')
59+
sfm_matches)
6160

6261
generate_query_list(root, query_list, slice_)
6362
global_descriptors = extract_features.main(

hloc/pipelines/Cambridge/pipeline.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ def run_scene(images, gt_dir, outputs, results, num_covis, num_loc):
6161
images,
6262
sfm_pairs,
6363
features,
64-
sfm_matches,
65-
colmap_path='colmap')
64+
sfm_matches)
6665

6766
loc_matches = match_features.main(
6867
matcher_conf, loc_pairs, feature_conf['output'], outputs)

hloc/pipelines/RobotCar/pipeline.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ def generate_query_list(dataset, image_dir, path):
8484
images,
8585
sfm_pairs,
8686
features,
87-
sfm_matches,
88-
colmap_path='colmap')
87+
sfm_matches)
8988

9089
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
9190
# TODO: do per location and per camera

hloc/reconstruction.py

Lines changed: 41 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import argparse
22
import logging
3-
from pathlib import Path
43
import shutil
54
import multiprocessing
6-
import subprocess
7-
import pprint
5+
from pathlib import Path
6+
import pycolmap
87

9-
from .utils.read_write_model import read_cameras_binary
108
from .utils.database import COLMAPDatabase
119
from .triangulation import (
12-
import_features, import_matches, geometric_verification, run_command)
10+
import_features, import_matches, geometric_verification, OutputCapture)
1311

1412

1513
def create_empty_db(database_path):
@@ -23,35 +21,13 @@ def create_empty_db(database_path):
2321
db.close()
2422

2523

26-
def import_images(colmap_path, sfm_dir, image_dir, database_path,
27-
single_camera=False, verbose=False):
24+
def import_images(image_dir, database_path, camera_mode):
2825
logging.info('Importing images into the database...')
2926
images = list(image_dir.iterdir())
3027
if len(images) == 0:
3128
raise IOError(f'No images found in {image_dir}.')
32-
33-
# We need to create dummy features for COLMAP to import images with EXIF
34-
dummy_dir = sfm_dir / 'dummy_features'
35-
dummy_dir.mkdir()
36-
for i in images:
37-
with open(str(dummy_dir / (i.name + '.txt')), 'w') as f:
38-
f.write('0 128')
39-
40-
cmd = [
41-
str(colmap_path), 'feature_importer',
42-
'--database_path', str(database_path),
43-
'--image_path', str(image_dir),
44-
'--import_path', str(dummy_dir),
45-
'--ImageReader.single_camera',
46-
str(int(single_camera))]
47-
run_command(cmd, verbose)
48-
49-
db = COLMAPDatabase.connect(database_path)
50-
db.execute("DELETE FROM keypoints;")
51-
db.execute("DELETE FROM descriptors;")
52-
db.commit()
53-
db.close()
54-
shutil.rmtree(str(dummy_dir))
29+
with pycolmap.ostream():
30+
pycolmap.import_images(database_path, image_dir, camera_mode)
5531

5632

5733
def get_image_ids(database_path):
@@ -63,67 +39,41 @@ def get_image_ids(database_path):
6339
return images
6440

6541

66-
def run_reconstruction(colmap_path, sfm_dir, database_path, image_dir,
67-
min_num_matches=None, verbose=False):
42+
def run_reconstruction(sfm_dir, database_path, image_dir, verbose=False):
6843
models_path = sfm_dir / 'models'
6944
models_path.mkdir(exist_ok=True, parents=True)
70-
cmd = [
71-
str(colmap_path), 'mapper',
72-
'--database_path', str(database_path),
73-
'--image_path', str(image_dir),
74-
'--output_path', str(models_path),
75-
'--Mapper.num_threads', str(min(multiprocessing.cpu_count(), 16))]
76-
if min_num_matches:
77-
cmd += ['--Mapper.min_num_matches', str(min_num_matches)]
78-
logging.info('Running the reconstruction with command:\n%s', ' '.join(cmd))
79-
run_command(cmd, verbose)
80-
81-
models = list(models_path.iterdir())
82-
if len(models) == 0:
45+
logging.info('Running 3D reconstruction...')
46+
with OutputCapture(verbose):
47+
with pycolmap.ostream():
48+
reconstructions = pycolmap.incremental_mapping(
49+
database_path, image_dir, models_path,
50+
num_threads=min(multiprocessing.cpu_count(), 16))
51+
52+
if len(reconstructions) == 0:
8353
logging.error('Could not reconstruct any model!')
8454
return None
85-
logging.info(f'Reconstructed {len(models)} models.')
86-
87-
largest_model = None
88-
largest_model_num_images = 0
89-
for model in models:
90-
num_images = len(read_cameras_binary(str(model / 'cameras.bin')))
91-
if num_images > largest_model_num_images:
92-
largest_model = model
93-
largest_model_num_images = num_images
94-
assert largest_model_num_images > 0
95-
logging.info(f'Largest model is #{largest_model.name} '
96-
f'with {largest_model_num_images} images.')
97-
98-
stats_raw = subprocess.check_output(
99-
[str(colmap_path), 'model_analyzer',
100-
'--path', str(largest_model)])
101-
stats_raw = stats_raw.decode().split("\n")
102-
stats = dict()
103-
for stat in stats_raw:
104-
if stat.startswith("Registered images"):
105-
stats['num_reg_images'] = int(stat.split()[-1])
106-
elif stat.startswith("Points"):
107-
stats['num_sparse_points'] = int(stat.split()[-1])
108-
elif stat.startswith("Observations"):
109-
stats['num_observations'] = int(stat.split()[-1])
110-
elif stat.startswith("Mean track length"):
111-
stats['mean_track_length'] = float(stat.split()[-1])
112-
elif stat.startswith("Mean observations per image"):
113-
stats['num_observations_per_image'] = float(stat.split()[-1])
114-
elif stat.startswith("Mean reprojection error"):
115-
stats['mean_reproj_error'] = float(stat.split()[-1][:-2])
55+
logging.info(f'Reconstructed {len(reconstructions)} model(s).')
56+
57+
largest_index = None
58+
largest_num_images = 0
59+
for index, rec in reconstructions.items():
60+
num_images = rec.num_reg_images()
61+
if num_images > largest_num_images:
62+
largest_index = index
63+
largest_num_images = num_images
64+
assert largest_index is not None
65+
logging.info(f'Largest model is #{largest_index} '
66+
f'with {largest_num_images} images.')
11667

11768
for filename in ['images.bin', 'cameras.bin', 'points3D.bin']:
118-
shutil.move(str(largest_model / filename), str(sfm_dir))
119-
120-
return stats
69+
shutil.move(
70+
str(models_path / str(largest_index) / filename), str(sfm_dir))
71+
return reconstructions[largest_index]
12172

12273

12374
def main(sfm_dir, image_dir, pairs, features, matches,
124-
colmap_path='colmap', single_camera=False,
125-
skip_geometric_verification=False,
126-
min_match_score=None, min_num_matches=None, verbose=False):
75+
camera_mode=pycolmap.CameraMode.AUTO, verbose=False,
76+
skip_geometric_verification=False, min_match_score=None):
12777

12878
assert features.exists(), features
12979
assert pairs.exists(), pairs
@@ -133,19 +83,18 @@ def main(sfm_dir, image_dir, pairs, features, matches,
13383
database = sfm_dir / 'database.db'
13484

13585
create_empty_db(database)
136-
import_images(
137-
colmap_path, sfm_dir, image_dir, database, single_camera, verbose)
86+
import_images(image_dir, database, camera_mode)
13887
image_ids = get_image_ids(database)
13988
import_features(image_ids, database, features)
14089
import_matches(image_ids, database, pairs, matches,
14190
min_match_score, skip_geometric_verification)
14291
if not skip_geometric_verification:
143-
geometric_verification(colmap_path, database, pairs, verbose)
144-
stats = run_reconstruction(
145-
colmap_path, sfm_dir, database, image_dir, min_num_matches, verbose)
146-
if stats is not None:
147-
stats['num_input_images'] = len(image_ids)
148-
logging.info('Reconstruction statistics:\n%s', pprint.pformat(stats))
92+
geometric_verification(database, pairs, verbose)
93+
reconstruction = run_reconstruction(sfm_dir, database, image_dir, verbose)
94+
if reconstruction is not None:
95+
logging.info(f'Reconstruction statistics:\n{reconstruction.summary()}'
96+
+ f'\n\tnum_input_images = {len(image_ids)}')
97+
return reconstruction
14998

15099

151100
if __name__ == '__main__':
@@ -157,12 +106,10 @@ def main(sfm_dir, image_dir, pairs, features, matches,
157106
parser.add_argument('--features', type=Path, required=True)
158107
parser.add_argument('--matches', type=Path, required=True)
159108

160-
parser.add_argument('--colmap_path', type=Path, default='colmap')
161-
162-
parser.add_argument('--single_camera', action='store_true')
109+
parser.add_argument('--camera_mode', type=str, default="AUTO",
110+
choices=list(pycolmap.CameraMode.__members__.keys()))
163111
parser.add_argument('--skip_geometric_verification', action='store_true')
164112
parser.add_argument('--min_match_score', type=float)
165-
parser.add_argument('--min_num_matches', type=int)
166113
parser.add_argument('--verbose', action='store_true')
167114
args = parser.parse_args()
168115

0 commit comments

Comments
 (0)