Skip to content

Commit

Permalink
Switch to pycolmap.Reconstruction for model converter. Add colmap vis…
Browse files Browse the repository at this point in the history
…ualizer with the same backend (Open3D) (#121)
  • Loading branch information
B1ueber2y authored Jan 9, 2025
1 parent ebdd5b0 commit abb923e
Show file tree
Hide file tree
Showing 18 changed files with 258 additions and 119 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,20 @@ jobs:
- name: Run Python tests
run: python -c "import limap; print(limap.__version__)"

- name: Run tests
run: |
pytest -m ci_workflow tests
- name: Run localization test
run: |
python runners/tests/localization.py
- name: Run E2E tests
run: |
bash scripts/quickstart.sh
python runners/hypersim/fitnmerge.py --default_config_file cfgs/fitnmerge/default_cpu.yaml \
--output_dir outputs/quickstart_test --visualize 0
python runners/hypersim/triangulation.py --default_config_file cfgs/triangulation/default_fast.yaml \
--output_dir outputs/quickstart_test --triangulation.use_exhaustive_matcher --skip_exists --visualize 0
python runners/tests/localization.py
1 change: 0 additions & 1 deletion limap/base/bindings.cc
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,6 @@ void bind_line_linker(py::module &m) {
}

void bind_camera(py::module &m) {
// TODO: use pycolmap
py::enum_<colmap::CameraModelId> PyCameraModelId(m, "CameraModelId",
py::module_local());
PyCameraModelId.value("INVALID", colmap::CameraModelId::kInvalid);
Expand Down
6 changes: 3 additions & 3 deletions limap/pointsfm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from _limap._pointsfm import * # noqa: F403

from .colmap_reader import (
PyReadCOLMAP,
ReadPointTracks,
check_exists_colmap_model,
convert_colmap_to_imagecols,
)
from .colmap_sfm import (
run_colmap_sfm,
Expand All @@ -20,9 +20,9 @@
)

__all__ = [n for n in _pointsfm.__dict__ if not n.startswith("_")] + [
"PyReadCOLMAP",
"ReadPointTracks",
"check_exists_colmap_model",
"convert_colmap_to_imagecols",
"ReadPointTracks",
"run_colmap_sfm",
"run_colmap_sfm_with_known_poses",
"run_hloc_matches",
Expand Down
106 changes: 35 additions & 71 deletions limap/pointsfm/colmap_reader.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import os
import sys

import pycolmap
from _limap import _base
from pycolmap import logging

sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from hloc.utils.read_write_model import (
read_cameras_binary,
read_cameras_text,
read_images_binary,
read_images_text,
read_points3D_binary,
read_points3D_text,
)


def check_exists_colmap_model(model_path):
if (
Expand All @@ -29,29 +19,12 @@ def check_exists_colmap_model(model_path):
)


def ReadInfos(colmap_path, model_path="sparse", image_path="images"):
logging.info("Start loading COLMAP sparse reconstruction.")
model_path = os.path.join(colmap_path, model_path)
image_path = os.path.join(colmap_path, image_path)
if os.path.exists(os.path.join(model_path, "cameras.bin")):
fname_cameras = os.path.join(model_path, "cameras.bin")
fname_images = os.path.join(model_path, "images.bin")
colmap_cameras = read_cameras_binary(fname_cameras)
colmap_images = read_images_binary(fname_images)
elif os.path.exists(os.path.join(model_path, "cameras.txt")):
fname_cameras = os.path.join(model_path, "cameras.txt")
fname_images = os.path.join(model_path, "images.txt")
colmap_cameras = read_cameras_text(fname_cameras)
colmap_images = read_images_text(fname_images)
else:
raise ValueError(
f"Error! The model file does not exist at {model_path}"
)
logging.info(f"Reconstruction loaded. (n_images = {len(colmap_images)})")

def convert_colmap_to_imagecols(
reconstruction: pycolmap.Reconstruction, image_path=None
):
# read cameras
cameras = {}
for cam_id, colmap_cam in colmap_cameras.items():
for cam_id, colmap_cam in reconstruction.cameras.items():
cameras[cam_id] = _base.Camera(
colmap_cam.model,
colmap_cam.params,
Expand All @@ -61,60 +34,51 @@ def ReadInfos(colmap_path, model_path="sparse", image_path="images"):

# read images
camimages = {}
for img_id, colmap_image in colmap_images.items():
for img_id, colmap_image in reconstruction.images.items():
imname = colmap_image.name
cam_id = colmap_image.camera_id
pose = _base.CameraPose(colmap_image.qvec, colmap_image.tvec)
camimage = _base.CameraImage(
cam_id, pose, image_name=os.path.join(image_path, imname)
qvec_xyzw = colmap_image.cam_from_world.rotation.quat
qvec = [qvec_xyzw[3], qvec_xyzw[0], qvec_xyzw[1], qvec_xyzw[2]]
tvec = colmap_image.cam_from_world.translation
pose = _base.CameraPose(qvec, tvec)
image_name = (
imname if image_path is None else os.path.join(image_path, imname)
)
camimage = _base.CameraImage(cam_id, pose, image_name=image_name)
camimages[img_id] = camimage

# get image collection
imagecols = _base.ImageCollection(cameras, camimages)
return imagecols


def PyReadCOLMAP(colmap_path, model_path=None):
if model_path is not None:
model_path = os.path.join(colmap_path, model_path)
else:
model_path = colmap_path
if os.path.exists(os.path.join(model_path, "points3D.bin")):
fname_cameras = os.path.join(model_path, "cameras.bin")
fname_points = os.path.join(model_path, "points3D.bin")
fname_images = os.path.join(model_path, "images.bin")
colmap_cameras = read_cameras_binary(fname_cameras)
colmap_images = read_images_binary(fname_images)
colmap_points = read_points3D_binary(fname_points)
elif os.path.exists(os.path.join(model_path, "points3D.txt")):
fname_cameras = os.path.join(model_path, "cameras.txt")
fname_points = os.path.join(model_path, "points3D.txt")
fname_images = os.path.join(model_path, "images.txt")
colmap_cameras = read_cameras_text(fname_cameras)
colmap_images = read_images_text(fname_images)
colmap_points = read_points3D_text(fname_points)
else:
raise ValueError(
f"Error! The model file does not exist at {model_path}"
)
reconstruction = {}
reconstruction["cameras"] = colmap_cameras
reconstruction["images"] = colmap_images
reconstruction["points"] = colmap_points
return reconstruction
def ReadInfos(colmap_path, model_path="sparse", image_path="images"):
logging.info("Start loading COLMAP sparse reconstruction.")
model_path = os.path.join(colmap_path, model_path)
image_path = os.path.join(colmap_path, image_path)
reconstruction = pycolmap.Reconstruction(model_path)
logging.info(
f"Reconstruction loaded. (n_images = {reconstruction.num_images()}"
)
imagecols = convert_colmap_to_imagecols(
reconstruction, image_path=image_path
)
return imagecols


def ReadPointTracks(colmap_reconstruction):
def ReadPointTracks(reconstruction: pycolmap.Reconstruction):
pointtracks = {}
for point3d_id, p in colmap_reconstruction["points"].items():
p_image_ids, point2d_ids = p.image_ids, p.point2D_idxs
for point3d_id, p in reconstruction.points3D.items():
p_image_ids = []
point2d_ids = []
p2d_list = []
for p_img_id, point2d_id in zip(
p_image_ids.tolist(), point2d_ids.tolist()
):
for elem in p.track.elements:
p_image_ids.append(elem.image_id)
point2d_ids.append(elem.point2D_idx)
p2d_list.append(
colmap_reconstruction["images"][p_img_id].xys[point2d_id]
reconstruction.images[elem.image_id]
.points2D[elem.point2D_idx]
.xy
)
ptrack = _base.PointTrack(p.xyz, p_image_ids, point2d_ids, p2d_list)
pointtracks[point3d_id] = ptrack
Expand Down
5 changes: 1 addition & 4 deletions limap/pointsfm/colmap_sfm.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import copy
import os
import shutil
import sys
from pathlib import Path

import cv2
import pycolmap
from pycolmap import logging

sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from model_converter import convert_imagecols_to_colmap

import hloc.utils.database as database
import hloc.utils.read_write_model as colmap_utils
from limap.pointsfm.model_converter import convert_imagecols_to_colmap


def import_images_with_known_cameras(image_dir, database_path, imagecols):
Expand Down
48 changes: 20 additions & 28 deletions limap/pointsfm/model_converter.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,36 @@
import os
import sys

from limap.util.geometry import rotation_from_quaternion

sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from colmap_reader import PyReadCOLMAP
import pycolmap

import hloc.utils.read_write_model as colmap_utils


def convert_colmap_to_visualsfm(colmap_model_path, output_nvm_file):
reconstruction = PyReadCOLMAP(colmap_model_path)
colmap_cameras, colmap_images, colmap_points = (
reconstruction["cameras"],
reconstruction["images"],
reconstruction["points"],
)
reconstruction = pycolmap.Reconstruction(colmap_model_path)
with open(output_nvm_file, "w") as f:
f.write("NVM_V3\n\n")

# write images
f.write(f"{len(colmap_images)}\n")
f.write(f"{reconstruction.num_images()}\n")
map_image_id = dict()
for cnt, item in enumerate(colmap_images.items()):
for cnt, item in enumerate(reconstruction.images.items()):
img_id, colmap_image = item
map_image_id[img_id] = cnt
img_name = colmap_image.name
cam_id = colmap_image.camera_id
cam = colmap_cameras[cam_id]
if cam.model == "SIMPLE_PINHOLE":
cam = reconstruction.cameras[cam_id]
if cam.model == pycolmap.CameraModelId.SIMPLE_PINHOLE:
assert cam.params[1] == 0.5 * cam.width
assert cam.params[2] == 0.5 * cam.height
focal = cam.params[0]
k1 = 0.0
elif cam.model == "PINHOLE":
elif cam.model == pycolmap.CameraModelId.PINHOLE:
assert cam.params[0] == cam.params[1]
assert cam.params[2] == 0.5 * cam.width
assert cam.params[3] == 0.5 * cam.height
focal = cam.params[0]
k1 = 0.0
elif cam.model == "SIMPLE_RADIAL":
elif cam.model == pycolmap.CameraModelId.SIMPLE_RADIAL:
assert cam.params[1] == 0.5 * cam.width
assert cam.params[2] == 0.5 * cam.height
focal = cam.params[0]
Expand All @@ -48,33 +39,34 @@ def convert_colmap_to_visualsfm(colmap_model_path, output_nvm_file):
raise ValueError("Camera model not supported in VisualSfM.")
f.write(f"{img_name}\t")
f.write(f" {focal}")
qvec, tvec = colmap_image.qvec, colmap_image.tvec
R = rotation_from_quaternion(qvec)
center = -R.transpose() @ tvec
qvec_xyzw = colmap_image.cam_from_world.rotation.quat
qvec = [qvec_xyzw[3], qvec_xyzw[0], qvec_xyzw[1], qvec_xyzw[2]]
center = colmap_image.cam_from_world.inverse().translation
f.write(f" {qvec[0]} {qvec[1]} {qvec[2]} {qvec[3]}")
f.write(f" {center[0]} {center[1]} {center[2]}")
f.write(f" {k1} 0\n")
f.write("\n")

# write points
f.write(f"{len(colmap_points)}\n")
for _, point in colmap_points.items():
f.write(f"{reconstruction.num_points3D()}\n")
for _, point in reconstruction.points3D.items():
xyz = point.xyz
track = point.track
f.write(f"{xyz[0]} {xyz[1]} {xyz[2]}")
f.write(" 128 128 128") # dummy color
n_supports = len(point.image_ids)
f.write(f" {n_supports}")
for idx in range(n_supports):
img_id = point.image_ids[idx]
xy_id = point.point2D_idxs[idx]
f.write(f" {len(track.elements)}")
for _, elem in enumerate(track.elements):
img_id = elem.image_id
xy_id = elem.point2D_idx
img_index = map_image_id[img_id]
f.write(f" {img_index} {xy_id}")
xy = colmap_images[img_id].xys[xy_id]
xy = reconstruction.images[img_id].points2D[xy_id].xy
f.write(f" {xy[0]} {xy[1]}")
f.write("\n")


def convert_imagecols_to_colmap(imagecols, colmap_output_path):
# TODO: change to pycolmap.Reconstruction
### make folders
if not os.path.exists(colmap_output_path):
os.makedirs(colmap_output_path)
Expand Down
16 changes: 6 additions & 10 deletions limap/runners/functions_structures.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

import numpy as np
import pycolmap
from pycolmap import logging
from tqdm import tqdm

Expand Down Expand Up @@ -78,19 +79,14 @@ def compute_colmap_model_with_junctions(


def compute_2d_bipartites_from_colmap(
reconstruction, imagecols, all_2d_lines, cfg=None
reconstruction: pycolmap.Reconstruction, imagecols, all_2d_lines, cfg=None
):
if cfg is None:
cfg = dict()
all_bpt2ds = {}
cfg_bpt2d = structures.PL_Bipartite2dConfig(cfg)
colmap_cameras, colmap_images, colmap_points = (
reconstruction["cameras"],
reconstruction["images"],
reconstruction["points"],
)
logging.info("Start computing 2D bipartites...")
for img_id, colmap_image in tqdm(colmap_images.items()):
for img_id, colmap_image in tqdm(reconstruction.images.items()):
n_points = colmap_image.xys.shape[0]
indexes = np.arange(0, n_points)
xys = colmap_image.xys
Expand All @@ -100,8 +96,8 @@ def compute_2d_bipartites_from_colmap(
# resize xys if needed
cam_id = imagecols.camimage(img_id).cam_id
orig_size = (
colmap_cameras[cam_id].width,
colmap_cameras[cam_id].height,
reconstruction.cameras[cam_id].width,
reconstruction.cameras[cam_id].height,
)
cam = imagecols.cam(cam_id)
new_size = (cam.w(), cam.h())
Expand All @@ -117,6 +113,6 @@ def compute_2d_bipartites_from_colmap(
)
all_bpt2ds[img_id] = bpt2d
points = {}
for point3d_id, p in tqdm(colmap_points.items()):
for point3d_id, p in tqdm(reconstruction.points3D.items()):
points[point3d_id] = p.xyz
return all_bpt2ds, points
3 changes: 2 additions & 1 deletion limap/runners/line_triangulation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os

import pycolmap
from pycolmap import logging
from tqdm import tqdm

Expand Down Expand Up @@ -145,7 +146,7 @@ def line_triangulation(cfg, imagecols, neighbors=None, ranges=None):
colmap_model_path = cfg["triangulation"]["use_pointsfm"][
"colmap_folder"
]
reconstruction = pointsfm.PyReadCOLMAP(colmap_model_path)
reconstruction = pycolmap.Reconstruction(colmap_model_path)
all_bpt2ds, sfm_points = runners.compute_2d_bipartites_from_colmap(
reconstruction, imagecols, all_2d_lines, cfg["structures"]["bpt2d"]
)
Expand Down
2 changes: 2 additions & 0 deletions limap/visualize/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from .vis_utils import (
compute_robust_range_lines,
compute_robust_range_points,
draw_segments,
vis_vpresult,
visualize_line_track,
Expand All @@ -28,6 +29,7 @@
"open3d_vis_3d_lines",
"vis_vpresult",
"draw_segments",
"compute_robust_range_points",
"compute_robust_range_lines",
"visualize_line_track",
]
Loading

0 comments on commit abb923e

Please sign in to comment.