Skip to content

Commit

Permalink
Bind substeps of incremental mapper with Python example (colmap#2478)
Browse files Browse the repository at this point in the history
  • Loading branch information
B1ueber2y authored Mar 25, 2024
1 parent a36156a commit 0ea2d5c
Show file tree
Hide file tree
Showing 13 changed files with 687 additions and 38 deletions.
313 changes: 313 additions & 0 deletions pycolmap/custom_incremental_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
"""
Python reimplementation of the C++ incremental mapper with equivalent logic.
"""

import shutil
import time
import urllib.request
import zipfile
from pathlib import Path

import enlighten

import pycolmap
from pycolmap import logging


def extract_colors(image_path, image_id, reconstruction):
if not reconstruction.extract_colors_for_image(image_id, image_path):
logging.warning(f"Could not read image {image_id} at path {image_path}")


def write_snapshot(reconstruction, snapshot_path):
logging.info("Creating snapshot")
timestamp = time.time() * 1000
path = snapshot_path / f"{timestamp:010d}"
path.mkdir(exist_ok=True, parents=True)
logging.verbose(f"=> Writing to {path}")
reconstruction.write(path)


def iterative_global_refinement(options, mapper_options, mapper):
logging.info("Retriangulation and Global bundle adjustment")
mapper.iterative_global_refinement(
options.ba_global_max_refinements,
options.ba_global_max_refinement_change,
mapper_options,
options.get_global_bundle_adjustment(),
options.get_triangulation(),
)
mapper.filter_images(mapper_options)


def initialize_reconstruction(
controller, mapper, mapper_options, reconstruction
):
"""Equivalent to IncrementalMapperController.initialize_reconstruction(...)"""
options = controller.options
init_pair = (options.init_image_id1, options.init_image_id2)

# Try to find good initial pair
if not options.is_initial_pair_provided():
logging.info("Finding good initial image pair")
ret = mapper.find_initial_image_pair(mapper_options, *init_pair)
if ret is None:
logging.info("No good initial image pair found.")
return pycolmap.IncrementalMapperStatus.NO_INITIAL_PAIR
init_pair, two_view_geometry = ret
else:
if not all(reconstruction.exists_image(i) for i in init_pair):
logging.info(f"=> Initial image pair {init_pair} does not exist.")
return pycolmap.IncrementalMapperStatus.BAD_INITIAL_PAIR
two_view_geometry = mapper.estimate_initial_two_view_geometry(
mapper_options, *init_pair
)
if two_view_geometry is None:
logging.info("Provided pair is insuitable for initialization")
return pycolmap.IncrementalMapperStatus.BAD_INITIAL_PAIR
logging.info(f"Initializing with image pair {init_pair}")
mapper.register_initial_image_pair(
mapper_options, two_view_geometry, *init_pair
)
logging.info("Global bundle adjustment")
mapper.adjust_global_bundle(
mapper_options, options.get_global_bundle_adjustment()
)
reconstruction.normalize()
mapper.filter_points(mapper_options)
mapper.filter_images(mapper_options)

# Initial image pair failed to register
if (
reconstruction.num_reg_images() == 0
or reconstruction.num_points3D() == 0
):
return pycolmap.IncrementalMapperStatus.BAD_INITIAL_PAIR
if options.extract_colors:
extract_colors(controller.image_path, init_pair[0], reconstruction)
return pycolmap.IncrementalMapperStatus.SUCCESS


def reconstruct_sub_model(controller, mapper, mapper_options, reconstruction):
"""Equivalent to IncrementalMapperController.reconstruct_sub_model(...)"""
# register initial pair
mapper.begin_reconstruction(reconstruction)
if reconstruction.num_reg_images() == 0:
init_status = initialize_reconstruction(
controller, mapper, mapper_options, reconstruction
)
if init_status != pycolmap.IncrementalMapperStatus.SUCCESS:
return init_status
controller.callback(
pycolmap.IncrementalMapperCallback.INITIAL_IMAGE_PAIR_REG_CALLBACK
)

# incremental mapping
options = controller.options
snapshot_prev_num_reg_images = reconstruction.num_reg_images()
ba_prev_num_reg_images = reconstruction.num_reg_images()
ba_prev_num_points = reconstruction.num_points3D()
reg_next_success, prev_reg_next_success = True, True
while True:
if not (reg_next_success or prev_reg_next_success):
break
prev_reg_next_success = reg_next_success
reg_next_success = False
next_images = mapper.find_next_images(mapper_options)
if len(next_images) == 0:
break
for reg_trial in range(len(next_images)):
next_image_id = next_images[reg_trial]
next_image = reconstruction.images[next_image_id]
logging.info(
f"Registering image #{next_image_id} "
f"({reconstruction.num_reg_images() + 1})"
)
logging.info(
f"=> Image sees {next_image.num_visible_points3D()} "
f"/ {next_image.num_observations} points"
)
reg_next_success = mapper.register_next_image(
mapper_options, next_image_id
)
if reg_next_success:
break
else:
logging.info("=> Could not register, trying another image.")
# If initial pair fails to continue for some time,
# abort and try different initial pair.
kMinNumInitialRegTrials = 30
if (
reg_trial >= kMinNumInitialRegTrials
and reconstruction.num_reg_images() < options.min_model_size
):
break
if reg_next_success:
mapper.triangulate_image(options.get_triangulation(), next_image_id)
mapper.iterative_local_refinement(
options.ba_local_max_refinements,
options.ba_local_max_refinement_change,
mapper_options,
options.get_local_bundle_adjustment(),
options.get_triangulation(),
next_image_id,
)
if controller.check_run_global_refinement(
reconstruction, ba_prev_num_reg_images, ba_prev_num_points
):
iterative_global_refinement(options, mapper_options, mapper)
ba_prev_num_points = reconstruction.num_points3D()
ba_prev_num_reg_images = reconstruction.num_reg_images()
if options.extract_colors:
extract_colors(
controller.image_path, next_image_id, reconstruction
)
if (
options.snapshot_images_freq > 0
and reconstruction.num_reg_images()
>= options.snapshot_images_freq + snapshot_prev_num_reg_images
):
snapshot_prev_num_reg_images = reconstruction.num_reg_images()
write_snapshot(reconstruction, Path(options.snapshot_path))
controller.callback(
pycolmap.IncrementalMapperCallback.NEXT_IMAGE_REG_CALLBACK
)
if mapper.num_shared_reg_images() >= int(options.max_model_overlap):
break
if (not reg_next_success) and prev_reg_next_success:
iterative_global_refinement(options, mapper_options, mapper)
if (
reconstruction.num_reg_images() >= 2
and reconstruction.num_reg_images() != ba_prev_num_reg_images
and reconstruction.num_points3D != ba_prev_num_points
):
iterative_global_refinement(options, mapper_options, mapper)
return pycolmap.IncrementalMapperStatus.SUCCESS


def reconstruct(controller, mapper_options):
"""Equivalent to IncrementalMapperController.reconstruct(...)"""
options = controller.options
reconstruction_manager = controller.reconstruction_manager
database_cache = controller.database_cache
mapper = pycolmap.IncrementalMapper(database_cache)
initial_reconstruction_given = reconstruction_manager.size() > 0
if reconstruction_manager.size() > 1:
logging.fatal(
"Can only resume from a single reconstruction, but multiple are given"
)
for num_trials in range(options.init_num_trials):
if (not initial_reconstruction_given) or num_trials > 0:
reconstruction_idx = reconstruction_manager.add()
else:
reconstruction_idx = 0
reconstruction = reconstruction_manager.get(reconstruction_idx)
status = reconstruct_sub_model(
controller, mapper, mapper_options, reconstruction
)
if status == pycolmap.IncrementalMapperStatus.INTERRUPTED:
mapper.end_reconstruction(False)
elif status in (
pycolmap.IncrementalMapperStatus.NO_INITIAL_PAIR,
pycolmap.IncrementalMapperStatus.BAD_INITIAL_PAIR,
):
mapper.end_reconstruction(True)
reconstruction_manager.delete(reconstruction_idx)
if options.is_initial_pair_provided():
return
elif status == pycolmap.IncrementalMapperStatus.SUCCESS:
total_num_reg_images = mapper.num_total_reg_images()
min_model_size = min(
0.8 * database_cache.num_images(), options.min_model_size
)
if (
options.multiple_models
and reconstruction_manager.size() > 1
and (
reconstruction.num_reg_images() < min_model_size
or reconstruction.num_reg_images() == 0
)
):
mapper.end_reconstruction(True)
reconstruction_manager.delete(reconstruction_idx)
else:
mapper.end_reconstruction(False)
controller.callback(
pycolmap.IncrementalMapperCallback.LAST_IMAGE_REG_CALLBACK
)
if (
initial_reconstruction_given
or options.multiple_models
or reconstruction_manager.size() >= options.max_num_models
or total_num_reg_images >= database_cache.num_images() - 1
):
return
else:
logging.fatal(f"Unknown reconstruction status: {status}")


def main_incremental_mapper(controller):
"""Equivalent to IncrementalMapperController.run()"""
timer = pycolmap.Timer()
timer.start()
if not controller.load_database():
return
init_mapper_options = controller.options.get_mapper()
reconstruct(controller, init_mapper_options)

for i in range(2): # number of relaxations
if controller.reconstruction_manager.size() > 0:
break
logging.info("=> Relaxing the initialization constraints")
init_mapper_options.init_min_num_inliers /= 2
reconstruct(controller, init_mapper_options)
if controller.reconstruction_manager.size() > 0:
break
logging.info("=> Relaxing the initialization constraints")
init_mapper_options.init_min_tri_angle /= 2
reconstruct(controller, init_mapper_options)
timer.print_minutes()


def main(
database_path,
image_path,
output_path,
options=pycolmap.IncrementalPipelineOptions(),
input_path=None,
):
if not database_path.exists():
logging.fatal(f"Database path does not exist: {database_path}")
if not image_path.exists():
logging.fatal(f"Image path does not exist: {image_path}")
output_path.mkdir(exist_ok=True, parents=True)
reconstruction_manager = pycolmap.ReconstructionManager()
if input_path is not None and input_path != "":
reconstruction_manager.read(input_path)
mapper = pycolmap.IncrementalMapperController(
options, image_path, database_path, reconstruction_manager
)

# main runner
num_images = pycolmap.Database(database_path).num_images
with enlighten.Manager() as manager:
with manager.counter(
total=num_images, desc="Images registered:"
) as pbar:
pbar.update(0, force=True)
mapper.add_callback(
pycolmap.IncrementalMapperCallback.INITIAL_IMAGE_PAIR_REG_CALLBACK,
lambda: pbar.update(2),
)
mapper.add_callback(
pycolmap.IncrementalMapperCallback.NEXT_IMAGE_REG_CALLBACK,
lambda: pbar.update(1),
)
main_incremental_mapper(mapper)

# write and output
reconstruction_manager.write(output_path)
reconstructions = {}
for i in range(reconstruction_manager.size()):
reconstructions[i] = reconstruction_manager.get(i)
return reconstructions
43 changes: 27 additions & 16 deletions pycolmap/example.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
An example for running incremental SfM on a set of images with pycolmap interfaces.
"""

import shutil
import urllib.request
import zipfile
Expand All @@ -9,16 +13,32 @@
from pycolmap import logging


def incremental_mapping_with_pbar(database_path, image_path, sfm_path):
num_images = pycolmap.Database(database_path).num_images
with enlighten.Manager() as manager:
with manager.counter(
total=num_images, desc="Images registered:"
) as pbar:
pbar.update(0, force=True)
reconstructions = pycolmap.incremental_mapping(
database_path,
image_path,
sfm_path,
initial_image_pair_callback=lambda: pbar.update(2),
next_image_callback=lambda: pbar.update(1),
)
return reconstructions


def run():
output_path = Path("example/")
image_path = output_path / "Fountain/images"
database_path = output_path / "database.db"
sfm_path = output_path / "sfm"

output_path.mkdir(exist_ok=True)
logging.set_log_destination(
logging.INFO, output_path / "INFO.log."
) # + time
# The log filename is postfixed with the execution timestamp.
logging.set_log_destination(logging.INFO, output_path / "INFO.log.")

data_url = "https://cvg-data.inf.ethz.ch/local-feature-evaluation-schoenberger2017/Strecha-Fountain.zip"
if not image_path.exists():
Expand All @@ -34,24 +54,15 @@ def run():
pycolmap.set_random_seed(0)
pycolmap.extract_features(database_path, image_path)
pycolmap.match_exhaustive(database_path)
num_images = pycolmap.Database(database_path).num_images

if sfm_path.exists():
shutil.rmtree(sfm_path)
sfm_path.mkdir(exist_ok=True)

with enlighten.Manager() as manager:
with manager.counter(
total=num_images, desc="Images registered:"
) as pbar:
pbar.update(0, force=True)
recs = pycolmap.incremental_mapping(
database_path,
image_path,
sfm_path,
initial_image_pair_callback=lambda: pbar.update(2),
next_image_callback=lambda: pbar.update(1),
)
recs = incremental_mapping_with_pbar(database_path, image_path, sfm_path)
# alternatively, use:
# import custom_incremental_mapping
# recs = custom_incremental_mapping.main(database_path, image_path, sfm_path)
for idx, rec in recs.items():
logging.info(f"#{idx} {rec.summary()}")

Expand Down
2 changes: 1 addition & 1 deletion src/colmap/controllers/incremental_mapper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ IncrementalMapperController::IncrementalMapperController(
std::shared_ptr<const IncrementalMapperOptions> options,
const std::string& image_path,
const std::string& database_path,
std::shared_ptr<ReconstructionManager> reconstruction_manager)
std::shared_ptr<class ReconstructionManager> reconstruction_manager)
: options_(std::move(options)),
image_path_(image_path),
database_path_(database_path),
Expand Down
Loading

0 comments on commit 0ea2d5c

Please sign in to comment.