Skip to content

Commit 0dce1db

Browse files
ClementPinardahojnnes
authored andcommitted
Add write_model functions for python scripts (colmap#753)
* Add write_model function This commit adds functions to write models using python, be it binary of textual Tests have been added too Both have been renamed for their new functionality * Put arg parser inside main This commit also unifies the notation between the binary and text writers Finally, it now longers takes the key of the dictionnary for id to let the user use they key dict they want
1 parent 5657e83 commit 0dce1db

File tree

2 files changed

+200
-7
lines changed

2 files changed

+200
-7
lines changed
Lines changed: 177 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import collections
3535
import numpy as np
3636
import struct
37+
import argparse
3738

3839

3940
CameraModel = collections.namedtuple(
@@ -45,6 +46,7 @@
4546
Point3D = collections.namedtuple(
4647
"Point3D", ["id", "xyz", "rgb", "error", "image_ids", "point2D_idxs"])
4748

49+
4850
class Image(BaseImage):
4951
def qvec2rotmat(self):
5052
return qvec2rotmat(self.qvec)
@@ -63,8 +65,10 @@ def qvec2rotmat(self):
6365
CameraModel(model_id=9, model_name="RADIAL_FISHEYE", num_params=5),
6466
CameraModel(model_id=10, model_name="THIN_PRISM_FISHEYE", num_params=12)
6567
}
66-
CAMERA_MODEL_IDS = dict([(camera_model.model_id, camera_model) \
68+
CAMERA_MODEL_IDS = dict([(camera_model.model_id, camera_model)
6769
for camera_model in CAMERA_MODELS])
70+
CAMERA_MODEL_NAMES = dict([(camera_model.model_name, camera_model)
71+
for camera_model in CAMERA_MODELS])
6872

6973

7074
def read_next_bytes(fid, num_bytes, format_char_sequence, endian_character="<"):
@@ -79,6 +83,22 @@ def read_next_bytes(fid, num_bytes, format_char_sequence, endian_character="<"):
7983
return struct.unpack(endian_character + format_char_sequence, data)
8084

8185

86+
def write_next_bytes(fid, data, format_char_sequence, endian_character="<"):
87+
"""pack and write to a binary file.
88+
:param fid:
89+
:param data: data to send, if multiple elements are sent at the same time,
90+
they should be encapsuled either in a list or a tuple
91+
:param format_char_sequence: List of {c, e, f, d, h, H, i, I, l, L, q, Q}.
92+
should be the same length as the data list or tuple
93+
:param endian_character: Any of {@, =, <, >, !}
94+
"""
95+
if isinstance(data, (list, tuple)):
96+
bytes = struct.pack(endian_character + format_char_sequence, *data)
97+
else:
98+
bytes = struct.pack(endian_character + format_char_sequence, data)
99+
fid.write(bytes)
100+
101+
82102
def read_cameras_text(path):
83103
"""
84104
see: src/base/reconstruction.cc
@@ -134,6 +154,43 @@ def read_cameras_binary(path_to_model_file):
134154
return cameras
135155

136156

157+
def write_cameras_text(cameras, path):
158+
"""
159+
see: src/base/reconstruction.cc
160+
void Reconstruction::WriteCamerasText(const std::string& path)
161+
void Reconstruction::ReadCamerasText(const std::string& path)
162+
"""
163+
HEADER = '# Camera list with one line of data per camera:\n'
164+
'# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]\n'
165+
'# Number of cameras: {}\n'.format(len(cameras))
166+
with open(path, "w") as fid:
167+
fid.write(HEADER)
168+
for _, cam in cameras.items():
169+
to_write = [cam.id, cam.model, cam.width, cam.height, *cam.params]
170+
line = " ".join([str(elem) for elem in to_write])
171+
fid.write(line + "\n")
172+
173+
174+
def write_cameras_binary(cameras, path_to_model_file):
175+
"""
176+
see: src/base/reconstruction.cc
177+
void Reconstruction::WriteCamerasBinary(const std::string& path)
178+
void Reconstruction::ReadCamerasBinary(const std::string& path)
179+
"""
180+
with open(path_to_model_file, "wb") as fid:
181+
write_next_bytes(fid, len(cameras), "Q")
182+
for _, cam in cameras.items():
183+
model_id = CAMERA_MODEL_NAMES[cam.model].model_id
184+
camera_properties = [cam.id,
185+
model_id,
186+
cam.width,
187+
cam.height]
188+
write_next_bytes(fid, camera_properties, "iiQQ")
189+
for p in cam.params:
190+
write_next_bytes(fid, float(p), "d")
191+
return cameras
192+
193+
137194
def read_images_text(path):
138195
"""
139196
see: src/base/reconstruction.cc
@@ -200,6 +257,55 @@ def read_images_binary(path_to_model_file):
200257
return images
201258

202259

260+
def write_images_text(images, path):
261+
"""
262+
see: src/base/reconstruction.cc
263+
void Reconstruction::ReadImagesText(const std::string& path)
264+
void Reconstruction::WriteImagesText(const std::string& path)
265+
"""
266+
if len(images) == 0:
267+
mean_observations = 0
268+
else:
269+
mean_observations = sum((len(img.point3D_ids) for _, img in images.items()))/len(images)
270+
HEADER = '# Image list with two lines of data per image:\n'
271+
'# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n'
272+
'# POINTS2D[] as (X, Y, POINT3D_ID)\n'
273+
'# Number of images: {}, mean observations per image: {}\n'.format(len(images), mean_observations)
274+
275+
with open(path, "w") as fid:
276+
fid.write(HEADER)
277+
for _, img in images.items():
278+
image_header = [img.id, *img.qvec, *img.tvec, img.camera_id, img.name]
279+
first_line = " ".join(map(str, image_header))
280+
fid.write(first_line + "\n")
281+
282+
points_strings = []
283+
for xy, point3D_id in zip(img.xys, img.point3D_ids):
284+
points_strings.append(" ".join(map(str, [*xy, point3D_id])))
285+
fid.write(" ".join(points_strings) + "\n")
286+
287+
288+
def write_images_binary(images, path_to_model_file):
289+
"""
290+
see: src/base/reconstruction.cc
291+
void Reconstruction::ReadImagesBinary(const std::string& path)
292+
void Reconstruction::WriteImagesBinary(const std::string& path)
293+
"""
294+
with open(path_to_model_file, "wb") as fid:
295+
write_next_bytes(fid, len(images), "Q")
296+
for _, img in images.items():
297+
write_next_bytes(fid, img.id, "i")
298+
write_next_bytes(fid, img.qvec.tolist(), "dddd")
299+
write_next_bytes(fid, img.tvec.tolist(), "ddd")
300+
write_next_bytes(fid, img.camera_id, "i")
301+
for char in img.name:
302+
write_next_bytes(fid, char.encode("utf-8"), "c")
303+
write_next_bytes(fid, b"\x00", "c")
304+
write_next_bytes(fid, len(img.point3D_ids), "Q")
305+
for xy, p3d_id in zip(img.xys, img.point3D_ids):
306+
write_next_bytes(fid, [*xy, p3d_id], "ddq")
307+
308+
203309
def read_points3D_text(path):
204310
"""
205311
see: src/base/reconstruction.cc
@@ -257,6 +363,50 @@ def read_points3d_binary(path_to_model_file):
257363
return points3D
258364

259365

366+
def write_points3D_text(points3D, path):
367+
"""
368+
see: src/base/reconstruction.cc
369+
void Reconstruction::ReadPoints3DText(const std::string& path)
370+
void Reconstruction::WritePoints3DText(const std::string& path)
371+
"""
372+
if len(points3D) == 0:
373+
mean_track_length = 0
374+
else:
375+
mean_track_length = sum((len(pt.image_ids) for _, pt in points3D.items()))/len(points3D)
376+
HEADER = '# 3D point list with one line of data per point:\n'
377+
'# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)\n'
378+
'# Number of points: {}, mean track length: {}\n'.format(len(points3D), mean_track_length)
379+
380+
with open(path, "w") as fid:
381+
fid.write(HEADER)
382+
for _, pt in points3D.items():
383+
point_header = [pt.id, *pt.xyz, *pt.rgb, pt.error]
384+
fid.write(" ".join(map(str, point_header)) + " ")
385+
track_strings = []
386+
for image_id, point2D in zip(pt.image_ids, pt.point2D_idxs):
387+
track_strings.append(" ".join(map(str, [image_id, point2D])))
388+
fid.write(" ".join(track_strings) + "\n")
389+
390+
391+
def write_points3d_binary(points3D, path_to_model_file):
392+
"""
393+
see: src/base/reconstruction.cc
394+
void Reconstruction::ReadPoints3DBinary(const std::string& path)
395+
void Reconstruction::WritePoints3DBinary(const std::string& path)
396+
"""
397+
with open(path_to_model_file, "wb") as fid:
398+
write_next_bytes(fid, len(points3D), "Q")
399+
for _, pt in points3D.items():
400+
write_next_bytes(fid, pt.id, "Q")
401+
write_next_bytes(fid, pt.xyz.tolist(), "ddd")
402+
write_next_bytes(fid, pt.rgb.tolist(), "BBB")
403+
write_next_bytes(fid, pt.error, "d")
404+
track_length = pt.image_ids.shape[0]
405+
write_next_bytes(fid, track_length, "Q")
406+
for image_id, point2D_id in zip(pt.image_ids, pt.point2D_idxs):
407+
write_next_bytes(fid, [image_id, point2D_id], "ii")
408+
409+
260410
def read_model(path, ext):
261411
if ext == ".txt":
262412
cameras = read_cameras_text(os.path.join(path, "cameras" + ext))
@@ -269,6 +419,18 @@ def read_model(path, ext):
269419
return cameras, images, points3D
270420

271421

422+
def write_model(cameras, images, points3D, path, ext):
423+
if ext == ".txt":
424+
write_cameras_text(cameras, os.path.join(path, "cameras" + ext))
425+
write_images_text(images, os.path.join(path, "images" + ext))
426+
write_points3D_text(points3D, os.path.join(path, "points3D") + ext)
427+
else:
428+
write_cameras_binary(cameras, os.path.join(path, "cameras" + ext))
429+
write_images_binary(images, os.path.join(path, "images" + ext))
430+
write_points3d_binary(points3D, os.path.join(path, "points3D") + ext)
431+
return cameras, images, points3D
432+
433+
272434
def qvec2rotmat(qvec):
273435
return np.array([
274436
[1 - 2 * qvec[2]**2 - 2 * qvec[3]**2,
@@ -297,16 +459,25 @@ def rotmat2qvec(R):
297459

298460

299461
def main():
300-
if len(sys.argv) != 3:
301-
print("Usage: python read_model.py path/to/model/folder [.txt,.bin]")
302-
return
303-
304-
cameras, images, points3D = read_model(path=sys.argv[1], ext=sys.argv[2])
462+
parser = argparse.ArgumentParser(description='Read and write COLMAP binary and text models')
463+
parser.add_argument('input_model', help='path to input model folder')
464+
parser.add_argument('input_format', choices=['.bin', '.txt'],
465+
help='input model format')
466+
parser.add_argument('--output_model', metavar='PATH',
467+
help='path to output model folder')
468+
parser.add_argument('--output_format', choices=['.bin', '.txt'],
469+
help='outut model format', default='.txt')
470+
args = parser.parse_args()
471+
472+
cameras, images, points3D = read_model(path=args.input_model, ext=args.input_format)
305473

306474
print("num_cameras:", len(cameras))
307475
print("num_images:", len(images))
308476
print("num_points3D:", len(points3D))
309477

478+
if args.output_model is not None:
479+
write_model(cameras, images, points3D, path=args.output_model, ext=args.output_format)
480+
310481

311482
if __name__ == "__main__":
312483
main()
Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)
3131

3232
import numpy as np
33-
from read_model import read_model
33+
from read_write_model import read_model, write_model
34+
from tempfile import mkdtemp
3435

3536

3637
def compare_cameras(cameras1, cameras2):
@@ -90,6 +91,27 @@ def main():
9091
compare_points(points3D_txt, points3D_bin)
9192

9293
print("... text and binary models are equal.")
94+
print("Saving text model and reloading it ...")
95+
96+
tmpdir = mkdtemp()
97+
write_model(cameras_bin, images_bin, points3D_bin, tmpdir, ext='.txt')
98+
cameras_txt, images_txt, points3D_txt = \
99+
read_model(tmpdir, ext=".txt")
100+
compare_cameras(cameras_txt, cameras_bin)
101+
compare_images(images_txt, images_bin)
102+
compare_points(points3D_txt, points3D_bin)
103+
104+
print("... saved text and loaded models are equal.")
105+
print("Saving binary model and reloading it ...")
106+
107+
write_model(cameras_bin, images_bin, points3D_bin, tmpdir, ext='.bin')
108+
cameras_bin, images_bin, points3D_bin = \
109+
read_model(tmpdir, ext=".bin")
110+
compare_cameras(cameras_txt, cameras_bin)
111+
compare_images(images_txt, images_bin)
112+
compare_points(points3D_txt, points3D_bin)
113+
114+
print("... saved binary and loaded models are equal.")
93115

94116

95117
if __name__ == "__main__":

0 commit comments

Comments
 (0)