Skip to content

Commit 4daa9ef

Browse files
committed
[filterSfM] move 2D observations filtering from prepareDenseScene to filterSfM
1 parent 1f698f5 commit 4daa9ef

File tree

4 files changed

+297
-168
lines changed

4 files changed

+297
-168
lines changed

src/aliceVision/sfmData/SfMData.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,5 +323,21 @@ LandmarksPerView getLandmarksPerViews(const SfMData& sfmData)
323323
return landmarksPerView;
324324
}
325325

326+
ObservationsPerView getObservationsPerViews(SfMData& sfmData)
327+
{
328+
ObservationsPerView observationsPerView;
329+
for(auto& landIt : sfmData.getLandmarks())
330+
{
331+
for(const auto& obsIt : landIt.second.observations)
332+
{
333+
IndexT viewId = obsIt.first;
334+
auto& landmarksSet = observationsPerView[viewId];
335+
landmarksSet.first.push_back(&obsIt.second);
336+
landmarksSet.second.push_back(&landIt.second);
337+
}
338+
}
339+
return observationsPerView;
340+
}
341+
326342
} // namespace sfmData
327343
} // namespace aliceVision

src/aliceVision/sfmData/SfMData.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,5 +588,15 @@ using LandmarksPerView = stl::flat_map<std::size_t, LandmarkIdSet>;
588588

589589
LandmarksPerView getLandmarksPerViews(const SfMData& sfmData);
590590

591+
using ObservationsPerView = stl::flat_map<std::size_t, std::pair<std::vector<const Observation*>, std::vector<Landmark*>>>;
592+
593+
/**
594+
* @brief Get the landmark observations of camera views
595+
* with the corresponding landmarks information.
596+
* @param[in] sfmData: A given SfMData
597+
* @return Observation information per camera view
598+
*/
599+
ObservationsPerView getObservationsPerViews(SfMData& sfmData);
600+
591601
} // namespace sfmData
592602
} // namespace aliceVision

src/software/pipeline/main_filterSfM.cpp

Lines changed: 209 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
#include <boost/program_options.hpp>
1919
#include <boost/filesystem.hpp>
2020
#include <boost/program_options.hpp>
21+
#include <boost/accumulators/accumulators.hpp>
22+
#include <boost/accumulators/statistics.hpp>
2123

2224
#include <stdlib.h>
2325
#include <stdio.h>
@@ -42,9 +44,42 @@ using namespace aliceVision::sfmDataIO;
4244

4345
namespace po = boost::program_options;
4446
namespace fs = boost::filesystem;
47+
using namespace boost::accumulators;
4548

4649
static const std::size_t MAX_LEAF_ELEMENTS = 64;
4750

51+
struct ObservationsAdaptator
52+
{
53+
using Derived = ObservationsAdaptator; //!< In this case the dataset class is myself.
54+
using T = double;
55+
56+
/// CRTP helper method
57+
inline const Derived& derived() const { return *static_cast<const Derived*>(this); }
58+
/// CRTP helper method
59+
inline Derived& derived() { return *static_cast<Derived*>(this); }
60+
61+
const std::vector<const Observation*> _data;
62+
ObservationsAdaptator(const std::vector<const Observation*>& data)
63+
: _data(data)
64+
{
65+
}
66+
67+
// Must return the number of data points
68+
inline size_t kdtree_get_point_count() const { return _data.size(); }
69+
70+
// Returns the dim'th component of the idx'th point in the class:
71+
inline T kdtree_get_pt(const size_t idx, int dim) const { return _data[idx]->x(dim); }
72+
73+
// Optional bounding-box computation: return false to default to a standard bbox computation loop.
74+
// Return true if the BBOX was already computed by the class and returned in "bb" so it can be avoided to redo it
75+
// again. Look at bb.size() to find out the expected dimensionality (e.g. 2 or 3 for point clouds)
76+
template <class BBOX>
77+
bool kdtree_get_bbox(BBOX& bb) const
78+
{
79+
return false;
80+
}
81+
};
82+
4883
struct LandmarksAdaptator
4984
{
5085
using Derived = LandmarksAdaptator; //!< In this case the dataset class is myself.
@@ -89,16 +124,42 @@ struct LandmarksAdaptator
89124
}
90125
};
91126

