From 83f89a329501b2cfc779dd8bdca147bb40efa06b Mon Sep 17 00:00:00 2001 From: Fabien Servant Date: Wed, 15 Apr 2026 09:36:31 +0200 Subject: [PATCH 1/2] Adding missing poses in sfmMerge --- src/software/utils/main_sfmMerge.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/software/utils/main_sfmMerge.cpp b/src/software/utils/main_sfmMerge.cpp index 7bba0e9f2c..5cbc213c6c 100644 --- a/src/software/utils/main_sfmMerge.cpp +++ b/src/software/utils/main_sfmMerge.cpp @@ -122,6 +122,19 @@ bool simpleMerge(sfmData::SfMData & sfmData1, const sfmData::SfMData & sfmData2, } } + { + auto& poses1 = sfmData1.getPoses(); + auto& poses2 = sfmData2.getPoses(); + const size_t totalSize = poses1.size() + poses2.size(); + + poses1.insert(poses2.begin(), poses2.end()); + if (poses1.size() < totalSize && !ignoreDuplicates) + { + ALICEVISION_LOG_ERROR("Unhandled error: common Poses ID between both SfMData"); + return false; + } + } + { auto& intrinsics1 = sfmData1.getIntrinsics(); auto& intrinsics2 = sfmData2.getIntrinsics(); From 89f83d9bd1036639128e32b9f77283b60314d57b Mon Sep 17 00:00:00 2001 From: Fabien Servant Date: Thu, 30 Apr 2026 15:44:31 +0200 Subject: [PATCH 2/2] Add new node for sfm triangulation from tracks --- meshroom/aliceVision/SfMTriangulating.py | 71 ++++++++++ src/software/pipeline/CMakeLists.txt | 13 ++ .../pipeline/main_sfmTriangulating.cpp | 128 ++++++++++++++++++ src/software/utils/main_sfmMerge.cpp | 2 - 4 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 meshroom/aliceVision/SfMTriangulating.py create mode 100644 src/software/pipeline/main_sfmTriangulating.cpp diff --git a/meshroom/aliceVision/SfMTriangulating.py b/meshroom/aliceVision/SfMTriangulating.py new file mode 100644 index 0000000000..ec15f58492 --- /dev/null +++ b/meshroom/aliceVision/SfMTriangulating.py @@ -0,0 +1,71 @@ +__version__ = "1.0" + +from meshroom.core import desc +from meshroom.core.utils import VERBOSE_LEVEL + + +class SfMTriangulating(desc.AVCommandLineNode): + commandLine = "aliceVision_sfmTriangulating {allParams}" + size = desc.DynamicNodeSize("input") + + category = "Sparse Reconstruction" + documentation = """ +This node performs keypoint triangulation on its input data. +Contrary to the StructureFromMotion node, this node does not infer the camera poses, therefore they must be given in the SfMData input. +""" + + inputs = [ + desc.File( + name="input", + label="SfMData", + description="SfMData file. Must contain the camera calibration.", + value="", + ), + desc.File( + name="tracksFilename", + label="Tracks File", + description="Tracks file.", + value="", + ), + desc.IntParam( + name="minNumberOfObservationsForTriangulation", + label="Min Observations For Triangulation", + description="Minimum number of observations to triangulate a point.\n" + "Setting it to 3 (or more) reduces drastically the noise in the point cloud.", + value=2, + range=(2, 10, 1), + advanced=True, + ), + desc.FloatParam( + name="minAngleForTriangulation", + label="Min Angle For Triangulation", + description="Minimum angle for triangulation.", + value=3.0, + range=(0.1, 10.0, 0.1), + advanced=True, + ), + desc.FloatParam( + name="maxTriangulationError", + label="Max Triangulation Error", + description="Maximum reprojection error in the triangulation process (in pixels).", + value=8.0, + range=(0.1, 10.0, 0.1), + advanced=True, + ), + desc.ChoiceParam( + name="verboseLevel", + label="Verbose Level", + description="Verbosity level (fatal, error, warning, info, debug, trace).", + values=VERBOSE_LEVEL, + value="info", + ), + ] + + outputs = [ + desc.File( + name="output", + label="SfMData", + description="Path to the output SfMData file.", + value="{nodeCacheFolder}/sfm.abc", + ) + ] diff --git a/src/software/pipeline/CMakeLists.txt b/src/software/pipeline/CMakeLists.txt index d66ebe5afb..13cab9661a 100644 --- a/src/software/pipeline/CMakeLists.txt +++ b/src/software/pipeline/CMakeLists.txt @@ -269,6 +269,19 @@ if (ALICEVISION_BUILD_SFM) Boost::program_options ) + # SfM Triangulation + alicevision_add_software(aliceVision_sfmTriangulating + SOURCE main_sfmTriangulating.cpp + FOLDER ${FOLDER_SOFTWARE_PIPELINE} + LINKS aliceVision_system + aliceVision_cmdline + aliceVision_track + aliceVision_sfm + aliceVision_sfmData + aliceVision_sfmDataIO + Boost::program_options + ) + # Global SfM alicevision_add_software(aliceVision_globalSfM SOURCE main_globalSfM.cpp diff --git a/src/software/pipeline/main_sfmTriangulating.cpp b/src/software/pipeline/main_sfmTriangulating.cpp new file mode 100644 index 0000000000..c1a2f505b5 --- /dev/null +++ b/src/software/pipeline/main_sfmTriangulating.cpp @@ -0,0 +1,128 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2026 AliceVision contributors. +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +// These constants define the current software version. +// They must be updated when the command line is changed. +#define ALICEVISION_SOFTWARE_VERSION_MAJOR 1 +#define ALICEVISION_SOFTWARE_VERSION_MINOR 0 + +using namespace aliceVision; + +namespace po = boost::program_options; + +int aliceVision_main(int argc, char** argv) +{ + std::string sfmDataFilename; + std::string sfmDataOutputFilename; + std::string tracksFilename; + + double minAngleForTriangulation = 1.0; + size_t minNbObservationsForTriangulation = 0; + double maxTriangulationError = 8.0; + + // clang-format off + po::options_description requiredParams("Required parameters"); + requiredParams.add_options() + ("input,i", po::value(&sfmDataFilename)->required(), "SfMData file, must contain the camera calibration.") + ("output,o", po::value(&sfmDataOutputFilename)->required(), "Path to the output SfMData file.") + ("tracksFilename,t", po::value(&tracksFilename)->required(), "Tracks file."); + + po::options_description optionalParams("Optional parameters"); + optionalParams.add_options() + ("maxTriangulationError", po::value(&maxTriangulationError)->default_value(maxTriangulationError), "Maximum reprojection error in the triangulation process (in pixels).") + ("minAngleForTriangulation", po::value(&minAngleForTriangulation)->default_value(minAngleForTriangulation), "Minimum angle for triangulation in degrees.") + ("minNumberOfObservationsForTriangulation", po::value(&minNbObservationsForTriangulation)->default_value(minNbObservationsForTriangulation), "Minimum number of observations to triangulate a point."); + // clang-format on + + CmdLine cmdline("AliceVision SfM Triangulation"); + cmdline.add(requiredParams); + cmdline.add(optionalParams); + if (!cmdline.execute(argc, argv)) + { + return EXIT_FAILURE; + } + + // set maxThreads + HardwareContext hwc = cmdline.getHardwareContext(); + omp_set_num_threads(hwc.getMaxThreads()); + + // load input SfMData scene + sfmData::SfMData sfmData; + if(!sfmDataIO::load(sfmData, sfmDataFilename, sfmDataIO::ESfMData::ALL)) + { + ALICEVISION_LOG_ERROR("The input SfMData file '" + sfmDataFilename + "' cannot be read."); + return EXIT_FAILURE; + } + + // Load tracks + ALICEVISION_LOG_INFO("Load tracks"); + track::TracksHandler tracksHandler; + if (!tracksHandler.load(tracksFilename, sfmData.getValidViews())) + { + ALICEVISION_LOG_ERROR("The input tracks file '" + tracksFilename + "' cannot be read."); + return EXIT_FAILURE; + } + + std::set evaluatedTracks; + std::map outputLandmarks; + std::mt19937 randomNumberGenerator; + + + // Effectively triangulate tracks + sfm::SfmTriangulation sfmTriangulation(minNbObservationsForTriangulation, maxTriangulationError); + if (!sfmTriangulation.process(sfmData, tracksHandler.getAllTracks(), tracksHandler.getTracksPerView(), + randomNumberGenerator, sfmData.getValidViews(), + evaluatedTracks, outputLandmarks, false)) + { + ALICEVISION_LOG_ERROR("Triangulation failed."); + return EXIT_FAILURE; + } + + auto & landmarks = sfmData.getLandmarks(); + landmarks.clear(); + + for (const auto & [landmarkId, outputLandmark] : outputLandmarks) + { + if (outputLandmark.getObservations().size() < minNbObservationsForTriangulation) + { + continue; + } + + if (!sfm::SfmTriangulation::checkChierality(sfmData, outputLandmark)) + { + continue; + } + + double maxAngle = sfm::SfmTriangulation::getMaximalAngle(sfmData, outputLandmark); + if (maxAngle < minAngleForTriangulation) + { + continue; + } + + landmarks[landmarkId] = outputLandmark; + } + + // Save output + if (!sfmDataIO::save(sfmData, sfmDataOutputFilename, sfmDataIO::ESfMData::ALL)) + { + ALICEVISION_LOG_ERROR("An error occurred while trying to save '" << sfmDataOutputFilename << "'"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/src/software/utils/main_sfmMerge.cpp b/src/software/utils/main_sfmMerge.cpp index 5cbc213c6c..9c75c5acb9 100644 --- a/src/software/utils/main_sfmMerge.cpp +++ b/src/software/utils/main_sfmMerge.cpp @@ -414,13 +414,11 @@ bool fromLandmarksMerge(sfmData::SfMData & sfmData1, const sfmData::SfMData & sf // Simple merge of intrinsics auto& intrinsics1 = sfmData1.getIntrinsics(); auto& intrinsics2 = sfmData2.getIntrinsics(); - totalSize = intrinsics1.size() + intrinsics2.size(); intrinsics1.insert(intrinsics2.begin(), intrinsics2.end()); // Simple merge of poses auto& poses1 = sfmData1.getPoses(); auto& poses2 = sfmData2.getPoses(); - totalSize = poses1.size() + poses2.size(); poses1.insert(poses2.begin(), poses2.end()); sfmData1.addFeaturesFolders(sfmData2.getRelativeFeaturesFolders());