From da19f5f1e8797cc5dd61c784bef1585b2172f0c6 Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Mon, 15 Sep 2025 18:02:50 +0200 Subject: [PATCH 01/13] Use pycolmap database for rig support and remove COLMAPDatabase. --- hloc/reconstruction.py | 12 +- hloc/triangulation.py | 181 +++++++++++++-------------- hloc/utils/database.py | 276 ----------------------------------------- 3 files changed, 89 insertions(+), 380 deletions(-) delete mode 100644 hloc/utils/database.py diff --git a/hloc/reconstruction.py b/hloc/reconstruction.py index e2c74528..f394a746 100644 --- a/hloc/reconstruction.py +++ b/hloc/reconstruction.py @@ -15,7 +15,6 @@ import_matches, parse_option_args, ) -from .utils.database import COLMAPDatabase def create_empty_db(database_path: Path): @@ -23,9 +22,7 @@ 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 = pycolmap.Database.open(database_path) db.close() @@ -53,11 +50,10 @@ 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 pycolmap.Database.open(database_path) as db: + for img_id, image in db.read_all_images().items(): + images[image.name] = img_id return images diff --git a/hloc/triangulation.py b/hloc/triangulation.py index 66e76091..73cf3052 100644 --- a/hloc/triangulation.py +++ b/hloc/triangulation.py @@ -7,7 +7,6 @@ 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.parsers import parse_retrieval @@ -33,40 +32,27 @@ 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 pycolmap.Database.open(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: img_id for img_id, image in reconstruction.images.items()} def import_features( image_ids: Dict[str, int], database_path: Path, 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() + with pycolmap.Database.open(database_path) as db: + for image_name, image_id in tqdm(image_ids.items()): + keypoints = get_keypoints(features_path, image_name) + keypoints += 0.5 # COLMAP origin + db.write_keypoints(image_id, keypoints) def import_matches( @@ -82,24 +68,22 @@ 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] - if len({(id0, id1), (id1, id0)} & matched) > 0: - continue - matches, scores = get_matches(matches_path, name0, name1) - if min_match_score: - matches = matches[scores > min_match_score] - db.add_matches(id0, id1, matches) - matched |= {(id0, id1), (id1, id0)} - - if skip_geometric_verification: - db.add_two_view_geometry(id0, id1, matches) + with pycolmap.Database.open(database_path) as db: + matched = set() + for name0, name1 in tqdm(pairs): + id0, id1 = image_ids[name0], image_ids[name1] + if len({(id0, id1), (id1, id0)} & matched) > 0: + continue + matches, scores = get_matches(matches_path, name0, name1) + if min_match_score: + matches = matches[scores > min_match_score] + db.write_matches(id0, id1, matches) + matched |= {(id0, id1), (id1, id0)} - db.commit() - db.close() + if skip_geometric_verification: + db.write_two_view_geometry( + id0, id1, pycolmap.TwoViewGeometry(inlier_matches=matches) + ) def estimation_and_geometric_verification( @@ -126,54 +110,62 @@ 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() - for name0 in tqdm(pairs): - id0 = image_ids[name0] - image0 = reference.images[id0] - cam0 = reference.cameras[image0.camera_id] - kps0, noise0 = get_keypoints(features_path, name0, return_uncertainty=True) - noise0 = 1.0 if noise0 is None else noise0 - if len(kps0) > 0: - kps0 = np.stack(cam0.cam_from_img(kps0)) - else: - kps0 = np.zeros((0, 2)) - - for name1 in pairs[name0]: - id1 = image_ids[name1] - image1 = reference.images[id1] - cam1 = reference.cameras[image1.camera_id] - kps1, noise1 = get_keypoints(features_path, name1, return_uncertainty=True) - noise1 = 1.0 if noise1 is None else noise1 - if len(kps1) > 0: - kps1 = np.stack(cam1.cam_from_img(kps1)) - else: - kps1 = np.zeros((0, 2)) - matches = get_matches(matches_path, name0, name1)[0] - - if len({(id0, id1), (id1, id0)} & matched) > 0: - continue - matched |= {(id0, id1), (id1, id0)} - - if matches.shape[0] == 0: - db.add_two_view_geometry(id0, id1, matches) - continue - - cam1_from_cam0 = image1.cam_from_world() * image0.cam_from_world().inverse() - errors0, errors1 = compute_epipolar_errors( - cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]] - ) - valid_matches = np.logical_and( - errors0 <= cam0.cam_from_img_threshold(noise0 * max_error), - errors1 <= cam1.cam_from_img_threshold(noise1 * max_error), - ) - # 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, :]) - inlier_ratios.append(np.mean(valid_matches)) + with pycolmap.Database.open(database_path) as db: + inlier_ratios = [] + matched = set() + for name0 in tqdm(pairs): + id0 = image_ids[name0] + image0 = reference.images[id0] + cam0 = reference.cameras[image0.camera_id] + kps0, noise0 = get_keypoints(features_path, name0, return_uncertainty=True) + noise0 = 1.0 if noise0 is None else noise0 + if len(kps0) > 0: + kps0 = np.stack(cam0.cam_from_img(kps0)) + else: + kps0 = np.zeros((0, 2)) + + for name1 in pairs[name0]: + id1 = image_ids[name1] + image1 = reference.images[id1] + cam1 = reference.cameras[image1.camera_id] + kps1, noise1 = get_keypoints( + features_path, name1, return_uncertainty=True + ) + noise1 = 1.0 if noise1 is None else noise1 + if len(kps1) > 0: + kps1 = np.stack(cam1.cam_from_img(kps1)) + else: + kps1 = np.zeros((0, 2)) + + matches = get_matches(matches_path, name0, name1)[0] + + if len({(id0, id1), (id1, id0)} & matched) > 0: + continue + matched |= {(id0, id1), (id1, id0)} + + if matches.shape[0] == 0: + db.write_two_view_geometry(id0, id1, pycolmap.TwoViewGeometry()) + continue + + cam1_from_cam0 = ( + image1.cam_from_world() * image0.cam_from_world().inverse() + ) + errors0, errors1 = compute_epipolar_errors( + cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]] + ) + valid_matches = np.logical_and( + errors0 <= cam0.cam_from_img_threshold(noise0 * max_error), + errors1 <= cam1.cam_from_img_threshold(noise1 * max_error), + ) + # TODO: We could also add E to the database, but we need + # to reverse the transformations if id0 > id1 in utils/database.py. + 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%%.", np.mean(inlier_ratios) * 100, @@ -182,9 +174,6 @@ def geometric_verification( np.max(inlier_ratios) * 100, ) - db.commit() - db.close() - def run_triangulation( model_path: Path, 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), - ), - ) From 1b524d7d9cbe084203c798319cb3f1f1a25636e4 Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Wed, 17 Sep 2025 11:39:36 +0200 Subject: [PATCH 02/13] add custom context manager to tag pycolmap>=3.12.6 --- hloc/__init__.py | 2 +- hloc/reconstruction.py | 3 ++- hloc/triangulation.py | 9 +++++---- hloc/utils/database.py | 17 +++++++++++++++++ requirements.txt | 2 +- 5 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 hloc/utils/database.py 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 f394a746..29f4723f 100644 --- a/hloc/reconstruction.py +++ b/hloc/reconstruction.py @@ -15,6 +15,7 @@ import_matches, parse_option_args, ) +from utils.database import open_colmap_database def create_empty_db(database_path: Path): @@ -51,7 +52,7 @@ def import_images( def get_image_ids(database_path: Path) -> Dict[str, int]: images = {} - with pycolmap.Database.open(database_path) as db: + with open_colmap_database(database_path) as db: for img_id, image in db.read_all_images().items(): images[image.name] = img_id return images diff --git a/hloc/triangulation.py b/hloc/triangulation.py index 73cf3052..58ec2558 100644 --- a/hloc/triangulation.py +++ b/hloc/triangulation.py @@ -7,6 +7,7 @@ from tqdm import tqdm from . import logger +from .utils.database import open_colmap_database from .utils.geometry import compute_epipolar_errors from .utils.io import get_keypoints, get_matches from .utils.parsers import parse_retrieval @@ -32,7 +33,7 @@ def create_db_from_model( logger.warning("The database already exists, deleting it.") database_path.unlink() - with pycolmap.Database.open(database_path) as db: + 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(): @@ -48,7 +49,7 @@ def import_features( image_ids: Dict[str, int], database_path: Path, features_path: Path ): logger.info("Importing features into the database...") - with pycolmap.Database.open(database_path) as db: + with open_colmap_database(database_path) as db: for image_name, image_id in tqdm(image_ids.items()): keypoints = get_keypoints(features_path, image_name) keypoints += 0.5 # COLMAP origin @@ -68,7 +69,7 @@ def import_matches( with open(str(pairs_path), "r") as f: pairs = [p.split() for p in f.readlines()] - with pycolmap.Database.open(database_path) as db: + with open_colmap_database(database_path) as db: matched = set() for name0, name1 in tqdm(pairs): id0, id1 = image_ids[name0], image_ids[name1] @@ -111,7 +112,7 @@ def geometric_verification( pairs = parse_retrieval(pairs_path) - with pycolmap.Database.open(database_path) as db: + with open_colmap_database(database_path) as db: inlier_ratios = [] matched = set() for name0 in tqdm(pairs): diff --git a/hloc/utils/database.py b/hloc/utils/database.py new file mode 100644 index 00000000..f8bfc925 --- /dev/null +++ b/hloc/utils/database.py @@ -0,0 +1,17 @@ +import contextlib +import pycolmap +from pathlib import Path + +@contextlib.contextmanager +def open_colmap_database(database_path: Path) -> pycolmap.Database: + # In preparation for the context support in the future pycolmap 3.13 release + if hasattr(pycolmap.Database, "open"): + with pycolmap.Database.open(db_path) as db: + yield db + else: + db = pycolmap.Database(db_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 From 8a9d68e2164018c96125e2f93343a75c46752a6d Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Wed, 17 Sep 2025 11:40:53 +0200 Subject: [PATCH 03/13] minor --- hloc/reconstruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hloc/reconstruction.py b/hloc/reconstruction.py index 29f4723f..26de67ef 100644 --- a/hloc/reconstruction.py +++ b/hloc/reconstruction.py @@ -15,7 +15,7 @@ import_matches, parse_option_args, ) -from utils.database import open_colmap_database +from .utils.database import open_colmap_database def create_empty_db(database_path: Path): From 5ad2759f2be0d1609c2ef3ba2308135489a07695 Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Wed, 17 Sep 2025 11:43:33 +0200 Subject: [PATCH 04/13] fix create_empty_db. --- hloc/reconstruction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hloc/reconstruction.py b/hloc/reconstruction.py index 26de67ef..bcd7e962 100644 --- a/hloc/reconstruction.py +++ b/hloc/reconstruction.py @@ -23,8 +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 = pycolmap.Database.open(database_path) - db.close() + with open_colmap_database(database_path) as _: + pass def import_images( From a323efe6f3f8538fe15ae9ce10cac770a17e90f7 Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Wed, 17 Sep 2025 11:47:26 +0200 Subject: [PATCH 05/13] fix typing and format. --- hloc/utils/database.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hloc/utils/database.py b/hloc/utils/database.py index f8bfc925..836a4af4 100644 --- a/hloc/utils/database.py +++ b/hloc/utils/database.py @@ -1,9 +1,11 @@ import contextlib +from typing import ContextManager import pycolmap from pathlib import Path + @contextlib.contextmanager -def open_colmap_database(database_path: Path) -> pycolmap.Database: +def open_colmap_database(database_path: Path) -> ContextManager[pycolmap.Database]: # In preparation for the context support in the future pycolmap 3.13 release if hasattr(pycolmap.Database, "open"): with pycolmap.Database.open(db_path) as db: @@ -14,4 +16,3 @@ def open_colmap_database(database_path: Path) -> pycolmap.Database: yield db finally: db.close() - From 9dc7943fb66d635d0b7b12ea79698bff746e8dc9 Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Wed, 17 Sep 2025 11:48:42 +0200 Subject: [PATCH 06/13] typo fix --- hloc/utils/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hloc/utils/database.py b/hloc/utils/database.py index 836a4af4..caad9b8d 100644 --- a/hloc/utils/database.py +++ b/hloc/utils/database.py @@ -8,10 +8,10 @@ def open_colmap_database(database_path: Path) -> ContextManager[pycolmap.Database]: # In preparation for the context support in the future pycolmap 3.13 release if hasattr(pycolmap.Database, "open"): - with pycolmap.Database.open(db_path) as db: + with pycolmap.Database.open(database_path) as db: yield db else: - db = pycolmap.Database(db_path) + db = pycolmap.Database(database_path) try: yield db finally: From 36e011d517a7b74e992b44d507b6cc5684df2d05 Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Wed, 17 Sep 2025 11:49:44 +0200 Subject: [PATCH 07/13] format --- hloc/utils/database.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hloc/utils/database.py b/hloc/utils/database.py index caad9b8d..ad86587d 100644 --- a/hloc/utils/database.py +++ b/hloc/utils/database.py @@ -1,7 +1,8 @@ import contextlib +from pathlib import Path from typing import ContextManager + import pycolmap -from pathlib import Path @contextlib.contextmanager From 6429cf1b38f219ef8e5fa18b0dbd80ab748030fc Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Wed, 17 Sep 2025 15:05:34 +0200 Subject: [PATCH 08/13] fix version switching. --- hloc/utils/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hloc/utils/database.py b/hloc/utils/database.py index ad86587d..d3e81354 100644 --- a/hloc/utils/database.py +++ b/hloc/utils/database.py @@ -8,7 +8,7 @@ @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 hasattr(pycolmap.Database, "open"): + if isinstance(pycolmap.Database.__dict__.get("open"), (staticmethod, classmethod)): with pycolmap.Database.open(database_path) as db: yield db else: From 95ee91a6ec14dcc0de81bc56e4227945e8caa3cf Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Wed, 17 Sep 2025 15:11:27 +0200 Subject: [PATCH 09/13] minor --- hloc/utils/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hloc/utils/database.py b/hloc/utils/database.py index d3e81354..c2186525 100644 --- a/hloc/utils/database.py +++ b/hloc/utils/database.py @@ -7,7 +7,7 @@ @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 + # 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 From baad3e0fbf1243ed0977c5bf3b3d29a031057d2e Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Thu, 18 Sep 2025 22:32:19 +0200 Subject: [PATCH 10/13] cr --- hloc/reconstruction.py | 4 ++-- hloc/triangulation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hloc/reconstruction.py b/hloc/reconstruction.py index bcd7e962..9ed514f7 100644 --- a/hloc/reconstruction.py +++ b/hloc/reconstruction.py @@ -53,8 +53,8 @@ def import_images( def get_image_ids(database_path: Path) -> Dict[str, int]: images = {} with open_colmap_database(database_path) as db: - for img_id, image in db.read_all_images().items(): - images[image.name] = img_id + for image_id, image in db.read_all_images().items(): + images[image.name] = image_id return images diff --git a/hloc/triangulation.py b/hloc/triangulation.py index 58ec2558..1aa23355 100644 --- a/hloc/triangulation.py +++ b/hloc/triangulation.py @@ -42,7 +42,7 @@ def create_db_from_model( 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: img_id for img_id, image in reconstruction.images.items()} + return {image.name: image_id for image_id, image in reconstruction.images.items()} def import_features( From f2fbb30a075810bfd7df669d58ee7d3ffa4de988 Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Mon, 22 Sep 2025 11:20:49 +0200 Subject: [PATCH 11/13] cr --- hloc/reconstruction.py | 27 ++++--- hloc/triangulation.py | 175 ++++++++++++++++++++--------------------- 2 files changed, 102 insertions(+), 100 deletions(-) diff --git a/hloc/reconstruction.py b/hloc/reconstruction.py index 9ed514f7..18eddb1e 100644 --- a/hloc/reconstruction.py +++ b/hloc/reconstruction.py @@ -53,8 +53,9 @@ def import_images( def get_image_ids(database_path: Path) -> Dict[str, int]: images = {} with open_colmap_database(database_path) as db: - for image_id, image in db.read_all_images().items(): - images[image.name] = image_id + images = { + image.name: image_id for image_id, image in db.read_all_images().items() + } return images @@ -168,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 1aa23355..2745cca2 100644 --- a/hloc/triangulation.py +++ b/hloc/triangulation.py @@ -46,19 +46,18 @@ def create_db_from_model( 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...") - with open_colmap_database(database_path) as db: - for image_name, image_id in tqdm(image_ids.items()): - keypoints = get_keypoints(features_path, image_name) - keypoints += 0.5 # COLMAP origin - db.write_keypoints(image_id, keypoints) + for image_name, image_id in tqdm(image_ids.items()): + keypoints = get_keypoints(features_path, image_name) + keypoints += 0.5 # COLMAP origin + 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, @@ -69,22 +68,21 @@ def import_matches( with open(str(pairs_path), "r") as f: pairs = [p.split() for p in f.readlines()] - with open_colmap_database(database_path) as db: - matched = set() - for name0, name1 in tqdm(pairs): - id0, id1 = image_ids[name0], image_ids[name1] - if len({(id0, id1), (id1, id0)} & matched) > 0: - continue - matches, scores = get_matches(matches_path, name0, name1) - if min_match_score: - matches = matches[scores > min_match_score] - db.write_matches(id0, id1, matches) - matched |= {(id0, id1), (id1, id0)} - - if skip_geometric_verification: - db.write_two_view_geometry( - id0, id1, pycolmap.TwoViewGeometry(inlier_matches=matches) - ) + matched = set() + for name0, name1 in tqdm(pairs): + id0, id1 = image_ids[name0], image_ids[name1] + if len({(id0, id1), (id1, id0)} & matched) > 0: + continue + matches, scores = get_matches(matches_path, name0, name1) + if min_match_score: + matches = matches[scores > min_match_score] + db.write_matches(id0, id1, matches) + matched |= {(id0, id1), (id1, id0)} + + if skip_geometric_verification: + db.write_two_view_geometry( + id0, id1, pycolmap.TwoViewGeometry(inlier_matches=matches) + ) def estimation_and_geometric_verification( @@ -102,7 +100,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, @@ -112,61 +110,60 @@ def geometric_verification( pairs = parse_retrieval(pairs_path) - with open_colmap_database(database_path) as db: - inlier_ratios = [] - matched = set() - for name0 in tqdm(pairs): - id0 = image_ids[name0] - image0 = reference.images[id0] - cam0 = reference.cameras[image0.camera_id] - kps0, noise0 = get_keypoints(features_path, name0, return_uncertainty=True) - noise0 = 1.0 if noise0 is None else noise0 - if len(kps0) > 0: - kps0 = np.stack(cam0.cam_from_img(kps0)) + inlier_ratios = [] + matched = set() + for name0 in tqdm(pairs): + id0 = image_ids[name0] + image0 = reference.images[id0] + cam0 = reference.cameras[image0.camera_id] + kps0, noise0 = get_keypoints(features_path, name0, return_uncertainty=True) + noise0 = 1.0 if noise0 is None else noise0 + if len(kps0) > 0: + kps0 = np.stack(cam0.cam_from_img(kps0)) + else: + kps0 = np.zeros((0, 2)) + + for name1 in pairs[name0]: + id1 = image_ids[name1] + image1 = reference.images[id1] + cam1 = reference.cameras[image1.camera_id] + kps1, noise1 = get_keypoints( + features_path, name1, return_uncertainty=True + ) + noise1 = 1.0 if noise1 is None else noise1 + if len(kps1) > 0: + kps1 = np.stack(cam1.cam_from_img(kps1)) else: - kps0 = np.zeros((0, 2)) - - for name1 in pairs[name0]: - id1 = image_ids[name1] - image1 = reference.images[id1] - cam1 = reference.cameras[image1.camera_id] - kps1, noise1 = get_keypoints( - features_path, name1, return_uncertainty=True - ) - noise1 = 1.0 if noise1 is None else noise1 - if len(kps1) > 0: - kps1 = np.stack(cam1.cam_from_img(kps1)) - else: - kps1 = np.zeros((0, 2)) - - matches = get_matches(matches_path, name0, name1)[0] - - if len({(id0, id1), (id1, id0)} & matched) > 0: - continue - matched |= {(id0, id1), (id1, id0)} - - if matches.shape[0] == 0: - db.write_two_view_geometry(id0, id1, pycolmap.TwoViewGeometry()) - continue - - cam1_from_cam0 = ( - image1.cam_from_world() * image0.cam_from_world().inverse() - ) - errors0, errors1 = compute_epipolar_errors( - cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]] - ) - valid_matches = np.logical_and( - errors0 <= cam0.cam_from_img_threshold(noise0 * max_error), - errors1 <= cam1.cam_from_img_threshold(noise1 * max_error), - ) - # TODO: We could also add E to the database, but we need - # to reverse the transformations if id0 > id1 in utils/database.py. - db.write_two_view_geometry( - id0, - id1, - pycolmap.TwoViewGeometry(inlier_matches=matches[valid_matches, :]), - ) - inlier_ratios.append(np.mean(valid_matches)) + kps1 = np.zeros((0, 2)) + + matches = get_matches(matches_path, name0, name1)[0] + + if len({(id0, id1), (id1, id0)} & matched) > 0: + continue + matched |= {(id0, id1), (id1, id0)} + + if matches.shape[0] == 0: + db.write_two_view_geometry(id0, id1, pycolmap.TwoViewGeometry()) + continue + + cam1_from_cam0 = ( + image1.cam_from_world() * image0.cam_from_world().inverse() + ) + errors0, errors1 = compute_epipolar_errors( + cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]] + ) + valid_matches = np.logical_and( + errors0 <= cam0.cam_from_img_threshold(noise0 * max_error), + errors1 <= cam1.cam_from_img_threshold(noise1 * max_error), + ) + # TODO: We could also add E to the database, but we need + # to reverse the transformations if id0 > id1 in utils/database.py. + 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%%.", np.mean(inlier_ratios) * 100, @@ -218,18 +215,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 From 23aa5c81f0efa04266467fd5423f22ab82f72d0e Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Mon, 22 Sep 2025 11:31:34 +0200 Subject: [PATCH 12/13] format --- hloc/triangulation.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/hloc/triangulation.py b/hloc/triangulation.py index 2745cca2..c6752124 100644 --- a/hloc/triangulation.py +++ b/hloc/triangulation.py @@ -127,9 +127,7 @@ def geometric_verification( id1 = image_ids[name1] image1 = reference.images[id1] cam1 = reference.cameras[image1.camera_id] - kps1, noise1 = get_keypoints( - features_path, name1, return_uncertainty=True - ) + kps1, noise1 = get_keypoints(features_path, name1, return_uncertainty=True) noise1 = 1.0 if noise1 is None else noise1 if len(kps1) > 0: kps1 = np.stack(cam1.cam_from_img(kps1)) @@ -146,9 +144,7 @@ def geometric_verification( db.write_two_view_geometry(id0, id1, pycolmap.TwoViewGeometry()) continue - cam1_from_cam0 = ( - image1.cam_from_world() * image0.cam_from_world().inverse() - ) + cam1_from_cam0 = image1.cam_from_world() * image0.cam_from_world().inverse() errors0, errors1 = compute_epipolar_errors( cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]] ) From 6bf63481379a746fa0f902bc30322814e0fe5194 Mon Sep 17 00:00:00 2001 From: Shaohui Liu Date: Mon, 22 Sep 2025 18:21:00 +0200 Subject: [PATCH 13/13] move to utils/io.py --- hloc/reconstruction.py | 2 +- hloc/triangulation.py | 3 +-- hloc/utils/database.py | 19 ------------------- hloc/utils/io.py | 17 ++++++++++++++++- 4 files changed, 18 insertions(+), 23 deletions(-) delete mode 100644 hloc/utils/database.py diff --git a/hloc/reconstruction.py b/hloc/reconstruction.py index 18eddb1e..faf06769 100644 --- a/hloc/reconstruction.py +++ b/hloc/reconstruction.py @@ -15,7 +15,7 @@ import_matches, parse_option_args, ) -from .utils.database import open_colmap_database +from .utils.io import open_colmap_database def create_empty_db(database_path: Path): diff --git a/hloc/triangulation.py b/hloc/triangulation.py index c6752124..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 open_colmap_database 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 diff --git a/hloc/utils/database.py b/hloc/utils/database.py deleted file mode 100644 index c2186525..00000000 --- a/hloc/utils/database.py +++ /dev/null @@ -1,19 +0,0 @@ -import contextlib -from pathlib import Path -from typing import ContextManager - -import pycolmap - - -@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/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()