#include "FlannIndexBackends.h" TEST(FlannIndexTest, ExactBackendsFindTheSameNeighbors) { for(int dim = 2; dim <= 3; ++dim) { const cv::Mat cloud = makeCloud(5000, dim, 1); // An odd number of queries: with an odd knn, the rtflann backend used // to write one index past the end of the output matrix, which needs // query.rows*knn to be odd to show. const cv::Mat queries = makeCloud(501, dim, 2); for(int knn = 1; knn <= 2; ++knn) { cv::Mat referenceIndices; cv::Mat referenceDists; for(const Backend & backend: EXACT_BACKENDS) { FlannIndex index; index.buildIndex(backend.algorithm, cloud, false, backend.rebalancingFactor); ASSERT_TRUE(index.isBuilt()) << backend.name; EXPECT_EQ(index.indexedFeatures(), (size_t)cloud.rows) << backend.name; cv::Mat indices; cv::Mat dists; index.knnSearch(queries, indices, dists, knn); ASSERT_EQ(indices.rows, queries.rows) << backend.name; ASSERT_EQ(indices.cols, knn) << backend.name; if(referenceIndices.empty()) { referenceIndices = indices; referenceDists = dists; // Sanity check the reference itself: every query is inside // the cloud's box, so all neighbors have been found. for(int i=0; i(i, j), 0) << "dim=" << dim << " knn=" << knn; } } continue; } for(int i=0; i(i, j), referenceIndices.at(i, j)) << backend.name << " dim=" << dim << " knn=" << knn << " query=" << i << " n=" << j; EXPECT_NEAR(dists.at(i, j), referenceDists.at(i, j), 1e-3f) << backend.name << " dim=" << dim << " knn=" << knn << " query=" << i << " n=" << j; } } } } } } TEST(FlannIndexTest, ExactBackendsFindTheSamePointsInRadius) { for(int dim = 2; dim <= 3; ++dim) { const cv::Mat cloud = makeCloud(5000, dim, 3); const cv::Mat queries = makeCloud(200, dim, 4); const float radius = 5.0f; std::vector > referenceIndices; for(const Backend & backend: EXACT_BACKENDS) { FlannIndex index; index.buildIndex(backend.algorithm, cloud, false, backend.rebalancingFactor); std::vector > indices; std::vector > dists; index.radiusSearch(queries, indices, dists, radius); ASSERT_EQ(indices.size(), (size_t)queries.rows) << backend.name; // The backends don't return the points in the same order when they // are not sorted by distance, compare them as sets. for(size_t i=0; i(i, j), indices.at(i, j)) << backend.name << " checksum=" << checksum << " query=" << i; } } // The raw pointer overload takes the same data. FlannIndex loadedRaw; EXPECT_TRUE(loadedRaw.loadIndex(data.data(), data.size(), backend.algorithm, cloud, false, backend.rebalancingFactor, &error)) << backend.name << ": " << error; } } } TEST(FlannIndexTest, LoadIndexRefusesDataItCannotUse) { const cv::Mat cloud = makeCloud(500, 3, 22); FlannIndex index; index.buildIndex(FlannIndex::NANOFLANN_INDEX_KDTREE_SINGLE, cloud); const std::vector data = index.serializeIndex(true); ASSERT_FALSE(data.empty()); const FlannIndex::flann_algorithm_t algorithm = FlannIndex::NANOFLANN_INDEX_KDTREE_SINGLE; FlannIndex loaded; std::string error; // Another algorithm than the one it was built with EXPECT_FALSE(loaded.loadIndex(data, FlannIndex::FLANN_INDEX_KDTREE_SINGLE, cloud, false, 2.0f, &error)); EXPECT_FALSE(error.empty()); // Another number of features error.clear(); EXPECT_FALSE(loaded.loadIndex(data, algorithm, cloud.rowRange(0, cloud.rows-1), false, 2.0f, &error)); EXPECT_FALSE(error.empty()); // Another dimension error.clear(); EXPECT_FALSE(loaded.loadIndex(data, algorithm, makeCloud(cloud.rows, 2, 22), false, 2.0f, &error)); EXPECT_FALSE(error.empty()); // Another distance error.clear(); EXPECT_FALSE(loaded.loadIndex(data, algorithm, cloud, true, 2.0f, &error)); EXPECT_FALSE(error.empty()); // Same shape, other content: caught by the checksum error.clear(); EXPECT_FALSE(loaded.loadIndex(data, algorithm, makeCloud(cloud.rows, cloud.cols, 23), false, 2.0f, &error)); EXPECT_FALSE(error.empty()); // Truncated error.clear(); EXPECT_FALSE(loaded.loadIndex(data.data(), data.size()/2, algorithm, cloud, false, 2.0f, &error)); EXPECT_FALSE(error.empty()); // Nothing at all error.clear(); std::vector empty; EXPECT_FALSE(loaded.loadIndex(empty, algorithm, cloud, false, 2.0f, &error)); // None of it left a half loaded index behind EXPECT_FALSE(loaded.isBuilt()); } // A descriptor header can cover a whole batch of points when the index is never // rebuilt (rebalancing factor of 1), which used to make serializeIndex() look // them up one by one and throw. TEST(FlannIndexTest, SerializesAnIndexWithBatchedHeadersAndRemovedPoints) { const cv::Mat cloud = makeCloud(500, 3, 24); const cv::Mat added = makeCloud(100, 3, 25); for(float factor: {1.0f, 2.0f}) { FlannIndex index; index.buildIndex(FlannIndex::FLANN_INDEX_KDTREE, cloud, false, factor); const std::vector indexes = index.addPoints(added); ASSERT_EQ(indexes.size(), (size_t)added.rows); for(size_t i=0; i<10; ++i) { index.removePoint(indexes[i]); } EXPECT_EQ(index.indexedFeatures(), (size_t)(cloud.rows + added.rows - 10)) << "factor=" << factor; EXPECT_NO_THROW(index.serializeIndex(true)) << "factor=" << factor; EXPECT_NO_THROW(index.serializeIndex(false)) << "factor=" << factor; } } TEST(FlannIndexTest, AddedPointsAreFoundAndRemovedOnesAreNot) { const cv::Mat cloud = makeCloud(500, 3, 26); const cv::Mat added = makeCloud(50, 3, 27); const Backend backends[] = { {"rtflann kd-tree (4 randomized) ", FlannIndex::FLANN_INDEX_KDTREE}, {"nanoflann kd-tree single incremental", FlannIndex::NANOFLANN_INDEX_KDTREE_SINGLE, 2.0f}, }; for(const Backend & backend: backends) { FlannIndex index; index.buildIndex(backend.algorithm, cloud, false, backend.rebalancingFactor); ASSERT_EQ(index.indexedFeatures(), (size_t)cloud.rows) << backend.name; const std::vector indexes = index.addPoints(added); ASSERT_EQ(indexes.size(), (size_t)added.rows) << backend.name; for(size_t i=0; i(0, 0), (int)indexes[0]) << backend.name; EXPECT_NEAR(dists.at(0, 0), 0.0f, 1e-3f) << backend.name; // Once removed, it is not returned anymore, by either search. index.removePoint(indexes[0]); EXPECT_EQ(index.indexedFeatures(), (size_t)(cloud.rows + added.rows - 1)) << backend.name; index.knnSearch(added.row(0), indices, dists, 1); EXPECT_NE(indices.at(0, 0), (int)indexes[0]) << backend.name; std::vector > radiusIndices; std::vector > radiusDists; index.radiusSearch(added.row(0), radiusIndices, radiusDists, 1.0f); ASSERT_EQ(radiusIndices.size(), 1u) << backend.name; for(size_t neighbor: radiusIndices[0]) { EXPECT_NE(neighbor, (size_t)indexes[0]) << backend.name; } } } // The rebuild triggered by the removals renumbers nothing: the indexes handed // out before it still designate the same points, which VWDictionary relies on. TEST(FlannIndexTest, IndexesSurviveARebuild) { const cv::Mat cloud = makeCloud(400, 3, 28); const cv::Mat added = makeCloud(400, 3, 29); const Backend backends[] = { {"rtflann kd-tree (4 randomized) ", FlannIndex::FLANN_INDEX_KDTREE}, {"nanoflann kd-tree single incremental", FlannIndex::NANOFLANN_INDEX_KDTREE_SINGLE, 2.0f}, }; for(const Backend & backend: backends) { FlannIndex index; index.buildIndex(backend.algorithm, cloud, false, 2.0f); std::vector indexes; for(int i=0; i added1 = index.addPoints(added.row(i)); ASSERT_EQ(added1.size(), 1u) << backend.name; indexes.push_back(added1[0]); } // Enough removals to get over the ratio a factor of 2 tolerates. for(int i=0; i(i, 0), (int)indexes[i]) << backend.name << " point=" << i; } } } TEST(FlannIndexTest, UnsupportedOperationsAreRefused) { const cv::Mat cloud = makeCloud(200, 3, 30); const cv::Mat added = makeCloud(10, 3, 31); // A nanoflann index built to never be rebuilt (factor 1) still takes points, // rebuilding itself as the tree that accepts them. FlannIndex staticIndex; staticIndex.buildIndex(FlannIndex::NANOFLANN_INDEX_KDTREE_SINGLE, cloud, false, 1.0f); const std::vector addedIndexes = staticIndex.addPoints(added); ASSERT_EQ(addedIndexes.size(), (size_t)added.rows); EXPECT_EQ(addedIndexes[0], (unsigned int)cloud.rows); EXPECT_EQ(staticIndex.indexedFeatures(), (size_t)(cloud.rows + added.rows)); staticIndex.removePoint(0); EXPECT_EQ(staticIndex.indexedFeatures(), (size_t)(cloud.rows + added.rows - 1)); // The points it held are still there, under the same indexes. cv::Mat indices; cv::Mat dists; staticIndex.knnSearch(cloud.row(1), indices, dists, 1); EXPECT_EQ(indices.at(0, 0), 1); staticIndex.knnSearch(added.row(0), indices, dists, 1); EXPECT_EQ(indices.at(0, 0), (int)addedIndexes[0]); // An index with removed points refers to holes in the features it was built // with, which the ones given back to loadIndex() cannot reproduce. FlannIndex incremental; incremental.buildIndex(FlannIndex::NANOFLANN_INDEX_KDTREE_SINGLE, cloud, false, 2.0f); EXPECT_FALSE(incremental.serializeIndex(true).empty()); incremental.removePoint(0); EXPECT_TRUE(incremental.serializeIndex(true).empty()); } TEST(FlannIndexTest, DistanceL1BackendsAgree) { const cv::Mat cloud = makeCloud(1000, 8, 32); const cv::Mat queries = makeCloud(51, 8, 33); const int knn = 2; cv::Mat reference; cv::Mat referenceDists; for(const Backend & backend: EXACT_BACKENDS) { FlannIndex index; index.buildIndex(backend.algorithm, cloud, true /* useDistanceL1 */, backend.rebalancingFactor); cv::Mat indices; cv::Mat dists; index.knnSearch(queries, indices, dists, knn); ASSERT_EQ(indices.rows, queries.rows) << backend.name; if(reference.empty()) { reference = indices; referenceDists = dists; // The exhaustive reference is L1 indeed, not L2. const float * query = queries.ptr(0); const float * neighbor = cloud.ptr(indices.at(0, 0)); float l1 = 0.0f; for(int i=0; i(0, 0), l1, 1e-2f); continue; } for(int i=0; i(i, j), reference.at(i, j)) << backend.name << " query=" << i; EXPECT_NEAR(dists.at(i, j), referenceDists.at(i, j), 1e-2f) << backend.name; } } } } TEST(FlannIndexTest, BinaryDescriptorsUseHammingDistances) { const cv::Mat descriptors = makeBinaryDescriptors(1000, 32, 100, 34); const cv::Mat queries = descriptors.rowRange(0, 20); // the indexed ones const Backend backends[] = { {"linear exhaustive (hamming) ", FlannIndex::FLANN_INDEX_LINEAR}, {"rtflann LSH ", FlannIndex::FLANN_INDEX_LSH}, }; for(const Backend & backend: backends) { FlannIndex index; index.buildIndex(backend.algorithm, descriptors, false, backend.rebalancingFactor); EXPECT_EQ(index.featuresType(), CV_8UC1) << backend.name; EXPECT_EQ(index.featuresDim(), descriptors.cols) << backend.name; cv::Mat indices; cv::Mat dists; index.knnSearch(queries, indices, dists, 1); // Hamming distances are integers ASSERT_EQ(dists.type(), CV_32S) << backend.name; for(int i=0; i(i, 0), i) << backend.name << " query=" << i; EXPECT_EQ(dists.at(i, 0), 0) << backend.name << " query=" << i; } } } TEST(FlannIndexTest, RadiusSearchKeepsTheNearestMaxNeighbors) { const cv::Mat cloud = makeCloud(2000, 2, 35); const cv::Mat queries = makeCloud(50, 2, 36); const float radius = 10.0f; const int maxNeighbors = 3; std::vector > reference; for(const Backend & backend: EXACT_BACKENDS) { FlannIndex index; index.buildIndex(backend.algorithm, cloud, false, backend.rebalancingFactor); std::vector > indices; std::vector > dists; index.radiusSearch(queries, indices, dists, radius, maxNeighbors, 32, 0.0f, true); ASSERT_EQ(indices.size(), (size_t)queries.rows) << backend.name; for(size_t i=0; i