Update COLMAP integration for 4.1.0: add GLOMAP mapper and matching type options

#1
.gitignore CHANGED
@@ -1,2 +1,6 @@
 
 
 
 
1
  LightGlue/
2
- __pycache__/
 
1
+ vocab_tree_flickr100K_words1M.bin
2
+ vocab_tree_flickr100K_words256K.bin
3
+ vocab_tree_flickr100K_words32K.bin
4
+
5
  LightGlue/
6
+ __pycache__/
colmap_mapper.py DELETED
@@ -1,85 +0,0 @@
1
- """
2
- Module: VSLAM-LAB - Baselines - colmap - colmap_mapper.py
3
- - Author: Alejandro Fontan Villacampa
4
- - Assisted by: Claude (Fable 5.1)
5
- - Version: 2.0
6
- - Created: 2024-07-12
7
- - Updated: 2026-10-03
8
- - License: GPLv3 License
9
-
10
- Mapping stage of the colmap pipeline (Python port of colmap_mapper.sh): runs COLMAP's incremental
11
- mapper or GLOMAP's global mapper on the database, picks the sub-model with the most registered
12
- images and exports it as TXT into the colmap folder. Called by vslamlab_colmap.py.
13
- """
14
-
15
- import re
16
- from pathlib import Path
17
-
18
- from colmap_utilities import colmap_camera, run
19
-
20
-
21
- def registered_images(model_dir: Path) -> int:
22
- out = run(["colmap", "model_analyzer", "--path", str(model_dir)], capture=True)
23
- match = re.search(r"Registered images: (\d+)", out)
24
- return int(match.group(1)) if match else 0
25
-
26
-
27
- def select_best_model(exp_folder_colmap: Path) -> tuple[Path, int]:
28
- """COLMAP may split the scene into several sub-models (<exp_folder_colmap>/0, /1, ...), and the
29
- largest one is not necessarily /0. Returns the sub-model with the most registered images and
30
- records the choice in <exp_folder_colmap>/best_model for the rest of the pipeline (gui, ...)."""
31
- best_model, best_num_images = None, -1
32
- for model_dir in sorted(p for p in exp_folder_colmap.iterdir() if p.is_dir() and p.name.isdigit()):
33
- if not (model_dir / "images.bin").is_file():
34
- continue
35
- num_images = registered_images(model_dir)
36
- print(f" sub-model {model_dir.name}: {num_images} registered images")
37
- if num_images > best_num_images:
38
- best_model, best_num_images = model_dir, num_images
39
- if best_model is None or best_num_images == 0: # glomap writes an empty /0 when its pose graph is empty
40
- raise SystemExit(f" no reconstruction produced (no sub-model with registered images in {exp_folder_colmap})")
41
- (exp_folder_colmap / "best_model").write_text(f"{best_model}\n")
42
- return best_model, best_num_images
43
-
44
-
45
- def run_mapper(exp_folder_colmap: Path, rgb_path: Path, calibration_yaml: Path, camera_name: str,
46
- mapper_type: str, optimize_intrinsics: int) -> Path:
47
- """Reconstruct from <exp_folder_colmap>/colmap_database.db; returns the best sub-model folder."""
48
- print("Executing colmap_mapper ...")
49
-
50
- calibration_model, _, _ = colmap_camera(calibration_yaml, camera_name)
51
- print(f" camera model : {calibration_model}")
52
-
53
- # optimize_intrinsics (0/1, default 1): whether bundle adjustment refines the camera intrinsics.
54
- # With 1, focal length and distortion (extra params) are refined and the principal point stays
55
- # fixed, matching COLMAP's own defaults; with 0 the intrinsics from the calibration yaml are kept
56
- # as given. An 'unknown' calibration model has no intrinsics to keep (the matcher started COLMAP
57
- # from a guess), so it always refines regardless of the flag.
58
- if calibration_model == "unknown" and optimize_intrinsics != 1:
59
- print(f" WARNING: optimize_intrinsics={optimize_intrinsics} ignored: camera model is 'unknown' (no intrinsics to keep fixed), refining intrinsics")
60
- optimize_intrinsics = 1
61
- refine = "1" if optimize_intrinsics == 1 else "0"
62
- ba_refine_focal_length, ba_refine_principal_point, ba_refine_extra_params = refine, "0", refine
63
- print(f" optimize_intrinsics: {optimize_intrinsics} (ba_refine_focal_length={ba_refine_focal_length}, "
64
- f"ba_refine_principal_point={ba_refine_principal_point}, ba_refine_extra_params={ba_refine_extra_params})")
65
-
66
- database = exp_folder_colmap / "colmap_database.db"
67
- if mapper_type == "glomap":
68
- print(" global mapper (GLOMAP) ...")
69
- command, prefix = "global_mapper", "GlobalMapper"
70
- else:
71
- print(" colmap mapper (COLMAP) ...")
72
- command, prefix = "mapper", "Mapper"
73
- run(["colmap", command,
74
- "--database_path", str(database),
75
- "--image_path", str(rgb_path),
76
- "--output_path", str(exp_folder_colmap),
77
- f"--{prefix}.ba_refine_focal_length", ba_refine_focal_length,
78
- f"--{prefix}.ba_refine_principal_point", ba_refine_principal_point,
79
- f"--{prefix}.ba_refine_extra_params", ba_refine_extra_params])
80
-
81
- best_model, best_num_images = select_best_model(exp_folder_colmap)
82
-
83
- print(f" colmap model_converter (sub-model {best_model.name}, {best_num_images} images) ...")
84
- run(["colmap", "model_converter", "--input_path", str(best_model), "--output_path", str(exp_folder_colmap), "--output_type", "TXT"])
85
- return best_model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
colmap_mapper.sh ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ echo "Executing colmap_mapper.sh ..."
3
+
4
+ sequence_path="$1"
5
+ exp_folder="$2"
6
+ exp_id="$3"
7
+ settings_yaml="$4"
8
+ calibration_yaml="$5"
9
+ rgb_csv="$6"
10
+ camera_name="$7"
11
+
12
+ exp_folder_colmap="${exp_folder}/colmap_${exp_id}"
13
+ rgb_dir="${camera_name}"
14
+ rgb_path="${sequence_path}/${rgb_dir}"
15
+
16
+ read -r calibration_model more_ <<< $(python3 Baselines/colmap/get_calibration.py "$calibration_yaml" "$camera_name")
17
+ echo " camera model : $calibration_model"
18
+ ba_refine_focal_length="0"
19
+ ba_refine_principal_point="0"
20
+ ba_refine_extra_params="0"
21
+ if [ "${calibration_model}" == "unknown" ]
22
+ then
23
+ ba_refine_focal_length="1"
24
+ ba_refine_principal_point="1"
25
+ ba_refine_extra_params="1"
26
+ fi
27
+
28
+ echo " colmap mapper ..."
29
+ database="${exp_folder_colmap}/colmap_database.db"
30
+
31
+ colmap mapper \
32
+ --database_path ${database} \
33
+ --image_path ${rgb_path} \
34
+ --output_path ${exp_folder_colmap} \
35
+ --Mapper.ba_refine_focal_length ${ba_refine_focal_length} \
36
+ --Mapper.ba_refine_principal_point ${ba_refine_principal_point} \
37
+ --Mapper.ba_refine_extra_params ${ba_refine_extra_params}
38
+
39
+ echo " colmap model_converter ..."
40
+ colmap model_converter \
41
+ --input_path ${exp_folder_colmap}/0 --output_path ${exp_folder_colmap} --output_type TXT
42
+
43
+
colmap_matcher.py DELETED
@@ -1,111 +0,0 @@
1
- """
2
- Module: VSLAM-LAB - Baselines - colmap - colmap_matcher.py
3
- - Author: Alejandro Fontan Villacampa
4
- - Assisted by: Claude (Fable 5.1)
5
- - Version: 2.0
6
- - Created: 2024-07-12
7
- - Updated: 2026-10-03
8
- - License: GPLv3 License
9
-
10
- Feature extraction and matching stage of the colmap pipeline (Python port of colmap_matcher.sh):
11
- creates the COLMAP database, extracts features for the frames listed in the experiment's rgb csv
12
- (optionally masked) and matches them exhaustively or sequentially. Called by vslamlab_colmap.py.
13
- """
14
-
15
- from pathlib import Path
16
-
17
- from colmap_utilities import colmap_camera, detect_gpus, run
18
- from create_colmap_image_list import create_colmap_image_list
19
- from create_colmap_mask_dir import create_colmap_mask_dir
20
-
21
- # matching_type -> (FeatureExtraction.type, FeatureMatching.type)
22
- MATCHING_TYPES: dict[str, tuple[str, str]] = {
23
- 'sift_bruteforce': ('SIFT', 'SIFT_BRUTEFORCE'),
24
- 'sift_lightglue': ('SIFT', 'SIFT_LIGHTGLUE'),
25
- 'aliked_bruteforce': ('ALIKED_N16ROT', 'ALIKED_BRUTEFORCE'),
26
- 'aliked_lightglue': ('ALIKED_N16ROT', 'ALIKED_LIGHTGLUE'),
27
- }
28
-
29
-
30
- def run_matcher(sequence_path: Path, exp_folder_colmap: Path, rgb_path: Path, rgb_csv: Path, calibration_yaml: Path,
31
- camera_name: str, matcher_type: str, matching_type: str, use_gpu: int, use_mask: int) -> Path:
32
- """Build <exp_folder_colmap>/colmap_database.db with features and matches; returns its path."""
33
- print("\nExecuting colmap_matcher ...")
34
-
35
- if matching_type not in MATCHING_TYPES:
36
- raise SystemExit(f"Unknown matching_type: {matching_type}")
37
- feature_extraction_type, feature_matching_type = MATCHING_TYPES[matching_type]
38
-
39
- gpu_index_list, num_threads = detect_gpus(use_gpu)
40
-
41
- calibration_model, colmap_camera_model, camera_params = colmap_camera(calibration_yaml, camera_name)
42
-
43
- colmap_image_list = exp_folder_colmap / "colmap_image_list.txt"
44
- create_colmap_image_list(str(rgb_csv), str(colmap_image_list), camera_name)
45
-
46
- database = exp_folder_colmap / "colmap_database.db"
47
- database.unlink(missing_ok=True)
48
- run(["colmap", "database_creator", "--database_path", str(database)])
49
-
50
- print(f" colmap feature_extractor ({feature_extraction_type}) ...")
51
- print(f" gpu_index: {gpu_index_list}")
52
- print(f" camera model : {calibration_model} (colmap: {colmap_camera_model})")
53
- camera_params_args: list[str] = []
54
- if camera_params:
55
- print(f" camera params: {camera_params}")
56
- camera_params_args = ["--ImageReader.camera_params", camera_params]
57
-
58
- # Masks (use_mask=1): the rgb csv's path_mask_<i> column (1 = usable pixel, 0 = masked out - no
59
- # features are extracted where the mask is 0). One shared mask (refrax) goes through
60
- # --ImageReader.camera_mask_path, per-frame masks (mask2former, datasets shipping masks) through
61
- # a directory of <image name>.png symlinks and --ImageReader.mask_path.
62
- mask_args: list[str] = []
63
- if use_mask == 1:
64
- mask_spec = create_colmap_mask_dir(str(rgb_csv), camera_name, str(sequence_path), str(exp_folder_colmap / "colmap_masks"))
65
- if mask_spec.startswith("camera_mask:"):
66
- mask_args = ["--ImageReader.camera_mask_path", mask_spec[len("camera_mask:"):]]
67
- print(f" mask: shared camera mask {mask_spec[len('camera_mask:'):]}")
68
- elif mask_spec.startswith("mask_dir:"):
69
- mask_args = ["--ImageReader.mask_path", mask_spec[len("mask_dir:"):]]
70
- print(f" mask: per-frame masks in {mask_spec[len('mask_dir:'):]}")
71
- else:
72
- print(f" mask: none available for {camera_name} in {rgb_csv}")
73
- else:
74
- print(f" mask: disabled (use_mask={use_mask})")
75
-
76
- run(["colmap", "feature_extractor",
77
- "--database_path", str(database),
78
- "--image_path", str(rgb_path),
79
- "--image_list_path", str(colmap_image_list),
80
- "--ImageReader.camera_model", colmap_camera_model,
81
- "--ImageReader.single_camera", "1",
82
- "--ImageReader.single_camera_per_folder", "1",
83
- "--FeatureExtraction.type", feature_extraction_type,
84
- "--FeatureExtraction.use_gpu", str(use_gpu),
85
- "--FeatureExtraction.gpu_index", gpu_index_list,
86
- "--FeatureExtraction.num_threads", str(num_threads),
87
- *camera_params_args, *mask_args])
88
-
89
- matching_args = ["--database_path", str(database),
90
- "--FeatureMatching.type", feature_matching_type,
91
- "--FeatureMatching.use_gpu", str(use_gpu),
92
- "--FeatureMatching.gpu_index", gpu_index_list,
93
- "--FeatureMatching.num_threads", str(num_threads)]
94
-
95
- if matcher_type == "exhaustive":
96
- print(f" colmap exhaustive_matcher ({feature_matching_type}) ...")
97
- print(f" gpu_index: {gpu_index_list}")
98
- run(["colmap", "exhaustive_matcher", *matching_args])
99
- elif matcher_type == "sequential":
100
- # Loop detection uses COLMAP's default vocabulary tree for the feature type (a FAISS index
101
- # per SIFT / ALIKED / LoMa, auto-downloaded once into ~/.cache/colmap). The legacy
102
- # flickr100K FLANN trees are rejected by COLMAP >= 3.12 ("Failed to read faiss index").
103
- print(f" colmap sequential_matcher ({feature_matching_type}) ...")
104
- print(f" Vocabulary Tree: COLMAP default for {feature_extraction_type} (cached in ~/.cache/colmap)")
105
- print(f" gpu_index: {gpu_index_list}")
106
- run(["colmap", "sequential_matcher", *matching_args,
107
- "--SequentialMatching.loop_detection", "1"])
108
- else:
109
- raise SystemExit(f"Unknown matcher_type: {matcher_type}")
110
-
111
- return database
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
colmap_matcher.sh ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ echo ""
3
+ echo "Executing colmap_matcher.sh ..."
4
+
5
+ sequence_path="$1"
6
+ exp_folder="$2"
7
+ exp_id="$3"
8
+ settings_yaml="$4"
9
+ calibration_yaml="$5"
10
+ rgb_csv="$6"
11
+ matcher_type="$7"
12
+ use_gpu="$8"
13
+ camera_name="$9"
14
+
15
+ exp_folder_colmap="${exp_folder}/colmap_${exp_id}"
16
+ rgb_dir=$(awk -F, 'NR==2 { split($2,a,"/"); print a[1]; exit }' "$rgb_csv")
17
+ rgb_path="${sequence_path}/${rgb_dir}"
18
+
19
+ # Get calibration model
20
+ read -r calibration_model more_ <<< $(python3 Baselines/colmap/get_calibration.py "$calibration_yaml" "$camera_name")
21
+
22
+ # Create colmap image list
23
+ colmap_image_list="${exp_folder_colmap}/colmap_image_list.txt"
24
+ python3 Baselines/colmap/create_colmap_image_list.py "$rgb_csv" "$colmap_image_list" "$camera_name"
25
+
26
+ # Create Colmap Database
27
+ database="${exp_folder_colmap}/colmap_database.db"
28
+ rm -rf ${database}
29
+ colmap database_creator --database_path ${database}
30
+
31
+ # Feature extractor
32
+ echo " colmap feature_extractor ..."
33
+
34
+ if [ "${calibration_model}" == "unknown" ]
35
+ then
36
+ echo " camera model : $calibration_model"
37
+ colmap feature_extractor \
38
+ --database_path ${database} \
39
+ --image_path ${rgb_path} \
40
+ --image_list_path ${colmap_image_list} \
41
+ --ImageReader.camera_model SIMPLE_PINHOLE \
42
+ --ImageReader.single_camera 1 \
43
+ --ImageReader.single_camera_per_folder 1 \
44
+ --FeatureExtraction.use_gpu ${use_gpu}
45
+ fi
46
+
47
+ if [ "${calibration_model}" == "pinhole" ]
48
+ then
49
+ read -r calibration_model fx fy cx cy <<< $(python3 Baselines/colmap/get_calibration.py "$calibration_yaml" "$camera_name")
50
+ echo " camera model : $calibration_model"
51
+ echo " fx: $fx , fy: $fy , cx: $cx , cy: $cy"
52
+ colmap feature_extractor \
53
+ --database_path ${database} \
54
+ --image_path ${rgb_path} \
55
+ --image_list_path ${colmap_image_list} \
56
+ --ImageReader.camera_model PINHOLE \
57
+ --ImageReader.single_camera 1 \
58
+ --ImageReader.single_camera_per_folder 1 \
59
+ --FeatureExtraction.use_gpu ${use_gpu} \
60
+ --ImageReader.camera_params "${fx},${fy},${cx},${cy}"
61
+ fi
62
+
63
+ if [ "${calibration_model}" == "radtan4" ]
64
+ then
65
+ read -r calibration_model fx fy cx cy k1 k2 p1 p2 <<< $(python3 Baselines/colmap/get_calibration.py "$calibration_yaml" "$camera_name")
66
+ echo " camera model : $calibration_model"
67
+ echo " fx: $fx , fy: $fy , cx: $cx , cy: $cy"
68
+ echo " k1: $k1 , k2: $k2 , p1: $p1 , p2: $p2"
69
+ colmap feature_extractor \
70
+ --database_path ${database} \
71
+ --image_path ${rgb_path} \
72
+ --image_list_path ${colmap_image_list} \
73
+ --ImageReader.camera_model "OPENCV" \
74
+ --ImageReader.single_camera 1 \
75
+ --ImageReader.single_camera_per_folder 0 \
76
+ --FeatureExtraction.use_gpu ${use_gpu} \
77
+ --ImageReader.camera_params "${fx},${fy},${cx},${cy},${k1},${k2},${p1},${p2}"
78
+ fi
79
+
80
+ if [ "${calibration_model}" == "radtan5" ]
81
+ then
82
+ read -r calibration_model fx fy cx cy k1 k2 p1 p2 k3 <<< $(python3 Baselines/colmap/get_calibration.py "$calibration_yaml" "$camera_name")
83
+ echo " camera model : $calibration_model"
84
+ echo " fx: $fx , fy: $fy , cx: $cx , cy: $cy"
85
+ echo " k1: $k1 , k2: $k2 , p1: $p1 , p2: $p2, k3: $k3"
86
+ colmap feature_extractor \
87
+ --database_path ${database} \
88
+ --image_path ${rgb_path} \
89
+ --image_list_path ${colmap_image_list} \
90
+ --ImageReader.camera_model "FULL_OPENCV" \
91
+ --ImageReader.single_camera 1 \
92
+ --ImageReader.single_camera_per_folder 1 \
93
+ --FeatureExtraction.use_gpu ${use_gpu} \
94
+ --ImageReader.camera_params "${fx},${fy},${cx},${cy},${k1},${k2},${p1},${p2},${k3},0,0,0"
95
+ fi
96
+
97
+ if [ "${calibration_model}" == "equid4" ]
98
+ then
99
+ read -r calibration_model fx fy cx cy k1 k2 k3 k4 <<< $(python3 Baselines/colmap/get_calibration.py "$calibration_yaml" "$camera_name")
100
+ echo " camera model : $calibration_model"
101
+ echo " fx: $fx , fy: $fy , cx: $cx , cy: $cy"
102
+ echo " k1: $k1 , k2: $k2 , k3: $k3 , k4: $k4"
103
+ colmap feature_extractor \
104
+ --database_path ${database} \
105
+ --image_path ${rgb_path} \
106
+ --image_list_path ${colmap_image_list} \
107
+ --ImageReader.camera_model "OPENCV_FISHEYE"\
108
+ --ImageReader.single_camera 1 \
109
+ --ImageReader.single_camera_per_folder 1 \
110
+ --FeatureExtraction.use_gpu ${use_gpu} \
111
+ --ImageReader.camera_params "${fx},${fy},${cx},${cy},${k1},${k2},${k3},${k4}"
112
+ fi
113
+
114
+ # Exhaustive Feature Matcher
115
+ if [ "${matcher_type}" == "exhaustive" ]
116
+ then
117
+ echo " colmap exhaustive_matcher ..."
118
+ colmap exhaustive_matcher \
119
+ --database_path ${database} \
120
+ --FeatureMatching.use_gpu ${use_gpu}
121
+ fi
122
+
123
+ # Sequential Feature Matcher
124
+ if [ "${matcher_type}" == "sequential" ]
125
+ then
126
+ num_rgb=$(( $(wc -l < "$rgb_csv") - 1 ))
127
+
128
+ # Pick vocabulary tree based on the number of images
129
+ vocabulary_tree="Baselines/colmap/vocab_tree_faiss_flickr100K_words32K.bin"
130
+ if [ "$num_rgb" -gt 1000 ]; then
131
+ vocabulary_tree="Baselines/colmap/vocab_tree_faiss_flickr100K_words256K.bin"
132
+ fi
133
+ if [ "$num_rgb" -gt 10000 ]; then
134
+ vocabulary_tree="Baselines/colmap/vocab_tree_faiss_flickr100K_words1M.bin"
135
+ fi
136
+
137
+ echo " colmap sequential_matcher ..."
138
+ echo " Vocabulary Tree: $vocabulary_tree"
139
+ colmap sequential_matcher \
140
+ --database_path "${database}" \
141
+ --SequentialMatching.loop_detection 1 \
142
+ --SequentialMatching.vocab_tree_path ${vocabulary_tree} \
143
+ --FeatureMatching.use_gpu "${use_gpu}"
144
+ fi
145
+
146
+ # LightGlue Feature Matcher
147
+ if [ "${matcher_type}" == "custom" ]
148
+ then
149
+ colmap exhaustive_matcher \
150
+ --database_path ${database} \
151
+ --FeatureMatching.use_gpu ${use_gpu}
152
+
153
+ pixi run -e lightglue python3 Baselines/colmap/feature_matcher.py --database ${database} --rgb_path ${rgb_path} --rgb_csv ${rgb_csv}
154
+ fi
155
+
colmap_reconstruction.sh ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Default values
4
+ matcher_type="exhaustive"
5
+ use_gpu="1"
6
+ verbose="0"
7
+ settings_yaml=""
8
+ sequence_path=""
9
+ exp_folder=""
10
+ exp_id=""
11
+ calibration_yaml=""
12
+ rgb_csv=""
13
+ camera_name="rgb_0"
14
+
15
+ # Function to split key-value pairs and assign them to variables
16
+ split_and_assign() {
17
+ local input=$1
18
+ local key=$(echo $input | cut -d':' -f1)
19
+ local value=$(echo $input | cut -d':' -f2-)
20
+ eval $key=$value
21
+ }
22
+
23
+ # Read Inputs
24
+ for ((i=1; i<=$#; i++)); do
25
+ split_and_assign "${!i}"
26
+ done
27
+
28
+ exp_id=$(printf "%05d" ${exp_id})
29
+
30
+ echo -e "\n================= Experiment Configuration ================="
31
+ echo " Sequence Path : $sequence_path"
32
+ echo " Experiment Folder : $exp_folder"
33
+ echo " Experiment ID : $exp_id"
34
+ echo " Verbose : $verbose"
35
+ echo " Matcher Type : $matcher_type"
36
+ echo " Use GPU : $use_gpu"
37
+ echo " Settings YAML : $settings_yaml"
38
+ echo " Calibration YAML : $calibration_yaml"
39
+ echo " RGB CSV : $rgb_csv"
40
+ echo " Camera Name : $camera_name"
41
+ echo "============================================================"
42
+
43
+ # Create folder to save colmap files
44
+ exp_folder_colmap="${exp_folder}/colmap_${exp_id}"
45
+ rm -rf "$exp_folder_colmap"
46
+ mkdir "$exp_folder_colmap"
47
+
48
+ # Run COLMAP scripts for matching and mapping
49
+ export QT_QPA_PLATFORM_PLUGIN_PATH="$CONDA_PREFIX/plugins/platforms"
50
+ colmap_args="$sequence_path $exp_folder $exp_id $settings_yaml $calibration_yaml $rgb_csv"
51
+ ./Baselines/colmap/colmap_matcher.sh $colmap_args $matcher_type $use_gpu $camera_name
52
+ ./Baselines/colmap/colmap_mapper.sh $colmap_args $camera_name
53
+
54
+ # Convert COLMAP outputs to a format suitable for VSLAM-LAB
55
+ python Baselines/colmap/colmap_to_vslamlab.py $sequence_path $exp_folder $exp_id $verbose $rgb_csv $camera_name
56
+
57
+ # Visualization with colmap gui
58
+ if [ "$verbose" -eq 1 ]; then
59
+ exp_folder_colmap="${exp_folder}/colmap_${exp_id}"
60
+ rgb_dir=$(awk -F, 'NR==2 { split($2,a,"/"); print a[1]; exit }' "$rgb_csv")
61
+ rgb_path="${sequence_path}/${rgb_dir}"
62
+ database="${exp_folder_colmap}/colmap_database.db"
63
+ colmap gui --import_path "${exp_folder_colmap}/0" --database_path ${database} --image_path ${rgb_path}
64
+ fi
65
+
66
+ # # Remove colmap data
67
+ # rm -rf ${exp_folder_colmap}
68
+
69
+
colmap_to_vslamlab.py CHANGED
@@ -1,89 +1,102 @@
1
- """
2
- Module: VSLAM-LAB - Baselines - colmap - colmap_to_vslamlab.py
3
- - Author: Alejandro Fontan Villacampa
4
- - Assisted by: Claude (Fable 5.1)
5
- - Version: 1.1
6
- - Created: 2024-07-12
7
- - Updated: 2026-10-03
8
- - License: GPLv3 License
9
-
10
- Writes <exp_folder>/<exp_id>_KeyFrameTrajectory.csv (VSLAM-LAB's TUM-style csv, camera-to-world
11
- poses) from the TXT model <exp_folder>/colmap_<exp_id>/images.txt, taking each image's timestamp
12
- from the experiment's rgb csv (COLMAP image ids are 1-based row indices of that csv, as the image
13
- list was written in csv order). Importable (vslamlab_colmap.py) and runnable with the positional
14
- arguments the old shell pipeline used.
15
- """
16
-
17
- import os
18
- import sys
19
- from pathlib import Path
20
-
21
  import numpy as np
22
- import pandas as pd
23
  from scipy.spatial.transform import Rotation as R
 
 
 
24
 
25
-
26
- def get_colmap_keyframes(images_file: str | Path, number_of_header_lines: int = 4) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
27
- """(image ids, t_wc, q_wc xyzw) sorted by image id, quaternion signs made continuous."""
28
  print(f"get_colmap_keyframes: {images_file}")
 
 
 
 
29
 
30
- image_id, q_wc_xyzw, t_wc = [], [], []
31
- with open(images_file, 'r') as file:
32
  for _ in range(number_of_header_lines):
33
  file.readline()
 
34
  while True:
35
  line1 = file.readline()
36
  if not line1:
37
  break
38
  elements = line1.split()
39
- image_id.append(int(elements[0]))
40
-
41
- qw, qx, qy, qz = (float(e) for e in elements[1:5])
42
- t_cw_i = np.array([float(e) for e in elements[5:8]])
43
- q_wc_i = R.from_quat([qx, qy, qz, qw]).inv()
44
- q_wc_xyzw.append(q_wc_i.as_quat())
45
- t_wc.append(-q_wc_i.as_matrix() @ t_cw_i)
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- file.readline() # POINTS2D line
 
 
 
 
48
 
49
- image_id, q_wc_xyzw, t_wc = np.array(image_id), np.array(q_wc_xyzw), np.array(t_wc)
50
- order = image_id.argsort()
51
- image_id, q_wc_xyzw, t_wc = image_id[order], q_wc_xyzw[order], t_wc[order]
 
52
 
53
- for i in range(1, len(q_wc_xyzw)):
54
- if np.dot(q_wc_xyzw[i - 1], q_wc_xyzw[i]) < 0:
55
- q_wc_xyzw[i] = -q_wc_xyzw[i]
56
- return image_id, t_wc, q_wc_xyzw
 
57
 
 
58
 
59
- def write_trajectory_tum_format(file_name: str | Path, image_ts: np.ndarray, t_wc: np.ndarray, q_wc_xyzw: np.ndarray) -> None:
60
  print(f"writeTrajectoryTUMformat: {file_name}")
 
61
  data = np.hstack((image_ts.reshape(-1, 1), t_wc, q_wc_xyzw))
62
  data = data[data[:, 0].argsort()]
 
63
  with open(file_name, 'w', newline='') as file:
64
  file.write('ts (ns),tx (m),ty (m),tz (m),qx,qy,qz,qw\n')
65
  for row in data:
66
  file.write(','.join(f'{x:.15f}' for x in row) + '\n')
67
 
 
 
 
 
 
 
 
68
 
69
- def get_timestamps(rgb_csv: str | Path, camera_name: str) -> list[float]:
70
- print(f"getTimestamps: {rgb_csv}")
71
- return pd.read_csv(rgb_csv)[f'ts_{camera_name} (ns)'].to_list()
72
-
 
 
73
 
74
- def colmap_to_vslamlab(exp_folder: str | Path, exp_id: str, rgb_csv: str | Path, camera_name: str) -> Path:
75
- images_file = Path(exp_folder) / f'colmap_{exp_id}' / 'images.txt'
76
- image_id, t_wc, q_wc_xyzw = get_colmap_keyframes(images_file)
77
 
78
- image_ts = np.array(get_timestamps(rgb_csv, camera_name))
79
- timestamps = np.array([float(image_ts[i - 1]) for i in image_id])
80
 
81
- trajectory_csv = Path(exp_folder) / f'{exp_id}_KeyFrameTrajectory.csv'
82
- write_trajectory_tum_format(trajectory_csv, timestamps, t_wc, q_wc_xyzw)
83
- return trajectory_csv
 
84
 
 
85
 
86
- if __name__ == "__main__":
87
- # positional: sequence_path exp_folder exp_id verbose rgb_csv camera_name (sequence_path/verbose unused)
88
- _, exp_folder_, exp_id_, _, rgb_csv_, camera_name_ = sys.argv[1:7]
89
- colmap_to_vslamlab(exp_folder_, exp_id_, rgb_csv_, camera_name_)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import numpy as np
 
2
  from scipy.spatial.transform import Rotation as R
3
+ import sys
4
+ import os
5
+ import pandas as pd
6
 
7
+ def get_colmap_keyframes(images_file, number_of_header_lines, verbose=False):
 
 
8
  print(f"get_colmap_keyframes: {images_file}")
9
+
10
+ image_id = []
11
+ q_wc_xyzw = []
12
+ t_wc = []
13
 
14
+ with open(f"{images_file}", 'r') as file:
15
+ # Skip the header lines
16
  for _ in range(number_of_header_lines):
17
  file.readline()
18
+
19
  while True:
20
  line1 = file.readline()
21
  if not line1:
22
  break
23
  elements = line1.split()
24
+
25
+ IMAGE_ID = int(elements[0])
26
+ image_id.append(IMAGE_ID)
27
+
28
+ QW = float(elements[1])
29
+ QX = float(elements[2])
30
+ QY = float(elements[3])
31
+ QZ = float(elements[4])
32
+
33
+ TX = float(elements[5])
34
+ TY = float(elements[6])
35
+ TZ = float(elements[7])
36
+
37
+ t_cw_i = np.array([TX, TY, TZ])
38
+ q_wc_i = R.from_quat([QX, QY, QZ, QW]).inv()
39
+ R_wc_i = q_wc_i.as_matrix()
40
+
41
+ q_wc_xyzw.append([q_wc_i.as_quat()[0], q_wc_i.as_quat()[1], q_wc_i.as_quat()[2], q_wc_i.as_quat()[3]])
42
+ t_wc.append(-R_wc_i @ t_cw_i)
43
 
44
+ file.readline()
45
+
46
+ image_id = np.array(image_id)
47
+ q_wc_xyzw = np.array(q_wc_xyzw)
48
+ t_wc = np.array(t_wc)
49
 
50
+ sorted_indices = image_id.argsort()
51
+ image_id = image_id[sorted_indices]
52
+ q_wc_xyzw = q_wc_xyzw[sorted_indices]
53
+ t_wc = t_wc[sorted_indices]
54
 
55
+ q_wc_xyzw_corrected = q_wc_xyzw.copy()
56
+ for i in range(1, len(q_wc_xyzw_corrected)):
57
+ dot_product = np.dot(q_wc_xyzw_corrected[i - 1], q_wc_xyzw_corrected[i])
58
+ if dot_product < 0:
59
+ q_wc_xyzw_corrected[i] = -q_wc_xyzw_corrected[i]
60
 
61
+ return image_id, t_wc, q_wc_xyzw_corrected
62
 
63
+ def write_trajectory_tum_format(file_name, image_ts, t_wc, q_wc_xyzw):
64
  print(f"writeTrajectoryTUMformat: {file_name}")
65
+
66
  data = np.hstack((image_ts.reshape(-1, 1), t_wc, q_wc_xyzw))
67
  data = data[data[:, 0].argsort()]
68
+
69
  with open(file_name, 'w', newline='') as file:
70
  file.write('ts (ns),tx (m),ty (m),tz (m),qx,qy,qz,qw\n')
71
  for row in data:
72
  file.write(','.join(f'{x:.15f}' for x in row) + '\n')
73
 
74
+ def get_timestamps(files_path, rgb_file, camera_name):
75
+ print(f"getTimestamps: {os.path.join(files_path, rgb_file)}")
76
+ df = pd.read_csv(rgb_file)
77
+ ts = df[f'ts_{camera_name} (ns)'].to_list()
78
+ return ts
79
+
80
+ if __name__ == "__main__":
81
 
82
+ sequence_path = sys.argv[1]
83
+ exp_folder = sys.argv[2]
84
+ exp_id = sys.argv[3]
85
+ verbose = bool(int(sys.argv[4]))
86
+ rgb_file = sys.argv[5]
87
+ camera_name = sys.argv[6]
88
 
89
+ images_file = os.path.join(exp_folder, f'colmap_{exp_id}', 'images.txt')
 
 
90
 
91
+ number_of_header_lines = 4
92
+ image_id, t_wc, q_wc_xyzw = get_colmap_keyframes(images_file, number_of_header_lines, verbose)
93
 
94
+ image_ts = np.array(get_timestamps(sequence_path, rgb_file, camera_name))
95
+ timestamps = []
96
+ for id in image_id:
97
+ timestamps.append(float(image_ts[id-1]))
98
 
99
+ timestamps = np.array(timestamps)
100
 
101
+ keyFrameTrajectory_txt = os.path.join(exp_folder, exp_id + '_KeyFrameTrajectory' + '.csv')
102
+ write_trajectory_tum_format(keyFrameTrajectory_txt, timestamps, t_wc, q_wc_xyzw)
 
 
colmap_utilities.py DELETED
@@ -1,114 +0,0 @@
1
- """
2
- Module: VSLAM-LAB - Baselines - colmap - colmap_utilities.py
3
- - Author: Alejandro Fontan Villacampa
4
- - Assisted by: Claude (Fable 5.1)
5
- - Version: 1.0
6
- - Created: 2026-10-03
7
- - Updated: 2026-10-03
8
- - License: GPLv3 License
9
-
10
- Helpers shared by the colmap pipeline stages (vslamlab_colmap.py, colmap_matcher.py,
11
- colmap_mapper.py): running colmap commands, GPU / thread detection, and mapping a VSLAM-LAB
12
- calibration onto a COLMAP camera model.
13
- """
14
-
15
- import os
16
- import subprocess
17
- import sys
18
- from pathlib import Path
19
-
20
- import pandas as pd
21
-
22
- from get_calibration import get_camera_intrinsics
23
-
24
- COLMAP_DIR = Path(__file__).resolve().parent
25
-
26
-
27
- def run(cmd: list[str], capture: bool = False) -> str:
28
- """Run a command, exiting with its return code if it fails. Returns the merged output when capture=True."""
29
- if capture:
30
- result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
31
- else:
32
- result = subprocess.run(cmd)
33
- if result.returncode != 0:
34
- print(f" ERROR: '{' '.join(cmd[:2])}' failed with return code {result.returncode}")
35
- if capture:
36
- print(result.stdout)
37
- sys.exit(result.returncode)
38
- return result.stdout if capture else ""
39
-
40
-
41
- def shell_output(cmd: list[str]) -> str:
42
- """stdout+stderr of a command, '' if it cannot run (e.g. no nvidia-smi)."""
43
- try:
44
- return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout
45
- except FileNotFoundError as e:
46
- return str(e)
47
-
48
-
49
- def indent(text: str, prefix: str) -> str:
50
- return "\n".join(prefix + line for line in text.rstrip("\n").split("\n"))
51
-
52
-
53
- def detect_gpus(use_gpu: int) -> tuple[str, int]:
54
- """GPU index list for COLMAP's gpu_index options and the CPU thread count.
55
-
56
- Extraction / matching run as a single job across all usable GPUs, since COLMAP spawns one
57
- worker per listed GPU index and splits the workload internally. When CUDA_VISIBLE_DEVICES is
58
- set (a scheduler restricted this job to a device set, often MIG slice UUIDs on HPC that
59
- nvidia-smi does not enumerate per job), CUDA remaps the listed devices to ordinals 0..N-1 inside
60
- the process and COLMAP's gpu_index is just cudaSetDevice(ordinal), so the ordinals are used
61
- rather than the real indices. The thread count comes from the scheduling affinity (what `nproc`
62
- reports) rather than COLMAP's num_threads=-1 auto-detection, which on cgroup-limited nodes often
63
- sees the whole node's cores.
64
- """
65
- print(" detecting GPUs ...")
66
- print(" nvidia-smi -L:")
67
- print(indent(shell_output(["nvidia-smi", "-L"]), " " * 12))
68
- print(" nvidia-smi --query-gpu=index,name,uuid --format=csv:")
69
- print(indent(shell_output(["nvidia-smi", "--query-gpu=index,name,uuid", "--format=csv"]), " " * 12))
70
- cuda_visible_devices = os.environ.get("CUDA_VISIBLE_DEVICES", "")
71
- print(f" CUDA_VISIBLE_DEVICES: {cuda_visible_devices}")
72
-
73
- if cuda_visible_devices:
74
- num_gpus = len([d for d in cuda_visible_devices.split(",") if d.strip()])
75
- gpu_ids = list(range(num_gpus))
76
- else:
77
- out = shell_output(["nvidia-smi", "--query-gpu=index", "--format=csv,noheader"])
78
- gpu_ids = [int(tok) for tok in out.split() if tok.isdigit()]
79
- if len(gpu_ids) < 1 or use_gpu == 0:
80
- gpu_ids = [0]
81
- gpu_index_list = ",".join(str(i) for i in gpu_ids)
82
- print(f" use_gpu: {use_gpu}")
83
- print(f" detected gpu_ids: {' '.join(str(i) for i in gpu_ids)}")
84
- print(f" gpu_index_list passed to colmap: {gpu_index_list}")
85
-
86
- num_threads = len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else (os.cpu_count() or 1)
87
- print(f" num_threads: {num_threads}")
88
- return gpu_index_list, num_threads
89
-
90
-
91
- def rgb_path_from_csv(sequence_path: Path, rgb_csv: Path, camera_name: str) -> Path:
92
- """Frame folder of camera_name as the csv names it: the run pipeline may point path_rgb_0 at a
93
- generated folder (refrax_0 for 'refraction: refrax') rather than rgb_0."""
94
- df = pd.read_csv(rgb_csv, nrows=1)
95
- rgb_dir = str(df[f"path_{camera_name}"].iloc[0]).split("/")[0]
96
- return sequence_path / rgb_dir
97
-
98
-
99
- def colmap_camera(calibration_yaml: Path, camera_name: str) -> tuple[str, str, str]:
100
- """(vslamlab calibration model, COLMAP camera model, comma-separated COLMAP params or '')."""
101
- calibration_model, params = get_camera_intrinsics(calibration_yaml, camera_name)
102
- params_csv = ",".join(str(p) for p in params)
103
- if calibration_model == "unknown":
104
- return calibration_model, "OPENCV", ""
105
- if calibration_model == "pinhole":
106
- return calibration_model, "PINHOLE", params_csv
107
- if calibration_model == "radtan4":
108
- return calibration_model, "OPENCV", params_csv
109
- if calibration_model == "radtan5":
110
- return calibration_model, "FULL_OPENCV", params_csv + ",0,0,0"
111
- if calibration_model == "equid4":
112
- return calibration_model, "OPENCV_FISHEYE", params_csv
113
- print(f"Unknown calibration_model: {calibration_model}")
114
- sys.exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
create_colmap_mask_dir.py DELETED
@@ -1,61 +0,0 @@
1
- """
2
- Module: VSLAM-LAB - Baselines - colmap - create_colmap_mask_dir.py
3
- - Author: Alejandro Fontan Villacampa
4
- - Assisted by: Claude (Fable 5)
5
- - Version: 1.0
6
- - Created: 2026-08-28
7
- - Updated: 2026-08-28
8
- - License: GPLv3 License
9
-
10
- Turns the path_mask_<i> column of an rgb_exp.csv (written by the run pipeline for
11
- 'segmentation: mask2former', 'refraction: refrax', or datasets that ship masks; 1 = usable pixel,
12
- 0 = masked out, which is COLMAP's own convention: no features where the mask is 0) into what
13
- colmap feature_extractor accepts, and prints one line the calling shell script evals:
14
- camera_mask:<png> every frame shares one mask (refrax's mask.png) -> --ImageReader.camera_mask_path
15
- mask_dir:<dir> per-frame masks -> --ImageReader.mask_path: <dir>/<image name>.png symlinks,
16
- one per frame (COLMAP looks masks up by image name + '.png')
17
- none the csv has no mask column for this camera (or a mask file is missing)
18
- """
19
-
20
- import argparse
21
- import os
22
- from pathlib import Path
23
-
24
- import pandas as pd
25
-
26
-
27
- def create_colmap_mask_dir(rgb_csv: str, camera_name: str, sequence_path: str, mask_dir: str) -> str:
28
- df = pd.read_csv(rgb_csv)
29
- cam_idx = camera_name.rsplit("_", 1)[-1]
30
- mask_col, path_col = f"path_mask_{cam_idx}", f"path_{camera_name}"
31
- if mask_col not in df.columns:
32
- print(f" no '{mask_col}' column in {os.path.basename(rgb_csv)}; extracting features without masks")
33
- return "none"
34
-
35
- masks = [Path(sequence_path) / p for p in df[mask_col]]
36
- missing = [m for m in masks if not m.exists()]
37
- if missing:
38
- print(f" {len(missing)}/{len(masks)} mask files missing (e.g. {missing[0]}); extracting features without masks")
39
- return "none"
40
-
41
- if len(set(masks)) == 1:
42
- return f"camera_mask:{masks[0].resolve()}"
43
-
44
- mask_dir = Path(mask_dir)
45
- mask_dir.mkdir(parents=True, exist_ok=True)
46
- for image, mask in zip(df[path_col], masks):
47
- link = mask_dir / f"{os.path.basename(image)}.png"
48
- if link.is_symlink() or link.exists():
49
- link.unlink()
50
- os.symlink(mask.resolve(), link)
51
- return f"mask_dir:{mask_dir.resolve()}"
52
-
53
-
54
- if __name__ == "__main__":
55
- parser = argparse.ArgumentParser()
56
- parser.add_argument("rgb_csv", help="Path to the experiment's rgb csv")
57
- parser.add_argument("camera_name", help="camera_name (e.g. rgb_0)")
58
- parser.add_argument("sequence_path", help="Sequence folder the csv paths are relative to")
59
- parser.add_argument("mask_dir", help="Where to build the per-frame mask directory if needed")
60
- args = parser.parse_args()
61
- print(create_colmap_mask_dir(args.rgb_csv, args.camera_name, args.sequence_path, args.mask_dir))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
get_calibration.py CHANGED
@@ -1,45 +1,33 @@
1
- """
2
- Module: VSLAM-LAB - Baselines - colmap - get_calibration.py
3
- - Author: Alejandro Fontan Villacampa
4
- - Assisted by: Claude (Fable 5.1)
5
- - Version: 1.1
6
- - Created: 2024-07-12
7
- - Updated: 2026-10-03
8
- - License: GPLv3 License
9
-
10
- Reads one camera of a VSLAM-LAB calibration yaml as (model, [fx, fy, cx, cy, *distortion]).
11
- The model is the distortion_type when the camera has distortion fields, else its cam_model
12
- (so 'pinhole' and 'unknown' come through unchanged). Importable (colmap_utilities.colmap_camera)
13
- and runnable: `python get_calibration.py <calibration_yaml> <camera_name>` prints the same
14
- fields space-separated on one line.
15
- """
16
-
17
- import argparse
18
- from pathlib import Path
19
-
20
  import yaml
 
 
 
21
 
22
-
23
- def get_camera_intrinsics(calibration_yaml: str | Path, cam_name: str) -> tuple[str, list[float]]:
24
  with open(calibration_yaml, 'r') as file:
25
  data = yaml.safe_load(file)
26
- cam = next(c for c in data.get('cameras', []) if c['cam_name'] == cam_name)
27
-
28
- fx, fy = cam['focal_length'][0], cam['focal_length'][1]
29
- cx, cy = cam['principal_point'][0], cam['principal_point'][1]
30
- params = [float(fx), float(fy), float(cx), float(cy)]
31
-
32
  has_dist = ('distortion_type' in cam) and ('distortion_coefficients' in cam)
 
 
 
 
33
  if has_dist:
34
- return cam['distortion_type'], params + [float(d) for d in cam['distortion_coefficients']]
35
- return cam['cam_model'], params
36
-
37
-
 
 
38
  if __name__ == "__main__":
39
  parser = argparse.ArgumentParser()
40
  parser.add_argument("calibration_yaml", help="Path to the calibration YAML")
41
  parser.add_argument("camera_name", help="camera_name")
42
  args = parser.parse_args()
43
-
44
- model, params = get_camera_intrinsics(args.calibration_yaml, args.camera_name)
45
- print(model, *params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import yaml
2
+ import sys
3
+ import argparse
4
+ import numpy as np
5
 
6
+ def get_camera_intrinsics(calibration_yaml, cam_name):
 
7
  with open(calibration_yaml, 'r') as file:
8
  data = yaml.safe_load(file)
9
+ cameras = data.get('cameras', [])
10
+ for cam_ in cameras:
11
+ if cam_['cam_name'] == cam_name:
12
+ cam = cam_;
13
+ break;
14
+
15
  has_dist = ('distortion_type' in cam) and ('distortion_coefficients' in cam)
16
+ K = np.array([[cam['focal_length'][0], 0, cam['principal_point'][0]],
17
+ [0, cam['focal_length'][1], cam['principal_point'][1]],
18
+ [0, 0, 1]], dtype=np.float32)
19
+
20
  if has_dist:
21
+ dist= " ".join(map(str, cam['distortion_coefficients']))
22
+ print(f"{cam['distortion_type']} {K[0,0]} {K[1,1]} {K[0,2]} {K[1,2]} {dist}")
23
+ else:
24
+ print(f"{cam['cam_model']} {K[0,0]} {K[1,1]} {K[0,2]} {K[1,2]}")
25
+
26
+
27
  if __name__ == "__main__":
28
  parser = argparse.ArgumentParser()
29
  parser.add_argument("calibration_yaml", help="Path to the calibration YAML")
30
  parser.add_argument("camera_name", help="camera_name")
31
  args = parser.parse_args()
32
+
33
+ get_camera_intrinsics(args.calibration_yaml, args.camera_name)
 
vslamlab_colmap.py DELETED
@@ -1,111 +0,0 @@
1
- """
2
- Module: VSLAM-LAB - Baselines - colmap - vslamlab_colmap.py
3
- - Author: Alejandro Fontan Villacampa
4
- - Assisted by: Claude (Fable 5.1)
5
- - Version: 2.0
6
- - Created: 2024-07-12
7
- - Updated: 2026-10-03
8
- - License: GPLv3 License
9
-
10
- VSLAM-LAB entry point for the colmap baseline (Python port of colmap_reconstruction.sh), run by
11
- the pixi task execute-mono with the arguments BaselineVSLAMLAB.build_execute_command passes for
12
- command_style 'python' (--key value). Stages, each in its own module:
13
- colmap_matcher.run_matcher database, feature extraction (masks optional), exhaustive / sequential matching
14
- colmap_mapper.run_mapper COLMAP incremental or GLOMAP global mapping, best sub-model, TXT export
15
- colmap_to_vslamlab <exp_folder>/<exp_id>_KeyFrameTrajectory.csv from images.txt
16
- With verbose=1 the best sub-model is opened in the colmap gui afterwards.
17
- """
18
-
19
- import argparse
20
- import os
21
- import shutil
22
- import sys
23
- from pathlib import Path
24
-
25
- sys.path.insert(0, str(Path(__file__).resolve().parent))
26
-
27
- from colmap_matcher import run_matcher # noqa: E402
28
- from colmap_mapper import run_mapper # noqa: E402
29
- from colmap_to_vslamlab import colmap_to_vslamlab # noqa: E402
30
- from colmap_utilities import rgb_path_from_csv, run # noqa: E402
31
-
32
-
33
- def parse_args() -> argparse.Namespace:
34
- parser = argparse.ArgumentParser(description="VSLAM-LAB colmap baseline")
35
- # Fixed per-run arguments (BaselineVSLAMLAB.build_execute_command)
36
- parser.add_argument("--sequence_path", type=Path, required=True)
37
- parser.add_argument("--calibration_yaml", type=Path, required=True)
38
- parser.add_argument("--rgb_csv", type=Path, required=True)
39
- parser.add_argument("--exp_folder", type=Path, required=True)
40
- parser.add_argument("--exp_it", type=int, required=True)
41
- parser.add_argument("--settings_yaml", type=Path, default=None)
42
- # Baseline parameters (baseline_colmap.py default_parameters, overridable per experiment)
43
- parser.add_argument("--verbose", type=int, default=0)
44
- parser.add_argument("--mode", type=str, default="mono")
45
- parser.add_argument("--matcher_type", type=str, default="exhaustive", choices=["exhaustive", "sequential"])
46
- parser.add_argument("--matching_type", type=str, default="sift_bruteforce")
47
- parser.add_argument("--mapper_type", type=str, default="colmap", choices=["colmap", "glomap"])
48
- parser.add_argument("--rgb_max", type=int, default=None, help="consumed by the run pipeline; accepted here because it is forwarded")
49
- parser.add_argument("--use_mask", type=int, default=0)
50
- parser.add_argument("--optimize_intrinsics", type=int, default=1)
51
- parser.add_argument("--dense", type=int, default=0)
52
- parser.add_argument("--dense_max_image_size", type=int, default=1600)
53
- parser.add_argument("--mesher", type=str, default="none")
54
- # Not exposed as baseline parameters
55
- parser.add_argument("--use_gpu", type=int, default=1)
56
- parser.add_argument("--camera_name", type=str, default="rgb_0")
57
- return parser.parse_args()
58
-
59
-
60
- def main() -> None:
61
- args = parse_args()
62
- exp_id = f"{args.exp_it:05d}"
63
-
64
- print("\n================= Experiment Configuration =================")
65
- print(f" Sequence Path : {args.sequence_path}")
66
- print(f" Experiment Folder : {args.exp_folder}")
67
- print(f" Experiment ID : {exp_id}")
68
- print(f" Verbose : {args.verbose}")
69
- print(f" Matcher Type : {args.matcher_type}")
70
- print(f" Matching Type : {args.matching_type}")
71
- print(f" Mapper Type : {args.mapper_type}")
72
- print(f" Use GPU : {args.use_gpu}")
73
- print(f" Use Mask : {args.use_mask}")
74
- print(f" Optimize Intrins. : {args.optimize_intrinsics}")
75
- print(f" Dense : {args.dense}")
76
- print(f" Dense Max Img Size: {args.dense_max_image_size}")
77
- print(f" Mesher : {args.mesher}")
78
- print(f" Settings YAML : {args.settings_yaml}")
79
- print(f" Calibration YAML : {args.calibration_yaml}")
80
- print(f" RGB CSV : {args.rgb_csv}")
81
- print(f" Camera Name : {args.camera_name}")
82
- print("============================================================")
83
-
84
- # Folder for the colmap files of this run
85
- exp_folder_colmap = args.exp_folder / f"colmap_{exp_id}"
86
- shutil.rmtree(exp_folder_colmap, ignore_errors=True)
87
- exp_folder_colmap.mkdir(parents=True)
88
-
89
- conda_prefix = os.environ.get("CONDA_PREFIX")
90
- if conda_prefix:
91
- os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = f"{conda_prefix}/plugins/platforms"
92
-
93
- rgb_path = rgb_path_from_csv(args.sequence_path, args.rgb_csv, args.camera_name)
94
-
95
- database = run_matcher(args.sequence_path, exp_folder_colmap, rgb_path, args.rgb_csv, args.calibration_yaml, args.camera_name,
96
- args.matcher_type, args.matching_type, args.use_gpu, args.use_mask)
97
- best_model = run_mapper(exp_folder_colmap, rgb_path, args.calibration_yaml, args.camera_name, args.mapper_type, args.optimize_intrinsics)
98
-
99
- # Convert COLMAP outputs to a format suitable for VSLAM-LAB
100
- colmap_to_vslamlab(args.exp_folder, exp_id, args.rgb_csv, args.camera_name)
101
-
102
- if args.dense == 1:
103
- print(f"\n WARNING: dense={args.dense} (mesher={args.mesher}) requested, but the dense stage is not wired yet; skipping")
104
-
105
- # Visualization with colmap gui
106
- if args.verbose == 1:
107
- run(["colmap", "gui", "--import_path", str(best_model), "--database_path", str(database), "--image_path", str(rgb_path)])
108
-
109
-
110
- if __name__ == "__main__":
111
- main()