Vincentqyw commited on
Commit
4c88343
1 Parent(s): f90241e

update: sync with hloc

Browse files
hloc/__init__.py CHANGED
@@ -3,7 +3,7 @@ import logging
3
  import torch
4
  from packaging import version
5
 
6
- __version__ = "1.3"
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
- minimal_version = version.parse("0.3.0")
27
- found_version = version.parse(getattr(pycolmap, "__version__"))
28
- if found_version < minimal_version:
29
- logger.warning(
30
- "hloc now requires pycolmap>=%s but found pycolmap==%s, "
31
- "please upgrade with `pip install --upgrade pycolmap`",
32
- minimal_version,
33
- found_version,
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, match_features, localize_sfm, logger
5
- from .utils import get_timestamps, delete_unused_images
6
- from .utils import generate_query_lists, generate_localization_pairs
7
- from .utils import prepare_submission, evaluate_submission
 
 
 
 
 
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 ... import pairs_from_poses, triangulation
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 os
2
- import numpy as np
3
  import logging
 
4
  from pathlib import Path
5
 
6
- from ...utils.read_write_model import qvec2rotmat, rotmat2qvec
7
- from ...utils.read_write_model import Image, write_model, Camera
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 = list(root.glob("**/*.png"))
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
- submission_dir / reloc.name, has_poses=True
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 write_model, read_model
9
 
10
 
11
  def scene_coordinates(p2D, R_w2c, t_w2c, depth, camera):
12
  assert len(depth) == len(p2D)
13
- ret = pycolmap.image_to_world(p2D, camera._asdict())
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
- ret = pycolmap.world_to_image(p2D_norm, camera._asdict())
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 .utils import create_reference_sfm
5
- from .create_gt_sfm import correct_sfm_with_gt_depth
 
 
 
 
 
 
6
  from ..Cambridge.utils import create_query_list_with_intrinsics, evaluate
7
- from ... import extract_features, match_features, pairs_from_covisibility
8
- from ... import triangulation, localize_sfm, logger
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/images
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 extract_features, match_features
6
- from ... import pairs_from_covisibility, pairs_from_retrieval
7
- from ... import colmap_from_nvm, triangulation, localize_sfm
 
 
 
 
 
 
 
8
 
9
 
10
- parser = argparse.ArgumentParser()
11
- parser.add_argument(
12
- "--dataset",
13
- type=Path,
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
- # Setup the paths
38
- dataset = args.dataset
39
- images = dataset / "images/images_upright/"
 
 
 
 
 
 
 
40
 
41
- outputs = args.outputs # where everything will be saved
42
- sift_sfm = outputs / "sfm_sift" # from which we extract the reference poses
43
- reference_sfm = (
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
- # list the standard configurations available
57
- print(f"Configs for feature extractors:\n{pformat(extract_features.confs)}")
58
- print(f"Configs for feature matchers:\n{pformat(match_features.confs)}")
 
59
 
60
- # pick one of the configurations for extraction and matching
61
- retrieval_conf = extract_features.confs["netvlad"]
62
- feature_conf = extract_features.confs["superpoint_aachen"]
63
- matcher_conf = match_features.confs["superglue"]
64
 
65
- features = extract_features.main(feature_conf, images, outputs)
 
 
 
 
 
 
 
 
 
66
 
67
- colmap_from_nvm.main(
68
- dataset / "3D-models/aachen_cvpr2018_db.nvm",
69
- dataset / "3D-models/database_intrinsics.txt",
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
- triangulation.main(
79
- reference_sfm, sift_sfm, images, sfm_pairs, features, sfm_matches
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
- localize_sfm.main(
95
- reference_sfm,
96
- dataset / "queries/*_time_queries_with_intrinsics.txt",
97
- loc_pairs,
98
- features,
99
- loc_matches,
100
- results,
101
- covisibility_clustering=False,
102
- ) # not required with SuperPoint+SuperGlue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/images
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 extract_features, match_features, triangulation
6
- from ... import pairs_from_covisibility, pairs_from_retrieval, localize_sfm
 
 
 
 
 
 
 
7
 
8
 
9
- parser = argparse.ArgumentParser()
10
- parser.add_argument(
11
- "--dataset",
12
- type=Path,
13
- default="datasets/aachen_v1.1",
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
- # Setup the paths
37
- dataset = args.dataset
38
- images = dataset / "images/images_upright/"
39
- sift_sfm = dataset / "3D-models/aachen_v_1_1"
 
 
 
 
 
 
 
40
 
41
- outputs = args.outputs # where everything will be saved
42
- reference_sfm = (
43
- outputs / "sfm_superpoint+superglue"
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
- # list the standard configurations available
56
- print(f"Configs for feature extractors:\n{pformat(extract_features.confs)}")
57
- print(f"Configs for feature matchers:\n{pformat(match_features.confs)}")
 
58
 
59
- # pick one of the configurations for extraction and matching
60
- retrieval_conf = extract_features.confs["netvlad"]
61
- feature_conf = extract_features.confs["superpoint_max"]
62
- matcher_conf = match_features.confs["superglue"]
63
 
64
- features = extract_features.main(feature_conf, images, outputs)
 
 
 
65
 
66
- pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=args.num_covis)
67
- sfm_matches = match_features.main(
68
- matcher_conf, sfm_pairs, feature_conf["output"], outputs
69
- )
70
 
71
- triangulation.main(
72
- reference_sfm, sift_sfm, images, sfm_pairs, features, sfm_matches
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
- localize_sfm.main(
88
- reference_sfm,
89
- dataset / "queries/*_time_queries_with_intrinsics.txt",
90
- loc_pairs,
91
- features,
92
- loc_matches,
93
- results,
94
- covisibility_clustering=False,
95
- ) # not required with SuperPoint+SuperGlue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 extract_features, match_dense, triangulation
6
- from ... import pairs_from_covisibility, pairs_from_retrieval, localize_sfm
 
 
 
 
 
 
 
7
 
8
 
9
- parser = argparse.ArgumentParser()
10
- parser.add_argument(
11
- "--dataset",
12
- type=Path,
13
- default="datasets/aachen_v1.1",
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
- # Setup the paths
37
- dataset = args.dataset
38
- images = dataset / "images/images_upright/"
39
- sift_sfm = dataset / "3D-models/aachen_v_1_1"
 
 
 
 
 
 
40
 
41
- outputs = args.outputs # where everything will be saved
42
- outputs.mkdir()
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
- # list the standard configurations available
53
- print(f"Configs for dense feature matchers:\n{pformat(match_dense.confs)}")
 
54
 
55
- # pick one of the configurations for extraction and matching
56
- retrieval_conf = extract_features.confs["netvlad"]
57
- matcher_conf = match_dense.confs["loftr_aachen"]
 
58
 
59
- pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=args.num_covis)
60
- features, sfm_matches = match_dense.main(
61
- matcher_conf, sfm_pairs, images, outputs, max_kps=8192, overwrite=False
62
- )
63
 
64
- triangulation.main(
65
- reference_sfm, sift_sfm, images, sfm_pairs, features, sfm_matches
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
- localize_sfm.main(
87
- reference_sfm,
88
- dataset / "queries/*_time_queries_with_intrinsics.txt",
89
- loc_pairs,
90
- features,
91
- loc_matches,
92
- results,
93
- covisibility_clustering=False,
94
- ) # not required with loftr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 extract_features, match_features, triangulation, logger
5
- from ... import pairs_from_covisibility, pairs_from_retrieval, localize_sfm
 
 
 
 
 
 
 
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
- retrieval_conf, ref_images, outputs
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 .utils import create_query_list_with_intrinsics, scale_sfm_images, evaluate
5
- from ... import extract_features, match_features, pairs_from_covisibility
6
- from ... import triangulation, localize_sfm, pairs_from_retrieval, logger
 
 
 
 
 
 
 
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
- import logging
 
 
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 = sorted(image_dir.glob("**/*.jpg"))
37
- queries = [str(q.relative_to(image_dir.parents[0])) for q in 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
- parser = argparse.ArgumentParser()
45
- parser.add_argument(
46
- "--dataset",
47
- type=Path,
48
- default="datasets/robotcar",
49
- help="Path to the dataset, default: %(default)s",
50
- )
51
- parser.add_argument(
52
- "--outputs",
53
- type=Path,
54
- default="outputs/robotcar",
55
- help="Path to the output directory, default: %(default)s",
56
- )
57
- parser.add_argument(
58
- "--num_covis",
59
- type=int,
60
- default=20,
61
- help="Number of image pairs for SfM, default: %(default)s",
62
- )
63
- parser.add_argument(
64
- "--num_loc",
65
- type=int,
66
- default=20,
67
- help="Number of image pairs for loc, default: %(default)s",
68
- )
69
- args = parser.parse_args()
70
-
71
- # Setup the paths
72
- dataset = args.dataset
73
- images = dataset / "images/"
74
-
75
- outputs = args.outputs # where everything will be saved
76
- outputs.mkdir(exist_ok=True, parents=True)
77
- query_list = outputs / "{condition}_queries_with_intrinsics.txt"
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
- features = extract_features.main(feature_conf, images, outputs, as_half=True)
 
 
97
 
98
- colmap_from_nvm.main(
99
- dataset / "3D-models/all-merged/all.nvm",
100
- dataset / "3D-models/overcast-reference.db",
101
- sift_sfm,
102
- )
103
- pairs_from_covisibility.main(sift_sfm, sfm_pairs, num_matched=args.num_covis)
104
- sfm_matches = match_features.main(
105
- matcher_conf, sfm_pairs, feature_conf["output"], outputs
106
- )
 
 
 
107
 
108
- triangulation.main(
109
- reference_sfm, sift_sfm, images, sfm_pairs, features, sfm_matches
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
- localize_sfm.main(
126
- reference_sfm,
127
- Path(str(query_list).format(condition="*")),
128
- loc_pairs,
129
- features,
130
- loc_matches,
131
- results,
132
- covisibility_clustering=False,
133
- prepend_camera_name=True,
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 numpy as np
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
- CREATE_KEYPOINTS_TABLE
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 vector_to_cross_product_matrix(v):
10
- return np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]])
11
-
12
-
13
- def compute_epipolar_errors(qvec_r2t, tvec_r2t, p2d_r, p2d_t):
14
- T_r2t = pose_matrix_from_qvec_tvec(qvec_r2t, tvec_r2t)
15
- # Compute errors in normalized plane to avoid distortion.
16
- E = vector_to_cross_product_matrix(T_r2t[:3, -1]) @ T_r2t[:3, :3]
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(model, int(width), int(height), params)
 
 
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
- fid, num_bytes=8, format_char_sequence="Q"
275
- )[0]
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
- fid, num_bytes=8, format_char_sequence="Q"
412
- )[0]
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
- os.path.join(path, "model/")
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) * 4.5, 4.5]
41
- fig, ax = plt.subplots(
42
  1, n, figsize=figsize, dpi=dpi, gridspec_kw={"width_ratios": ratios}
43
  )
44
  if n == 1:
45
- ax = [ax]
46
- for i in range(n):
47
- ax[i].imshow(imgs[i], cmap=plt.get_cmap(cmaps[i]))
48
- ax[i].get_yaxis().set_ticks([])
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[i].set_title(titles[i])
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
- transFigure = fig.transFigure.inverted()
100
- fkpts0 = transFigure.transform(ax0.transData.transform(kpts0))
101
- fkpts1 = transFigure.transform(ax1.transData.transform(kpts1))
102
- fig.lines += [
103
- matplotlib.lines.Line2D(
104
- (fkpts0[i, 0], fkpts1[i, 0]),
105
- (fkpts0[i, 1], fkpts1[i, 1]),
106
- zorder=1,
107
- transform=fig.transFigure,
108
- c=color[i],
109
- linewidth=lw,
110
- alpha=a,
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
- pyramid = go.Mesh3d(
121
- x=x,
122
- y=y,
123
- z=z,
124
- color=color,
125
- i=i,
126
- j=j,
127
- k=k,
128
- legendgroup=legendgroup,
129
- name=name,
130
- showlegend=False,
131
- )
132
- fig.add_trace(pyramid)
 
 
 
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
- image.rotmat().T,
162
- image.projection_center(),
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
- xyzs = [
194
- p3D.xyz
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=color, ps=1, name=name)
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)