// Comparison of the FlannIndex backends: times, memory and recall of every // algorithm on synthetic point clouds and descriptors. // // Their own executable, run by ctest under the "performance" label, so that // they don't slow down the unit tests and can be scaled up freely: // ctest -L performance to run them // ctest -LE performance to skip them // // Nothing is asserted on the times, which depend on the machine: they are // printed so that a change of backend, of parameters or of a vendored library // version can be compared to what it replaces. #include "FlannIndexBackends.h" // The times are reported rather than asserted on: which backend is the fastest // depends on the machine. They are here so that a change of backend, of // parameters or of nanoflann version can be compared to what it replaces. TEST(FlannIndexPerfTest, AllBackendsOnPointClouds) { const int cloudSize = 100000; const int querySize = 10000; const int knn = 2; const float radius = 1.0f; for(int dim = 2; dim <= 3; ++dim) { const cv::Mat cloud = makeCloud(cloudSize, dim, 5); const cv::Mat queries = makeCloud(querySize, dim, 6); for(float factor: REBALANCING_FACTORS) { std::cout << "[ ] " << dim << "D cloud of " << cloudSize << " points, " << querySize << " queries, knn=" << knn << ", rebalancing factor=" << factor << std::endl; compare(FLOAT_BACKENDS, sizeof(FLOAT_BACKENDS)/sizeof(Backend), cloud, queries, knn, radius, factor); } } } // The same backends on descriptor dimensions, where the exact kd-trees lose // their advantage over the exhaustive search: the higher the dimension, the // more of the tree a search has to visit. A single rebalancing factor here, it // doesn't affect the searches being compared. TEST(FlannIndexPerfTest, AllBackendsOnFloatDescriptors) { const int descriptorCount = 50000; const int querySize = 300; const int knn = 2; for(int dim: {32, 64, 128, 256}) { const cv::Mat descriptors = makeDescriptors(descriptorCount, dim, 1000, 11); const cv::Mat queries = perturbedQueries(descriptors, querySize, 12); std::cout << "[ ] " << dim << "D float descriptors, " << descriptorCount << " indexed, " << querySize << " queries, knn=" << knn << std::endl; compare(FLOAT_BACKENDS, sizeof(FLOAT_BACKENDS)/sizeof(Backend), descriptors, queries, knn); } } TEST(FlannIndexPerfTest, AllBackendsOnBinaryDescriptors) { const int descriptorCount = 50000; const int querySize = 300; const int knn = 2; for(int bytes: {32, 64}) // 256 and 512 bits { const cv::Mat descriptors = makeBinaryDescriptors(descriptorCount, bytes, 1000, 13); const cv::Mat queries = perturbedQueries(descriptors, querySize, 14); std::cout << "[ ] " << bytes*8 << " bits binary descriptors, " << descriptorCount << " indexed, " << querySize << " queries, knn=" << knn << std::endl; compare(BINARY_BACKENDS, sizeof(BINARY_BACKENDS)/sizeof(Backend), descriptors, queries, knn); } } // rtflann's single kd-tree rebuilds itself entirely on every addPoints() call // (see KDTreeSingleIndex::addPoints()), which is what the incremental nanoflann // tree is for. The cloud is kept small here because of it. TEST(FlannIndexPerfTest, IncrementalInsertionSpeed) { const int dim = 3; const int cloudSize = 10000; const int addedPoints = 500; const cv::Mat cloud = makeCloud(cloudSize, dim, 7); const cv::Mat addedCloud = makeCloud(addedPoints, dim, 8); for(float factor: REBALANCING_FACTORS) { std::cout << "[ ] " << dim << "D cloud of " << cloudSize << " points, " << addedPoints << " points added one by one, rebalancing factor=" << factor << std::endl; for(const Backend & backend: INCREMENTAL_BACKENDS) { FlannIndex index; index.buildIndex(backend.algorithm, cloud, false, factor); UTimer timer; for(int i=0; i(0, 0), 0.0f, 1e-3f) << backend.name; std::cout << "[ ] " << backend.name << " insertions=" << uFormat("%7.1f", addTime*1000.0) << " ms" << " memory=" << uFormat("%6d", (int)(index.memoryUsed()/1024)) << " KB" << std::endl; RecordProperty(uFormat("alg%d_factor%d_insert_us", (int)backend.algorithm, (int)factor), (int)(addTime*1e6)); } } } // What the rebalancing factor buys. rtflann inserts new points into the tree // the split planes of which were chosen for the points it was built with, so // the tree slowly degrades as it grows; over 1, the factor tells by how much it // is allowed to grow before being rebuilt. That only shows on an index that // grew a lot since it was built, which is what this does: a quarter of the // points are indexed, the rest is added one by one, as the dictionary does. // // The single kd-trees are not part of it: rtflann's rebuilds itself on every // addPoints() whatever the factor, which takes minutes at this size. TEST(FlannIndexPerfTest, RebalancingFactorOnAGrowingIndex) { const int dim = 128; const int initialCount = 5000; const int addedCount = 10000; const int querySize = 500; const int knn = 2; const cv::Mat descriptors = makeDescriptors(initialCount+addedCount, dim, 200, 15); const cv::Mat queries = perturbedQueries(descriptors, querySize, 16); // Ground truth over all the points, indexed or added. cv::Mat reference; { FlannIndex linear; cv::Mat dists; linear.buildIndex(FlannIndex::FLANN_INDEX_LINEAR, descriptors, false, 1.0f); linear.knnSearch(queries, reference, dists, knn); } const Backend growingBackends[] = { {"rtflann kd-tree (4 randomized) ", FlannIndex::FLANN_INDEX_KDTREE}, {"nanoflann kd-tree single incremental", FlannIndex::NANOFLANN_INDEX_KDTREE_SINGLE, 2.0f}, }; std::cout << "[ ] " << dim << "D float descriptors, " << initialCount << " indexed then " << addedCount << " added one by one, " << querySize << " queries, knn=" << knn << std::endl; for(const Backend & backend: growingBackends) { for(float factor: REBALANCING_FACTORS) { FlannIndex index; index.buildIndex(backend.algorithm, descriptors.rowRange(0, initialCount), false, factor); UTimer timer; for(int i=0; i & growths) { std::cout << "[ ] " << DIM << "D float descriptors, " << finalCount << " indexed, " << queryCount << " queries, knn=" << KNN << ", averaged over " << seeds << " seed" << (seeds>1?"s":"") << std::endl; std::map grown; // growth factor -> average Average fresh; for(int seed=0; seed & removedPercents) { std::cout << "[ ] " << DIM << "D float descriptors, " << count << " indexed then partly removed, " << queryCount << " queries, knn=" << KNN << ", averaged over " << seeds << " seed" << (seeds>1?"s":"") << std::endl; std::map kept; // removed % -> index that kept the removed points std::map rebuilt; // removed % -> index built on the live ones only for(int seed=0; seed live; std::vector indexOfLive(count, -1); for(int i=0; i= removed) { indexOfLive[i] = (int)live.size(); live.push_back(i); } } cv::Mat liveDescriptors((int)live.size(), DIM, CV_32FC1); for(size_t i=0; i(i, j); translated.at(i, j) = found>=0?indexOfLive[found]:-1; } } kept[removed].add(recall(translated, reference), buildTime, knnTime, index.memoryUsed()); } // Only the live points indexed: what rebuilding gives. { FlannIndex index; UTimer timer; index.buildIndex(FlannIndex::FLANN_INDEX_KDTREE, liveDescriptors, false, 1.0f); const double buildTime = timer.ticks(); index.knnSearch(queries, indices, dists, KNN); rebuilt[removed].add(recall(indices, reference), buildTime, timer.ticks(), index.memoryUsed()); } } } for(const auto & iter: kept) { reportAverage(uFormat("%3d%% removed, kept in the index ", iter.first), iter.second); reportAverage(uFormat("%3d%% removed, rebuilt without them ", iter.first), rebuilt[iter.first]); } } } // namespace TEST(FlannIndexPerfTest, GrownIndexAgainstFreshlyBuiltOne) { compareGrownAndFreshlyBuilt(20000, 3, 200, {2, 10, 100, 1000}); } TEST(FlannIndexPerfTest, RecallAgainstTheFractionOfRemovedPoints) { compareRemovedFractions(10000, 3, 200, {0, 25, 50, 75, 90}); } // The same two comparisons on a dictionary of a million words, where a single // descriptor matrix is already 512 MB and each of them takes minutes. Disabled // so that a plain run of this executable stays in the seconds, run them with: // bin/test_flann_index_perf --gtest_also_run_disabled_tests // They stay compiled, so they cannot rot as FlannIndex changes. TEST(FlannIndexPerfTest, DISABLED_GrownIndexAgainstFreshlyBuiltOneOnAMillionWords) { compareGrownAndFreshlyBuilt(1000000, 3, 100, {2, 100, 1000}); } TEST(FlannIndexPerfTest, DISABLED_RecallAgainstTheFractionOfRemovedPointsOnAMillionWords) { compareRemovedFractions(1000000, 3, 100, {50, 90}); } // The search RegistrationVis does per frame when a guess transform is given // (Vis/CorGuessWinSize): the keypoints of the frame are indexed, and the points // projected from the previous frame are looked up around their projection. The // index is built and thrown away every frame, so its build time weighs as much // as its search time. Before the nanoflann backend, this was a rtflann // randomized kd-tree forest. TEST(FlannIndexPerfTest, RegistrationGuessMatching) { const int keypoints = 1000; // Vis/MaxFeatures const float radius = 40.0f; // Vis/CorGuessWinSize const int frames = 1000; // ~ a 50 s sequence at 20 Hz // Image points rather than a cube of them. cv::RNG rng(140); cv::Mat points(keypoints, 2, CV_32FC1); cv::Mat projected(keypoints, 2, CV_32FC1); for(int i=0; i(i, 0) = rng.uniform(0.0f, 640.0f); points.at(i, 1) = rng.uniform(0.0f, 480.0f); projected.at(i, 0) = rng.uniform(0.0f, 640.0f); projected.at(i, 1) = rng.uniform(0.0f, 480.0f); } // A factor of 1 for the rtflann rows keeps their per-point bookkeeping out // of the measurement, and picks the nanoflann tree that is built once. const Backend backends[] = { {"cv BFMatcher ", FlannIndex::FLANN_INDEX_LINEAR, 1.0f, true}, {"rtflann kd-tree (4 randomized) ", FlannIndex::FLANN_INDEX_KDTREE, 1.0f}, {"rtflann kd-tree single ", FlannIndex::FLANN_INDEX_KDTREE_SINGLE, 1.0f}, {"nanoflann kd-tree single ", FlannIndex::NANOFLANN_INDEX_KDTREE_SINGLE, 1.0f}, {"nanoflann kd-tree single incremental", FlannIndex::NANOFLANN_INDEX_KDTREE_SINGLE, 2.0f}, }; std::cout << "[ ] " << keypoints << " keypoints indexed and as many looked up in a " << radius << " px radius, per frame" << std::endl; for(const Backend & backend: backends) { std::vector > indices; std::vector > dists; UTimer timer; for(int frame=0; frame