From 761e55eff4d3bbd4dddf10040813ae8c9042661b Mon Sep 17 00:00:00 2001 From: Fabien Servant Date: Thu, 28 May 2026 09:42:23 +0200 Subject: [PATCH] Add main_sfmDataIntersection.cpp, CMakeLists entry, and SfMDataIntersection.py Meshroom node --- meshroom/aliceVision/SfMDataIntersection.py | 59 ++++++++ src/software/utils/CMakeLists.txt | 11 ++ .../utils/main_sfmDataIntersection.cpp | 142 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 meshroom/aliceVision/SfMDataIntersection.py create mode 100644 src/software/utils/main_sfmDataIntersection.cpp diff --git a/meshroom/aliceVision/SfMDataIntersection.py b/meshroom/aliceVision/SfMDataIntersection.py new file mode 100644 index 0000000000..7ac7d7d6f2 --- /dev/null +++ b/meshroom/aliceVision/SfMDataIntersection.py @@ -0,0 +1,59 @@ +__version__ = "1.0" + +from meshroom.core import desc +from meshroom.core.utils import VERBOSE_LEVEL + + +class SfMDataIntersection(desc.AVCommandLineNode): + commandLine = "aliceVision_sfmDataIntersection {allParams}" + size = desc.DynamicNodeSize("input") + + category = "Utils" + documentation = """ +Filters the 3D landmarks of a SfMData file to keep only the ones that have +observations from both camera group A and camera group B. + +The main input SfMData provides all the content (views, poses, intrinsics, landmarks). +InputA and InputB each define a subset of cameras (by their view IDs) that must both +be represented in the observations of a landmark for it to be retained. + +This is typically used after a merge (e.g. SfMMerge) to extract only the landmarks +that are co-observed by cameras from two distinct acquisition groups. +""" + + inputs = [ + desc.File( + name="input", + label="Input SfMData", + description="Main SfMData file providing all views, poses, intrinsics and landmarks.", + value="", + ), + desc.File( + name="inputA", + label="Input SfMData A", + description="SfMData file whose view IDs define camera group A.", + value="", + ), + desc.File( + name="inputB", + label="Input SfMData B", + description="SfMData file whose view IDs define camera group B.", + value="", + ), + 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 containing only intersection landmarks.", + value="{nodeCacheFolder}/sfmData.abc", + ), + ] diff --git a/src/software/utils/CMakeLists.txt b/src/software/utils/CMakeLists.txt index 111b6cebdb..a3d3d520f3 100644 --- a/src/software/utils/CMakeLists.txt +++ b/src/software/utils/CMakeLists.txt @@ -315,6 +315,17 @@ if (ALICEVISION_BUILD_SFM) Boost::program_options ) + # SfM Data Intersection + alicevision_add_software(aliceVision_sfmDataIntersection + SOURCE main_sfmDataIntersection.cpp + FOLDER ${FOLDER_SOFTWARE_UTILS} + LINKS aliceVision_system + aliceVision_cmdline + aliceVision_sfmData + aliceVision_sfmDataIO + Boost::program_options + ) + # Tracks Merge alicevision_add_software(aliceVision_tracksMerging SOURCE main_tracksMerging.cpp diff --git a/src/software/utils/main_sfmDataIntersection.cpp b/src/software/utils/main_sfmDataIntersection.cpp new file mode 100644 index 0000000000..baf63eb733 --- /dev/null +++ b/src/software/utils/main_sfmDataIntersection.cpp @@ -0,0 +1,142 @@ +// 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 + +// 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) +{ + // command-line parameters + std::string sfmDataFilename; + std::string sfmDataFilenameA; + std::string sfmDataFilenameB; + std::string outSfMDataFilename; + + // clang-format off + po::options_description requiredParams("Required parameters"); + requiredParams.add_options() + ("input,i", po::value(&sfmDataFilename)->required(), + "Path to the main SfMData file (content source: views, poses, intrinsics, landmarks).") + ("inputA,a", po::value(&sfmDataFilenameA)->required(), + "Path to SfMData file A. Only its view IDs are used to define camera group A.") + ("inputB,b", po::value(&sfmDataFilenameB)->required(), + "Path to SfMData file B. Only its view IDs are used to define camera group B.") + ("output,o", po::value(&outSfMDataFilename)->required(), + "Output SfMData scene."); + // clang-format on + + CmdLine cmdline("AliceVision sfmDataIntersection"); + cmdline.add(requiredParams); + if (!cmdline.execute(argc, argv)) + { + return EXIT_FAILURE; + } + + // Load the main input scene (content source) + 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 inputA and inputB — only their view IDs are needed + sfmData::SfMData sfmDataA; + if (!sfmDataIO::load(sfmDataA, sfmDataFilenameA, sfmDataIO::ESfMData::VIEWS)) + { + ALICEVISION_LOG_ERROR("The input SfMData file A '" << sfmDataFilenameA << "' cannot be read"); + return EXIT_FAILURE; + } + + sfmData::SfMData sfmDataB; + if (!sfmDataIO::load(sfmDataB, sfmDataFilenameB, sfmDataIO::ESfMData::VIEWS)) + { + ALICEVISION_LOG_ERROR("The input SfMData file B '" << sfmDataFilenameB << "' cannot be read"); + return EXIT_FAILURE; + } + + // Build sets of view IDs from inputA and inputB + std::set viewsSetA; + for (const auto& [viewId, _] : sfmDataA.getViews()) + { + viewsSetA.insert(viewId); + } + + std::set viewsSetB; + for (const auto& [viewId, _] : sfmDataB.getViews()) + { + viewsSetB.insert(viewId); + } + + ALICEVISION_LOG_INFO("Input SfMData: " << sfmData.getViews().size() << " views, " + << sfmData.getLandmarks().size() << " landmarks"); + ALICEVISION_LOG_INFO("InputA: " << viewsSetA.size() << " views"); + ALICEVISION_LOG_INFO("InputB: " << viewsSetB.size() << " views"); + + // The output is the main input SfMData with landmarks filtered + sfmData::SfMData outputSfmData = sfmData; + + ALICEVISION_LOG_INFO("Total landmarks before filtering: " << outputSfmData.getLandmarks().size()); + + // Filter landmarks: keep only those with observations from BOTH camera groups + auto& landmarks = outputSfmData.getLandmarks(); + for (auto it = landmarks.begin(); it != landmarks.end();) + { + bool hasObsFromSetA = false; + bool hasObsFromSetB = false; + + for (const auto& [viewId, obs] : it->second.getObservations()) + { + if (viewsSetA.count(viewId)) + { + hasObsFromSetA = true; + } + if (viewsSetB.count(viewId)) + { + hasObsFromSetB = true; + } + + if (hasObsFromSetA && hasObsFromSetB) + { + break; + } + } + + if (hasObsFromSetA && hasObsFromSetB) + { + ++it; + } + else + { + it = landmarks.erase(it); + } + } + + ALICEVISION_LOG_INFO("Total landmarks after filtering: " << outputSfmData.getLandmarks().size()); + + if (!sfmDataIO::save(outputSfmData, outSfMDataFilename, sfmDataIO::ESfMData::ALL)) + { + ALICEVISION_LOG_ERROR("An error occurred while trying to save '" << outSfMDataFilename << "'"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +}