Spaces:
Running
Running
Vincentqyw
commited on
Commit
·
4c88343
1
Parent(s):
f90241e
update: sync with hloc
Browse files- hloc/__init__.py +14 -10
- hloc/colmap_from_nvm.py +204 -0
- hloc/extract_features.py +1 -0
- hloc/localize_inloc.py +179 -0
- hloc/localize_sfm.py +240 -0
- hloc/match_dense.py +533 -0
- hloc/matchers/superglue.py +1 -1
- hloc/pairs_from_covisibility.py +60 -0
- hloc/pairs_from_exhaustive.py +64 -0
- hloc/pairs_from_poses.py +68 -0
- hloc/pairs_from_retrieval.py +133 -0
- hloc/pipelines/4Seasons/localize.py +15 -12
- hloc/pipelines/4Seasons/prepare_reference.py +3 -5
- hloc/pipelines/4Seasons/utils.py +17 -20
- hloc/pipelines/7Scenes/create_gt_sfm.py +8 -19
- hloc/pipelines/7Scenes/pipeline.py +13 -11
- hloc/pipelines/7Scenes/utils.py +1 -0
- hloc/pipelines/Aachen/README.md +1 -1
- hloc/pipelines/Aachen/pipeline.py +95 -88
- hloc/pipelines/Aachen_v1_1/README.md +1 -2
- hloc/pipelines/Aachen_v1_1/pipeline.py +90 -81
- hloc/pipelines/Aachen_v1_1/pipeline_loftr.py +91 -81
- hloc/pipelines/CMU/pipeline.py +17 -28
- hloc/pipelines/Cambridge/pipeline.py +14 -19
- hloc/pipelines/Cambridge/utils.py +6 -7
- hloc/pipelines/RobotCar/colmap_from_nvm.py +14 -12
- hloc/pipelines/RobotCar/pipeline.py +100 -91
- hloc/reconstruction.py +194 -0
- hloc/triangulation.py +306 -0
- hloc/utils/database.py +8 -24
- hloc/utils/geometry.py +8 -25
- hloc/utils/parsers.py +6 -3
- hloc/utils/read_write_model.py +22 -51
- hloc/utils/viz.py +21 -33
- hloc/utils/viz_3d.py +39 -42
- hloc/visualization.py +163 -0
hloc/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@ import logging
|
|
| 3 |
import torch
|
| 4 |
from packaging import version
|
| 5 |
|
| 6 |
-
__version__ = "1.
|
| 7 |
|
| 8 |
formatter = logging.Formatter(
|
| 9 |
fmt="[%(asctime)s %(name)s %(levelname)s] %(message)s",
|
|
@@ -23,14 +23,18 @@ try:
|
|
| 23 |
except ImportError:
|
| 24 |
logger.warning("pycolmap is not installed, some features may not work.")
|
| 25 |
else:
|
| 26 |
-
|
| 27 |
-
found_version =
|
| 28 |
-
if found_version
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
|
| 3 |
import torch
|
| 4 |
from packaging import version
|
| 5 |
|
| 6 |
+
__version__ = "1.5"
|
| 7 |
|
| 8 |
formatter = logging.Formatter(
|
| 9 |
fmt="[%(asctime)s %(name)s %(levelname)s] %(message)s",
|
|
|
|
| 23 |
except ImportError:
|
| 24 |
logger.warning("pycolmap is not installed, some features may not work.")
|
| 25 |
else:
|
| 26 |
+
min_version = version.parse("0.6.0")
|
| 27 |
+
found_version = pycolmap.__version__
|
| 28 |
+
if found_version != "dev":
|
| 29 |
+
version = version.parse(found_version)
|
| 30 |
+
if version < min_version:
|
| 31 |
+
s = f"pycolmap>={min_version}"
|
| 32 |
+
logger.warning(
|
| 33 |
+
"hloc requires %s but found pycolmap==%s, "
|
| 34 |
+
'please upgrade with `pip install --upgrade "%s"`',
|
| 35 |
+
s,
|
| 36 |
+
found_version,
|
| 37 |
+
s,
|
| 38 |
+
)
|
| 39 |
|
| 40 |
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
hloc/colmap_from_nvm.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import sqlite3
|
| 3 |
+
from collections import defaultdict
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
|
| 9 |
+
from . import logger
|
| 10 |
+
from .utils.read_write_model import (
|
| 11 |
+
CAMERA_MODEL_NAMES,
|
| 12 |
+
Camera,
|
| 13 |
+
Image,
|
| 14 |
+
Point3D,
|
| 15 |
+
write_model,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def recover_database_images_and_ids(database_path):
|
| 20 |
+
images = {}
|
| 21 |
+
cameras = {}
|
| 22 |
+
db = sqlite3.connect(str(database_path))
|
| 23 |
+
ret = db.execute("SELECT name, image_id, camera_id FROM images;")
|
| 24 |
+
for name, image_id, camera_id in ret:
|
| 25 |
+
images[name] = image_id
|
| 26 |
+
cameras[name] = camera_id
|
| 27 |
+
db.close()
|
| 28 |
+
logger.info(f"Found {len(images)} images and {len(cameras)} cameras in database.")
|
| 29 |
+
return images, cameras
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def quaternion_to_rotation_matrix(qvec):
|
| 33 |
+
qvec = qvec / np.linalg.norm(qvec)
|
| 34 |
+
w, x, y, z = qvec
|
| 35 |
+
R = np.array(
|
| 36 |
+
[
|
| 37 |
+
[1 - 2 * y * y - 2 * z * z, 2 * x * y - 2 * z * w, 2 * x * z + 2 * y * w],
|
| 38 |
+
[2 * x * y + 2 * z * w, 1 - 2 * x * x - 2 * z * z, 2 * y * z - 2 * x * w],
|
| 39 |
+
[2 * x * z - 2 * y * w, 2 * y * z + 2 * x * w, 1 - 2 * x * x - 2 * y * y],
|
| 40 |
+
]
|
| 41 |
+
)
|
| 42 |
+
return R
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def camera_center_to_translation(c, qvec):
|
| 46 |
+
R = quaternion_to_rotation_matrix(qvec)
|
| 47 |
+
return (-1) * np.matmul(R, c)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def read_nvm_model(nvm_path, intrinsics_path, image_ids, camera_ids, skip_points=False):
|
| 51 |
+
with open(intrinsics_path, "r") as f:
|
| 52 |
+
raw_intrinsics = f.readlines()
|
| 53 |
+
|
| 54 |
+
logger.info(f"Reading {len(raw_intrinsics)} cameras...")
|
| 55 |
+
cameras = {}
|
| 56 |
+
for intrinsics in raw_intrinsics:
|
| 57 |
+
intrinsics = intrinsics.strip("\n").split(" ")
|
| 58 |
+
name, camera_model, width, height = intrinsics[:4]
|
| 59 |
+
params = [float(p) for p in intrinsics[4:]]
|
| 60 |
+
camera_model = CAMERA_MODEL_NAMES[camera_model]
|
| 61 |
+
assert len(params) == camera_model.num_params
|
| 62 |
+
camera_id = camera_ids[name]
|
| 63 |
+
camera = Camera(
|
| 64 |
+
id=camera_id,
|
| 65 |
+
model=camera_model.model_name,
|
| 66 |
+
width=int(width),
|
| 67 |
+
height=int(height),
|
| 68 |
+
params=params,
|
| 69 |
+
)
|
| 70 |
+
cameras[camera_id] = camera
|
| 71 |
+
|
| 72 |
+
nvm_f = open(nvm_path, "r")
|
| 73 |
+
line = nvm_f.readline()
|
| 74 |
+
while line == "\n" or line.startswith("NVM_V3"):
|
| 75 |
+
line = nvm_f.readline()
|
| 76 |
+
num_images = int(line)
|
| 77 |
+
assert num_images == len(cameras)
|
| 78 |
+
|
| 79 |
+
logger.info(f"Reading {num_images} images...")
|
| 80 |
+
image_idx_to_db_image_id = []
|
| 81 |
+
image_data = []
|
| 82 |
+
i = 0
|
| 83 |
+
while i < num_images:
|
| 84 |
+
line = nvm_f.readline()
|
| 85 |
+
if line == "\n":
|
| 86 |
+
continue
|
| 87 |
+
data = line.strip("\n").split(" ")
|
| 88 |
+
image_data.append(data)
|
| 89 |
+
image_idx_to_db_image_id.append(image_ids[data[0]])
|
| 90 |
+
i += 1
|
| 91 |
+
|
| 92 |
+
line = nvm_f.readline()
|
| 93 |
+
while line == "\n":
|
| 94 |
+
line = nvm_f.readline()
|
| 95 |
+
num_points = int(line)
|
| 96 |
+
|
| 97 |
+
if skip_points:
|
| 98 |
+
logger.info(f"Skipping {num_points} points.")
|
| 99 |
+
num_points = 0
|
| 100 |
+
else:
|
| 101 |
+
logger.info(f"Reading {num_points} points...")
|
| 102 |
+
points3D = {}
|
| 103 |
+
image_idx_to_keypoints = defaultdict(list)
|
| 104 |
+
i = 0
|
| 105 |
+
pbar = tqdm(total=num_points, unit="pts")
|
| 106 |
+
while i < num_points:
|
| 107 |
+
line = nvm_f.readline()
|
| 108 |
+
if line == "\n":
|
| 109 |
+
continue
|
| 110 |
+
|
| 111 |
+
data = line.strip("\n").split(" ")
|
| 112 |
+
x, y, z, r, g, b, num_observations = data[:7]
|
| 113 |
+
obs_image_ids, point2D_idxs = [], []
|
| 114 |
+
for j in range(int(num_observations)):
|
| 115 |
+
s = 7 + 4 * j
|
| 116 |
+
img_index, kp_index, kx, ky = data[s : s + 4]
|
| 117 |
+
image_idx_to_keypoints[int(img_index)].append(
|
| 118 |
+
(int(kp_index), float(kx), float(ky), i)
|
| 119 |
+
)
|
| 120 |
+
db_image_id = image_idx_to_db_image_id[int(img_index)]
|
| 121 |
+
obs_image_ids.append(db_image_id)
|
| 122 |
+
point2D_idxs.append(kp_index)
|
| 123 |
+
|
| 124 |
+
point = Point3D(
|
| 125 |
+
id=i,
|
| 126 |
+
xyz=np.array([x, y, z], float),
|
| 127 |
+
rgb=np.array([r, g, b], int),
|
| 128 |
+
error=1.0, # fake
|
| 129 |
+
image_ids=np.array(obs_image_ids, int),
|
| 130 |
+
point2D_idxs=np.array(point2D_idxs, int),
|
| 131 |
+
)
|
| 132 |
+
points3D[i] = point
|
| 133 |
+
|
| 134 |
+
i += 1
|
| 135 |
+
pbar.update(1)
|
| 136 |
+
pbar.close()
|
| 137 |
+
|
| 138 |
+
logger.info("Parsing image data...")
|
| 139 |
+
images = {}
|
| 140 |
+
for i, data in enumerate(image_data):
|
| 141 |
+
# Skip the focal length. Skip the distortion and terminal 0.
|
| 142 |
+
name, _, qw, qx, qy, qz, cx, cy, cz, _, _ = data
|
| 143 |
+
qvec = np.array([qw, qx, qy, qz], float)
|
| 144 |
+
c = np.array([cx, cy, cz], float)
|
| 145 |
+
t = camera_center_to_translation(c, qvec)
|
| 146 |
+
|
| 147 |
+
if i in image_idx_to_keypoints:
|
| 148 |
+
# NVM only stores triangulated 2D keypoints: add dummy ones
|
| 149 |
+
keypoints = image_idx_to_keypoints[i]
|
| 150 |
+
point2D_idxs = np.array([d[0] for d in keypoints])
|
| 151 |
+
tri_xys = np.array([[x, y] for _, x, y, _ in keypoints])
|
| 152 |
+
tri_ids = np.array([i for _, _, _, i in keypoints])
|
| 153 |
+
|
| 154 |
+
num_2Dpoints = max(point2D_idxs) + 1
|
| 155 |
+
xys = np.zeros((num_2Dpoints, 2), float)
|
| 156 |
+
point3D_ids = np.full(num_2Dpoints, -1, int)
|
| 157 |
+
xys[point2D_idxs] = tri_xys
|
| 158 |
+
point3D_ids[point2D_idxs] = tri_ids
|
| 159 |
+
else:
|
| 160 |
+
xys = np.zeros((0, 2), float)
|
| 161 |
+
point3D_ids = np.full(0, -1, int)
|
| 162 |
+
|
| 163 |
+
image_id = image_ids[name]
|
| 164 |
+
image = Image(
|
| 165 |
+
id=image_id,
|
| 166 |
+
qvec=qvec,
|
| 167 |
+
tvec=t,
|
| 168 |
+
camera_id=camera_ids[name],
|
| 169 |
+
name=name,
|
| 170 |
+
xys=xys,
|
| 171 |
+
point3D_ids=point3D_ids,
|
| 172 |
+
)
|
| 173 |
+
images[image_id] = image
|
| 174 |
+
|
| 175 |
+
return cameras, images, points3D
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def main(nvm, intrinsics, database, output, skip_points=False):
|
| 179 |
+
assert nvm.exists(), nvm
|
| 180 |
+
assert intrinsics.exists(), intrinsics
|
| 181 |
+
assert database.exists(), database
|
| 182 |
+
|
| 183 |
+
image_ids, camera_ids = recover_database_images_and_ids(database)
|
| 184 |
+
|
| 185 |
+
logger.info("Reading the NVM model...")
|
| 186 |
+
model = read_nvm_model(
|
| 187 |
+
nvm, intrinsics, image_ids, camera_ids, skip_points=skip_points
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
logger.info("Writing the COLMAP model...")
|
| 191 |
+
output.mkdir(exist_ok=True, parents=True)
|
| 192 |
+
write_model(*model, path=str(output), ext=".bin")
|
| 193 |
+
logger.info("Done.")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
parser = argparse.ArgumentParser()
|
| 198 |
+
parser.add_argument("--nvm", required=True, type=Path)
|
| 199 |
+
parser.add_argument("--intrinsics", required=True, type=Path)
|
| 200 |
+
parser.add_argument("--database", required=True, type=Path)
|
| 201 |
+
parser.add_argument("--output", required=True, type=Path)
|
| 202 |
+
parser.add_argument("--skip_points", action="store_true")
|
| 203 |
+
args = parser.parse_args()
|
| 204 |
+
main(**args.__dict__)
|
hloc/extract_features.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import argparse
|
| 2 |
import collections.abc as collections
|
|
|
|
| 3 |
import pprint
|
| 4 |
from pathlib import Path
|
| 5 |
from types import SimpleNamespace
|
|
|
|
| 1 |
import argparse
|
| 2 |
import collections.abc as collections
|
| 3 |
+
import glob
|
| 4 |
import pprint
|
| 5 |
from pathlib import Path
|
| 6 |
from types import SimpleNamespace
|
hloc/localize_inloc.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import pickle
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import cv2
|
| 6 |
+
import h5py
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pycolmap
|
| 9 |
+
import torch
|
| 10 |
+
from scipy.io import loadmat
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
|
| 13 |
+
from . import logger
|
| 14 |
+
from .utils.parsers import names_to_pair, parse_retrieval
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def interpolate_scan(scan, kp):
|
| 18 |
+
h, w, c = scan.shape
|
| 19 |
+
kp = kp / np.array([[w - 1, h - 1]]) * 2 - 1
|
| 20 |
+
assert np.all(kp > -1) and np.all(kp < 1)
|
| 21 |
+
scan = torch.from_numpy(scan).permute(2, 0, 1)[None]
|
| 22 |
+
kp = torch.from_numpy(kp)[None, None]
|
| 23 |
+
grid_sample = torch.nn.functional.grid_sample
|
| 24 |
+
|
| 25 |
+
# To maximize the number of points that have depth:
|
| 26 |
+
# do bilinear interpolation first and then nearest for the remaining points
|
| 27 |
+
interp_lin = grid_sample(scan, kp, align_corners=True, mode="bilinear")[0, :, 0]
|
| 28 |
+
interp_nn = torch.nn.functional.grid_sample(
|
| 29 |
+
scan, kp, align_corners=True, mode="nearest"
|
| 30 |
+
)[0, :, 0]
|
| 31 |
+
interp = torch.where(torch.isnan(interp_lin), interp_nn, interp_lin)
|
| 32 |
+
valid = ~torch.any(torch.isnan(interp), 0)
|
| 33 |
+
|
| 34 |
+
kp3d = interp.T.numpy()
|
| 35 |
+
valid = valid.numpy()
|
| 36 |
+
return kp3d, valid
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def get_scan_pose(dataset_dir, rpath):
|
| 40 |
+
split_image_rpath = rpath.split("/")
|
| 41 |
+
floor_name = split_image_rpath[-3]
|
| 42 |
+
scan_id = split_image_rpath[-2]
|
| 43 |
+
image_name = split_image_rpath[-1]
|
| 44 |
+
building_name = image_name[:3]
|
| 45 |
+
|
| 46 |
+
path = Path(
|
| 47 |
+
dataset_dir,
|
| 48 |
+
"database/alignments",
|
| 49 |
+
floor_name,
|
| 50 |
+
f"transformations/{building_name}_trans_{scan_id}.txt",
|
| 51 |
+
)
|
| 52 |
+
with open(path) as f:
|
| 53 |
+
raw_lines = f.readlines()
|
| 54 |
+
|
| 55 |
+
P_after_GICP = np.array(
|
| 56 |
+
[
|
| 57 |
+
np.fromstring(raw_lines[7], sep=" "),
|
| 58 |
+
np.fromstring(raw_lines[8], sep=" "),
|
| 59 |
+
np.fromstring(raw_lines[9], sep=" "),
|
| 60 |
+
np.fromstring(raw_lines[10], sep=" "),
|
| 61 |
+
]
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
return P_after_GICP
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def pose_from_cluster(dataset_dir, q, retrieved, feature_file, match_file, skip=None):
|
| 68 |
+
height, width = cv2.imread(str(dataset_dir / q)).shape[:2]
|
| 69 |
+
cx = 0.5 * width
|
| 70 |
+
cy = 0.5 * height
|
| 71 |
+
focal_length = 4032.0 * 28.0 / 36.0
|
| 72 |
+
|
| 73 |
+
all_mkpq = []
|
| 74 |
+
all_mkpr = []
|
| 75 |
+
all_mkp3d = []
|
| 76 |
+
all_indices = []
|
| 77 |
+
kpq = feature_file[q]["keypoints"].__array__()
|
| 78 |
+
num_matches = 0
|
| 79 |
+
|
| 80 |
+
for i, r in enumerate(retrieved):
|
| 81 |
+
kpr = feature_file[r]["keypoints"].__array__()
|
| 82 |
+
pair = names_to_pair(q, r)
|
| 83 |
+
m = match_file[pair]["matches0"].__array__()
|
| 84 |
+
v = m > -1
|
| 85 |
+
|
| 86 |
+
if skip and (np.count_nonzero(v) < skip):
|
| 87 |
+
continue
|
| 88 |
+
|
| 89 |
+
mkpq, mkpr = kpq[v], kpr[m[v]]
|
| 90 |
+
num_matches += len(mkpq)
|
| 91 |
+
|
| 92 |
+
scan_r = loadmat(Path(dataset_dir, r + ".mat"))["XYZcut"]
|
| 93 |
+
mkp3d, valid = interpolate_scan(scan_r, mkpr)
|
| 94 |
+
Tr = get_scan_pose(dataset_dir, r)
|
| 95 |
+
mkp3d = (Tr[:3, :3] @ mkp3d.T + Tr[:3, -1:]).T
|
| 96 |
+
|
| 97 |
+
all_mkpq.append(mkpq[valid])
|
| 98 |
+
all_mkpr.append(mkpr[valid])
|
| 99 |
+
all_mkp3d.append(mkp3d[valid])
|
| 100 |
+
all_indices.append(np.full(np.count_nonzero(valid), i))
|
| 101 |
+
|
| 102 |
+
all_mkpq = np.concatenate(all_mkpq, 0)
|
| 103 |
+
all_mkpr = np.concatenate(all_mkpr, 0)
|
| 104 |
+
all_mkp3d = np.concatenate(all_mkp3d, 0)
|
| 105 |
+
all_indices = np.concatenate(all_indices, 0)
|
| 106 |
+
|
| 107 |
+
cfg = {
|
| 108 |
+
"model": "SIMPLE_PINHOLE",
|
| 109 |
+
"width": width,
|
| 110 |
+
"height": height,
|
| 111 |
+
"params": [focal_length, cx, cy],
|
| 112 |
+
}
|
| 113 |
+
ret = pycolmap.absolute_pose_estimation(all_mkpq, all_mkp3d, cfg, 48.00)
|
| 114 |
+
ret["cfg"] = cfg
|
| 115 |
+
return ret, all_mkpq, all_mkpr, all_mkp3d, all_indices, num_matches
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def main(dataset_dir, retrieval, features, matches, results, skip_matches=None):
|
| 119 |
+
assert retrieval.exists(), retrieval
|
| 120 |
+
assert features.exists(), features
|
| 121 |
+
assert matches.exists(), matches
|
| 122 |
+
|
| 123 |
+
retrieval_dict = parse_retrieval(retrieval)
|
| 124 |
+
queries = list(retrieval_dict.keys())
|
| 125 |
+
|
| 126 |
+
feature_file = h5py.File(features, "r", libver="latest")
|
| 127 |
+
match_file = h5py.File(matches, "r", libver="latest")
|
| 128 |
+
|
| 129 |
+
poses = {}
|
| 130 |
+
logs = {
|
| 131 |
+
"features": features,
|
| 132 |
+
"matches": matches,
|
| 133 |
+
"retrieval": retrieval,
|
| 134 |
+
"loc": {},
|
| 135 |
+
}
|
| 136 |
+
logger.info("Starting localization...")
|
| 137 |
+
for q in tqdm(queries):
|
| 138 |
+
db = retrieval_dict[q]
|
| 139 |
+
ret, mkpq, mkpr, mkp3d, indices, num_matches = pose_from_cluster(
|
| 140 |
+
dataset_dir, q, db, feature_file, match_file, skip_matches
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
poses[q] = (ret["qvec"], ret["tvec"])
|
| 144 |
+
logs["loc"][q] = {
|
| 145 |
+
"db": db,
|
| 146 |
+
"PnP_ret": ret,
|
| 147 |
+
"keypoints_query": mkpq,
|
| 148 |
+
"keypoints_db": mkpr,
|
| 149 |
+
"3d_points": mkp3d,
|
| 150 |
+
"indices_db": indices,
|
| 151 |
+
"num_matches": num_matches,
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
logger.info(f"Writing poses to {results}...")
|
| 155 |
+
with open(results, "w") as f:
|
| 156 |
+
for q in queries:
|
| 157 |
+
qvec, tvec = poses[q]
|
| 158 |
+
qvec = " ".join(map(str, qvec))
|
| 159 |
+
tvec = " ".join(map(str, tvec))
|
| 160 |
+
name = q.split("/")[-1]
|
| 161 |
+
f.write(f"{name} {qvec} {tvec}\n")
|
| 162 |
+
|
| 163 |
+
logs_path = f"{results}_logs.pkl"
|
| 164 |
+
logger.info(f"Writing logs to {logs_path}...")
|
| 165 |
+
with open(logs_path, "wb") as f:
|
| 166 |
+
pickle.dump(logs, f)
|
| 167 |
+
logger.info("Done!")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
if __name__ == "__main__":
|
| 171 |
+
parser = argparse.ArgumentParser()
|
| 172 |
+
parser.add_argument("--dataset_dir", type=Path, required=True)
|
| 173 |
+
parser.add_argument("--retrieval", type=Path, required=True)
|
| 174 |
+
parser.add_argument("--features", type=Path, required=True)
|
| 175 |
+
parser.add_argument("--matches", type=Path, required=True)
|
| 176 |
+
parser.add_argument("--results", type=Path, required=True)
|
| 177 |
+
parser.add_argument("--skip_matches", type=int)
|
| 178 |
+
args = parser.parse_args()
|
| 179 |
+
main(**args.__dict__)
|
hloc/localize_sfm.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import pickle
|
| 3 |
+
from collections import defaultdict
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List, Union
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pycolmap
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
|
| 11 |
+
from . import logger
|
| 12 |
+
from .utils.io import get_keypoints, get_matches
|
| 13 |
+
from .utils.parsers import parse_image_lists, parse_retrieval
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def do_covisibility_clustering(
|
| 17 |
+
frame_ids: List[int], reconstruction: pycolmap.Reconstruction
|
| 18 |
+
):
|
| 19 |
+
clusters = []
|
| 20 |
+
visited = set()
|
| 21 |
+
for frame_id in frame_ids:
|
| 22 |
+
# Check if already labeled
|
| 23 |
+
if frame_id in visited:
|
| 24 |
+
continue
|
| 25 |
+
|
| 26 |
+
# New component
|
| 27 |
+
clusters.append([])
|
| 28 |
+
queue = {frame_id}
|
| 29 |
+
while len(queue):
|
| 30 |
+
exploration_frame = queue.pop()
|
| 31 |
+
|
| 32 |
+
# Already part of the component
|
| 33 |
+
if exploration_frame in visited:
|
| 34 |
+
continue
|
| 35 |
+
visited.add(exploration_frame)
|
| 36 |
+
clusters[-1].append(exploration_frame)
|
| 37 |
+
|
| 38 |
+
observed = reconstruction.images[exploration_frame].points2D
|
| 39 |
+
connected_frames = {
|
| 40 |
+
obs.image_id
|
| 41 |
+
for p2D in observed
|
| 42 |
+
if p2D.has_point3D()
|
| 43 |
+
for obs in reconstruction.points3D[p2D.point3D_id].track.elements
|
| 44 |
+
}
|
| 45 |
+
connected_frames &= set(frame_ids)
|
| 46 |
+
connected_frames -= visited
|
| 47 |
+
queue |= connected_frames
|
| 48 |
+
|
| 49 |
+
clusters = sorted(clusters, key=len, reverse=True)
|
| 50 |
+
return clusters
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class QueryLocalizer:
|
| 54 |
+
def __init__(self, reconstruction, config=None):
|
| 55 |
+
self.reconstruction = reconstruction
|
| 56 |
+
self.config = config or {}
|
| 57 |
+
|
| 58 |
+
def localize(self, points2D_all, points2D_idxs, points3D_id, query_camera):
|
| 59 |
+
points2D = points2D_all[points2D_idxs]
|
| 60 |
+
points3D = [self.reconstruction.points3D[j].xyz for j in points3D_id]
|
| 61 |
+
ret = pycolmap.absolute_pose_estimation(
|
| 62 |
+
points2D,
|
| 63 |
+
points3D,
|
| 64 |
+
query_camera,
|
| 65 |
+
estimation_options=self.config.get("estimation", {}),
|
| 66 |
+
refinement_options=self.config.get("refinement", {}),
|
| 67 |
+
)
|
| 68 |
+
return ret
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def pose_from_cluster(
|
| 72 |
+
localizer: QueryLocalizer,
|
| 73 |
+
qname: str,
|
| 74 |
+
query_camera: pycolmap.Camera,
|
| 75 |
+
db_ids: List[int],
|
| 76 |
+
features_path: Path,
|
| 77 |
+
matches_path: Path,
|
| 78 |
+
**kwargs,
|
| 79 |
+
):
|
| 80 |
+
kpq = get_keypoints(features_path, qname)
|
| 81 |
+
kpq += 0.5 # COLMAP coordinates
|
| 82 |
+
|
| 83 |
+
kp_idx_to_3D = defaultdict(list)
|
| 84 |
+
kp_idx_to_3D_to_db = defaultdict(lambda: defaultdict(list))
|
| 85 |
+
num_matches = 0
|
| 86 |
+
for i, db_id in enumerate(db_ids):
|
| 87 |
+
image = localizer.reconstruction.images[db_id]
|
| 88 |
+
if image.num_points3D == 0:
|
| 89 |
+
logger.debug(f"No 3D points found for {image.name}.")
|
| 90 |
+
continue
|
| 91 |
+
points3D_ids = np.array(
|
| 92 |
+
[p.point3D_id if p.has_point3D() else -1 for p in image.points2D]
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
matches, _ = get_matches(matches_path, qname, image.name)
|
| 96 |
+
matches = matches[points3D_ids[matches[:, 1]] != -1]
|
| 97 |
+
num_matches += len(matches)
|
| 98 |
+
for idx, m in matches:
|
| 99 |
+
id_3D = points3D_ids[m]
|
| 100 |
+
kp_idx_to_3D_to_db[idx][id_3D].append(i)
|
| 101 |
+
# avoid duplicate observations
|
| 102 |
+
if id_3D not in kp_idx_to_3D[idx]:
|
| 103 |
+
kp_idx_to_3D[idx].append(id_3D)
|
| 104 |
+
|
| 105 |
+
idxs = list(kp_idx_to_3D.keys())
|
| 106 |
+
mkp_idxs = [i for i in idxs for _ in kp_idx_to_3D[i]]
|
| 107 |
+
mp3d_ids = [j for i in idxs for j in kp_idx_to_3D[i]]
|
| 108 |
+
ret = localizer.localize(kpq, mkp_idxs, mp3d_ids, query_camera, **kwargs)
|
| 109 |
+
if ret is not None:
|
| 110 |
+
ret["camera"] = query_camera
|
| 111 |
+
|
| 112 |
+
# mostly for logging and post-processing
|
| 113 |
+
mkp_to_3D_to_db = [
|
| 114 |
+
(j, kp_idx_to_3D_to_db[i][j]) for i in idxs for j in kp_idx_to_3D[i]
|
| 115 |
+
]
|
| 116 |
+
log = {
|
| 117 |
+
"db": db_ids,
|
| 118 |
+
"PnP_ret": ret,
|
| 119 |
+
"keypoints_query": kpq[mkp_idxs],
|
| 120 |
+
"points3D_ids": mp3d_ids,
|
| 121 |
+
"points3D_xyz": None, # we don't log xyz anymore because of file size
|
| 122 |
+
"num_matches": num_matches,
|
| 123 |
+
"keypoint_index_to_db": (mkp_idxs, mkp_to_3D_to_db),
|
| 124 |
+
}
|
| 125 |
+
return ret, log
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def main(
|
| 129 |
+
reference_sfm: Union[Path, pycolmap.Reconstruction],
|
| 130 |
+
queries: Path,
|
| 131 |
+
retrieval: Path,
|
| 132 |
+
features: Path,
|
| 133 |
+
matches: Path,
|
| 134 |
+
results: Path,
|
| 135 |
+
ransac_thresh: int = 12,
|
| 136 |
+
covisibility_clustering: bool = False,
|
| 137 |
+
prepend_camera_name: bool = False,
|
| 138 |
+
config: Dict = None,
|
| 139 |
+
):
|
| 140 |
+
assert retrieval.exists(), retrieval
|
| 141 |
+
assert features.exists(), features
|
| 142 |
+
assert matches.exists(), matches
|
| 143 |
+
|
| 144 |
+
queries = parse_image_lists(queries, with_intrinsics=True)
|
| 145 |
+
retrieval_dict = parse_retrieval(retrieval)
|
| 146 |
+
|
| 147 |
+
logger.info("Reading the 3D model...")
|
| 148 |
+
if not isinstance(reference_sfm, pycolmap.Reconstruction):
|
| 149 |
+
reference_sfm = pycolmap.Reconstruction(reference_sfm)
|
| 150 |
+
db_name_to_id = {img.name: i for i, img in reference_sfm.images.items()}
|
| 151 |
+
|
| 152 |
+
config = {"estimation": {"ransac": {"max_error": ransac_thresh}}, **(config or {})}
|
| 153 |
+
localizer = QueryLocalizer(reference_sfm, config)
|
| 154 |
+
|
| 155 |
+
cam_from_world = {}
|
| 156 |
+
logs = {
|
| 157 |
+
"features": features,
|
| 158 |
+
"matches": matches,
|
| 159 |
+
"retrieval": retrieval,
|
| 160 |
+
"loc": {},
|
| 161 |
+
}
|
| 162 |
+
logger.info("Starting localization...")
|
| 163 |
+
for qname, qcam in tqdm(queries):
|
| 164 |
+
if qname not in retrieval_dict:
|
| 165 |
+
logger.warning(f"No images retrieved for query image {qname}. Skipping...")
|
| 166 |
+
continue
|
| 167 |
+
db_names = retrieval_dict[qname]
|
| 168 |
+
db_ids = []
|
| 169 |
+
for n in db_names:
|
| 170 |
+
if n not in db_name_to_id:
|
| 171 |
+
logger.warning(f"Image {n} was retrieved but not in database")
|
| 172 |
+
continue
|
| 173 |
+
db_ids.append(db_name_to_id[n])
|
| 174 |
+
|
| 175 |
+
if covisibility_clustering:
|
| 176 |
+
clusters = do_covisibility_clustering(db_ids, reference_sfm)
|
| 177 |
+
best_inliers = 0
|
| 178 |
+
best_cluster = None
|
| 179 |
+
logs_clusters = []
|
| 180 |
+
for i, cluster_ids in enumerate(clusters):
|
| 181 |
+
ret, log = pose_from_cluster(
|
| 182 |
+
localizer, qname, qcam, cluster_ids, features, matches
|
| 183 |
+
)
|
| 184 |
+
if ret is not None and ret["num_inliers"] > best_inliers:
|
| 185 |
+
best_cluster = i
|
| 186 |
+
best_inliers = ret["num_inliers"]
|
| 187 |
+
logs_clusters.append(log)
|
| 188 |
+
if best_cluster is not None:
|
| 189 |
+
ret = logs_clusters[best_cluster]["PnP_ret"]
|
| 190 |
+
cam_from_world[qname] = ret["cam_from_world"]
|
| 191 |
+
logs["loc"][qname] = {
|
| 192 |
+
"db": db_ids,
|
| 193 |
+
"best_cluster": best_cluster,
|
| 194 |
+
"log_clusters": logs_clusters,
|
| 195 |
+
"covisibility_clustering": covisibility_clustering,
|
| 196 |
+
}
|
| 197 |
+
else:
|
| 198 |
+
ret, log = pose_from_cluster(
|
| 199 |
+
localizer, qname, qcam, db_ids, features, matches
|
| 200 |
+
)
|
| 201 |
+
if ret is not None:
|
| 202 |
+
cam_from_world[qname] = ret["cam_from_world"]
|
| 203 |
+
else:
|
| 204 |
+
closest = reference_sfm.images[db_ids[0]]
|
| 205 |
+
cam_from_world[qname] = closest.cam_from_world
|
| 206 |
+
log["covisibility_clustering"] = covisibility_clustering
|
| 207 |
+
logs["loc"][qname] = log
|
| 208 |
+
|
| 209 |
+
logger.info(f"Localized {len(cam_from_world)} / {len(queries)} images.")
|
| 210 |
+
logger.info(f"Writing poses to {results}...")
|
| 211 |
+
with open(results, "w") as f:
|
| 212 |
+
for query, t in cam_from_world.items():
|
| 213 |
+
qvec = " ".join(map(str, t.rotation.quat[[3, 0, 1, 2]]))
|
| 214 |
+
tvec = " ".join(map(str, t.translation))
|
| 215 |
+
name = query.split("/")[-1]
|
| 216 |
+
if prepend_camera_name:
|
| 217 |
+
name = query.split("/")[-2] + "/" + name
|
| 218 |
+
f.write(f"{name} {qvec} {tvec}\n")
|
| 219 |
+
|
| 220 |
+
logs_path = f"{results}_logs.pkl"
|
| 221 |
+
logger.info(f"Writing logs to {logs_path}...")
|
| 222 |
+
# TODO: Resolve pickling issue with pycolmap objects.
|
| 223 |
+
with open(logs_path, "wb") as f:
|
| 224 |
+
pickle.dump(logs, f)
|
| 225 |
+
logger.info("Done!")
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
if __name__ == "__main__":
|
| 229 |
+
parser = argparse.ArgumentParser()
|
| 230 |
+
parser.add_argument("--reference_sfm", type=Path, required=True)
|
| 231 |
+
parser.add_argument("--queries", type=Path, required=True)
|
| 232 |
+
parser.add_argument("--features", type=Path, required=True)
|
| 233 |
+
parser.add_argument("--matches", type=Path, required=True)
|
| 234 |
+
parser.add_argument("--retrieval", type=Path, required=True)
|
| 235 |
+
parser.add_argument("--results", type=Path, required=True)
|
| 236 |
+
parser.add_argument("--ransac_thresh", type=float, default=12.0)
|
| 237 |
+
parser.add_argument("--covisibility_clustering", action="store_true")
|
| 238 |
+
parser.add_argument("--prepend_camera_name", action="store_true")
|
| 239 |
+
args = parser.parse_args()
|
| 240 |
+
main(**args.__dict__)
|
hloc/match_dense.py
CHANGED
|
@@ -275,12 +275,473 @@ confs = {
|
|
| 275 |
},
|
| 276 |
}
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
|
| 279 |
def scale_keypoints(kpts, scale):
|
| 280 |
if np.any(scale != 1.0):
|
| 281 |
kpts *= kpts.new_tensor(scale)
|
| 282 |
return kpts
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
|
| 285 |
def scale_lines(lines, scale):
|
| 286 |
if np.any(scale != 1.0):
|
|
@@ -497,3 +958,75 @@ def match_images(model, image_0, image_1, conf, device="cpu"):
|
|
| 497 |
del pred
|
| 498 |
torch.cuda.empty_cache()
|
| 499 |
return ret
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
},
|
| 276 |
}
|
| 277 |
|
| 278 |
+
def to_cpts(kpts, ps):
|
| 279 |
+
if ps > 0.0:
|
| 280 |
+
kpts = np.round(np.round((kpts + 0.5) / ps) * ps - 0.5, 2)
|
| 281 |
+
return [tuple(cpt) for cpt in kpts]
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def assign_keypoints(
|
| 285 |
+
kpts: np.ndarray,
|
| 286 |
+
other_cpts: Union[List[Tuple], np.ndarray],
|
| 287 |
+
max_error: float,
|
| 288 |
+
update: bool = False,
|
| 289 |
+
ref_bins: Optional[List[Counter]] = None,
|
| 290 |
+
scores: Optional[np.ndarray] = None,
|
| 291 |
+
cell_size: Optional[int] = None,
|
| 292 |
+
):
|
| 293 |
+
if not update:
|
| 294 |
+
# Without update this is just a NN search
|
| 295 |
+
if len(other_cpts) == 0 or len(kpts) == 0:
|
| 296 |
+
return np.full(len(kpts), -1)
|
| 297 |
+
dist, kpt_ids = KDTree(np.array(other_cpts)).query(kpts)
|
| 298 |
+
valid = dist <= max_error
|
| 299 |
+
kpt_ids[~valid] = -1
|
| 300 |
+
return kpt_ids
|
| 301 |
+
else:
|
| 302 |
+
ps = cell_size if cell_size is not None else max_error
|
| 303 |
+
ps = max(ps, max_error)
|
| 304 |
+
# With update we quantize and bin (optionally)
|
| 305 |
+
assert isinstance(other_cpts, list)
|
| 306 |
+
kpt_ids = []
|
| 307 |
+
cpts = to_cpts(kpts, ps)
|
| 308 |
+
bpts = to_cpts(kpts, int(max_error))
|
| 309 |
+
cp_to_id = {val: i for i, val in enumerate(other_cpts)}
|
| 310 |
+
for i, (cpt, bpt) in enumerate(zip(cpts, bpts)):
|
| 311 |
+
try:
|
| 312 |
+
kid = cp_to_id[cpt]
|
| 313 |
+
except KeyError:
|
| 314 |
+
kid = len(cp_to_id)
|
| 315 |
+
cp_to_id[cpt] = kid
|
| 316 |
+
other_cpts.append(cpt)
|
| 317 |
+
if ref_bins is not None:
|
| 318 |
+
ref_bins.append(Counter())
|
| 319 |
+
if ref_bins is not None:
|
| 320 |
+
score = scores[i] if scores is not None else 1
|
| 321 |
+
ref_bins[cp_to_id[cpt]][bpt] += score
|
| 322 |
+
kpt_ids.append(kid)
|
| 323 |
+
return np.array(kpt_ids)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def get_grouped_ids(array):
|
| 327 |
+
# Group array indices based on its values
|
| 328 |
+
# all duplicates are grouped as a set
|
| 329 |
+
idx_sort = np.argsort(array)
|
| 330 |
+
sorted_array = array[idx_sort]
|
| 331 |
+
_, ids, _ = np.unique(sorted_array, return_counts=True, return_index=True)
|
| 332 |
+
res = np.split(idx_sort, ids[1:])
|
| 333 |
+
return res
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def get_unique_matches(match_ids, scores):
|
| 337 |
+
if len(match_ids.shape) == 1:
|
| 338 |
+
return [0]
|
| 339 |
+
|
| 340 |
+
isets1 = get_grouped_ids(match_ids[:, 0])
|
| 341 |
+
isets2 = get_grouped_ids(match_ids[:, 1])
|
| 342 |
+
uid1s = [ids[scores[ids].argmax()] for ids in isets1 if len(ids) > 0]
|
| 343 |
+
uid2s = [ids[scores[ids].argmax()] for ids in isets2 if len(ids) > 0]
|
| 344 |
+
uids = list(set(uid1s).intersection(uid2s))
|
| 345 |
+
return match_ids[uids], scores[uids]
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def matches_to_matches0(matches, scores):
|
| 349 |
+
if len(matches) == 0:
|
| 350 |
+
return np.zeros(0, dtype=np.int32), np.zeros(0, dtype=np.float16)
|
| 351 |
+
n_kps0 = np.max(matches[:, 0]) + 1
|
| 352 |
+
matches0 = -np.ones((n_kps0,))
|
| 353 |
+
scores0 = np.zeros((n_kps0,))
|
| 354 |
+
matches0[matches[:, 0]] = matches[:, 1]
|
| 355 |
+
scores0[matches[:, 0]] = scores
|
| 356 |
+
return matches0.astype(np.int32), scores0.astype(np.float16)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def kpids_to_matches0(kpt_ids0, kpt_ids1, scores):
|
| 360 |
+
valid = (kpt_ids0 != -1) & (kpt_ids1 != -1)
|
| 361 |
+
matches = np.dstack([kpt_ids0[valid], kpt_ids1[valid]])
|
| 362 |
+
matches = matches.reshape(-1, 2)
|
| 363 |
+
scores = scores[valid]
|
| 364 |
+
|
| 365 |
+
# Remove n-to-1 matches
|
| 366 |
+
matches, scores = get_unique_matches(matches, scores)
|
| 367 |
+
return matches_to_matches0(matches, scores)
|
| 368 |
|
| 369 |
def scale_keypoints(kpts, scale):
|
| 370 |
if np.any(scale != 1.0):
|
| 371 |
kpts *= kpts.new_tensor(scale)
|
| 372 |
return kpts
|
| 373 |
|
| 374 |
+
class ImagePairDataset(torch.utils.data.Dataset):
|
| 375 |
+
default_conf = {
|
| 376 |
+
"grayscale": True,
|
| 377 |
+
"resize_max": 1024,
|
| 378 |
+
"dfactor": 8,
|
| 379 |
+
"cache_images": False,
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
def __init__(self, image_dir, conf, pairs):
|
| 383 |
+
self.image_dir = image_dir
|
| 384 |
+
self.conf = conf = SimpleNamespace(**{**self.default_conf, **conf})
|
| 385 |
+
self.pairs = pairs
|
| 386 |
+
if self.conf.cache_images:
|
| 387 |
+
image_names = set(sum(pairs, ())) # unique image names in pairs
|
| 388 |
+
logger.info(f"Loading and caching {len(image_names)} unique images.")
|
| 389 |
+
self.images = {}
|
| 390 |
+
self.scales = {}
|
| 391 |
+
for name in tqdm(image_names):
|
| 392 |
+
image = read_image(self.image_dir / name, self.conf.grayscale)
|
| 393 |
+
self.images[name], self.scales[name] = self.preprocess(image)
|
| 394 |
+
|
| 395 |
+
def preprocess(self, image: np.ndarray):
|
| 396 |
+
image = image.astype(np.float32, copy=False)
|
| 397 |
+
size = image.shape[:2][::-1]
|
| 398 |
+
scale = np.array([1.0, 1.0])
|
| 399 |
+
|
| 400 |
+
if self.conf.resize_max:
|
| 401 |
+
scale = self.conf.resize_max / max(size)
|
| 402 |
+
if scale < 1.0:
|
| 403 |
+
size_new = tuple(int(round(x * scale)) for x in size)
|
| 404 |
+
image = resize_image(image, size_new, "cv2_area")
|
| 405 |
+
scale = np.array(size) / np.array(size_new)
|
| 406 |
+
|
| 407 |
+
if self.conf.grayscale:
|
| 408 |
+
assert image.ndim == 2, image.shape
|
| 409 |
+
image = image[None]
|
| 410 |
+
else:
|
| 411 |
+
image = image.transpose((2, 0, 1)) # HxWxC to CxHxW
|
| 412 |
+
image = torch.from_numpy(image / 255.0).float()
|
| 413 |
+
|
| 414 |
+
# assure that the size is divisible by dfactor
|
| 415 |
+
size_new = tuple(
|
| 416 |
+
map(
|
| 417 |
+
lambda x: int(x // self.conf.dfactor * self.conf.dfactor),
|
| 418 |
+
image.shape[-2:],
|
| 419 |
+
)
|
| 420 |
+
)
|
| 421 |
+
image = F.resize(image, size=size_new)
|
| 422 |
+
scale = np.array(size) / np.array(size_new)[::-1]
|
| 423 |
+
return image, scale
|
| 424 |
+
|
| 425 |
+
def __len__(self):
|
| 426 |
+
return len(self.pairs)
|
| 427 |
+
|
| 428 |
+
def __getitem__(self, idx):
|
| 429 |
+
name0, name1 = self.pairs[idx]
|
| 430 |
+
if self.conf.cache_images:
|
| 431 |
+
image0, scale0 = self.images[name0], self.scales[name0]
|
| 432 |
+
image1, scale1 = self.images[name1], self.scales[name1]
|
| 433 |
+
else:
|
| 434 |
+
image0 = read_image(self.image_dir / name0, self.conf.grayscale)
|
| 435 |
+
image1 = read_image(self.image_dir / name1, self.conf.grayscale)
|
| 436 |
+
image0, scale0 = self.preprocess(image0)
|
| 437 |
+
image1, scale1 = self.preprocess(image1)
|
| 438 |
+
return image0, image1, scale0, scale1, name0, name1
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
@torch.no_grad()
|
| 442 |
+
def match_dense(
|
| 443 |
+
conf: Dict,
|
| 444 |
+
pairs: List[Tuple[str, str]],
|
| 445 |
+
image_dir: Path,
|
| 446 |
+
match_path: Path, # out
|
| 447 |
+
existing_refs: Optional[List] = [],
|
| 448 |
+
):
|
| 449 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 450 |
+
Model = dynamic_load(matchers, conf["model"]["name"])
|
| 451 |
+
model = Model(conf["model"]).eval().to(device)
|
| 452 |
+
|
| 453 |
+
dataset = ImagePairDataset(image_dir, conf["preprocessing"], pairs)
|
| 454 |
+
loader = torch.utils.data.DataLoader(
|
| 455 |
+
dataset, num_workers=16, batch_size=1, shuffle=False
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
logger.info("Performing dense matching...")
|
| 459 |
+
with h5py.File(str(match_path), "a") as fd:
|
| 460 |
+
for data in tqdm(loader, smoothing=0.1):
|
| 461 |
+
# load image-pair data
|
| 462 |
+
image0, image1, scale0, scale1, (name0,), (name1,) = data
|
| 463 |
+
scale0, scale1 = scale0[0].numpy(), scale1[0].numpy()
|
| 464 |
+
image0, image1 = image0.to(device), image1.to(device)
|
| 465 |
+
|
| 466 |
+
# match semi-dense
|
| 467 |
+
# for consistency with pairs_from_*: refine kpts of image0
|
| 468 |
+
if name0 in existing_refs:
|
| 469 |
+
# special case: flip to enable refinement in query image
|
| 470 |
+
pred = model({"image0": image1, "image1": image0})
|
| 471 |
+
pred = {
|
| 472 |
+
**pred,
|
| 473 |
+
"keypoints0": pred["keypoints1"],
|
| 474 |
+
"keypoints1": pred["keypoints0"],
|
| 475 |
+
}
|
| 476 |
+
else:
|
| 477 |
+
# usual case
|
| 478 |
+
pred = model({"image0": image0, "image1": image1})
|
| 479 |
+
|
| 480 |
+
# Rescale keypoints and move to cpu
|
| 481 |
+
kpts0, kpts1 = pred["keypoints0"], pred["keypoints1"]
|
| 482 |
+
kpts0 = scale_keypoints(kpts0 + 0.5, scale0) - 0.5
|
| 483 |
+
kpts1 = scale_keypoints(kpts1 + 0.5, scale1) - 0.5
|
| 484 |
+
kpts0 = kpts0.cpu().numpy()
|
| 485 |
+
kpts1 = kpts1.cpu().numpy()
|
| 486 |
+
scores = pred["scores"].cpu().numpy()
|
| 487 |
+
|
| 488 |
+
# Write matches and matching scores in hloc format
|
| 489 |
+
pair = names_to_pair(name0, name1)
|
| 490 |
+
if pair in fd:
|
| 491 |
+
del fd[pair]
|
| 492 |
+
grp = fd.create_group(pair)
|
| 493 |
+
|
| 494 |
+
# Write dense matching output
|
| 495 |
+
grp.create_dataset("keypoints0", data=kpts0)
|
| 496 |
+
grp.create_dataset("keypoints1", data=kpts1)
|
| 497 |
+
grp.create_dataset("scores", data=scores)
|
| 498 |
+
del model, loader
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
# default: quantize all!
|
| 502 |
+
def load_keypoints(
|
| 503 |
+
conf: Dict, feature_paths_refs: List[Path], quantize: Optional[set] = None
|
| 504 |
+
):
|
| 505 |
+
name2ref = {
|
| 506 |
+
n: i for i, p in enumerate(feature_paths_refs) for n in list_h5_names(p)
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
existing_refs = set(name2ref.keys())
|
| 510 |
+
if quantize is None:
|
| 511 |
+
quantize = existing_refs # quantize all
|
| 512 |
+
if len(existing_refs) > 0:
|
| 513 |
+
logger.info(f"Loading keypoints from {len(existing_refs)} images.")
|
| 514 |
+
|
| 515 |
+
# Load query keypoints
|
| 516 |
+
cpdict = defaultdict(list)
|
| 517 |
+
bindict = defaultdict(list)
|
| 518 |
+
for name in existing_refs:
|
| 519 |
+
with h5py.File(str(feature_paths_refs[name2ref[name]]), "r") as fd:
|
| 520 |
+
kps = fd[name]["keypoints"].__array__()
|
| 521 |
+
if name not in quantize:
|
| 522 |
+
cpdict[name] = kps
|
| 523 |
+
else:
|
| 524 |
+
if "scores" in fd[name].keys():
|
| 525 |
+
kp_scores = fd[name]["scores"].__array__()
|
| 526 |
+
else:
|
| 527 |
+
# we set the score to 1.0 if not provided
|
| 528 |
+
# increase for more weight on reference keypoints for
|
| 529 |
+
# stronger anchoring
|
| 530 |
+
kp_scores = [1.0 for _ in range(kps.shape[0])]
|
| 531 |
+
# bin existing keypoints of reference images for association
|
| 532 |
+
assign_keypoints(
|
| 533 |
+
kps,
|
| 534 |
+
cpdict[name],
|
| 535 |
+
conf["max_error"],
|
| 536 |
+
True,
|
| 537 |
+
bindict[name],
|
| 538 |
+
kp_scores,
|
| 539 |
+
conf["cell_size"],
|
| 540 |
+
)
|
| 541 |
+
return cpdict, bindict
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
def aggregate_matches(
|
| 545 |
+
conf: Dict,
|
| 546 |
+
pairs: List[Tuple[str, str]],
|
| 547 |
+
match_path: Path,
|
| 548 |
+
feature_path: Path,
|
| 549 |
+
required_queries: Optional[Set[str]] = None,
|
| 550 |
+
max_kps: Optional[int] = None,
|
| 551 |
+
cpdict: Dict[str, Iterable] = defaultdict(list),
|
| 552 |
+
bindict: Dict[str, List[Counter]] = defaultdict(list),
|
| 553 |
+
):
|
| 554 |
+
if required_queries is None:
|
| 555 |
+
required_queries = set(sum(pairs, ()))
|
| 556 |
+
# default: do not overwrite existing features in feature_path!
|
| 557 |
+
required_queries -= set(list_h5_names(feature_path))
|
| 558 |
+
|
| 559 |
+
# if an entry in cpdict is provided as np.ndarray we assume it is fixed
|
| 560 |
+
required_queries -= set([k for k, v in cpdict.items() if isinstance(v, np.ndarray)])
|
| 561 |
+
|
| 562 |
+
# sort pairs for reduced RAM
|
| 563 |
+
pairs_per_q = Counter(list(chain(*pairs)))
|
| 564 |
+
pairs_score = [min(pairs_per_q[i], pairs_per_q[j]) for i, j in pairs]
|
| 565 |
+
pairs = [p for _, p in sorted(zip(pairs_score, pairs))]
|
| 566 |
+
|
| 567 |
+
if len(required_queries) > 0:
|
| 568 |
+
logger.info(f"Aggregating keypoints for {len(required_queries)} images.")
|
| 569 |
+
n_kps = 0
|
| 570 |
+
with h5py.File(str(match_path), "a") as fd:
|
| 571 |
+
for name0, name1 in tqdm(pairs, smoothing=0.1):
|
| 572 |
+
pair = names_to_pair(name0, name1)
|
| 573 |
+
grp = fd[pair]
|
| 574 |
+
kpts0 = grp["keypoints0"].__array__()
|
| 575 |
+
kpts1 = grp["keypoints1"].__array__()
|
| 576 |
+
scores = grp["scores"].__array__()
|
| 577 |
+
|
| 578 |
+
# Aggregate local features
|
| 579 |
+
update0 = name0 in required_queries
|
| 580 |
+
update1 = name1 in required_queries
|
| 581 |
+
|
| 582 |
+
# in localization we do not want to bin the query kp
|
| 583 |
+
# assumes that the query is name0!
|
| 584 |
+
if update0 and not update1 and max_kps is None:
|
| 585 |
+
max_error0 = cell_size0 = 0.0
|
| 586 |
+
else:
|
| 587 |
+
max_error0 = conf["max_error"]
|
| 588 |
+
cell_size0 = conf["cell_size"]
|
| 589 |
+
|
| 590 |
+
# Get match ids and extend query keypoints (cpdict)
|
| 591 |
+
mkp_ids0 = assign_keypoints(
|
| 592 |
+
kpts0,
|
| 593 |
+
cpdict[name0],
|
| 594 |
+
max_error0,
|
| 595 |
+
update0,
|
| 596 |
+
bindict[name0],
|
| 597 |
+
scores,
|
| 598 |
+
cell_size0,
|
| 599 |
+
)
|
| 600 |
+
mkp_ids1 = assign_keypoints(
|
| 601 |
+
kpts1,
|
| 602 |
+
cpdict[name1],
|
| 603 |
+
conf["max_error"],
|
| 604 |
+
update1,
|
| 605 |
+
bindict[name1],
|
| 606 |
+
scores,
|
| 607 |
+
conf["cell_size"],
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
# Build matches from assignments
|
| 611 |
+
matches0, scores0 = kpids_to_matches0(mkp_ids0, mkp_ids1, scores)
|
| 612 |
+
|
| 613 |
+
assert kpts0.shape[0] == scores.shape[0]
|
| 614 |
+
grp.create_dataset("matches0", data=matches0)
|
| 615 |
+
grp.create_dataset("matching_scores0", data=scores0)
|
| 616 |
+
|
| 617 |
+
# Convert bins to kps if finished, and store them
|
| 618 |
+
for name in (name0, name1):
|
| 619 |
+
pairs_per_q[name] -= 1
|
| 620 |
+
if pairs_per_q[name] > 0 or name not in required_queries:
|
| 621 |
+
continue
|
| 622 |
+
kp_score = [c.most_common(1)[0][1] for c in bindict[name]]
|
| 623 |
+
cpdict[name] = [c.most_common(1)[0][0] for c in bindict[name]]
|
| 624 |
+
cpdict[name] = np.array(cpdict[name], dtype=np.float32)
|
| 625 |
+
|
| 626 |
+
# Select top-k query kps by score (reassign matches later)
|
| 627 |
+
if max_kps:
|
| 628 |
+
top_k = min(max_kps, cpdict[name].shape[0])
|
| 629 |
+
top_k = np.argsort(kp_score)[::-1][:top_k]
|
| 630 |
+
cpdict[name] = cpdict[name][top_k]
|
| 631 |
+
kp_score = np.array(kp_score)[top_k]
|
| 632 |
+
|
| 633 |
+
# Write query keypoints
|
| 634 |
+
with h5py.File(feature_path, "a") as kfd:
|
| 635 |
+
if name in kfd:
|
| 636 |
+
del kfd[name]
|
| 637 |
+
kgrp = kfd.create_group(name)
|
| 638 |
+
kgrp.create_dataset("keypoints", data=cpdict[name])
|
| 639 |
+
kgrp.create_dataset("score", data=kp_score)
|
| 640 |
+
n_kps += cpdict[name].shape[0]
|
| 641 |
+
del bindict[name]
|
| 642 |
+
|
| 643 |
+
if len(required_queries) > 0:
|
| 644 |
+
avg_kp_per_image = round(n_kps / len(required_queries), 1)
|
| 645 |
+
logger.info(
|
| 646 |
+
f"Finished assignment, found {avg_kp_per_image} "
|
| 647 |
+
f"keypoints/image (avg.), total {n_kps}."
|
| 648 |
+
)
|
| 649 |
+
return cpdict
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
def assign_matches(
|
| 653 |
+
pairs: List[Tuple[str, str]],
|
| 654 |
+
match_path: Path,
|
| 655 |
+
keypoints: Union[List[Path], Dict[str, np.array]],
|
| 656 |
+
max_error: float,
|
| 657 |
+
):
|
| 658 |
+
if isinstance(keypoints, list):
|
| 659 |
+
keypoints = load_keypoints({}, keypoints, kpts_as_bin=set([]))
|
| 660 |
+
assert len(set(sum(pairs, ())) - set(keypoints.keys())) == 0
|
| 661 |
+
with h5py.File(str(match_path), "a") as fd:
|
| 662 |
+
for name0, name1 in tqdm(pairs):
|
| 663 |
+
pair = names_to_pair(name0, name1)
|
| 664 |
+
grp = fd[pair]
|
| 665 |
+
kpts0 = grp["keypoints0"].__array__()
|
| 666 |
+
kpts1 = grp["keypoints1"].__array__()
|
| 667 |
+
scores = grp["scores"].__array__()
|
| 668 |
+
|
| 669 |
+
# NN search across cell boundaries
|
| 670 |
+
mkp_ids0 = assign_keypoints(kpts0, keypoints[name0], max_error)
|
| 671 |
+
mkp_ids1 = assign_keypoints(kpts1, keypoints[name1], max_error)
|
| 672 |
+
|
| 673 |
+
matches0, scores0 = kpids_to_matches0(mkp_ids0, mkp_ids1, scores)
|
| 674 |
+
|
| 675 |
+
# overwrite matches0 and matching_scores0
|
| 676 |
+
del grp["matches0"], grp["matching_scores0"]
|
| 677 |
+
grp.create_dataset("matches0", data=matches0)
|
| 678 |
+
grp.create_dataset("matching_scores0", data=scores0)
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
@torch.no_grad()
|
| 682 |
+
def match_and_assign(
|
| 683 |
+
conf: Dict,
|
| 684 |
+
pairs_path: Path,
|
| 685 |
+
image_dir: Path,
|
| 686 |
+
match_path: Path, # out
|
| 687 |
+
feature_path_q: Path, # out
|
| 688 |
+
feature_paths_refs: Optional[List[Path]] = [],
|
| 689 |
+
max_kps: Optional[int] = 8192,
|
| 690 |
+
overwrite: bool = False,
|
| 691 |
+
) -> Path:
|
| 692 |
+
for path in feature_paths_refs:
|
| 693 |
+
if not path.exists():
|
| 694 |
+
raise FileNotFoundError(f"Reference feature file {path}.")
|
| 695 |
+
pairs = parse_retrieval(pairs_path)
|
| 696 |
+
pairs = [(q, r) for q, rs in pairs.items() for r in rs]
|
| 697 |
+
pairs = find_unique_new_pairs(pairs, None if overwrite else match_path)
|
| 698 |
+
required_queries = set(sum(pairs, ()))
|
| 699 |
+
|
| 700 |
+
name2ref = {
|
| 701 |
+
n: i for i, p in enumerate(feature_paths_refs) for n in list_h5_names(p)
|
| 702 |
+
}
|
| 703 |
+
existing_refs = required_queries.intersection(set(name2ref.keys()))
|
| 704 |
+
|
| 705 |
+
# images which require feature extraction
|
| 706 |
+
required_queries = required_queries - existing_refs
|
| 707 |
+
|
| 708 |
+
if feature_path_q.exists():
|
| 709 |
+
existing_queries = set(list_h5_names(feature_path_q))
|
| 710 |
+
feature_paths_refs.append(feature_path_q)
|
| 711 |
+
existing_refs = set.union(existing_refs, existing_queries)
|
| 712 |
+
if not overwrite:
|
| 713 |
+
required_queries = required_queries - existing_queries
|
| 714 |
+
|
| 715 |
+
if len(pairs) == 0 and len(required_queries) == 0:
|
| 716 |
+
logger.info("All pairs exist. Skipping dense matching.")
|
| 717 |
+
return
|
| 718 |
+
|
| 719 |
+
# extract semi-dense matches
|
| 720 |
+
match_dense(conf, pairs, image_dir, match_path, existing_refs=existing_refs)
|
| 721 |
+
|
| 722 |
+
logger.info("Assigning matches...")
|
| 723 |
+
|
| 724 |
+
# Pre-load existing keypoints
|
| 725 |
+
cpdict, bindict = load_keypoints(
|
| 726 |
+
conf, feature_paths_refs, quantize=required_queries
|
| 727 |
+
)
|
| 728 |
+
|
| 729 |
+
# Reassign matches by aggregation
|
| 730 |
+
cpdict = aggregate_matches(
|
| 731 |
+
conf,
|
| 732 |
+
pairs,
|
| 733 |
+
match_path,
|
| 734 |
+
feature_path=feature_path_q,
|
| 735 |
+
required_queries=required_queries,
|
| 736 |
+
max_kps=max_kps,
|
| 737 |
+
cpdict=cpdict,
|
| 738 |
+
bindict=bindict,
|
| 739 |
+
)
|
| 740 |
+
|
| 741 |
+
# Invalidate matches that are far from selected bin by reassignment
|
| 742 |
+
if max_kps is not None:
|
| 743 |
+
logger.info(f'Reassign matches with max_error={conf["max_error"]}.')
|
| 744 |
+
assign_matches(pairs, match_path, cpdict, max_error=conf["max_error"])
|
| 745 |
|
| 746 |
def scale_lines(lines, scale):
|
| 747 |
if np.any(scale != 1.0):
|
|
|
|
| 958 |
del pred
|
| 959 |
torch.cuda.empty_cache()
|
| 960 |
return ret
|
| 961 |
+
|
| 962 |
+
@torch.no_grad()
|
| 963 |
+
def main(
|
| 964 |
+
conf: Dict,
|
| 965 |
+
pairs: Path,
|
| 966 |
+
image_dir: Path,
|
| 967 |
+
export_dir: Optional[Path] = None,
|
| 968 |
+
matches: Optional[Path] = None, # out
|
| 969 |
+
features: Optional[Path] = None, # out
|
| 970 |
+
features_ref: Optional[Path] = None,
|
| 971 |
+
max_kps: Optional[int] = 8192,
|
| 972 |
+
overwrite: bool = False,
|
| 973 |
+
) -> Path:
|
| 974 |
+
logger.info(
|
| 975 |
+
"Extracting semi-dense features with configuration:" f"\n{pprint.pformat(conf)}"
|
| 976 |
+
)
|
| 977 |
+
|
| 978 |
+
if features is None:
|
| 979 |
+
features = "feats_"
|
| 980 |
+
|
| 981 |
+
if isinstance(features, Path):
|
| 982 |
+
features_q = features
|
| 983 |
+
if matches is None:
|
| 984 |
+
raise ValueError(
|
| 985 |
+
"Either provide both features and matches as Path" " or both as names."
|
| 986 |
+
)
|
| 987 |
+
else:
|
| 988 |
+
if export_dir is None:
|
| 989 |
+
raise ValueError(
|
| 990 |
+
"Provide an export_dir if features and matches"
|
| 991 |
+
f" are not file paths: {features}, {matches}."
|
| 992 |
+
)
|
| 993 |
+
features_q = Path(export_dir, f'{features}{conf["output"]}.h5')
|
| 994 |
+
if matches is None:
|
| 995 |
+
matches = Path(export_dir, f'{conf["output"]}_{pairs.stem}.h5')
|
| 996 |
+
|
| 997 |
+
if features_ref is None:
|
| 998 |
+
features_ref = []
|
| 999 |
+
elif isinstance(features_ref, list):
|
| 1000 |
+
features_ref = list(features_ref)
|
| 1001 |
+
elif isinstance(features_ref, Path):
|
| 1002 |
+
features_ref = [features_ref]
|
| 1003 |
+
else:
|
| 1004 |
+
raise TypeError(str(features_ref))
|
| 1005 |
+
|
| 1006 |
+
match_and_assign(
|
| 1007 |
+
conf, pairs, image_dir, matches, features_q, features_ref, max_kps, overwrite
|
| 1008 |
+
)
|
| 1009 |
+
|
| 1010 |
+
return features_q, matches
|
| 1011 |
+
|
| 1012 |
+
|
| 1013 |
+
if __name__ == "__main__":
|
| 1014 |
+
parser = argparse.ArgumentParser()
|
| 1015 |
+
parser.add_argument("--pairs", type=Path, required=True)
|
| 1016 |
+
parser.add_argument("--image_dir", type=Path, required=True)
|
| 1017 |
+
parser.add_argument("--export_dir", type=Path, required=True)
|
| 1018 |
+
parser.add_argument("--matches", type=Path, default=confs["loftr"]["output"])
|
| 1019 |
+
parser.add_argument(
|
| 1020 |
+
"--features", type=str, default="feats_" + confs["loftr"]["output"]
|
| 1021 |
+
)
|
| 1022 |
+
parser.add_argument("--conf", type=str, default="loftr", choices=list(confs.keys()))
|
| 1023 |
+
args = parser.parse_args()
|
| 1024 |
+
main(
|
| 1025 |
+
confs[args.conf],
|
| 1026 |
+
args.pairs,
|
| 1027 |
+
args.image_dir,
|
| 1028 |
+
args.export_dir,
|
| 1029 |
+
args.matches,
|
| 1030 |
+
args.features,
|
| 1031 |
+
)
|
| 1032 |
+
|
hloc/matchers/superglue.py
CHANGED
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
| 4 |
from ..utils.base_model import BaseModel
|
| 5 |
|
| 6 |
sys.path.append(str(Path(__file__).parent / "../../third_party"))
|
| 7 |
-
from SuperGluePretrainedNetwork.models.superglue import SuperGlue as SG
|
| 8 |
|
| 9 |
|
| 10 |
class SuperGlue(BaseModel):
|
|
|
|
| 4 |
from ..utils.base_model import BaseModel
|
| 5 |
|
| 6 |
sys.path.append(str(Path(__file__).parent / "../../third_party"))
|
| 7 |
+
from SuperGluePretrainedNetwork.models.superglue import SuperGlue as SG # noqa: E402
|
| 8 |
|
| 9 |
|
| 10 |
class SuperGlue(BaseModel):
|
hloc/pairs_from_covisibility.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
from collections import defaultdict
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
|
| 8 |
+
from . import logger
|
| 9 |
+
from .utils.read_write_model import read_model
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def main(model, output, num_matched):
|
| 13 |
+
logger.info("Reading the COLMAP model...")
|
| 14 |
+
cameras, images, points3D = read_model(model)
|
| 15 |
+
|
| 16 |
+
logger.info("Extracting image pairs from covisibility info...")
|
| 17 |
+
pairs = []
|
| 18 |
+
for image_id, image in tqdm(images.items()):
|
| 19 |
+
matched = image.point3D_ids != -1
|
| 20 |
+
points3D_covis = image.point3D_ids[matched]
|
| 21 |
+
|
| 22 |
+
covis = defaultdict(int)
|
| 23 |
+
for point_id in points3D_covis:
|
| 24 |
+
for image_covis_id in points3D[point_id].image_ids:
|
| 25 |
+
if image_covis_id != image_id:
|
| 26 |
+
covis[image_covis_id] += 1
|
| 27 |
+
|
| 28 |
+
if len(covis) == 0:
|
| 29 |
+
logger.info(f"Image {image_id} does not have any covisibility.")
|
| 30 |
+
continue
|
| 31 |
+
|
| 32 |
+
covis_ids = np.array(list(covis.keys()))
|
| 33 |
+
covis_num = np.array([covis[i] for i in covis_ids])
|
| 34 |
+
|
| 35 |
+
if len(covis_ids) <= num_matched:
|
| 36 |
+
top_covis_ids = covis_ids[np.argsort(-covis_num)]
|
| 37 |
+
else:
|
| 38 |
+
# get covisible image ids with top k number of common matches
|
| 39 |
+
ind_top = np.argpartition(covis_num, -num_matched)
|
| 40 |
+
ind_top = ind_top[-num_matched:] # unsorted top k
|
| 41 |
+
ind_top = ind_top[np.argsort(-covis_num[ind_top])]
|
| 42 |
+
top_covis_ids = [covis_ids[i] for i in ind_top]
|
| 43 |
+
assert covis_num[ind_top[0]] == np.max(covis_num)
|
| 44 |
+
|
| 45 |
+
for i in top_covis_ids:
|
| 46 |
+
pair = (image.name, images[i].name)
|
| 47 |
+
pairs.append(pair)
|
| 48 |
+
|
| 49 |
+
logger.info(f"Found {len(pairs)} pairs.")
|
| 50 |
+
with open(output, "w") as f:
|
| 51 |
+
f.write("\n".join(" ".join([i, j]) for i, j in pairs))
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
parser = argparse.ArgumentParser()
|
| 56 |
+
parser.add_argument("--model", required=True, type=Path)
|
| 57 |
+
parser.add_argument("--output", required=True, type=Path)
|
| 58 |
+
parser.add_argument("--num_matched", required=True, type=int)
|
| 59 |
+
args = parser.parse_args()
|
| 60 |
+
main(**args.__dict__)
|
hloc/pairs_from_exhaustive.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import collections.abc as collections
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import List, Optional, Union
|
| 5 |
+
|
| 6 |
+
from . import logger
|
| 7 |
+
from .utils.io import list_h5_names
|
| 8 |
+
from .utils.parsers import parse_image_lists
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def main(
|
| 12 |
+
output: Path,
|
| 13 |
+
image_list: Optional[Union[Path, List[str]]] = None,
|
| 14 |
+
features: Optional[Path] = None,
|
| 15 |
+
ref_list: Optional[Union[Path, List[str]]] = None,
|
| 16 |
+
ref_features: Optional[Path] = None,
|
| 17 |
+
):
|
| 18 |
+
if image_list is not None:
|
| 19 |
+
if isinstance(image_list, (str, Path)):
|
| 20 |
+
names_q = parse_image_lists(image_list)
|
| 21 |
+
elif isinstance(image_list, collections.Iterable):
|
| 22 |
+
names_q = list(image_list)
|
| 23 |
+
else:
|
| 24 |
+
raise ValueError(f"Unknown type for image list: {image_list}")
|
| 25 |
+
elif features is not None:
|
| 26 |
+
names_q = list_h5_names(features)
|
| 27 |
+
else:
|
| 28 |
+
raise ValueError("Provide either a list of images or a feature file.")
|
| 29 |
+
|
| 30 |
+
self_matching = False
|
| 31 |
+
if ref_list is not None:
|
| 32 |
+
if isinstance(ref_list, (str, Path)):
|
| 33 |
+
names_ref = parse_image_lists(ref_list)
|
| 34 |
+
elif isinstance(image_list, collections.Iterable):
|
| 35 |
+
names_ref = list(ref_list)
|
| 36 |
+
else:
|
| 37 |
+
raise ValueError(f"Unknown type for reference image list: {ref_list}")
|
| 38 |
+
elif ref_features is not None:
|
| 39 |
+
names_ref = list_h5_names(ref_features)
|
| 40 |
+
else:
|
| 41 |
+
self_matching = True
|
| 42 |
+
names_ref = names_q
|
| 43 |
+
|
| 44 |
+
pairs = []
|
| 45 |
+
for i, n1 in enumerate(names_q):
|
| 46 |
+
for j, n2 in enumerate(names_ref):
|
| 47 |
+
if self_matching and j <= i:
|
| 48 |
+
continue
|
| 49 |
+
pairs.append((n1, n2))
|
| 50 |
+
|
| 51 |
+
logger.info(f"Found {len(pairs)} pairs.")
|
| 52 |
+
with open(output, "w") as f:
|
| 53 |
+
f.write("\n".join(" ".join([i, j]) for i, j in pairs))
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
if __name__ == "__main__":
|
| 57 |
+
parser = argparse.ArgumentParser()
|
| 58 |
+
parser.add_argument("--output", required=True, type=Path)
|
| 59 |
+
parser.add_argument("--image_list", type=Path)
|
| 60 |
+
parser.add_argument("--features", type=Path)
|
| 61 |
+
parser.add_argument("--ref_list", type=Path)
|
| 62 |
+
parser.add_argument("--ref_features", type=Path)
|
| 63 |
+
args = parser.parse_args()
|
| 64 |
+
main(**args.__dict__)
|
hloc/pairs_from_poses.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import scipy.spatial
|
| 6 |
+
|
| 7 |
+
from . import logger
|
| 8 |
+
from .pairs_from_retrieval import pairs_from_score_matrix
|
| 9 |
+
from .utils.read_write_model import read_images_binary
|
| 10 |
+
|
| 11 |
+
DEFAULT_ROT_THRESH = 30 # in degrees
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def get_pairwise_distances(images):
|
| 15 |
+
ids = np.array(list(images.keys()))
|
| 16 |
+
Rs = []
|
| 17 |
+
ts = []
|
| 18 |
+
for id_ in ids:
|
| 19 |
+
image = images[id_]
|
| 20 |
+
R = image.qvec2rotmat()
|
| 21 |
+
t = image.tvec
|
| 22 |
+
Rs.append(R)
|
| 23 |
+
ts.append(t)
|
| 24 |
+
Rs = np.stack(Rs, 0)
|
| 25 |
+
ts = np.stack(ts, 0)
|
| 26 |
+
|
| 27 |
+
# Invert the poses from world-to-camera to camera-to-world.
|
| 28 |
+
Rs = Rs.transpose(0, 2, 1)
|
| 29 |
+
ts = -(Rs @ ts[:, :, None])[:, :, 0]
|
| 30 |
+
|
| 31 |
+
dist = scipy.spatial.distance.squareform(scipy.spatial.distance.pdist(ts))
|
| 32 |
+
|
| 33 |
+
# Instead of computing the angle between two camera orientations,
|
| 34 |
+
# we compute the angle between the principal axes, as two images rotated
|
| 35 |
+
# around their principal axis still observe the same scene.
|
| 36 |
+
axes = Rs[:, :, -1]
|
| 37 |
+
dots = np.einsum("mi,ni->mn", axes, axes, optimize=True)
|
| 38 |
+
dR = np.rad2deg(np.arccos(np.clip(dots, -1.0, 1.0)))
|
| 39 |
+
|
| 40 |
+
return ids, dist, dR
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def main(model, output, num_matched, rotation_threshold=DEFAULT_ROT_THRESH):
|
| 44 |
+
logger.info("Reading the COLMAP model...")
|
| 45 |
+
images = read_images_binary(model / "images.bin")
|
| 46 |
+
|
| 47 |
+
logger.info(f"Obtaining pairwise distances between {len(images)} images...")
|
| 48 |
+
ids, dist, dR = get_pairwise_distances(images)
|
| 49 |
+
scores = -dist
|
| 50 |
+
|
| 51 |
+
invalid = dR >= rotation_threshold
|
| 52 |
+
np.fill_diagonal(invalid, True)
|
| 53 |
+
pairs = pairs_from_score_matrix(scores, invalid, num_matched)
|
| 54 |
+
pairs = [(images[ids[i]].name, images[ids[j]].name) for i, j in pairs]
|
| 55 |
+
|
| 56 |
+
logger.info(f"Found {len(pairs)} pairs.")
|
| 57 |
+
with open(output, "w") as f:
|
| 58 |
+
f.write("\n".join(" ".join(p) for p in pairs))
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
parser = argparse.ArgumentParser()
|
| 63 |
+
parser.add_argument("--model", required=True, type=Path)
|
| 64 |
+
parser.add_argument("--output", required=True, type=Path)
|
| 65 |
+
parser.add_argument("--num_matched", required=True, type=int)
|
| 66 |
+
parser.add_argument("--rotation_threshold", default=DEFAULT_ROT_THRESH, type=float)
|
| 67 |
+
args = parser.parse_args()
|
| 68 |
+
main(**args.__dict__)
|
hloc/pairs_from_retrieval.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import collections.abc as collections
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
import h5py
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from . import logger
|
| 11 |
+
from .utils.io import list_h5_names
|
| 12 |
+
from .utils.parsers import parse_image_lists
|
| 13 |
+
from .utils.read_write_model import read_images_binary
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def parse_names(prefix, names, names_all):
|
| 17 |
+
if prefix is not None:
|
| 18 |
+
if not isinstance(prefix, str):
|
| 19 |
+
prefix = tuple(prefix)
|
| 20 |
+
names = [n for n in names_all if n.startswith(prefix)]
|
| 21 |
+
if len(names) == 0:
|
| 22 |
+
raise ValueError(f"Could not find any image with the prefix `{prefix}`.")
|
| 23 |
+
elif names is not None:
|
| 24 |
+
if isinstance(names, (str, Path)):
|
| 25 |
+
names = parse_image_lists(names)
|
| 26 |
+
elif isinstance(names, collections.Iterable):
|
| 27 |
+
names = list(names)
|
| 28 |
+
else:
|
| 29 |
+
raise ValueError(
|
| 30 |
+
f"Unknown type of image list: {names}."
|
| 31 |
+
"Provide either a list or a path to a list file."
|
| 32 |
+
)
|
| 33 |
+
else:
|
| 34 |
+
names = names_all
|
| 35 |
+
return names
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def get_descriptors(names, path, name2idx=None, key="global_descriptor"):
|
| 39 |
+
if name2idx is None:
|
| 40 |
+
with h5py.File(str(path), "r", libver="latest") as fd:
|
| 41 |
+
desc = [fd[n][key].__array__() for n in names]
|
| 42 |
+
else:
|
| 43 |
+
desc = []
|
| 44 |
+
for n in names:
|
| 45 |
+
with h5py.File(str(path[name2idx[n]]), "r", libver="latest") as fd:
|
| 46 |
+
desc.append(fd[n][key].__array__())
|
| 47 |
+
return torch.from_numpy(np.stack(desc, 0)).float()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def pairs_from_score_matrix(
|
| 51 |
+
scores: torch.Tensor,
|
| 52 |
+
invalid: np.array,
|
| 53 |
+
num_select: int,
|
| 54 |
+
min_score: Optional[float] = None,
|
| 55 |
+
):
|
| 56 |
+
assert scores.shape == invalid.shape
|
| 57 |
+
if isinstance(scores, np.ndarray):
|
| 58 |
+
scores = torch.from_numpy(scores)
|
| 59 |
+
invalid = torch.from_numpy(invalid).to(scores.device)
|
| 60 |
+
if min_score is not None:
|
| 61 |
+
invalid |= scores < min_score
|
| 62 |
+
scores.masked_fill_(invalid, float("-inf"))
|
| 63 |
+
|
| 64 |
+
topk = torch.topk(scores, num_select, dim=1)
|
| 65 |
+
indices = topk.indices.cpu().numpy()
|
| 66 |
+
valid = topk.values.isfinite().cpu().numpy()
|
| 67 |
+
|
| 68 |
+
pairs = []
|
| 69 |
+
for i, j in zip(*np.where(valid)):
|
| 70 |
+
pairs.append((i, indices[i, j]))
|
| 71 |
+
return pairs
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def main(
|
| 75 |
+
descriptors,
|
| 76 |
+
output,
|
| 77 |
+
num_matched,
|
| 78 |
+
query_prefix=None,
|
| 79 |
+
query_list=None,
|
| 80 |
+
db_prefix=None,
|
| 81 |
+
db_list=None,
|
| 82 |
+
db_model=None,
|
| 83 |
+
db_descriptors=None,
|
| 84 |
+
):
|
| 85 |
+
logger.info("Extracting image pairs from a retrieval database.")
|
| 86 |
+
|
| 87 |
+
# We handle multiple reference feature files.
|
| 88 |
+
# We only assume that names are unique among them and map names to files.
|
| 89 |
+
if db_descriptors is None:
|
| 90 |
+
db_descriptors = descriptors
|
| 91 |
+
if isinstance(db_descriptors, (Path, str)):
|
| 92 |
+
db_descriptors = [db_descriptors]
|
| 93 |
+
name2db = {n: i for i, p in enumerate(db_descriptors) for n in list_h5_names(p)}
|
| 94 |
+
db_names_h5 = list(name2db.keys())
|
| 95 |
+
query_names_h5 = list_h5_names(descriptors)
|
| 96 |
+
|
| 97 |
+
if db_model:
|
| 98 |
+
images = read_images_binary(db_model / "images.bin")
|
| 99 |
+
db_names = [i.name for i in images.values()]
|
| 100 |
+
else:
|
| 101 |
+
db_names = parse_names(db_prefix, db_list, db_names_h5)
|
| 102 |
+
if len(db_names) == 0:
|
| 103 |
+
raise ValueError("Could not find any database image.")
|
| 104 |
+
query_names = parse_names(query_prefix, query_list, query_names_h5)
|
| 105 |
+
|
| 106 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 107 |
+
db_desc = get_descriptors(db_names, db_descriptors, name2db)
|
| 108 |
+
query_desc = get_descriptors(query_names, descriptors)
|
| 109 |
+
sim = torch.einsum("id,jd->ij", query_desc.to(device), db_desc.to(device))
|
| 110 |
+
|
| 111 |
+
# Avoid self-matching
|
| 112 |
+
self = np.array(query_names)[:, None] == np.array(db_names)[None]
|
| 113 |
+
pairs = pairs_from_score_matrix(sim, self, num_matched, min_score=0)
|
| 114 |
+
pairs = [(query_names[i], db_names[j]) for i, j in pairs]
|
| 115 |
+
|
| 116 |
+
logger.info(f"Found {len(pairs)} pairs.")
|
| 117 |
+
with open(output, "w") as f:
|
| 118 |
+
f.write("\n".join(" ".join([i, j]) for i, j in pairs))
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
if __name__ == "__main__":
|
| 122 |
+
parser = argparse.ArgumentParser()
|
| 123 |
+
parser.add_argument("--descriptors", type=Path, required=True)
|
| 124 |
+
parser.add_argument("--output", type=Path, required=True)
|
| 125 |
+
parser.add_argument("--num_matched", type=int, required=True)
|
| 126 |
+
parser.add_argument("--query_prefix", type=str, nargs="+")
|
| 127 |
+
parser.add_argument("--query_list", type=Path)
|
| 128 |
+
parser.add_argument("--db_prefix", type=str, nargs="+")
|
| 129 |
+
parser.add_argument("--db_list", type=Path)
|
| 130 |
+
parser.add_argument("--db_model", type=Path)
|
| 131 |
+
parser.add_argument("--db_descriptors", type=Path)
|
| 132 |
+
args = parser.parse_args()
|
| 133 |
+
main(**args.__dict__)
|
hloc/pipelines/4Seasons/localize.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
import argparse
|
|
|
|
| 3 |
|
| 4 |
-
from ... import extract_features,
|
| 5 |
-
from .utils import
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
relocalization_files = {
|
| 10 |
-
"training": "RelocalizationFilesTrain//relocalizationFile_recording_2020-03-24_17-36-22.txt",
|
| 11 |
-
"validation": "RelocalizationFilesVal/relocalizationFile_recording_2020-03-03_12-03-23.txt",
|
| 12 |
-
"test0": "RelocalizationFilesTest/relocalizationFile_recording_2020-03-24_17-45-31_*.txt",
|
| 13 |
-
"test1": "RelocalizationFilesTest/relocalizationFile_recording_2020-04-23_19-37-00_*.txt",
|
| 14 |
}
|
| 15 |
|
| 16 |
parser = argparse.ArgumentParser()
|
|
@@ -67,9 +72,7 @@ delete_unused_images(seq_images, timestamps)
|
|
| 67 |
generate_query_lists(timestamps, seq_dir, query_list)
|
| 68 |
|
| 69 |
# Generate the localization pairs from the given reference frames.
|
| 70 |
-
generate_localization_pairs(
|
| 71 |
-
sequence, reloc, num_loc_pairs, ref_pairs, loc_pairs
|
| 72 |
-
)
|
| 73 |
|
| 74 |
# Extract, match, amd localize.
|
| 75 |
ffile = extract_features.main(fconf, seq_images, output_dir)
|
|
|
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
from pathlib import Path
|
| 3 |
|
| 4 |
+
from ... import extract_features, localize_sfm, logger, match_features
|
| 5 |
+
from .utils import (
|
| 6 |
+
delete_unused_images,
|
| 7 |
+
evaluate_submission,
|
| 8 |
+
generate_localization_pairs,
|
| 9 |
+
generate_query_lists,
|
| 10 |
+
get_timestamps,
|
| 11 |
+
prepare_submission,
|
| 12 |
+
)
|
| 13 |
|
| 14 |
relocalization_files = {
|
| 15 |
+
"training": "RelocalizationFilesTrain//relocalizationFile_recording_2020-03-24_17-36-22.txt", # noqa: E501
|
| 16 |
+
"validation": "RelocalizationFilesVal/relocalizationFile_recording_2020-03-03_12-03-23.txt", # noqa: E501
|
| 17 |
+
"test0": "RelocalizationFilesTest/relocalizationFile_recording_2020-03-24_17-45-31_*.txt", # noqa: E501
|
| 18 |
+
"test1": "RelocalizationFilesTest/relocalizationFile_recording_2020-04-23_19-37-00_*.txt", # noqa: E501
|
| 19 |
}
|
| 20 |
|
| 21 |
parser = argparse.ArgumentParser()
|
|
|
|
| 72 |
generate_query_lists(timestamps, seq_dir, query_list)
|
| 73 |
|
| 74 |
# Generate the localization pairs from the given reference frames.
|
| 75 |
+
generate_localization_pairs(sequence, reloc, num_loc_pairs, ref_pairs, loc_pairs)
|
|
|
|
|
|
|
| 76 |
|
| 77 |
# Extract, match, amd localize.
|
| 78 |
ffile = extract_features.main(fconf, seq_images, output_dir)
|
hloc/pipelines/4Seasons/prepare_reference.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
import argparse
|
|
|
|
| 3 |
|
| 4 |
-
from ... import extract_features, match_features
|
| 5 |
-
from
|
| 6 |
-
from .utils import get_timestamps, delete_unused_images
|
| 7 |
-
from .utils import build_empty_colmap_model
|
| 8 |
|
| 9 |
parser = argparse.ArgumentParser()
|
| 10 |
parser.add_argument(
|
|
|
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
from pathlib import Path
|
| 3 |
|
| 4 |
+
from ... import extract_features, match_features, pairs_from_poses, triangulation
|
| 5 |
+
from .utils import build_empty_colmap_model, delete_unused_images, get_timestamps
|
|
|
|
|
|
|
| 6 |
|
| 7 |
parser = argparse.ArgumentParser()
|
| 8 |
parser.add_argument(
|
hloc/pipelines/4Seasons/utils.py
CHANGED
|
@@ -1,11 +1,18 @@
|
|
| 1 |
-
import
|
| 2 |
-
import numpy as np
|
| 3 |
import logging
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
from ...utils.parsers import parse_retrieval
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
|
@@ -28,10 +35,10 @@ def get_timestamps(files, idx):
|
|
| 28 |
|
| 29 |
def delete_unused_images(root, timestamps):
|
| 30 |
"""Delete all images in root if they are not contained in timestamps."""
|
| 31 |
-
images =
|
| 32 |
deleted = 0
|
| 33 |
for image in images:
|
| 34 |
-
ts = image.stem
|
| 35 |
if ts not in timestamps:
|
| 36 |
os.remove(image)
|
| 37 |
deleted += 1
|
|
@@ -48,11 +55,7 @@ def camera_from_calibration_file(id_, path):
|
|
| 48 |
model_name = "PINHOLE"
|
| 49 |
params = [float(i) for i in [fx, fy, cx, cy]]
|
| 50 |
camera = Camera(
|
| 51 |
-
id=id_,
|
| 52 |
-
model=model_name,
|
| 53 |
-
width=int(width),
|
| 54 |
-
height=int(height),
|
| 55 |
-
params=params,
|
| 56 |
)
|
| 57 |
return camera
|
| 58 |
|
|
@@ -153,9 +156,7 @@ def generate_localization_pairs(sequence, reloc, num, ref_pairs, out_path):
|
|
| 153 |
"""
|
| 154 |
if "test" in sequence:
|
| 155 |
# hard pairs will be overwritten by easy ones if available
|
| 156 |
-
relocs = [
|
| 157 |
-
str(reloc).replace("*", d) for d in ["hard", "moderate", "easy"]
|
| 158 |
-
]
|
| 159 |
else:
|
| 160 |
relocs = [reloc]
|
| 161 |
query_to_ref_ts = {}
|
|
@@ -213,12 +214,8 @@ def evaluate_submission(submission_dir, relocs, ths=[0.1, 0.2, 0.5]):
|
|
| 213 |
"""Compute the relocalization recall from predicted and ground truth poses."""
|
| 214 |
for reloc in relocs.parent.glob(relocs.name):
|
| 215 |
poses_gt = parse_relocalization(reloc, has_poses=True)
|
| 216 |
-
poses_pred = parse_relocalization(
|
| 217 |
-
|
| 218 |
-
)
|
| 219 |
-
poses_pred = {
|
| 220 |
-
(ref_ts, q_ts): (R, t) for ref_ts, q_ts, R, t in poses_pred
|
| 221 |
-
}
|
| 222 |
|
| 223 |
error = []
|
| 224 |
for ref_ts, q_ts, R_gt, t_gt in poses_gt:
|
|
|
|
| 1 |
+
import glob
|
|
|
|
| 2 |
import logging
|
| 3 |
+
import os
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
from ...utils.parsers import parse_retrieval
|
| 9 |
+
from ...utils.read_write_model import (
|
| 10 |
+
Camera,
|
| 11 |
+
Image,
|
| 12 |
+
qvec2rotmat,
|
| 13 |
+
rotmat2qvec,
|
| 14 |
+
write_model,
|
| 15 |
+
)
|
| 16 |
|
| 17 |
logger = logging.getLogger(__name__)
|
| 18 |
|
|
|
|
| 35 |
|
| 36 |
def delete_unused_images(root, timestamps):
|
| 37 |
"""Delete all images in root if they are not contained in timestamps."""
|
| 38 |
+
images = glob.glob((root / "**/*.png").as_posix(), recursive=True)
|
| 39 |
deleted = 0
|
| 40 |
for image in images:
|
| 41 |
+
ts = Path(image).stem
|
| 42 |
if ts not in timestamps:
|
| 43 |
os.remove(image)
|
| 44 |
deleted += 1
|
|
|
|
| 55 |
model_name = "PINHOLE"
|
| 56 |
params = [float(i) for i in [fx, fy, cx, cy]]
|
| 57 |
camera = Camera(
|
| 58 |
+
id=id_, model=model_name, width=int(width), height=int(height), params=params
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
)
|
| 60 |
return camera
|
| 61 |
|
|
|
|
| 156 |
"""
|
| 157 |
if "test" in sequence:
|
| 158 |
# hard pairs will be overwritten by easy ones if available
|
| 159 |
+
relocs = [str(reloc).replace("*", d) for d in ["hard", "moderate", "easy"]]
|
|
|
|
|
|
|
| 160 |
else:
|
| 161 |
relocs = [reloc]
|
| 162 |
query_to_ref_ts = {}
|
|
|
|
| 214 |
"""Compute the relocalization recall from predicted and ground truth poses."""
|
| 215 |
for reloc in relocs.parent.glob(relocs.name):
|
| 216 |
poses_gt = parse_relocalization(reloc, has_poses=True)
|
| 217 |
+
poses_pred = parse_relocalization(submission_dir / reloc.name, has_poses=True)
|
| 218 |
+
poses_pred = {(ref_ts, q_ts): (R, t) for ref_ts, q_ts, R, t in poses_pred}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
error = []
|
| 221 |
for ref_ts, q_ts, R_gt, t_gt in poses_gt:
|
hloc/pipelines/7Scenes/create_gt_sfm.py
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
from pathlib import Path
|
|
|
|
| 2 |
import numpy as np
|
| 3 |
-
import torch
|
| 4 |
import PIL.Image
|
| 5 |
-
from tqdm import tqdm
|
| 6 |
import pycolmap
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
from ...utils.read_write_model import
|
| 9 |
|
| 10 |
|
| 11 |
def scene_coordinates(p2D, R_w2c, t_w2c, depth, camera):
|
| 12 |
assert len(depth) == len(p2D)
|
| 13 |
-
|
| 14 |
-
p2D_norm = np.asarray(ret["world_points"])
|
| 15 |
p2D_h = np.concatenate([p2D_norm, np.ones_like(p2D_norm[:, :1])], 1)
|
| 16 |
p3D_c = p2D_h * depth[:, None]
|
| 17 |
p3D_w = (p3D_c - t_w2c) @ R_w2c
|
|
@@ -28,9 +28,7 @@ def interpolate_depth(depth, kp):
|
|
| 28 |
|
| 29 |
# To maximize the number of points that have depth:
|
| 30 |
# do bilinear interpolation first and then nearest for the remaining points
|
| 31 |
-
interp_lin = grid_sample(depth, kp, align_corners=True, mode="bilinear")[
|
| 32 |
-
0, :, 0
|
| 33 |
-
]
|
| 34 |
interp_nn = torch.nn.functional.grid_sample(
|
| 35 |
depth, kp, align_corners=True, mode="nearest"
|
| 36 |
)[0, :, 0]
|
|
@@ -54,8 +52,7 @@ def project_to_image(p3D, R, t, camera, eps: float = 1e-4, pad: int = 1):
|
|
| 54 |
p3D = (p3D @ R.T) + t
|
| 55 |
visible = p3D[:, -1] >= eps # keep points in front of the camera
|
| 56 |
p2D_norm = p3D[:, :-1] / p3D[:, -1:].clip(min=eps)
|
| 57 |
-
|
| 58 |
-
p2D = np.asarray(ret["image_points"])
|
| 59 |
size = np.array([camera.width - pad - 1, camera.height - pad - 1])
|
| 60 |
valid = np.all((p2D >= pad) & (p2D <= size), -1)
|
| 61 |
valid &= visible
|
|
@@ -129,15 +126,7 @@ if __name__ == "__main__":
|
|
| 129 |
dataset = Path("datasets/7scenes")
|
| 130 |
outputs = Path("outputs/7Scenes")
|
| 131 |
|
| 132 |
-
SCENES = [
|
| 133 |
-
"chess",
|
| 134 |
-
"fire",
|
| 135 |
-
"heads",
|
| 136 |
-
"office",
|
| 137 |
-
"pumpkin",
|
| 138 |
-
"redkitchen",
|
| 139 |
-
"stairs",
|
| 140 |
-
]
|
| 141 |
for scene in SCENES:
|
| 142 |
sfm_path = outputs / scene / "sfm_superpoint+superglue"
|
| 143 |
depth_path = dataset / f"depth/7scenes_{scene}/train/depth"
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
+
|
| 3 |
import numpy as np
|
|
|
|
| 4 |
import PIL.Image
|
|
|
|
| 5 |
import pycolmap
|
| 6 |
+
import torch
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
|
| 9 |
+
from ...utils.read_write_model import read_model, write_model
|
| 10 |
|
| 11 |
|
| 12 |
def scene_coordinates(p2D, R_w2c, t_w2c, depth, camera):
|
| 13 |
assert len(depth) == len(p2D)
|
| 14 |
+
p2D_norm = np.stack(pycolmap.Camera(camera._asdict()).image_to_world(p2D))
|
|
|
|
| 15 |
p2D_h = np.concatenate([p2D_norm, np.ones_like(p2D_norm[:, :1])], 1)
|
| 16 |
p3D_c = p2D_h * depth[:, None]
|
| 17 |
p3D_w = (p3D_c - t_w2c) @ R_w2c
|
|
|
|
| 28 |
|
| 29 |
# To maximize the number of points that have depth:
|
| 30 |
# do bilinear interpolation first and then nearest for the remaining points
|
| 31 |
+
interp_lin = grid_sample(depth, kp, align_corners=True, mode="bilinear")[0, :, 0]
|
|
|
|
|
|
|
| 32 |
interp_nn = torch.nn.functional.grid_sample(
|
| 33 |
depth, kp, align_corners=True, mode="nearest"
|
| 34 |
)[0, :, 0]
|
|
|
|
| 52 |
p3D = (p3D @ R.T) + t
|
| 53 |
visible = p3D[:, -1] >= eps # keep points in front of the camera
|
| 54 |
p2D_norm = p3D[:, :-1] / p3D[:, -1:].clip(min=eps)
|
| 55 |
+
p2D = np.stack(pycolmap.Camera(camera._asdict()).world_to_image(p2D_norm))
|
|
|
|
| 56 |
size = np.array([camera.width - pad - 1, camera.height - pad - 1])
|
| 57 |
valid = np.all((p2D >= pad) & (p2D <= size), -1)
|
| 58 |
valid &= visible
|
|
|
|
| 126 |
dataset = Path("datasets/7scenes")
|
| 127 |
outputs = Path("outputs/7Scenes")
|
| 128 |
|
| 129 |
+
SCENES = ["chess", "fire", "heads", "office", "pumpkin", "redkitchen", "stairs"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
for scene in SCENES:
|
| 131 |
sfm_path = outputs / scene / "sfm_superpoint+superglue"
|
| 132 |
depth_path = dataset / f"depth/7scenes_{scene}/train/depth"
|
hloc/pipelines/7Scenes/pipeline.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
import argparse
|
|
|
|
| 3 |
|
| 4 |
-
from
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from ..Cambridge.utils import create_query_list_with_intrinsics, evaluate
|
| 7 |
-
from
|
| 8 |
-
from
|
| 9 |
|
| 10 |
SCENES = ["chess", "fire", "heads", "office", "pumpkin", "redkitchen", "stairs"]
|
| 11 |
|
|
@@ -45,9 +51,7 @@ def run_scene(
|
|
| 45 |
create_reference_sfm(gt_dir, ref_sfm_sift, test_list)
|
| 46 |
create_query_list_with_intrinsics(gt_dir, query_list, test_list)
|
| 47 |
|
| 48 |
-
features = extract_features.main(
|
| 49 |
-
feature_conf, images, outputs, as_half=True
|
| 50 |
-
)
|
| 51 |
|
| 52 |
sfm_pairs = outputs / f"pairs-db-covis{num_covis}.txt"
|
| 53 |
pairs_from_covisibility.main(ref_sfm_sift, sfm_pairs, num_matched=num_covis)
|
|
@@ -114,9 +118,7 @@ if __name__ == "__main__":
|
|
| 114 |
results = (
|
| 115 |
args.outputs
|
| 116 |
/ scene
|
| 117 |
-
/ "results_{}.txt".format(
|
| 118 |
-
"dense" if args.use_dense_depth else "sparse"
|
| 119 |
-
)
|
| 120 |
)
|
| 121 |
if args.overwrite or not results.exists():
|
| 122 |
run_scene(
|
|
|
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
from pathlib import Path
|
| 3 |
|
| 4 |
+
from ... import (
|
| 5 |
+
extract_features,
|
| 6 |
+
localize_sfm,
|
| 7 |
+
logger,
|
| 8 |
+
match_features,
|
| 9 |
+
pairs_from_covisibility,
|
| 10 |
+
triangulation,
|
| 11 |
+
)
|
| 12 |
from ..Cambridge.utils import create_query_list_with_intrinsics, evaluate
|
| 13 |
+
from .create_gt_sfm import correct_sfm_with_gt_depth
|
| 14 |
+
from .utils import create_reference_sfm
|
| 15 |
|
| 16 |
SCENES = ["chess", "fire", "heads", "office", "pumpkin", "redkitchen", "stairs"]
|
| 17 |
|
|
|
|
| 51 |
create_reference_sfm(gt_dir, ref_sfm_sift, test_list)
|
| 52 |
create_query_list_with_intrinsics(gt_dir, query_list, test_list)
|
| 53 |
|
| 54 |
+
features = extract_features.main(feature_conf, images, outputs, as_half=True)
|
|
|
|
|
|
|
| 55 |
|
| 56 |
sfm_pairs = outputs / f"pairs-db-covis{num_covis}.txt"
|
| 57 |
pairs_from_covisibility.main(ref_sfm_sift, sfm_pairs, num_matched=num_covis)
|
|
|
|
| 118 |
results = (
|
| 119 |
args.outputs
|
| 120 |
/ scene
|
| 121 |
+
/ "results_{}.txt".format("dense" if args.use_dense_depth else "sparse")
|
|
|
|
|
|
|
| 122 |
)
|
| 123 |
if args.overwrite or not results.exists():
|
| 124 |
run_scene(
|
hloc/pipelines/7Scenes/utils.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import logging
|
|
|
|
| 2 |
import numpy as np
|
| 3 |
|
| 4 |
from hloc.utils.read_write_model import read_model, write_model
|
|
|
|
| 1 |
import logging
|
| 2 |
+
|
| 3 |
import numpy as np
|
| 4 |
|
| 5 |
from hloc.utils.read_write_model import read_model, write_model
|
hloc/pipelines/Aachen/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Download the dataset from [visuallocalization.net](https://www.visuallocalizatio
|
|
| 6 |
```bash
|
| 7 |
export dataset=datasets/aachen
|
| 8 |
wget -r -np -nH -R "index.html*,aachen_v1_1.zip" --cut-dirs=4 https://data.ciirc.cvut.cz/public/projects/2020VisualLocalization/Aachen-Day-Night/ -P $dataset
|
| 9 |
-
unzip $dataset/images/database_and_query_images.zip -d $dataset
|
| 10 |
```
|
| 11 |
|
| 12 |
## Pipeline
|
|
|
|
| 6 |
```bash
|
| 7 |
export dataset=datasets/aachen
|
| 8 |
wget -r -np -nH -R "index.html*,aachen_v1_1.zip" --cut-dirs=4 https://data.ciirc.cvut.cz/public/projects/2020VisualLocalization/Aachen-Day-Night/ -P $dataset
|
| 9 |
+
unzip $dataset/images/database_and_query_images.zip -d $dataset
|
| 10 |
```
|
| 11 |
|
| 12 |
## Pipeline
|
hloc/pipelines/Aachen/pipeline.py
CHANGED
|
@@ -1,102 +1,109 @@
|
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
from pprint import pformat
|
| 3 |
-
import argparse
|
| 4 |
|
| 5 |
-
from ... import
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
default="datasets/aachen",
|
| 15 |
-
help="Path to the dataset, default: %(default)s",
|
| 16 |
-
)
|
| 17 |
-
parser.add_argument(
|
| 18 |
-
"--outputs",
|
| 19 |
-
type=Path,
|
| 20 |
-
default="outputs/aachen",
|
| 21 |
-
help="Path to the output directory, default: %(default)s",
|
| 22 |
-
)
|
| 23 |
-
parser.add_argument(
|
| 24 |
-
"--num_covis",
|
| 25 |
-
type=int,
|
| 26 |
-
default=20,
|
| 27 |
-
help="Number of image pairs for SfM, default: %(default)s",
|
| 28 |
-
)
|
| 29 |
-
parser.add_argument(
|
| 30 |
-
"--num_loc",
|
| 31 |
-
type=int,
|
| 32 |
-
default=50,
|
| 33 |
-
help="Number of image pairs for loc, default: %(default)s",
|
| 34 |
-
)
|
| 35 |
-
args = parser.parse_args()
|
| 36 |
|
| 37 |
-
#
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
outputs / "sfm_superpoint+superglue"
|
| 45 |
-
) # the SfM model we will build
|
| 46 |
-
sfm_pairs = (
|
| 47 |
-
outputs / f"pairs-db-covis{args.num_covis}.txt"
|
| 48 |
-
) # top-k most covisible in SIFT model
|
| 49 |
-
loc_pairs = (
|
| 50 |
-
outputs / f"pairs-query-netvlad{args.num_loc}.txt"
|
| 51 |
-
) # top-k retrieved by NetVLAD
|
| 52 |
-
results = (
|
| 53 |
-
outputs / f"Aachen_hloc_superpoint+superglue_netvlad{args.num_loc}.txt"
|
| 54 |
-
)
|
| 55 |
|
| 56 |
-
#
|
| 57 |
-
|
| 58 |
-
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
retrieval_conf = extract_features.confs["netvlad"]
|
| 62 |
-
feature_conf = extract_features.confs["superpoint_aachen"]
|
| 63 |
-
matcher_conf = match_features.confs["superglue"]
|
| 64 |
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
dataset / "aachen.db",
|
| 71 |
-
sift_sfm,
|
| 72 |
-
)
|
| 73 |
-
pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=args.num_covis)
|
| 74 |
-
sfm_matches = match_features.main(
|
| 75 |
-
matcher_conf, sfm_pairs, feature_conf["output"], outputs
|
| 76 |
-
)
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
-
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
|
| 83 |
-
pairs_from_retrieval.main(
|
| 84 |
-
global_descriptors,
|
| 85 |
-
loc_pairs,
|
| 86 |
-
args.num_loc,
|
| 87 |
-
query_prefix="query",
|
| 88 |
-
db_model=reference_sfm,
|
| 89 |
-
)
|
| 90 |
-
loc_matches = match_features.main(
|
| 91 |
-
matcher_conf, loc_pairs, feature_conf["output"], outputs
|
| 92 |
-
)
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
from pathlib import Path
|
| 3 |
from pprint import pformat
|
|
|
|
| 4 |
|
| 5 |
+
from ... import (
|
| 6 |
+
colmap_from_nvm,
|
| 7 |
+
extract_features,
|
| 8 |
+
localize_sfm,
|
| 9 |
+
logger,
|
| 10 |
+
match_features,
|
| 11 |
+
pairs_from_covisibility,
|
| 12 |
+
pairs_from_retrieval,
|
| 13 |
+
triangulation,
|
| 14 |
+
)
|
| 15 |
|
| 16 |
|
| 17 |
+
def run(args):
|
| 18 |
+
# Setup the paths
|
| 19 |
+
dataset = args.dataset
|
| 20 |
+
images = dataset / "images_upright/"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
outputs = args.outputs # where everything will be saved
|
| 23 |
+
sift_sfm = outputs / "sfm_sift" # from which we extract the reference poses
|
| 24 |
+
reference_sfm = outputs / "sfm_superpoint+superglue" # the SfM model we will build
|
| 25 |
+
sfm_pairs = (
|
| 26 |
+
outputs / f"pairs-db-covis{args.num_covis}.txt"
|
| 27 |
+
) # top-k most covisible in SIFT model
|
| 28 |
+
loc_pairs = (
|
| 29 |
+
outputs / f"pairs-query-netvlad{args.num_loc}.txt"
|
| 30 |
+
) # top-k retrieved by NetVLAD
|
| 31 |
+
results = outputs / f"Aachen_hloc_superpoint+superglue_netvlad{args.num_loc}.txt"
|
| 32 |
|
| 33 |
+
# list the standard configurations available
|
| 34 |
+
logger.info("Configs for feature extractors:\n%s", pformat(extract_features.confs))
|
| 35 |
+
logger.info("Configs for feature matchers:\n%s", pformat(match_features.confs))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
# pick one of the configurations for extraction and matching
|
| 38 |
+
retrieval_conf = extract_features.confs["netvlad"]
|
| 39 |
+
feature_conf = extract_features.confs["superpoint_aachen"]
|
| 40 |
+
matcher_conf = match_features.confs["superglue"]
|
| 41 |
|
| 42 |
+
features = extract_features.main(feature_conf, images, outputs)
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
colmap_from_nvm.main(
|
| 45 |
+
dataset / "3D-models/aachen_cvpr2018_db.nvm",
|
| 46 |
+
dataset / "3D-models/database_intrinsics.txt",
|
| 47 |
+
dataset / "aachen.db",
|
| 48 |
+
sift_sfm,
|
| 49 |
+
)
|
| 50 |
+
pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=args.num_covis)
|
| 51 |
+
sfm_matches = match_features.main(
|
| 52 |
+
matcher_conf, sfm_pairs, feature_conf["output"], outputs
|
| 53 |
+
)
|
| 54 |
|
| 55 |
+
triangulation.main(
|
| 56 |
+
reference_sfm, sift_sfm, images, sfm_pairs, features, sfm_matches
|
| 57 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
+
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
|
| 60 |
+
pairs_from_retrieval.main(
|
| 61 |
+
global_descriptors,
|
| 62 |
+
loc_pairs,
|
| 63 |
+
args.num_loc,
|
| 64 |
+
query_prefix="query",
|
| 65 |
+
db_model=reference_sfm,
|
| 66 |
+
)
|
| 67 |
+
loc_matches = match_features.main(
|
| 68 |
+
matcher_conf, loc_pairs, feature_conf["output"], outputs
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
localize_sfm.main(
|
| 72 |
+
reference_sfm,
|
| 73 |
+
dataset / "queries/*_time_queries_with_intrinsics.txt",
|
| 74 |
+
loc_pairs,
|
| 75 |
+
features,
|
| 76 |
+
loc_matches,
|
| 77 |
+
results,
|
| 78 |
+
covisibility_clustering=False,
|
| 79 |
+
) # not required with SuperPoint+SuperGlue
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
parser = argparse.ArgumentParser()
|
| 84 |
+
parser.add_argument(
|
| 85 |
+
"--dataset",
|
| 86 |
+
type=Path,
|
| 87 |
+
default="datasets/aachen",
|
| 88 |
+
help="Path to the dataset, default: %(default)s",
|
| 89 |
+
)
|
| 90 |
+
parser.add_argument(
|
| 91 |
+
"--outputs",
|
| 92 |
+
type=Path,
|
| 93 |
+
default="outputs/aachen",
|
| 94 |
+
help="Path to the output directory, default: %(default)s",
|
| 95 |
+
)
|
| 96 |
+
parser.add_argument(
|
| 97 |
+
"--num_covis",
|
| 98 |
+
type=int,
|
| 99 |
+
default=20,
|
| 100 |
+
help="Number of image pairs for SfM, default: %(default)s",
|
| 101 |
+
)
|
| 102 |
+
parser.add_argument(
|
| 103 |
+
"--num_loc",
|
| 104 |
+
type=int,
|
| 105 |
+
default=50,
|
| 106 |
+
help="Number of image pairs for loc, default: %(default)s",
|
| 107 |
+
)
|
| 108 |
+
args = parser.parse_args()
|
| 109 |
+
run(args)
|
hloc/pipelines/Aachen_v1_1/README.md
CHANGED
|
@@ -6,9 +6,8 @@ Download the dataset from [visuallocalization.net](https://www.visuallocalizatio
|
|
| 6 |
```bash
|
| 7 |
export dataset=datasets/aachen_v1.1
|
| 8 |
wget -r -np -nH -R "index.html*" --cut-dirs=4 https://data.ciirc.cvut.cz/public/projects/2020VisualLocalization/Aachen-Day-Night/ -P $dataset
|
| 9 |
-
unzip $dataset/images/database_and_query_images.zip -d $dataset
|
| 10 |
unzip $dataset/aachen_v1_1.zip -d $dataset
|
| 11 |
-
rsync -a $dataset/images_upright/ $dataset/images/images_upright/
|
| 12 |
```
|
| 13 |
|
| 14 |
## Pipeline
|
|
|
|
| 6 |
```bash
|
| 7 |
export dataset=datasets/aachen_v1.1
|
| 8 |
wget -r -np -nH -R "index.html*" --cut-dirs=4 https://data.ciirc.cvut.cz/public/projects/2020VisualLocalization/Aachen-Day-Night/ -P $dataset
|
| 9 |
+
unzip $dataset/images/database_and_query_images.zip -d $dataset
|
| 10 |
unzip $dataset/aachen_v1_1.zip -d $dataset
|
|
|
|
| 11 |
```
|
| 12 |
|
| 13 |
## Pipeline
|
hloc/pipelines/Aachen_v1_1/pipeline.py
CHANGED
|
@@ -1,95 +1,104 @@
|
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
from pprint import pformat
|
| 3 |
-
import argparse
|
| 4 |
|
| 5 |
-
from ... import
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
help="Path to the dataset, default: %(default)s",
|
| 15 |
-
)
|
| 16 |
-
parser.add_argument(
|
| 17 |
-
"--outputs",
|
| 18 |
-
type=Path,
|
| 19 |
-
default="outputs/aachen_v1.1",
|
| 20 |
-
help="Path to the output directory, default: %(default)s",
|
| 21 |
-
)
|
| 22 |
-
parser.add_argument(
|
| 23 |
-
"--num_covis",
|
| 24 |
-
type=int,
|
| 25 |
-
default=20,
|
| 26 |
-
help="Number of image pairs for SfM, default: %(default)s",
|
| 27 |
-
)
|
| 28 |
-
parser.add_argument(
|
| 29 |
-
"--num_loc",
|
| 30 |
-
type=int,
|
| 31 |
-
default=50,
|
| 32 |
-
help="Number of image pairs for loc, default: %(default)s",
|
| 33 |
-
)
|
| 34 |
-
args = parser.parse_args()
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
) # the SfM model we will build
|
| 45 |
-
sfm_pairs = (
|
| 46 |
-
outputs / f"pairs-db-covis{args.num_covis}.txt"
|
| 47 |
-
) # top-k most covisible in SIFT model
|
| 48 |
-
loc_pairs = (
|
| 49 |
-
outputs / f"pairs-query-netvlad{args.num_loc}.txt"
|
| 50 |
-
) # top-k retrieved by NetVLAD
|
| 51 |
-
results = (
|
| 52 |
-
outputs / f"Aachen-v1.1_hloc_superpoint+superglue_netvlad{args.num_loc}.txt"
|
| 53 |
-
)
|
| 54 |
|
| 55 |
-
#
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
retrieval_conf = extract_features.confs["netvlad"]
|
| 61 |
-
feature_conf = extract_features.confs["superpoint_max"]
|
| 62 |
-
matcher_conf = match_features.confs["superglue"]
|
| 63 |
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
)
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
|
| 76 |
-
pairs_from_retrieval.main(
|
| 77 |
-
global_descriptors,
|
| 78 |
-
loc_pairs,
|
| 79 |
-
args.num_loc,
|
| 80 |
-
query_prefix="query",
|
| 81 |
-
db_model=reference_sfm,
|
| 82 |
-
)
|
| 83 |
-
loc_matches = match_features.main(
|
| 84 |
-
matcher_conf, loc_pairs, feature_conf["output"], outputs
|
| 85 |
-
)
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
from pathlib import Path
|
| 3 |
from pprint import pformat
|
|
|
|
| 4 |
|
| 5 |
+
from ... import (
|
| 6 |
+
extract_features,
|
| 7 |
+
localize_sfm,
|
| 8 |
+
logger,
|
| 9 |
+
match_features,
|
| 10 |
+
pairs_from_covisibility,
|
| 11 |
+
pairs_from_retrieval,
|
| 12 |
+
triangulation,
|
| 13 |
+
)
|
| 14 |
|
| 15 |
|
| 16 |
+
def run(args):
|
| 17 |
+
# Setup the paths
|
| 18 |
+
dataset = args.dataset
|
| 19 |
+
images = dataset / "images_upright/"
|
| 20 |
+
sift_sfm = dataset / "3D-models/aachen_v_1_1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
outputs = args.outputs # where everything will be saved
|
| 23 |
+
reference_sfm = outputs / "sfm_superpoint+superglue" # the SfM model we will build
|
| 24 |
+
sfm_pairs = (
|
| 25 |
+
outputs / f"pairs-db-covis{args.num_covis}.txt"
|
| 26 |
+
) # top-k most covisible in SIFT model
|
| 27 |
+
loc_pairs = (
|
| 28 |
+
outputs / f"pairs-query-netvlad{args.num_loc}.txt"
|
| 29 |
+
) # top-k retrieved by NetVLAD
|
| 30 |
+
results = (
|
| 31 |
+
outputs / f"Aachen-v1.1_hloc_superpoint+superglue_netvlad{args.num_loc}.txt"
|
| 32 |
+
)
|
| 33 |
|
| 34 |
+
# list the standard configurations available
|
| 35 |
+
logger.info("Configs for feature extractors:\n%s", pformat(extract_features.confs))
|
| 36 |
+
logger.info("Configs for feature matchers:\n%s", pformat(match_features.confs))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
# pick one of the configurations for extraction and matching
|
| 39 |
+
retrieval_conf = extract_features.confs["netvlad"]
|
| 40 |
+
feature_conf = extract_features.confs["superpoint_max"]
|
| 41 |
+
matcher_conf = match_features.confs["superglue"]
|
| 42 |
|
| 43 |
+
features = extract_features.main(feature_conf, images, outputs)
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=args.num_covis)
|
| 46 |
+
sfm_matches = match_features.main(
|
| 47 |
+
matcher_conf, sfm_pairs, feature_conf["output"], outputs
|
| 48 |
+
)
|
| 49 |
|
| 50 |
+
triangulation.main(
|
| 51 |
+
reference_sfm, sift_sfm, images, sfm_pairs, features, sfm_matches
|
| 52 |
+
)
|
|
|
|
| 53 |
|
| 54 |
+
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
|
| 55 |
+
pairs_from_retrieval.main(
|
| 56 |
+
global_descriptors,
|
| 57 |
+
loc_pairs,
|
| 58 |
+
args.num_loc,
|
| 59 |
+
query_prefix="query",
|
| 60 |
+
db_model=reference_sfm,
|
| 61 |
+
)
|
| 62 |
+
loc_matches = match_features.main(
|
| 63 |
+
matcher_conf, loc_pairs, feature_conf["output"], outputs
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
localize_sfm.main(
|
| 67 |
+
reference_sfm,
|
| 68 |
+
dataset / "queries/*_time_queries_with_intrinsics.txt",
|
| 69 |
+
loc_pairs,
|
| 70 |
+
features,
|
| 71 |
+
loc_matches,
|
| 72 |
+
results,
|
| 73 |
+
covisibility_clustering=False,
|
| 74 |
+
) # not required with SuperPoint+SuperGlue
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
parser = argparse.ArgumentParser()
|
| 79 |
+
parser.add_argument(
|
| 80 |
+
"--dataset",
|
| 81 |
+
type=Path,
|
| 82 |
+
default="datasets/aachen_v1.1",
|
| 83 |
+
help="Path to the dataset, default: %(default)s",
|
| 84 |
+
)
|
| 85 |
+
parser.add_argument(
|
| 86 |
+
"--outputs",
|
| 87 |
+
type=Path,
|
| 88 |
+
default="outputs/aachen_v1.1",
|
| 89 |
+
help="Path to the output directory, default: %(default)s",
|
| 90 |
+
)
|
| 91 |
+
parser.add_argument(
|
| 92 |
+
"--num_covis",
|
| 93 |
+
type=int,
|
| 94 |
+
default=20,
|
| 95 |
+
help="Number of image pairs for SfM, default: %(default)s",
|
| 96 |
+
)
|
| 97 |
+
parser.add_argument(
|
| 98 |
+
"--num_loc",
|
| 99 |
+
type=int,
|
| 100 |
+
default=50,
|
| 101 |
+
help="Number of image pairs for loc, default: %(default)s",
|
| 102 |
+
)
|
| 103 |
+
args = parser.parse_args()
|
| 104 |
+
run(args)
|
hloc/pipelines/Aachen_v1_1/pipeline_loftr.py
CHANGED
|
@@ -1,94 +1,104 @@
|
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
from pprint import pformat
|
| 3 |
-
import argparse
|
| 4 |
|
| 5 |
-
from ... import
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
help="Path to the dataset, default: %(default)s",
|
| 15 |
-
)
|
| 16 |
-
parser.add_argument(
|
| 17 |
-
"--outputs",
|
| 18 |
-
type=Path,
|
| 19 |
-
default="outputs/aachen_v1.1",
|
| 20 |
-
help="Path to the output directory, default: %(default)s",
|
| 21 |
-
)
|
| 22 |
-
parser.add_argument(
|
| 23 |
-
"--num_covis",
|
| 24 |
-
type=int,
|
| 25 |
-
default=20,
|
| 26 |
-
help="Number of image pairs for SfM, default: %(default)s",
|
| 27 |
-
)
|
| 28 |
-
parser.add_argument(
|
| 29 |
-
"--num_loc",
|
| 30 |
-
type=int,
|
| 31 |
-
default=50,
|
| 32 |
-
help="Number of image pairs for loc, default: %(default)s",
|
| 33 |
-
)
|
| 34 |
-
args = parser.parse_args()
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
reference_sfm = outputs / "sfm_loftr" # the SfM model we will build
|
| 44 |
-
sfm_pairs = (
|
| 45 |
-
outputs / f"pairs-db-covis{args.num_covis}.txt"
|
| 46 |
-
) # top-k most covisible in SIFT model
|
| 47 |
-
loc_pairs = (
|
| 48 |
-
outputs / f"pairs-query-netvlad{args.num_loc}.txt"
|
| 49 |
-
) # top-k retrieved by NetVLAD
|
| 50 |
-
results = outputs / f"Aachen-v1.1_hloc_loftr_netvlad{args.num_loc}.txt"
|
| 51 |
|
| 52 |
-
#
|
| 53 |
-
|
|
|
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
matcher_conf =
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
features, sfm_matches
|
| 61 |
-
|
| 62 |
-
)
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
|
| 69 |
-
pairs_from_retrieval.main(
|
| 70 |
-
global_descriptors,
|
| 71 |
-
loc_pairs,
|
| 72 |
-
args.num_loc,
|
| 73 |
-
query_prefix="query",
|
| 74 |
-
db_model=reference_sfm,
|
| 75 |
-
)
|
| 76 |
-
features, loc_matches = match_dense.main(
|
| 77 |
-
matcher_conf,
|
| 78 |
-
loc_pairs,
|
| 79 |
-
images,
|
| 80 |
-
outputs,
|
| 81 |
-
features=features,
|
| 82 |
-
max_kps=None,
|
| 83 |
-
matches=sfm_matches,
|
| 84 |
-
)
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
from pathlib import Path
|
| 3 |
from pprint import pformat
|
|
|
|
| 4 |
|
| 5 |
+
from ... import (
|
| 6 |
+
extract_features,
|
| 7 |
+
localize_sfm,
|
| 8 |
+
logger,
|
| 9 |
+
match_dense,
|
| 10 |
+
pairs_from_covisibility,
|
| 11 |
+
pairs_from_retrieval,
|
| 12 |
+
triangulation,
|
| 13 |
+
)
|
| 14 |
|
| 15 |
|
| 16 |
+
def run(args):
|
| 17 |
+
# Setup the paths
|
| 18 |
+
dataset = args.dataset
|
| 19 |
+
images = dataset / "images_upright/"
|
| 20 |
+
sift_sfm = dataset / "3D-models/aachen_v_1_1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
outputs = args.outputs # where everything will be saved
|
| 23 |
+
outputs.mkdir()
|
| 24 |
+
reference_sfm = outputs / "sfm_loftr" # the SfM model we will build
|
| 25 |
+
sfm_pairs = (
|
| 26 |
+
outputs / f"pairs-db-covis{args.num_covis}.txt"
|
| 27 |
+
) # top-k most covisible in SIFT model
|
| 28 |
+
loc_pairs = (
|
| 29 |
+
outputs / f"pairs-query-netvlad{args.num_loc}.txt"
|
| 30 |
+
) # top-k retrieved by NetVLAD
|
| 31 |
+
results = outputs / f"Aachen-v1.1_hloc_loftr_netvlad{args.num_loc}.txt"
|
| 32 |
|
| 33 |
+
# list the standard configurations available
|
| 34 |
+
logger.info("Configs for dense feature matchers:\n%s", pformat(match_dense.confs))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
# pick one of the configurations for extraction and matching
|
| 37 |
+
retrieval_conf = extract_features.confs["netvlad"]
|
| 38 |
+
matcher_conf = match_dense.confs["loftr_aachen"]
|
| 39 |
|
| 40 |
+
pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=args.num_covis)
|
| 41 |
+
features, sfm_matches = match_dense.main(
|
| 42 |
+
matcher_conf, sfm_pairs, images, outputs, max_kps=8192, overwrite=False
|
| 43 |
+
)
|
| 44 |
|
| 45 |
+
triangulation.main(
|
| 46 |
+
reference_sfm, sift_sfm, images, sfm_pairs, features, sfm_matches
|
| 47 |
+
)
|
|
|
|
| 48 |
|
| 49 |
+
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
|
| 50 |
+
pairs_from_retrieval.main(
|
| 51 |
+
global_descriptors,
|
| 52 |
+
loc_pairs,
|
| 53 |
+
args.num_loc,
|
| 54 |
+
query_prefix="query",
|
| 55 |
+
db_model=reference_sfm,
|
| 56 |
+
)
|
| 57 |
+
features, loc_matches = match_dense.main(
|
| 58 |
+
matcher_conf,
|
| 59 |
+
loc_pairs,
|
| 60 |
+
images,
|
| 61 |
+
outputs,
|
| 62 |
+
features=features,
|
| 63 |
+
max_kps=None,
|
| 64 |
+
matches=sfm_matches,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
localize_sfm.main(
|
| 68 |
+
reference_sfm,
|
| 69 |
+
dataset / "queries/*_time_queries_with_intrinsics.txt",
|
| 70 |
+
loc_pairs,
|
| 71 |
+
features,
|
| 72 |
+
loc_matches,
|
| 73 |
+
results,
|
| 74 |
+
covisibility_clustering=False,
|
| 75 |
+
) # not required with loftr
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
parser = argparse.ArgumentParser()
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--dataset",
|
| 82 |
+
type=Path,
|
| 83 |
+
default="datasets/aachen_v1.1",
|
| 84 |
+
help="Path to the dataset, default: %(default)s",
|
| 85 |
+
)
|
| 86 |
+
parser.add_argument(
|
| 87 |
+
"--outputs",
|
| 88 |
+
type=Path,
|
| 89 |
+
default="outputs/aachen_v1.1",
|
| 90 |
+
help="Path to the output directory, default: %(default)s",
|
| 91 |
+
)
|
| 92 |
+
parser.add_argument(
|
| 93 |
+
"--num_covis",
|
| 94 |
+
type=int,
|
| 95 |
+
default=20,
|
| 96 |
+
help="Number of image pairs for SfM, default: %(default)s",
|
| 97 |
+
)
|
| 98 |
+
parser.add_argument(
|
| 99 |
+
"--num_loc",
|
| 100 |
+
type=int,
|
| 101 |
+
default=50,
|
| 102 |
+
help="Number of image pairs for loc, default: %(default)s",
|
| 103 |
+
)
|
| 104 |
+
args = parser.parse_args()
|
hloc/pipelines/CMU/pipeline.py
CHANGED
|
@@ -1,8 +1,15 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
import argparse
|
|
|
|
| 3 |
|
| 4 |
-
from ... import
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
TEST_SLICES = [2, 3, 4, 5, 6, 13, 14, 15, 16, 17, 18, 19, 20, 21]
|
| 8 |
|
|
@@ -46,34 +53,20 @@ def run_slice(slice_, root, outputs, num_covis, num_loc):
|
|
| 46 |
matcher_conf = match_features.confs["superglue"]
|
| 47 |
|
| 48 |
pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=num_covis)
|
| 49 |
-
features = extract_features.main(
|
| 50 |
-
feature_conf, ref_images, outputs, as_half=True
|
| 51 |
-
)
|
| 52 |
sfm_matches = match_features.main(
|
| 53 |
matcher_conf, sfm_pairs, feature_conf["output"], outputs
|
| 54 |
)
|
| 55 |
-
triangulation.main(
|
| 56 |
-
ref_sfm, sift_sfm, ref_images, sfm_pairs, features, sfm_matches
|
| 57 |
-
)
|
| 58 |
|
| 59 |
generate_query_list(root, query_list, slice_)
|
| 60 |
-
global_descriptors = extract_features.main(
|
| 61 |
-
|
| 62 |
-
)
|
| 63 |
-
global_descriptors = extract_features.main(
|
| 64 |
-
retrieval_conf, query_images, outputs
|
| 65 |
-
)
|
| 66 |
pairs_from_retrieval.main(
|
| 67 |
-
global_descriptors,
|
| 68 |
-
loc_pairs,
|
| 69 |
-
num_loc,
|
| 70 |
-
query_list=query_list,
|
| 71 |
-
db_model=ref_sfm,
|
| 72 |
)
|
| 73 |
|
| 74 |
-
features = extract_features.main(
|
| 75 |
-
feature_conf, query_images, outputs, as_half=True
|
| 76 |
-
)
|
| 77 |
loc_matches = match_features.main(
|
| 78 |
matcher_conf, loc_pairs, feature_conf["output"], outputs
|
| 79 |
)
|
|
@@ -136,9 +129,5 @@ if __name__ == "__main__":
|
|
| 136 |
for slice_ in slices:
|
| 137 |
logger.info("Working on slice %s.", slice_)
|
| 138 |
run_slice(
|
| 139 |
-
f"slice{slice_}",
|
| 140 |
-
args.dataset,
|
| 141 |
-
args.outputs,
|
| 142 |
-
args.num_covis,
|
| 143 |
-
args.num_loc,
|
| 144 |
)
|
|
|
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
from pathlib import Path
|
| 3 |
|
| 4 |
+
from ... import (
|
| 5 |
+
extract_features,
|
| 6 |
+
localize_sfm,
|
| 7 |
+
logger,
|
| 8 |
+
match_features,
|
| 9 |
+
pairs_from_covisibility,
|
| 10 |
+
pairs_from_retrieval,
|
| 11 |
+
triangulation,
|
| 12 |
+
)
|
| 13 |
|
| 14 |
TEST_SLICES = [2, 3, 4, 5, 6, 13, 14, 15, 16, 17, 18, 19, 20, 21]
|
| 15 |
|
|
|
|
| 53 |
matcher_conf = match_features.confs["superglue"]
|
| 54 |
|
| 55 |
pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=num_covis)
|
| 56 |
+
features = extract_features.main(feature_conf, ref_images, outputs, as_half=True)
|
|
|
|
|
|
|
| 57 |
sfm_matches = match_features.main(
|
| 58 |
matcher_conf, sfm_pairs, feature_conf["output"], outputs
|
| 59 |
)
|
| 60 |
+
triangulation.main(ref_sfm, sift_sfm, ref_images, sfm_pairs, features, sfm_matches)
|
|
|
|
|
|
|
| 61 |
|
| 62 |
generate_query_list(root, query_list, slice_)
|
| 63 |
+
global_descriptors = extract_features.main(retrieval_conf, ref_images, outputs)
|
| 64 |
+
global_descriptors = extract_features.main(retrieval_conf, query_images, outputs)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
pairs_from_retrieval.main(
|
| 66 |
+
global_descriptors, loc_pairs, num_loc, query_list=query_list, db_model=ref_sfm
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
)
|
| 68 |
|
| 69 |
+
features = extract_features.main(feature_conf, query_images, outputs, as_half=True)
|
|
|
|
|
|
|
| 70 |
loc_matches = match_features.main(
|
| 71 |
matcher_conf, loc_pairs, feature_conf["output"], outputs
|
| 72 |
)
|
|
|
|
| 129 |
for slice_ in slices:
|
| 130 |
logger.info("Working on slice %s.", slice_)
|
| 131 |
run_slice(
|
| 132 |
+
f"slice{slice_}", args.dataset, args.outputs, args.num_covis, args.num_loc
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
)
|
hloc/pipelines/Cambridge/pipeline.py
CHANGED
|
@@ -1,17 +1,18 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
import argparse
|
|
|
|
| 3 |
|
| 4 |
-
from
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
SCENES = [
|
| 9 |
-
"KingsCollege",
|
| 10 |
-
"OldHospital",
|
| 11 |
-
"ShopFacade",
|
| 12 |
-
"StMarysChurch",
|
| 13 |
-
"GreatCourt",
|
| 14 |
-
]
|
| 15 |
|
| 16 |
|
| 17 |
def run_scene(images, gt_dir, outputs, results, num_covis, num_loc):
|
|
@@ -41,11 +42,7 @@ def run_scene(images, gt_dir, outputs, results, num_covis, num_loc):
|
|
| 41 |
retrieval_conf = extract_features.confs["netvlad"]
|
| 42 |
|
| 43 |
create_query_list_with_intrinsics(
|
| 44 |
-
gt_dir / "empty_all",
|
| 45 |
-
query_list,
|
| 46 |
-
test_list,
|
| 47 |
-
ext=".txt",
|
| 48 |
-
image_dir=images,
|
| 49 |
)
|
| 50 |
with open(test_list, "r") as f:
|
| 51 |
query_seqs = {q.split("/")[0] for q in f.read().rstrip().split("\n")}
|
|
@@ -59,9 +56,7 @@ def run_scene(images, gt_dir, outputs, results, num_covis, num_loc):
|
|
| 59 |
query_prefix=query_seqs,
|
| 60 |
)
|
| 61 |
|
| 62 |
-
features = extract_features.main(
|
| 63 |
-
feature_conf, images, outputs, as_half=True
|
| 64 |
-
)
|
| 65 |
pairs_from_covisibility.main(ref_sfm_sift, sfm_pairs, num_matched=num_covis)
|
| 66 |
sfm_matches = match_features.main(
|
| 67 |
matcher_conf, sfm_pairs, feature_conf["output"], outputs
|
|
|
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
from pathlib import Path
|
| 3 |
|
| 4 |
+
from ... import (
|
| 5 |
+
extract_features,
|
| 6 |
+
localize_sfm,
|
| 7 |
+
logger,
|
| 8 |
+
match_features,
|
| 9 |
+
pairs_from_covisibility,
|
| 10 |
+
pairs_from_retrieval,
|
| 11 |
+
triangulation,
|
| 12 |
+
)
|
| 13 |
+
from .utils import create_query_list_with_intrinsics, evaluate, scale_sfm_images
|
| 14 |
|
| 15 |
+
SCENES = ["KingsCollege", "OldHospital", "ShopFacade", "StMarysChurch", "GreatCourt"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
def run_scene(images, gt_dir, outputs, results, num_covis, num_loc):
|
|
|
|
| 42 |
retrieval_conf = extract_features.confs["netvlad"]
|
| 43 |
|
| 44 |
create_query_list_with_intrinsics(
|
| 45 |
+
gt_dir / "empty_all", query_list, test_list, ext=".txt", image_dir=images
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
)
|
| 47 |
with open(test_list, "r") as f:
|
| 48 |
query_seqs = {q.split("/")[0] for q in f.read().rstrip().split("\n")}
|
|
|
|
| 56 |
query_prefix=query_seqs,
|
| 57 |
)
|
| 58 |
|
| 59 |
+
features = extract_features.main(feature_conf, images, outputs, as_half=True)
|
|
|
|
|
|
|
| 60 |
pairs_from_covisibility.main(ref_sfm_sift, sfm_pairs, num_matched=num_covis)
|
| 61 |
sfm_matches = match_features.main(
|
| 62 |
matcher_conf, sfm_pairs, feature_conf["output"], outputs
|
hloc/pipelines/Cambridge/utils.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
| 1 |
-
import cv2
|
| 2 |
import logging
|
|
|
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
|
| 5 |
from hloc.utils.read_write_model import (
|
|
|
|
| 6 |
read_cameras_binary,
|
|
|
|
| 7 |
read_images_binary,
|
|
|
|
| 8 |
read_model,
|
| 9 |
write_model,
|
| 10 |
-
qvec2rotmat,
|
| 11 |
-
read_images_text,
|
| 12 |
-
read_cameras_text,
|
| 13 |
)
|
| 14 |
|
| 15 |
logger = logging.getLogger(__name__)
|
|
@@ -42,9 +43,7 @@ def scale_sfm_images(full_model, scaled_model, image_dir):
|
|
| 42 |
sy = h / camera.height
|
| 43 |
assert sx == sy, (sx, sy)
|
| 44 |
scaled_cameras[cam_id] = camera._replace(
|
| 45 |
-
width=w,
|
| 46 |
-
height=h,
|
| 47 |
-
params=camera.params * np.array([sx, sx, sy, 1.0]),
|
| 48 |
)
|
| 49 |
|
| 50 |
write_model(scaled_cameras, images, points3D, scaled_model)
|
|
|
|
|
|
|
| 1 |
import logging
|
| 2 |
+
|
| 3 |
+
import cv2
|
| 4 |
import numpy as np
|
| 5 |
|
| 6 |
from hloc.utils.read_write_model import (
|
| 7 |
+
qvec2rotmat,
|
| 8 |
read_cameras_binary,
|
| 9 |
+
read_cameras_text,
|
| 10 |
read_images_binary,
|
| 11 |
+
read_images_text,
|
| 12 |
read_model,
|
| 13 |
write_model,
|
|
|
|
|
|
|
|
|
|
| 14 |
)
|
| 15 |
|
| 16 |
logger = logging.getLogger(__name__)
|
|
|
|
| 43 |
sy = h / camera.height
|
| 44 |
assert sx == sy, (sx, sy)
|
| 45 |
scaled_cameras[cam_id] = camera._replace(
|
| 46 |
+
width=w, height=h, params=camera.params * np.array([sx, sx, sy, 1.0])
|
|
|
|
|
|
|
| 47 |
)
|
| 48 |
|
| 49 |
write_model(scaled_cameras, images, points3D, scaled_model)
|
hloc/pipelines/RobotCar/colmap_from_nvm.py
CHANGED
|
@@ -1,29 +1,31 @@
|
|
| 1 |
import argparse
|
|
|
|
| 2 |
import sqlite3
|
| 3 |
-
from tqdm import tqdm
|
| 4 |
from collections import defaultdict
|
| 5 |
-
import numpy as np
|
| 6 |
from pathlib import Path
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
|
| 9 |
from ...colmap_from_nvm import (
|
| 10 |
-
recover_database_images_and_ids,
|
| 11 |
camera_center_to_translation,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
)
|
| 13 |
-
from ...utils.read_write_model import Camera, Image, Point3D, CAMERA_MODEL_IDS
|
| 14 |
-
from ...utils.read_write_model import write_model
|
| 15 |
|
| 16 |
logger = logging.getLogger(__name__)
|
| 17 |
|
| 18 |
|
| 19 |
-
def read_nvm_model(
|
| 20 |
-
nvm_path, database_path, image_ids, camera_ids, skip_points=False
|
| 21 |
-
):
|
| 22 |
# Extract the intrinsics from the db file instead of the NVM model
|
| 23 |
db = sqlite3.connect(str(database_path))
|
| 24 |
-
ret = db.execute(
|
| 25 |
-
"SELECT camera_id, model, width, height, params FROM cameras;"
|
| 26 |
-
)
|
| 27 |
cameras = {}
|
| 28 |
for camera_id, camera_model, width, height, params in ret:
|
| 29 |
params = np.fromstring(params, dtype=np.double).reshape(-1)
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
import logging
|
| 3 |
import sqlite3
|
|
|
|
| 4 |
from collections import defaultdict
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
|
| 10 |
from ...colmap_from_nvm import (
|
|
|
|
| 11 |
camera_center_to_translation,
|
| 12 |
+
recover_database_images_and_ids,
|
| 13 |
+
)
|
| 14 |
+
from ...utils.read_write_model import (
|
| 15 |
+
CAMERA_MODEL_IDS,
|
| 16 |
+
Camera,
|
| 17 |
+
Image,
|
| 18 |
+
Point3D,
|
| 19 |
+
write_model,
|
| 20 |
)
|
|
|
|
|
|
|
| 21 |
|
| 22 |
logger = logging.getLogger(__name__)
|
| 23 |
|
| 24 |
|
| 25 |
+
def read_nvm_model(nvm_path, database_path, image_ids, camera_ids, skip_points=False):
|
|
|
|
|
|
|
| 26 |
# Extract the intrinsics from the db file instead of the NVM model
|
| 27 |
db = sqlite3.connect(str(database_path))
|
| 28 |
+
ret = db.execute("SELECT camera_id, model, width, height, params FROM cameras;")
|
|
|
|
|
|
|
| 29 |
cameras = {}
|
| 30 |
for camera_id, camera_model, width, height, params in ret:
|
| 31 |
params = np.fromstring(params, dtype=np.double).reshape(-1)
|
hloc/pipelines/RobotCar/pipeline.py
CHANGED
|
@@ -1,10 +1,16 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
import argparse
|
|
|
|
|
|
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from . import colmap_from_nvm
|
| 5 |
-
from ... import extract_features, match_features, triangulation
|
| 6 |
-
from ... import pairs_from_covisibility, pairs_from_retrieval, localize_sfm
|
| 7 |
-
|
| 8 |
|
| 9 |
CONDITIONS = [
|
| 10 |
"dawn",
|
|
@@ -33,102 +39,105 @@ def generate_query_list(dataset, image_dir, path):
|
|
| 33 |
params = ["SIMPLE_RADIAL", w, h, fx, cx, cy, 0.0]
|
| 34 |
cameras[side] = [str(p) for p in params]
|
| 35 |
|
| 36 |
-
queries =
|
| 37 |
-
queries = [
|
|
|
|
|
|
|
| 38 |
|
| 39 |
out = [[q] + cameras[Path(q).parent.name] for q in queries]
|
| 40 |
with open(path, "w") as f:
|
| 41 |
f.write("\n".join(map(" ".join, out)))
|
| 42 |
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
dataset
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
sift_sfm = outputs / "sfm_sift"
|
| 79 |
-
reference_sfm = outputs / "sfm_superpoint+superglue"
|
| 80 |
-
sfm_pairs = outputs / f"pairs-db-covis{args.num_covis}.txt"
|
| 81 |
-
loc_pairs = outputs / f"pairs-query-netvlad{args.num_loc}.txt"
|
| 82 |
-
results = (
|
| 83 |
-
outputs / f"RobotCar_hloc_superpoint+superglue_netvlad{args.num_loc}.txt"
|
| 84 |
-
)
|
| 85 |
-
|
| 86 |
-
# pick one of the configurations for extraction and matching
|
| 87 |
-
retrieval_conf = extract_features.confs["netvlad"]
|
| 88 |
-
feature_conf = extract_features.confs["superpoint_aachen"]
|
| 89 |
-
matcher_conf = match_features.confs["superglue"]
|
| 90 |
-
|
| 91 |
-
for condition in CONDITIONS:
|
| 92 |
-
generate_query_list(
|
| 93 |
-
dataset, images / condition, str(query_list).format(condition=condition)
|
| 94 |
)
|
| 95 |
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
)
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
|
| 113 |
-
# TODO: do per location and per camera
|
| 114 |
-
pairs_from_retrieval.main(
|
| 115 |
-
global_descriptors,
|
| 116 |
-
loc_pairs,
|
| 117 |
-
args.num_loc,
|
| 118 |
-
query_prefix=CONDITIONS,
|
| 119 |
-
db_model=reference_sfm,
|
| 120 |
-
)
|
| 121 |
-
loc_matches = match_features.main(
|
| 122 |
-
matcher_conf, loc_pairs, feature_conf["output"], outputs
|
| 123 |
-
)
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
import glob
|
| 3 |
+
from pathlib import Path
|
| 4 |
|
| 5 |
+
from ... import (
|
| 6 |
+
extract_features,
|
| 7 |
+
localize_sfm,
|
| 8 |
+
match_features,
|
| 9 |
+
pairs_from_covisibility,
|
| 10 |
+
pairs_from_retrieval,
|
| 11 |
+
triangulation,
|
| 12 |
+
)
|
| 13 |
from . import colmap_from_nvm
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
CONDITIONS = [
|
| 16 |
"dawn",
|
|
|
|
| 39 |
params = ["SIMPLE_RADIAL", w, h, fx, cx, cy, 0.0]
|
| 40 |
cameras[side] = [str(p) for p in params]
|
| 41 |
|
| 42 |
+
queries = glob.glob((image_dir / "**/*.jpg").as_posix(), recursive=True)
|
| 43 |
+
queries = [
|
| 44 |
+
Path(q).relative_to(image_dir.parents[0]).as_posix() for q in sorted(queries)
|
| 45 |
+
]
|
| 46 |
|
| 47 |
out = [[q] + cameras[Path(q).parent.name] for q in queries]
|
| 48 |
with open(path, "w") as f:
|
| 49 |
f.write("\n".join(map(" ".join, out)))
|
| 50 |
|
| 51 |
|
| 52 |
+
def run(args):
|
| 53 |
+
# Setup the paths
|
| 54 |
+
dataset = args.dataset
|
| 55 |
+
images = dataset / "images/"
|
| 56 |
+
|
| 57 |
+
outputs = args.outputs # where everything will be saved
|
| 58 |
+
outputs.mkdir(exist_ok=True, parents=True)
|
| 59 |
+
query_list = outputs / "{condition}_queries_with_intrinsics.txt"
|
| 60 |
+
sift_sfm = outputs / "sfm_sift"
|
| 61 |
+
reference_sfm = outputs / "sfm_superpoint+superglue"
|
| 62 |
+
sfm_pairs = outputs / f"pairs-db-covis{args.num_covis}.txt"
|
| 63 |
+
loc_pairs = outputs / f"pairs-query-netvlad{args.num_loc}.txt"
|
| 64 |
+
results = outputs / f"RobotCar_hloc_superpoint+superglue_netvlad{args.num_loc}.txt"
|
| 65 |
+
|
| 66 |
+
# pick one of the configurations for extraction and matching
|
| 67 |
+
retrieval_conf = extract_features.confs["netvlad"]
|
| 68 |
+
feature_conf = extract_features.confs["superpoint_aachen"]
|
| 69 |
+
matcher_conf = match_features.confs["superglue"]
|
| 70 |
+
|
| 71 |
+
for condition in CONDITIONS:
|
| 72 |
+
generate_query_list(
|
| 73 |
+
dataset, images / condition, str(query_list).format(condition=condition)
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
features = extract_features.main(feature_conf, images, outputs, as_half=True)
|
| 77 |
+
|
| 78 |
+
colmap_from_nvm.main(
|
| 79 |
+
dataset / "3D-models/all-merged/all.nvm",
|
| 80 |
+
dataset / "3D-models/overcast-reference.db",
|
| 81 |
+
sift_sfm,
|
| 82 |
+
)
|
| 83 |
+
pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=args.num_covis)
|
| 84 |
+
sfm_matches = match_features.main(
|
| 85 |
+
matcher_conf, sfm_pairs, feature_conf["output"], outputs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
)
|
| 87 |
|
| 88 |
+
triangulation.main(
|
| 89 |
+
reference_sfm, sift_sfm, images, sfm_pairs, features, sfm_matches
|
| 90 |
+
)
|
| 91 |
|
| 92 |
+
global_descriptors = extract_features.main(retrieval_conf, images, outputs)
|
| 93 |
+
# TODO: do per location and per camera
|
| 94 |
+
pairs_from_retrieval.main(
|
| 95 |
+
global_descriptors,
|
| 96 |
+
loc_pairs,
|
| 97 |
+
args.num_loc,
|
| 98 |
+
query_prefix=CONDITIONS,
|
| 99 |
+
db_model=reference_sfm,
|
| 100 |
+
)
|
| 101 |
+
loc_matches = match_features.main(
|
| 102 |
+
matcher_conf, loc_pairs, feature_conf["output"], outputs
|
| 103 |
+
)
|
| 104 |
|
| 105 |
+
localize_sfm.main(
|
| 106 |
+
reference_sfm,
|
| 107 |
+
Path(str(query_list).format(condition="*")),
|
| 108 |
+
loc_pairs,
|
| 109 |
+
features,
|
| 110 |
+
loc_matches,
|
| 111 |
+
results,
|
| 112 |
+
covisibility_clustering=False,
|
| 113 |
+
prepend_camera_name=True,
|
| 114 |
+
)
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
+
if __name__ == "__main__":
|
| 118 |
+
parser = argparse.ArgumentParser()
|
| 119 |
+
parser.add_argument(
|
| 120 |
+
"--dataset",
|
| 121 |
+
type=Path,
|
| 122 |
+
default="datasets/robotcar",
|
| 123 |
+
help="Path to the dataset, default: %(default)s",
|
| 124 |
+
)
|
| 125 |
+
parser.add_argument(
|
| 126 |
+
"--outputs",
|
| 127 |
+
type=Path,
|
| 128 |
+
default="outputs/robotcar",
|
| 129 |
+
help="Path to the output directory, default: %(default)s",
|
| 130 |
+
)
|
| 131 |
+
parser.add_argument(
|
| 132 |
+
"--num_covis",
|
| 133 |
+
type=int,
|
| 134 |
+
default=20,
|
| 135 |
+
help="Number of image pairs for SfM, default: %(default)s",
|
| 136 |
+
)
|
| 137 |
+
parser.add_argument(
|
| 138 |
+
"--num_loc",
|
| 139 |
+
type=int,
|
| 140 |
+
default=20,
|
| 141 |
+
help="Number of image pairs for loc, default: %(default)s",
|
| 142 |
+
)
|
| 143 |
+
args = parser.parse_args()
|
hloc/reconstruction.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import multiprocessing
|
| 3 |
+
import shutil
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any, Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
import pycolmap
|
| 8 |
+
|
| 9 |
+
from . import logger
|
| 10 |
+
from .triangulation import (
|
| 11 |
+
OutputCapture,
|
| 12 |
+
estimation_and_geometric_verification,
|
| 13 |
+
import_features,
|
| 14 |
+
import_matches,
|
| 15 |
+
parse_option_args,
|
| 16 |
+
)
|
| 17 |
+
from .utils.database import COLMAPDatabase
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def create_empty_db(database_path: Path):
|
| 21 |
+
if database_path.exists():
|
| 22 |
+
logger.warning("The database already exists, deleting it.")
|
| 23 |
+
database_path.unlink()
|
| 24 |
+
logger.info("Creating an empty database...")
|
| 25 |
+
db = COLMAPDatabase.connect(database_path)
|
| 26 |
+
db.create_tables()
|
| 27 |
+
db.commit()
|
| 28 |
+
db.close()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def import_images(
|
| 32 |
+
image_dir: Path,
|
| 33 |
+
database_path: Path,
|
| 34 |
+
camera_mode: pycolmap.CameraMode,
|
| 35 |
+
image_list: Optional[List[str]] = None,
|
| 36 |
+
options: Optional[Dict[str, Any]] = None,
|
| 37 |
+
):
|
| 38 |
+
logger.info("Importing images into the database...")
|
| 39 |
+
if options is None:
|
| 40 |
+
options = {}
|
| 41 |
+
images = list(image_dir.iterdir())
|
| 42 |
+
if len(images) == 0:
|
| 43 |
+
raise IOError(f"No images found in {image_dir}.")
|
| 44 |
+
with pycolmap.ostream():
|
| 45 |
+
pycolmap.import_images(
|
| 46 |
+
database_path,
|
| 47 |
+
image_dir,
|
| 48 |
+
camera_mode,
|
| 49 |
+
image_list=image_list or [],
|
| 50 |
+
options=options,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def get_image_ids(database_path: Path) -> Dict[str, int]:
|
| 55 |
+
db = COLMAPDatabase.connect(database_path)
|
| 56 |
+
images = {}
|
| 57 |
+
for name, image_id in db.execute("SELECT name, image_id FROM images;"):
|
| 58 |
+
images[name] = image_id
|
| 59 |
+
db.close()
|
| 60 |
+
return images
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def run_reconstruction(
|
| 64 |
+
sfm_dir: Path,
|
| 65 |
+
database_path: Path,
|
| 66 |
+
image_dir: Path,
|
| 67 |
+
verbose: bool = False,
|
| 68 |
+
options: Optional[Dict[str, Any]] = None,
|
| 69 |
+
) -> pycolmap.Reconstruction:
|
| 70 |
+
models_path = sfm_dir / "models"
|
| 71 |
+
models_path.mkdir(exist_ok=True, parents=True)
|
| 72 |
+
logger.info("Running 3D reconstruction...")
|
| 73 |
+
if options is None:
|
| 74 |
+
options = {}
|
| 75 |
+
options = {"num_threads": min(multiprocessing.cpu_count(), 16), **options}
|
| 76 |
+
with OutputCapture(verbose):
|
| 77 |
+
with pycolmap.ostream():
|
| 78 |
+
reconstructions = pycolmap.incremental_mapping(
|
| 79 |
+
database_path, image_dir, models_path, options=options
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
if len(reconstructions) == 0:
|
| 83 |
+
logger.error("Could not reconstruct any model!")
|
| 84 |
+
return None
|
| 85 |
+
logger.info(f"Reconstructed {len(reconstructions)} model(s).")
|
| 86 |
+
|
| 87 |
+
largest_index = None
|
| 88 |
+
largest_num_images = 0
|
| 89 |
+
for index, rec in reconstructions.items():
|
| 90 |
+
num_images = rec.num_reg_images()
|
| 91 |
+
if num_images > largest_num_images:
|
| 92 |
+
largest_index = index
|
| 93 |
+
largest_num_images = num_images
|
| 94 |
+
assert largest_index is not None
|
| 95 |
+
logger.info(
|
| 96 |
+
f"Largest model is #{largest_index} " f"with {largest_num_images} images."
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
for filename in ["images.bin", "cameras.bin", "points3D.bin"]:
|
| 100 |
+
if (sfm_dir / filename).exists():
|
| 101 |
+
(sfm_dir / filename).unlink()
|
| 102 |
+
shutil.move(str(models_path / str(largest_index) / filename), str(sfm_dir))
|
| 103 |
+
return reconstructions[largest_index]
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def main(
|
| 107 |
+
sfm_dir: Path,
|
| 108 |
+
image_dir: Path,
|
| 109 |
+
pairs: Path,
|
| 110 |
+
features: Path,
|
| 111 |
+
matches: Path,
|
| 112 |
+
camera_mode: pycolmap.CameraMode = pycolmap.CameraMode.AUTO,
|
| 113 |
+
verbose: bool = False,
|
| 114 |
+
skip_geometric_verification: bool = False,
|
| 115 |
+
min_match_score: Optional[float] = None,
|
| 116 |
+
image_list: Optional[List[str]] = None,
|
| 117 |
+
image_options: Optional[Dict[str, Any]] = None,
|
| 118 |
+
mapper_options: Optional[Dict[str, Any]] = None,
|
| 119 |
+
) -> pycolmap.Reconstruction:
|
| 120 |
+
assert features.exists(), features
|
| 121 |
+
assert pairs.exists(), pairs
|
| 122 |
+
assert matches.exists(), matches
|
| 123 |
+
|
| 124 |
+
sfm_dir.mkdir(parents=True, exist_ok=True)
|
| 125 |
+
database = sfm_dir / "database.db"
|
| 126 |
+
|
| 127 |
+
create_empty_db(database)
|
| 128 |
+
import_images(image_dir, database, camera_mode, image_list, image_options)
|
| 129 |
+
image_ids = get_image_ids(database)
|
| 130 |
+
import_features(image_ids, database, features)
|
| 131 |
+
import_matches(
|
| 132 |
+
image_ids,
|
| 133 |
+
database,
|
| 134 |
+
pairs,
|
| 135 |
+
matches,
|
| 136 |
+
min_match_score,
|
| 137 |
+
skip_geometric_verification,
|
| 138 |
+
)
|
| 139 |
+
if not skip_geometric_verification:
|
| 140 |
+
estimation_and_geometric_verification(database, pairs, verbose)
|
| 141 |
+
reconstruction = run_reconstruction(
|
| 142 |
+
sfm_dir, database, image_dir, verbose, mapper_options
|
| 143 |
+
)
|
| 144 |
+
if reconstruction is not None:
|
| 145 |
+
logger.info(
|
| 146 |
+
f"Reconstruction statistics:\n{reconstruction.summary()}"
|
| 147 |
+
+ f"\n\tnum_input_images = {len(image_ids)}"
|
| 148 |
+
)
|
| 149 |
+
return reconstruction
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
if __name__ == "__main__":
|
| 153 |
+
parser = argparse.ArgumentParser()
|
| 154 |
+
parser.add_argument("--sfm_dir", type=Path, required=True)
|
| 155 |
+
parser.add_argument("--image_dir", type=Path, required=True)
|
| 156 |
+
|
| 157 |
+
parser.add_argument("--pairs", type=Path, required=True)
|
| 158 |
+
parser.add_argument("--features", type=Path, required=True)
|
| 159 |
+
parser.add_argument("--matches", type=Path, required=True)
|
| 160 |
+
|
| 161 |
+
parser.add_argument(
|
| 162 |
+
"--camera_mode",
|
| 163 |
+
type=str,
|
| 164 |
+
default="AUTO",
|
| 165 |
+
choices=list(pycolmap.CameraMode.__members__.keys()),
|
| 166 |
+
)
|
| 167 |
+
parser.add_argument("--skip_geometric_verification", action="store_true")
|
| 168 |
+
parser.add_argument("--min_match_score", type=float)
|
| 169 |
+
parser.add_argument("--verbose", action="store_true")
|
| 170 |
+
|
| 171 |
+
parser.add_argument(
|
| 172 |
+
"--image_options",
|
| 173 |
+
nargs="+",
|
| 174 |
+
default=[],
|
| 175 |
+
help="List of key=value from {}".format(pycolmap.ImageReaderOptions().todict()),
|
| 176 |
+
)
|
| 177 |
+
parser.add_argument(
|
| 178 |
+
"--mapper_options",
|
| 179 |
+
nargs="+",
|
| 180 |
+
default=[],
|
| 181 |
+
help="List of key=value from {}".format(
|
| 182 |
+
pycolmap.IncrementalMapperOptions().todict()
|
| 183 |
+
),
|
| 184 |
+
)
|
| 185 |
+
args = parser.parse_args().__dict__
|
| 186 |
+
|
| 187 |
+
image_options = parse_option_args(
|
| 188 |
+
args.pop("image_options"), pycolmap.ImageReaderOptions()
|
| 189 |
+
)
|
| 190 |
+
mapper_options = parse_option_args(
|
| 191 |
+
args.pop("mapper_options"), pycolmap.IncrementalMapperOptions()
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
main(**args, image_options=image_options, mapper_options=mapper_options)
|
hloc/triangulation.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import contextlib
|
| 3 |
+
import io
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pycolmap
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
|
| 12 |
+
from . import logger
|
| 13 |
+
from .utils.database import COLMAPDatabase
|
| 14 |
+
from .utils.geometry import compute_epipolar_errors
|
| 15 |
+
from .utils.io import get_keypoints, get_matches
|
| 16 |
+
from .utils.parsers import parse_retrieval
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class OutputCapture:
|
| 20 |
+
def __init__(self, verbose: bool):
|
| 21 |
+
self.verbose = verbose
|
| 22 |
+
|
| 23 |
+
def __enter__(self):
|
| 24 |
+
if not self.verbose:
|
| 25 |
+
self.capture = contextlib.redirect_stdout(io.StringIO())
|
| 26 |
+
self.out = self.capture.__enter__()
|
| 27 |
+
|
| 28 |
+
def __exit__(self, exc_type, *args):
|
| 29 |
+
if not self.verbose:
|
| 30 |
+
self.capture.__exit__(exc_type, *args)
|
| 31 |
+
if exc_type is not None:
|
| 32 |
+
logger.error("Failed with output:\n%s", self.out.getvalue())
|
| 33 |
+
sys.stdout.flush()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def create_db_from_model(
|
| 37 |
+
reconstruction: pycolmap.Reconstruction, database_path: Path
|
| 38 |
+
) -> Dict[str, int]:
|
| 39 |
+
if database_path.exists():
|
| 40 |
+
logger.warning("The database already exists, deleting it.")
|
| 41 |
+
database_path.unlink()
|
| 42 |
+
|
| 43 |
+
db = COLMAPDatabase.connect(database_path)
|
| 44 |
+
db.create_tables()
|
| 45 |
+
|
| 46 |
+
for i, camera in reconstruction.cameras.items():
|
| 47 |
+
db.add_camera(
|
| 48 |
+
camera.model.value,
|
| 49 |
+
camera.width,
|
| 50 |
+
camera.height,
|
| 51 |
+
camera.params,
|
| 52 |
+
camera_id=i,
|
| 53 |
+
prior_focal_length=True,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
for i, image in reconstruction.images.items():
|
| 57 |
+
db.add_image(image.name, image.camera_id, image_id=i)
|
| 58 |
+
|
| 59 |
+
db.commit()
|
| 60 |
+
db.close()
|
| 61 |
+
return {image.name: i for i, image in reconstruction.images.items()}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def import_features(
|
| 65 |
+
image_ids: Dict[str, int], database_path: Path, features_path: Path
|
| 66 |
+
):
|
| 67 |
+
logger.info("Importing features into the database...")
|
| 68 |
+
db = COLMAPDatabase.connect(database_path)
|
| 69 |
+
|
| 70 |
+
for image_name, image_id in tqdm(image_ids.items()):
|
| 71 |
+
keypoints = get_keypoints(features_path, image_name)
|
| 72 |
+
keypoints += 0.5 # COLMAP origin
|
| 73 |
+
db.add_keypoints(image_id, keypoints)
|
| 74 |
+
|
| 75 |
+
db.commit()
|
| 76 |
+
db.close()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def import_matches(
|
| 80 |
+
image_ids: Dict[str, int],
|
| 81 |
+
database_path: Path,
|
| 82 |
+
pairs_path: Path,
|
| 83 |
+
matches_path: Path,
|
| 84 |
+
min_match_score: Optional[float] = None,
|
| 85 |
+
skip_geometric_verification: bool = False,
|
| 86 |
+
):
|
| 87 |
+
logger.info("Importing matches into the database...")
|
| 88 |
+
|
| 89 |
+
with open(str(pairs_path), "r") as f:
|
| 90 |
+
pairs = [p.split() for p in f.readlines()]
|
| 91 |
+
|
| 92 |
+
db = COLMAPDatabase.connect(database_path)
|
| 93 |
+
|
| 94 |
+
matched = set()
|
| 95 |
+
for name0, name1 in tqdm(pairs):
|
| 96 |
+
id0, id1 = image_ids[name0], image_ids[name1]
|
| 97 |
+
if len({(id0, id1), (id1, id0)} & matched) > 0:
|
| 98 |
+
continue
|
| 99 |
+
matches, scores = get_matches(matches_path, name0, name1)
|
| 100 |
+
if min_match_score:
|
| 101 |
+
matches = matches[scores > min_match_score]
|
| 102 |
+
db.add_matches(id0, id1, matches)
|
| 103 |
+
matched |= {(id0, id1), (id1, id0)}
|
| 104 |
+
|
| 105 |
+
if skip_geometric_verification:
|
| 106 |
+
db.add_two_view_geometry(id0, id1, matches)
|
| 107 |
+
|
| 108 |
+
db.commit()
|
| 109 |
+
db.close()
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def estimation_and_geometric_verification(
|
| 113 |
+
database_path: Path, pairs_path: Path, verbose: bool = False
|
| 114 |
+
):
|
| 115 |
+
logger.info("Performing geometric verification of the matches...")
|
| 116 |
+
with OutputCapture(verbose):
|
| 117 |
+
with pycolmap.ostream():
|
| 118 |
+
pycolmap.verify_matches(
|
| 119 |
+
database_path,
|
| 120 |
+
pairs_path,
|
| 121 |
+
options=dict(ransac=dict(max_num_trials=20000, min_inlier_ratio=0.1)),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def geometric_verification(
|
| 126 |
+
image_ids: Dict[str, int],
|
| 127 |
+
reference: pycolmap.Reconstruction,
|
| 128 |
+
database_path: Path,
|
| 129 |
+
features_path: Path,
|
| 130 |
+
pairs_path: Path,
|
| 131 |
+
matches_path: Path,
|
| 132 |
+
max_error: float = 4.0,
|
| 133 |
+
):
|
| 134 |
+
logger.info("Performing geometric verification of the matches...")
|
| 135 |
+
|
| 136 |
+
pairs = parse_retrieval(pairs_path)
|
| 137 |
+
db = COLMAPDatabase.connect(database_path)
|
| 138 |
+
|
| 139 |
+
inlier_ratios = []
|
| 140 |
+
matched = set()
|
| 141 |
+
for name0 in tqdm(pairs):
|
| 142 |
+
id0 = image_ids[name0]
|
| 143 |
+
image0 = reference.images[id0]
|
| 144 |
+
cam0 = reference.cameras[image0.camera_id]
|
| 145 |
+
kps0, noise0 = get_keypoints(features_path, name0, return_uncertainty=True)
|
| 146 |
+
noise0 = 1.0 if noise0 is None else noise0
|
| 147 |
+
if len(kps0) > 0:
|
| 148 |
+
kps0 = np.stack(cam0.cam_from_img(kps0))
|
| 149 |
+
else:
|
| 150 |
+
kps0 = np.zeros((0, 2))
|
| 151 |
+
|
| 152 |
+
for name1 in pairs[name0]:
|
| 153 |
+
id1 = image_ids[name1]
|
| 154 |
+
image1 = reference.images[id1]
|
| 155 |
+
cam1 = reference.cameras[image1.camera_id]
|
| 156 |
+
kps1, noise1 = get_keypoints(features_path, name1, return_uncertainty=True)
|
| 157 |
+
noise1 = 1.0 if noise1 is None else noise1
|
| 158 |
+
if len(kps1) > 0:
|
| 159 |
+
kps1 = np.stack(cam1.cam_from_img(kps1))
|
| 160 |
+
else:
|
| 161 |
+
kps1 = np.zeros((0, 2))
|
| 162 |
+
|
| 163 |
+
matches = get_matches(matches_path, name0, name1)[0]
|
| 164 |
+
|
| 165 |
+
if len({(id0, id1), (id1, id0)} & matched) > 0:
|
| 166 |
+
continue
|
| 167 |
+
matched |= {(id0, id1), (id1, id0)}
|
| 168 |
+
|
| 169 |
+
if matches.shape[0] == 0:
|
| 170 |
+
db.add_two_view_geometry(id0, id1, matches)
|
| 171 |
+
continue
|
| 172 |
+
|
| 173 |
+
cam1_from_cam0 = image1.cam_from_world * image0.cam_from_world.inverse()
|
| 174 |
+
errors0, errors1 = compute_epipolar_errors(
|
| 175 |
+
cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]]
|
| 176 |
+
)
|
| 177 |
+
valid_matches = np.logical_and(
|
| 178 |
+
errors0 <= cam0.cam_from_img_threshold(noise0 * max_error),
|
| 179 |
+
errors1 <= cam1.cam_from_img_threshold(noise1 * max_error),
|
| 180 |
+
)
|
| 181 |
+
# TODO: We could also add E to the database, but we need
|
| 182 |
+
# to reverse the transformations if id0 > id1 in utils/database.py.
|
| 183 |
+
db.add_two_view_geometry(id0, id1, matches[valid_matches, :])
|
| 184 |
+
inlier_ratios.append(np.mean(valid_matches))
|
| 185 |
+
logger.info(
|
| 186 |
+
"mean/med/min/max valid matches %.2f/%.2f/%.2f/%.2f%%.",
|
| 187 |
+
np.mean(inlier_ratios) * 100,
|
| 188 |
+
np.median(inlier_ratios) * 100,
|
| 189 |
+
np.min(inlier_ratios) * 100,
|
| 190 |
+
np.max(inlier_ratios) * 100,
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
db.commit()
|
| 194 |
+
db.close()
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def run_triangulation(
|
| 198 |
+
model_path: Path,
|
| 199 |
+
database_path: Path,
|
| 200 |
+
image_dir: Path,
|
| 201 |
+
reference_model: pycolmap.Reconstruction,
|
| 202 |
+
verbose: bool = False,
|
| 203 |
+
options: Optional[Dict[str, Any]] = None,
|
| 204 |
+
) -> pycolmap.Reconstruction:
|
| 205 |
+
model_path.mkdir(parents=True, exist_ok=True)
|
| 206 |
+
logger.info("Running 3D triangulation...")
|
| 207 |
+
if options is None:
|
| 208 |
+
options = {}
|
| 209 |
+
with OutputCapture(verbose):
|
| 210 |
+
with pycolmap.ostream():
|
| 211 |
+
reconstruction = pycolmap.triangulate_points(
|
| 212 |
+
reference_model, database_path, image_dir, model_path, options=options
|
| 213 |
+
)
|
| 214 |
+
return reconstruction
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def main(
|
| 218 |
+
sfm_dir: Path,
|
| 219 |
+
reference_model: Path,
|
| 220 |
+
image_dir: Path,
|
| 221 |
+
pairs: Path,
|
| 222 |
+
features: Path,
|
| 223 |
+
matches: Path,
|
| 224 |
+
skip_geometric_verification: bool = False,
|
| 225 |
+
estimate_two_view_geometries: bool = False,
|
| 226 |
+
min_match_score: Optional[float] = None,
|
| 227 |
+
verbose: bool = False,
|
| 228 |
+
mapper_options: Optional[Dict[str, Any]] = None,
|
| 229 |
+
) -> pycolmap.Reconstruction:
|
| 230 |
+
assert reference_model.exists(), reference_model
|
| 231 |
+
assert features.exists(), features
|
| 232 |
+
assert pairs.exists(), pairs
|
| 233 |
+
assert matches.exists(), matches
|
| 234 |
+
|
| 235 |
+
sfm_dir.mkdir(parents=True, exist_ok=True)
|
| 236 |
+
database = sfm_dir / "database.db"
|
| 237 |
+
reference = pycolmap.Reconstruction(reference_model)
|
| 238 |
+
|
| 239 |
+
image_ids = create_db_from_model(reference, database)
|
| 240 |
+
import_features(image_ids, database, features)
|
| 241 |
+
import_matches(
|
| 242 |
+
image_ids,
|
| 243 |
+
database,
|
| 244 |
+
pairs,
|
| 245 |
+
matches,
|
| 246 |
+
min_match_score,
|
| 247 |
+
skip_geometric_verification,
|
| 248 |
+
)
|
| 249 |
+
if not skip_geometric_verification:
|
| 250 |
+
if estimate_two_view_geometries:
|
| 251 |
+
estimation_and_geometric_verification(database, pairs, verbose)
|
| 252 |
+
else:
|
| 253 |
+
geometric_verification(
|
| 254 |
+
image_ids, reference, database, features, pairs, matches
|
| 255 |
+
)
|
| 256 |
+
reconstruction = run_triangulation(
|
| 257 |
+
sfm_dir, database, image_dir, reference, verbose, mapper_options
|
| 258 |
+
)
|
| 259 |
+
logger.info(
|
| 260 |
+
"Finished the triangulation with statistics:\n%s", reconstruction.summary()
|
| 261 |
+
)
|
| 262 |
+
return reconstruction
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def parse_option_args(args: List[str], default_options) -> Dict[str, Any]:
|
| 266 |
+
options = {}
|
| 267 |
+
for arg in args:
|
| 268 |
+
idx = arg.find("=")
|
| 269 |
+
if idx == -1:
|
| 270 |
+
raise ValueError("Options format: key1=value1 key2=value2 etc.")
|
| 271 |
+
key, value = arg[:idx], arg[idx + 1 :]
|
| 272 |
+
if not hasattr(default_options, key):
|
| 273 |
+
raise ValueError(
|
| 274 |
+
f'Unknown option "{key}", allowed options and default values'
|
| 275 |
+
f" for {default_options.summary()}"
|
| 276 |
+
)
|
| 277 |
+
value = eval(value)
|
| 278 |
+
target_type = type(getattr(default_options, key))
|
| 279 |
+
if not isinstance(value, target_type):
|
| 280 |
+
raise ValueError(
|
| 281 |
+
f'Incorrect type for option "{key}":' f" {type(value)} vs {target_type}"
|
| 282 |
+
)
|
| 283 |
+
options[key] = value
|
| 284 |
+
return options
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
if __name__ == "__main__":
|
| 288 |
+
parser = argparse.ArgumentParser()
|
| 289 |
+
parser.add_argument("--sfm_dir", type=Path, required=True)
|
| 290 |
+
parser.add_argument("--reference_sfm_model", type=Path, required=True)
|
| 291 |
+
parser.add_argument("--image_dir", type=Path, required=True)
|
| 292 |
+
|
| 293 |
+
parser.add_argument("--pairs", type=Path, required=True)
|
| 294 |
+
parser.add_argument("--features", type=Path, required=True)
|
| 295 |
+
parser.add_argument("--matches", type=Path, required=True)
|
| 296 |
+
|
| 297 |
+
parser.add_argument("--skip_geometric_verification", action="store_true")
|
| 298 |
+
parser.add_argument("--min_match_score", type=float)
|
| 299 |
+
parser.add_argument("--verbose", action="store_true")
|
| 300 |
+
args = parser.parse_args().__dict__
|
| 301 |
+
|
| 302 |
+
mapper_options = parse_option_args(
|
| 303 |
+
args.pop("mapper_options"), pycolmap.IncrementalMapperOptions()
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
main(**args, mapper_options=mapper_options)
|
hloc/utils/database.py
CHANGED
|
@@ -31,10 +31,10 @@
|
|
| 31 |
|
| 32 |
# This script is based on an original implementation by True Price.
|
| 33 |
|
| 34 |
-
import sys
|
| 35 |
import sqlite3
|
| 36 |
-
import
|
| 37 |
|
|
|
|
| 38 |
|
| 39 |
IS_PYTHON3 = sys.version_info[0] >= 3
|
| 40 |
|
|
@@ -100,9 +100,7 @@ CREATE_MATCHES_TABLE = """CREATE TABLE IF NOT EXISTS matches (
|
|
| 100 |
cols INTEGER NOT NULL,
|
| 101 |
data BLOB)"""
|
| 102 |
|
| 103 |
-
CREATE_NAME_INDEX = (
|
| 104 |
-
"CREATE UNIQUE INDEX IF NOT EXISTS index_name ON images(name)"
|
| 105 |
-
)
|
| 106 |
|
| 107 |
CREATE_ALL = "; ".join(
|
| 108 |
[
|
|
@@ -152,34 +150,20 @@ class COLMAPDatabase(sqlite3.Connection):
|
|
| 152 |
super(COLMAPDatabase, self).__init__(*args, **kwargs)
|
| 153 |
|
| 154 |
self.create_tables = lambda: self.executescript(CREATE_ALL)
|
| 155 |
-
self.create_cameras_table = lambda: self.executescript(
|
| 156 |
-
CREATE_CAMERAS_TABLE
|
| 157 |
-
)
|
| 158 |
self.create_descriptors_table = lambda: self.executescript(
|
| 159 |
CREATE_DESCRIPTORS_TABLE
|
| 160 |
)
|
| 161 |
-
self.create_images_table = lambda: self.executescript(
|
| 162 |
-
CREATE_IMAGES_TABLE
|
| 163 |
-
)
|
| 164 |
self.create_two_view_geometries_table = lambda: self.executescript(
|
| 165 |
CREATE_TWO_VIEW_GEOMETRIES_TABLE
|
| 166 |
)
|
| 167 |
-
self.create_keypoints_table = lambda: self.executescript(
|
| 168 |
-
|
| 169 |
-
)
|
| 170 |
-
self.create_matches_table = lambda: self.executescript(
|
| 171 |
-
CREATE_MATCHES_TABLE
|
| 172 |
-
)
|
| 173 |
self.create_name_index = lambda: self.executescript(CREATE_NAME_INDEX)
|
| 174 |
|
| 175 |
def add_camera(
|
| 176 |
-
self,
|
| 177 |
-
model,
|
| 178 |
-
width,
|
| 179 |
-
height,
|
| 180 |
-
params,
|
| 181 |
-
prior_focal_length=False,
|
| 182 |
-
camera_id=None,
|
| 183 |
):
|
| 184 |
params = np.asarray(params, np.float64)
|
| 185 |
cursor = self.execute(
|
|
|
|
| 31 |
|
| 32 |
# This script is based on an original implementation by True Price.
|
| 33 |
|
|
|
|
| 34 |
import sqlite3
|
| 35 |
+
import sys
|
| 36 |
|
| 37 |
+
import numpy as np
|
| 38 |
|
| 39 |
IS_PYTHON3 = sys.version_info[0] >= 3
|
| 40 |
|
|
|
|
| 100 |
cols INTEGER NOT NULL,
|
| 101 |
data BLOB)"""
|
| 102 |
|
| 103 |
+
CREATE_NAME_INDEX = "CREATE UNIQUE INDEX IF NOT EXISTS index_name ON images(name)"
|
|
|
|
|
|
|
| 104 |
|
| 105 |
CREATE_ALL = "; ".join(
|
| 106 |
[
|
|
|
|
| 150 |
super(COLMAPDatabase, self).__init__(*args, **kwargs)
|
| 151 |
|
| 152 |
self.create_tables = lambda: self.executescript(CREATE_ALL)
|
| 153 |
+
self.create_cameras_table = lambda: self.executescript(CREATE_CAMERAS_TABLE)
|
|
|
|
|
|
|
| 154 |
self.create_descriptors_table = lambda: self.executescript(
|
| 155 |
CREATE_DESCRIPTORS_TABLE
|
| 156 |
)
|
| 157 |
+
self.create_images_table = lambda: self.executescript(CREATE_IMAGES_TABLE)
|
|
|
|
|
|
|
| 158 |
self.create_two_view_geometries_table = lambda: self.executescript(
|
| 159 |
CREATE_TWO_VIEW_GEOMETRIES_TABLE
|
| 160 |
)
|
| 161 |
+
self.create_keypoints_table = lambda: self.executescript(CREATE_KEYPOINTS_TABLE)
|
| 162 |
+
self.create_matches_table = lambda: self.executescript(CREATE_MATCHES_TABLE)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
self.create_name_index = lambda: self.executescript(CREATE_NAME_INDEX)
|
| 164 |
|
| 165 |
def add_camera(
|
| 166 |
+
self, model, width, height, params, prior_focal_length=False, camera_id=None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
):
|
| 168 |
params = np.asarray(params, np.float64)
|
| 169 |
cursor = self.execute(
|
hloc/utils/geometry.py
CHANGED
|
@@ -6,28 +6,11 @@ def to_homogeneous(p):
|
|
| 6 |
return np.pad(p, ((0, 0),) * (p.ndim - 1) + ((0, 1),), constant_values=1)
|
| 7 |
|
| 8 |
|
| 9 |
-
def
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
l2d_r2t = (E @ to_homogeneous(p2d_r).T).T
|
| 18 |
-
l2d_t2r = (E.T @ to_homogeneous(p2d_t).T).T
|
| 19 |
-
errors_r = np.abs(
|
| 20 |
-
np.sum(to_homogeneous(p2d_r) * l2d_t2r, axis=1)
|
| 21 |
-
) / np.linalg.norm(l2d_t2r[:, :2], axis=1)
|
| 22 |
-
errors_t = np.abs(
|
| 23 |
-
np.sum(to_homogeneous(p2d_t) * l2d_r2t, axis=1)
|
| 24 |
-
) / np.linalg.norm(l2d_r2t[:, :2], axis=1)
|
| 25 |
-
return E, errors_r, errors_t
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def pose_matrix_from_qvec_tvec(qvec, tvec):
|
| 29 |
-
pose = np.zeros((4, 4))
|
| 30 |
-
pose[:3, :3] = pycolmap.qvec_to_rotmat(qvec)
|
| 31 |
-
pose[:3, -1] = tvec
|
| 32 |
-
pose[-1, -1] = 1
|
| 33 |
-
return pose
|
|
|
|
| 6 |
return np.pad(p, ((0, 0),) * (p.ndim - 1) + ((0, 1),), constant_values=1)
|
| 7 |
|
| 8 |
|
| 9 |
+
def compute_epipolar_errors(j_from_i: pycolmap.Rigid3d, p2d_i, p2d_j):
|
| 10 |
+
j_E_i = j_from_i.essential_matrix()
|
| 11 |
+
l2d_j = to_homogeneous(p2d_i) @ j_E_i.T
|
| 12 |
+
l2d_i = to_homogeneous(p2d_j) @ j_E_i
|
| 13 |
+
dist = np.abs(np.sum(to_homogeneous(p2d_i) * l2d_i, axis=1))
|
| 14 |
+
errors_i = dist / np.linalg.norm(l2d_i[:, :2], axis=1)
|
| 15 |
+
errors_j = dist / np.linalg.norm(l2d_j[:, :2], axis=1)
|
| 16 |
+
return errors_i, errors_j
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hloc/utils/parsers.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
import logging
|
| 3 |
-
import numpy as np
|
| 4 |
from collections import defaultdict
|
|
|
|
|
|
|
|
|
|
| 5 |
import pycolmap
|
| 6 |
|
| 7 |
logger = logging.getLogger(__name__)
|
|
@@ -18,7 +19,9 @@ def parse_image_list(path, with_intrinsics=False):
|
|
| 18 |
if with_intrinsics:
|
| 19 |
model, width, height, *params = data
|
| 20 |
params = np.array(params, float)
|
| 21 |
-
cam = pycolmap.Camera(
|
|
|
|
|
|
|
| 22 |
images.append((name, cam))
|
| 23 |
else:
|
| 24 |
images.append(name)
|
|
|
|
|
|
|
| 1 |
import logging
|
|
|
|
| 2 |
from collections import defaultdict
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
import pycolmap
|
| 7 |
|
| 8 |
logger = logging.getLogger(__name__)
|
|
|
|
| 19 |
if with_intrinsics:
|
| 20 |
model, width, height, *params = data
|
| 21 |
params = np.array(params, float)
|
| 22 |
+
cam = pycolmap.Camera(
|
| 23 |
+
model=model, width=int(width), height=int(height), params=params
|
| 24 |
+
)
|
| 25 |
images.append((name, cam))
|
| 26 |
else:
|
| 27 |
images.append(name)
|
hloc/utils/read_write_model.py
CHANGED
|
@@ -29,12 +29,13 @@
|
|
| 29 |
#
|
| 30 |
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)
|
| 31 |
|
| 32 |
-
import os
|
| 33 |
-
import collections
|
| 34 |
-
import numpy as np
|
| 35 |
-
import struct
|
| 36 |
import argparse
|
|
|
|
| 37 |
import logging
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
logger = logging.getLogger(__name__)
|
| 40 |
|
|
@@ -42,9 +43,7 @@ logger = logging.getLogger(__name__)
|
|
| 42 |
CameraModel = collections.namedtuple(
|
| 43 |
"CameraModel", ["model_id", "model_name", "num_params"]
|
| 44 |
)
|
| 45 |
-
Camera = collections.namedtuple(
|
| 46 |
-
"Camera", ["id", "model", "width", "height", "params"]
|
| 47 |
-
)
|
| 48 |
BaseImage = collections.namedtuple(
|
| 49 |
"Image", ["id", "qvec", "tvec", "camera_id", "name", "xys", "point3D_ids"]
|
| 50 |
)
|
|
@@ -128,11 +127,7 @@ def read_cameras_text(path):
|
|
| 128 |
height = int(elems[3])
|
| 129 |
params = np.array(tuple(map(float, elems[4:])))
|
| 130 |
cameras[camera_id] = Camera(
|
| 131 |
-
id=camera_id,
|
| 132 |
-
model=model,
|
| 133 |
-
width=width,
|
| 134 |
-
height=height,
|
| 135 |
-
params=params,
|
| 136 |
)
|
| 137 |
return cameras
|
| 138 |
|
|
@@ -157,9 +152,7 @@ def read_cameras_binary(path_to_model_file):
|
|
| 157 |
height = camera_properties[3]
|
| 158 |
num_params = CAMERA_MODEL_IDS[model_id].num_params
|
| 159 |
params = read_next_bytes(
|
| 160 |
-
fid,
|
| 161 |
-
num_bytes=8 * num_params,
|
| 162 |
-
format_char_sequence="d" * num_params,
|
| 163 |
)
|
| 164 |
cameras[camera_id] = Camera(
|
| 165 |
id=camera_id,
|
|
@@ -230,10 +223,7 @@ def read_images_text(path):
|
|
| 230 |
image_name = elems[9]
|
| 231 |
elems = fid.readline().split()
|
| 232 |
xys = np.column_stack(
|
| 233 |
-
[
|
| 234 |
-
tuple(map(float, elems[0::3])),
|
| 235 |
-
tuple(map(float, elems[1::3])),
|
| 236 |
-
]
|
| 237 |
)
|
| 238 |
point3D_ids = np.array(tuple(map(int, elems[2::3])))
|
| 239 |
images[image_id] = Image(
|
|
@@ -270,19 +260,16 @@ def read_images_binary(path_to_model_file):
|
|
| 270 |
while current_char != b"\x00": # look for the ASCII 0 entry
|
| 271 |
image_name += current_char.decode("utf-8")
|
| 272 |
current_char = read_next_bytes(fid, 1, "c")[0]
|
| 273 |
-
num_points2D = read_next_bytes(
|
| 274 |
-
|
| 275 |
-
|
| 276 |
x_y_id_s = read_next_bytes(
|
| 277 |
fid,
|
| 278 |
num_bytes=24 * num_points2D,
|
| 279 |
format_char_sequence="ddq" * num_points2D,
|
| 280 |
)
|
| 281 |
xys = np.column_stack(
|
| 282 |
-
[
|
| 283 |
-
tuple(map(float, x_y_id_s[0::3])),
|
| 284 |
-
tuple(map(float, x_y_id_s[1::3])),
|
| 285 |
-
]
|
| 286 |
)
|
| 287 |
point3D_ids = np.array(tuple(map(int, x_y_id_s[2::3])))
|
| 288 |
images[image_id] = Image(
|
|
@@ -321,13 +308,7 @@ def write_images_text(images, path):
|
|
| 321 |
with open(path, "w") as fid:
|
| 322 |
fid.write(HEADER)
|
| 323 |
for _, img in images.items():
|
| 324 |
-
image_header = [
|
| 325 |
-
img.id,
|
| 326 |
-
*img.qvec,
|
| 327 |
-
*img.tvec,
|
| 328 |
-
img.camera_id,
|
| 329 |
-
img.name,
|
| 330 |
-
]
|
| 331 |
first_line = " ".join(map(str, image_header))
|
| 332 |
fid.write(first_line + "\n")
|
| 333 |
|
|
@@ -407,9 +388,9 @@ def read_points3D_binary(path_to_model_file):
|
|
| 407 |
xyz = np.array(binary_point_line_properties[1:4])
|
| 408 |
rgb = np.array(binary_point_line_properties[4:7])
|
| 409 |
error = np.array(binary_point_line_properties[7])
|
| 410 |
-
track_length = read_next_bytes(
|
| 411 |
-
|
| 412 |
-
|
| 413 |
track_elems = read_next_bytes(
|
| 414 |
fid,
|
| 415 |
num_bytes=8 * track_length,
|
|
@@ -442,7 +423,7 @@ def write_points3D_text(points3D, path):
|
|
| 442 |
) / len(points3D)
|
| 443 |
HEADER = (
|
| 444 |
"# 3D point list with one line of data per point:\n"
|
| 445 |
-
+ "# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)\n"
|
| 446 |
+ "# Number of points: {}, mean track length: {}\n".format(
|
| 447 |
len(points3D), mean_track_length
|
| 448 |
)
|
|
@@ -498,12 +479,8 @@ def read_model(path, ext=""):
|
|
| 498 |
ext = ".txt"
|
| 499 |
else:
|
| 500 |
try:
|
| 501 |
-
cameras, images, points3D = read_model(
|
| 502 |
-
|
| 503 |
-
)
|
| 504 |
-
logger.warning(
|
| 505 |
-
"This SfM file structure was deprecated in hloc v1.1"
|
| 506 |
-
)
|
| 507 |
return cameras, images, points3D
|
| 508 |
except FileNotFoundError:
|
| 509 |
raise FileNotFoundError(
|
|
@@ -595,9 +572,7 @@ def main():
|
|
| 595 |
)
|
| 596 |
args = parser.parse_args()
|
| 597 |
|
| 598 |
-
cameras, images, points3D = read_model(
|
| 599 |
-
path=args.input_model, ext=args.input_format
|
| 600 |
-
)
|
| 601 |
|
| 602 |
print("num_cameras:", len(cameras))
|
| 603 |
print("num_images:", len(images))
|
|
@@ -605,11 +580,7 @@ def main():
|
|
| 605 |
|
| 606 |
if args.output_model is not None:
|
| 607 |
write_model(
|
| 608 |
-
cameras,
|
| 609 |
-
images,
|
| 610 |
-
points3D,
|
| 611 |
-
path=args.output_model,
|
| 612 |
-
ext=args.output_format,
|
| 613 |
)
|
| 614 |
|
| 615 |
|
|
|
|
| 29 |
#
|
| 30 |
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
import argparse
|
| 33 |
+
import collections
|
| 34 |
import logging
|
| 35 |
+
import os
|
| 36 |
+
import struct
|
| 37 |
+
|
| 38 |
+
import numpy as np
|
| 39 |
|
| 40 |
logger = logging.getLogger(__name__)
|
| 41 |
|
|
|
|
| 43 |
CameraModel = collections.namedtuple(
|
| 44 |
"CameraModel", ["model_id", "model_name", "num_params"]
|
| 45 |
)
|
| 46 |
+
Camera = collections.namedtuple("Camera", ["id", "model", "width", "height", "params"])
|
|
|
|
|
|
|
| 47 |
BaseImage = collections.namedtuple(
|
| 48 |
"Image", ["id", "qvec", "tvec", "camera_id", "name", "xys", "point3D_ids"]
|
| 49 |
)
|
|
|
|
| 127 |
height = int(elems[3])
|
| 128 |
params = np.array(tuple(map(float, elems[4:])))
|
| 129 |
cameras[camera_id] = Camera(
|
| 130 |
+
id=camera_id, model=model, width=width, height=height, params=params
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
)
|
| 132 |
return cameras
|
| 133 |
|
|
|
|
| 152 |
height = camera_properties[3]
|
| 153 |
num_params = CAMERA_MODEL_IDS[model_id].num_params
|
| 154 |
params = read_next_bytes(
|
| 155 |
+
fid, num_bytes=8 * num_params, format_char_sequence="d" * num_params
|
|
|
|
|
|
|
| 156 |
)
|
| 157 |
cameras[camera_id] = Camera(
|
| 158 |
id=camera_id,
|
|
|
|
| 223 |
image_name = elems[9]
|
| 224 |
elems = fid.readline().split()
|
| 225 |
xys = np.column_stack(
|
| 226 |
+
[tuple(map(float, elems[0::3])), tuple(map(float, elems[1::3]))]
|
|
|
|
|
|
|
|
|
|
| 227 |
)
|
| 228 |
point3D_ids = np.array(tuple(map(int, elems[2::3])))
|
| 229 |
images[image_id] = Image(
|
|
|
|
| 260 |
while current_char != b"\x00": # look for the ASCII 0 entry
|
| 261 |
image_name += current_char.decode("utf-8")
|
| 262 |
current_char = read_next_bytes(fid, 1, "c")[0]
|
| 263 |
+
num_points2D = read_next_bytes(fid, num_bytes=8, format_char_sequence="Q")[
|
| 264 |
+
0
|
| 265 |
+
]
|
| 266 |
x_y_id_s = read_next_bytes(
|
| 267 |
fid,
|
| 268 |
num_bytes=24 * num_points2D,
|
| 269 |
format_char_sequence="ddq" * num_points2D,
|
| 270 |
)
|
| 271 |
xys = np.column_stack(
|
| 272 |
+
[tuple(map(float, x_y_id_s[0::3])), tuple(map(float, x_y_id_s[1::3]))]
|
|
|
|
|
|
|
|
|
|
| 273 |
)
|
| 274 |
point3D_ids = np.array(tuple(map(int, x_y_id_s[2::3])))
|
| 275 |
images[image_id] = Image(
|
|
|
|
| 308 |
with open(path, "w") as fid:
|
| 309 |
fid.write(HEADER)
|
| 310 |
for _, img in images.items():
|
| 311 |
+
image_header = [img.id, *img.qvec, *img.tvec, img.camera_id, img.name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
first_line = " ".join(map(str, image_header))
|
| 313 |
fid.write(first_line + "\n")
|
| 314 |
|
|
|
|
| 388 |
xyz = np.array(binary_point_line_properties[1:4])
|
| 389 |
rgb = np.array(binary_point_line_properties[4:7])
|
| 390 |
error = np.array(binary_point_line_properties[7])
|
| 391 |
+
track_length = read_next_bytes(fid, num_bytes=8, format_char_sequence="Q")[
|
| 392 |
+
0
|
| 393 |
+
]
|
| 394 |
track_elems = read_next_bytes(
|
| 395 |
fid,
|
| 396 |
num_bytes=8 * track_length,
|
|
|
|
| 423 |
) / len(points3D)
|
| 424 |
HEADER = (
|
| 425 |
"# 3D point list with one line of data per point:\n"
|
| 426 |
+
+ "# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)\n" # noqa: E501
|
| 427 |
+ "# Number of points: {}, mean track length: {}\n".format(
|
| 428 |
len(points3D), mean_track_length
|
| 429 |
)
|
|
|
|
| 479 |
ext = ".txt"
|
| 480 |
else:
|
| 481 |
try:
|
| 482 |
+
cameras, images, points3D = read_model(os.path.join(path, "model/"))
|
| 483 |
+
logger.warning("This SfM file structure was deprecated in hloc v1.1")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
return cameras, images, points3D
|
| 485 |
except FileNotFoundError:
|
| 486 |
raise FileNotFoundError(
|
|
|
|
| 572 |
)
|
| 573 |
args = parser.parse_args()
|
| 574 |
|
| 575 |
+
cameras, images, points3D = read_model(path=args.input_model, ext=args.input_format)
|
|
|
|
|
|
|
| 576 |
|
| 577 |
print("num_cameras:", len(cameras))
|
| 578 |
print("num_images:", len(images))
|
|
|
|
| 580 |
|
| 581 |
if args.output_model is not None:
|
| 582 |
write_model(
|
| 583 |
+
cameras, images, points3D, path=args.output_model, ext=args.output_format
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
)
|
| 585 |
|
| 586 |
|
hloc/utils/viz.py
CHANGED
|
@@ -20,7 +20,7 @@ def cm_RdGn(x):
|
|
| 20 |
|
| 21 |
|
| 22 |
def plot_images(
|
| 23 |
-
imgs, titles=None, cmaps="gray", dpi=100, pad=0.5, adaptive=True
|
| 24 |
):
|
| 25 |
"""Plot a set of images horizontally.
|
| 26 |
Args:
|
|
@@ -37,21 +37,17 @@ def plot_images(
|
|
| 37 |
ratios = [i.shape[1] / i.shape[0] for i in imgs] # W / H
|
| 38 |
else:
|
| 39 |
ratios = [4 / 3] * n
|
| 40 |
-
figsize = [sum(ratios) *
|
| 41 |
-
fig,
|
| 42 |
1, n, figsize=figsize, dpi=dpi, gridspec_kw={"width_ratios": ratios}
|
| 43 |
)
|
| 44 |
if n == 1:
|
| 45 |
-
|
| 46 |
-
for i in
|
| 47 |
-
ax
|
| 48 |
-
ax
|
| 49 |
-
ax[i].get_xaxis().set_ticks([])
|
| 50 |
-
ax[i].set_axis_off()
|
| 51 |
-
for spine in ax[i].spines.values(): # remove frame
|
| 52 |
-
spine.set_visible(False)
|
| 53 |
if titles:
|
| 54 |
-
ax
|
| 55 |
fig.tight_layout(pad=pad)
|
| 56 |
|
| 57 |
|
|
@@ -96,21 +92,19 @@ def plot_matches(kpts0, kpts1, color=None, lw=1.5, ps=4, indices=(0, 1), a=1.0):
|
|
| 96 |
|
| 97 |
if lw > 0:
|
| 98 |
# transform the points into the figure coordinate system
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
)
|
| 112 |
-
for i in range(len(kpts0))
|
| 113 |
-
]
|
| 114 |
|
| 115 |
# freeze the axes to prevent the transform to change
|
| 116 |
ax0.autoscale(enable=False)
|
|
@@ -134,13 +128,7 @@ def add_text(
|
|
| 134 |
):
|
| 135 |
ax = plt.gcf().axes[idx]
|
| 136 |
t = ax.text(
|
| 137 |
-
*pos,
|
| 138 |
-
text,
|
| 139 |
-
fontsize=fs,
|
| 140 |
-
ha=ha,
|
| 141 |
-
va=va,
|
| 142 |
-
color=color,
|
| 143 |
-
transform=ax.transAxes
|
| 144 |
)
|
| 145 |
if lcolor is not None:
|
| 146 |
t.set_path_effects(
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def plot_images(
|
| 23 |
+
imgs, titles=None, cmaps="gray", dpi=100, pad=0.5, adaptive=True, figsize=4.5
|
| 24 |
):
|
| 25 |
"""Plot a set of images horizontally.
|
| 26 |
Args:
|
|
|
|
| 37 |
ratios = [i.shape[1] / i.shape[0] for i in imgs] # W / H
|
| 38 |
else:
|
| 39 |
ratios = [4 / 3] * n
|
| 40 |
+
figsize = [sum(ratios) * figsize, figsize]
|
| 41 |
+
fig, axs = plt.subplots(
|
| 42 |
1, n, figsize=figsize, dpi=dpi, gridspec_kw={"width_ratios": ratios}
|
| 43 |
)
|
| 44 |
if n == 1:
|
| 45 |
+
axs = [axs]
|
| 46 |
+
for i, (img, ax) in enumerate(zip(imgs, axs)):
|
| 47 |
+
ax.imshow(img, cmap=plt.get_cmap(cmaps[i]))
|
| 48 |
+
ax.set_axis_off()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
if titles:
|
| 50 |
+
ax.set_title(titles[i])
|
| 51 |
fig.tight_layout(pad=pad)
|
| 52 |
|
| 53 |
|
|
|
|
| 92 |
|
| 93 |
if lw > 0:
|
| 94 |
# transform the points into the figure coordinate system
|
| 95 |
+
for i in range(len(kpts0)):
|
| 96 |
+
fig.add_artist(
|
| 97 |
+
matplotlib.patches.ConnectionPatch(
|
| 98 |
+
xyA=(kpts0[i, 0], kpts0[i, 1]),
|
| 99 |
+
coordsA=ax0.transData,
|
| 100 |
+
xyB=(kpts1[i, 0], kpts1[i, 1]),
|
| 101 |
+
coordsB=ax1.transData,
|
| 102 |
+
zorder=1,
|
| 103 |
+
color=color[i],
|
| 104 |
+
linewidth=lw,
|
| 105 |
+
alpha=a,
|
| 106 |
+
)
|
| 107 |
)
|
|
|
|
|
|
|
| 108 |
|
| 109 |
# freeze the axes to prevent the transform to change
|
| 110 |
ax0.autoscale(enable=False)
|
|
|
|
| 128 |
):
|
| 129 |
ax = plt.gcf().axes[idx]
|
| 130 |
t = ax.text(
|
| 131 |
+
*pos, text, fontsize=fs, ha=ha, va=va, color=color, transform=ax.transAxes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
)
|
| 133 |
if lcolor is not None:
|
| 134 |
t.set_path_effects(
|
hloc/utils/viz_3d.py
CHANGED
|
@@ -9,9 +9,10 @@ Written by Paul-Edouard Sarlin and Philipp Lindenberger.
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
from typing import Optional
|
|
|
|
| 12 |
import numpy as np
|
| 13 |
-
import pycolmap
|
| 14 |
import plotly.graph_objects as go
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
def to_homogeneous(points):
|
|
@@ -46,9 +47,7 @@ def init_figure(height: int = 800) -> go.Figure:
|
|
| 46 |
dragmode="orbit",
|
| 47 |
),
|
| 48 |
margin=dict(l=0, r=0, b=0, t=0, pad=0),
|
| 49 |
-
legend=dict(
|
| 50 |
-
orientation="h", yanchor="top", y=0.99, xanchor="left", x=0.1
|
| 51 |
-
),
|
| 52 |
)
|
| 53 |
return fig
|
| 54 |
|
|
@@ -70,9 +69,7 @@ def plot_points(
|
|
| 70 |
mode="markers",
|
| 71 |
name=name,
|
| 72 |
legendgroup=name,
|
| 73 |
-
marker=dict(
|
| 74 |
-
size=ps, color=color, line_width=0.0, colorscale=colorscale
|
| 75 |
-
),
|
| 76 |
)
|
| 77 |
fig.add_trace(tr)
|
| 78 |
|
|
@@ -85,7 +82,9 @@ def plot_camera(
|
|
| 85 |
color: str = "rgb(0, 0, 255)",
|
| 86 |
name: Optional[str] = None,
|
| 87 |
legendgroup: Optional[str] = None,
|
|
|
|
| 88 |
size: float = 1.0,
|
|
|
|
| 89 |
):
|
| 90 |
"""Plot a camera frustum from pose and intrinsic matrix."""
|
| 91 |
W, H = K[0, 2] * 2, K[1, 2] * 2
|
|
@@ -98,43 +97,34 @@ def plot_camera(
|
|
| 98 |
scale = 1.0
|
| 99 |
corners = to_homogeneous(corners) @ np.linalg.inv(K).T
|
| 100 |
corners = (corners / 2 * scale) @ R.T + t
|
| 101 |
-
|
| 102 |
-
x, y, z = corners.T
|
| 103 |
-
rect = go.Scatter3d(
|
| 104 |
-
x=x,
|
| 105 |
-
y=y,
|
| 106 |
-
z=z,
|
| 107 |
-
line=dict(color=color),
|
| 108 |
-
legendgroup=legendgroup,
|
| 109 |
-
name=name,
|
| 110 |
-
marker=dict(size=0.0001),
|
| 111 |
-
showlegend=False,
|
| 112 |
-
)
|
| 113 |
-
fig.add_trace(rect)
|
| 114 |
|
| 115 |
x, y, z = np.concatenate(([t], corners)).T
|
| 116 |
i = [0, 0, 0, 0]
|
| 117 |
j = [1, 2, 3, 4]
|
| 118 |
k = [2, 3, 4, 1]
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
| 133 |
triangles = np.vstack((i, j, k)).T
|
| 134 |
vertices = np.concatenate(([t], corners))
|
| 135 |
tri_points = np.array([vertices[i] for i in triangles.reshape(-1)])
|
| 136 |
-
|
| 137 |
x, y, z = tri_points.T
|
|
|
|
| 138 |
pyramid = go.Scatter3d(
|
| 139 |
x=x,
|
| 140 |
y=y,
|
|
@@ -144,6 +134,7 @@ def plot_camera(
|
|
| 144 |
name=name,
|
| 145 |
line=dict(color=color, width=1),
|
| 146 |
showlegend=False,
|
|
|
|
| 147 |
)
|
| 148 |
fig.add_trace(pyramid)
|
| 149 |
|
|
@@ -156,19 +147,19 @@ def plot_camera_colmap(
|
|
| 156 |
**kwargs
|
| 157 |
):
|
| 158 |
"""Plot a camera frustum from PyCOLMAP objects"""
|
|
|
|
| 159 |
plot_camera(
|
| 160 |
fig,
|
| 161 |
-
|
| 162 |
-
|
| 163 |
camera.calibration_matrix(),
|
| 164 |
name=name or str(image.image_id),
|
|
|
|
| 165 |
**kwargs
|
| 166 |
)
|
| 167 |
|
| 168 |
|
| 169 |
-
def plot_cameras(
|
| 170 |
-
fig: go.Figure, reconstruction: pycolmap.Reconstruction, **kwargs
|
| 171 |
-
):
|
| 172 |
"""Plot a camera as a cone with camera frustum."""
|
| 173 |
for image_id, image in reconstruction.images.items():
|
| 174 |
plot_camera_colmap(
|
|
@@ -185,13 +176,14 @@ def plot_reconstruction(
|
|
| 185 |
min_track_length: int = 2,
|
| 186 |
points: bool = True,
|
| 187 |
cameras: bool = True,
|
|
|
|
| 188 |
cs: float = 1.0,
|
| 189 |
):
|
| 190 |
# Filter outliers
|
| 191 |
bbs = rec.compute_bounding_box(0.001, 0.999)
|
| 192 |
# Filter points, use original reproj error here
|
| 193 |
-
|
| 194 |
-
p3D
|
| 195 |
for _, p3D in rec.points3D.items()
|
| 196 |
if (
|
| 197 |
(p3D.xyz >= bbs[0]).all()
|
|
@@ -200,7 +192,12 @@ def plot_reconstruction(
|
|
| 200 |
and p3D.track.length() >= min_track_length
|
| 201 |
)
|
| 202 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
if points:
|
| 204 |
-
plot_points(fig, np.array(xyzs), color=
|
| 205 |
if cameras:
|
| 206 |
plot_cameras(fig, rec, color=color, legendgroup=name, size=cs)
|
|
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
from typing import Optional
|
| 12 |
+
|
| 13 |
import numpy as np
|
|
|
|
| 14 |
import plotly.graph_objects as go
|
| 15 |
+
import pycolmap
|
| 16 |
|
| 17 |
|
| 18 |
def to_homogeneous(points):
|
|
|
|
| 47 |
dragmode="orbit",
|
| 48 |
),
|
| 49 |
margin=dict(l=0, r=0, b=0, t=0, pad=0),
|
| 50 |
+
legend=dict(orientation="h", yanchor="top", y=0.99, xanchor="left", x=0.1),
|
|
|
|
|
|
|
| 51 |
)
|
| 52 |
return fig
|
| 53 |
|
|
|
|
| 69 |
mode="markers",
|
| 70 |
name=name,
|
| 71 |
legendgroup=name,
|
| 72 |
+
marker=dict(size=ps, color=color, line_width=0.0, colorscale=colorscale),
|
|
|
|
|
|
|
| 73 |
)
|
| 74 |
fig.add_trace(tr)
|
| 75 |
|
|
|
|
| 82 |
color: str = "rgb(0, 0, 255)",
|
| 83 |
name: Optional[str] = None,
|
| 84 |
legendgroup: Optional[str] = None,
|
| 85 |
+
fill: bool = False,
|
| 86 |
size: float = 1.0,
|
| 87 |
+
text: Optional[str] = None,
|
| 88 |
):
|
| 89 |
"""Plot a camera frustum from pose and intrinsic matrix."""
|
| 90 |
W, H = K[0, 2] * 2, K[1, 2] * 2
|
|
|
|
| 97 |
scale = 1.0
|
| 98 |
corners = to_homogeneous(corners) @ np.linalg.inv(K).T
|
| 99 |
corners = (corners / 2 * scale) @ R.T + t
|
| 100 |
+
legendgroup = legendgroup if legendgroup is not None else name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
x, y, z = np.concatenate(([t], corners)).T
|
| 103 |
i = [0, 0, 0, 0]
|
| 104 |
j = [1, 2, 3, 4]
|
| 105 |
k = [2, 3, 4, 1]
|
| 106 |
|
| 107 |
+
if fill:
|
| 108 |
+
pyramid = go.Mesh3d(
|
| 109 |
+
x=x,
|
| 110 |
+
y=y,
|
| 111 |
+
z=z,
|
| 112 |
+
color=color,
|
| 113 |
+
i=i,
|
| 114 |
+
j=j,
|
| 115 |
+
k=k,
|
| 116 |
+
legendgroup=legendgroup,
|
| 117 |
+
name=name,
|
| 118 |
+
showlegend=False,
|
| 119 |
+
hovertemplate=text.replace("\n", "<br>"),
|
| 120 |
+
)
|
| 121 |
+
fig.add_trace(pyramid)
|
| 122 |
+
|
| 123 |
triangles = np.vstack((i, j, k)).T
|
| 124 |
vertices = np.concatenate(([t], corners))
|
| 125 |
tri_points = np.array([vertices[i] for i in triangles.reshape(-1)])
|
|
|
|
| 126 |
x, y, z = tri_points.T
|
| 127 |
+
|
| 128 |
pyramid = go.Scatter3d(
|
| 129 |
x=x,
|
| 130 |
y=y,
|
|
|
|
| 134 |
name=name,
|
| 135 |
line=dict(color=color, width=1),
|
| 136 |
showlegend=False,
|
| 137 |
+
hovertemplate=text.replace("\n", "<br>"),
|
| 138 |
)
|
| 139 |
fig.add_trace(pyramid)
|
| 140 |
|
|
|
|
| 147 |
**kwargs
|
| 148 |
):
|
| 149 |
"""Plot a camera frustum from PyCOLMAP objects"""
|
| 150 |
+
world_t_camera = image.cam_from_world.inverse()
|
| 151 |
plot_camera(
|
| 152 |
fig,
|
| 153 |
+
world_t_camera.rotation.matrix(),
|
| 154 |
+
world_t_camera.translation,
|
| 155 |
camera.calibration_matrix(),
|
| 156 |
name=name or str(image.image_id),
|
| 157 |
+
text=str(image),
|
| 158 |
**kwargs
|
| 159 |
)
|
| 160 |
|
| 161 |
|
| 162 |
+
def plot_cameras(fig: go.Figure, reconstruction: pycolmap.Reconstruction, **kwargs):
|
|
|
|
|
|
|
| 163 |
"""Plot a camera as a cone with camera frustum."""
|
| 164 |
for image_id, image in reconstruction.images.items():
|
| 165 |
plot_camera_colmap(
|
|
|
|
| 176 |
min_track_length: int = 2,
|
| 177 |
points: bool = True,
|
| 178 |
cameras: bool = True,
|
| 179 |
+
points_rgb: bool = True,
|
| 180 |
cs: float = 1.0,
|
| 181 |
):
|
| 182 |
# Filter outliers
|
| 183 |
bbs = rec.compute_bounding_box(0.001, 0.999)
|
| 184 |
# Filter points, use original reproj error here
|
| 185 |
+
p3Ds = [
|
| 186 |
+
p3D
|
| 187 |
for _, p3D in rec.points3D.items()
|
| 188 |
if (
|
| 189 |
(p3D.xyz >= bbs[0]).all()
|
|
|
|
| 192 |
and p3D.track.length() >= min_track_length
|
| 193 |
)
|
| 194 |
]
|
| 195 |
+
xyzs = [p3D.xyz for p3D in p3Ds]
|
| 196 |
+
if points_rgb:
|
| 197 |
+
pcolor = [p3D.color for p3D in p3Ds]
|
| 198 |
+
else:
|
| 199 |
+
pcolor = color
|
| 200 |
if points:
|
| 201 |
+
plot_points(fig, np.array(xyzs), color=pcolor, ps=1, name=name)
|
| 202 |
if cameras:
|
| 203 |
plot_cameras(fig, rec, color=color, legendgroup=name, size=cs)
|
hloc/visualization.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pickle
|
| 2 |
+
import random
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import pycolmap
|
| 6 |
+
from matplotlib import cm
|
| 7 |
+
|
| 8 |
+
from .utils.io import read_image
|
| 9 |
+
from .utils.viz import add_text, cm_RdGn, plot_images, plot_keypoints, plot_matches
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def visualize_sfm_2d(
|
| 13 |
+
reconstruction, image_dir, color_by="visibility", selected=[], n=1, seed=0, dpi=75
|
| 14 |
+
):
|
| 15 |
+
assert image_dir.exists()
|
| 16 |
+
if not isinstance(reconstruction, pycolmap.Reconstruction):
|
| 17 |
+
reconstruction = pycolmap.Reconstruction(reconstruction)
|
| 18 |
+
|
| 19 |
+
if not selected:
|
| 20 |
+
image_ids = reconstruction.reg_image_ids()
|
| 21 |
+
selected = random.Random(seed).sample(image_ids, min(n, len(image_ids)))
|
| 22 |
+
|
| 23 |
+
for i in selected:
|
| 24 |
+
image = reconstruction.images[i]
|
| 25 |
+
keypoints = np.array([p.xy for p in image.points2D])
|
| 26 |
+
visible = np.array([p.has_point3D() for p in image.points2D])
|
| 27 |
+
|
| 28 |
+
if color_by == "visibility":
|
| 29 |
+
color = [(0, 0, 1) if v else (1, 0, 0) for v in visible]
|
| 30 |
+
text = f"visible: {np.count_nonzero(visible)}/{len(visible)}"
|
| 31 |
+
elif color_by == "track_length":
|
| 32 |
+
tl = np.array(
|
| 33 |
+
[
|
| 34 |
+
reconstruction.points3D[p.point3D_id].track.length()
|
| 35 |
+
if p.has_point3D()
|
| 36 |
+
else 1
|
| 37 |
+
for p in image.points2D
|
| 38 |
+
]
|
| 39 |
+
)
|
| 40 |
+
max_, med_ = np.max(tl), np.median(tl[tl > 1])
|
| 41 |
+
tl = np.log(tl)
|
| 42 |
+
color = cm.jet(tl / tl.max()).tolist()
|
| 43 |
+
text = f"max/median track length: {max_}/{med_}"
|
| 44 |
+
elif color_by == "depth":
|
| 45 |
+
p3ids = [p.point3D_id for p in image.points2D if p.has_point3D()]
|
| 46 |
+
z = np.array(
|
| 47 |
+
[
|
| 48 |
+
(image.cam_from_world * reconstruction.points3D[j].xyz)[-1]
|
| 49 |
+
for j in p3ids
|
| 50 |
+
]
|
| 51 |
+
)
|
| 52 |
+
z -= z.min()
|
| 53 |
+
color = cm.jet(z / np.percentile(z, 99.9))
|
| 54 |
+
text = f"visible: {np.count_nonzero(visible)}/{len(visible)}"
|
| 55 |
+
keypoints = keypoints[visible]
|
| 56 |
+
else:
|
| 57 |
+
raise NotImplementedError(f"Coloring not implemented: {color_by}.")
|
| 58 |
+
|
| 59 |
+
name = image.name
|
| 60 |
+
plot_images([read_image(image_dir / name)], dpi=dpi)
|
| 61 |
+
plot_keypoints([keypoints], colors=[color], ps=4)
|
| 62 |
+
add_text(0, text)
|
| 63 |
+
add_text(0, name, pos=(0.01, 0.01), fs=5, lcolor=None, va="bottom")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def visualize_loc(
|
| 67 |
+
results,
|
| 68 |
+
image_dir,
|
| 69 |
+
reconstruction=None,
|
| 70 |
+
db_image_dir=None,
|
| 71 |
+
selected=[],
|
| 72 |
+
n=1,
|
| 73 |
+
seed=0,
|
| 74 |
+
prefix=None,
|
| 75 |
+
**kwargs,
|
| 76 |
+
):
|
| 77 |
+
assert image_dir.exists()
|
| 78 |
+
|
| 79 |
+
with open(str(results) + "_logs.pkl", "rb") as f:
|
| 80 |
+
logs = pickle.load(f)
|
| 81 |
+
|
| 82 |
+
if not selected:
|
| 83 |
+
queries = list(logs["loc"].keys())
|
| 84 |
+
if prefix:
|
| 85 |
+
queries = [q for q in queries if q.startswith(prefix)]
|
| 86 |
+
selected = random.Random(seed).sample(queries, min(n, len(queries)))
|
| 87 |
+
|
| 88 |
+
if reconstruction is not None:
|
| 89 |
+
if not isinstance(reconstruction, pycolmap.Reconstruction):
|
| 90 |
+
reconstruction = pycolmap.Reconstruction(reconstruction)
|
| 91 |
+
|
| 92 |
+
for qname in selected:
|
| 93 |
+
loc = logs["loc"][qname]
|
| 94 |
+
visualize_loc_from_log(
|
| 95 |
+
image_dir, qname, loc, reconstruction, db_image_dir, **kwargs
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def visualize_loc_from_log(
|
| 100 |
+
image_dir,
|
| 101 |
+
query_name,
|
| 102 |
+
loc,
|
| 103 |
+
reconstruction=None,
|
| 104 |
+
db_image_dir=None,
|
| 105 |
+
top_k_db=2,
|
| 106 |
+
dpi=75,
|
| 107 |
+
):
|
| 108 |
+
q_image = read_image(image_dir / query_name)
|
| 109 |
+
if loc.get("covisibility_clustering", False):
|
| 110 |
+
# select the first, largest cluster if the localization failed
|
| 111 |
+
loc = loc["log_clusters"][loc["best_cluster"] or 0]
|
| 112 |
+
|
| 113 |
+
inliers = np.array(loc["PnP_ret"]["inliers"])
|
| 114 |
+
mkp_q = loc["keypoints_query"]
|
| 115 |
+
n = len(loc["db"])
|
| 116 |
+
if reconstruction is not None:
|
| 117 |
+
# for each pair of query keypoint and its matched 3D point,
|
| 118 |
+
# we need to find its corresponding keypoint in each database image
|
| 119 |
+
# that observes it. We also count the number of inliers in each.
|
| 120 |
+
kp_idxs, kp_to_3D_to_db = loc["keypoint_index_to_db"]
|
| 121 |
+
counts = np.zeros(n)
|
| 122 |
+
dbs_kp_q_db = [[] for _ in range(n)]
|
| 123 |
+
inliers_dbs = [[] for _ in range(n)]
|
| 124 |
+
for i, (inl, (p3D_id, db_idxs)) in enumerate(zip(inliers, kp_to_3D_to_db)):
|
| 125 |
+
track = reconstruction.points3D[p3D_id].track
|
| 126 |
+
track = {el.image_id: el.point2D_idx for el in track.elements}
|
| 127 |
+
for db_idx in db_idxs:
|
| 128 |
+
counts[db_idx] += inl
|
| 129 |
+
kp_db = track[loc["db"][db_idx]]
|
| 130 |
+
dbs_kp_q_db[db_idx].append((i, kp_db))
|
| 131 |
+
inliers_dbs[db_idx].append(inl)
|
| 132 |
+
else:
|
| 133 |
+
# for inloc the database keypoints are already in the logs
|
| 134 |
+
assert "keypoints_db" in loc
|
| 135 |
+
assert "indices_db" in loc
|
| 136 |
+
counts = np.array([np.sum(loc["indices_db"][inliers] == i) for i in range(n)])
|
| 137 |
+
|
| 138 |
+
# display the database images with the most inlier matches
|
| 139 |
+
db_sort = np.argsort(-counts)
|
| 140 |
+
for db_idx in db_sort[:top_k_db]:
|
| 141 |
+
if reconstruction is not None:
|
| 142 |
+
db = reconstruction.images[loc["db"][db_idx]]
|
| 143 |
+
db_name = db.name
|
| 144 |
+
db_kp_q_db = np.array(dbs_kp_q_db[db_idx])
|
| 145 |
+
kp_q = mkp_q[db_kp_q_db[:, 0]]
|
| 146 |
+
kp_db = np.array([db.points2D[i].xy for i in db_kp_q_db[:, 1]])
|
| 147 |
+
inliers_db = inliers_dbs[db_idx]
|
| 148 |
+
else:
|
| 149 |
+
db_name = loc["db"][db_idx]
|
| 150 |
+
kp_q = mkp_q[loc["indices_db"] == db_idx]
|
| 151 |
+
kp_db = loc["keypoints_db"][loc["indices_db"] == db_idx]
|
| 152 |
+
inliers_db = inliers[loc["indices_db"] == db_idx]
|
| 153 |
+
|
| 154 |
+
db_image = read_image((db_image_dir or image_dir) / db_name)
|
| 155 |
+
color = cm_RdGn(inliers_db).tolist()
|
| 156 |
+
text = f"inliers: {sum(inliers_db)}/{len(inliers_db)}"
|
| 157 |
+
|
| 158 |
+
plot_images([q_image, db_image], dpi=dpi)
|
| 159 |
+
plot_matches(kp_q, kp_db, color, a=0.1)
|
| 160 |
+
add_text(0, text)
|
| 161 |
+
opts = dict(pos=(0.01, 0.01), fs=5, lcolor=None, va="bottom")
|
| 162 |
+
add_text(0, query_name, **opts)
|
| 163 |
+
add_text(1, db_name, **opts)
|