import argparse from pathlib import Path import numpy as np import scipy.spatial from . import logger from .pairs_from_retrieval import pairs_from_score_matrix from .utils.read_write_model import read_images_binary DEFAULT_ROT_THRESH = 30 # in degrees def get_pairwise_distances(images): ids = np.array(list(images.keys())) Rs = [] ts = [] for id_ in ids: image = images[id_] R = image.qvec2rotmat() t = image.tvec Rs.append(R) ts.append(t) Rs = np.stack(Rs, 0) ts = np.stack(ts, 0) # Invert the poses from world-to-camera to camera-to-world. Rs = Rs.transpose(0, 2, 1) ts = -(Rs @ ts[:, :, None])[:, :, 0] dist = scipy.spatial.distance.squareform(scipy.spatial.distance.pdist(ts)) # Instead of computing the angle between two camera orientations, # we compute the angle between the principal axes, as two images rotated # around their principal axis still observe the same scene. axes = Rs[:, :, -1] dots = np.einsum("mi,ni->mn", axes, axes, optimize=True) dR = np.rad2deg(np.arccos(np.clip(dots, -1.0, 1.0))) return ids, dist, dR def main(model, output, num_matched, rotation_threshold=DEFAULT_ROT_THRESH): logger.info("Reading the COLMAP model...") images = read_images_binary(model / "images.bin") logger.info(f"Obtaining pairwise distances between {len(images)} images...") ids, dist, dR = get_pairwise_distances(images) scores = -dist invalid = dR >= rotation_threshold np.fill_diagonal(invalid, True) pairs = pairs_from_score_matrix(scores, invalid, num_matched) pairs = [(images[ids[i]].name, images[ids[j]].name) for i, j in pairs] logger.info(f"Found {len(pairs)} pairs.") with open(output, "w") as f: f.write("\n".join(" ".join(p) for p in pairs)) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--model", required=True, type=Path) parser.add_argument("--output", required=True, type=Path) parser.add_argument("--num_matched", required=True, type=int) parser.add_argument( "--rotation_threshold", default=DEFAULT_ROT_THRESH, type=float ) args = parser.parse_args() main(**args.__dict__)