Skip to content

Commit 2cc0187

Browse files
Merge pull request #583 from mapillary/feat-import-colmap
feat: script to import from colmap
2 parents 8821886 + 16feb0b commit 2cc0187

1 file changed

Lines changed: 329 additions & 0 deletions

File tree

bin/import_colmap

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
#!/usr/bin/env python3
2+
3+
# Snippets to read from the colmap database taken from:
4+
# https://github.com/colmap/colmap/blob/ad7bd93f1a27af7533121aa043a167fe1490688c /
5+
# scripts/python/export_to_bundler.py
6+
# scripts/python/read_write_model.py
7+
# License is that derived from those files.
8+
9+
from __future__ import absolute_import
10+
from __future__ import division
11+
from __future__ import print_function
12+
from __future__ import unicode_literals
13+
14+
import argparse
15+
import math
16+
import os
17+
import sqlite3
18+
from pathlib import Path
19+
20+
import networkx
21+
import numpy as np
22+
23+
from opensfm import dataset
24+
from opensfm import features
25+
from opensfm import io
26+
from opensfm import types
27+
28+
EXPORT_DIR_NAME = 'opensfm_export'
29+
30+
31+
def import_cameras_images(db, data):
32+
cursor = db.cursor()
33+
cursor.execute("SELECT camera_id, model, width, height, prior_focal_length, params FROM "
34+
"cameras;")
35+
cameras = {}
36+
for row in cursor:
37+
camera_id, camera_model_id, width, height, prior_focal, params = row
38+
params = np.fromstring(params, dtype=np.double)
39+
cam = cam_from_colmap_params(camera_model_id, width, height, params, prior_focal)
40+
cam.id = str(camera_id)
41+
cameras[camera_id] = cam
42+
43+
data.save_camera_models(cameras)
44+
45+
images_map = {}
46+
cursor.execute("SELECT image_id, camera_id, name FROM images;")
47+
for row in cursor:
48+
image_id, camera_id, filename = int(row[0]), int(row[1]), row[2]
49+
images_map[image_id] = (filename, camera_id)
50+
cam = cameras[camera_id]
51+
focal_ratio = cam.focal_x if isinstance(cam, types.BrownPerspectiveCamera) else cam.focal
52+
exif_data = {
53+
"make": "unknown",
54+
"model": "unknown",
55+
"width": cam.width,
56+
"height": cam.height,
57+
"projection_type": cam.projection_type,
58+
"focal_ratio": focal_ratio,
59+
"orientation": 1,
60+
"camera": "{}".format(camera_id),
61+
"skey": "TheSequence",
62+
"capture_time": 0.0,
63+
"gps": {},
64+
}
65+
data.save_exif(filename, exif_data)
66+
67+
cursor.close()
68+
return cameras, images_map
69+
70+
71+
def pair_id_to_image_ids(pair_id):
72+
image_id2 = pair_id % 2147483647
73+
image_id1 = (pair_id - image_id2) // 2147483647
74+
return image_id1, image_id2
75+
76+
77+
def get_scale_orientation_from_affine(arr):
78+
# (x, y, a_11, a_12, a_21, a_22)
79+
a11 = arr[:, 2]
80+
a12 = arr[:, 3]
81+
a21 = arr[:, 4]
82+
a22 = arr[:, 5]
83+
scale_x = np.sqrt(a11 * a11 + a21 * a21)
84+
scale_y = np.sqrt(a12 * a12 + a22 * a22)
85+
orientation = np.arctan2(a21, a11)
86+
# shear = np.arctan2(-a12, a22) - orientation
87+
scale = (scale_x + scale_y) / 2
88+
return scale, orientation
89+
90+
91+
def import_features(db, data, image_map, camera_map):
92+
cursor = db.cursor()
93+
cursor.execute("SELECT image_id, rows, cols, data FROM keypoints;")
94+
keypoints = {}
95+
colors = {}
96+
for row in cursor:
97+
image_id, n_rows, n_cols, arr = row
98+
filename, camera_id = image_map[image_id]
99+
cam = camera_map[camera_id]
100+
101+
arr = np.fromstring(arr, dtype=np.float32).reshape((n_rows, n_cols))
102+
103+
rgb = data.load_image(filename).astype(np.float32)
104+
xc = np.clip(arr[:, 1].astype(int), 0, rgb.shape[0] - 1)
105+
yc = np.clip(arr[:, 0].astype(int), 0, rgb.shape[1] - 1)
106+
colors[image_id] = rgb[xc, yc, :]
107+
108+
arr[:, :2] = features.normalized_image_coordinates(arr[:, :2], cam.width, cam.height)
109+
if n_cols == 4:
110+
x, y, s, o = arr[:, 0], arr[:, 1], arr[:, 2], arr[:, 3]
111+
elif n_cols == 6:
112+
x, y = arr[:, 0], arr[:, 1]
113+
s, o = get_scale_orientation_from_affine(arr)
114+
elif n_cols == 2:
115+
x, y = arr[:, 0], arr[:, 1]
116+
s = np.zeros_like(x)
117+
o = np.zeros_like(x)
118+
else:
119+
raise ValueError
120+
s = s / max(cam.width, cam.height)
121+
keypoints[image_id] = np.vstack((x, y, s, o)).T
122+
123+
cursor.execute("SELECT image_id, rows, cols, data FROM descriptors;")
124+
for row in cursor:
125+
image_id, n_rows, n_cols, arr = row
126+
filename, _ = image_map[image_id]
127+
descriptors = np.fromstring(arr, dtype=np.uint8).reshape((n_rows, n_cols))
128+
kp = keypoints[image_id]
129+
data.save_features(filename, kp, descriptors, colors[image_id])
130+
131+
cursor.close()
132+
return keypoints
133+
134+
135+
def import_matches(db, data, image_map):
136+
cursor = db.cursor()
137+
min_matches = 1
138+
cursor.execute("SELECT pair_id, data FROM two_view_geometries WHERE rows>=?;", (min_matches,))
139+
140+
matches_per_im1 = {m[0]: {} for m in image_map.values()}
141+
142+
for row in cursor:
143+
pair_id = row[0]
144+
inlier_matches = np.fromstring(row[1], dtype=np.uint32).reshape(-1, 2)
145+
image_id1, image_id2 = pair_id_to_image_ids(pair_id)
146+
image_name1 = image_map[image_id1][0]
147+
image_name2 = image_map[image_id2][0]
148+
matches_per_im1[image_name1][image_name2] = inlier_matches
149+
150+
for image_name1, matches in matches_per_im1.items():
151+
data.save_matches(image_name1, matches)
152+
153+
cursor.close()
154+
155+
156+
def import_cameras_reconstruction(path_cameras):
157+
"""
158+
Imports cameras from a COLMAP reconstruction text file
159+
"""
160+
r_cams = {}
161+
mapping = {'FULL_OPENCV': 6, 'RADIAL': 3, 'RADIAL_FISHEYE': 9}
162+
with io.open_rt(path_cameras) as fin:
163+
for row in fin:
164+
if row[0] == '#':
165+
continue
166+
row = row[:-1].split(' ')
167+
camera_id = row[0]
168+
camera_model = row[1]
169+
width = int(row[2])
170+
height = int(row[3])
171+
params = [float(p) for p in row[4:]]
172+
camera_model_id = mapping[camera_model]
173+
cam = cam_from_colmap_params(camera_model_id, width, height, params)
174+
cam.id = camera_id
175+
r_cams[camera_id] = cam
176+
return r_cams
177+
178+
179+
def cam_from_colmap_params(camera_model_id, width, height, params, prior_focal=1):
180+
"""
181+
Helper function to map from colmap parameters to an OpenSfM camera
182+
"""
183+
mapping = {3: 'perspective', 6: 'brown', 9: 'fisheye'}
184+
projection_type = mapping[camera_model_id]
185+
normalizer = max(width, height)
186+
if projection_type == 'perspective':
187+
cam = types.PerspectiveCamera()
188+
cam.focal = params[0] / normalizer if prior_focal else 0.85
189+
cam.k1 = params[3]
190+
cam.k2 = params[4]
191+
elif projection_type == 'brown':
192+
cam = types.BrownPerspectiveCamera()
193+
cam.focal_x = params[0] / normalizer if prior_focal else 0.85
194+
cam.focal_y = params[1] / normalizer if prior_focal else 0.85
195+
cam.c_x = (params[2] - (width - 1) * 0.5) / normalizer
196+
cam.c_y = (params[3] - (height - 1) * 0.5) / normalizer
197+
cam.k1, cam.k2, cam.p1, cam.p2, cam.k3 = params[4:9]
198+
else: # projection_type == 'fisheye'
199+
cam = types.FisheyeCamera()
200+
cam.focal = params[0] / normalizer if prior_focal else 0.85
201+
cam.k1 = params[3]
202+
cam.width = width
203+
cam.height = height
204+
return cam
205+
206+
207+
def import_shots_reconstruction(path_shots, camera_map, keypoints, points3D):
208+
"""
209+
Reads the points.txt from colmap, which contains two lines of data per image:
210+
# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME
211+
# POINTS2D[] as (X, Y, POINT3D_ID)
212+
213+
With this, we build the OpenSfM shots and a partial tracks graph.
214+
"""
215+
tracks_graph = networkx.Graph()
216+
shots = {}
217+
with io.open_rt(path_shots) as fin:
218+
while True:
219+
row = fin.readline().strip()
220+
if not row: # None or len==0
221+
break
222+
if row[0] == '#':
223+
continue
224+
225+
row = row.split(' ')
226+
colmap_shot_id = int(row[0])
227+
q = np.array(tuple(map(float, row[1:5])))
228+
t = np.array(tuple(map(float, row[5:8])))
229+
colmap_camera_id = row[8]
230+
image = row[9] # filename / key
231+
232+
shot = types.Shot()
233+
shot.pose = types.Pose(rotation=quaternion_to_angle_axis(q), translation=t)
234+
shot.camera = camera_map[colmap_camera_id]
235+
shot.id = image
236+
shots[shot.id] = shot
237+
238+
row = fin.readline().strip().split(' ')
239+
xys = np.column_stack([tuple(map(float, row[0::3])), tuple(map(float, row[1::3]))])
240+
xys = features.normalized_image_coordinates(xys,
241+
shot.camera.width,
242+
shot.camera.height)
243+
point3D_ids = np.array(tuple(map(int, row[2::3])))
244+
245+
point2d_idx = 0
246+
for point3D_id, xy in zip(point3D_ids, xys):
247+
if point3D_id == -1:
248+
continue
249+
kp = keypoints[colmap_shot_id][point2d_idx]
250+
s = kp[2]
251+
tracks_graph.add_node(str(image), bipartite=0)
252+
tracks_graph.add_node(str(point3D_id), bipartite=1)
253+
tracks_graph.add_edge(str(image),
254+
str(point3D_id),
255+
feature=(float(xy[0]), float(xy[1])),
256+
feature_id=point2d_idx,
257+
feature_scale=float(s),
258+
feature_color=points3D[point3D_id].color)
259+
point2d_idx += 1
260+
261+
return shots, tracks_graph
262+
263+
264+
def import_points_reconstruction(path_points):
265+
points3d = {}
266+
267+
with io.open_rt(path_points) as fin:
268+
for row in fin:
269+
if row[0] == '#':
270+
continue
271+
row = row[:-1].split(' ')
272+
p = types.Point()
273+
p.id = int(row[0])
274+
p.coordinates = tuple(map(float, row[1:4]))
275+
p.color = tuple(map(int, row[4:7]))
276+
points3d[p.id] = p
277+
return points3d
278+
279+
280+
def quaternion_to_angle_axis(quaternion):
281+
if quaternion[0] > 1:
282+
quaternion = quaternion / np.linalg.norm(quaternion)
283+
qw, qx, qy, qz = quaternion
284+
s = max(0.001, math.sqrt(1 - qw * qw))
285+
x = qx / s
286+
y = qy / s
287+
z = qz / s
288+
angle = 2 * math.acos(qw)
289+
return [angle * x, angle * y, angle * z]
290+
291+
292+
if __name__ == "__main__":
293+
parser = argparse.ArgumentParser(
294+
description='Convert COLMAP database to OpenSfM dataset')
295+
parser.add_argument('database', help='path to the database to be processed')
296+
parser.add_argument('images', help='path to the images')
297+
args = parser.parse_args()
298+
299+
p_db = Path(args.database)
300+
export_folder = p_db.parent / EXPORT_DIR_NAME
301+
export_folder.mkdir(exist_ok=True)
302+
images_path = export_folder / 'images'
303+
if not images_path.exists():
304+
os.symlink(args.images, images_path, target_is_directory=True)
305+
306+
data = dataset.DataSet(export_folder)
307+
db = sqlite3.connect(p_db.as_posix())
308+
camera_map, image_map = import_cameras_images(db, data)
309+
keypoints = import_features(db, data, image_map, camera_map)
310+
import_matches(db, data, image_map)
311+
312+
rec_cameras = p_db.parent / 'cameras.txt'
313+
rec_points = p_db.parent / 'points3D.txt'
314+
rec_images = p_db.parent / 'images.txt'
315+
if rec_cameras.exists() and rec_images.exists() and rec_points.exists():
316+
cameras = import_cameras_reconstruction(rec_cameras)
317+
points3D = import_points_reconstruction(rec_points)
318+
shots, tracks_graph = import_shots_reconstruction(rec_images, cameras, keypoints, points3D)
319+
data.save_tracks_graph(tracks_graph)
320+
321+
reconstruction = types.Reconstruction()
322+
reconstruction.cameras = cameras
323+
reconstruction.shots = shots
324+
reconstruction.points = points3D
325+
data.save_reconstruction([reconstruction])
326+
else:
327+
print("Didn't find reconstruction files in text format")
328+
329+
db.close()

0 commit comments

Comments
 (0)