diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml new file mode 100644 index 00000000..6cfdbeb0 --- /dev/null +++ b/.github/workflows/mac.yml @@ -0,0 +1,83 @@ +name: Mac + +on: + push: + branches: + - main + pull_request: + types: [ assigned, opened, synchronize, reopened ] + release: + types: [ published, edited ] + +jobs: + build: + name: ${{ matrix.config.os }} ${{ matrix.config.arch }} ${{ matrix.config.cmakeBuildType }} + runs-on: ${{ matrix.config.os }} + strategy: + matrix: + config: [ + { + os: macos-14, + arch: arm64, + cmakeBuildType: Release, + }, + ] + + env: + COMPILER_CACHE_VERSION: 1 + COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache + CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache + CCACHE_BASEDIR: ${{ github.workspace }} + + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + id: cache-builds + with: + key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ matrix.config.cmakeBuildType }}-${{ github.run_id }}-${{ github.run_number }} + restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ matrix.config.cmakeBuildType }} + path: ${{ env.COMPILER_CACHE_DIR }} + + - name: Setup Mac + run: | + brew install \ + cmake \ + ninja \ + boost \ + eigen \ + flann \ + freeimage \ + metis \ + glog \ + googletest \ + ceres-solver \ + qt5 \ + glew \ + cgal \ + sqlite3 \ + ccache + + - name: Configure and build + run: | + cmake --version + mkdir build + cd build + cmake .. \ + -GNinja \ + -DCMAKE_BUILD_TYPE=${{ matrix.config.cmakeBuildType }} \ + -DTESTS_ENABLED=ON \ + -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" + ninja + + - name: Run tests + run: | + cd build + set +e + ctest --output-on-failure -E .+colmap_.* + + - name: Cleanup compiler cache + run: | + set -x + ccache --show-stats --verbose + ccache --evict-older-than 1d + ccache --show-stats --verbose diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 455e8d83..a8246f96 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -185,4 +185,4 @@ jobs: # Delete cache older than 10 days. find "$CTCACHE_DIR"/*/ -mtime +10 -print0 | xargs -0 rm -rf echo "Size of ctcache after: $(du -sh $CTCACHE_DIR)" - echo "Number of ctcache files after: $(find $CTCACHE_DIR | wc -l)" \ No newline at end of file + echo "Number of ctcache files after: $(find $CTCACHE_DIR | wc -l)"'' diff --git a/.gitignore b/.gitignore index b4caee1c..52abcd91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -build -data +/build +/data +/.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt index 56919a11..1d1c2729 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set_property(GLOBAL PROPERTY GLOBAL_DEPENDS_NO_CYCLES ON) -option(OPENMP_ENABLED "Whether to enable OpenMP parallelization" ON) option(TESTS_ENABLED "Whether to build test binaries" OFF) option(ASAN_ENABLED "Whether to enable AddressSanitizer flags" OFF) option(CCACHE_ENABLED "Whether to enable compiler caching, if available" ON) diff --git a/cmake/FindDependencies.cmake b/cmake/FindDependencies.cmake index 2425a523..7bcf2020 100644 --- a/cmake/FindDependencies.cmake +++ b/cmake/FindDependencies.cmake @@ -19,11 +19,6 @@ if(TESTS_ENABLED) find_package(GTest REQUIRED) endif() -if (OPENMP_ENABLED) - message(STATUS "Enabling OpenMP") - find_package(OpenMP REQUIRED) -endif() - include(FetchContent) FetchContent_Declare(PoseLib GIT_REPOSITORY https://github.com/PoseLib/PoseLib.git diff --git a/docs/INSTALL_MAC.md b/docs/INSTALL_MAC.md deleted file mode 100644 index 638f5743..00000000 --- a/docs/INSTALL_MAC.md +++ /dev/null @@ -1,193 +0,0 @@ -We would like to thank [Asadali242](https://github.com/Asadali242) for providing the installation guide for MAC. -Leave your comments at [Issue #62](https://github.com/colmap/glomap/issues/62) if you encounter any problems. - -## Installing the COLMAP: - -*1. Open the Terminal and install the brew dependencies:* -``` -brew install \ -cmake \ -ninja \ -boost \ -eigen \ -flann \ -libomp \ #(Install Libomp as well) -freeimage \ -metis \ -glog \ -googletest \ -ceres-solver \ -qt@5 \ -glew \ -cgal \ -sqlite3 -``` - -*2. Clone the COLMAP repository:* -``` -git clone https://github.com/colmap/colmap.git -cd colmap -``` - -*3. Ensure Qt5 is in your PATH:* -``` -export PATH="/opt/homebrew/opt/qt@5/bin:$PATH" -``` - -*4. Create a build directory:* -``` -mkdir build -cd build -``` - -*5. After installing, link Qt5 to make sure it’s accessible:* -``` -brew link qt@5 --force -``` - -*6. Run CMake with the specific paths for ARM Mac(M1 and above):* -``` -cmake .. -GNinja \ - -DCMAKE_PREFIX_PATH="/opt/homebrew/opt/flann;/opt/homebrew/opt/metis;/opt/homebrew/opt/suite-sparse;/opt/homebrew/opt/qt@5;/opt/homebrew/opt/freeimage" -``` - -*7. Build and install COLMAP:* -``` -ninja -sudo ninja install -``` - -*8. Confirm COLMAP installation by running:* -``` -colmap -h -colmap gui -``` - -**This is the first part and will install Colmap on your device.** - - - -## Installing the GLOMAP: - -*1. Clone the GitHub repository:* -``` -git clone https://github.com/colmap/glomap -cd glomap -``` - -*2. Create a build directory:* -``` -mkdir build -cd build -``` - -*3. Export Environment Variables Again:* -``` -export PATH="/opt/homebrew/opt/qt@5/bin:$PATH" -export LDFLAGS="-L/opt/homebrew/opt/libomp/lib" -export CPPFLAGS="-I/opt/homebrew/opt/libomp/include" -export CMAKE_PREFIX_PATH="/opt/homebrew/opt/qt@5;/opt/homebrew/opt/libomp" -export PKG_CONFIG_PATH="/opt/homebrew/opt/qt@5/lib/pkgconfig" -``` - -*4. Run the CMake Command:* -``` -cmake -DCMAKE_PREFIX_PATH="/opt/homebrew/Cellar/qt@5/5.15.13_1;/opt/homebrew/opt/libomp" \ --DOpenMP_C_FLAGS="-Xclang -fopenmp" \ --DOpenMP_C_LIB_NAMES="libomp" \ --DOpenMP_CXX_FLAGS="-Xclang -fopenmp" \ --DOpenMP_CXX_LIB_NAMES="libomp" \ --DOpenMP_C_INCLUDE_DIRS="/opt/homebrew/opt/libomp/include" \ --DOpenMP_CXX_INCLUDE_DIRS="/opt/homebrew/opt/libomp/include" \ --DOpenMP_libomp_LIBRARY=/opt/homebrew/opt/libomp/lib/libomp.dylib \ --DOpenMP_INCLUDE_DIR=/opt/homebrew/opt/libomp/include \ -.. -GNinja -``` - -*5. Build the Project:* -``` -ninja -``` - -**NOTE: If at this point, there are build errors related to ‘cholmod.h’ or ‘omp.h’, clean the build and then re-run the make with the following commands:** -``` -cmake -DCMAKE_PREFIX_PATH="/opt/homebrew/Cellar/qt@5/5.15.13_1;/opt/homebrew/opt/libomp" \ --DOpenMP_C_FLAGS="-Xclang -fopenmp -I/opt/homebrew/opt/libomp/include" \ --DOpenMP_C_LIB_NAMES="libomp" \ --DOpenMP_CXX_FLAGS="-Xclang -fopenmp -I/opt/homebrew/opt/libomp/include" \ --DOpenMP_CXX_LIB_NAMES="libomp" \ --DOpenMP_libomp_LIBRARY=/opt/homebrew/opt/libomp/lib/libomp.dylib \ -.. -GNinja -``` - -**After the build is successful:** - -*6. Install the Built Project:* -``` -sudo ninja install -``` - -*7. Test the Installation:* -``` -glomap -h -``` - -**It should display something like:** -``` -GLOMAP -- Global Structure-from-Motion -Usage: -glomap mapper --database_path DATABASE --output_path -MODEL -glomap mapper_resume --input_path MODEL_INPUT --output_path MODEL_OUTPUT -Available commands: -help -mapper -mapper_resume -``` - - -## Testing with the end-to-end examples provided: - -*1. Open the end-to-end example database link provided:* -https://lpanaf.github.io/eccv24_glomap/ - -*2. Download one of the datasets provided and extract the zip file.* - -*3. Create a new directory named ‘data’ in the root directory ‘glomap’.* - -*4. Place the extracted dataset in the directory ‘data’.* - -*5. Now navigate back to the project directory ‘glomap’.* - -**NOTE: Following commands are assuming the dataset to be south-building:** - -*6. Extract Features with COLMAP:* -``` -colmap feature_extractor \ ---image_path ./data/south-building/images \ ---database_path ./data/south-building/database.db -``` - -*7. Match Features with COLMAP:* -``` -colmap exhaustive_matcher \ ---database_path ./data/south-building/database.db -``` - -*8. Run GLOMAP Mapper:* -``` -glomap mapper \ ---database_path ./data/south-building/database.db \ ---image_path ./data/south-building/images \ ---output_path ./output/south-building/sparse -``` - -**This should generate a new directory named ‘output’ in the project directory.** - -*9. Visualize the Results:* -``` -colmap gui \ ---database_path ./data/south-building/database.db \ ---image_path ./data/south-building/images \ ---import_path ./output/south-building/sparse/0 -``` diff --git a/glomap/CMakeLists.txt b/glomap/CMakeLists.txt index a5d82447..1325715b 100644 --- a/glomap/CMakeLists.txt +++ b/glomap/CMakeLists.txt @@ -84,10 +84,6 @@ target_link_libraries( ) target_include_directories(glomap PUBLIC ..) -if(OPENMP_FOUND) - target_link_libraries(glomap PUBLIC OpenMP::OpenMP_CXX) -endif() - if(MSVC) target_compile_options(glomap PRIVATE /bigobj) else() diff --git a/glomap/estimators/relpose_estimation.cc b/glomap/estimators/relpose_estimation.cc index 45f206bd..4676eb22 100644 --- a/glomap/estimators/relpose_estimation.cc +++ b/glomap/estimators/relpose_estimation.cc @@ -1,5 +1,7 @@ #include "glomap/estimators/relpose_estimation.h" +#include + #include namespace glomap { @@ -14,14 +16,13 @@ void EstimateRelativePoses(ViewGraph& view_graph, valid_pair_ids.push_back(image_pair_id); } - // Define outside loop to reuse memory and avoid reallocation. - std::vector points2D_1, points2D_2; - std::vector inliers; - const int64_t num_image_pairs = valid_pair_ids.size(); const int64_t kNumChunks = 10; const int64_t interval = std::ceil(static_cast(num_image_pairs) / kNumChunks); + + colmap::ThreadPool thread_pool(colmap::ThreadPool::kMaxNumThreads); + LOG(INFO) << "Estimating relative pose for " << num_image_pairs << " pairs"; for (int64_t chunk_id = 0; chunk_id < kNumChunks; chunk_id++) { std::cout << "\r Estimating relative pose: " << chunk_id * kNumChunks << "%" @@ -30,47 +31,55 @@ void EstimateRelativePoses(ViewGraph& view_graph, const int64_t end = std::min((chunk_id + 1) * interval, num_image_pairs); -#pragma omp parallel for schedule(dynamic) private( \ - points2D_1, points2D_2, inliers) for (int64_t pair_idx = start; pair_idx < end; pair_idx++) { - ImagePair& image_pair = view_graph.image_pairs[valid_pair_ids[pair_idx]]; - const Image& image1 = images[image_pair.image_id1]; - const Image& image2 = images[image_pair.image_id2]; - const Eigen::MatrixXi& matches = image_pair.matches; + thread_pool.AddTask([&, pair_idx]() { + // Define as thread-local to reuse memory allocation in different tasks. + thread_local std::vector points2D_1; + thread_local std::vector points2D_2; + thread_local std::vector inliers; + + ImagePair& image_pair = + view_graph.image_pairs[valid_pair_ids[pair_idx]]; + const Image& image1 = images[image_pair.image_id1]; + const Image& image2 = images[image_pair.image_id2]; + const Eigen::MatrixXi& matches = image_pair.matches; - // Collect the original 2D points - points2D_1.clear(); - points2D_2.clear(); - for (size_t idx = 0; idx < matches.rows(); idx++) { - points2D_1.push_back(image1.features[matches(idx, 0)]); - points2D_2.push_back(image2.features[matches(idx, 1)]); - } + // Collect the original 2D points + points2D_1.clear(); + points2D_2.clear(); + for (size_t idx = 0; idx < matches.rows(); idx++) { + points2D_1.push_back(image1.features[matches(idx, 0)]); + points2D_2.push_back(image2.features[matches(idx, 1)]); + } - inliers.clear(); - poselib::CameraPose pose_rel_calc; - try { - poselib::estimate_relative_pose( - points2D_1, - points2D_2, - ColmapCameraToPoseLibCamera(cameras[image1.camera_id]), - ColmapCameraToPoseLibCamera(cameras[image2.camera_id]), - options.ransac_options, - options.bundle_options, - &pose_rel_calc, - &inliers); - } catch (const std::exception& e) { - LOG(ERROR) << "Error in relative pose estimation: " << e.what(); - image_pair.is_valid = false; - continue; - } + inliers.clear(); + poselib::CameraPose pose_rel_calc; + try { + poselib::estimate_relative_pose( + points2D_1, + points2D_2, + ColmapCameraToPoseLibCamera(cameras[image1.camera_id]), + ColmapCameraToPoseLibCamera(cameras[image2.camera_id]), + options.ransac_options, + options.bundle_options, + &pose_rel_calc, + &inliers); + } catch (const std::exception& e) { + LOG(ERROR) << "Error in relative pose estimation: " << e.what(); + image_pair.is_valid = false; + return; + } - // Convert the relative pose to the glomap format - for (int i = 0; i < 4; i++) { - image_pair.cam2_from_cam1.rotation.coeffs()[i] = - pose_rel_calc.q[(i + 1) % 4]; - } - image_pair.cam2_from_cam1.translation = pose_rel_calc.t; + // Convert the relative pose to the glomap format + for (int i = 0; i < 4; i++) { + image_pair.cam2_from_cam1.rotation.coeffs()[i] = + pose_rel_calc.q[(i + 1) % 4]; + } + image_pair.cam2_from_cam1.translation = pose_rel_calc.t; + }); } + + thread_pool.Wait(); } std::cout << "\r Estimating relative pose: 100%" << std::endl; diff --git a/glomap/processors/image_undistorter.cc b/glomap/processors/image_undistorter.cc index c83402ae..012465d1 100644 --- a/glomap/processors/image_undistorter.cc +++ b/glomap/processors/image_undistorter.cc @@ -1,5 +1,7 @@ #include "glomap/processors/image_undistorter.h" +#include + namespace glomap { void UndistortImages(std::unordered_map& cameras, @@ -7,33 +9,35 @@ void UndistortImages(std::unordered_map& cameras, bool clean_points) { std::vector image_ids; for (auto& [image_id, image] : images) { - int num_points = image.features.size(); - + const int num_points = image.features.size(); if (image.features_undist.size() == num_points && !clean_points) continue; // already undistorted image_ids.push_back(image_id); } + colmap::ThreadPool thread_pool(colmap::ThreadPool::kMaxNumThreads); + LOG(INFO) << "Undistorting images.."; const int num_images = image_ids.size(); -#pragma omp parallel for for (int image_idx = 0; image_idx < num_images; image_idx++) { Image& image = images[image_ids[image_idx]]; - - int camera_id = image.camera_id; - Camera& camera = cameras[camera_id]; - int num_points = image.features.size(); - + const int num_points = image.features.size(); if (image.features_undist.size() == num_points && !clean_points) continue; // already undistorted - image.features_undist.clear(); - image.features_undist.reserve(num_points); - for (int i = 0; i < num_points; i++) { - image.features_undist.emplace_back( - camera.CamFromImg(image.features[i]).homogeneous().normalized()); - } + const Camera& camera = cameras[image.camera_id]; + + thread_pool.AddTask([&image, &camera, num_points]() { + image.features_undist.clear(); + image.features_undist.reserve(num_points); + for (int i = 0; i < num_points; i++) { + image.features_undist.emplace_back( + camera.CamFromImg(image.features[i]).homogeneous().normalized()); + } + }); } + + thread_pool.Wait(); LOG(INFO) << "Image undistortion done"; } diff --git a/glomap/processors/view_graph_manipulation.cc b/glomap/processors/view_graph_manipulation.cc index 9373fbb2..38ec4dc6 100644 --- a/glomap/processors/view_graph_manipulation.cc +++ b/glomap/processors/view_graph_manipulation.cc @@ -3,7 +3,10 @@ #include "glomap/math/two_view_geometry.h" #include "glomap/math/union_find.h" +#include + namespace glomap { + image_pair_t ViewGraphManipulater::SparsifyGraph( ViewGraph& view_graph, std::unordered_map& images, @@ -245,46 +248,51 @@ void ViewGraphManipulater::DecomposeRelPose( const int64_t num_image_pairs = image_pair_ids.size(); LOG(INFO) << "Decompose relative pose for " << num_image_pairs << " pairs"; -#pragma omp parallel for + colmap::ThreadPool thread_pool(colmap::ThreadPool::kMaxNumThreads); for (int64_t idx = 0; idx < num_image_pairs; idx++) { - ImagePair& image_pair = view_graph.image_pairs.at(image_pair_ids[idx]); - image_t image_id1 = image_pair.image_id1; - image_t image_id2 = image_pair.image_id2; + thread_pool.AddTask([&, idx]() { + ImagePair& image_pair = view_graph.image_pairs.at(image_pair_ids[idx]); + image_t image_id1 = image_pair.image_id1; + image_t image_id2 = image_pair.image_id2; - camera_t camera_id1 = images.at(image_id1).camera_id; - camera_t camera_id2 = images.at(image_id2).camera_id; - - // Use the two-view geometry to re-estimate the relative pose - colmap::TwoViewGeometry two_view_geometry; - two_view_geometry.E = image_pair.E; - two_view_geometry.F = image_pair.F; - two_view_geometry.H = image_pair.H; - two_view_geometry.config = image_pair.config; - - colmap::EstimateTwoViewGeometryPose(cameras[camera_id1], - images[image_id1].features, - cameras[camera_id2], - images[image_id2].features, - &two_view_geometry); - - // if it planar, then use the estimated relative pose - if (image_pair.config == colmap::TwoViewGeometry::PLANAR && - cameras[camera_id1].has_prior_focal_length && - cameras[camera_id2].has_prior_focal_length) { - image_pair.config = colmap::TwoViewGeometry::CALIBRATED; - continue; - } else if (!(cameras[camera_id1].has_prior_focal_length && - cameras[camera_id2].has_prior_focal_length)) - continue; + camera_t camera_id1 = images.at(image_id1).camera_id; + camera_t camera_id2 = images.at(image_id2).camera_id; + + // Use the two-view geometry to re-estimate the relative pose + colmap::TwoViewGeometry two_view_geometry; + two_view_geometry.E = image_pair.E; + two_view_geometry.F = image_pair.F; + two_view_geometry.H = image_pair.H; + two_view_geometry.config = image_pair.config; + + colmap::EstimateTwoViewGeometryPose(cameras[camera_id1], + images[image_id1].features, + cameras[camera_id2], + images[image_id2].features, + &two_view_geometry); + + // if it planar, then use the estimated relative pose + if (image_pair.config == colmap::TwoViewGeometry::PLANAR && + cameras[camera_id1].has_prior_focal_length && + cameras[camera_id2].has_prior_focal_length) { + image_pair.config = colmap::TwoViewGeometry::CALIBRATED; + return; + } else if (!(cameras[camera_id1].has_prior_focal_length && + cameras[camera_id2].has_prior_focal_length)) + return; + + image_pair.config = two_view_geometry.config; + image_pair.cam2_from_cam1 = two_view_geometry.cam2_from_cam1; + + if (image_pair.cam2_from_cam1.translation.norm() > EPS) { + image_pair.cam2_from_cam1.translation = + image_pair.cam2_from_cam1.translation.normalized(); + } + }); + } - image_pair.config = two_view_geometry.config; - image_pair.cam2_from_cam1 = two_view_geometry.cam2_from_cam1; + thread_pool.Wait(); - if (image_pair.cam2_from_cam1.translation.norm() > EPS) { - image_pair.cam2_from_cam1.translation = - image_pair.cam2_from_cam1.translation.normalized(); - } - } size_t counter = 0; for (size_t idx = 0; idx < image_pair_ids.size(); idx++) { ImagePair& image_pair = view_graph.image_pairs.at(image_pair_ids[idx]);