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)
|