bev-gorry commited on
Commit
f5514a5
·
1 Parent(s): 9872e4a

add python scripts

Browse files
scripts/python/benchmark_eth3d.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import subprocess
4
+ import sys
5
+ import urllib.request
6
+
7
+
8
+ def download_file(url, file_path, max_retries=3):
9
+ if os.path.exists(file_path):
10
+ return
11
+ print(f"Downloading {url} to {file_path}")
12
+ for retry in range(max_retries):
13
+ try:
14
+ urllib.request.urlretrieve(url, file_path)
15
+ return
16
+ except Exception as exc:
17
+ print(
18
+ f"Failed to download {url} (trial={retry + 1}) to {file_path} due to {exc}"
19
+ )
20
+
21
+
22
+ def check_small_errors_or_exit(
23
+ dataset_name,
24
+ max_rotation_error,
25
+ max_proj_center_error,
26
+ expected_num_images,
27
+ errors_csv_path,
28
+ ):
29
+ print(f"Evaluating errors for {dataset_name}")
30
+
31
+ error = False
32
+ with open(errors_csv_path, "r") as fid:
33
+ num_images = 0
34
+ for line in fid:
35
+ line = line.strip()
36
+ if len(line) == 0 or line.startswith("#"):
37
+ continue
38
+ rotation_error, proj_center_error = map(float, line.split(","))
39
+ num_images += 1
40
+ if rotation_error > max_rotation_error:
41
+ print("Exceeded rotation error threshold:", rotation_error)
42
+ error = True
43
+ if proj_center_error > max_proj_center_error:
44
+ print(
45
+ "Exceeded projection center error threshold:",
46
+ proj_center_error,
47
+ )
48
+ error = True
49
+
50
+ if num_images != expected_num_images:
51
+ print("Unexpected number of images:", num_images)
52
+ error = True
53
+
54
+ if error:
55
+ sys.exit(1)
56
+
57
+
58
+ def process_dataset(args, dataset_name):
59
+ print("Processing dataset:", dataset_name)
60
+
61
+ workspace_path = os.path.join(
62
+ os.path.realpath(args.workspace_path), dataset_name
63
+ )
64
+ os.makedirs(workspace_path, exist_ok=True)
65
+
66
+ dataset_archive_path = os.path.join(workspace_path, f"{dataset_name}.7z")
67
+ download_file(
68
+ f"https://www.eth3d.net/data/{dataset_name}_dslr_undistorted.7z",
69
+ dataset_archive_path,
70
+ )
71
+
72
+ subprocess.check_call(
73
+ ["7zz", "x", "-y", f"{dataset_name}.7z"], cwd=workspace_path
74
+ )
75
+
76
+ # Find undistorted parameters of first camera and initialize all images with it.
77
+ with open(
78
+ os.path.join(
79
+ workspace_path,
80
+ f"{dataset_name}/dslr_calibration_undistorted/cameras.txt",
81
+ ),
82
+ "r",
83
+ ) as fid:
84
+ for line in fid:
85
+ if not line.startswith("#"):
86
+ first_camera_data = line.split()
87
+ camera_model = first_camera_data[1]
88
+ assert camera_model == "PINHOLE"
89
+ camera_params = first_camera_data[4:]
90
+ assert len(camera_params) == 4
91
+ break
92
+
93
+ # Count the number of expected images in the GT.
94
+ expected_num_images = 0
95
+ with open(
96
+ os.path.join(
97
+ workspace_path,
98
+ f"{dataset_name}/dslr_calibration_undistorted/images.txt",
99
+ ),
100
+ "r",
101
+ ) as fid:
102
+ for line in fid:
103
+ if not line.startswith("#") and line.strip():
104
+ expected_num_images += 1
105
+ # Each image uses two consecutive lines.
106
+ assert expected_num_images % 2 == 0
107
+ expected_num_images /= 2
108
+
109
+ # Run automatic reconstruction pipeline.
110
+ subprocess.check_call(
111
+ [
112
+ os.path.realpath(args.colmap_path),
113
+ "automatic_reconstructor",
114
+ "--image_path",
115
+ f"{dataset_name}/images/",
116
+ "--workspace_path",
117
+ workspace_path,
118
+ "--use_gpu",
119
+ "1" if args.use_gpu else "0",
120
+ "--num_threads",
121
+ str(args.num_threads),
122
+ "--quality",
123
+ args.quality,
124
+ "--camera_model",
125
+ "PINHOLE",
126
+ "--camera_params",
127
+ ",".join(camera_params),
128
+ ],
129
+ cwd=workspace_path,
130
+ )
131
+
132
+ # Compare reconstructed model to GT model.
133
+ subprocess.check_call(
134
+ [
135
+ os.path.realpath(args.colmap_path),
136
+ "model_comparer",
137
+ "--input_path1",
138
+ "sparse/0",
139
+ "--input_path2",
140
+ f"{dataset_name}/dslr_calibration_undistorted/",
141
+ "--output_path",
142
+ ".",
143
+ "--alignment_error",
144
+ "proj_center",
145
+ "--max_proj_center_error",
146
+ str(args.max_proj_center_error),
147
+ ],
148
+ cwd=workspace_path,
149
+ )
150
+
151
+ # Ensure discrepancy between reconstructed model and GT is small.
152
+ check_small_errors_or_exit(
153
+ dataset_name,
154
+ args.max_rotation_error,
155
+ args.max_proj_center_error,
156
+ expected_num_images,
157
+ os.path.join(workspace_path, "errors.csv"),
158
+ )
159
+
160
+
161
+ def parse_args():
162
+ parser = argparse.ArgumentParser()
163
+ parser.add_argument("--dataset_names", required=True)
164
+ parser.add_argument("--workspace_path", required=True)
165
+ parser.add_argument("--colmap_path", required=True)
166
+ parser.add_argument("--use_gpu", default=True, action="store_true")
167
+ parser.add_argument("--use_cpu", dest="use_gpu", action="store_false")
168
+ parser.add_argument("--num_threads", type=int, default=-1)
169
+ parser.add_argument("--quality", default="medium")
170
+ parser.add_argument("--max_rotation_error", type=float, default=1.0)
171
+ parser.add_argument("--max_proj_center_error", type=float, default=0.1)
172
+ return parser.parse_args()
173
+
174
+
175
+ def main():
176
+ args = parse_args()
177
+
178
+ for dataset_name in args.dataset_names.split(","):
179
+ process_dataset(args, dataset_name.strip())
180
+
181
+
182
+ if __name__ == "__main__":
183
+ main()
scripts/python/build_windows_app.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ import argparse
32
+ import glob
33
+ import os
34
+ import shutil
35
+
36
+
37
+ def parse_args():
38
+ parser = argparse.ArgumentParser()
39
+ parser.add_argument(
40
+ "--install_path",
41
+ required=True,
42
+ help="The installation prefix, e.g., build/__install__",
43
+ )
44
+ parser.add_argument(
45
+ "--app_path",
46
+ required=True,
47
+ help="The application path, e.g., build/COLMAP-dev-windows",
48
+ )
49
+ args = parser.parse_args()
50
+ return args
51
+
52
+
53
+ def mkdir_if_not_exists(path):
54
+ assert os.path.exists(os.path.dirname(os.path.abspath(path)))
55
+ if not os.path.exists(path):
56
+ os.makedirs(path)
57
+
58
+
59
+ def main():
60
+ args = parse_args()
61
+
62
+ mkdir_if_not_exists(args.app_path)
63
+ mkdir_if_not_exists(os.path.join(args.app_path, "bin"))
64
+ mkdir_if_not_exists(os.path.join(args.app_path, "lib"))
65
+ mkdir_if_not_exists(os.path.join(args.app_path, "lib/platforms"))
66
+
67
+ # Copy batch scripts to app directory.
68
+ shutil.copyfile(
69
+ os.path.join(args.install_path, "COLMAP.bat"),
70
+ os.path.join(args.app_path, "COLMAP.bat"),
71
+ )
72
+ shutil.copyfile(
73
+ os.path.join(args.install_path, "RUN_TESTS.bat"),
74
+ os.path.join(args.app_path, "RUN_TESTS.bat"),
75
+ )
76
+
77
+ # Copy executables to app directory.
78
+ exe_files = glob.glob(os.path.join(args.install_path, "bin/*.exe"))
79
+ for exe_file in exe_files:
80
+ shutil.copyfile(
81
+ exe_file,
82
+ os.path.join(args.app_path, "bin", os.path.basename(exe_file)),
83
+ )
84
+
85
+ # Copy shared libraries to app directory.
86
+ dll_files = glob.glob(os.path.join(args.install_path, "lib/*.dll"))
87
+ for dll_file in dll_files:
88
+ shutil.copyfile(
89
+ dll_file,
90
+ os.path.join(args.app_path, "lib", os.path.basename(dll_file)),
91
+ )
92
+ shutil.copyfile(
93
+ os.path.join(args.install_path, "lib/platforms/qwindows.dll"),
94
+ os.path.join(args.app_path, "lib/platforms/qwindows.dll"),
95
+ )
96
+
97
+ # Create zip archive for deployment.
98
+ shutil.make_archive(args.app_path, "zip", root_dir=args.app_path)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
scripts/python/bundler_to_ply.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ # This script converts a Bundler reconstruction file to a PLY point cloud.
32
+
33
+ import argparse
34
+
35
+ import numpy as np
36
+
37
+
38
+ def parse_args():
39
+ parser = argparse.ArgumentParser()
40
+ parser.add_argument("--bundler_path", required=True)
41
+ parser.add_argument("--ply_path", required=True)
42
+ parser.add_argument("--normalize", type=bool, default=True)
43
+ parser.add_argument("--normalize_p0", type=float, default=0.2)
44
+ parser.add_argument("--normalize_p1", type=float, default=0.8)
45
+ parser.add_argument("--min_track_length", type=int, default=3)
46
+ args = parser.parse_args()
47
+ return args
48
+
49
+
50
+ def main():
51
+ args = parse_args()
52
+
53
+ with open(args.bundler_path, "r") as fid:
54
+ line = fid.readline()
55
+ line = fid.readline()
56
+ num_images, num_points = map(int, line.split())
57
+
58
+ for i in range(5 * num_images):
59
+ fid.readline()
60
+
61
+ xyz = np.zeros((num_points, 3), dtype=np.float64)
62
+ rgb = np.zeros((num_points, 3), dtype=np.uint16)
63
+ track_lengths = np.zeros((num_points,), dtype=np.uint32)
64
+
65
+ for i in range(num_points):
66
+ if i % 1000 == 0:
67
+ print("Reading point", i, "/", num_points)
68
+ xyz[i] = map(float, fid.readline().split())
69
+ rgb[i] = map(int, fid.readline().split())
70
+ track_lengths[i] = int(fid.readline().split()[0])
71
+
72
+ mask = track_lengths >= args.min_track_length
73
+ xyz = xyz[mask]
74
+ rgb = rgb[mask]
75
+
76
+ if args.normalize:
77
+ sorted_x = np.sort(xyz[:, 0])
78
+ sorted_y = np.sort(xyz[:, 1])
79
+ sorted_z = np.sort(xyz[:, 2])
80
+
81
+ num_coords = sorted_x.size
82
+ min_coord = int(args.normalize_p0 * num_coords)
83
+ max_coord = int(args.normalize_p1 * num_coords)
84
+ mean_coords = xyz.mean(0)
85
+
86
+ bbox_min = np.array(
87
+ [sorted_x[min_coord], sorted_y[min_coord], sorted_z[min_coord]]
88
+ )
89
+ bbox_max = np.array(
90
+ [sorted_x[max_coord], sorted_y[max_coord], sorted_z[max_coord]]
91
+ )
92
+
93
+ extent = np.linalg.norm(bbox_max - bbox_min)
94
+ scale = 10.0 / extent
95
+
96
+ xyz -= mean_coords
97
+ xyz *= scale
98
+
99
+ xyz[:, 2] *= -1
100
+
101
+ with open(args.ply_path, "w") as fid:
102
+ fid.write("ply\n")
103
+ fid.write("format ascii 1.0\n")
104
+ fid.write("element vertex %d\n" % xyz.shape[0])
105
+ fid.write("property float x\n")
106
+ fid.write("property float y\n")
107
+ fid.write("property float z\n")
108
+ fid.write("property float nx\n")
109
+ fid.write("property float ny\n")
110
+ fid.write("property float nz\n")
111
+ fid.write("property uchar diffuse_red\n")
112
+ fid.write("property uchar diffuse_green\n")
113
+ fid.write("property uchar diffuse_blue\n")
114
+ fid.write("end_header\n")
115
+ for i in range(xyz.shape[0]):
116
+ if i % 1000 == 0:
117
+ print("Writing point", i, "/", xyz.shape[0])
118
+ fid.write(
119
+ "%f %f %f 0 0 0 %d %d %d\n"
120
+ % (
121
+ xyz[i, 0],
122
+ xyz[i, 1],
123
+ xyz[i, 2],
124
+ rgb[i, 0],
125
+ rgb[i, 1],
126
+ rgb[i, 2],
127
+ )
128
+ )
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()
scripts/python/clang_format_code.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ import argparse
32
+ import os
33
+ import string
34
+ import subprocess
35
+
36
+
37
+ def parse_args():
38
+ parser = argparse.ArgumentParser()
39
+ parser.add_argument("--path", required=True)
40
+ parser.add_argument("--exts", default=".h,.cc")
41
+ parser.add_argument("--style", default="File")
42
+ args = parser.parse_args()
43
+ return args
44
+
45
+
46
+ def main():
47
+ args = parse_args()
48
+
49
+ exts = map(string.lower, args.exts.split(","))
50
+
51
+ for root, subdirs, files in os.walk(args.path):
52
+ for f in files:
53
+ name, ext = os.path.splitext(f)
54
+ if ext.lower() in exts:
55
+ file_path = os.path.join(root, f)
56
+ proc = subprocess.Popen(
57
+ ["clang-format", "--style", args.style, file_path],
58
+ stdout=subprocess.PIPE,
59
+ )
60
+
61
+ text = "".join(proc.stdout)
62
+
63
+ with open(file_path, "w") as fd:
64
+ fd.write(text)
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
scripts/python/crawl_camera_specs.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ import argparse
32
+ import re
33
+
34
+ import requests
35
+ from lxml.html import soupparser
36
+
37
+ MAX_REQUEST_TRIALS = 10
38
+
39
+
40
+ def parse_args():
41
+ parser = argparse.ArgumentParser()
42
+ parser.add_argument("--lib_path", required=True)
43
+ args = parser.parse_args()
44
+ return args
45
+
46
+
47
+ def request_trial(func, *args, **kwargs):
48
+ for i in range(MAX_REQUEST_TRIALS):
49
+ try:
50
+ response = func(*args, **kwargs)
51
+ except: # noqa E722
52
+ continue
53
+ else:
54
+ return response
55
+
56
+ raise SystemError
57
+
58
+
59
+ def main():
60
+ args = parse_args()
61
+
62
+ ##########################################################################
63
+ # Header file
64
+ ##########################################################################
65
+
66
+ with open(args.lib_path + ".h", "w") as f:
67
+ f.write("#include <vector>\n")
68
+ f.write("#include <string>\n")
69
+ f.write("#include <unordered_map>\n\n")
70
+ f.write("// { make1 : ({ model1 : sensor-width in mm }, ...), ... }\n")
71
+ f.write(
72
+ "typedef std::vector<std::pair<std::string, float>> make_specs_t;\n"
73
+ )
74
+ f.write(
75
+ "typedef std::unordered_map<std::string, make_specs_t> camera_specs_t;;\n\n"
76
+ )
77
+ f.write("camera_specs_t InitializeCameraSpecs();\n\n")
78
+
79
+ ##########################################################################
80
+ # Source file
81
+ ##########################################################################
82
+
83
+ makes_response = requests.get("http://www.digicamdb.com")
84
+ makes_tree = soupparser.fromstring(makes_response.text)
85
+ makes_node = makes_tree.find('.//select[@id="select_brand"]')
86
+ makes = [b.attrib["value"] for b in makes_node.iter("option")]
87
+
88
+ with open(args.lib_path + ".cc", "w") as f:
89
+ f.write("camera_specs_t InitializeCameraSpecs() {\n")
90
+ f.write(" camera_specs_t specs;\n\n")
91
+ for make in makes:
92
+ f.write(" {\n")
93
+ f.write(
94
+ ' auto& make_specs = specs["%s"];\n'
95
+ % make.lower().replace(" ", "")
96
+ )
97
+
98
+ models_response = request_trial(
99
+ requests.post,
100
+ "http://www.digicamdb.com/inc/ajax.php",
101
+ data={"b": make, "role": "header_search"},
102
+ )
103
+
104
+ models_tree = soupparser.fromstring(models_response.text)
105
+ models_code = ""
106
+ num_models = 0
107
+ for model_node in models_tree.iter("option"):
108
+ model = model_node.attrib.get("value")
109
+ model_name = model_node.text
110
+ if model is None:
111
+ continue
112
+
113
+ url = "http://www.digicamdb.com/specs/{0}_{1}".format(
114
+ make, model
115
+ )
116
+ specs_response = request_trial(requests.get, url)
117
+
118
+ specs_tree = soupparser.fromstring(specs_response.text)
119
+ for spec in specs_tree.findall('.//td[@class="info_key"]'):
120
+ if spec.text.strip() == "Sensor:":
121
+ sensor_text = spec.find("..").find(
122
+ './td[@class="bold"]'
123
+ )
124
+ sensor_text = sensor_text.text.strip()
125
+ m = re.match(".*?([\d.]+) x ([\d.]+).*?", sensor_text)
126
+ sensor_width = m.group(1)
127
+ data = (
128
+ model_name.lower().replace(" ", ""),
129
+ float(sensor_width.replace(" ", "")),
130
+ )
131
+ models_code += (
132
+ ' make_specs.emplace_back("%s", %.4ff);\n' % data
133
+ )
134
+
135
+ print(make, model_name)
136
+ print(" ", sensor_text)
137
+
138
+ num_models += 1
139
+
140
+ f.write(" make_specs.reserve(%d);\n" % num_models)
141
+ f.write(models_code)
142
+ f.write(" }\n\n")
143
+
144
+ f.write(" return specs;\n")
145
+ f.write("}\n")
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()
scripts/python/database.py ADDED
@@ -0,0 +1,468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ # This script is based on an original implementation by True Price.
32
+
33
+ import sqlite3
34
+ import sys
35
+
36
+ import numpy as np
37
+
38
+ IS_PYTHON3 = sys.version_info[0] >= 3
39
+
40
+ MAX_IMAGE_ID = 2**31 - 1
41
+
42
+ CREATE_CAMERAS_TABLE = """CREATE TABLE IF NOT EXISTS cameras (
43
+ camera_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
44
+ model INTEGER NOT NULL,
45
+ width INTEGER NOT NULL,
46
+ height INTEGER NOT NULL,
47
+ params BLOB,
48
+ prior_focal_length INTEGER NOT NULL)"""
49
+
50
+ CREATE_DESCRIPTORS_TABLE = """CREATE TABLE IF NOT EXISTS descriptors (
51
+ image_id INTEGER PRIMARY KEY NOT NULL,
52
+ rows INTEGER NOT NULL,
53
+ cols INTEGER NOT NULL,
54
+ data BLOB,
55
+ FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE)"""
56
+
57
+ CREATE_IMAGES_TABLE = """CREATE TABLE IF NOT EXISTS images (
58
+ image_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
59
+ name TEXT NOT NULL UNIQUE,
60
+ camera_id INTEGER NOT NULL,
61
+ CONSTRAINT image_id_check CHECK(image_id >= 0 and image_id < {}),
62
+ FOREIGN KEY(camera_id) REFERENCES cameras(camera_id))
63
+ """.format(MAX_IMAGE_ID)
64
+
65
+ CREATE_POSE_PRIORS_TABLE = """CREATE TABLE IF NOT EXISTS pose_priors (
66
+ image_id INTEGER PRIMARY KEY NOT NULL,
67
+ position BLOB,
68
+ coordinate_system INTEGER NOT NULL,
69
+ position_covariance BLOB,
70
+ FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE)"""
71
+
72
+ CREATE_TWO_VIEW_GEOMETRIES_TABLE = """
73
+ CREATE TABLE IF NOT EXISTS two_view_geometries (
74
+ pair_id INTEGER PRIMARY KEY NOT NULL,
75
+ rows INTEGER NOT NULL,
76
+ cols INTEGER NOT NULL,
77
+ data BLOB,
78
+ config INTEGER NOT NULL,
79
+ F BLOB,
80
+ E BLOB,
81
+ H BLOB,
82
+ qvec BLOB,
83
+ tvec BLOB)
84
+ """
85
+
86
+ CREATE_KEYPOINTS_TABLE = """CREATE TABLE IF NOT EXISTS keypoints (
87
+ image_id INTEGER PRIMARY KEY NOT NULL,
88
+ rows INTEGER NOT NULL,
89
+ cols INTEGER NOT NULL,
90
+ data BLOB,
91
+ FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE)
92
+ """
93
+
94
+ CREATE_MATCHES_TABLE = """CREATE TABLE IF NOT EXISTS matches (
95
+ pair_id INTEGER PRIMARY KEY NOT NULL,
96
+ rows INTEGER NOT NULL,
97
+ cols INTEGER NOT NULL,
98
+ data BLOB)"""
99
+
100
+ CREATE_NAME_INDEX = (
101
+ "CREATE UNIQUE INDEX IF NOT EXISTS index_name ON images(name)"
102
+ )
103
+
104
+ CREATE_ALL = "; ".join(
105
+ [
106
+ CREATE_CAMERAS_TABLE,
107
+ CREATE_IMAGES_TABLE,
108
+ CREATE_POSE_PRIORS_TABLE,
109
+ CREATE_KEYPOINTS_TABLE,
110
+ CREATE_DESCRIPTORS_TABLE,
111
+ CREATE_MATCHES_TABLE,
112
+ CREATE_TWO_VIEW_GEOMETRIES_TABLE,
113
+ CREATE_NAME_INDEX,
114
+ ]
115
+ )
116
+
117
+
118
+ def image_ids_to_pair_id(image_id1, image_id2):
119
+ if image_id1 > image_id2:
120
+ image_id1, image_id2 = image_id2, image_id1
121
+ return image_id1 * MAX_IMAGE_ID + image_id2
122
+
123
+
124
+ def pair_id_to_image_ids(pair_id):
125
+ image_id2 = pair_id % MAX_IMAGE_ID
126
+ image_id1 = (pair_id - image_id2) / MAX_IMAGE_ID
127
+ return image_id1, image_id2
128
+
129
+
130
+ def array_to_blob(array):
131
+ if IS_PYTHON3:
132
+ return array.tostring()
133
+ else:
134
+ return np.getbuffer(array)
135
+
136
+
137
+ def blob_to_array(blob, dtype, shape=(-1,)):
138
+ if IS_PYTHON3:
139
+ return np.fromstring(blob, dtype=dtype).reshape(*shape)
140
+ else:
141
+ return np.frombuffer(blob, dtype=dtype).reshape(*shape)
142
+
143
+
144
+ class COLMAPDatabase(sqlite3.Connection):
145
+ @staticmethod
146
+ def connect(database_path):
147
+ return sqlite3.connect(database_path, factory=COLMAPDatabase)
148
+
149
+ def __init__(self, *args, **kwargs):
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(
154
+ CREATE_CAMERAS_TABLE
155
+ )
156
+ self.create_descriptors_table = lambda: self.executescript(
157
+ CREATE_DESCRIPTORS_TABLE
158
+ )
159
+ self.create_images_table = lambda: self.executescript(
160
+ CREATE_IMAGES_TABLE
161
+ )
162
+ self.create_pose_priors_table = lambda: self.executescript(
163
+ CREATE_POSE_PRIORS_TABLE
164
+ )
165
+ self.create_two_view_geometries_table = lambda: self.executescript(
166
+ CREATE_TWO_VIEW_GEOMETRIES_TABLE
167
+ )
168
+ self.create_keypoints_table = lambda: self.executescript(
169
+ CREATE_KEYPOINTS_TABLE
170
+ )
171
+ self.create_matches_table = lambda: self.executescript(
172
+ CREATE_MATCHES_TABLE
173
+ )
174
+ self.create_name_index = lambda: self.executescript(CREATE_NAME_INDEX)
175
+
176
+ def add_camera(
177
+ self,
178
+ model,
179
+ width,
180
+ height,
181
+ params,
182
+ prior_focal_length=False,
183
+ camera_id=None,
184
+ ):
185
+ params = np.asarray(params, np.float64)
186
+ cursor = self.execute(
187
+ "INSERT INTO cameras VALUES (?, ?, ?, ?, ?, ?)",
188
+ (
189
+ camera_id,
190
+ model,
191
+ width,
192
+ height,
193
+ array_to_blob(params),
194
+ prior_focal_length,
195
+ ),
196
+ )
197
+ return cursor.lastrowid
198
+
199
+ def add_image(
200
+ self,
201
+ name,
202
+ camera_id,
203
+ image_id=None,
204
+ ):
205
+ cursor = self.execute(
206
+ "INSERT INTO images VALUES (?, ?, ?)", (image_id, name, camera_id)
207
+ )
208
+ return cursor.lastrowid
209
+
210
+ def add_pose_prior(
211
+ self, image_id, position, coordinate_system=-1, position_covariance=None
212
+ ):
213
+ position = np.asarray(position, dtype=np.float64)
214
+ if position_covariance is None:
215
+ position_covariance = np.full((3, 3), np.nan, dtype=np.float64)
216
+ self.execute(
217
+ "INSERT INTO pose_priors VALUES (?, ?, ?, ?)",
218
+ (
219
+ image_id,
220
+ array_to_blob(position),
221
+ coordinate_system,
222
+ array_to_blob(position_covariance),
223
+ ),
224
+ )
225
+
226
+ def add_keypoints(self, image_id, keypoints):
227
+ assert len(keypoints.shape) == 2
228
+ assert keypoints.shape[1] in [2, 4, 6]
229
+
230
+ keypoints = np.asarray(keypoints, np.float32)
231
+ self.execute(
232
+ "INSERT INTO keypoints VALUES (?, ?, ?, ?)",
233
+ (image_id,) + keypoints.shape + (array_to_blob(keypoints),),
234
+ )
235
+
236
+ def add_descriptors(self, image_id, descriptors):
237
+ descriptors = np.ascontiguousarray(descriptors, np.uint8)
238
+ self.execute(
239
+ "INSERT INTO descriptors VALUES (?, ?, ?, ?)",
240
+ (image_id,) + descriptors.shape + (array_to_blob(descriptors),),
241
+ )
242
+
243
+ def add_matches(self, image_id1, image_id2, matches):
244
+ assert len(matches.shape) == 2
245
+ assert matches.shape[1] == 2
246
+
247
+ if image_id1 > image_id2:
248
+ matches = matches[:, ::-1]
249
+
250
+ pair_id = image_ids_to_pair_id(image_id1, image_id2)
251
+ matches = np.asarray(matches, np.uint32)
252
+ self.execute(
253
+ "INSERT INTO matches VALUES (?, ?, ?, ?)",
254
+ (pair_id,) + matches.shape + (array_to_blob(matches),),
255
+ )
256
+
257
+ def add_two_view_geometry(
258
+ self,
259
+ image_id1,
260
+ image_id2,
261
+ matches,
262
+ F=np.eye(3),
263
+ E=np.eye(3),
264
+ H=np.eye(3),
265
+ qvec=np.array([1.0, 0.0, 0.0, 0.0]),
266
+ tvec=np.zeros(3),
267
+ config=2,
268
+ ):
269
+ assert len(matches.shape) == 2
270
+ assert matches.shape[1] == 2
271
+
272
+ if image_id1 > image_id2:
273
+ matches = matches[:, ::-1]
274
+
275
+ pair_id = image_ids_to_pair_id(image_id1, image_id2)
276
+ matches = np.asarray(matches, np.uint32)
277
+ F = np.asarray(F, dtype=np.float64)
278
+ E = np.asarray(E, dtype=np.float64)
279
+ H = np.asarray(H, dtype=np.float64)
280
+ qvec = np.asarray(qvec, dtype=np.float64)
281
+ tvec = np.asarray(tvec, dtype=np.float64)
282
+ self.execute(
283
+ "INSERT INTO two_view_geometries VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
284
+ (pair_id,)
285
+ + matches.shape
286
+ + (
287
+ array_to_blob(matches),
288
+ config,
289
+ array_to_blob(F),
290
+ array_to_blob(E),
291
+ array_to_blob(H),
292
+ array_to_blob(qvec),
293
+ array_to_blob(tvec),
294
+ ),
295
+ )
296
+
297
+
298
+ def example_usage():
299
+ import argparse
300
+ import os
301
+
302
+ parser = argparse.ArgumentParser()
303
+ parser.add_argument("--database_path", default="database.db")
304
+ args = parser.parse_args()
305
+
306
+ if os.path.exists(args.database_path):
307
+ print("ERROR: database path already exists -- will not modify it.")
308
+ return
309
+
310
+ # Open the database.
311
+
312
+ db = COLMAPDatabase.connect(args.database_path)
313
+
314
+ # For convenience, try creating all the tables upfront.
315
+
316
+ db.create_tables()
317
+
318
+ # Create dummy cameras.
319
+
320
+ model1, width1, height1, params1 = (
321
+ 0,
322
+ 1024,
323
+ 768,
324
+ np.array((1024.0, 512.0, 384.0)),
325
+ )
326
+ model2, width2, height2, params2 = (
327
+ 2,
328
+ 1024,
329
+ 768,
330
+ np.array((1024.0, 512.0, 384.0, 0.1)),
331
+ )
332
+
333
+ camera_id1 = db.add_camera(model1, width1, height1, params1)
334
+ camera_id2 = db.add_camera(model2, width2, height2, params2)
335
+
336
+ # Create dummy images.
337
+
338
+ image_id1 = db.add_image("image1.png", camera_id1)
339
+ image_id2 = db.add_image("image2.png", camera_id1)
340
+ image_id3 = db.add_image("image3.png", camera_id2)
341
+ image_id4 = db.add_image("image4.png", camera_id2)
342
+
343
+ # Create dummy keypoints.
344
+ #
345
+ # Note that COLMAP supports:
346
+ # - 2D keypoints: (x, y)
347
+ # - 4D keypoints: (x, y, theta, scale)
348
+ # - 6D affine keypoints: (x, y, a_11, a_12, a_21, a_22)
349
+
350
+ num_keypoints = 1000
351
+ keypoints1 = np.random.rand(num_keypoints, 2) * (width1, height1)
352
+ keypoints2 = np.random.rand(num_keypoints, 2) * (width1, height1)
353
+ keypoints3 = np.random.rand(num_keypoints, 2) * (width2, height2)
354
+ keypoints4 = np.random.rand(num_keypoints, 2) * (width2, height2)
355
+
356
+ db.add_keypoints(image_id1, keypoints1)
357
+ db.add_keypoints(image_id2, keypoints2)
358
+ db.add_keypoints(image_id3, keypoints3)
359
+ db.add_keypoints(image_id4, keypoints4)
360
+
361
+ # Create dummy matches.
362
+
363
+ M = 50
364
+ matches12 = np.random.randint(num_keypoints, size=(M, 2))
365
+ matches23 = np.random.randint(num_keypoints, size=(M, 2))
366
+ matches34 = np.random.randint(num_keypoints, size=(M, 2))
367
+
368
+ db.add_matches(image_id1, image_id2, matches12)
369
+ db.add_matches(image_id2, image_id3, matches23)
370
+ db.add_matches(image_id3, image_id4, matches34)
371
+
372
+ # Create dummy pose_priors.
373
+
374
+ pos1 = np.random.rand(3, 1) * np.random.randint(10)
375
+ pos2 = np.random.rand(3, 1) * np.random.randint(10)
376
+ pos3 = np.random.rand(3, 1) * np.random.randint(10)
377
+
378
+ cov3 = np.random.rand(3, 3) * np.random.randint(10)
379
+
380
+ pose_prior1 = [image_id1, pos1, 1, None]
381
+ pose_prior2 = [image_id2, pos2, -1, None]
382
+ pose_prior3 = [image_id3, pos3, 0, cov3]
383
+
384
+ db.add_pose_prior(*pose_prior1)
385
+ db.add_pose_prior(*pose_prior2)
386
+ db.add_pose_prior(*pose_prior3)
387
+
388
+ # Convert unset covariance to nan matrix for later check
389
+ pose_prior1[3] = np.full((3, 3), np.nan, dtype=np.float64)
390
+ pose_prior2[3] = np.full((3, 3), np.nan, dtype=np.float64)
391
+
392
+ # Commit the data to the file.
393
+
394
+ db.commit()
395
+
396
+ # Read and check cameras.
397
+
398
+ rows = db.execute("SELECT * FROM cameras")
399
+
400
+ camera_id, model, width, height, params, prior = next(rows)
401
+ params = blob_to_array(params, np.float64)
402
+ assert camera_id == camera_id1
403
+ assert model == model1 and width == width1 and height == height1
404
+ assert np.allclose(params, params1)
405
+
406
+ camera_id, model, width, height, params, prior = next(rows)
407
+ params = blob_to_array(params, np.float64)
408
+ assert camera_id == camera_id2
409
+ assert model == model2 and width == width2 and height == height2
410
+ assert np.allclose(params, params2)
411
+
412
+ # Read and check keypoints.
413
+
414
+ keypoints = dict(
415
+ (image_id, blob_to_array(data, np.float32, (-1, 2)))
416
+ for image_id, data in db.execute("SELECT image_id, data FROM keypoints")
417
+ )
418
+
419
+ assert np.allclose(keypoints[image_id1], keypoints1)
420
+ assert np.allclose(keypoints[image_id2], keypoints2)
421
+ assert np.allclose(keypoints[image_id3], keypoints3)
422
+ assert np.allclose(keypoints[image_id4], keypoints4)
423
+
424
+ # Read and check matches.
425
+
426
+ matches = dict(
427
+ (pair_id_to_image_ids(pair_id), blob_to_array(data, np.uint32, (-1, 2)))
428
+ for pair_id, data in db.execute("SELECT pair_id, data FROM matches")
429
+ )
430
+
431
+ assert np.all(matches[(image_id1, image_id2)] == matches12)
432
+ assert np.all(matches[(image_id2, image_id3)] == matches23)
433
+ assert np.all(matches[(image_id3, image_id4)] == matches34)
434
+
435
+ # Read and check pose_priors
436
+
437
+ rows = db.execute("SELECT * FROM pose_priors")
438
+
439
+ img_id1, pos1, coord_sys1, cov1 = next(rows)
440
+ img_id2, pos2, coord_sys2, cov2 = next(rows)
441
+ img_id3, pos3, coord_sys3, cov3 = next(rows)
442
+
443
+ assert pose_prior1[0] == img_id1
444
+ assert pose_prior2[0] == img_id2
445
+ assert pose_prior3[0] == img_id3
446
+
447
+ assert pose_prior1[1].all() == blob_to_array(pos1, np.float64, (3, 1)).all()
448
+ assert pose_prior2[1].all() == blob_to_array(pos2, np.float64, (3, 1)).all()
449
+ assert pose_prior3[1].all() == blob_to_array(pos3, np.float64, (3, 1)).all()
450
+
451
+ assert pose_prior1[2] == coord_sys1
452
+ assert pose_prior2[2] == coord_sys2
453
+ assert pose_prior3[2] == coord_sys3
454
+
455
+ assert pose_prior1[3].all() == blob_to_array(cov1, np.float64, (3, 3)).all()
456
+ assert pose_prior2[3].all() == blob_to_array(cov2, np.float64, (3, 3)).all()
457
+ assert pose_prior3[3].all() == blob_to_array(cov3, np.float64, (3, 3)).all()
458
+
459
+ # Clean up.
460
+
461
+ db.close()
462
+
463
+ if os.path.exists(args.database_path):
464
+ os.remove(args.database_path)
465
+
466
+
467
+ if __name__ == "__main__":
468
+ example_usage()
scripts/python/export_inlier_matches.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ # This script exports inlier matches from a COLMAP database to a text file.
32
+
33
+ import argparse
34
+ import os
35
+ import sqlite3
36
+
37
+ import numpy as np
38
+
39
+
40
+ def parse_args():
41
+ parser = argparse.ArgumentParser()
42
+ parser.add_argument("--database_path", required=True)
43
+ parser.add_argument("--output_path", required=True)
44
+ parser.add_argument("--min_num_matches", type=int, default=15)
45
+ args = parser.parse_args()
46
+ return args
47
+
48
+
49
+ def pair_id_to_image_ids(pair_id):
50
+ image_id2 = pair_id % 2147483647
51
+ image_id1 = (pair_id - image_id2) / 2147483647
52
+ return image_id1, image_id2
53
+
54
+
55
+ def main():
56
+ args = parse_args()
57
+
58
+ connection = sqlite3.connect(args.database_path)
59
+ cursor = connection.cursor()
60
+
61
+ images = {}
62
+ cursor.execute("SELECT image_id, camera_id, name FROM images;")
63
+ for row in cursor:
64
+ image_id = row[0]
65
+ image_name = row[2]
66
+ images[image_id] = image_name
67
+
68
+ with open(os.path.join(args.output_path), "w") as fid:
69
+ cursor.execute(
70
+ "SELECT pair_id, data FROM two_view_geometries WHERE rows>=?;",
71
+ (args.min_num_matches,),
72
+ )
73
+ for row in cursor:
74
+ pair_id = row[0]
75
+ inlier_matches = np.fromstring(row[1], dtype=np.uint32).reshape(
76
+ -1, 2
77
+ )
78
+ image_id1, image_id2 = pair_id_to_image_ids(pair_id)
79
+ image_name1 = images[image_id1]
80
+ image_name2 = images[image_id2]
81
+ fid.write(
82
+ "%s %s %d\n"
83
+ % (image_name1, image_name2, inlier_matches.shape[0])
84
+ )
85
+ for i in range(inlier_matches.shape[0]):
86
+ fid.write("%d %d\n" % tuple(inlier_matches[i]))
87
+
88
+ cursor.close()
89
+ connection.close()
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
scripts/python/export_inlier_pairs.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ # This script exports inlier image pairs from a COLMAP database to a text file.
32
+
33
+ import argparse
34
+ import sqlite3
35
+
36
+
37
+ def parse_args():
38
+ parser = argparse.ArgumentParser()
39
+ parser.add_argument("--database_path", required=True)
40
+ parser.add_argument("--match_list_path", required=True)
41
+ parser.add_argument("--min_num_matches", type=int, default=15)
42
+ args = parser.parse_args()
43
+ return args
44
+
45
+
46
+ def pair_id_to_image_ids(pair_id):
47
+ image_id2 = pair_id % 2147483647
48
+ image_id1 = (pair_id - image_id2) / 2147483647
49
+ return image_id1, image_id2
50
+
51
+
52
+ def main():
53
+ args = parse_args()
54
+
55
+ connection = sqlite3.connect(args.database_path)
56
+ cursor = connection.cursor()
57
+
58
+ # Get a mapping between image ids and image names
59
+ image_id_to_name = dict()
60
+ cursor.execute("SELECT image_id, name FROM images;")
61
+ for row in cursor:
62
+ image_id = row[0]
63
+ name = row[1]
64
+ image_id_to_name[image_id] = name
65
+
66
+ # Iterate over entries in the two_view_geometries table
67
+ output = open(args.match_list_path, "w")
68
+ cursor.execute("SELECT pair_id, rows FROM two_view_geometries;")
69
+ for row in cursor:
70
+ pair_id = row[0]
71
+ rows = row[1]
72
+
73
+ if rows < args.min_num_matches:
74
+ continue
75
+
76
+ image_id1, image_id2 = pair_id_to_image_ids(pair_id)
77
+ image_name1 = image_id_to_name[image_id1]
78
+ image_name2 = image_id_to_name[image_id2]
79
+
80
+ output.write("%s %s\n" % (image_name1, image_name2))
81
+
82
+ output.close()
83
+ cursor.close()
84
+ connection.close()
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()
scripts/python/export_to_bundler.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ # This script exports a COLMAP database to the file structure to run Bundler.
32
+
33
+ import argparse
34
+ import gzip
35
+ import os
36
+ import shutil
37
+ import sqlite3
38
+
39
+ import numpy as np
40
+
41
+
42
+ def parse_args():
43
+ parser = argparse.ArgumentParser()
44
+ parser.add_argument("--database_path", required=True)
45
+ parser.add_argument("--image_path", required=True)
46
+ parser.add_argument("--output_path", required=True)
47
+ parser.add_argument("--min_num_matches", type=int, default=15)
48
+ args = parser.parse_args()
49
+ return args
50
+
51
+
52
+ def pair_id_to_image_ids(pair_id):
53
+ image_id2 = pair_id % 2147483647
54
+ image_id1 = (pair_id - image_id2) / 2147483647
55
+ return image_id1, image_id2
56
+
57
+
58
+ def main():
59
+ args = parse_args()
60
+
61
+ connection = sqlite3.connect(args.database_path)
62
+ cursor = connection.cursor()
63
+
64
+ try:
65
+ os.makedirs(args.output_path)
66
+ except: # noqa E722
67
+ pass
68
+
69
+ cameras = {}
70
+ cursor.execute("SELECT camera_id, params FROM cameras;")
71
+ for row in cursor:
72
+ camera_id = row[0]
73
+ params = np.fromstring(row[1], dtype=np.double)
74
+ cameras[camera_id] = params
75
+
76
+ images = {}
77
+ with open(os.path.join(args.output_path, "list.txt"), "w") as fid:
78
+ cursor.execute("SELECT image_id, camera_id, name FROM images;")
79
+ for row in cursor:
80
+ image_id = row[0]
81
+ camera_id = row[1]
82
+ image_name = row[2]
83
+ print("Copying image", image_name)
84
+ images[image_id] = (len(images), image_name)
85
+ fid.write("./%s 0 %f\n" % (image_name, cameras[camera_id][0]))
86
+ if not os.path.exists(os.path.join(args.output_path, image_name)):
87
+ shutil.copyfile(
88
+ os.path.join(args.image_path, image_name),
89
+ os.path.join(args.output_path, image_name),
90
+ )
91
+
92
+ for image_id, (image_idx, image_name) in images.iteritems():
93
+ print("Exporting key file for", image_name)
94
+ base_name, ext = os.path.splitext(image_name)
95
+ key_file_name = os.path.join(args.output_path, base_name + ".key")
96
+ key_file_name_gz = key_file_name + ".gz"
97
+ if os.path.exists(key_file_name_gz):
98
+ continue
99
+
100
+ cursor.execute(
101
+ "SELECT data FROM keypoints WHERE image_id=?;", (image_id,)
102
+ )
103
+ row = next(cursor)
104
+ if row[0] is None:
105
+ keypoints = np.zeros((0, 6), dtype=np.float32)
106
+ descriptors = np.zeros((0, 128), dtype=np.uint8)
107
+ else:
108
+ keypoints = np.fromstring(row[0], dtype=np.float32).reshape(-1, 6)
109
+ cursor.execute(
110
+ "SELECT data FROM descriptors WHERE image_id=?;", (image_id,)
111
+ )
112
+ row = next(cursor)
113
+ descriptors = np.fromstring(row[0], dtype=np.uint8).reshape(-1, 128)
114
+
115
+ with open(key_file_name, "w") as fid:
116
+ fid.write("%d %d\n" % (keypoints.shape[0], descriptors.shape[1]))
117
+ for r in range(keypoints.shape[0]):
118
+ fid.write(
119
+ "%f %f %f %f\n"
120
+ % (
121
+ keypoints[r, 1],
122
+ keypoints[r, 0],
123
+ keypoints[r, 2],
124
+ keypoints[r, 3],
125
+ )
126
+ )
127
+ for i in range(0, 128, 20):
128
+ desc_block = descriptors[r, i : i + 20]
129
+ fid.write(" ".join(map(str, desc_block.ravel().tolist())))
130
+ fid.write("\n")
131
+
132
+ with open(key_file_name, "rb") as fid_in:
133
+ with gzip.open(key_file_name + ".gz", "wb") as fid_out:
134
+ fid_out.writelines(fid_in)
135
+
136
+ os.remove(key_file_name)
137
+
138
+ with open(os.path.join(args.output_path, "matches.init.txt"), "w") as fid:
139
+ cursor.execute(
140
+ "SELECT pair_id, data FROM two_view_geometries WHERE rows>=?;",
141
+ (args.min_num_matches,),
142
+ )
143
+ for row in cursor:
144
+ pair_id = row[0]
145
+ inlier_matches = np.fromstring(row[1], dtype=np.uint32).reshape(
146
+ -1, 2
147
+ )
148
+ image_id1, image_id2 = pair_id_to_image_ids(pair_id)
149
+ image_idx1 = images[image_id1][0]
150
+ image_idx2 = images[image_id2][0]
151
+ fid.write(
152
+ "%d %d\n%d\n"
153
+ % (image_idx1, image_idx2, inlier_matches.shape[0])
154
+ )
155
+ for i in range(inlier_matches.shape[0]):
156
+ fid.write(
157
+ "%d %d\n" % (inlier_matches[i, 0], inlier_matches[i, 1])
158
+ )
159
+
160
+ with open(os.path.join(args.output_path, "run_bundler.sh"), "w") as fid:
161
+ fid.write("bin/Bundler list.txt \\\n")
162
+ fid.write("--run_bundle \\\n")
163
+ fid.write("--use_focal_estimate \\\n")
164
+ fid.write("--output_all bundle_ \\\n")
165
+ fid.write("--constrain_focal \\\n")
166
+ fid.write("--estimate_distortion \\\n")
167
+ fid.write("--match_table matches.init.txt \\\n")
168
+ fid.write("--variable_focal_length \\\n")
169
+ fid.write("--output_dir bundle \\\n")
170
+ fid.write("--output bundle.out \\\n")
171
+ fid.write("--constrain_focal_weight 0.0001 \\\n")
172
+
173
+ cursor.close()
174
+ connection.close()
175
+
176
+
177
+ if __name__ == "__main__":
178
+ main()
scripts/python/export_to_visualsfm.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ # This script exports a COLMAP database to the file structure to run VisualSfM.
32
+
33
+ import argparse
34
+ import os
35
+ import shutil
36
+ import sqlite3
37
+ import struct
38
+
39
+ import numpy as np
40
+
41
+
42
+ def parse_args():
43
+ parser = argparse.ArgumentParser()
44
+ parser.add_argument("--database_path", required=True)
45
+ parser.add_argument("--image_path", required=True)
46
+ parser.add_argument("--output_path", required=True)
47
+ parser.add_argument("--min_num_matches", type=int, default=15)
48
+ parser.add_argument("--binary_feature_files", type=bool, default=True)
49
+ args = parser.parse_args()
50
+ return args
51
+
52
+
53
+ def pair_id_to_image_ids(pair_id):
54
+ image_id2 = pair_id % 2147483647
55
+ image_id1 = (pair_id - image_id2) / 2147483647
56
+ return image_id1, image_id2
57
+
58
+
59
+ def main():
60
+ args = parse_args()
61
+
62
+ connection = sqlite3.connect(args.database_path)
63
+ cursor = connection.cursor()
64
+
65
+ try:
66
+ os.makedirs(args.output_path)
67
+ except: # noqa E722
68
+ pass
69
+
70
+ cameras = {}
71
+ cursor.execute("SELECT camera_id, params FROM cameras;")
72
+ for row in cursor:
73
+ camera_id = row[0]
74
+ params = np.fromstring(row[1], dtype=np.double)
75
+ cameras[camera_id] = params
76
+
77
+ images = {}
78
+ cursor.execute("SELECT image_id, camera_id, name FROM images;")
79
+ for row in cursor:
80
+ image_id = row[0]
81
+ camera_id = row[1]
82
+ image_name = row[2]
83
+ print("Copying image", image_name)
84
+ images[image_id] = (len(images), image_name)
85
+ if not os.path.exists(os.path.join(args.output_path, image_name)):
86
+ shutil.copyfile(
87
+ os.path.join(args.image_path, image_name),
88
+ os.path.join(args.output_path, image_name),
89
+ )
90
+
91
+ # The magic numbers used in VisualSfM's binary file format for storing the
92
+ # feature descriptors.
93
+ sift_name = 1413892435
94
+ sift_version_v4 = 808334422
95
+ sift_eof_marker = 1179600383
96
+
97
+ for image_id, (image_idx, image_name) in images.iteritems():
98
+ print("Exporting key file for", image_name)
99
+ base_name, ext = os.path.splitext(image_name)
100
+ key_file_name = os.path.join(args.output_path, base_name + ".sift")
101
+ if os.path.exists(key_file_name):
102
+ continue
103
+
104
+ cursor.execute(
105
+ "SELECT data FROM keypoints WHERE image_id=?;", (image_id,)
106
+ )
107
+ row = next(cursor)
108
+ if row[0] is None:
109
+ keypoints = np.zeros((0, 6), dtype=np.float32)
110
+ descriptors = np.zeros((0, 128), dtype=np.uint8)
111
+ else:
112
+ keypoints = np.fromstring(row[0], dtype=np.float32).reshape(-1, 6)
113
+ cursor.execute(
114
+ "SELECT data FROM descriptors WHERE image_id=?;", (image_id,)
115
+ )
116
+ row = next(cursor)
117
+ descriptors = np.fromstring(row[0], dtype=np.uint8).reshape(-1, 128)
118
+
119
+ if args.binary_feature_files:
120
+ with open(key_file_name, "wb") as fid:
121
+ fid.write(struct.pack("i", sift_name))
122
+ fid.write(struct.pack("i", sift_version_v4))
123
+ fid.write(struct.pack("i", keypoints.shape[0]))
124
+ fid.write(struct.pack("i", 4))
125
+ fid.write(struct.pack("i", 128))
126
+ keypoints[:, :4].astype(np.float32).tofile(fid)
127
+ descriptors.astype(np.uint8).tofile(fid)
128
+ fid.write(struct.pack("i", sift_eof_marker))
129
+ else:
130
+ with open(key_file_name, "w") as fid:
131
+ fid.write(
132
+ "%d %d\n" % (keypoints.shape[0], descriptors.shape[1])
133
+ )
134
+ for r in range(keypoints.shape[0]):
135
+ fid.write("%f %f 0 0 " % (keypoints[r, 0], keypoints[r, 1]))
136
+ fid.write(
137
+ " ".join(map(str, descriptors[r].ravel().tolist()))
138
+ )
139
+ fid.write("\n")
140
+
141
+ with open(os.path.join(args.output_path, "matches.txt"), "w") as fid:
142
+ cursor.execute(
143
+ "SELECT pair_id, data FROM two_view_geometries WHERE rows>=?;",
144
+ (args.min_num_matches,),
145
+ )
146
+ for row in cursor:
147
+ pair_id = row[0]
148
+ inlier_matches = np.fromstring(row[1], dtype=np.uint32).reshape(
149
+ -1, 2
150
+ )
151
+ image_id1, image_id2 = pair_id_to_image_ids(pair_id)
152
+ image_name1 = images[image_id1][1]
153
+ image_name2 = images[image_id2][1]
154
+ fid.write(
155
+ "%s %s %d\n"
156
+ % (image_name1, image_name2, inlier_matches.shape[0])
157
+ )
158
+ line1 = ""
159
+ line2 = ""
160
+ for i in range(inlier_matches.shape[0]):
161
+ line1 += "%d " % inlier_matches[i, 0]
162
+ line2 += "%d " % inlier_matches[i, 1]
163
+ fid.write(line1 + "\n")
164
+ fid.write(line2 + "\n")
165
+
166
+ cursor.close()
167
+ connection.close()
168
+
169
+
170
+ if __name__ == "__main__":
171
+ main()
scripts/python/flickr_downloader.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ import argparse
32
+ import datetime
33
+ import multiprocessing
34
+ import os
35
+ import socket
36
+ import time
37
+ import urllib
38
+ import xml.etree.ElementTree as ElementTree
39
+
40
+ import urllib2
41
+ import urlparse
42
+
43
+ PER_PAGE = 500
44
+ SORT = "date-posted-desc"
45
+ URL = (
46
+ "https://api.flickr.com/services/rest/?method=flickr.photos.search&"
47
+ "api_key=%s&text=%s&sort=%s&per_page=%d&page=%d&min_upload_date=%s&"
48
+ "max_upload_date=%s&format=rest&extras=url_o,url_l,url_c,url_z,url_n"
49
+ )
50
+ MAX_PAGE_REQUESTS = 5
51
+ MAX_PAGE_TIMEOUT = 20
52
+ MAX_IMAGE_REQUESTS = 3
53
+ TIME_SKIP = 24 * 60 * 60
54
+ MAX_DATE = time.time()
55
+ MIN_DATE = MAX_DATE - TIME_SKIP
56
+
57
+
58
+ def parse_args():
59
+ parser = argparse.ArgumentParser()
60
+ parser.add_argument("--search_text", required=True)
61
+ parser.add_argument("--api_key", required=True)
62
+ parser.add_argument("--image_path", required=True)
63
+ parser.add_argument("--num_procs", type=int, default=10)
64
+ parser.add_argument("--max_days_without_image", type=int, default=365)
65
+ args = parser.parse_args()
66
+ return args
67
+
68
+
69
+ def compose_url(page, api_key, text, min_date, max_date):
70
+ return URL % (
71
+ api_key,
72
+ text,
73
+ SORT,
74
+ PER_PAGE,
75
+ page,
76
+ str(min_date),
77
+ str(max_date),
78
+ )
79
+
80
+
81
+ def parse_page(page, api_key, text, min_date, max_date):
82
+ f = None
83
+ for _ in range(MAX_PAGE_REQUESTS):
84
+ try:
85
+ f = urllib2.urlopen(
86
+ compose_url(page, api_key, text, min_date, max_date),
87
+ timeout=MAX_PAGE_TIMEOUT,
88
+ )
89
+ except socket.timeout:
90
+ continue
91
+ else:
92
+ break
93
+
94
+ if f is None:
95
+ return {
96
+ "pages": "0",
97
+ "total": "0",
98
+ "page": "0",
99
+ "perpage": "0",
100
+ }, tuple()
101
+
102
+ response = f.read()
103
+ root = ElementTree.fromstring(response)
104
+
105
+ if root.attrib["stat"] != "ok":
106
+ raise IOError
107
+
108
+ photos = []
109
+ for photo in root.iter("photo"):
110
+ photos.append(photo.attrib)
111
+
112
+ return root.find("photos").attrib, photos
113
+
114
+
115
+ class PhotoDownloader(object):
116
+ def __init__(self, image_path):
117
+ self.image_path = image_path
118
+
119
+ def __call__(self, photo):
120
+ # Find the URL corresponding to the highest image resolution. We will
121
+ # need this URL here to determine the image extension (typically .jpg,
122
+ # but could be .png, .gif, etc).
123
+ url = None
124
+ for url_suffix in ("o", "l", "k", "h", "b", "c", "z"):
125
+ url_attr = "url_%s" % url_suffix
126
+ if photo.get(url_attr) is not None:
127
+ url = photo.get(url_attr)
128
+ break
129
+
130
+ if url is not None:
131
+ # Note that the following statement may fail in Python 3. urlparse
132
+ # may need to be replaced with urllib.parse.
133
+ url_filename = urlparse.urlparse(url).path
134
+ image_ext = os.path.splitext(url_filename)[1]
135
+
136
+ image_name = "%s_%s%s" % (photo["id"], photo["secret"], image_ext)
137
+ path = os.path.join(self.image_path, image_name)
138
+ if not os.path.exists(path):
139
+ print(url)
140
+ for _ in range(MAX_IMAGE_REQUESTS):
141
+ try:
142
+ urllib.urlretrieve(url, path)
143
+ except urllib.ContentTooShortError:
144
+ continue
145
+ else:
146
+ break
147
+
148
+
149
+ def main():
150
+ args = parse_args()
151
+
152
+ downloader = PhotoDownloader(args.image_path)
153
+ pool = multiprocessing.Pool(processes=args.num_procs)
154
+
155
+ num_pages = float("inf")
156
+ page = 0
157
+
158
+ min_date = MIN_DATE
159
+ max_date = MAX_DATE
160
+
161
+ days_in_row = 0
162
+
163
+ search_text = args.search_text.replace(" ", "-")
164
+
165
+ while num_pages > page:
166
+ page += 1
167
+
168
+ metadata, photos = parse_page(
169
+ page, args.api_key, search_text, min_date, max_date
170
+ )
171
+
172
+ num_pages = int(metadata["pages"])
173
+
174
+ print(78 * "=")
175
+ print("Page:\t\t", page, "of", num_pages)
176
+ print("Min-Date:\t", datetime.datetime.fromtimestamp(min_date))
177
+ print("Max-Date:\t", datetime.datetime.fromtimestamp(max_date))
178
+ print("Num-Photos:\t", len(photos))
179
+ print(78 * "=")
180
+
181
+ try:
182
+ pool.map_async(downloader, photos).get(1e10)
183
+ except KeyboardInterrupt:
184
+ pool.wait()
185
+ break
186
+
187
+ if page >= num_pages:
188
+ max_date -= TIME_SKIP
189
+ min_date -= TIME_SKIP
190
+ page = 0
191
+
192
+ if num_pages == 0:
193
+ days_in_row = days_in_row + 1
194
+ num_pages = float("inf")
195
+
196
+ print(" No images in", days_in_row, "days in a row")
197
+
198
+ if days_in_row == args.max_days_without_image:
199
+ break
200
+ else:
201
+ days_in_row = 0
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()
scripts/python/merge_ply_files.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ # This script merges multiple homogeneous PLY files into a single PLY file.
32
+
33
+ import argparse
34
+ import os
35
+
36
+ import numpy as np
37
+ import plyfile
38
+
39
+
40
+ def parse_args():
41
+ parser = argparse.ArgumentParser()
42
+ parser.add_argument("--folder_path", required=True)
43
+ parser.add_argument("--merged_path", required=True)
44
+ args = parser.parse_args()
45
+ return args
46
+
47
+
48
+ def main():
49
+ args = parse_args()
50
+
51
+ files = []
52
+ for file_name in os.listdir(args.folder_path):
53
+ if len(file_name) < 4 or file_name[-4:].lower() != ".ply":
54
+ continue
55
+
56
+ print("Reading file", file_name)
57
+ file = plyfile.PlyData.read(os.path.join(args.folder_path, file_name))
58
+ for element in file.elements:
59
+ files.append(element.data)
60
+
61
+ print("Merging files")
62
+ merged_file = np.concatenate(files, -1)
63
+ merged_el = plyfile.PlyElement.describe(merged_file, "vertex")
64
+
65
+ print("Writing merged file")
66
+ plyfile.PlyData([merged_el]).write(args.merged_path)
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()
scripts/python/migrate_database_pose_prior.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ import argparse
32
+
33
+ import numpy as np
34
+ from database import COLMAPDatabase
35
+
36
+ if __name__ == "__main__":
37
+ parser = argparse.ArgumentParser()
38
+ parser.add_argument("--database_path", type=str, required=True)
39
+ parser.add_argument("--is_cartesian", action="store_true")
40
+ parser.add_argument("--cleanup", action="store_true")
41
+ args = parser.parse_args()
42
+
43
+ db = COLMAPDatabase.connect(args.database_path)
44
+
45
+ pose_priors = {}
46
+ rows = db.execute("SELECT * FROM images")
47
+ for image_id, _, _, *cam_from_world_prior in rows:
48
+ if not cam_from_world_prior: # newer format database
49
+ continue
50
+ qvec = np.array(cam_from_world_prior[:4], dtype=float)
51
+ tvec = np.array(cam_from_world_prior[4:], dtype=float)
52
+ if np.isfinite(qvec).any():
53
+ print(
54
+ f"Warning: rotation prior for image {image_id} "
55
+ "will be lost during migration."
56
+ )
57
+ if np.isfinite(tvec).any():
58
+ pose_priors[image_id] = tvec
59
+ print(f"Found location priors for {len(pose_priors)} images.")
60
+
61
+ coordinate_systems = {"UNKNOWN": -1, "WGS84": 0, "CARTESIAN": 1}
62
+ coordinate_system = coordinate_systems[
63
+ "CARTESIAN" if args.is_cartesian else "WGS84"
64
+ ]
65
+ db.create_pose_priors_table()
66
+ for image_id, position in pose_priors.items():
67
+ (exists,) = db.execute(
68
+ "SELECT COUNT(*) FROM pose_priors WHERE image_id = ?",
69
+ (image_id,),
70
+ ).fetchone()
71
+ if exists:
72
+ print(f"Location prior for {image_id} already exists, skipping.")
73
+ continue
74
+ db.add_pose_prior(image_id, position, coordinate_system)
75
+
76
+ if args.cleanup:
77
+ for col in ["qw", "qx", "qy", "qz", "tx", "ty", "tz"]:
78
+ db.execute(f"ALTER TABLE images DROP COLUMN prior_{col}")
79
+
80
+ db.commit()
scripts/python/nvm_to_ply.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ # This script converts a VisualSfM reconstruction file to a PLY point cloud.
32
+
33
+ import argparse
34
+
35
+ import numpy as np
36
+
37
+
38
+ def parse_args():
39
+ parser = argparse.ArgumentParser()
40
+ parser.add_argument("--nvm_path", required=True)
41
+ parser.add_argument("--ply_path", required=True)
42
+ parser.add_argument("--normalize", type=bool, default=True)
43
+ parser.add_argument("--normalize_p0", type=float, default=0.2)
44
+ parser.add_argument("--normalize_p1", type=float, default=0.8)
45
+ parser.add_argument("--min_track_length", type=int, default=3)
46
+ args = parser.parse_args()
47
+ return args
48
+
49
+
50
+ def main():
51
+ args = parse_args()
52
+
53
+ with open(args.nvm_path, "r") as fid:
54
+ fid.readline()
55
+ fid.readline()
56
+ num_images = int(fid.readline())
57
+
58
+ for i in range(num_images + 1):
59
+ fid.readline()
60
+
61
+ num_points = int(fid.readline())
62
+
63
+ xyz = np.zeros((num_points, 3), dtype=np.float64)
64
+ rgb = np.zeros((num_points, 3), dtype=np.uint16)
65
+ track_lengths = np.zeros((num_points,), dtype=np.uint32)
66
+
67
+ for i in range(num_points):
68
+ if i % 1000 == 0:
69
+ print("Reading point", i, "/", num_points)
70
+ elems = fid.readline().split()
71
+ xyz[i] = map(float, elems[0:3])
72
+ rgb[i] = map(int, elems[3:6])
73
+ track_lengths[i] = int(elems[6])
74
+
75
+ mask = track_lengths >= args.min_track_length
76
+ xyz = xyz[mask]
77
+ rgb = rgb[mask]
78
+
79
+ if args.normalize:
80
+ sorted_x = np.sort(xyz[:, 0])
81
+ sorted_y = np.sort(xyz[:, 1])
82
+ sorted_z = np.sort(xyz[:, 2])
83
+
84
+ num_coords = sorted_x.size
85
+ min_coord = int(args.normalize_p0 * num_coords)
86
+ max_coord = int(args.normalize_p1 * num_coords)
87
+ mean_coords = xyz.mean(0)
88
+
89
+ bbox_min = np.array(
90
+ [sorted_x[min_coord], sorted_y[min_coord], sorted_z[min_coord]]
91
+ )
92
+ bbox_max = np.array(
93
+ [sorted_x[max_coord], sorted_y[max_coord], sorted_z[max_coord]]
94
+ )
95
+
96
+ extent = np.linalg.norm(bbox_max - bbox_min)
97
+ scale = 10.0 / extent
98
+
99
+ xyz -= mean_coords
100
+ xyz *= scale
101
+
102
+ with open(args.ply_path, "w") as fid:
103
+ fid.write("ply\n")
104
+ fid.write("format ascii 1.0\n")
105
+ fid.write("element vertex %d\n" % xyz.shape[0])
106
+ fid.write("property float x\n")
107
+ fid.write("property float y\n")
108
+ fid.write("property float z\n")
109
+ fid.write("property float nx\n")
110
+ fid.write("property float ny\n")
111
+ fid.write("property float nz\n")
112
+ fid.write("property uchar diffuse_red\n")
113
+ fid.write("property uchar diffuse_green\n")
114
+ fid.write("property uchar diffuse_blue\n")
115
+ fid.write("end_header\n")
116
+ for i in range(xyz.shape[0]):
117
+ if i % 1000 == 0:
118
+ print("Writing point", i, "/", xyz.shape[0])
119
+ fid.write(
120
+ "%f %f %f 0 0 0 %d %d %d\n"
121
+ % (
122
+ xyz[i, 0],
123
+ xyz[i, 1],
124
+ xyz[i, 2],
125
+ rgb[i, 0],
126
+ rgb[i, 1],
127
+ rgb[i, 2],
128
+ )
129
+ )
130
+
131
+
132
+ if __name__ == "__main__":
133
+ main()
scripts/python/read_write_dense.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+
3
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ #
12
+ # * Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ #
16
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
17
+ # its contributors may be used to endorse or promote products derived
18
+ # from this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30
+ # POSSIBILITY OF SUCH DAMAGE.
31
+
32
+
33
+ import argparse
34
+ import os
35
+ import struct
36
+
37
+ import numpy as np
38
+
39
+
40
+ def read_array(path):
41
+ with open(path, "rb") as fid:
42
+ width, height, channels = np.genfromtxt(
43
+ fid, delimiter="&", max_rows=1, usecols=(0, 1, 2), dtype=int
44
+ )
45
+ fid.seek(0)
46
+ num_delimiter = 0
47
+ byte = fid.read(1)
48
+ while True:
49
+ if byte == b"&":
50
+ num_delimiter += 1
51
+ if num_delimiter >= 3:
52
+ break
53
+ byte = fid.read(1)
54
+ array = np.fromfile(fid, np.float32)
55
+ array = array.reshape((width, height, channels), order="F")
56
+ return np.transpose(array, (1, 0, 2)).squeeze()
57
+
58
+
59
+ def write_array(array, path):
60
+ """
61
+ see: src/mvs/mat.h
62
+ void Mat<T>::Write(const std::string& path)
63
+ """
64
+ assert array.dtype == np.float32
65
+ if len(array.shape) == 2:
66
+ height, width = array.shape
67
+ channels = 1
68
+ elif len(array.shape) == 3:
69
+ height, width, channels = array.shape
70
+ else:
71
+ assert False
72
+
73
+ with open(path, "w") as fid:
74
+ fid.write(str(width) + "&" + str(height) + "&" + str(channels) + "&")
75
+
76
+ with open(path, "ab") as fid:
77
+ if len(array.shape) == 2:
78
+ array_trans = np.transpose(array, (1, 0))
79
+ elif len(array.shape) == 3:
80
+ array_trans = np.transpose(array, (1, 0, 2))
81
+ else:
82
+ assert False
83
+ data_1d = array_trans.reshape(-1, order="F")
84
+ data_list = data_1d.tolist()
85
+ endian_character = "<"
86
+ format_char_sequence = "".join(["f"] * len(data_list))
87
+ byte_data = struct.pack(
88
+ endian_character + format_char_sequence, *data_list
89
+ )
90
+ fid.write(byte_data)
91
+
92
+
93
+ def parse_args():
94
+ parser = argparse.ArgumentParser()
95
+ parser.add_argument(
96
+ "-d", "--depth_map", help="path to depth map", type=str, required=True
97
+ )
98
+ parser.add_argument(
99
+ "-n", "--normal_map", help="path to normal map", type=str, required=True
100
+ )
101
+ parser.add_argument(
102
+ "--min_depth_percentile",
103
+ help="minimum visualization depth percentile",
104
+ type=float,
105
+ default=5,
106
+ )
107
+ parser.add_argument(
108
+ "--max_depth_percentile",
109
+ help="maximum visualization depth percentile",
110
+ type=float,
111
+ default=95,
112
+ )
113
+ args = parser.parse_args()
114
+ return args
115
+
116
+
117
+ def main():
118
+ args = parse_args()
119
+
120
+ if args.min_depth_percentile > args.max_depth_percentile:
121
+ raise ValueError(
122
+ "min_depth_percentile should be less than or equal "
123
+ "to the max_depth_percentile."
124
+ )
125
+
126
+ # Read depth and normal maps corresponding to the same image.
127
+ if not os.path.exists(args.depth_map):
128
+ raise FileNotFoundError("File not found: {}".format(args.depth_map))
129
+
130
+ if not os.path.exists(args.normal_map):
131
+ raise FileNotFoundError("File not found: {}".format(args.normal_map))
132
+
133
+ depth_map = read_array(args.depth_map)
134
+ normal_map = read_array(args.normal_map)
135
+
136
+ min_depth, max_depth = np.percentile(
137
+ depth_map, [args.min_depth_percentile, args.max_depth_percentile]
138
+ )
139
+ depth_map[depth_map < min_depth] = min_depth
140
+ depth_map[depth_map > max_depth] = max_depth
141
+
142
+ import pylab as plt
143
+
144
+ # Visualize the depth map.
145
+ plt.figure()
146
+ plt.imshow(depth_map)
147
+ plt.title("depth map")
148
+
149
+ # Visualize the normal map.
150
+ plt.figure()
151
+ plt.imshow(normal_map)
152
+ plt.title("normal map")
153
+
154
+ plt.show()
155
+
156
+
157
+ if __name__ == "__main__":
158
+ main()
scripts/python/read_write_fused_vis.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+
3
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # * Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ #
12
+ # * Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ #
16
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
17
+ # its contributors may be used to endorse or promote products derived
18
+ # from this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30
+ # POSSIBILITY OF SUCH DAMAGE.
31
+
32
+
33
+ import collections
34
+ import os
35
+
36
+ import numpy as np
37
+ import pandas as pd
38
+ from pyntcloud import PyntCloud
39
+ from read_write_model import read_next_bytes, write_next_bytes
40
+
41
+ MeshPoint = collections.namedtuple(
42
+ "MeshingPoint",
43
+ ["position", "color", "normal", "num_visible_images", "visible_image_idxs"],
44
+ )
45
+
46
+
47
+ def read_fused(path_to_fused_ply, path_to_fused_ply_vis):
48
+ """
49
+ see: src/mvs/meshing.cc
50
+ void ReadDenseReconstruction(const std::string& path
51
+ """
52
+ assert os.path.isfile(path_to_fused_ply)
53
+ assert os.path.isfile(path_to_fused_ply_vis)
54
+
55
+ point_cloud = PyntCloud.from_file(path_to_fused_ply)
56
+ xyz_arr = point_cloud.points.loc[:, ["x", "y", "z"]].to_numpy()
57
+ normal_arr = point_cloud.points.loc[:, ["nx", "ny", "nz"]].to_numpy()
58
+ color_arr = point_cloud.points.loc[:, ["red", "green", "blue"]].to_numpy()
59
+
60
+ with open(path_to_fused_ply_vis, "rb") as fid:
61
+ num_points = read_next_bytes(fid, 8, "Q")[0]
62
+ mesh_points = [0] * num_points
63
+ for i in range(num_points):
64
+ num_visible_images = read_next_bytes(fid, 4, "I")[0]
65
+ visible_image_idxs = read_next_bytes(
66
+ fid,
67
+ num_bytes=4 * num_visible_images,
68
+ format_char_sequence="I" * num_visible_images,
69
+ )
70
+ visible_image_idxs = np.array(tuple(map(int, visible_image_idxs)))
71
+ mesh_point = MeshPoint(
72
+ position=xyz_arr[i],
73
+ color=color_arr[i],
74
+ normal=normal_arr[i],
75
+ num_visible_images=num_visible_images,
76
+ visible_image_idxs=visible_image_idxs,
77
+ )
78
+ mesh_points[i] = mesh_point
79
+ return mesh_points
80
+
81
+
82
+ def write_fused_ply(mesh_points, path_to_fused_ply):
83
+ columns = ["x", "y", "z", "nx", "ny", "nz", "red", "green", "blue"]
84
+ points_data_frame = pd.DataFrame(
85
+ np.zeros((len(mesh_points), len(columns))), columns=columns
86
+ )
87
+
88
+ positions = np.asarray([point.position for point in mesh_points])
89
+ normals = np.asarray([point.normal for point in mesh_points])
90
+ colors = np.asarray([point.color for point in mesh_points])
91
+
92
+ points_data_frame.loc[:, ["x", "y", "z"]] = positions
93
+ points_data_frame.loc[:, ["nx", "ny", "nz"]] = normals
94
+ points_data_frame.loc[:, ["red", "green", "blue"]] = colors
95
+
96
+ points_data_frame = points_data_frame.astype(
97
+ {
98
+ "x": positions.dtype,
99
+ "y": positions.dtype,
100
+ "z": positions.dtype,
101
+ "red": colors.dtype,
102
+ "green": colors.dtype,
103
+ "blue": colors.dtype,
104
+ "nx": normals.dtype,
105
+ "ny": normals.dtype,
106
+ "nz": normals.dtype,
107
+ }
108
+ )
109
+
110
+ point_cloud = PyntCloud(points_data_frame)
111
+ point_cloud.to_file(path_to_fused_ply)
112
+
113
+
114
+ def write_fused_ply_vis(mesh_points, path_to_fused_ply_vis):
115
+ """
116
+ see: src/mvs/fusion.cc
117
+ void WritePointsVisibility(const std::string& path, const std::vector<std::vector<int>>& points_visibility)
118
+ """
119
+ with open(path_to_fused_ply_vis, "wb") as fid:
120
+ write_next_bytes(fid, len(mesh_points), "Q")
121
+ for point in mesh_points:
122
+ write_next_bytes(fid, point.num_visible_images, "I")
123
+ format_char_sequence = "I" * point.num_visible_images
124
+ write_next_bytes(
125
+ fid, [*point.visible_image_idxs], format_char_sequence
126
+ )
127
+
128
+
129
+ def write_fused(points, path_to_fused_ply, path_to_fused_ply_vis):
130
+ write_fused_ply(points, path_to_fused_ply)
131
+ write_fused_ply_vis(points, path_to_fused_ply_vis)
scripts/python/read_write_model.py ADDED
@@ -0,0 +1,605 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ import argparse
32
+ import collections
33
+ import os
34
+ import struct
35
+
36
+ import numpy as np
37
+
38
+ CameraModel = collections.namedtuple(
39
+ "CameraModel", ["model_id", "model_name", "num_params"]
40
+ )
41
+ Camera = collections.namedtuple(
42
+ "Camera", ["id", "model", "width", "height", "params"]
43
+ )
44
+ BaseImage = collections.namedtuple(
45
+ "Image", ["id", "qvec", "tvec", "camera_id", "name", "xys", "point3D_ids"]
46
+ )
47
+ Point3D = collections.namedtuple(
48
+ "Point3D", ["id", "xyz", "rgb", "error", "image_ids", "point2D_idxs"]
49
+ )
50
+
51
+
52
+ class Image(BaseImage):
53
+ def qvec2rotmat(self):
54
+ return qvec2rotmat(self.qvec)
55
+
56
+
57
+ CAMERA_MODELS = {
58
+ CameraModel(model_id=0, model_name="SIMPLE_PINHOLE", num_params=3),
59
+ CameraModel(model_id=1, model_name="PINHOLE", num_params=4),
60
+ CameraModel(model_id=2, model_name="SIMPLE_RADIAL", num_params=4),
61
+ CameraModel(model_id=3, model_name="RADIAL", num_params=5),
62
+ CameraModel(model_id=4, model_name="OPENCV", num_params=8),
63
+ CameraModel(model_id=5, model_name="OPENCV_FISHEYE", num_params=8),
64
+ CameraModel(model_id=6, model_name="FULL_OPENCV", num_params=12),
65
+ CameraModel(model_id=7, model_name="FOV", num_params=5),
66
+ CameraModel(model_id=8, model_name="SIMPLE_RADIAL_FISHEYE", num_params=4),
67
+ CameraModel(model_id=9, model_name="RADIAL_FISHEYE", num_params=5),
68
+ CameraModel(model_id=10, model_name="THIN_PRISM_FISHEYE", num_params=12),
69
+ }
70
+ CAMERA_MODEL_IDS = dict(
71
+ [(camera_model.model_id, camera_model) for camera_model in CAMERA_MODELS]
72
+ )
73
+ CAMERA_MODEL_NAMES = dict(
74
+ [(camera_model.model_name, camera_model) for camera_model in CAMERA_MODELS]
75
+ )
76
+
77
+
78
+ def read_next_bytes(fid, num_bytes, format_char_sequence, endian_character="<"):
79
+ """Read and unpack the next bytes from a binary file.
80
+ :param fid:
81
+ :param num_bytes: Sum of combination of {2, 4, 8}, e.g. 2, 6, 16, 30, etc.
82
+ :param format_char_sequence: List of {c, e, f, d, h, H, i, I, l, L, q, Q}.
83
+ :param endian_character: Any of {@, =, <, >, !}
84
+ :return: Tuple of read and unpacked values.
85
+ """
86
+ data = fid.read(num_bytes)
87
+ return struct.unpack(endian_character + format_char_sequence, data)
88
+
89
+
90
+ def write_next_bytes(fid, data, format_char_sequence, endian_character="<"):
91
+ """pack and write to a binary file.
92
+ :param fid:
93
+ :param data: data to send, if multiple elements are sent at the same time,
94
+ they should be encapsuled either in a list or a tuple
95
+ :param format_char_sequence: List of {c, e, f, d, h, H, i, I, l, L, q, Q}.
96
+ should be the same length as the data list or tuple
97
+ :param endian_character: Any of {@, =, <, >, !}
98
+ """
99
+ if isinstance(data, (list, tuple)):
100
+ bytes = struct.pack(endian_character + format_char_sequence, *data)
101
+ else:
102
+ bytes = struct.pack(endian_character + format_char_sequence, data)
103
+ fid.write(bytes)
104
+
105
+
106
+ def read_cameras_text(path):
107
+ """
108
+ see: src/colmap/scene/reconstruction.cc
109
+ void Reconstruction::WriteCamerasText(const std::string& path)
110
+ void Reconstruction::ReadCamerasText(const std::string& path)
111
+ """
112
+ cameras = {}
113
+ with open(path, "r") as fid:
114
+ while True:
115
+ line = fid.readline()
116
+ if not line:
117
+ break
118
+ line = line.strip()
119
+ if len(line) > 0 and line[0] != "#":
120
+ elems = line.split()
121
+ camera_id = int(elems[0])
122
+ model = elems[1]
123
+ width = int(elems[2])
124
+ height = int(elems[3])
125
+ params = np.array(tuple(map(float, elems[4:])))
126
+ cameras[camera_id] = Camera(
127
+ id=camera_id,
128
+ model=model,
129
+ width=width,
130
+ height=height,
131
+ params=params,
132
+ )
133
+ return cameras
134
+
135
+
136
+ def read_cameras_binary(path_to_model_file):
137
+ """
138
+ see: src/colmap/scene/reconstruction.cc
139
+ void Reconstruction::WriteCamerasBinary(const std::string& path)
140
+ void Reconstruction::ReadCamerasBinary(const std::string& path)
141
+ """
142
+ cameras = {}
143
+ with open(path_to_model_file, "rb") as fid:
144
+ num_cameras = read_next_bytes(fid, 8, "Q")[0]
145
+ for _ in range(num_cameras):
146
+ camera_properties = read_next_bytes(
147
+ fid, num_bytes=24, format_char_sequence="iiQQ"
148
+ )
149
+ camera_id = camera_properties[0]
150
+ model_id = camera_properties[1]
151
+ model_name = CAMERA_MODEL_IDS[camera_properties[1]].model_name
152
+ width = camera_properties[2]
153
+ height = camera_properties[3]
154
+ num_params = CAMERA_MODEL_IDS[model_id].num_params
155
+ params = read_next_bytes(
156
+ fid,
157
+ num_bytes=8 * num_params,
158
+ format_char_sequence="d" * num_params,
159
+ )
160
+ cameras[camera_id] = Camera(
161
+ id=camera_id,
162
+ model=model_name,
163
+ width=width,
164
+ height=height,
165
+ params=np.array(params),
166
+ )
167
+ assert len(cameras) == num_cameras
168
+ return cameras
169
+
170
+
171
+ def write_cameras_text(cameras, path):
172
+ """
173
+ see: src/colmap/scene/reconstruction.cc
174
+ void Reconstruction::WriteCamerasText(const std::string& path)
175
+ void Reconstruction::ReadCamerasText(const std::string& path)
176
+ """
177
+ HEADER = (
178
+ "# Camera list with one line of data per camera:\n"
179
+ + "# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]\n"
180
+ + "# Number of cameras: {}\n".format(len(cameras))
181
+ )
182
+ with open(path, "w") as fid:
183
+ fid.write(HEADER)
184
+ for _, cam in cameras.items():
185
+ to_write = [cam.id, cam.model, cam.width, cam.height, *cam.params]
186
+ line = " ".join([str(elem) for elem in to_write])
187
+ fid.write(line + "\n")
188
+
189
+
190
+ def write_cameras_binary(cameras, path_to_model_file):
191
+ """
192
+ see: src/colmap/scene/reconstruction.cc
193
+ void Reconstruction::WriteCamerasBinary(const std::string& path)
194
+ void Reconstruction::ReadCamerasBinary(const std::string& path)
195
+ """
196
+ with open(path_to_model_file, "wb") as fid:
197
+ write_next_bytes(fid, len(cameras), "Q")
198
+ for _, cam in cameras.items():
199
+ model_id = CAMERA_MODEL_NAMES[cam.model].model_id
200
+ camera_properties = [cam.id, model_id, cam.width, cam.height]
201
+ write_next_bytes(fid, camera_properties, "iiQQ")
202
+ for p in cam.params:
203
+ write_next_bytes(fid, float(p), "d")
204
+ return cameras
205
+
206
+
207
+ def read_images_text(path):
208
+ """
209
+ see: src/colmap/scene/reconstruction.cc
210
+ void Reconstruction::ReadImagesText(const std::string& path)
211
+ void Reconstruction::WriteImagesText(const std::string& path)
212
+ """
213
+ images = {}
214
+ with open(path, "r") as fid:
215
+ while True:
216
+ line = fid.readline()
217
+ if not line:
218
+ break
219
+ line = line.strip()
220
+ if len(line) > 0 and line[0] != "#":
221
+ elems = line.split()
222
+ image_id = int(elems[0])
223
+ qvec = np.array(tuple(map(float, elems[1:5])))
224
+ tvec = np.array(tuple(map(float, elems[5:8])))
225
+ camera_id = int(elems[8])
226
+ image_name = elems[9]
227
+ elems = fid.readline().split()
228
+ xys = np.column_stack(
229
+ [
230
+ tuple(map(float, elems[0::3])),
231
+ tuple(map(float, elems[1::3])),
232
+ ]
233
+ )
234
+ point3D_ids = np.array(tuple(map(int, elems[2::3])))
235
+ images[image_id] = Image(
236
+ id=image_id,
237
+ qvec=qvec,
238
+ tvec=tvec,
239
+ camera_id=camera_id,
240
+ name=image_name,
241
+ xys=xys,
242
+ point3D_ids=point3D_ids,
243
+ )
244
+ return images
245
+
246
+
247
+ def read_images_binary(path_to_model_file):
248
+ """
249
+ see: src/colmap/scene/reconstruction.cc
250
+ void Reconstruction::ReadImagesBinary(const std::string& path)
251
+ void Reconstruction::WriteImagesBinary(const std::string& path)
252
+ """
253
+ images = {}
254
+ with open(path_to_model_file, "rb") as fid:
255
+ num_reg_images = read_next_bytes(fid, 8, "Q")[0]
256
+ for _ in range(num_reg_images):
257
+ binary_image_properties = read_next_bytes(
258
+ fid, num_bytes=64, format_char_sequence="idddddddi"
259
+ )
260
+ image_id = binary_image_properties[0]
261
+ qvec = np.array(binary_image_properties[1:5])
262
+ tvec = np.array(binary_image_properties[5:8])
263
+ camera_id = binary_image_properties[8]
264
+ binary_image_name = b""
265
+ current_char = read_next_bytes(fid, 1, "c")[0]
266
+ while current_char != b"\x00": # look for the ASCII 0 entry
267
+ binary_image_name += current_char
268
+ current_char = read_next_bytes(fid, 1, "c")[0]
269
+ image_name = binary_image_name.decode("utf-8")
270
+ num_points2D = read_next_bytes(
271
+ fid, num_bytes=8, format_char_sequence="Q"
272
+ )[0]
273
+ x_y_id_s = read_next_bytes(
274
+ fid,
275
+ num_bytes=24 * num_points2D,
276
+ format_char_sequence="ddq" * num_points2D,
277
+ )
278
+ xys = np.column_stack(
279
+ [
280
+ tuple(map(float, x_y_id_s[0::3])),
281
+ tuple(map(float, x_y_id_s[1::3])),
282
+ ]
283
+ )
284
+ point3D_ids = np.array(tuple(map(int, x_y_id_s[2::3])))
285
+ images[image_id] = Image(
286
+ id=image_id,
287
+ qvec=qvec,
288
+ tvec=tvec,
289
+ camera_id=camera_id,
290
+ name=image_name,
291
+ xys=xys,
292
+ point3D_ids=point3D_ids,
293
+ )
294
+ return images
295
+
296
+
297
+ def write_images_text(images, path):
298
+ """
299
+ see: src/colmap/scene/reconstruction.cc
300
+ void Reconstruction::ReadImagesText(const std::string& path)
301
+ void Reconstruction::WriteImagesText(const std::string& path)
302
+ """
303
+ if len(images) == 0:
304
+ mean_observations = 0
305
+ else:
306
+ mean_observations = sum(
307
+ (len(img.point3D_ids) for _, img in images.items())
308
+ ) / len(images)
309
+ HEADER = (
310
+ "# Image list with two lines of data per image:\n"
311
+ + "# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n"
312
+ + "# POINTS2D[] as (X, Y, POINT3D_ID)\n"
313
+ + "# Number of images: {}, mean observations per image: {}\n".format(
314
+ len(images), mean_observations
315
+ )
316
+ )
317
+
318
+ with open(path, "w") as fid:
319
+ fid.write(HEADER)
320
+ for _, img in images.items():
321
+ image_header = [
322
+ img.id,
323
+ *img.qvec,
324
+ *img.tvec,
325
+ img.camera_id,
326
+ img.name,
327
+ ]
328
+ first_line = " ".join(map(str, image_header))
329
+ fid.write(first_line + "\n")
330
+
331
+ points_strings = []
332
+ for xy, point3D_id in zip(img.xys, img.point3D_ids):
333
+ points_strings.append(" ".join(map(str, [*xy, point3D_id])))
334
+ fid.write(" ".join(points_strings) + "\n")
335
+
336
+
337
+ def write_images_binary(images, path_to_model_file):
338
+ """
339
+ see: src/colmap/scene/reconstruction.cc
340
+ void Reconstruction::ReadImagesBinary(const std::string& path)
341
+ void Reconstruction::WriteImagesBinary(const std::string& path)
342
+ """
343
+ with open(path_to_model_file, "wb") as fid:
344
+ write_next_bytes(fid, len(images), "Q")
345
+ for _, img in images.items():
346
+ write_next_bytes(fid, img.id, "i")
347
+ write_next_bytes(fid, img.qvec.tolist(), "dddd")
348
+ write_next_bytes(fid, img.tvec.tolist(), "ddd")
349
+ write_next_bytes(fid, img.camera_id, "i")
350
+ for char in img.name:
351
+ write_next_bytes(fid, char.encode("utf-8"), "c")
352
+ write_next_bytes(fid, b"\x00", "c")
353
+ write_next_bytes(fid, len(img.point3D_ids), "Q")
354
+ for xy, p3d_id in zip(img.xys, img.point3D_ids):
355
+ write_next_bytes(fid, [*xy, p3d_id], "ddq")
356
+
357
+
358
+ def read_points3D_text(path):
359
+ """
360
+ see: src/colmap/scene/reconstruction.cc
361
+ void Reconstruction::ReadPoints3DText(const std::string& path)
362
+ void Reconstruction::WritePoints3DText(const std::string& path)
363
+ """
364
+ points3D = {}
365
+ with open(path, "r") as fid:
366
+ while True:
367
+ line = fid.readline()
368
+ if not line:
369
+ break
370
+ line = line.strip()
371
+ if len(line) > 0 and line[0] != "#":
372
+ elems = line.split()
373
+ point3D_id = int(elems[0])
374
+ xyz = np.array(tuple(map(float, elems[1:4])))
375
+ rgb = np.array(tuple(map(int, elems[4:7])))
376
+ error = float(elems[7])
377
+ image_ids = np.array(tuple(map(int, elems[8::2])))
378
+ point2D_idxs = np.array(tuple(map(int, elems[9::2])))
379
+ points3D[point3D_id] = Point3D(
380
+ id=point3D_id,
381
+ xyz=xyz,
382
+ rgb=rgb,
383
+ error=error,
384
+ image_ids=image_ids,
385
+ point2D_idxs=point2D_idxs,
386
+ )
387
+ return points3D
388
+
389
+
390
+ def read_points3D_binary(path_to_model_file):
391
+ """
392
+ see: src/colmap/scene/reconstruction.cc
393
+ void Reconstruction::ReadPoints3DBinary(const std::string& path)
394
+ void Reconstruction::WritePoints3DBinary(const std::string& path)
395
+ """
396
+ points3D = {}
397
+ with open(path_to_model_file, "rb") as fid:
398
+ num_points = read_next_bytes(fid, 8, "Q")[0]
399
+ for _ in range(num_points):
400
+ binary_point_line_properties = read_next_bytes(
401
+ fid, num_bytes=43, format_char_sequence="QdddBBBd"
402
+ )
403
+ point3D_id = binary_point_line_properties[0]
404
+ xyz = np.array(binary_point_line_properties[1:4])
405
+ rgb = np.array(binary_point_line_properties[4:7])
406
+ error = np.array(binary_point_line_properties[7])
407
+ track_length = read_next_bytes(
408
+ fid, num_bytes=8, format_char_sequence="Q"
409
+ )[0]
410
+ track_elems = read_next_bytes(
411
+ fid,
412
+ num_bytes=8 * track_length,
413
+ format_char_sequence="ii" * track_length,
414
+ )
415
+ image_ids = np.array(tuple(map(int, track_elems[0::2])))
416
+ point2D_idxs = np.array(tuple(map(int, track_elems[1::2])))
417
+ points3D[point3D_id] = Point3D(
418
+ id=point3D_id,
419
+ xyz=xyz,
420
+ rgb=rgb,
421
+ error=error,
422
+ image_ids=image_ids,
423
+ point2D_idxs=point2D_idxs,
424
+ )
425
+ return points3D
426
+
427
+
428
+ def write_points3D_text(points3D, path):
429
+ """
430
+ see: src/colmap/scene/reconstruction.cc
431
+ void Reconstruction::ReadPoints3DText(const std::string& path)
432
+ void Reconstruction::WritePoints3DText(const std::string& path)
433
+ """
434
+ if len(points3D) == 0:
435
+ mean_track_length = 0
436
+ else:
437
+ mean_track_length = sum(
438
+ (len(pt.image_ids) for _, pt in points3D.items())
439
+ ) / len(points3D)
440
+ HEADER = (
441
+ "# 3D point list with one line of data per point:\n"
442
+ + "# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)\n"
443
+ + "# Number of points: {}, mean track length: {}\n".format(
444
+ len(points3D), mean_track_length
445
+ )
446
+ )
447
+
448
+ with open(path, "w") as fid:
449
+ fid.write(HEADER)
450
+ for _, pt in points3D.items():
451
+ point_header = [pt.id, *pt.xyz, *pt.rgb, pt.error]
452
+ fid.write(" ".join(map(str, point_header)) + " ")
453
+ track_strings = []
454
+ for image_id, point2D in zip(pt.image_ids, pt.point2D_idxs):
455
+ track_strings.append(" ".join(map(str, [image_id, point2D])))
456
+ fid.write(" ".join(track_strings) + "\n")
457
+
458
+
459
+ def write_points3D_binary(points3D, path_to_model_file):
460
+ """
461
+ see: src/colmap/scene/reconstruction.cc
462
+ void Reconstruction::ReadPoints3DBinary(const std::string& path)
463
+ void Reconstruction::WritePoints3DBinary(const std::string& path)
464
+ """
465
+ with open(path_to_model_file, "wb") as fid:
466
+ write_next_bytes(fid, len(points3D), "Q")
467
+ for _, pt in points3D.items():
468
+ write_next_bytes(fid, pt.id, "Q")
469
+ write_next_bytes(fid, pt.xyz.tolist(), "ddd")
470
+ write_next_bytes(fid, pt.rgb.tolist(), "BBB")
471
+ write_next_bytes(fid, pt.error, "d")
472
+ track_length = pt.image_ids.shape[0]
473
+ write_next_bytes(fid, track_length, "Q")
474
+ for image_id, point2D_id in zip(pt.image_ids, pt.point2D_idxs):
475
+ write_next_bytes(fid, [image_id, point2D_id], "ii")
476
+
477
+
478
+ def detect_model_format(path, ext):
479
+ if (
480
+ os.path.isfile(os.path.join(path, "cameras" + ext))
481
+ and os.path.isfile(os.path.join(path, "images" + ext))
482
+ and os.path.isfile(os.path.join(path, "points3D" + ext))
483
+ ):
484
+ print("Detected model format: '" + ext + "'")
485
+ return True
486
+
487
+ return False
488
+
489
+
490
+ def read_model(path, ext=""):
491
+ # try to detect the extension automatically
492
+ if ext == "":
493
+ if detect_model_format(path, ".bin"):
494
+ ext = ".bin"
495
+ elif detect_model_format(path, ".txt"):
496
+ ext = ".txt"
497
+ else:
498
+ print("Provide model format: '.bin' or '.txt'")
499
+ return
500
+
501
+ if ext == ".txt":
502
+ cameras = read_cameras_text(os.path.join(path, "cameras" + ext))
503
+ images = read_images_text(os.path.join(path, "images" + ext))
504
+ points3D = read_points3D_text(os.path.join(path, "points3D") + ext)
505
+ else:
506
+ cameras = read_cameras_binary(os.path.join(path, "cameras" + ext))
507
+ images = read_images_binary(os.path.join(path, "images" + ext))
508
+ points3D = read_points3D_binary(os.path.join(path, "points3D") + ext)
509
+ return cameras, images, points3D
510
+
511
+
512
+ def write_model(cameras, images, points3D, path, ext=".bin"):
513
+ if ext == ".txt":
514
+ write_cameras_text(cameras, os.path.join(path, "cameras" + ext))
515
+ write_images_text(images, os.path.join(path, "images" + ext))
516
+ write_points3D_text(points3D, os.path.join(path, "points3D") + ext)
517
+ else:
518
+ write_cameras_binary(cameras, os.path.join(path, "cameras" + ext))
519
+ write_images_binary(images, os.path.join(path, "images" + ext))
520
+ write_points3D_binary(points3D, os.path.join(path, "points3D") + ext)
521
+ return cameras, images, points3D
522
+
523
+
524
+ def qvec2rotmat(qvec):
525
+ return np.array(
526
+ [
527
+ [
528
+ 1 - 2 * qvec[2] ** 2 - 2 * qvec[3] ** 2,
529
+ 2 * qvec[1] * qvec[2] - 2 * qvec[0] * qvec[3],
530
+ 2 * qvec[3] * qvec[1] + 2 * qvec[0] * qvec[2],
531
+ ],
532
+ [
533
+ 2 * qvec[1] * qvec[2] + 2 * qvec[0] * qvec[3],
534
+ 1 - 2 * qvec[1] ** 2 - 2 * qvec[3] ** 2,
535
+ 2 * qvec[2] * qvec[3] - 2 * qvec[0] * qvec[1],
536
+ ],
537
+ [
538
+ 2 * qvec[3] * qvec[1] - 2 * qvec[0] * qvec[2],
539
+ 2 * qvec[2] * qvec[3] + 2 * qvec[0] * qvec[1],
540
+ 1 - 2 * qvec[1] ** 2 - 2 * qvec[2] ** 2,
541
+ ],
542
+ ]
543
+ )
544
+
545
+
546
+ def rotmat2qvec(R):
547
+ Rxx, Ryx, Rzx, Rxy, Ryy, Rzy, Rxz, Ryz, Rzz = R.flat
548
+ K = (
549
+ np.array(
550
+ [
551
+ [Rxx - Ryy - Rzz, 0, 0, 0],
552
+ [Ryx + Rxy, Ryy - Rxx - Rzz, 0, 0],
553
+ [Rzx + Rxz, Rzy + Ryz, Rzz - Rxx - Ryy, 0],
554
+ [Ryz - Rzy, Rzx - Rxz, Rxy - Ryx, Rxx + Ryy + Rzz],
555
+ ]
556
+ )
557
+ / 3.0
558
+ )
559
+ eigvals, eigvecs = np.linalg.eigh(K)
560
+ qvec = eigvecs[[3, 0, 1, 2], np.argmax(eigvals)]
561
+ if qvec[0] < 0:
562
+ qvec *= -1
563
+ return qvec
564
+
565
+
566
+ def main():
567
+ parser = argparse.ArgumentParser(
568
+ description="Read and write COLMAP binary and text models"
569
+ )
570
+ parser.add_argument("--input_model", help="path to input model folder")
571
+ parser.add_argument(
572
+ "--input_format",
573
+ choices=[".bin", ".txt"],
574
+ help="input model format",
575
+ default="",
576
+ )
577
+ parser.add_argument("--output_model", help="path to output model folder")
578
+ parser.add_argument(
579
+ "--output_format",
580
+ choices=[".bin", ".txt"],
581
+ help="output model format",
582
+ default=".txt",
583
+ )
584
+ args = parser.parse_args()
585
+
586
+ cameras, images, points3D = read_model(
587
+ path=args.input_model, ext=args.input_format
588
+ )
589
+
590
+ print("num_cameras:", len(cameras))
591
+ print("num_images:", len(images))
592
+ print("num_points3D:", len(points3D))
593
+
594
+ if args.output_model is not None:
595
+ write_model(
596
+ cameras,
597
+ images,
598
+ points3D,
599
+ path=args.output_model,
600
+ ext=args.output_format,
601
+ )
602
+
603
+
604
+ if __name__ == "__main__":
605
+ main()
scripts/python/test_read_write_dense.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ import numpy as np
32
+ from read_write_dense import read_array, write_array
33
+
34
+
35
+ def main():
36
+ import sys
37
+
38
+ if len(sys.argv) != 3:
39
+ print(
40
+ "Usage: python test_read_write_dense.py "
41
+ "path/to/dense/input.bin path/to/dense/output.bin"
42
+ )
43
+ return
44
+
45
+ print(
46
+ "Checking consistency of reading and writing dense arrays "
47
+ + "(depth maps / normal maps) ..."
48
+ )
49
+
50
+ path_to_dense_input = sys.argv[1]
51
+ path_to_dense_output = sys.argv[2]
52
+
53
+ dense_input = read_array(path_to_dense_input)
54
+ print("Input shape: " + str(dense_input.shape))
55
+
56
+ write_array(dense_input, path_to_dense_output)
57
+ dense_output = read_array(path_to_dense_output)
58
+
59
+ np.testing.assert_array_equal(dense_input, dense_output)
60
+
61
+ print("... dense arrays are equal.")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
scripts/python/test_read_write_fused_vis.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ import filecmp
32
+
33
+ from read_write_fused_vis import read_fused, write_fused
34
+
35
+
36
+ def main():
37
+ import sys
38
+
39
+ if len(sys.argv) != 5:
40
+ print(
41
+ "Usage: python test_read_write_fused_vis.py "
42
+ "path/to/input_fused.ply path/to/input_fused.ply.vis "
43
+ "path/to/output_fused.ply path/to/output_fused.ply.vis"
44
+ )
45
+ return
46
+
47
+ print(
48
+ "Checking consistency of reading and writing fused.ply and fused.ply.vis files ..."
49
+ )
50
+
51
+ path_to_fused_ply_input = sys.argv[1]
52
+ path_to_fused_ply_vis_input = sys.argv[2]
53
+ path_to_fused_ply_output = sys.argv[3]
54
+ path_to_fused_ply_vis_output = sys.argv[4]
55
+
56
+ mesh_points = read_fused(
57
+ path_to_fused_ply_input, path_to_fused_ply_vis_input
58
+ )
59
+ write_fused(
60
+ mesh_points, path_to_fused_ply_output, path_to_fused_ply_vis_output
61
+ )
62
+
63
+ assert filecmp.cmp(path_to_fused_ply_input, path_to_fused_ply_output)
64
+ assert filecmp.cmp(
65
+ path_to_fused_ply_vis_input, path_to_fused_ply_vis_output
66
+ )
67
+
68
+ print("... Results are equal.")
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
scripts/python/test_read_write_model.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ from tempfile import mkdtemp
32
+
33
+ import numpy as np
34
+ from read_write_model import read_model, write_model
35
+
36
+
37
+ def compare_cameras(cameras1, cameras2):
38
+ assert len(cameras1) == len(cameras2)
39
+ for camera_id1 in cameras1:
40
+ camera1 = cameras1[camera_id1]
41
+ camera2 = cameras2[camera_id1]
42
+ assert camera1.id == camera2.id
43
+ assert camera1.width == camera2.width
44
+ assert camera1.height == camera2.height
45
+ assert np.allclose(camera1.params, camera2.params)
46
+
47
+
48
+ def compare_images(images1, images2):
49
+ assert len(images1) == len(images2)
50
+ for image_id1 in images1:
51
+ image1 = images1[image_id1]
52
+ image2 = images2[image_id1]
53
+ assert image1.id == image2.id
54
+ assert np.allclose(image1.qvec, image2.qvec)
55
+ assert np.allclose(image1.tvec, image2.tvec)
56
+ assert image1.camera_id == image2.camera_id
57
+ assert image1.name == image2.name
58
+ assert np.allclose(image1.xys, image2.xys)
59
+ assert np.array_equal(image1.point3D_ids, image2.point3D_ids)
60
+
61
+
62
+ def compare_points(points3D1, points3D2):
63
+ for point3D_id1 in points3D1:
64
+ point3D1 = points3D1[point3D_id1]
65
+ point3D2 = points3D2[point3D_id1]
66
+ assert point3D1.id == point3D2.id
67
+ assert np.allclose(point3D1.xyz, point3D2.xyz)
68
+ assert np.array_equal(point3D1.rgb, point3D2.rgb)
69
+ assert np.allclose(point3D1.error, point3D2.error)
70
+ assert np.array_equal(point3D1.image_ids, point3D2.image_ids)
71
+ assert np.array_equal(point3D1.point2D_idxs, point3D2.point2D_idxs)
72
+
73
+
74
+ def main():
75
+ import sys
76
+
77
+ if len(sys.argv) != 3:
78
+ print(
79
+ "Usage: python read_model.py "
80
+ "path/to/model/folder/txt path/to/model/folder/bin"
81
+ )
82
+ return
83
+
84
+ print("Comparing text and binary models ...")
85
+
86
+ path_to_model_txt_folder = sys.argv[1]
87
+ path_to_model_bin_folder = sys.argv[2]
88
+ cameras_txt, images_txt, points3D_txt = read_model(
89
+ path_to_model_txt_folder, ext=".txt"
90
+ )
91
+ cameras_bin, images_bin, points3D_bin = read_model(
92
+ path_to_model_bin_folder, ext=".bin"
93
+ )
94
+ compare_cameras(cameras_txt, cameras_bin)
95
+ compare_images(images_txt, images_bin)
96
+ compare_points(points3D_txt, points3D_bin)
97
+
98
+ print("... text and binary models are equal.")
99
+ print("Saving text model and reloading it ...")
100
+
101
+ tmpdir = mkdtemp()
102
+ write_model(cameras_bin, images_bin, points3D_bin, tmpdir, ext=".txt")
103
+ cameras_txt, images_txt, points3D_txt = read_model(tmpdir, ext=".txt")
104
+ compare_cameras(cameras_txt, cameras_bin)
105
+ compare_images(images_txt, images_bin)
106
+ compare_points(points3D_txt, points3D_bin)
107
+
108
+ print("... saved text and loaded models are equal.")
109
+ print("Saving binary model and reloading it ...")
110
+
111
+ write_model(cameras_bin, images_bin, points3D_bin, tmpdir, ext=".bin")
112
+ cameras_bin, images_bin, points3D_bin = read_model(tmpdir, ext=".bin")
113
+ compare_cameras(cameras_txt, cameras_bin)
114
+ compare_images(images_txt, images_bin)
115
+ compare_points(points3D_txt, points3D_bin)
116
+
117
+ print("... saved binary and loaded models are equal.")
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
scripts/python/visualize_model.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c), ETH Zurich and UNC Chapel Hill.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15
+ # its contributors may be used to endorse or promote products derived
16
+ # from this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ import argparse
31
+
32
+ import numpy as np
33
+ import open3d
34
+ from read_write_model import qvec2rotmat, read_model
35
+
36
+
37
+ class Model:
38
+ def __init__(self):
39
+ self.cameras = []
40
+ self.images = []
41
+ self.points3D = []
42
+ self.__vis = None
43
+
44
+ def read_model(self, path, ext=""):
45
+ self.cameras, self.images, self.points3D = read_model(path, ext)
46
+
47
+ def add_points(self, min_track_len=3, remove_statistical_outlier=True):
48
+ pcd = open3d.geometry.PointCloud()
49
+
50
+ xyz = []
51
+ rgb = []
52
+ for point3D in self.points3D.values():
53
+ track_len = len(point3D.point2D_idxs)
54
+ if track_len < min_track_len:
55
+ continue
56
+ xyz.append(point3D.xyz)
57
+ rgb.append(point3D.rgb / 255)
58
+
59
+ pcd.points = open3d.utility.Vector3dVector(xyz)
60
+ pcd.colors = open3d.utility.Vector3dVector(rgb)
61
+
62
+ # remove obvious outliers
63
+ if remove_statistical_outlier:
64
+ [pcd, _] = pcd.remove_statistical_outlier(
65
+ nb_neighbors=20, std_ratio=2.0
66
+ )
67
+
68
+ # open3d.visualization.draw_geometries([pcd])
69
+ self.__vis.add_geometry(pcd)
70
+ self.__vis.poll_events()
71
+ self.__vis.update_renderer()
72
+
73
+ def add_cameras(self, scale=1):
74
+ frames = []
75
+ for img in self.images.values():
76
+ # rotation
77
+ R = qvec2rotmat(img.qvec)
78
+
79
+ # translation
80
+ t = img.tvec
81
+
82
+ # invert
83
+ t = -R.T @ t
84
+ R = R.T
85
+
86
+ # intrinsics
87
+ cam = self.cameras[img.camera_id]
88
+
89
+ if cam.model in ("SIMPLE_PINHOLE", "SIMPLE_RADIAL", "RADIAL"):
90
+ fx = fy = cam.params[0]
91
+ cx = cam.params[1]
92
+ cy = cam.params[2]
93
+ elif cam.model in (
94
+ "PINHOLE",
95
+ "OPENCV",
96
+ "OPENCV_FISHEYE",
97
+ "FULL_OPENCV",
98
+ ):
99
+ fx = cam.params[0]
100
+ fy = cam.params[1]
101
+ cx = cam.params[2]
102
+ cy = cam.params[3]
103
+ else:
104
+ raise Exception("Camera model not supported")
105
+
106
+ # intrinsics
107
+ K = np.identity(3)
108
+ K[0, 0] = fx
109
+ K[1, 1] = fy
110
+ K[0, 2] = cx
111
+ K[1, 2] = cy
112
+
113
+ # create axis, plane and pyramed geometries that will be drawn
114
+ cam_model = draw_camera(K, R, t, cam.width, cam.height, scale)
115
+ frames.extend(cam_model)
116
+
117
+ # add geometries to visualizer
118
+ for i in frames:
119
+ self.__vis.add_geometry(i)
120
+
121
+ def create_window(self):
122
+ self.__vis = open3d.visualization.Visualizer()
123
+ self.__vis.create_window()
124
+
125
+ def show(self):
126
+ self.__vis.poll_events()
127
+ self.__vis.update_renderer()
128
+ self.__vis.run()
129
+ self.__vis.destroy_window()
130
+
131
+
132
+ def draw_camera(K, R, t, w, h, scale=1, color=[0.8, 0.2, 0.8]):
133
+ """Create axis, plane and pyramed geometries in Open3D format.
134
+ :param K: calibration matrix (camera intrinsics)
135
+ :param R: rotation matrix
136
+ :param t: translation
137
+ :param w: image width
138
+ :param h: image height
139
+ :param scale: camera model scale
140
+ :param color: color of the image plane and pyramid lines
141
+ :return: camera model geometries (axis, plane and pyramid)
142
+ """
143
+
144
+ # intrinsics
145
+ K = K.copy() / scale
146
+ Kinv = np.linalg.inv(K)
147
+
148
+ # 4x4 transformation
149
+ T = np.column_stack((R, t))
150
+ T = np.vstack((T, (0, 0, 0, 1)))
151
+
152
+ # axis
153
+ axis = open3d.geometry.TriangleMesh.create_coordinate_frame(
154
+ size=0.5 * scale
155
+ )
156
+ axis.transform(T)
157
+
158
+ # points in pixel
159
+ points_pixel = [
160
+ [0, 0, 0],
161
+ [0, 0, 1],
162
+ [w, 0, 1],
163
+ [0, h, 1],
164
+ [w, h, 1],
165
+ ]
166
+
167
+ # pixel to camera coordinate system
168
+ points = [Kinv @ p for p in points_pixel]
169
+
170
+ # image plane
171
+ width = abs(points[1][0]) + abs(points[3][0])
172
+ height = abs(points[1][1]) + abs(points[3][1])
173
+ plane = open3d.geometry.TriangleMesh.create_box(width, height, depth=1e-6)
174
+ plane.paint_uniform_color(color)
175
+ plane.translate([points[1][0], points[1][1], scale])
176
+ plane.transform(T)
177
+
178
+ # pyramid
179
+ points_in_world = [(R @ p + t) for p in points]
180
+ lines = [
181
+ [0, 1],
182
+ [0, 2],
183
+ [0, 3],
184
+ [0, 4],
185
+ ]
186
+ colors = [color for i in range(len(lines))]
187
+ line_set = open3d.geometry.LineSet(
188
+ points=open3d.utility.Vector3dVector(points_in_world),
189
+ lines=open3d.utility.Vector2iVector(lines),
190
+ )
191
+ line_set.colors = open3d.utility.Vector3dVector(colors)
192
+
193
+ # return as list in Open3D format
194
+ return [axis, plane, line_set]
195
+
196
+
197
+ def parse_args():
198
+ parser = argparse.ArgumentParser(
199
+ description="Visualize COLMAP binary and text models"
200
+ )
201
+ parser.add_argument(
202
+ "--input_model", required=True, help="path to input model folder"
203
+ )
204
+ parser.add_argument(
205
+ "--input_format",
206
+ choices=[".bin", ".txt"],
207
+ help="input model format",
208
+ default="",
209
+ )
210
+ args = parser.parse_args()
211
+ return args
212
+
213
+
214
+ def main():
215
+ args = parse_args()
216
+
217
+ # read COLMAP model
218
+ model = Model()
219
+ model.read_model(args.input_model, ext=args.input_format)
220
+
221
+ print("num_cameras:", len(model.cameras))
222
+ print("num_images:", len(model.images))
223
+ print("num_points3D:", len(model.points3D))
224
+
225
+ # display using Open3D visualization tools
226
+ model.create_window()
227
+ model.add_points()
228
+ model.add_cameras(scale=0.25)
229
+ model.show()
230
+
231
+
232
+ if __name__ == "__main__":
233
+ main()