diff --git a/meshroom/aliceVision/LockSfMData.py b/meshroom/aliceVision/LockSfMData.py new file mode 100644 index 0000000000..a4b6a81395 --- /dev/null +++ b/meshroom/aliceVision/LockSfMData.py @@ -0,0 +1,122 @@ +__version__ = "1.2" + +from meshroom.core import desc +from meshroom.core.utils import DESCRIBER_TYPES, VERBOSE_LEVEL + +import os.path + + +class LockSfMData(desc.AVCommandLineNode): + commandLine = "aliceVision_lockSfmData {allParams}" + size = desc.DynamicNodeSize("input") + + category = "Utils" + documentation = """ +Lock specific elements of an SfMData scene so that they are kept fixed during subsequent +bundle adjustment steps. + +An optional **Selected Views** SfMData can be provided to restrict locking to elements +associated with that subset of views. + +The following elements can be locked independently: + * **Camera Intrinsics**: all intrinsic parameters, or specific sub-parts: + * **Focal Length**: scale parameters of the camera model. + * **Principal Point**: offset parameters of the camera model. + * **Distortion**: distortion parameters of the camera model. + * **Camera Poses**: position and orientation of all reconstructed cameras. + * **Landmarks**: 3D points of the sparse point cloud, optionally filtered by describer type + and/or by landmark selection mode (fully or partially contained within the selected views). +""" + + inputs = [ + desc.File( + name="input", + label="SfMData", + description="Input SfMData file.", + value="", + ), + desc.File( + name="selectedViews", + label="Selected Views", + description="Optional SfMData file used to define a subset of views.\n" + "When provided, locking is restricted to elements associated with those views.", + value="", + ), + desc.BoolParam( + name="lockIntrinsics", + label="Lock Intrinsics", + description="Lock camera intrinsics.", + value=False, + ), + desc.BoolParam( + name="lockFocalLength", + label="Lock Focal Length", + description="Lock the focal length of camera intrinsics.", + value=True, + enabled=lambda node: node.lockIntrinsics.value, + ), + desc.BoolParam( + name="lockPrincipalPoint", + label="Lock Principal Point", + description="Lock the principal point of camera intrinsics.", + value=True, + enabled=lambda node: node.lockIntrinsics.value, + ), + desc.BoolParam( + name="lockDistortion", + label="Lock Distortion", + description="Lock the distortion parameters of camera intrinsics.", + value=True, + enabled=lambda node: node.lockIntrinsics.value, + ), + desc.BoolParam( + name="lockPoses", + label="Lock Poses", + description="Lock all camera poses (position and orientation).", + value=False, + ), + desc.BoolParam( + name="lockLandmarks", + label="Lock Landmarks", + description="Lock 3D landmarks (sparse point cloud).", + value=False, + ), + desc.ChoiceParam( + name="lockLandmarkTypes", + label="Landmark Types To Lock", + description="Describer types of landmarks to lock.\n" + "If empty, all landmark types will be locked.", + values=DESCRIBER_TYPES, + value=[], + exclusive=False, + joinChar=",", + enabled=lambda node: node.lockLandmarks.value, + ), + desc.ChoiceParam( + name="landmarkSelectionMode", + label="Landmark Selection Mode", + description="Determines which landmarks to lock when Selected Views is provided:\n" + " - fully_contained: lock only landmarks whose all observations belong to the selected views.\n" + " - partially_contained: lock landmarks with at least one observation in the selected views.", + value="fully_contained", + values=["fully_contained", "partially_contained"], + exclusive=True, + enabled=lambda node: node.lockLandmarks.value and bool(node.selectedViews.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="Output SfMData file with locked elements.", + value=lambda attr: "{nodeCacheFolder}/" + (os.path.splitext(os.path.basename(attr.node.input.value))[0] or "sfmData") + ".abc", + ), + ] diff --git a/src/software/pipeline/CMakeLists.txt b/src/software/pipeline/CMakeLists.txt index d66ebe5afb..f653f98c1c 100644 --- a/src/software/pipeline/CMakeLists.txt +++ b/src/software/pipeline/CMakeLists.txt @@ -357,6 +357,18 @@ if (ALICEVISION_BUILD_SFM) Boost::timer ) + # Lock SfMData elements (intrinsics, poses, landmarks) + alicevision_add_software(aliceVision_lockSfmData + SOURCE main_lockSfmData.cpp + FOLDER ${FOLDER_SOFTWARE_PIPELINE} + LINKS aliceVision_system + aliceVision_cmdline + aliceVision_feature + aliceVision_sfmData + aliceVision_sfmDataIO + Boost::program_options + ) + endif() # if(ALICEVISION_BUILD_SFM) if (ALICEVISION_BUILD_PANORAMA) diff --git a/src/software/pipeline/main_lockSfmData.cpp b/src/software/pipeline/main_lockSfmData.cpp new file mode 100644 index 0000000000..574e1419dd --- /dev/null +++ b/src/software/pipeline/main_lockSfmData.cpp @@ -0,0 +1,274 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2024 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 + +#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 2 + +using namespace aliceVision; + +namespace po = boost::program_options; + +int aliceVision_main(int argc, char** argv) +{ + // command-line parameters + std::string sfmDataFilename; + std::string sfmDataOutputFilename; + std::string selectedViewsFilename; + bool lockIntrinsics = false; + bool lockFocalLength = true; + bool lockPrincipalPoint = true; + bool lockDistortion = true; + bool lockPoses = false; + bool lockLandmarks = false; + std::string lockLandmarkTypes; + std::string landmarkSelectionMode = "fully_contained"; + + // clang-format off + po::options_description requiredParams("Required parameters"); + requiredParams.add_options() + ("input,i", po::value(&sfmDataFilename)->required(), + "SfMData file.") + ("output,o", po::value(&sfmDataOutputFilename)->required(), + "Output SfMData file."); + + po::options_description optionalParams("Optional parameters"); + optionalParams.add_options() + ("selectedViews,s", po::value(&selectedViewsFilename)->default_value(selectedViewsFilename), + "Optional SfMData file used to define a subset of views. " + "When provided, locking is restricted to elements associated with those views.") + ("lockIntrinsics", po::value(&lockIntrinsics)->default_value(lockIntrinsics), + "Lock camera intrinsics.") + ("lockFocalLength", po::value(&lockFocalLength)->default_value(lockFocalLength), + "Lock the focal length of camera intrinsics. Only used when lockIntrinsics is enabled.") + ("lockPrincipalPoint", po::value(&lockPrincipalPoint)->default_value(lockPrincipalPoint), + "Lock the principal point of camera intrinsics. Only used when lockIntrinsics is enabled.") + ("lockDistortion", po::value(&lockDistortion)->default_value(lockDistortion), + "Lock the distortion parameters of camera intrinsics. Only used when lockIntrinsics is enabled.") + ("lockPoses", po::value(&lockPoses)->default_value(lockPoses), + "Lock all camera poses.") + ("lockLandmarks", po::value(&lockLandmarks)->default_value(lockLandmarks), + "Lock landmarks.") + ("lockLandmarkTypes", po::value(&lockLandmarkTypes)->default_value(lockLandmarkTypes), + "Comma-separated list of landmark describer types to lock (e.g. 'sift,dspsift'). " + "If empty, all landmark types will be locked.") + ("landmarkSelectionMode", po::value(&landmarkSelectionMode)->default_value(landmarkSelectionMode), + "Landmark selection mode when selectedViews is provided: " + "'fully_contained' to lock landmarks whose all observations belong to the selected views, " + "'partially_contained' to lock landmarks with at least one observation in the selected views."); + // clang-format on + + CmdLine cmdline("AliceVision lockSfmData"); + cmdline.add(requiredParams); + cmdline.add(optionalParams); + if (!cmdline.execute(argc, argv)) + { + return EXIT_FAILURE; + } + + // 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; + } + + // Validate landmarkSelectionMode + if (landmarkSelectionMode != "fully_contained" && landmarkSelectionMode != "partially_contained") + { + ALICEVISION_LOG_ERROR("Invalid landmarkSelectionMode '" << landmarkSelectionMode + << "'. Expected 'fully_contained' or 'partially_contained'."); + return EXIT_FAILURE; + } + + // Build the set of selected view IDs (from the optional selectedViews SfMData) + std::set selectedViewIds; + const bool hasSelectedViews = !selectedViewsFilename.empty(); + if (hasSelectedViews) + { + sfmData::SfMData selectedViewsSfmData; + // Only VIEWS data is needed since we only extract view IDs from this SfMData + if (!sfmDataIO::load(selectedViewsSfmData, selectedViewsFilename, sfmDataIO::ESfMData::VIEWS)) + { + ALICEVISION_LOG_ERROR("The selectedViews SfMData file '" + selectedViewsFilename + "' cannot be read."); + return EXIT_FAILURE; + } + for (const auto& [viewId, _] : selectedViewsSfmData.getViews()) + { + selectedViewIds.insert(viewId); + } + ALICEVISION_LOG_INFO("Selected views subset contains " << selectedViewIds.size() << " view(s)."); + } + + // Lock camera intrinsics + if (lockIntrinsics) + { + // If selectedViews is provided, collect the intrinsic IDs referenced by those views + std::set intrinsicIdsToLock; + if (hasSelectedViews) + { + for (const auto& [viewId, view] : sfmData.getViews()) + { + if (selectedViewIds.count(viewId) && view->getIntrinsicId() != UndefinedIndexT) + { + intrinsicIdsToLock.insert(view->getIntrinsicId()); + } + } + } + + std::size_t lockedCount = 0; + for (auto& [intrinsicId, intrinsic] : sfmData.getIntrinsics().valueRange()) + { + if (hasSelectedViews && !intrinsicIdsToLock.count(intrinsicId)) + { + continue; + } + + if (lockFocalLength && lockPrincipalPoint && lockDistortion) + { + // Lock all intrinsic parts at once using the global lock + intrinsic.lock(); + } + else + { + // Lock only the requested parts + auto* isoPtr = dynamic_cast(&intrinsic); + if (isoPtr) + { + isoPtr->setScaleLocked(lockFocalLength); + isoPtr->setOffsetLocked(lockPrincipalPoint); + } + + if (lockDistortion) + { + auto* isodPtr = dynamic_cast(&intrinsic); + if (isodPtr && isodPtr->getDistortion()) + { + isodPtr->getDistortion()->setLocked(true); + } + } + } + ++lockedCount; + } + ALICEVISION_LOG_INFO("Processed " << lockedCount << " camera intrinsic(s)."); + } + + // Lock camera poses + if (lockPoses) + { + // If selectedViews is provided, collect the pose IDs referenced by those views + std::set poseIdsToLock; + if (hasSelectedViews) + { + for (const auto& [viewId, view] : sfmData.getViews()) + { + if (selectedViewIds.count(viewId) && view->getPoseId() != UndefinedIndexT) + { + poseIdsToLock.insert(view->getPoseId()); + } + } + } + + std::size_t lockedCount = 0; + for (auto& [poseId, pose] : sfmData.getPoses().valueRange()) + { + if (hasSelectedViews && !poseIdsToLock.count(poseId)) + { + continue; + } + pose.lock(); + ++lockedCount; + } + ALICEVISION_LOG_INFO("Locked " << lockedCount << " camera pose(s)."); + } + + // Lock landmarks (optionally filtered by describer type and/or selected views) + if (lockLandmarks) + { + std::set typesToLock; + if (!lockLandmarkTypes.empty()) + { + const std::vector typesList = + feature::EImageDescriberType_stringToEnums(lockLandmarkTypes); + typesToLock.insert(typesList.begin(), typesList.end()); + } + + std::size_t lockedCount = 0; + for (auto& [_, landmark] : sfmData.getLandmarks()) + { + if (!typesToLock.empty() && !typesToLock.count(landmark.getDescType())) + { + continue; + } + + if (hasSelectedViews) + { + const Observations& obs = landmark.getObservations(); + bool include = false; + if (landmarkSelectionMode == "partially_contained") + { + // At least one observation in the selected views + for (const auto& [viewId, _] : obs) + { + if (selectedViewIds.count(viewId)) + { + include = true; + break; + } + } + } + else // fully_contained + { + // All observations must belong to selected views + include = !obs.empty(); + for (const auto& [viewId, _] : obs) + { + if (!selectedViewIds.count(viewId)) + { + include = false; + break; + } + } + } + + if (!include) + { + continue; + } + } + + landmark.setLocked(true); + ++lockedCount; + } + ALICEVISION_LOG_INFO("Locked " << lockedCount << " landmark(s) out of " << sfmData.getLandmarks().size() << "."); + } + + // Save output SfMData + if (!sfmDataIO::save(sfmData, sfmDataOutputFilename, sfmDataIO::ESfMData::ALL)) + { + ALICEVISION_LOG_ERROR("The output SfMData file '" + sfmDataOutputFilename + "' cannot be written."); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +}