image-matching-webui / hloc /reconstruction.py
Realcat
update: sfm
57c1094
raw
history blame
6.12 kB
import argparse
import multiprocessing
import shutil
from pathlib import Path
from typing import Any, Dict, List, Optional
import pycolmap
from . import logger
from .triangulation import (
OutputCapture,
estimation_and_geometric_verification,
import_features,
import_matches,
parse_option_args,
)
from .utils.database import COLMAPDatabase
def create_empty_db(database_path: Path):
if database_path.exists():
logger.warning("The database already exists, deleting it.")
database_path.unlink()
logger.info("Creating an empty database...")
db = COLMAPDatabase.connect(database_path)
db.create_tables()
db.commit()
db.close()
def import_images(
image_dir: Path,
database_path: Path,
camera_mode: pycolmap.CameraMode,
image_list: Optional[List[str]] = None,
options: Optional[Dict[str, Any]] = None,
):
logger.info("Importing images into the database...")
if options is None:
options = {}
images = list(image_dir.iterdir())
if len(images) == 0:
raise IOError(f"No images found in {image_dir}.")
with pycolmap.ostream():
pycolmap.import_images(
database_path,
image_dir,
camera_mode,
image_list=image_list or [],
options=options,
)
def get_image_ids(database_path: Path) -> Dict[str, int]:
db = COLMAPDatabase.connect(database_path)
images = {}
for name, image_id in db.execute("SELECT name, image_id FROM images;"):
images[name] = image_id
db.close()
return images
def run_reconstruction(
sfm_dir: Path,
database_path: Path,
image_dir: Path,
verbose: bool = False,
options: Optional[Dict[str, Any]] = None,
) -> pycolmap.Reconstruction:
models_path = sfm_dir / "models"
models_path.mkdir(exist_ok=True, parents=True)
logger.info("Running 3D 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
)
if len(reconstructions) == 0:
logger.error("Could not reconstruct any model!")
return None
logger.info(f"Reconstructed {len(reconstructions)} model(s).")
largest_index = None
largest_num_images = 0
for index, rec in reconstructions.items():
num_images = rec.num_reg_images()
if num_images > largest_num_images:
largest_index = index
largest_num_images = num_images
assert largest_index is not None
logger.info(
f"Largest model is #{largest_index} "
f"with {largest_num_images} images."
)
for filename in ["images.bin", "cameras.bin", "points3D.bin"]:
if (sfm_dir / filename).exists():
(sfm_dir / filename).unlink()
shutil.move(
str(models_path / str(largest_index) / filename), str(sfm_dir)
)
return reconstructions[largest_index]
def main(
sfm_dir: Path,
image_dir: Path,
pairs: Path,
features: Path,
matches: Path,
camera_mode: pycolmap.CameraMode = pycolmap.CameraMode.AUTO,
verbose: bool = False,
skip_geometric_verification: bool = False,
min_match_score: Optional[float] = None,
image_list: Optional[List[str]] = None,
image_options: Optional[Dict[str, Any]] = None,
mapper_options: Optional[Dict[str, Any]] = None,
) -> pycolmap.Reconstruction:
assert features.exists(), features
assert pairs.exists(), pairs
assert matches.exists(), matches
sfm_dir.mkdir(parents=True, exist_ok=True)
database = sfm_dir / "database.db"
create_empty_db(database)
import_images(image_dir, database, camera_mode, image_list, image_options)
image_ids = get_image_ids(database)
import_features(image_ids, database, features)
import_matches(
image_ids,
database,
pairs,
matches,
min_match_score,
skip_geometric_verification,
)
if not skip_geometric_verification:
estimation_and_geometric_verification(database, pairs, verbose)
reconstruction = run_reconstruction(
sfm_dir, database, image_dir, verbose, mapper_options
)
if reconstruction is not None:
logger.info(
f"Reconstruction statistics:\n{reconstruction.summary()}"
+ f"\n\tnum_input_images = {len(image_ids)}"
)
return reconstruction
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--sfm_dir", type=Path, required=True)
parser.add_argument("--image_dir", type=Path, required=True)
parser.add_argument("--pairs", type=Path, required=True)
parser.add_argument("--features", type=Path, required=True)
parser.add_argument("--matches", type=Path, required=True)
parser.add_argument(
"--camera_mode",
type=str,
default="AUTO",
choices=list(pycolmap.CameraMode.__members__.keys()),
)
parser.add_argument("--skip_geometric_verification", action="store_true")
parser.add_argument("--min_match_score", type=float)
parser.add_argument("--verbose", action="store_true")
parser.add_argument(
"--image_options",
nargs="+",
default=[],
help="List of key=value from {}".format(
pycolmap.ImageReaderOptions().todict()
),
)
parser.add_argument(
"--mapper_options",
nargs="+",
default=[],
help="List of key=value from {}".format(
pycolmap.IncrementalMapperOptions().todict()
),
)
args = parser.parse_args().__dict__
image_options = parse_option_args(
args.pop("image_options"), pycolmap.ImageReaderOptions()
)
mapper_options = parse_option_args(
args.pop("mapper_options"), pycolmap.IncrementalMapperOptions()
)
main(**args, image_options=image_options, mapper_options=mapper_options)