From f51c021630fcaeb7b1de1dabfd9530c6605e0f6f Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Tue, 7 Jan 2025 18:19:03 +0100 Subject: [PATCH 01/10] switch to pycolmap for the colmap IO --- .gitignore | 1 + limap/base/bindings.cc | 1 - limap/pointsfm/colmap_reader.py | 99 ++++++++------------------ limap/pointsfm/colmap_sfm.py | 4 +- limap/pointsfm/model_converter.py | 45 ++++++------ limap/runners/functions_structures.py | 16 ++--- pytest.ini | 4 ++ tests/base/test_linebase.py | 12 ++++ tests/pointsfm/test_colmap_reader.py | 23 ++++++ tests/pointsfm/test_model_converter.py | 14 ++++ tests/third-party/test_open3d.py | 8 +++ tests/third-party/test_pycolmap.py | 12 ++++ 12 files changed, 131 insertions(+), 108 deletions(-) create mode 100644 pytest.ini create mode 100644 tests/base/test_linebase.py create mode 100644 tests/pointsfm/test_colmap_reader.py create mode 100644 tests/pointsfm/test_model_converter.py create mode 100644 tests/third-party/test_open3d.py create mode 100644 tests/third-party/test_pycolmap.py diff --git a/.gitignore b/.gitignore index da7295b0..5a8e63f6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /dist/ cmake-build-* *.egg-info/ +src/hloc *.npy *.png diff --git a/limap/base/bindings.cc b/limap/base/bindings.cc index 4d40068d..102d6a8f 100644 --- a/limap/base/bindings.cc +++ b/limap/base/bindings.cc @@ -678,7 +678,6 @@ void bind_line_linker(py::module &m) { } void bind_camera(py::module &m) { - // TODO: use pycolmap py::enum_ PyCameraModelId(m, "CameraModelId", py::module_local()); PyCameraModelId.value("INVALID", colmap::CameraModelId::kInvalid); diff --git a/limap/pointsfm/colmap_reader.py b/limap/pointsfm/colmap_reader.py index 9f896c4d..1eb7bf85 100644 --- a/limap/pointsfm/colmap_reader.py +++ b/limap/pointsfm/colmap_reader.py @@ -2,19 +2,9 @@ import sys from _limap import _base +import pycolmap 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 ( os.path.exists(os.path.join(model_path, "cameras.bin")) @@ -28,30 +18,10 @@ def check_exists_colmap_model(model_path): and os.path.exists(os.path.join(model_path, "points3D.txt")) ) - -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, @@ -61,13 +31,15 @@ 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 @@ -75,47 +47,34 @@ def ReadInfos(colmap_path, model_path="sparse", image_path="images"): return imagecols +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 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 + return pycolmap.Reconstruction(model_path) -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() - ): - p2d_list.append( - colmap_reconstruction["images"][p_img_id].xys[point2d_id] - ) + for elem in p.track.elements: + p_image_ids.append(elem.image_id) + point2d_ids.append(elem.point2D_idx) + p2d_list.append(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 return pointtracks diff --git a/limap/pointsfm/colmap_sfm.py b/limap/pointsfm/colmap_sfm.py index b1562f52..1232e2b8 100644 --- a/limap/pointsfm/colmap_sfm.py +++ b/limap/pointsfm/colmap_sfm.py @@ -8,10 +8,10 @@ import pycolmap from pycolmap import logging -sys.path.append(os.path.dirname(os.path.abspath(__file__))) import hloc.utils.database as database import hloc.utils.read_write_model as colmap_utils -from model_converter import convert_imagecols_to_colmap + +from limap.pointsfm.model_converter import convert_imagecols_to_colmap def import_images_with_known_cameras(image_dir, database_path, imagecols): diff --git a/limap/pointsfm/model_converter.py b/limap/pointsfm/model_converter.py index e13524ad..9b3f2f8a 100644 --- a/limap/pointsfm/model_converter.py +++ b/limap/pointsfm/model_converter.py @@ -1,44 +1,38 @@ import os -import sys +import pycolmap from limap.util.geometry import rotation_from_quaternion -sys.path.append(os.path.dirname(os.path.abspath(__file__))) import hloc.utils.read_write_model as colmap_utils -from colmap_reader import PyReadCOLMAP +from limap.pointsfm.colmap_reader import PyReadCOLMAP 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"], - ) 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] @@ -47,33 +41,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 idx, 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) diff --git a/limap/runners/functions_structures.py b/limap/runners/functions_structures.py index 669dbeb8..96e8c506 100644 --- a/limap/runners/functions_structures.py +++ b/limap/runners/functions_structures.py @@ -1,6 +1,7 @@ import os import numpy as np +import pycolmap from pycolmap import logging from tqdm import tqdm @@ -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 @@ -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()) @@ -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 diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..784ba86d --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +markers = + ci_workflow: mark a test as part of the CI workflow + diff --git a/tests/base/test_linebase.py b/tests/base/test_linebase.py new file mode 100644 index 00000000..f11163f3 --- /dev/null +++ b/tests/base/test_linebase.py @@ -0,0 +1,12 @@ +import pytest + +import limap +import numpy as np +import numpy.testing as npt + +@pytest.mark.ci_workflow +def test_line2d(): + line = limap.base.Line2d(np.array([0.0, 0.0]), np.array([1.0, 1.0])) + npt.assert_allclose(line.length(), np.sqrt(2), rtol=1e-5, atol=1e-8) + npt.assert_allclose(line.direction(), np.array([1., 1.]) / line.length(), rtol=1e-5, atol=1e-8) + diff --git a/tests/pointsfm/test_colmap_reader.py b/tests/pointsfm/test_colmap_reader.py new file mode 100644 index 00000000..7bd4904d --- /dev/null +++ b/tests/pointsfm/test_colmap_reader.py @@ -0,0 +1,23 @@ +import pytest +import pycolmap +from limap.pointsfm.colmap_reader import ReadInfos, ReadPointTracks +import numpy.testing as npt + +@pytest.mark.ci_workflow +def test_convert_colmap_to_imagecols(tmp_path): + syn_options = pycolmap.SyntheticDatasetOptions() + recon = pycolmap.synthesize_dataset(syn_options) + recon.write(tmp_path) + imagecols = ReadInfos(tmp_path, model_path = ".") + assert imagecols.NumCameras() == recon.num_cameras() + assert imagecols.NumImages() == recon.num_images() + npt.assert_allclose(recon.images[1].cam_from_world.inverse().translation, imagecols.camimage(1).pose.center(), rtol=1e-5, atol=1e-8) + + +@pytest.mark.ci_workflow +def test_read_point_tracks(tmp_path): + syn_options = pycolmap.SyntheticDatasetOptions() + recon = pycolmap.synthesize_dataset(syn_options) + point_tracks = ReadPointTracks(recon) + assert len(point_tracks) == recon.num_points3D() + diff --git a/tests/pointsfm/test_model_converter.py b/tests/pointsfm/test_model_converter.py new file mode 100644 index 00000000..9867e482 --- /dev/null +++ b/tests/pointsfm/test_model_converter.py @@ -0,0 +1,14 @@ +import pytest +import pycolmap +from limap.pointsfm.model_converter import convert_colmap_to_visualsfm +from limap.pointsfm.visualsfm_reader import ReadModelVisualSfM +import numpy.testing as npt + +@pytest.mark.ci_workflow +def test_convert_colmap_to_visualsfm(tmp_path): + syn_options = pycolmap.SyntheticDatasetOptions() + recon = pycolmap.synthesize_dataset(syn_options) + recon.write(tmp_path) + output_nvm_file = tmp_path / "test_output.nvm" + convert_colmap_to_visualsfm(tmp_path, output_nvm_file) + diff --git a/tests/third-party/test_open3d.py b/tests/third-party/test_open3d.py new file mode 100644 index 00000000..081bc1bf --- /dev/null +++ b/tests/third-party/test_open3d.py @@ -0,0 +1,8 @@ +import pytest + +import numpy as np +import open3d as o3d + +@pytest.mark.ci_workflow +def test_open3d(): + o3d.geometry.LineSet.create_camera_visualization(500, 500, np.array([[500., 0., 250.], [0., 500., 250.], [0., 0., 1.]]), np.eye(4), scale=1.0) diff --git a/tests/third-party/test_pycolmap.py b/tests/third-party/test_pycolmap.py new file mode 100644 index 00000000..46eb3b12 --- /dev/null +++ b/tests/third-party/test_pycolmap.py @@ -0,0 +1,12 @@ +import pytest + +import pycolmap + +@pytest.mark.ci_workflow +def test_pycolmap(): + syn_options = pycolmap.SyntheticDatasetOptions(num_cameras=3, num_images=8, num_points3D=100) + recon = pycolmap.synthesize_dataset(syn_options) + assert recon.num_cameras() == 3 + assert recon.num_images() == 8 + assert recon.num_points3D() == 100 + From 2e54f8054dec3e7302389824b5f2568c15efd0ac Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Tue, 7 Jan 2025 18:20:53 +0100 Subject: [PATCH 02/10] deprecate PyReadCOLMAP --- limap/pointsfm/__init__.py | 2 -- limap/pointsfm/colmap_reader.py | 8 -------- limap/pointsfm/model_converter.py | 4 +--- limap/runners/line_triangulation.py | 3 ++- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/limap/pointsfm/__init__.py b/limap/pointsfm/__init__.py index 82d55b7d..946b9744 100644 --- a/limap/pointsfm/__init__.py +++ b/limap/pointsfm/__init__.py @@ -2,7 +2,6 @@ from _limap._pointsfm import * # noqa: F403 from .colmap_reader import ( - PyReadCOLMAP, ReadPointTracks, check_exists_colmap_model, ) @@ -20,7 +19,6 @@ ) __all__ = [n for n in _pointsfm.__dict__ if not n.startswith("_")] + [ - "PyReadCOLMAP", "ReadPointTracks", "check_exists_colmap_model", "run_colmap_sfm", diff --git a/limap/pointsfm/colmap_reader.py b/limap/pointsfm/colmap_reader.py index 1eb7bf85..f7ed7b30 100644 --- a/limap/pointsfm/colmap_reader.py +++ b/limap/pointsfm/colmap_reader.py @@ -57,14 +57,6 @@ def ReadInfos(colmap_path, model_path="sparse", image_path="images"): 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 - return pycolmap.Reconstruction(model_path) - - def ReadPointTracks(reconstruction: pycolmap.Reconstruction): pointtracks = {} for point3d_id, p in reconstruction.points3D.items(): diff --git a/limap/pointsfm/model_converter.py b/limap/pointsfm/model_converter.py index 9b3f2f8a..836e66de 100644 --- a/limap/pointsfm/model_converter.py +++ b/limap/pointsfm/model_converter.py @@ -4,11 +4,9 @@ from limap.util.geometry import rotation_from_quaternion import hloc.utils.read_write_model as colmap_utils -from limap.pointsfm.colmap_reader import PyReadCOLMAP - def convert_colmap_to_visualsfm(colmap_model_path, output_nvm_file): - reconstruction = PyReadCOLMAP(colmap_model_path) + reconstruction = pycolmap.Reconstruction(colmap_model_path) with open(output_nvm_file, "w") as f: f.write("NVM_V3\n\n") diff --git a/limap/runners/line_triangulation.py b/limap/runners/line_triangulation.py index e1a0349d..8bb9b8f6 100644 --- a/limap/runners/line_triangulation.py +++ b/limap/runners/line_triangulation.py @@ -1,5 +1,6 @@ import os +import pycolmap from pycolmap import logging from tqdm import tqdm @@ -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"] ) From 4df2fa4ddcb757fef3e714ae4297b19ebd37a9a3 Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Tue, 7 Jan 2025 18:35:24 +0100 Subject: [PATCH 03/10] add colmap visualizer support --- limap/pointsfm/__init__.py | 6 ++-- limap/visualize/__init__.py | 2 ++ limap/visualize/vis_utils.py | 21 ++++++++++++ visualize_colmap_model.py | 66 ++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 visualize_colmap_model.py diff --git a/limap/pointsfm/__init__.py b/limap/pointsfm/__init__.py index 946b9744..61582160 100644 --- a/limap/pointsfm/__init__.py +++ b/limap/pointsfm/__init__.py @@ -2,8 +2,9 @@ from _limap._pointsfm import * # noqa: F403 from .colmap_reader import ( - ReadPointTracks, check_exists_colmap_model, + convert_colmap_to_imagecols, + ReadPointTracks, ) from .colmap_sfm import ( run_colmap_sfm, @@ -19,8 +20,9 @@ ) __all__ = [n for n in _pointsfm.__dict__ if not n.startswith("_")] + [ - "ReadPointTracks", "check_exists_colmap_model", + "convert_colmap_to_imagecols", + "ReadPointTracks", "run_colmap_sfm", "run_colmap_sfm_with_known_poses", "run_hloc_matches", diff --git a/limap/visualize/__init__.py b/limap/visualize/__init__.py index 3a87bae1..a0cf12f0 100644 --- a/limap/visualize/__init__.py +++ b/limap/visualize/__init__.py @@ -11,6 +11,7 @@ open3d_vis_3d_lines, ) from .vis_utils import ( + compute_robust_range_points, compute_robust_range_lines, draw_segments, vis_vpresult, @@ -28,6 +29,7 @@ "open3d_vis_3d_lines", "vis_vpresult", "draw_segments", + "compute_robust_range_points", "compute_robust_range_lines", "visualize_line_track", ] diff --git a/limap/visualize/vis_utils.py b/limap/visualize/vis_utils.py index 142a495a..f6e14a10 100644 --- a/limap/visualize/vis_utils.py +++ b/limap/visualize/vis_utils.py @@ -334,6 +334,27 @@ def compute_robust_range(arr, range_robust=None, k_stretch=2.0): return start_stretched, end_stretched +def compute_robust_range_points(points, range_robust=None, k_stretch=2.0): + if range_robust is None: + range_robust = [0.05, 0.95] + points_array = np.array(points) + x_array = points_array.reshape(-1, 3)[:, 0] + y_array = points_array.reshape(-1, 3)[:, 1] + z_array = points_array.reshape(-1, 3)[:, 2] + + x_start, x_end = compute_robust_range( + x_array, range_robust=range_robust, k_stretch=k_stretch + ) + y_start, y_end = compute_robust_range( + y_array, range_robust=range_robust, k_stretch=k_stretch + ) + z_start, z_end = compute_robust_range( + z_array, range_robust=range_robust, k_stretch=k_stretch + ) + ranges = np.array([[x_start, y_start, z_start], [x_end, y_end, z_end]]) + return ranges + + def compute_robust_range_lines(lines, range_robust=None, k_stretch=2.0): if range_robust is None: range_robust = [0.05, 0.95] diff --git a/visualize_colmap_model.py b/visualize_colmap_model.py new file mode 100644 index 00000000..92fac923 --- /dev/null +++ b/visualize_colmap_model.py @@ -0,0 +1,66 @@ +import pycolmap +import numpy as np +import open3d as o3d +import limap.pointsfm as pointsfm +import limap.visualize as limapvis + + +def parse_args(): + import argparse + + arg_parser = argparse.ArgumentParser(description="visualize colmap model using Open3D backend") + arg_parser.add_argument( + "-i", + "--input_dir", + type=str, + required=True, + help="input colmap folder" + ) + arg_parser.add_argument( + "--use_robust_ranges", + action="store_true", + help="whether to use computed robust ranges", + ) + arg_parser.add_argument( + "--scale", + type=float, + default=1.0, + help="scaling both the lines and the camera geometry", + ) + arg_parser.add_argument( + "--cam_scale", + type=float, + default=1.0, + help="scale of the camera geometry", + ) + args = arg_parser.parse_args() + return args + +def vis_colmap_reconstruction(recon: pycolmap.Reconstruction, ranges=None): + vis = o3d.visualization.Visualizer() + vis.create_window(height=1080, width=1920) + points = np.array([point.xyz for _, point in recon.points3D.items()]) + pcd = limapvis.open3d_get_points(points, ranges=ranges) + vis.add_geometry(pcd) + imagecols = pointsfm.convert_colmap_to_imagecols(recon) + camera_set = limapvis.open3d_get_cameras( + imagecols, + ranges=ranges, + scale_cam_geometry=args.scale * args.cam_scale, + scale=args.scale, + ) + vis.add_geometry(camera_set) + vis.run() + vis.destroy_window() + +def main(args): + recon = pycolmap.Reconstruction(args.input_dir) + ranges = None + if args.use_robust_ranges: + points = np.array([point.xyz for _, point in recon.points3D.items()]) + ranges = limapvis.compute_robust_range_points(points) + vis_colmap_reconstruction(recon, ranges=ranges) + +if __name__ == "__main__": + args = parse_args() + main(args) From 503cce4bfba521f89789b6e57c6581c4c293c780 Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Tue, 7 Jan 2025 18:39:01 +0100 Subject: [PATCH 04/10] formatting --- limap/pointsfm/__init__.py | 2 +- limap/pointsfm/colmap_reader.py | 27 +++++++++++++++++++------- limap/pointsfm/colmap_sfm.py | 2 -- limap/pointsfm/model_converter.py | 6 +++--- limap/visualize/__init__.py | 2 +- tests/base/test_linebase.py | 13 +++++++++---- tests/pointsfm/test_colmap_reader.py | 16 ++++++++++----- tests/pointsfm/test_model_converter.py | 7 +++---- tests/third-party/test_open3d.py | 12 +++++++++--- tests/third-party/test_pycolmap.py | 7 ++++--- visualize_colmap_model.py | 16 ++++++++------- 11 files changed, 70 insertions(+), 40 deletions(-) diff --git a/limap/pointsfm/__init__.py b/limap/pointsfm/__init__.py index 61582160..d75376b2 100644 --- a/limap/pointsfm/__init__.py +++ b/limap/pointsfm/__init__.py @@ -2,9 +2,9 @@ from _limap._pointsfm import * # noqa: F403 from .colmap_reader import ( + ReadPointTracks, check_exists_colmap_model, convert_colmap_to_imagecols, - ReadPointTracks, ) from .colmap_sfm import ( run_colmap_sfm, diff --git a/limap/pointsfm/colmap_reader.py b/limap/pointsfm/colmap_reader.py index f7ed7b30..b02d5535 100644 --- a/limap/pointsfm/colmap_reader.py +++ b/limap/pointsfm/colmap_reader.py @@ -1,10 +1,10 @@ import os -import sys -from _limap import _base import pycolmap +from _limap import _base from pycolmap import logging + def check_exists_colmap_model(model_path): if ( os.path.exists(os.path.join(model_path, "cameras.bin")) @@ -18,7 +18,10 @@ def check_exists_colmap_model(model_path): and os.path.exists(os.path.join(model_path, "points3D.txt")) ) -def convert_colmap_to_imagecols(reconstruction: pycolmap.Reconstruction, image_path=None): + +def convert_colmap_to_imagecols( + reconstruction: pycolmap.Reconstruction, image_path=None +): # read cameras cameras = {} for cam_id, colmap_cam in reconstruction.cameras.items(): @@ -38,7 +41,9 @@ def convert_colmap_to_imagecols(reconstruction: pycolmap.Reconstruction, image_p 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) + 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 @@ -52,8 +57,12 @@ def ReadInfos(colmap_path, model_path="sparse", image_path="images"): 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) + logging.info( + f"Reconstruction loaded. (n_images = {reconstruction.num_images()}" + ) + imagecols = convert_colmap_to_imagecols( + reconstruction, image_path=image_path + ) return imagecols @@ -66,7 +75,11 @@ def ReadPointTracks(reconstruction: pycolmap.Reconstruction): for elem in p.track.elements: p_image_ids.append(elem.image_id) point2d_ids.append(elem.point2D_idx) - p2d_list.append(reconstruction.images[elem.image_id].points2D[elem.point2D_idx].xy) + p2d_list.append( + 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 return pointtracks diff --git a/limap/pointsfm/colmap_sfm.py b/limap/pointsfm/colmap_sfm.py index 1232e2b8..3ddca072 100644 --- a/limap/pointsfm/colmap_sfm.py +++ b/limap/pointsfm/colmap_sfm.py @@ -1,7 +1,6 @@ import copy import os import shutil -import sys from pathlib import Path import cv2 @@ -10,7 +9,6 @@ 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 diff --git a/limap/pointsfm/model_converter.py b/limap/pointsfm/model_converter.py index 836e66de..3090c2c3 100644 --- a/limap/pointsfm/model_converter.py +++ b/limap/pointsfm/model_converter.py @@ -1,10 +1,10 @@ import os -import pycolmap -from limap.util.geometry import rotation_from_quaternion +import pycolmap import hloc.utils.read_write_model as colmap_utils + def convert_colmap_to_visualsfm(colmap_model_path, output_nvm_file): reconstruction = pycolmap.Reconstruction(colmap_model_path) with open(output_nvm_file, "w") as f: @@ -55,7 +55,7 @@ def convert_colmap_to_visualsfm(colmap_model_path, output_nvm_file): f.write(f"{xyz[0]} {xyz[1]} {xyz[2]}") f.write(" 128 128 128") # dummy color f.write(f" {len(track.elements)}") - for idx, elem in enumerate(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] diff --git a/limap/visualize/__init__.py b/limap/visualize/__init__.py index a0cf12f0..928caa5c 100644 --- a/limap/visualize/__init__.py +++ b/limap/visualize/__init__.py @@ -11,8 +11,8 @@ open3d_vis_3d_lines, ) from .vis_utils import ( - compute_robust_range_points, compute_robust_range_lines, + compute_robust_range_points, draw_segments, vis_vpresult, visualize_line_track, diff --git a/tests/base/test_linebase.py b/tests/base/test_linebase.py index f11163f3..68a8d167 100644 --- a/tests/base/test_linebase.py +++ b/tests/base/test_linebase.py @@ -1,12 +1,17 @@ +import numpy as np +import numpy.testing as npt import pytest import limap -import numpy as np -import numpy.testing as npt + @pytest.mark.ci_workflow def test_line2d(): line = limap.base.Line2d(np.array([0.0, 0.0]), np.array([1.0, 1.0])) npt.assert_allclose(line.length(), np.sqrt(2), rtol=1e-5, atol=1e-8) - npt.assert_allclose(line.direction(), np.array([1., 1.]) / line.length(), rtol=1e-5, atol=1e-8) - + npt.assert_allclose( + line.direction(), + np.array([1.0, 1.0]) / line.length(), + rtol=1e-5, + atol=1e-8, + ) diff --git a/tests/pointsfm/test_colmap_reader.py b/tests/pointsfm/test_colmap_reader.py index 7bd4904d..02ff0e71 100644 --- a/tests/pointsfm/test_colmap_reader.py +++ b/tests/pointsfm/test_colmap_reader.py @@ -1,17 +1,24 @@ -import pytest +import numpy.testing as npt import pycolmap +import pytest + from limap.pointsfm.colmap_reader import ReadInfos, ReadPointTracks -import numpy.testing as npt + @pytest.mark.ci_workflow def test_convert_colmap_to_imagecols(tmp_path): syn_options = pycolmap.SyntheticDatasetOptions() recon = pycolmap.synthesize_dataset(syn_options) recon.write(tmp_path) - imagecols = ReadInfos(tmp_path, model_path = ".") + imagecols = ReadInfos(tmp_path, model_path=".") assert imagecols.NumCameras() == recon.num_cameras() assert imagecols.NumImages() == recon.num_images() - npt.assert_allclose(recon.images[1].cam_from_world.inverse().translation, imagecols.camimage(1).pose.center(), rtol=1e-5, atol=1e-8) + npt.assert_allclose( + recon.images[1].cam_from_world.inverse().translation, + imagecols.camimage(1).pose.center(), + rtol=1e-5, + atol=1e-8, + ) @pytest.mark.ci_workflow @@ -20,4 +27,3 @@ def test_read_point_tracks(tmp_path): recon = pycolmap.synthesize_dataset(syn_options) point_tracks = ReadPointTracks(recon) assert len(point_tracks) == recon.num_points3D() - diff --git a/tests/pointsfm/test_model_converter.py b/tests/pointsfm/test_model_converter.py index 9867e482..8a274acf 100644 --- a/tests/pointsfm/test_model_converter.py +++ b/tests/pointsfm/test_model_converter.py @@ -1,8 +1,8 @@ -import pytest import pycolmap +import pytest + from limap.pointsfm.model_converter import convert_colmap_to_visualsfm -from limap.pointsfm.visualsfm_reader import ReadModelVisualSfM -import numpy.testing as npt + @pytest.mark.ci_workflow def test_convert_colmap_to_visualsfm(tmp_path): @@ -11,4 +11,3 @@ def test_convert_colmap_to_visualsfm(tmp_path): recon.write(tmp_path) output_nvm_file = tmp_path / "test_output.nvm" convert_colmap_to_visualsfm(tmp_path, output_nvm_file) - diff --git a/tests/third-party/test_open3d.py b/tests/third-party/test_open3d.py index 081bc1bf..d5903ca2 100644 --- a/tests/third-party/test_open3d.py +++ b/tests/third-party/test_open3d.py @@ -1,8 +1,14 @@ -import pytest - import numpy as np import open3d as o3d +import pytest + @pytest.mark.ci_workflow def test_open3d(): - o3d.geometry.LineSet.create_camera_visualization(500, 500, np.array([[500., 0., 250.], [0., 500., 250.], [0., 0., 1.]]), np.eye(4), scale=1.0) + o3d.geometry.LineSet.create_camera_visualization( + 500, + 500, + np.array([[500.0, 0.0, 250.0], [0.0, 500.0, 250.0], [0.0, 0.0, 1.0]]), + np.eye(4), + scale=1.0, + ) diff --git a/tests/third-party/test_pycolmap.py b/tests/third-party/test_pycolmap.py index 46eb3b12..39f18bba 100644 --- a/tests/third-party/test_pycolmap.py +++ b/tests/third-party/test_pycolmap.py @@ -1,12 +1,13 @@ +import pycolmap import pytest -import pycolmap @pytest.mark.ci_workflow def test_pycolmap(): - syn_options = pycolmap.SyntheticDatasetOptions(num_cameras=3, num_images=8, num_points3D=100) + syn_options = pycolmap.SyntheticDatasetOptions( + num_cameras=3, num_images=8, num_points3D=100 + ) recon = pycolmap.synthesize_dataset(syn_options) assert recon.num_cameras() == 3 assert recon.num_images() == 8 assert recon.num_points3D() == 100 - diff --git a/visualize_colmap_model.py b/visualize_colmap_model.py index 92fac923..dcff7dc5 100644 --- a/visualize_colmap_model.py +++ b/visualize_colmap_model.py @@ -1,6 +1,7 @@ -import pycolmap import numpy as np import open3d as o3d +import pycolmap + import limap.pointsfm as pointsfm import limap.visualize as limapvis @@ -8,13 +9,11 @@ def parse_args(): import argparse - arg_parser = argparse.ArgumentParser(description="visualize colmap model using Open3D backend") + arg_parser = argparse.ArgumentParser( + description="visualize colmap model using Open3D backend" + ) arg_parser.add_argument( - "-i", - "--input_dir", - type=str, - required=True, - help="input colmap folder" + "-i", "--input_dir", type=str, required=True, help="input colmap folder" ) arg_parser.add_argument( "--use_robust_ranges", @@ -36,6 +35,7 @@ def parse_args(): args = arg_parser.parse_args() return args + def vis_colmap_reconstruction(recon: pycolmap.Reconstruction, ranges=None): vis = o3d.visualization.Visualizer() vis.create_window(height=1080, width=1920) @@ -53,6 +53,7 @@ def vis_colmap_reconstruction(recon: pycolmap.Reconstruction, ranges=None): vis.run() vis.destroy_window() + def main(args): recon = pycolmap.Reconstruction(args.input_dir) ranges = None @@ -61,6 +62,7 @@ def main(args): ranges = limapvis.compute_robust_range_points(points) vis_colmap_reconstruction(recon, ranges=ranges) + if __name__ == "__main__": args = parse_args() main(args) From 4cb3fa3ddddb0cc9ac71eca3c1dae86323eabed2 Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Tue, 7 Jan 2025 18:46:53 +0100 Subject: [PATCH 05/10] upgrade ruff to 0.8.6 --- limap/features/models/s2dnet.py | 5 ++--- limap/fitting/fitting.py | 1 + limap/point2d/superglue/superglue.py | 11 +++++------ limap/point2d/superpoint/main.py | 11 ++++++----- limap/pointsfm/colmap_sfm.py | 3 ++- limap/pointsfm/model_converter.py | 3 ++- limap/runners/hybrid_localization.py | 2 +- runners/7scenes/localization.py | 4 ++-- runners/7scenes/utils.py | 10 +++++----- runners/cambridge/localization.py | 2 +- runners/cambridge/utils.py | 10 +++++----- runners/inloc/utils.py | 4 ++-- scripts/format/python.sh | 2 +- 13 files changed, 35 insertions(+), 33 deletions(-) diff --git a/limap/features/models/s2dnet.py b/limap/features/models/s2dnet.py index 2788234b..ecaddaeb 100644 --- a/limap/features/models/s2dnet.py +++ b/limap/features/models/s2dnet.py @@ -1,6 +1,5 @@ import os from pathlib import Path -from typing import List import numpy as np import torch @@ -64,7 +63,7 @@ def print_gpu_memory(): class AdapLayers(nn.Module): """Small adaptation layers.""" - def __init__(self, hypercolumn_layers: List[str], output_dim: int = 128): + def __init__(self, hypercolumn_layers: list[str], output_dim: int = 128): """Initialize one adaptation layer for every extraction point. Args: hypercolumn_layers: The list of the hypercolumn layer names. @@ -84,7 +83,7 @@ def __init__(self, hypercolumn_layers: List[str], output_dim: int = 128): self.layers.append(layer) self.add_module(f"adap_layer_{i}", layer) - def forward(self, features: List[torch.tensor]): + def forward(self, features: list[torch.tensor]): """Apply adaptation layers.""" for i, _ in enumerate(features): features[i] = getattr(self, f"adap_layer_{i}")(features[i]) diff --git a/limap/fitting/fitting.py b/limap/fitting/fitting.py index 9dbb13d5..b6a192c1 100644 --- a/limap/fitting/fitting.py +++ b/limap/fitting/fitting.py @@ -1,6 +1,7 @@ import numpy as np from _limap import _estimators, _fitting from bresenham import bresenham + from hloc.localize_inloc import interpolate_scan diff --git a/limap/point2d/superglue/superglue.py b/limap/point2d/superglue/superglue.py index f4e5942f..ff9d01db 100644 --- a/limap/point2d/superglue/superglue.py +++ b/limap/point2d/superglue/superglue.py @@ -43,14 +43,13 @@ import os from copy import deepcopy from pathlib import Path -from typing import List, Tuple import torch from pycolmap import logging from torch import nn -def MLP(channels: List[int], do_bn: bool = True) -> nn.Module: +def MLP(channels: list[int], do_bn: bool = True) -> nn.Module: """Multi-layer perceptron""" n = len(channels) layers = [] @@ -78,7 +77,7 @@ def normalize_keypoints(kpts, image_shape): class KeypointEncoder(nn.Module): """Joint encoding of visual appearance and location using MLPs""" - def __init__(self, feature_dim: int, layers: List[int]) -> None: + def __init__(self, feature_dim: int, layers: list[int]) -> None: super().__init__() self.encoder = MLP([3] + layers + [feature_dim]) nn.init.constant_(self.encoder[-1].bias, 0.0) @@ -90,7 +89,7 @@ def forward(self, kpts, scores): def attention( query: torch.Tensor, key: torch.Tensor, value: torch.Tensor -) -> Tuple[torch.Tensor, torch.Tensor]: +) -> tuple[torch.Tensor, torch.Tensor]: dim = query.shape[1] scores = torch.einsum("bdhn,bdhm->bhnm", query, key) / dim**0.5 prob = torch.nn.functional.softmax(scores, dim=-1) @@ -135,7 +134,7 @@ def forward(self, x: torch.Tensor, source: torch.Tensor) -> torch.Tensor: class AttentionalGNN(nn.Module): - def __init__(self, feature_dim: int, layer_names: List[str]) -> None: + def __init__(self, feature_dim: int, layer_names: list[str]) -> None: super().__init__() self.layers = nn.ModuleList( [ @@ -147,7 +146,7 @@ def __init__(self, feature_dim: int, layer_names: List[str]) -> None: def forward( self, desc0: torch.Tensor, desc1: torch.Tensor - ) -> Tuple[torch.Tensor, torch.Tensor]: + ) -> tuple[torch.Tensor, torch.Tensor]: for layer, name in zip(self.layers, self.names): if name == "cross": src0, src1 = desc1, desc0 diff --git a/limap/point2d/superpoint/main.py b/limap/point2d/superpoint/main.py index 8735f992..f7411087 100644 --- a/limap/point2d/superpoint/main.py +++ b/limap/point2d/superpoint/main.py @@ -1,16 +1,17 @@ import collections.abc as collections import pprint from pathlib import Path -from typing import Dict, List, Optional, Union +from typing import Optional, Union import h5py import numpy as np import torch -from hloc import extract_features -from hloc.utils.io import list_h5_names from pycolmap import logging from tqdm import tqdm +from hloc import extract_features +from hloc.utils.io import list_h5_names + from .superpoint import SuperPoint string_classes = str @@ -34,11 +35,11 @@ def map_tensor(input_, func): @torch.no_grad() def run_superpoint( - conf: Dict, + conf: dict, image_dir: Path, export_dir: Optional[Path] = None, as_half: bool = True, - image_list: Optional[Union[Path, List[str]]] = None, + image_list: Optional[Union[Path, list[str]]] = None, feature_path: Optional[Path] = None, overwrite: bool = False, keypoints=None, diff --git a/limap/pointsfm/colmap_sfm.py b/limap/pointsfm/colmap_sfm.py index b1562f52..4da3a425 100644 --- a/limap/pointsfm/colmap_sfm.py +++ b/limap/pointsfm/colmap_sfm.py @@ -9,9 +9,10 @@ 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 model_converter import convert_imagecols_to_colmap def import_images_with_known_cameras(image_dir, database_path, imagecols): diff --git a/limap/pointsfm/model_converter.py b/limap/pointsfm/model_converter.py index e13524ad..3c6abff0 100644 --- a/limap/pointsfm/model_converter.py +++ b/limap/pointsfm/model_converter.py @@ -4,9 +4,10 @@ from limap.util.geometry import rotation_from_quaternion sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import hloc.utils.read_write_model as colmap_utils from colmap_reader import PyReadCOLMAP +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) diff --git a/limap/runners/hybrid_localization.py b/limap/runners/hybrid_localization.py index b854c507..2030d8c5 100644 --- a/limap/runners/hybrid_localization.py +++ b/limap/runners/hybrid_localization.py @@ -2,7 +2,6 @@ from collections import defaultdict import numpy as np -from hloc.utils.io import get_keypoints, get_matches from pycolmap import logging from tqdm import tqdm @@ -11,6 +10,7 @@ import limap.line2d import limap.runners as runners import limap.util.io as limapio +from hloc.utils.io import get_keypoints, get_matches from limap.optimize.hybrid_localization.functions import ( filter_line_2to2_epipolarIoU, get_reprojection_dist_func, diff --git a/runners/7scenes/localization.py b/runners/7scenes/localization.py index 4775df5a..e08f56d0 100644 --- a/runners/7scenes/localization.py +++ b/runners/7scenes/localization.py @@ -10,9 +10,7 @@ import pickle from pathlib import Path -import hloc.utils.read_write_model as colmap_utils import pycolmap -from hloc.utils.parsers import parse_retrieval from utils import ( DepthReader, evaluate, @@ -22,9 +20,11 @@ run_hloc_7scenes, ) +import hloc.utils.read_write_model as colmap_utils import limap.runners as runners import limap.util.config as cfgutils import limap.util.io as limapio +from hloc.utils.parsers import parse_retrieval formatter = logging.Formatter( fmt="[%(asctime)s %(name)s %(levelname)s] %(message)s", diff --git a/runners/7scenes/utils.py b/runners/7scenes/utils.py index f4eef0ba..9ecbf758 100644 --- a/runners/7scenes/utils.py +++ b/runners/7scenes/utils.py @@ -7,6 +7,11 @@ import PIL.Image import pycolmap import torch +from tqdm import tqdm + +import limap.base as base +import limap.pointsfm as pointsfm +import limap.util.io as limapio from hloc import ( extract_features, localize_sfm, @@ -19,11 +24,6 @@ evaluate, ) from hloc.utils.read_write_model import read_model, write_model -from tqdm import tqdm - -import limap.base as base -import limap.pointsfm as pointsfm -import limap.util.io as limapio ############################################################################### # The following utils functions are taken/modified from hloc.pipelines.7scenes diff --git a/runners/cambridge/localization.py b/runners/cambridge/localization.py index 495aa1cc..c1d2c6d3 100644 --- a/runners/cambridge/localization.py +++ b/runners/cambridge/localization.py @@ -10,7 +10,6 @@ import pickle from pathlib import Path -from hloc.utils.parsers import parse_retrieval from utils import ( eval, get_result_filenames, @@ -23,6 +22,7 @@ import limap.runners as runners import limap.util.config as cfgutils import limap.util.io as limapio +from hloc.utils.parsers import parse_retrieval formatter = logging.Formatter( fmt="[%(asctime)s %(name)s %(levelname)s] %(message)s", diff --git a/runners/cambridge/utils.py b/runners/cambridge/utils.py index 5996dadf..23c8e120 100644 --- a/runners/cambridge/utils.py +++ b/runners/cambridge/utils.py @@ -11,17 +11,17 @@ import imagesize import pycolmap +from tqdm import tqdm + +import limap.base as base +import limap.pointsfm as pointsfm +import limap.util.io as limapio from hloc import ( extract_features, localize_sfm, match_features, pairs_from_retrieval, ) -from tqdm import tqdm - -import limap.base as base -import limap.pointsfm as pointsfm -import limap.util.io as limapio def read_scene_visualsfm( diff --git a/runners/inloc/utils.py b/runners/inloc/utils.py index 26d5c61b..f6762d0e 100644 --- a/runners/inloc/utils.py +++ b/runners/inloc/utils.py @@ -3,13 +3,13 @@ import imagesize import numpy as np -from hloc import extract_features, localize_inloc, match_features -from hloc.utils.parsers import parse_retrieval from scipy.io import loadmat from tqdm import tqdm import limap.base as base import limap.util.io as limapio +from hloc import extract_features, localize_inloc, match_features +from hloc.utils.parsers import parse_retrieval class InLocP3DReader(base.BaseP3DReader): diff --git a/scripts/format/python.sh b/scripts/format/python.sh index 7dd575c3..b840a8a1 100755 --- a/scripts/format/python.sh +++ b/scripts/format/python.sh @@ -4,7 +4,7 @@ # Check version version_string=$(ruff --version | sed -E 's/^.*(\d+\.\d+-.*).*$/\1/') -expected_version_string='0.6.7' +expected_version_string='0.8.6' if [[ "$version_string" =~ "$expected_version_string" ]]; then echo "ruff version '$version_string' matches '$expected_version_string'" else From e983cf17bd9b90f73e5f72d142708f6ce4c89765 Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Tue, 7 Jan 2025 18:47:00 +0100 Subject: [PATCH 06/10] update ci --- .github/workflows/format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 767948e4..544d81ec 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -37,7 +37,7 @@ jobs: exit 0 fi set +x -euo pipefail - python -m pip install ruff==0.6.7 clang-format==19.1.0 + python -m pip install ruff==0.8.6 clang-format==19.1.0 ./scripts/format/clang_format.sh ./scripts/format/python.sh git diff --name-only From 713c8b611f6f67d0441910a0362c51438b11ee40 Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Tue, 7 Jan 2025 18:51:03 +0100 Subject: [PATCH 07/10] ignore I001 --- ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.toml b/ruff.toml index 3aa6f99f..93e59ee2 100644 --- a/ruff.toml +++ b/ruff.toml @@ -15,6 +15,7 @@ select = [ # isort "I", ] +ignore = ['I001'] [lint.per-file-ignores] "limap/line2d/L2D2/RAL_net_cov.py" = ["SIM"] From 31d781013a70560094538c51e3d70180c6d2c720 Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Tue, 7 Jan 2025 18:56:31 +0100 Subject: [PATCH 08/10] add pytest into requirements.txt --- .github/workflows/build.yml | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6fecef17..d229abd2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -92,4 +92,5 @@ jobs: 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 + pytest -m ci_workflow tests diff --git a/requirements.txt b/requirements.txt index 96728ceb..3129ccdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ imagesize einops ninja yacs +pytest numpy<=1.26.4 # Tag numpy 1.26.4 for Open3D issue (https://github.com/numpy/numpy/issues/26853) open3d==0.18.0 pycolmap>=3.11.1 From 37b70d86bfe439d2039eaaaeafd074c3d9bb1dd1 Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Tue, 7 Jan 2025 19:30:50 +0100 Subject: [PATCH 09/10] remove support for python 3.8 --- .github/workflows/build.yml | 1 - README.md | 2 +- setup.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6fecef17..28f5c5c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,6 @@ jobs: fail-fast: false matrix: config: [ - {os: ubuntu-latest, python-version: "3.8"}, {os: ubuntu-latest, python-version: "3.9"}, {os: ubuntu-latest, python-version: "3.10"}, {os: ubuntu-latest, python-version: "3.11"}, diff --git a/README.md b/README.md index 97b9bc66..f93e2324 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ In this project, we provide interfaces for various geometric operations on 2D/3D ## Installation **Install the dependencies as follows:** -* Python 3.8/9/10/11 +* Python 3.9/10/11 * CMake >= 3.17 * CUDA (for deep learning based detectors/matchers) * System dependencies [[Command line](./misc/install/dependencies.md)] diff --git a/setup.py b/setup.py index 1ea0e7d4..0093f3f2 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ def build_extension(self, ext): name="limap", version="1.0.0", packages=find_packages(), - python_requires=">=3.8, < 3.12", + python_requires=">=3.9, < 3.12", author="Shaohui Liu", author_email="b1ueber2y@gmail.com", description="A toolbox for mapping and localization with line features", From b4a47c9df44c7e9971cd6eb9f93b29d794622102 Mon Sep 17 00:00:00 2001 From: B1ueber2y Date: Wed, 8 Jan 2025 17:15:58 +0100 Subject: [PATCH 10/10] refactor ci infra --- .github/workflows/build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6cd2ebc..0ffbdf8d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,6 +83,15 @@ 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 @@ -90,6 +99,4 @@ jobs: --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 - pytest -m ci_workflow tests