diff --git a/meshroom/aliceVision/GeometricFilterApplying.py b/meshroom/aliceVision/GeometricFilterApplying.py new file mode 100644 index 0000000000..330352ab95 --- /dev/null +++ b/meshroom/aliceVision/GeometricFilterApplying.py @@ -0,0 +1,89 @@ +__version__ = "1.0" + +from meshroom.core import desc +from meshroom.core.utils import DESCRIBER_TYPES, VERBOSE_LEVEL + + +class GeometricFilterApplying(desc.AVCommandLineNode): + commandLine = 'aliceVision_geometricFilterApplying {allParams}' + size = desc.DynamicNodeSize('input') + parallelization = desc.Parallelization(blockSize=20) + commandLineRange = '--rangeIteration {rangeIteration} --rangeBlocksCount {rangeBlocksCount}' + + category = 'Sparse Reconstruction' + documentation = ''' +Apply precomputed transforms to matches to filter geometric matches +''' + + inputs = [ + desc.File( + name="input", + label="SfMData", + description="Input SfMData file.", + value="", + ), + desc.ListAttribute( + elementDesc=desc.File( + name="featuresFolder", + label="Features Folder", + description="Folder containing some extracted features and descriptors.", + value="", + ), + name="featuresFolders", + label="Features Folders", + description="Folder(s) containing the extracted features and descriptors.", + exposed=True, + ), + desc.ListAttribute( + elementDesc=desc.File( + name="matchesFolder", + label="Matches Folder", + description="Folder containing some matches.", + value="", + ), + name="matchesFolders", + label="Matches Folders", + description="Folder(s) in which computed matches are stored.", + exposed=True, + ), + desc.File( + name="filters", + label="Filters Folder", + description="Path to a folder in which the computed filters are stored.", + value="", + exposed=True + ), + desc.ChoiceParam( + name="describerTypes", + label="Describer Types", + description="Describer types used to describe an image.", + values=DESCRIBER_TYPES, + value=["dspsift"], + exclusive=False, + joinChar=",", + exposed=True, + ), + desc.IntParam( + name="maxMatches", + label="Max Matches", + description="Maximum number of matches to keep.", + value=0, + range=(0, 10000, 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="Matches Folder", + description="Path to a folder in which the computed matches are stored.", + value="{nodeCacheFolder}", + ), + ] diff --git a/meshroom/aliceVision/GeometricFilterEstimating.py b/meshroom/aliceVision/GeometricFilterEstimating.py new file mode 100644 index 0000000000..b5c4d0a1ab --- /dev/null +++ b/meshroom/aliceVision/GeometricFilterEstimating.py @@ -0,0 +1,104 @@ +__version__ = "1.0" + +from meshroom.core import desc +from meshroom.core.utils import DESCRIBER_TYPES, VERBOSE_LEVEL + + +class GeometricFilterEstimating(desc.AVCommandLineNode): + commandLine = 'aliceVision_geometricFilterEstimating {allParams}' + size = desc.DynamicNodeSize('input') + parallelization = desc.Parallelization(blockSize=20) + commandLineRange = '--rangeIteration {rangeIteration} --rangeBlocksCount {rangeBlocksCount}' + + category = 'Sparse Reconstruction' + documentation = ''' +It performs a geometric filtering of the photometric match candidates. +It uses the features positions in the images to make a geometric filtering by using epipolar geometry in an outlier detection framework +called RANSAC (RANdom SAmple Consensus). It randomly selects a small set of feature correspondences and compute the fundamental (or essential) matrix, +then it checks the number of features that validates this model and iterate through the RANSAC framework. + +## Online +[https://alicevision.org/#photogrammetry/feature_matching](https://alicevision.org/#photogrammetry/feature_matching) +''' + + inputs = [ + desc.File( + name="input", + label="SfMData", + description="Input SfMData file.", + value="", + ), + desc.ListAttribute( + elementDesc=desc.File( + name="featuresFolder", + label="Features Folder", + description="Folder containing some extracted features and descriptors.", + value="", + ), + name="featuresFolders", + label="Features Folders", + description="Folder(s) containing the extracted features and descriptors.", + exposed=True, + ), + desc.ListAttribute( + elementDesc=desc.File( + name="matchesFolder", + label="Matches Folder", + description="Folder containing some matches.", + value="", + ), + name="matchesFolders", + label="Matches Folders", + description="Folder(s) in which computed matches are stored.", + exposed=True, + ), + desc.ChoiceParam( + name="describerTypes", + label="Describer Types", + description="Describer types used to describe an image.", + values=DESCRIBER_TYPES, + value=["dspsift"], + exclusive=False, + joinChar=",", + exposed=True, + ), + desc.IntParam( + name="maxIteration", + label="Max Iterations", + description="Maximum number of iterations allowed in the Ransac step.", + value=50000, + range=(1, 100000, 1), + advanced=True, + ), + desc.FloatParam( + name="geometricError", + label="Geometric Validation Error", + description="Maximum error (in pixels) allowed for features matching during geometric verification", + value=0.0, + range=(0.0, 10.0, 0.1), + advanced=True, + ), + desc.IntParam( + name="maxMatches", + label="Max Matches", + description="Maximum number of matches to keep.", + value=0, + range=(0, 10000, 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="Filters Folder", + description="Path to a folder in which the computed filters are stored.", + value="{nodeCacheFolder}", + ), + ] diff --git a/meshroom/aliceVision/ImageMatching.py b/meshroom/aliceVision/ImageMatching.py index d5a0c9f530..5467b64c61 100644 --- a/meshroom/aliceVision/ImageMatching.py +++ b/meshroom/aliceVision/ImageMatching.py @@ -63,9 +63,10 @@ class ImageMatching(desc.AVCommandLineNode): " - SequentialAndVocabularyTree: Combines sequential approach with VocTree to enable connections between keyframes at different times.\n" " - Exhaustive: Export all image pairs.\n" " - Frustum: If images have known poses, computes the intersection between cameras frustums to create the list of image pairs.\n" - " - FrustumOrVocabularyTree: If images have known poses, use frustum intersection else use VocabularyTree.\n", + " - FrustumOrVocabularyTree: If images have known poses, use frustum intersection else use VocabularyTree.\n" + " - Mirror: Try to match images with themselves. \n", value="SequentialAndVocabularyTree", - values=["VocabularyTree", "Sequential", "SequentialAndVocabularyTree", "Exhaustive", "Frustum", "FrustumOrVocabularyTree"], + values=["VocabularyTree", "Sequential", "SequentialAndVocabularyTree", "Exhaustive", "Frustum", "FrustumOrVocabularyTree", "Mirror"], ), desc.File( name="tree", diff --git a/src/aliceVision/feature/feature.i b/src/aliceVision/feature/feature.i index 546b846e43..ec7156acfa 100644 --- a/src/aliceVision/feature/feature.i +++ b/src/aliceVision/feature/feature.i @@ -9,6 +9,15 @@ %include %include +namespace std +{ + #ifdef LINUXPLATFORM + typedef long unsigned int size_t; + #else + typedef long unsigned long size_t; + #endif +} + %{ #include #include diff --git a/src/aliceVision/imageMatching/ImageMatching.cpp b/src/aliceVision/imageMatching/ImageMatching.cpp index 734cfd6559..18fae7ed9b 100644 --- a/src/aliceVision/imageMatching/ImageMatching.cpp +++ b/src/aliceVision/imageMatching/ImageMatching.cpp @@ -40,6 +40,8 @@ std::string EImageMatchingMethod_enumToString(EImageMatchingMethod m) return "Frustum"; case EImageMatchingMethod::FRUSTUM_OR_VOCABULARYTREE: return "FrustumOrVocabularyTree"; + case EImageMatchingMethod::MIRROR: + return "Mirror"; } throw std::out_of_range("Invalid EImageMatchingMethod enum: " + std::to_string(int(m))); } @@ -61,6 +63,8 @@ EImageMatchingMethod EImageMatchingMethod_stringToEnum(const std::string& m) return EImageMatchingMethod::FRUSTUM; if (mode == "frustumorvocabularytree") return EImageMatchingMethod::FRUSTUM_OR_VOCABULARYTREE; + if (mode == "mirror") + return EImageMatchingMethod::MIRROR; throw std::out_of_range("Invalid EImageMatchingMethod: " + m); } @@ -185,6 +189,14 @@ void generateSequentialMatches(const sfmData::SfMData& sfmData, size_t nbMatches } } +void generateMirrorsMatches(const sfmData::SfMData& sfmData, OrderedPairList& outPairList) +{ + for (const auto& [index, _] : sfmData.getViews()) + { + outPairList[index].insert(index); + } +} + void generateAllMatchesInOneMap(const std::set& viewIds, OrderedPairList& outPairList) { for (const IndexT imgA : viewIds) diff --git a/src/aliceVision/imageMatching/ImageMatching.hpp b/src/aliceVision/imageMatching/ImageMatching.hpp index a6828dc8cd..452a95e8eb 100644 --- a/src/aliceVision/imageMatching/ImageMatching.hpp +++ b/src/aliceVision/imageMatching/ImageMatching.hpp @@ -54,7 +54,8 @@ enum class EImageMatchingMethod SEQUENTIAL = 2, SEQUENTIAL_AND_VOCABULARYTREE = 3, FRUSTUM = 4, - FRUSTUM_OR_VOCABULARYTREE = 5 + FRUSTUM_OR_VOCABULARYTREE = 5, + MIRROR = 6, }; /** @@ -116,6 +117,7 @@ EImageMatchingMode EImageMatchingMode_stringToEnum(const std::string& modeMultiS void convertAllMatchesToPairList(const PairList& allMatches, std::size_t numMatches, OrderedPairList& outPairList); void generateSequentialMatches(const sfmData::SfMData& sfmData, size_t nbMatches, OrderedPairList& outPairList); +void generateMirrorsMatches(const sfmData::SfMData& sfmData, OrderedPairList& outPairList); void generateAllMatchesInOneMap(const std::set& viewIds, OrderedPairList& outPairList); void generateAllMatchesBetweenTwoMap(const std::set& viewIdsA, const std::set& viewIdsB, OrderedPairList& outPairList); diff --git a/src/aliceVision/matchingImageCollection/CMakeLists.txt b/src/aliceVision/matchingImageCollection/CMakeLists.txt index c56a1c0502..dc4417afe6 100644 --- a/src/aliceVision/matchingImageCollection/CMakeLists.txt +++ b/src/aliceVision/matchingImageCollection/CMakeLists.txt @@ -14,6 +14,7 @@ set(matching_collection_images_files_headers ImagePairListIO.hpp geometricFilterUtils.hpp pairBuilder.hpp + GeometricInfo.hpp ) # Sources @@ -26,6 +27,7 @@ set(matching_collection_images_files_sources geometricFilterUtils.cpp ImagePairListIO.cpp pairBuilder.cpp + GeometricInfo.cpp ) alicevision_add_library(aliceVision_matchingImageCollection @@ -38,6 +40,7 @@ alicevision_add_library(aliceVision_matchingImageCollection aliceVision_sfmData Boost::boost Boost::timer + Boost::json PRIVATE_LINKS aliceVision_system ${CERES_LIBRARIES} diff --git a/src/aliceVision/matchingImageCollection/GeometricFilter.hpp b/src/aliceVision/matchingImageCollection/GeometricFilter.hpp index 62e9407d5a..f5dc39e809 100644 --- a/src/aliceVision/matchingImageCollection/GeometricFilter.hpp +++ b/src/aliceVision/matchingImageCollection/GeometricFilter.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -28,7 +29,7 @@ using namespace aliceVision::matching; * or all the pairs and regions correspondences contained in the putativeMatches set. * Allow to keep only geometrically coherent matches. * It discards pairs that do not lead to a valid robust model estimation. - * @param[out] geometricMatches + * @param[out] out_geometricMatches * @param[in] sfmData * @param[in] regionsPerView * @param[in] functor @@ -39,7 +40,7 @@ using namespace aliceVision::matching; */ template void robustModelEstimation(PairwiseMatches& out_geometricMatches, - const sfmData::SfMData* sfmData, + const sfmData::SfMData& sfmData, const feature::RegionsPerView& regionsPerView, const GeometryFunctor& functor, const PairwiseMatches& putativeMatches, @@ -73,7 +74,6 @@ void robustModelEstimation(PairwiseMatches& out_geometricMatches, { MatchesPerDescType guidedGeometricInliers; geometricFilter.Geometry_guided_matching(sfmData, regionsPerView, imagePair, distanceRatio, guidedGeometricInliers); - // ALICEVISION_LOG_DEBUG("#before/#after: " << putative_inliers.size() << "/" << guided_geometric_inliers.size()); std::swap(inliers, guidedGeometricInliers); } @@ -87,6 +87,64 @@ void robustModelEstimation(PairwiseMatches& out_geometricMatches, } } +/** + * @brief Perform robust model estimation (with optional guided_matching) + * or all the pairs and regions correspondences contained in the putativeMatches set. + * Allow to keep only geometrically coherent matches. + * It discards pairs that do not lead to a valid robust model estimation. + * @param[out] out_geometricInfos + * @param[in] sfmData + * @param[in] regionsPerView + * @param[in] functor + * @param[in] putativeMatches + * @param[in] randomNumberGenerator + */ +template +void robustModelEstimation(PairwiseGeometricInfo& out_geometricInfos, + const sfmData::SfMData& sfmData, + const feature::RegionsPerView& regionsPerView, + const GeometryFunctor& functor, + const PairwiseMatches& putativeMatches, + std::mt19937& randomNumberGenerator) +{ + out_geometricInfos.clear(); + + auto progressDisplay = system::createConsoleProgressDisplay(putativeMatches.size(), std::cout, "Robust Model Estimation\n"); + +#pragma omp parallel for schedule(dynamic) + for (int i = 0; i < (int)putativeMatches.size(); ++i) + { + PairwiseMatches::const_iterator iter = putativeMatches.begin(); + std::advance(iter, i); + + const Pair currentPair = iter->first; + const MatchesPerDescType& putativeMatchesPerType = iter->second; + const Pair& imagePair = iter->first; + + // apply the geometric filter (robust model estimation) + { + MatchesPerDescType inliers; + GeometryFunctor geometricFilter = functor; // use a copy since we are in a multi-thread context + const EstimationStatus state = + geometricFilter.geometricEstimation(sfmData, regionsPerView, imagePair, putativeMatchesPerType, randomNumberGenerator, inliers); + + if (state.hasStrongSupport) + { +#pragma omp critical + { + PairGeometricInfo info; + info.model = geometricFilter.getMatrix(); + info.inliers = inliers.getNbAllMatches(); + info.threshold = geometricFilter.m_dPrecision_robust; + info.type = geometricFilter.getType(); + out_geometricInfos.emplace(currentPair, info); + } + } + } + ++progressDisplay; + } +} + /** * @brief removePoorlyOverlappingImagePairs Removes image pairs from the given list of geometric * matches that have poor overlap according to the supplied criteria. diff --git a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix.hpp b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix.hpp index f92bab3ce5..44712c2f0e 100644 --- a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix.hpp +++ b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix.hpp @@ -7,6 +7,8 @@ #pragma once +#include + namespace aliceVision { namespace feature { @@ -31,6 +33,10 @@ struct GeometricFilterMatrix m_stIteration(stIteration) {} + virtual Eigen::Matrix3d getMatrix() = 0; + + virtual EGeometricFilterType getType() = 0; + /** * @brief Geometry_guided_matching * @param sfm_data @@ -40,7 +46,7 @@ struct GeometricFilterMatrix * @param matches * @return */ - virtual bool Geometry_guided_matching(const sfmData::SfMData* sfmData, + virtual bool Geometry_guided_matching(const sfmData::SfMData& sfmData, const feature::RegionsPerView& regionsPerView, const Pair imageIdsPair, const double dDistanceRatio, diff --git a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_E_AC.hpp b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_E_AC.hpp index 4d2723f23d..e8d5bd4e19 100644 --- a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_E_AC.hpp +++ b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_E_AC.hpp @@ -10,8 +10,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -41,7 +43,7 @@ struct GeometricFilterMatrix_E_AC : public GeometricFilterMatrix * relating them using a robust method (like A Contrario Ransac). */ template - EstimationStatus geometricEstimation(const sfmData::SfMData* sfmData, + EstimationStatus geometricEstimation(const sfmData::SfMData& sfmData, const Regions_or_Features_ProviderT& regionsPerView, const Pair& pairIndex, const matching::MatchesPerDescType& putativeMatchesPerType, @@ -60,12 +62,12 @@ struct GeometricFilterMatrix_E_AC : public GeometricFilterMatrix return EstimationStatus(false, false); // reject pair with missing Intrinsic information - const sfmData::View& viewI = sfmData->getView(I); - const sfmData::View& viewJ = sfmData->getView(J); + const sfmData::View& viewI = sfmData.getView(I); + const sfmData::View& viewJ = sfmData.getView(J); // Check that valid cameras can be retrieved for the pair of views - std::shared_ptr cam_I = sfmData->getIntrinsicSharedPtr(viewI.getIntrinsicId()); - std::shared_ptr cam_J = sfmData->getIntrinsicSharedPtr(viewJ.getIntrinsicId()); + std::shared_ptr cam_I = sfmData.getIntrinsicSharedPtr(viewI.getIntrinsicId()); + std::shared_ptr cam_J = sfmData.getIntrinsicSharedPtr(viewJ.getIntrinsicId()); if (!cam_I || !cam_J) return EstimationStatus(false, false); @@ -127,7 +129,7 @@ struct GeometricFilterMatrix_E_AC : public GeometricFilterMatrix * @param matches * @return */ - bool Geometry_guided_matching(const sfmData::SfMData* sfmData, + bool Geometry_guided_matching(const sfmData::SfMData& sfmData, const feature::RegionsPerView& regionsPerView, const Pair imageIdsPair, const double dDistanceRatio, @@ -139,14 +141,14 @@ struct GeometricFilterMatrix_E_AC : public GeometricFilterMatrix const IndexT I = imageIdsPair.first; const IndexT J = imageIdsPair.second; - const sfmData::View& viewI = sfmData->getView(I); - const sfmData::View& viewJ = sfmData->getView(J); + const sfmData::View& viewI = sfmData.getView(I); + const sfmData::View& viewJ = sfmData.getView(J); // check that valid cameras can be retrieved for the pair of views const camera::IntrinsicBase* camI = - sfmData->getIntrinsics().count(viewI.getIntrinsicId()) ? sfmData->getIntrinsics().at(viewI.getIntrinsicId()).get() : nullptr; + sfmData.getIntrinsics().count(viewI.getIntrinsicId()) ? sfmData.getIntrinsics().at(viewI.getIntrinsicId()).get() : nullptr; const camera::IntrinsicBase* camJ = - sfmData->getIntrinsics().count(viewJ.getIntrinsicId()) ? sfmData->getIntrinsics().at(viewJ.getIntrinsicId()).get() : nullptr; + sfmData.getIntrinsics().count(viewJ.getIntrinsicId()) ? sfmData.getIntrinsics().at(viewJ.getIntrinsicId()).get() : nullptr; if (!camI || !camJ) return false; @@ -175,6 +177,13 @@ struct GeometricFilterMatrix_E_AC : public GeometricFilterMatrix return matches.getNbAllMatches() != 0; } + Eigen::Matrix3d getMatrix() { return m_E; } + + /** + * @return geometric filter type represented by this class + */ + EGeometricFilterType getType() { return EGeometricFilterType::ESSENTIAL_MATRIX; } + // stored data Mat3 m_E; }; diff --git a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_F_AC.hpp b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_F_AC.hpp index 8a126f4284..c777844c3f 100644 --- a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_F_AC.hpp +++ b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_F_AC.hpp @@ -50,7 +50,7 @@ struct GeometricFilterMatrix_F_AC : public GeometricFilterMatrix * relating them using a robust method (like A Contrario Ransac). */ template - EstimationStatus geometricEstimation(const sfmData::SfMData* sfmData, + EstimationStatus geometricEstimation(const sfmData::SfMData& sfmData, const Regions_or_Features_ProviderT& regionsPerView, const Pair& pairIndex, const matching::MatchesPerDescType& putativeMatchesPerType, @@ -63,11 +63,11 @@ struct GeometricFilterMatrix_F_AC : public GeometricFilterMatrix const IndexT I = pairIndex.first; const IndexT J = pairIndex.second; - const sfmData::View& viewI = sfmData->getView(I); - const sfmData::View& viewJ = sfmData->getView(J); + const sfmData::View& viewI = sfmData.getView(I); + const sfmData::View& viewJ = sfmData.getView(J); - const camera::IntrinsicBase* camI = sfmData->getIntrinsicPtr(viewI.getIntrinsicId()); - const camera::IntrinsicBase* camJ = sfmData->getIntrinsicPtr(viewJ.getIntrinsicId()); + const camera::IntrinsicBase* camI = sfmData.getIntrinsicPtr(viewI.getIntrinsicId()); + const camera::IntrinsicBase* camJ = sfmData.getIntrinsicPtr(viewJ.getIntrinsicId()); const std::pair imageSizeI(viewI.getImage().getWidth(), viewI.getImage().getHeight()); const std::pair imageSizeJ(viewJ.getImage().getWidth(), viewJ.getImage().getHeight()); @@ -281,7 +281,7 @@ struct GeometricFilterMatrix_F_AC : public GeometricFilterMatrix multiview::UnnormalizerT, ModelT_>; - const KernelT kernel(xI, imageSizeI.first, imageSizeI.second, xJ, imageSizeJ.first, imageSizeJ.second, true); + const KernelT kernel(xI, imageSizeI.first, imageSizeI.first, xJ, imageSizeJ.first, imageSizeJ.first, true); // robustly estimate the Fundamental matrix with A Contrario ransac const double upperBoundPrecision = m_dPrecision; @@ -355,7 +355,7 @@ struct GeometricFilterMatrix_F_AC : public GeometricFilterMatrix * @param matches * @return */ - bool Geometry_guided_matching(const sfmData::SfMData* sfmData, + bool Geometry_guided_matching(const sfmData::SfMData& sfmData, const feature::RegionsPerView& regionsPerView, const Pair imageIdsPair, const double dDistanceRatio, @@ -367,14 +367,14 @@ struct GeometricFilterMatrix_F_AC : public GeometricFilterMatrix const IndexT I = imageIdsPair.first; const IndexT J = imageIdsPair.second; - const sfmData::View& viewI = sfmData->getView(I); - const sfmData::View& viewJ = sfmData->getView(J); + const sfmData::View& viewI = sfmData.getView(I); + const sfmData::View& viewJ = sfmData.getView(J); // retrieve corresponding pair camera intrinsic if any const camera::IntrinsicBase* camI = - sfmData->getIntrinsics().count(viewI.getIntrinsicId()) ? sfmData->getIntrinsics().at(viewI.getIntrinsicId()).get() : nullptr; + sfmData.getIntrinsics().count(viewI.getIntrinsicId()) ? sfmData.getIntrinsics().at(viewI.getIntrinsicId()).get() : nullptr; const camera::IntrinsicBase* camJ = - sfmData->getIntrinsics().count(viewJ.getIntrinsicId()) ? sfmData->getIntrinsics().at(viewJ.getIntrinsicId()).get() : nullptr; + sfmData.getIntrinsics().count(viewJ.getIntrinsicId()) ? sfmData.getIntrinsics().at(viewJ.getIntrinsicId()).get() : nullptr; robustEstimation::Mat3Model model(m_F); @@ -392,6 +392,21 @@ struct GeometricFilterMatrix_F_AC : public GeometricFilterMatrix return matches.getNbAllMatches() != 0; } + Eigen::Matrix3d getMatrix() { return m_F; } + + /** + * @return geometric filter type represented by this class + */ + EGeometricFilterType getType() + { + if (m_estimateDistortion) + { + return EGeometricFilterType::FUNDAMENTAL_WITH_DISTORTION; + } + + return EGeometricFilterType::FUNDAMENTAL_MATRIX; + } + // Stored data Mat3 m_F; robustEstimation::ERobustEstimator m_estimator; diff --git a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_HGrowing.cpp b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_HGrowing.cpp index 91e9d8d0bb..fa5ccaa7b0 100644 --- a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_HGrowing.cpp +++ b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_HGrowing.cpp @@ -4,7 +4,6 @@ // 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 "GeometricFilterMatrix_HGrowing.hpp" #include diff --git a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_HGrowing.hpp b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_HGrowing.hpp index e747619adb..3b1b71801d 100644 --- a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_HGrowing.hpp +++ b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_HGrowing.hpp @@ -13,7 +13,6 @@ #include #include - #include #include @@ -94,7 +93,7 @@ struct HGrowingFilteringParam HGrowingFilteringParam(std::size_t maxNbHomographies, std::size_t minNbMatchesPerH, const GrowParameters& growParam) : _maxNbHomographies(maxNbHomographies), _minNbMatchesPerH(minNbMatchesPerH), - _growParam(growParam){}; + _growParam(growParam) {}; /// Max. number of homographies to estimate. std::size_t _maxNbHomographies{10}; @@ -176,7 +175,7 @@ struct GeometricFilterMatrix_HGrowing : public GeometricFilterMatrix * @return The estimation status. */ template - EstimationStatus geometricEstimation(const sfmData::SfMData* sfmData, + EstimationStatus geometricEstimation(const sfmData::SfMData& sfmData, const Regions_or_Features_ProviderT& regionsPerView, const Pair& pairIndex, const matching::MatchesPerDescType& putativeMatchesPerType, @@ -213,8 +212,8 @@ struct GeometricFilterMatrix_HGrowing : public GeometricFilterMatrix const IndexT viewId_I = pairIndex.first; const IndexT viewId_J = pairIndex.second; - const sfmData::View& viewI = *(sfmData->getViews().at(viewId_I)); - const sfmData::View& viewJ = *(sfmData->getViews().at(viewId_J)); + const sfmData::View& viewI = sfmData.getView(viewId_I); + const sfmData::View& viewJ = sfmData.getView(viewId_J); for (const EImageDescriberType& descType : descTypes) { @@ -276,7 +275,7 @@ struct GeometricFilterMatrix_HGrowing : public GeometricFilterMatrix * @param matches * @return */ - bool Geometry_guided_matching(const sfmData::SfMData* sfmData, + bool Geometry_guided_matching(const sfmData::SfMData& sfmData, const feature::RegionsPerView& regionsPerView, const Pair imageIdsPair, const double dDistanceRatio, @@ -325,6 +324,22 @@ struct GeometricFilterMatrix_HGrowing : public GeometricFilterMatrix */ bool getMatches(const feature::EImageDescriberType& descType, const IndexT homographyId, matching::IndMatches& matches) const; + /** + * @brief A fake implementation of getMatrix + * Should not be used for homography growing + * @throw always throw an error + */ + Eigen::Matrix3d getMatrix() + { + ALICEVISION_THROW_ERROR("Homography growing has no single model"); + return Eigen::Matrix3d::Identity(); + } + + /** + * @return geometric filter type represented by this class + */ + EGeometricFilterType getType() { return EGeometricFilterType::HOMOGRAPHY_GROWING; } + private: // -- Results container diff --git a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_H_AC.hpp b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_H_AC.hpp index d3379ace73..30814bf4af 100644 --- a/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_H_AC.hpp +++ b/src/aliceVision/matchingImageCollection/GeometricFilterMatrix_H_AC.hpp @@ -10,8 +10,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -37,7 +39,7 @@ struct GeometricFilterMatrix_H_AC : public GeometricFilterMatrix * relating them using a robust method (like A Contrario Ransac). */ template - EstimationStatus geometricEstimation(const sfmData::SfMData* sfmData, + EstimationStatus geometricEstimation(const sfmData::SfMData& sfmData, const Regions_or_Features_ProviderT& regionsPerView, const Pair& pairIndex, const matching::MatchesPerDescType& putativeMatchesPerType, @@ -50,8 +52,8 @@ struct GeometricFilterMatrix_H_AC : public GeometricFilterMatrix const IndexT I = pairIndex.first; const IndexT J = pairIndex.second; - const sfmData::View& viewI = sfmData->getView(I); - const sfmData::View& viewJ = sfmData->getView(J); + const sfmData::View& viewI = sfmData.getView(I); + const sfmData::View& viewJ = sfmData.getView(J); const std::vector descTypes = regionsPerView.getCommonDescTypes(pairIndex); @@ -157,7 +159,7 @@ struct GeometricFilterMatrix_H_AC : public GeometricFilterMatrix * @param matches * @return */ - bool Geometry_guided_matching(const sfmData::SfMData* sfmData, + bool Geometry_guided_matching(const sfmData::SfMData& sfmData, const feature::RegionsPerView& regionsPerView, const Pair imageIdsPair, const double dDistanceRatio, @@ -174,14 +176,14 @@ struct GeometricFilterMatrix_H_AC : public GeometricFilterMatrix const IndexT I = imageIdsPair.first; const IndexT J = imageIdsPair.second; - const sfmData::View& viewI = sfmData->getView(I); - const sfmData::View& viewJ = sfmData->getView(J); + const sfmData::View& viewI = sfmData.getView(I); + const sfmData::View& viewJ = sfmData.getView(J); // retrieve corresponding pair camera intrinsic if any const camera::IntrinsicBase* camI = - sfmData->getIntrinsics().count(viewI.getIntrinsicId()) ? sfmData->getIntrinsics().at(viewI.getIntrinsicId()).get() : nullptr; + sfmData.getIntrinsics().count(viewI.getIntrinsicId()) ? sfmData.getIntrinsics().at(viewI.getIntrinsicId()).get() : nullptr; const camera::IntrinsicBase* camJ = - sfmData->getIntrinsics().count(viewJ.getIntrinsicId()) ? sfmData->getIntrinsics().at(viewJ.getIntrinsicId()).get() : nullptr; + sfmData.getIntrinsics().count(viewJ.getIntrinsicId()) ? sfmData.getIntrinsics().at(viewJ.getIntrinsicId()).get() : nullptr; robustEstimation::Mat3Model model(m_H); @@ -229,6 +231,13 @@ struct GeometricFilterMatrix_H_AC : public GeometricFilterMatrix return matches.getNbAllMatches() != 0; } + Eigen::Matrix3d getMatrix() { return m_H; } + + /** + * @return geometric filter type represented by this class + */ + EGeometricFilterType getType() { return EGeometricFilterType::HOMOGRAPHY_MATRIX; } + // stored data Mat3 m_H; }; diff --git a/src/aliceVision/matchingImageCollection/GeometricInfo.cpp b/src/aliceVision/matchingImageCollection/GeometricInfo.cpp new file mode 100644 index 0000000000..b6c29a1ec2 --- /dev/null +++ b/src/aliceVision/matchingImageCollection/GeometricInfo.cpp @@ -0,0 +1,77 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2025 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 + +namespace aliceVision { +namespace matchingImageCollection { + +void tag_invoke(const boost::json::value_from_tag&, boost::json::value& jv, aliceVision::matchingImageCollection::PairGeometricInfo const& input) +{ + // ATM, all model are 3x3 + Eigen::Matrix3d M = input.model; + + jv = { + {"type", EGeometricFilterType_enumToString(input.type)}, + {"model", boost::json::value_from(M)}, + {"threshold", boost::json::value_from(input.threshold)}, + {"inliers", boost::json::value_from(input.inliers)}, + }; +} + +aliceVision::matchingImageCollection::PairGeometricInfo tag_invoke(boost::json::value_to_tag, + boost::json::value const& jv) +{ + const boost::json::object& obj = jv.as_object(); + + aliceVision::matchingImageCollection::PairGeometricInfo ret; + ret.type = EGeometricFilterType_stringToEnum(boost::json::value_to(obj.at("type"))); + ret.inliers = boost::json::value_to(obj.at("inliers")); + ret.threshold = boost::json::value_to(obj.at("threshold")); + ret.model = boost::json::value_to(obj.at("model")); + + return ret; +} + +bool saveGeometricInfos(const std::string& path, const PairwiseGeometricInfo& infos) +{ + std::ofstream giFile(path); + if (giFile.is_open() == false) + { + return false; + } + + boost::json::value jv = boost::json::value_from(infos); + + giFile << boost::json::serialize(jv); + giFile.close(); + + return true; +} + +bool loadGeometricInfos(const std::string& path, PairwiseGeometricInfo& infos) +{ + std::ifstream giFile(path); + if (giFile.is_open() == false) + { + return false; + } + + std::stringstream buffer; + buffer << giFile.rdbuf(); + giFile.close(); + + // Parse json + boost::json::value jv = boost::json::parse(buffer.str()); + infos = PairwiseGeometricInfo(map_value_to(jv)); + + return true; +} + +} // namespace matchingImageCollection +} // namespace aliceVision \ No newline at end of file diff --git a/src/aliceVision/matchingImageCollection/GeometricInfo.hpp b/src/aliceVision/matchingImageCollection/GeometricInfo.hpp new file mode 100644 index 0000000000..2ecb93328e --- /dev/null +++ b/src/aliceVision/matchingImageCollection/GeometricInfo.hpp @@ -0,0 +1,48 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2025 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 + +namespace aliceVision { +namespace matchingImageCollection { + +struct PairGeometricInfo +{ + Eigen::MatrixXd model; + size_t inliers; + double threshold; + EGeometricFilterType type; +}; + +typedef std::map PairwiseGeometricInfo; + +/** + * @brief Serialize PairGeometricInfo to JSON object. + */ +void tag_invoke(const boost::json::value_from_tag&, boost::json::value& jv, aliceVision::matchingImageCollection::PairGeometricInfo const& input); + +/** + * @brief save Geometric infos to a file + * @param path the path to the output file + * @param infos the PairwiseGeometricInfo to save + * @return false on failure + */ +bool saveGeometricInfos(const std::string& path, const PairwiseGeometricInfo& infos); + +/** + * @brief load Geometric infos to a file + * @param path the path to the input file + * @param infos the PairwiseGeometricInfo to load + * @return false on failure + */ +bool loadGeometricInfos(const std::string& path, PairwiseGeometricInfo& infos); + +} // namespace matchingImageCollection +} // namespace aliceVision \ No newline at end of file diff --git a/src/aliceVision/matchingImageCollection/ImagePairListIO.cpp b/src/aliceVision/matchingImageCollection/ImagePairListIO.cpp index 9cd15baf39..5270f21a9f 100644 --- a/src/aliceVision/matchingImageCollection/ImagePairListIO.cpp +++ b/src/aliceVision/matchingImageCollection/ImagePairListIO.cpp @@ -14,7 +14,7 @@ namespace aliceVision { namespace matchingImageCollection { -bool loadPairs(std::istream& stream, PairSet& pairs, int rangeStart, int rangeSize) +bool loadPairs(std::istream& stream, PairSet& pairs, int rangeStart, int rangeSize, bool useSymmetry) { std::size_t nbLine = 0; std::string sValue; @@ -49,19 +49,25 @@ bool loadPairs(std::istream& stream, PairSet& pairs, int rangeStart, int rangeSi oss.clear(); oss.str(vec_str[i]); oss >> J; - if (I == J) + + Pair pairToInsert; + + if (useSymmetry) { - ALICEVISION_LOG_WARNING("loadPairs: Invalid input file. Image " << I << " sees itself."); - return false; + pairToInsert = (I < J) ? std::make_pair(I, J) : std::make_pair(J, I); } - Pair pairToInsert = (I < J) ? std::make_pair(I, J) : std::make_pair(J, I); + else + { + pairToInsert = std::make_pair(I, J); + } + if (pairs.find(pairToInsert) != pairs.end()) { // There is no reason to have the same image pair twice in the list of image pairs // to match. ALICEVISION_LOG_WARNING("loadPairs: image pair (" << I << ", " << J << ") already added."); } - ALICEVISION_LOG_INFO("loadPairs: image pair (" << I << ", " << J << ") added."); + ALICEVISION_LOG_TRACE("loadPairs: image pair (" << I << ", " << J << ") added."); pairs.insert(pairToInsert); } } @@ -97,7 +103,8 @@ void savePairs(std::ostream& stream, const PairSet& pairs) bool loadPairsFromFile(const std::string& sFileName, // filename of the list file, PairSet& pairs, int rangeStart, - int rangeSize) + int rangeSize, + bool useSymmetry) { std::ifstream in(sFileName); if (!in.is_open()) @@ -106,7 +113,7 @@ bool loadPairsFromFile(const std::string& sFileName, // filename of the list fi return false; } - if (!loadPairs(in, pairs, rangeStart, rangeSize)) + if (!loadPairs(in, pairs, rangeStart, rangeSize, useSymmetry)) { ALICEVISION_LOG_WARNING("loadPairsFromFile: Failed to read file: \"" << sFileName << "\"."); return false; diff --git a/src/aliceVision/matchingImageCollection/ImagePairListIO.hpp b/src/aliceVision/matchingImageCollection/ImagePairListIO.hpp index 7ebd63d507..64972232ba 100644 --- a/src/aliceVision/matchingImageCollection/ImagePairListIO.hpp +++ b/src/aliceVision/matchingImageCollection/ImagePairListIO.hpp @@ -15,7 +15,7 @@ namespace matchingImageCollection { /// Load a set of PairSet from a stream /// I J K L (pair that link I) -bool loadPairs(std::istream& stream, PairSet& pairs, int rangeStart = -1, int rangeSize = 0); +bool loadPairs(std::istream& stream, PairSet& pairs, int rangeStart = -1, int rangeSize = 0, bool useSymmetry = true); /// Save a set of PairSet to a stream (one pair per line) /// I J @@ -26,7 +26,8 @@ void savePairs(std::ostream& stream, const PairSet& pairs); bool loadPairsFromFile(const std::string& sFileName, // filename of the list file, PairSet& pairs, int rangeStart = -1, - int rangeSize = 0); + int rangeSize = 0, + bool useSymmetry = true); /// Same as savePairs, but saves to a given file bool savePairsToFile(const std::string& sFileName, const PairSet& pairs); diff --git a/src/aliceVision/matchingImageCollection/geometricFilterUtils.hpp b/src/aliceVision/matchingImageCollection/geometricFilterUtils.hpp index fd778b8f3b..2aa07aba14 100644 --- a/src/aliceVision/matchingImageCollection/geometricFilterUtils.hpp +++ b/src/aliceVision/matchingImageCollection/geometricFilterUtils.hpp @@ -129,18 +129,18 @@ void fillMatricesWithUndistortFeaturesMatches(const matching::MatchesPerDescType template void fillMatricesWithUndistortFeaturesMatches(const Pair& pairIndex, const matching::MatchesPerDescType& putativeMatchesPerType, - const sfmData::SfMData* sfmData, + const sfmData::SfMData& sfmData, const feature::RegionsPerView& regionsPerView, const std::vector& descTypes, MatT& x_I, MatT& x_J) { - const sfmData::View* view_I = sfmData->getViews().at(pairIndex.first).get(); - const sfmData::View* view_J = sfmData->getViews().at(pairIndex.second).get(); + const sfmData::View& view_I = sfmData.getView(pairIndex.first); + const sfmData::View& view_J = sfmData.getView(pairIndex.second); // Retrieve corresponding pair camera intrinsic if any - const camera::IntrinsicBase* cam_I = sfmData->getIntrinsicPtr(view_I->getIntrinsicId()); - const camera::IntrinsicBase* cam_J = sfmData->getIntrinsicPtr(view_J->getIntrinsicId()); + const camera::IntrinsicBase* cam_I = sfmData.getIntrinsicPtr(view_I.getIntrinsicId()); + const camera::IntrinsicBase* cam_J = sfmData.getIntrinsicPtr(view_J.getIntrinsicId()); fillMatricesWithUndistortFeaturesMatches(putativeMatchesPerType, cam_I, diff --git a/src/aliceVision/multiview/RelativePoseKernel.hpp b/src/aliceVision/multiview/RelativePoseKernel.hpp index cf06bd5647..c0a3ab3aeb 100644 --- a/src/aliceVision/multiview/RelativePoseKernel.hpp +++ b/src/aliceVision/multiview/RelativePoseKernel.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include diff --git a/src/software/pipeline/CMakeLists.txt b/src/software/pipeline/CMakeLists.txt index cce5944bf8..6b8089e7bc 100644 --- a/src/software/pipeline/CMakeLists.txt +++ b/src/software/pipeline/CMakeLists.txt @@ -90,6 +90,36 @@ if (ALICEVISION_BUILD_SFM) Boost::program_options ) + # Geometric filtering + alicevision_add_software(aliceVision_geometricFilterEstimating + SOURCE main_geometricFilterEstimating.cpp + FOLDER ${FOLDER_SOFTWARE_PIPELINE} + LINKS aliceVision_system + aliceVision_cmdline + aliceVision_feature + aliceVision_multiview + aliceVision_matchingImageCollection + aliceVision_sfm + aliceVision_sfmData + aliceVision_sfmDataIO + Boost::program_options + ) + + # Geometric filtering + alicevision_add_software(aliceVision_geometricFilterApplying + SOURCE main_geometricFilterApplying.cpp + FOLDER ${FOLDER_SOFTWARE_PIPELINE} + LINKS aliceVision_system + aliceVision_cmdline + aliceVision_feature + aliceVision_multiview + aliceVision_matchingImageCollection + aliceVision_sfm + aliceVision_sfmData + aliceVision_sfmDataIO + Boost::program_options + ) + # Track builder alicevision_add_software(aliceVision_tracksBuilding SOURCE main_tracksBuilding.cpp diff --git a/src/software/pipeline/main_featureMatching.cpp b/src/software/pipeline/main_featureMatching.cpp index e0bdf387f3..2415f297b5 100644 --- a/src/software/pipeline/main_featureMatching.cpp +++ b/src/software/pipeline/main_featureMatching.cpp @@ -443,7 +443,7 @@ int aliceVision_main(int argc, char** argv) case EGeometricFilterType::FUNDAMENTAL_MATRIX: { matchingImageCollection::robustModelEstimation(geometricMatches, - &sfmData, + sfmData, regionPerView, GeometricFilterMatrix_F_AC(geometricErrorMax, maxIteration, geometricEstimator), mapPutativesMatches, @@ -455,7 +455,7 @@ int aliceVision_main(int argc, char** argv) case EGeometricFilterType::FUNDAMENTAL_WITH_DISTORTION: { matchingImageCollection::robustModelEstimation(geometricMatches, - &sfmData, + sfmData, regionPerView, GeometricFilterMatrix_F_AC(geometricErrorMax, maxIteration, geometricEstimator, true), mapPutativesMatches, @@ -467,7 +467,7 @@ int aliceVision_main(int argc, char** argv) case EGeometricFilterType::ESSENTIAL_MATRIX: { matchingImageCollection::robustModelEstimation(geometricMatches, - &sfmData, + sfmData, regionPerView, GeometricFilterMatrix_E_AC(geometricErrorMax, maxIteration), mapPutativesMatches, @@ -482,7 +482,7 @@ int aliceVision_main(int argc, char** argv) { const bool onlyGuidedMatching = true; matchingImageCollection::robustModelEstimation(geometricMatches, - &sfmData, + sfmData, regionPerView, GeometricFilterMatrix_H_AC(geometricErrorMax, maxIteration), mapPutativesMatches, @@ -495,7 +495,7 @@ int aliceVision_main(int argc, char** argv) case EGeometricFilterType::HOMOGRAPHY_GROWING: { matchingImageCollection::robustModelEstimation(geometricMatches, - &sfmData, + sfmData, regionPerView, GeometricFilterMatrix_HGrowing(geometricErrorMax, maxIteration), mapPutativesMatches, diff --git a/src/software/pipeline/main_geometricFilterApplying.cpp b/src/software/pipeline/main_geometricFilterApplying.cpp new file mode 100644 index 0000000000..ea8ce24452 --- /dev/null +++ b/src/software/pipeline/main_geometricFilterApplying.cpp @@ -0,0 +1,242 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2025 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 +#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; +namespace fs = std::filesystem; + +int aliceVision_main(int argc, char** argv) +{ + // command-line parameters + std::string sfmDataFilename; + std::string outputMatchesFolder; + std::string filtersFolder; + std::vector featuresFolders; + std::vector matchesFolders; + const std::string fileExtension = "txt"; + + size_t numMatchesToKeep = 0; + + std::string describerTypesName = feature::EImageDescriberType_enumToString(feature::EImageDescriberType::SIFT); + + int rangeIteration = 0; + int rangeBlocksCount = 1; + int randomSeed = std::mt19937::default_seed; + + // clang-format off + po::options_description requiredParams("Required parameters"); + requiredParams.add_options() + ("input,i", po::value(&sfmDataFilename)->required(), + "SfMData file.") + ("output,o", po::value(&outputMatchesFolder)->required(), + "Path to a folder in which computed matches will be stored.") + ("filters", po::value(&filtersFolder)->required(), + "Filters Folder.") + ("featuresFolders,f", po::value>(&featuresFolders)->multitoken(), + "Path to folder(s) containing the extracted features.") + ("matchesFolders,m", po::value>(&matchesFolders)->multitoken(), + "Path to folder(s) in which computed matches are stored."); + + po::options_description optionalParams("Optional parameters"); + optionalParams.add_options() + ("describerTypes,d", po::value(&describerTypesName)->default_value(describerTypesName), + feature::EImageDescriberType_informations().c_str()) + ("maxMatches", po::value(&numMatchesToKeep)->default_value(numMatchesToKeep), + "Maximum number pf matches to keep.") + ("rangeIteration", po::value(&rangeIteration)->default_value(rangeIteration), "Range current iteration.") + ("rangeBlocksCount", po::value(&rangeBlocksCount)->default_value(rangeBlocksCount), "Range number of blocks.") + ("randomSeed", po::value(&randomSeed)->default_value(randomSeed), + "This seed value will generate a sequence using a linear random generator. Set -1 to use a random seed.");; + // clang-format on + + CmdLine cmdline("AliceVision geometricFilterApplying"); + cmdline.add(requiredParams); + cmdline.add(optionalParams); + if (!cmdline.execute(argc, argv)) + { + return EXIT_FAILURE; + } + + std::mt19937 randomNumberGenerator(randomSeed == -1 ? std::random_device()() : randomSeed); + + // load input SfMData scene + sfmData::SfMData sfmData; + if (!sfmDataIO::load(sfmData, sfmDataFilename, sfmDataIO::ESfMData(sfmDataIO::VIEWS | sfmDataIO::INTRINSICS | sfmDataIO::EXTRINSICS))) + { + ALICEVISION_LOG_ERROR("The input SfMData file '" + sfmDataFilename + "' cannot be read."); + return EXIT_FAILURE; + } + + if (describerTypesName.empty()) + { + ALICEVISION_LOG_ERROR("Empty option: --describerMethods"); + return EXIT_FAILURE; + } + + const std::vector describerTypes = feature::EImageDescriberType_stringToEnums(describerTypesName); + + // matches reading. + // We read everything even if we only process a sub part. + matching::PairwiseMatches pairwiseMatches; + ALICEVISION_LOG_INFO("Load features matches"); + if (!sfm::loadPairwiseMatches(pairwiseMatches, sfmData, matchesFolders, describerTypes, numMatchesToKeep, 0, false)) + { + ALICEVISION_LOG_ERROR("Unable to load matches."); + return EXIT_FAILURE; + } + + int chunkStart, chunkEnd; + if (!rangeComputation(chunkStart, chunkEnd, rangeIteration, rangeBlocksCount, pairwiseMatches.size())) + { + ALICEVISION_LOG_INFO("Nothing to compute in this chunk"); + } + + // We want to process only a subset of the pairs + // Assuming range start lies between [0 and pairwiseMatches.size()] + matching::PairwiseMatches filteredMatches; + std::set filter; + + int pos = 0; + for (const auto& [pair, content] : pairwiseMatches) + { + if (pos == chunkEnd) + { + break; + } + + if (pos >= chunkStart) + { + filteredMatches[pair] = content; + filter.insert(pair.first); + filter.insert(pair.second); + } + + pos++; + } + + ALICEVISION_LOG_INFO("A total of " << pairwiseMatches.size() << " pairs has to be processed."); + ALICEVISION_LOG_INFO("Current chunk will analyze pairs from " << chunkStart << " to " << chunkEnd << "."); + + pairwiseMatches.clear(); + + // features reading + feature::RegionsPerView regionPerView; + if (!sfm::loadRegionsPerView(regionPerView, sfmData, featuresFolders, describerTypes, filter)) + { + ALICEVISION_LOG_ERROR("Invalid regions in '" + sfmDataFilename + "'"); + return EXIT_FAILURE; + } + + ALICEVISION_LOG_INFO(std::to_string(pairwiseMatches.size()) << " putative image pair matches"); + + for (const auto& imageMatch : pairwiseMatches) + { + ALICEVISION_LOG_INFO("\t- image pair (" << imageMatch.first.first + ", " << imageMatch.first.second << ") contains " + << imageMatch.second.getNbAllMatches() << " putative matches."); + } + + matchingImageCollection::PairwiseGeometricInfo geometricInfos; + std::stringstream ss; + ss << filtersFolder << "/matches_" << std::to_string(rangeIteration) << ".json"; + ALICEVISION_LOG_INFO("Loading from " << ss.str()); + if (!matchingImageCollection::loadGeometricInfos(ss.str(), geometricInfos)) + { + ALICEVISION_LOG_ERROR("Impossible to save geometricInfos"); + return EXIT_FAILURE; + } + + int count = 0; + matching::PairwiseMatches finalMatches; + + ALICEVISION_LOG_INFO("Processing all pairs"); + auto progressDisplay = system::createConsoleProgressDisplay(filteredMatches.size(), std::cout); + for (const auto& [pair, perDesc] : filteredMatches) + { + const auto it = geometricInfos.find(pair); + if (it == geometricInfos.end()) + { + progressDisplay += 1; + continue; + } + + const auto geometricInfo = it->second; + + robustEstimation::Mat3Model model(geometricInfo.model); + multiview::relativePose::FundamentalEpipolarDistanceError errorFunc; + + const IndexT idViewI = pair.first; + const IndexT idViewJ = pair.second; + const auto& viewI = sfmData.getView(idViewI); + const auto& viewJ = sfmData.getView(idViewJ); + const auto& camI = sfmData.getIntrinsicSharedPtr(viewI.getIntrinsicId()); + const auto& camJ = sfmData.getIntrinsicSharedPtr(viewJ.getIntrinsicId()); + + for (const auto& [desc, matches] : perDesc) + { + const auto& featuresI = regionPerView.getRegions(idViewI, desc).Features(); + const auto& featuresJ = regionPerView.getRegions(idViewJ, desc).Features(); + + matching::IndMatches copied; + for (const auto& match : matches) + { + const auto& ptI = featuresI[match._i].coords().cast(); + const auto& ptJ = featuresJ[match._j].coords().cast(); + + if (sqrt(errorFunc.error(model, ptI, ptJ)) < geometricInfo.threshold) + { + copied.push_back(match); + } + } + + if (copied.size() == 0) + { + continue; + } + + finalMatches[pair][desc] = copied; + } + + progressDisplay += 1; + } + + for (const auto& imageMatch : finalMatches) + { + ALICEVISION_LOG_INFO("\t- image pair (" << imageMatch.first.first + ", " << imageMatch.first.second << ") contains " + << imageMatch.second.getNbAllMatches() << " filtered matches."); + } + + // export geometric filtered matches + ALICEVISION_LOG_INFO("Save geometric matches."); + + const std::string filePrefix = std::to_string(rangeIteration) + "."; + Save(finalMatches, outputMatchesFolder, fileExtension, false, filePrefix); + + return EXIT_SUCCESS; +} diff --git a/src/software/pipeline/main_geometricFilterEstimating.cpp b/src/software/pipeline/main_geometricFilterEstimating.cpp new file mode 100644 index 0000000000..0c593caf1e --- /dev/null +++ b/src/software/pipeline/main_geometricFilterEstimating.cpp @@ -0,0 +1,201 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2025 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 + +#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; +namespace fs = std::filesystem; + +int aliceVision_main(int argc, char** argv) +{ + // command-line parameters + std::string sfmDataFilename; + std::string outputMatchesFolder; + std::vector featuresFolders; + std::vector matchesFolders; + + size_t numMatchesToKeep = 0; + double geometricErrorMax = 0.0; + int maxIteration = 50000; + + std::string describerTypesName = feature::EImageDescriberType_enumToString(feature::EImageDescriberType::SIFT); + + int rangeIteration = 0; + int rangeBlocksCount = 1; + int randomSeed = std::mt19937::default_seed; + + // clang-format off + po::options_description requiredParams("Required parameters"); + requiredParams.add_options() + ("input,i", po::value(&sfmDataFilename)->required(), + "SfMData file.") + ("output,o", po::value(&outputMatchesFolder)->required(), + "Path to a folder in which computed matches will be stored.") + ("featuresFolders,f", po::value>(&featuresFolders)->multitoken(), + "Path to folder(s) containing the extracted features.") + ("matchesFolders,m", po::value>(&matchesFolders)->multitoken(), + "Path to folder(s) in which computed matches are stored."); + + po::options_description optionalParams("Optional parameters"); + optionalParams.add_options() + ("describerTypes,d", po::value(&describerTypesName)->default_value(describerTypesName), + feature::EImageDescriberType_informations().c_str()) + ("maxMatches", po::value(&numMatchesToKeep)->default_value(numMatchesToKeep), + "Maximum number pf matches to keep.") + ("geometricError", po::value(&geometricErrorMax)->default_value(geometricErrorMax), + "Maximum error (in pixels) allowed for features matching during geometric verification. " + "If set to 0, it lets the ACRansac select an optimal value.") + ("maxIteration", po::value(&maxIteration)->default_value(maxIteration), + "Maximum number of iterations allowed in Ransac step.") + ("rangeIteration", po::value(&rangeIteration)->default_value(rangeIteration), "Range current iteration id.") + ("rangeBlocksCount", po::value(&rangeBlocksCount)->default_value(rangeBlocksCount), "Range blocks count.") + ("randomSeed", po::value(&randomSeed)->default_value(randomSeed), + "This seed value will generate a sequence using a linear random generator. Set -1 to use a random seed.");; + // clang-format on + + CmdLine cmdline("AliceVision geometricFilterApplying"); + cmdline.add(requiredParams); + cmdline.add(optionalParams); + if (!cmdline.execute(argc, argv)) + { + return EXIT_FAILURE; + } + + std::mt19937 randomNumberGenerator(randomSeed == -1 ? std::random_device()() : randomSeed); + + if (geometricErrorMax == 0.0) + { + geometricErrorMax = std::numeric_limits::infinity(); + } + + // load input SfMData scene + sfmData::SfMData sfmData; + if (!sfmDataIO::load(sfmData, sfmDataFilename, sfmDataIO::ESfMData(sfmDataIO::VIEWS | sfmDataIO::INTRINSICS | sfmDataIO::EXTRINSICS))) + { + ALICEVISION_LOG_ERROR("The input SfMData file '" + sfmDataFilename + "' cannot be read."); + return EXIT_FAILURE; + } + + if (describerTypesName.empty()) + { + ALICEVISION_LOG_ERROR("Empty option: --describerMethods"); + return EXIT_FAILURE; + } + + const std::vector describerTypes = feature::EImageDescriberType_stringToEnums(describerTypesName); + + // matches reading. + // We read everything even if we only process a sub part. + matching::PairwiseMatches pairwiseMatches; + ALICEVISION_LOG_INFO("Load features matches"); + if (!sfm::loadPairwiseMatches(pairwiseMatches, sfmData, matchesFolders, describerTypes, numMatchesToKeep, 0, false)) + { + ALICEVISION_LOG_ERROR("Unable to load matches."); + return EXIT_FAILURE; + } + + int chunkStart, chunkEnd; + if (!rangeComputation(chunkStart, chunkEnd, rangeIteration, rangeBlocksCount, pairwiseMatches.size())) + { + ALICEVISION_LOG_INFO("Nothing to compute in this chunk"); + } + + ALICEVISION_LOG_INFO("processing " << chunkStart << " to " << chunkEnd << " over " << pairwiseMatches.size()); + + // We want to process only a subset of the pairs + // Assuming range start lies between [0 and pairwiseMatches.size()] + matching::PairwiseMatches filteredMatches; + std::set filter; + + int pos = 0; + for (const auto& [pair, content] : pairwiseMatches) + { + if (pos == chunkEnd) + { + break; + } + + if (pos >= chunkStart) + { + filteredMatches[pair] = content; + filter.insert(pair.first); + filter.insert(pair.second); + } + + pos++; + } + + ALICEVISION_LOG_INFO("A total of " << pairwiseMatches.size() << " pairs has to be processed."); + ALICEVISION_LOG_INFO("Current chunk will analyze pairs from " << chunkStart << " to " << chunkEnd << "."); + + pairwiseMatches.clear(); + + // features reading + feature::RegionsPerView regionPerView; + if (!sfm::loadRegionsPerView(regionPerView, sfmData, featuresFolders, describerTypes, filter)) + { + ALICEVISION_LOG_ERROR("Invalid regions in '" + sfmDataFilename + "'"); + return EXIT_FAILURE; + } + + ALICEVISION_LOG_INFO(std::to_string(filteredMatches.size()) << " putative image pair matches"); + for (const auto& imageMatch : filteredMatches) + { + ALICEVISION_LOG_INFO("\t- image pair (" << imageMatch.first.first << ", " << imageMatch.first.second << ") contains " + << imageMatch.second.getNbAllMatches() << " putative matches."); + } + + // Effectively process matching and output results to geometricInfos + matchingImageCollection::PairwiseGeometricInfo geometricInfos; + matchingImageCollection::robustModelEstimation( + geometricInfos, + sfmData, + regionPerView, + matchingImageCollection::GeometricFilterMatrix_F_AC(geometricErrorMax, maxIteration, robustEstimation::ERobustEstimator::ACRANSAC), + filteredMatches, + randomNumberGenerator); + + ALICEVISION_LOG_INFO(geometricInfos.size() << "/" << filteredMatches.size() << " pairs have been matched"); + for (const auto& [pair, info] : geometricInfos) + { + ALICEVISION_LOG_INFO("Pair [" << pair.first << "," << pair.second << "] has " << info.inliers << " inliers"); + } + + // Output result to json file + std::stringstream ss; + ss << outputMatchesFolder << "/matches_" << std::to_string(rangeIteration) << ".json"; + ALICEVISION_LOG_INFO("Saving to " << ss.str()); + if (!matchingImageCollection::saveGeometricInfos(ss.str(), geometricInfos)) + { + ALICEVISION_LOG_ERROR("Impossible to save geometricInfos"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/src/software/pipeline/main_imageMatching.cpp b/src/software/pipeline/main_imageMatching.cpp index 93866583b0..4c554e281f 100644 --- a/src/software/pipeline/main_imageMatching.cpp +++ b/src/software/pipeline/main_imageMatching.cpp @@ -191,6 +191,16 @@ int aliceVision_main(int argc, char** argv) switch (method) { + case EImageMatchingMethod::MIRROR: + if (!useMultiSfM) + { + generateMirrorsMatches(sfmDataA, selectedPairs); + } + else + { + ALICEVISION_THROW_ERROR("Invalid mirror mode for multisfm"); + } + break; case EImageMatchingMethod::EXHAUSTIVE: { ALICEVISION_LOG_INFO("Use EXHAUSTIVE method.");