diff --git a/hloc/__init__.py b/hloc/__init__.py index a291882f..afd0d4bd 100644 --- a/hloc/__init__.py +++ b/hloc/__init__.py @@ -21,7 +21,7 @@ except ImportError: logger.warning("pycolmap is not installed, some features may not work.") else: - min_version = version.parse("3.12.3") + min_version = version.parse("3.12.6") found_version = pycolmap.__version__ if found_version != "dev": version = version.parse(found_version) diff --git a/hloc/reconstruction.py b/hloc/reconstruction.py index e2c74528..faf06769 100644 --- a/hloc/reconstruction.py +++ b/hloc/reconstruction.py @@ -15,7 +15,7 @@ import_matches, parse_option_args, ) -from .utils.database import COLMAPDatabase +from .utils.io import open_colmap_database def create_empty_db(database_path: Path): @@ -23,10 +23,8 @@ def create_empty_db(database_path: Path): 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() + with open_colmap_database(database_path) as _: + pass def import_images( @@ -53,11 +51,11 @@ def import_images( 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() + with open_colmap_database(database_path) as db: + images = { + image.name: image_id for image_id, image in db.read_all_images().items() + } return images @@ -171,17 +169,19 @@ def main( 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, - ) + with open_colmap_database(database) as db: + import_features(image_ids, db, features) + import_matches( + image_ids, + db, + pairs, + matches, + min_match_score, + skip_geometric_verification, + ) if not skip_geometric_verification: - estimation_and_geometric_verification(database, pairs, verbose) + with open_colmap_database(database) as db: + estimation_and_geometric_verification(db, pairs, verbose) reconstruction = run_reconstruction( sfm_dir, database, image_dir, verbose, mapper_options ) diff --git a/hloc/triangulation.py b/hloc/triangulation.py index 66e76091..f32ff9c1 100644 --- a/hloc/triangulation.py +++ b/hloc/triangulation.py @@ -7,9 +7,8 @@ from tqdm import tqdm from . import logger -from .utils.database import COLMAPDatabase from .utils.geometry import compute_epipolar_errors -from .utils.io import get_keypoints, get_matches +from .utils.io import get_keypoints, get_matches, open_colmap_database from .utils.parsers import parse_retrieval @@ -33,45 +32,31 @@ def create_db_from_model( logger.warning("The database already exists, deleting it.") database_path.unlink() - db = COLMAPDatabase.connect(database_path) - db.create_tables() - - for i, camera in reconstruction.cameras.items(): - db.add_camera( - camera.model.value, - camera.width, - camera.height, - camera.params, - camera_id=i, - prior_focal_length=True, - ) - - for i, image in reconstruction.images.items(): - db.add_image(image.name, image.camera_id, image_id=i) - - db.commit() - db.close() - return {image.name: i for i, image in reconstruction.images.items()} + with open_colmap_database(database_path) as db: + for camera_id, camera in reconstruction.cameras.items(): + db.write_camera(camera, use_camera_id=True) + for rig_id, rig in reconstruction.rigs.items(): + db.write_rig(rig, use_rig_id=True) + for frame_id, frame in reconstruction.frames.items(): + db.write_frame(frame, use_frame_id=True) + for image_id, image in reconstruction.images.items(): + db.write_image(image, use_image_id=True) + return {image.name: image_id for image_id, image in reconstruction.images.items()} def import_features( - image_ids: Dict[str, int], database_path: Path, features_path: Path + image_ids: Dict[str, int], db: pycolmap.Database, features_path: Path ): logger.info("Importing features into the database...") - db = COLMAPDatabase.connect(database_path) - for image_name, image_id in tqdm(image_ids.items()): keypoints = get_keypoints(features_path, image_name) keypoints += 0.5 # COLMAP origin - db.add_keypoints(image_id, keypoints) - - db.commit() - db.close() + db.write_keypoints(image_id, keypoints) def import_matches( image_ids: Dict[str, int], - database_path: Path, + db: pycolmap.Database, pairs_path: Path, matches_path: Path, min_match_score: Optional[float] = None, @@ -82,8 +67,6 @@ def import_matches( with open(str(pairs_path), "r") as f: pairs = [p.split() for p in f.readlines()] - db = COLMAPDatabase.connect(database_path) - matched = set() for name0, name1 in tqdm(pairs): id0, id1 = image_ids[name0], image_ids[name1] @@ -92,14 +75,13 @@ def import_matches( matches, scores = get_matches(matches_path, name0, name1) if min_match_score: matches = matches[scores > min_match_score] - db.add_matches(id0, id1, matches) + db.write_matches(id0, id1, matches) matched |= {(id0, id1), (id1, id0)} if skip_geometric_verification: - db.add_two_view_geometry(id0, id1, matches) - - db.commit() - db.close() + db.write_two_view_geometry( + id0, id1, pycolmap.TwoViewGeometry(inlier_matches=matches) + ) def estimation_and_geometric_verification( @@ -117,7 +99,7 @@ def estimation_and_geometric_verification( def geometric_verification( image_ids: Dict[str, int], reference: pycolmap.Reconstruction, - database_path: Path, + db: pycolmap.Database, features_path: Path, pairs_path: Path, matches_path: Path, @@ -126,7 +108,6 @@ def geometric_verification( logger.info("Performing geometric verification of the matches...") pairs = parse_retrieval(pairs_path) - db = COLMAPDatabase.connect(database_path) inlier_ratios = [] matched = set() @@ -159,7 +140,7 @@ def geometric_verification( matched |= {(id0, id1), (id1, id0)} if matches.shape[0] == 0: - db.add_two_view_geometry(id0, id1, matches) + db.write_two_view_geometry(id0, id1, pycolmap.TwoViewGeometry()) continue cam1_from_cam0 = image1.cam_from_world() * image0.cam_from_world().inverse() @@ -172,7 +153,11 @@ def geometric_verification( ) # TODO: We could also add E to the database, but we need # to reverse the transformations if id0 > id1 in utils/database.py. - db.add_two_view_geometry(id0, id1, matches[valid_matches, :]) + db.write_two_view_geometry( + id0, + id1, + pycolmap.TwoViewGeometry(inlier_matches=matches[valid_matches, :]), + ) inlier_ratios.append(np.mean(valid_matches)) logger.info( "mean/med/min/max valid matches %.2f/%.2f/%.2f/%.2f%%.", @@ -182,9 +167,6 @@ def geometric_verification( np.max(inlier_ratios) * 100, ) - db.commit() - db.close() - def run_triangulation( model_path: Path, @@ -228,18 +210,20 @@ def main( reference = pycolmap.Reconstruction(reference_model) image_ids = create_db_from_model(reference, database) - import_features(image_ids, database, features) - import_matches( - image_ids, - database, - pairs, - matches, - min_match_score, - skip_geometric_verification, - ) + with open_colmap_database(database) as db: + import_features(image_ids, db, features) + import_matches( + image_ids, + db, + pairs, + matches, + min_match_score, + skip_geometric_verification, + ) if not skip_geometric_verification: if estimate_two_view_geometries: - estimation_and_geometric_verification(database, pairs, verbose) + with open_colmap_database(database) as db: + estimation_and_geometric_verification(db, pairs, verbose) else: geometric_verification( image_ids, reference, database, features, pairs, matches diff --git a/hloc/utils/database.py b/hloc/utils/database.py deleted file mode 100644 index 6be9d51a..00000000 --- a/hloc/utils/database.py +++ /dev/null @@ -1,276 +0,0 @@ -# Copyright (c) 2018, ETH Zurich and UNC Chapel Hill. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of -# its contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de) - -# This script is based on an original implementation by True Price. - -import sqlite3 -import sys - -import numpy as np - -IS_PYTHON3 = sys.version_info[0] >= 3 - -MAX_IMAGE_ID = 2**31 - 1 - -CREATE_CAMERAS_TABLE = """CREATE TABLE IF NOT EXISTS cameras ( - camera_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - model INTEGER NOT NULL, - width INTEGER NOT NULL, - height INTEGER NOT NULL, - params BLOB, - prior_focal_length INTEGER NOT NULL)""" - -CREATE_DESCRIPTORS_TABLE = """CREATE TABLE IF NOT EXISTS descriptors ( - image_id INTEGER PRIMARY KEY NOT NULL, - rows INTEGER NOT NULL, - cols INTEGER NOT NULL, - data BLOB, - FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE)""" - -CREATE_IMAGES_TABLE = """CREATE TABLE IF NOT EXISTS images ( - image_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - name TEXT NOT NULL UNIQUE, - camera_id INTEGER NOT NULL, - prior_qw REAL, - prior_qx REAL, - prior_qy REAL, - prior_qz REAL, - prior_tx REAL, - prior_ty REAL, - prior_tz REAL, - CONSTRAINT image_id_check CHECK(image_id >= 0 and image_id < {}), - FOREIGN KEY(camera_id) REFERENCES cameras(camera_id)) -""".format( - MAX_IMAGE_ID -) - -CREATE_TWO_VIEW_GEOMETRIES_TABLE = """ -CREATE TABLE IF NOT EXISTS two_view_geometries ( - pair_id INTEGER PRIMARY KEY NOT NULL, - rows INTEGER NOT NULL, - cols INTEGER NOT NULL, - data BLOB, - config INTEGER NOT NULL, - F BLOB, - E BLOB, - H BLOB, - qvec BLOB, - tvec BLOB) -""" - -CREATE_KEYPOINTS_TABLE = """CREATE TABLE IF NOT EXISTS keypoints ( - image_id INTEGER PRIMARY KEY NOT NULL, - rows INTEGER NOT NULL, - cols INTEGER NOT NULL, - data BLOB, - FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE) -""" - -CREATE_MATCHES_TABLE = """CREATE TABLE IF NOT EXISTS matches ( - pair_id INTEGER PRIMARY KEY NOT NULL, - rows INTEGER NOT NULL, - cols INTEGER NOT NULL, - data BLOB)""" - -CREATE_NAME_INDEX = "CREATE UNIQUE INDEX IF NOT EXISTS index_name ON images(name)" - -CREATE_ALL = "; ".join( - [ - CREATE_CAMERAS_TABLE, - CREATE_IMAGES_TABLE, - CREATE_KEYPOINTS_TABLE, - CREATE_DESCRIPTORS_TABLE, - CREATE_MATCHES_TABLE, - CREATE_TWO_VIEW_GEOMETRIES_TABLE, - CREATE_NAME_INDEX, - ] -) - - -def image_ids_to_pair_id(image_id1, image_id2): - if image_id1 > image_id2: - image_id1, image_id2 = image_id2, image_id1 - return image_id1 * MAX_IMAGE_ID + image_id2 - - -def pair_id_to_image_ids(pair_id): - image_id2 = pair_id % MAX_IMAGE_ID - image_id1 = (pair_id - image_id2) / MAX_IMAGE_ID - return image_id1, image_id2 - - -def array_to_blob(array): - if IS_PYTHON3: - return array.tobytes() - else: - return np.getbuffer(array) - - -def blob_to_array(blob, dtype, shape=(-1,)): - if IS_PYTHON3: - return np.fromstring(blob, dtype=dtype).reshape(*shape) - else: - return np.frombuffer(blob, dtype=dtype).reshape(*shape) - - -class COLMAPDatabase(sqlite3.Connection): - @staticmethod - def connect(database_path): - return sqlite3.connect(str(database_path), factory=COLMAPDatabase) - - def __init__(self, *args, **kwargs): - super(COLMAPDatabase, self).__init__(*args, **kwargs) - - self.create_tables = lambda: self.executescript(CREATE_ALL) - self.create_cameras_table = lambda: self.executescript(CREATE_CAMERAS_TABLE) - self.create_descriptors_table = lambda: self.executescript( - CREATE_DESCRIPTORS_TABLE - ) - self.create_images_table = lambda: self.executescript(CREATE_IMAGES_TABLE) - self.create_two_view_geometries_table = lambda: self.executescript( - CREATE_TWO_VIEW_GEOMETRIES_TABLE - ) - self.create_keypoints_table = lambda: self.executescript(CREATE_KEYPOINTS_TABLE) - self.create_matches_table = lambda: self.executescript(CREATE_MATCHES_TABLE) - self.create_name_index = lambda: self.executescript(CREATE_NAME_INDEX) - - def add_camera( - self, model, width, height, params, prior_focal_length=False, camera_id=None - ): - params = np.asarray(params, np.float64) - cursor = self.execute( - "INSERT INTO cameras VALUES (?, ?, ?, ?, ?, ?)", - ( - camera_id, - model, - width, - height, - array_to_blob(params), - prior_focal_length, - ), - ) - return cursor.lastrowid - - def add_image( - self, - name, - camera_id, - prior_q=np.full(4, np.nan), - prior_t=np.full(3, np.nan), - image_id=None, - ): - cursor = self.execute( - "INSERT INTO images VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - ( - image_id, - name, - camera_id, - prior_q[0], - prior_q[1], - prior_q[2], - prior_q[3], - prior_t[0], - prior_t[1], - prior_t[2], - ), - ) - return cursor.lastrowid - - def add_keypoints(self, image_id, keypoints): - assert len(keypoints.shape) == 2 - assert keypoints.shape[1] in [2, 4, 6] - - keypoints = np.asarray(keypoints, np.float32) - self.execute( - "INSERT INTO keypoints VALUES (?, ?, ?, ?)", - (image_id,) + keypoints.shape + (array_to_blob(keypoints),), - ) - - def add_descriptors(self, image_id, descriptors): - descriptors = np.ascontiguousarray(descriptors, np.uint8) - self.execute( - "INSERT INTO descriptors VALUES (?, ?, ?, ?)", - (image_id,) + descriptors.shape + (array_to_blob(descriptors),), - ) - - def add_matches(self, image_id1, image_id2, matches): - assert len(matches.shape) == 2 - assert matches.shape[1] == 2 - - if image_id1 > image_id2: - matches = matches[:, ::-1] - - pair_id = image_ids_to_pair_id(image_id1, image_id2) - matches = np.asarray(matches, np.uint32) - self.execute( - "INSERT INTO matches VALUES (?, ?, ?, ?)", - (pair_id,) + matches.shape + (array_to_blob(matches),), - ) - - def add_two_view_geometry( - self, - image_id1, - image_id2, - matches, - F=np.eye(3), - E=np.eye(3), - H=np.eye(3), - qvec=np.array([1.0, 0.0, 0.0, 0.0]), - tvec=np.zeros(3), - config=2, - ): - assert len(matches.shape) == 2 - assert matches.shape[1] == 2 - - if image_id1 > image_id2: - matches = matches[:, ::-1] - - pair_id = image_ids_to_pair_id(image_id1, image_id2) - matches = np.asarray(matches, np.uint32) - F = np.asarray(F, dtype=np.float64) - E = np.asarray(E, dtype=np.float64) - H = np.asarray(H, dtype=np.float64) - qvec = np.asarray(qvec, dtype=np.float64) - tvec = np.asarray(tvec, dtype=np.float64) - self.execute( - "INSERT INTO two_view_geometries VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - (pair_id,) - + matches.shape - + ( - array_to_blob(matches), - config, - array_to_blob(F), - array_to_blob(E), - array_to_blob(H), - array_to_blob(qvec), - array_to_blob(tvec), - ), - ) diff --git a/hloc/utils/io.py b/hloc/utils/io.py index 80e2a909..1423960d 100644 --- a/hloc/utils/io.py +++ b/hloc/utils/io.py @@ -1,5 +1,6 @@ +import contextlib from pathlib import Path -from typing import Mapping, Tuple +from typing import ContextManager, Mapping, Tuple import cv2 import h5py @@ -90,3 +91,17 @@ def write_poses( if prepend_camera_name: name = query.split("/")[-2] + "/" + name f.write(f"{name} {qvec} {tvec}\n") + + +@contextlib.contextmanager +def open_colmap_database(database_path: Path) -> ContextManager[pycolmap.Database]: + # In preparation for the context support in the future pycolmap >= 3.13 release + if isinstance(pycolmap.Database.__dict__.get("open"), (staticmethod, classmethod)): + with pycolmap.Database.open(database_path) as db: + yield db + else: + db = pycolmap.Database(database_path) + try: + yield db + finally: + db.close() diff --git a/requirements.txt b/requirements.txt index 3af88f68..27d828dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ matplotlib plotly scipy h5py -pycolmap>=3.12.3 +pycolmap>=3.12.6 kornia>=0.6.11 gdown lightglue @ git+https://github.com/cvg/LightGlue