From 7433e77cad39e2b01ee5835193ecf4615da17ea7 Mon Sep 17 00:00:00 2001 From: Francesco Ioli Date: Sat, 30 Mar 2024 16:36:32 +0100 Subject: [PATCH] updated triangulation module --- src/deep_image_matching/triangulation.py | 76 +++++++++++++++++++++++- src/deep_image_matching/utils/utils.py | 60 +++++++++++-------- 2 files changed, 109 insertions(+), 27 deletions(-) diff --git a/src/deep_image_matching/triangulation.py b/src/deep_image_matching/triangulation.py index eb62cb6..f7ecd17 100644 --- a/src/deep_image_matching/triangulation.py +++ b/src/deep_image_matching/triangulation.py @@ -17,7 +17,6 @@ logger = logging.getLogger("dim") -# Utils functions def parse_retrieval(path): retrieval = defaultdict(list) with open(path, "r") as f: @@ -30,6 +29,18 @@ def parse_retrieval(path): def create_db_from_model(reconstruction: pycolmap.Reconstruction, database_path: Path) -> Dict[str, int]: + """ + Creates a COLMAP database from a PyCOLMAP reconstruction (it deletes an existing database if found at the specified path). The function + populates the database with camera parameters and image information, but does not add 2D and 3D points. + + Args: + reconstruction (pycolmap.Reconstruction): The input PyCOLMAP reconstruction. + database_path (Path): Path to the COLMAP database file. + + Returns: + Dict[str, int]: A dictionary mapping image names to their corresponding + image IDs in the database. + """ if database_path.exists(): logger.warning("The database already exists, deleting it.") database_path.unlink() @@ -56,6 +67,19 @@ def create_db_from_model(reconstruction: pycolmap.Reconstruction, database_path: def get_keypoints(features_h5: Path, name: str) -> np.ndarray: + """ + Loads keypoints from an HDF5 file. + + Args: + features_h5 (Path): Path to the HDF5 file containing image features. + name (str): The name of the image whose keypoints are to be retrieved. + + Returns: + np.ndarray: An array of keypoints. + + Raises: + KeyError: If the specified image name is not found within the HDF5 file. + """ with h5py.File(str(features_h5), "r", libver="latest") as f: if name not in f: raise KeyError(f"Key '{name}' not found in '{features_h5}'") @@ -63,6 +87,20 @@ def get_keypoints(features_h5: Path, name: str) -> np.ndarray: def get_matches(matches_h5: Path, name0: str, name1) -> np.ndarray: + """ + Retrieves feature matches between two images from an HDF5 file. + + Args: + matches_h5 (Path): Path to the HDF5 file storing feature matches. + name0 (str): Name of the first image. + name1 (str): Name of the second image. + + Returns: + np.ndarray: An array representing the feature matches. + + Raises: + KeyError: If either image name is not found in the HDF5 file. + """ with h5py.File(str(matches_h5), "r", libver="latest") as f: if name0 not in f: if name1 in f: @@ -73,6 +111,14 @@ def get_matches(matches_h5: Path, name0: str, name1) -> np.ndarray: def import_keypoints(features_h5: Path, image_ids: Dict[str, int], database_path: Path) -> None: + """ + Imports keypoints from an HDF5 file into a COLMAP database. + + Args: + features_h5 (Path): Path to the HDF5 file containing image features. + image_ids (Dict[str, int]): A dictionary mapping image names to image IDs. + database_path (Path): Path to the COLMAP database file. + """ with COLMAPDatabase.connect(database_path) as db: for name, image_id in tqdm(image_ids.items(), desc="Importing keypoints"): keypoints = get_keypoints(features_h5, name) @@ -88,6 +134,19 @@ def import_matches( pair_file: Path, add_two_view_geometry: bool = False, ): + """ + Imports feature matches into a COLMAP database. + + Reads image pairs from a file and retrieves corresponding matches from + an HDF5 file, adding them to the specified database. + + Args: + matches_h5 (Path): Path to the HDF5 file containing feature matches. + image_ids (Dict[str, int]): A dictionary mapping image names to their corresponding image IDs. + database_path (Path): Path to the COLMAP database file. + pair_file (Path): Path to a file containing image pairs (one pair per line). + add_two_view_geometry (bool): If True, calculates and adds two-view geometry to the database. Defaults to False. + """ pairs = get_pairs_from_file(pair_file) with COLMAPDatabase.connect(database_path) as db: for name0, name1 in tqdm(pairs, desc="Importing matches"): @@ -108,6 +167,21 @@ def import_verifed_matches( pairs_path: Path, max_error: float = 4.0, ): + """ + Imports geometrically verified matches into a COLMAP database. + + Performs geometric verification of matches using epipolar constraints. + Only matches that pass the verification are added to the database. + + Args: + image_ids (Dict[str, int]): A dictionary mapping image names to their corresponding image IDs. + reference (pycolmap.Reconstruction): The reference PyCOLMAP reconstruction. + database_path (Path): Path to the COLMAP database file. + features_path (Path): Path to the HDF5 file containing image features. + matches_path (Path): Path to the HDF5 file containing feature matches. + pairs_path (Path): Path to a file specifying image pairs for retrieval. + max_error (float): Maximum allowable epipolar error (in pixels) for a match to be considered valid. Defaults to 4.0. + """ logger.info("Performing geometric verification of the matches...") pairs = parse_retrieval(pairs_path) diff --git a/src/deep_image_matching/utils/utils.py b/src/deep_image_matching/utils/utils.py index e716161..dd2687f 100644 --- a/src/deep_image_matching/utils/utils.py +++ b/src/deep_image_matching/utils/utils.py @@ -62,14 +62,48 @@ def get_pairs_from_file(pair_file: Path) -> list: def to_homogeneous(p): + """ + Converts 2D points to homogeneous coordinates. + + Args: + p (np.ndarray): A numpy array of shape (N, 2) representing 2D points. + + Returns: + np.ndarray: Array of shape (N, 3) where the last column is appended + with ones for homogeneous representation. + """ return np.pad(p, ((0, 0),) * (p.ndim - 1) + ((0, 1),), constant_values=1) def vector_to_cross_product_matrix(v): + """ + Converts a 3D vector into its corresponding skew-symmetric cross-product matrix. + + This matrix is useful for representing cross-product operations in vector-matrix calculations. + + Args: + v (np.ndarray): A 3-dimensional NumPy array representing the input vector. + + Returns: + np.ndarray: A 3x3 NumPy array representing the skew-symmetric cross-product matrix. + """ return np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]]) def compute_epipolar_errors(j_from_i: pycolmap.Rigid3d, p2d_i, p2d_j): + """ + Computes epipolar errors for corresponding 2D points between two images. + + Args: + j_from_i (pycolmap.Rigid3d): The transformation from image i to image j. + p2d_i (np.ndarray): 2D points in image i (shape: (N, 2)). + p2d_j (np.ndarray): Corresponding 2D points in image j (shape: (N, 2)). + + Returns: + tuple: A tuple containing two numpy arrays: + * errors_i: Epipolar errors for points in image i. + * errors_j: Epipolar errors for points in image j. + """ j_E_i = j_from_i.essential_matrix() l2d_j = to_homogeneous(p2d_i) @ j_E_i.T l2d_i = to_homogeneous(p2d_j) @ j_E_i @@ -77,29 +111,3 @@ def compute_epipolar_errors(j_from_i: pycolmap.Rigid3d, p2d_i, p2d_j): errors_i = dist / np.linalg.norm(l2d_i[:, :2], axis=1) errors_j = dist / np.linalg.norm(l2d_j[:, :2], axis=1) return errors_i, errors_j - - -def create_db_from_model(reconstruction: pycolmap.Reconstruction, database_path: Path) -> Dict[str, int]: - if database_path.exists(): - logger.warning("The database already exists, deleting it.") - database_path.unlink() - - with COLMAPDatabase.connect(database_path) as db: - 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() - - return {image.name: i for i, image in reconstruction.images.items()}