92-
using KdTree = nanoflann::KDTreeSingleIndexAdaptor<
127+
using KdTree2D = nanoflann::KDTreeSingleIndexAdaptor<
128+
nanoflann::L2_Simple_Adaptor<double, ObservationsAdaptator>,
129+
ObservationsAdaptator, 2 /* dim */, size_t>;
130+
131+
using KdTree3D = nanoflann::KDTreeSingleIndexAdaptor<
93132
nanoflann::L2_Simple_Adaptor<double, LandmarksAdaptator>,
94-
LandmarksAdaptator,
95-
3, /* dim */
96-
size_t
97-
>;
98-
99-
/**
100-
* A result-set class used when performing a radius based search.
101-
*/
133+
LandmarksAdaptator, 3 /* dim */,size_t>;
134+
135+
class RadiusKnnSearch
136+
{
137+
public:
138+
const double _radius_sq;
139+
const int _nb_neighbors;
140+
int nb_found = 0;
141+
142+
inline RadiusKnnSearch(double radius, int k)
143+
: _radius_sq(radius * radius)
144+
, _nb_neighbors(k)
145+
{
146+
}
147+
148+
inline bool full() const { return nb_found == _nb_neighbors; }
149+
150+
inline bool addPoint(double dist, IndexT index)
151+
{
152+
if(dist < _radius_sq)
153+
{
154+
nb_found++;
155+
return nb_found < _nb_neighbors;
156+
}
157+
return true;
158+
}
159+
160+
inline double worstDist() const { return _radius_sq; }
161+
};
162+
102163
class PixSizeSearch
103164
{
104165
public:
@@ -118,6 +179,7 @@ class PixSizeSearch
118179

119180
inline bool addPoint(double dist, size_t index)
120181
{
182+
// strict comparison because a point in the tree can be a neighbor of itself
121183
if(dist < _radius_sq && _pixSize[index] < _pixSize_i)
122184
{
123185
found = true;
@@ -169,7 +231,7 @@ bool filterLandmarks(SfMData& sfmData, double radiusScale, bool useFeatureScale,
169231
{
170232
filteredLandmarks[newIdx] = std::make_pair(newIdx, landmarksData[newIdx]);
171233
}
172-
sfmData.getLandmarks() = Landmarks(filteredLandmarks.begin(), filteredLandmarks.end());
234+
sfmData.getLandmarks() = std::move(Landmarks(filteredLandmarks.begin(), filteredLandmarks.end()));
173235
return true;
174236
}
175237

@@ -203,9 +265,9 @@ bool filterLandmarks(SfMData& sfmData, double radiusScale, bool useFeatureScale,
203265
//// sort landmarks by descending pixSize order
204266
//std::stable_sort(landmarksPixSize.begin(), landmarksPixSize.end(), std::greater<>{});
205267

206-
ALICEVISION_LOG_INFO("Build nanoflann KdTree index.");
268+
ALICEVISION_LOG_INFO("Build nanoflann KdTree index for landmarks in 3D.");
207269
LandmarksAdaptator data(landmarksData);
208-
KdTree tree(3, data, nanoflann::KDTreeSingleIndexAdaptorParams(MAX_LEAF_ELEMENTS));
270+
KdTree3D tree(3, data, nanoflann::KDTreeSingleIndexAdaptorParams(MAX_LEAF_ELEMENTS));
209271
tree.buildIndex();
210272
ALICEVISION_LOG_INFO("KdTree created for " << landmarksData.size() << " points.");
211273
std::vector<bool> toRemove(landmarksData.size(), false);
@@ -249,7 +311,7 @@ bool filterLandmarks(SfMData& sfmData, double radiusScale, bool useFeatureScale,
249311
return true;
250312
}
251313

252-
bool filterObservations(SfMData& sfmData, int maxNbObservationsPerLandmark, int nbNeighbors, double neighborsInfluence,
314+
bool filterObservations3D(SfMData& sfmData, int maxNbObservationsPerLandmark, int nbNeighbors3D, double neighborsInfluence,
253315
int nbIterations)
254316
{
255317
// store in vector for faster access
@@ -316,13 +378,13 @@ bool filterObservations(SfMData& sfmData, int maxNbObservationsPerLandmark, int
316378
ALICEVISION_LOG_INFO("Computing initial observation scores based on distance to observing view: done");
317379

318380
ALICEVISION_LOG_INFO("Computing landmark neighbors and distance-based weights: started");
319-
ALICEVISION_LOG_INFO("Build nanoflann KdTree index.");
381+
ALICEVISION_LOG_INFO("Build nanoflann KdTree index for landmarks in 3D.");
320382
LandmarksAdaptator dataAdaptor(landmarksData);
321-
KdTree tree(3, dataAdaptor, nanoflann::KDTreeSingleIndexAdaptorParams(MAX_LEAF_ELEMENTS));
383+
KdTree3D tree(3, dataAdaptor, nanoflann::KDTreeSingleIndexAdaptorParams(MAX_LEAF_ELEMENTS));
322384
tree.buildIndex();
323385
ALICEVISION_LOG_INFO("KdTree created for " << landmarksData.size() << " points.");
324386
// note that the landmark is a neighbor to itself with zero distance, hence the +/- 1
325-
int nbNeighbors_ = std::min(nbNeighbors, static_cast<int>(landmarksData.size() - 1)) + 1;
387+
int nbNeighbors_ = std::min(nbNeighbors3D, static_cast<int>(landmarksData.size() - 1)) + 1;
326388
// contains the neighbor landmarks ids with their corresponding weights
327389
std::vector<std::pair<std::vector<size_t>, std::vector<double>>> neighborsData(landmarksData.size());
328390
#pragma omp parallel for
@@ -372,7 +434,7 @@ bool filterObservations(SfMData& sfmData, int maxNbObservationsPerLandmark, int
372434
// accumulator for normalisation
373435
double viewScores_total = 0.;
374436
auto& [indices_, weights_] = neighborsData[id];
375-
for(auto j = 0; j < nbNeighbors; j++)
437+
for(auto j = 0; j < nbNeighbors3D; j++)
376438
{
377439
const auto& neighborId = indices_[j];
378440
const auto& neighborWeight = weights_[j];
@@ -467,19 +529,118 @@ bool filterObservations(SfMData& sfmData, int maxNbObservationsPerLandmark, int
467529
return true;
468530
}
469531

532+
bool filterObservations2D(SfMData& sfmData, int nbNeighbors2D, float percentile,
533+
HashMap<IndexT, double>& estimatedRadii)
534+
{
535+
std::set<IndexT> viewIds = sfmData.getValidViews();
536+
std::vector<double> estimatedRadii_(viewIds.size(), -1.);
537+
auto observationsPerView = getObservationsPerViews(sfmData);
538+
539+
#pragma omp parallel for
540+
for(int i = 0; i < viewIds.size(); ++i)
541+
{
542+
auto itView = viewIds.begin();
543+
std::advance(itView, i);
544+
const IndexT viewId = *itView;
545+
546+
auto& observationsIt = observationsPerView.find(viewId);
547+
if(observationsIt != observationsPerView.end())
548+
{
549+
auto& observations = observationsIt->second.first;
550+
auto& landmarks = observationsIt->second.second;
551+
552+
ALICEVISION_LOG_INFO("Build nanoflann KdTree index for projected landmarks in 2D.");
553+
ObservationsAdaptator data(observations);
554+
KdTree2D tree(2, data, nanoflann::KDTreeSingleIndexAdaptorParams(MAX_LEAF_ELEMENTS));
555+
tree.buildIndex();
556+
ALICEVISION_LOG_INFO("KdTree created for " << observations.size() << " points.");
557+
558+
// note that the observation is a neighbor to itself with zero distance, hence the +/- 1
559+
size_t nbNeighbors_ = std::min(static_cast<size_t>(nbNeighbors2D), observations.size() - 1) + 1;
560+
std::vector<double> means(observations.size());
561+
const std::size_t cacheSize = 1000;
562+
accumulator_set<double, stats<tag::tail_quantile<right>, tag::mean>> acc(tag::tail<right>::cache_size =
563+
cacheSize);
564+
for(auto j = 0; j < observations.size(); j++)
565+
{
566+
const auto& obs = *observations[j];
567+
std::vector<size_t> indices_(nbNeighbors_);
568+
std::vector<double> distances_(nbNeighbors_);
569+
tree.knnSearch(obs.x.data(), nbNeighbors_, &indices_[0], &distances_[0]);
570+
571+
std::transform(distances_.begin(), distances_.end(), distances_.begin(),
572+
static_cast<double (*)(double)>(std::sqrt));
573+
const auto& mean = std::accumulate(distances_.begin(), distances_.end(), 0.0) / (nbNeighbors_ - 1);
574+
means[j] = mean;
575+
acc(mean);
576+
}
577+
double mean_max = std::numeric_limits<double>::max();
578+
if(percentile != 1.f)
579+
mean_max = quantile(acc, quantile_probability = percentile);
580+
estimatedRadii_[i] = mean(acc);
581+
582+
std::vector<const Observation*> filteredObservations;
583+
std::vector<Landmark*> filteredLandmarks;
584+
filteredObservations.reserve(observations.size());
585+
filteredLandmarks.reserve(landmarks.size());
586+
for(auto j = 0; j < observations.size(); j++)
587+
if (means[j] < mean_max)
588+
{
589+
filteredObservations.push_back(observations[j]);
590+
filteredLandmarks.push_back(landmarks[j]);
591+
}
592+
filteredObservations.shrink_to_fit();
593+
filteredLandmarks.shrink_to_fit();
594+
observations = std::move(filteredObservations);
595+
landmarks = std::move(filteredLandmarks);
596+
}
597+
}
598+
599+
for(auto& landmark : sfmData.getLandmarks())
600+
{
601+
landmark.second.observations.clear();
602+
}
603+
604+
for(int i = 0; i < viewIds.size(); ++i)
605+
{
606+
auto itView = viewIds.begin();
607+
std::advance(itView, i);
608+
const IndexT viewId = *itView;
609+
610+
if(estimatedRadii_[i] != -1.)
611+
estimatedRadii[viewId] = estimatedRadii_[i];
612+
613+
auto& observationsIt = observationsPerView.find(viewId);
614+
if(observationsIt != observationsPerView.end())
615+
{
616+
auto& observations = observationsIt->second.first;
617+
auto& landmarks = observationsIt->second.second;
618+
for(int j = 0; j < observations.size(); j++)
619+
{
620+
landmarks[j]->observations[viewId] = *observations[j];
621+
}
622+
}
623+
}
624+
625+
return true;
626+
}
627+
470628
int aliceVision_main(int argc, char *argv[])
471629
{
472630
// command-line parameters
473631

474632
std::string inputSfmFilename;
475633
std::string outputSfmFilename;
634+
std::string outputRadiiFilename;
476635
int maxNbObservationsPerLandmark = 2;
477636
int minNbObservationsPerLandmark = 5;
478637
double radiusScale = 2;
479638
bool useFeatureScale = true;
480-
int nbNeighbors = 10;
639+
int nbNeighbors3D = 10;
481640
double neighborsInfluence = 0.5;
482641
int nbIterations = 5;
642+
int nbNeighbors2D = 5;
643+
float percentile = 0.95;
483644

484645
// user optional parameters
485646
std::vector<std::string> featuresFolders;
@@ -503,19 +664,25 @@ int aliceVision_main(int argc, char *argv[])
503664
"Scale factor applied to pixel size based radius filter applied to landmarks.")
504665
("useFeatureScale", po::value<bool>(&useFeatureScale)->default_value(useFeatureScale),
505666
"If true, use feature scale for computing pixel size. Otherwise, use a scale of 1 pixel.")
506-
("nbNeighbors", po::value<int>(&nbNeighbors)->default_value(nbNeighbors),
667+
("nbNeighbors3D", po::value<int>(&nbNeighbors3D)->default_value(nbNeighbors3D),
507668
"Number of neighbor landmarks used in making the decision for best observations.")
508669
("neighborsInfluence", po::value<double>(&neighborsInfluence)->default_value(neighborsInfluence),
509670
"Specifies how much influential the neighbors are in selecting the best observations."
510671
"Between 0. and 1., the closer to 1., the more influencial the neighborhood is.")
511672
("nbIterations", po::value<int>(&nbIterations)->default_value(nbIterations),
512673
"Number of iterations to propagate neighbors information.")
674+
("nbNeighbors2D", po::value<int>(&nbNeighbors2D)->default_value(nbNeighbors2D),
675+
"Number of neighbor observations to be considered for the landmarks-based masking.")
676+
("percentile", po::value<float>(&percentile)->default_value(percentile),
677+
"TODO.")
513678
("featuresFolders,f", po::value<std::vector<std::string>>(&featuresFolders)->multitoken(),
514679
"Path to folder(s) containing the extracted features.")
515680
("matchesFolders,m", po::value<std::vector<std::string>>(&matchesFolders)->multitoken(),
516681
"Path to folder(s) in which computed matches are stored.")
517682
("describerTypes,d", po::value<std::string>(&describerTypesName)->default_value(describerTypesName),
518-
feature::EImageDescriberType_informations().c_str());
683+
feature::EImageDescriberType_informations().c_str())
684+
("outputRadiiFile", po::value<std::string>(&outputRadiiFilename)->default_value(outputRadiiFilename),
685+
"Output Radii file containing the estimated projection radius of observations per view.");
519686

520687
CmdLine cmdline("AliceVision SfM filtering."); // TODO add description
521688
cmdline.add(requiredParams);
@@ -551,11 +718,30 @@ int aliceVision_main(int argc, char *argv[])
551718

552719
if(maxNbObservationsPerLandmark > 0)
553720
{
554-
ALICEVISION_LOG_INFO("Filtering observations: started.");
555-
filterObservations(sfmData, maxNbObservationsPerLandmark, nbNeighbors, neighborsInfluence, nbIterations);
556-
ALICEVISION_LOG_INFO("Filtering observations: done.");
721+
ALICEVISION_LOG_INFO("Filtering observations in 3D: started.");
722+
filterObservations3D(sfmData, maxNbObservationsPerLandmark, nbNeighbors3D, neighborsInfluence, nbIterations);
723+
ALICEVISION_LOG_INFO("Filtering observations in 3D: done.");
557724
}
558725

726+
if(nbNeighbors2D > 0 || percentile < 1.f)
727+
{
728+
HashMap<IndexT, double> estimatedRadii;
729+
ALICEVISION_LOG_INFO("Filtering observations in 2D: started.");
730+
filterObservations2D(sfmData, nbNeighbors2D, percentile, estimatedRadii);
731+
ALICEVISION_LOG_INFO("Filtering observations in 2D: done.");
732+
733+
if(outputRadiiFilename.empty())
734+
outputRadiiFilename = (fs::path(outputSfmFilename).parent_path() / "radii.txt").string();
735+
std::ofstream fs(outputRadiiFilename, std::ios::out);
736+
if(!fs.is_open())
737+
ALICEVISION_LOG_WARNING("Unable to create the radii file " << outputRadiiFilename);
738+
else
739+
{
740+
for(const auto& radius : estimatedRadii)
741+
fs << radius.first << "\t" << radius.second << std::endl;
742+
fs.close();
743+
}
744+
}
559745

560746
sfmDataIO::Save(sfmData, outputSfmFilename, sfmDataIO::ESfMData::ALL);
561747
return EXIT_SUCCESS;

0 commit comments

Comments
 (0)