Skip to content

Commit

Permalink
[Matching] Add HNSWL2 matching with HNSWLib openMVG#1665
Browse files Browse the repository at this point in the history
- add reference implentation of the library
- add matcher_hnsw.hpp wrapper
- add references for HNSW
  • Loading branch information
rjanvier authored and pmoulon committed Jan 5, 2020
1 parent e41d6cd commit a1565ed
Show file tree
Hide file tree
Showing 14 changed files with 2,047 additions and 2 deletions.
3 changes: 3 additions & 0 deletions docs/sphinx/rst/bibliography.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ Bibliography
.. [CASCADEHASHING] **Fast and Accurate Image Matching with Cascade Hashing for 3D Reconstruction.**
Jian Cheng, Cong Leng, Jiaxiang Wu, Hainan Cui, Hanqing Lu. CVPR 2014.
.. [HNSW] **Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs.**
Yu. A. Malkov, D. A. Yashunin, TPAMI 2018.
.. [Magnus] **Two-View Orthographic Epipolar Geometry: Minimal and Optimal Solvers.**
Magnus Oskarsson. In Journal of Mathematical Imaging and Vision, 2017.
Expand Down
1 change: 1 addition & 0 deletions docs/sphinx/rst/openMVG/matching/matching.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Three implementations are available:
* a Brute force,
* an Approximate Nearest Neighbor [FLANN]_,
* a Cascade hashing Nearest Neighbor [CASCADEHASHING]_.
* an approximate nearest neighbor search using Hierarchical Navigable Small World graphs [HNSW]

This module works for data of any dimensionality, it could be use to match:

Expand Down
170 changes: 170 additions & 0 deletions src/openMVG/matching/matcher_hnsw.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// This file is part of OpenMVG, an Open Multiple View Geometry C++ library.

// Copyright (c) 2019 Romain Janvier and Pierre Moulon

// 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 http://mozilla.org/MPL/2.0/.

#ifndef OPENMVG_MATCHING_MATCHER_HNSW_HPP
#define OPENMVG_MATCHING_MATCHER_HNSW_HPP

#include <memory>
#ifdef OPENMVG_USE_OPENMP
#include <omp.h>
#endif
#include <vector>

#include "openMVG/matching/matching_interface.hpp"
#include "openMVG/matching/metric.hpp"

#include "third_party/hnswlib/hnswlib.h"

using namespace hnswlib;

namespace openMVG {
namespace matching {

// By default compute square(L2 distance).
template <typename Scalar = float, typename Metric = L2<Scalar>>
class HNSWMatcher: public ArrayMatcher<Scalar, Metric>
{
public:
using DistanceType = typename Metric::ResultType;

HNSWMatcher() = default;
virtual ~HNSWMatcher()= default;

/**
* Build the matching structure
*
* \param[in] dataset Input data.
* \param[in] nbRows The number of component.
* \param[in] dimension Length of the data contained in the dataset.
*
* \return True if success.
*/
bool Build
(
const Scalar * dataset,
int nbRows,
int dimension
) override
{
if (nbRows < 1)
{
HNSWmetric.reset(nullptr);
HNSWmatcher.reset(nullptr);
return false;
}

dimension_ = dimension;

// Here this is tricky since there is no specialization
if(typeid(DistanceType)== typeid(int)) {
HNSWmetric.reset(dynamic_cast<SpaceInterface<DistanceType> *>(new L2SpaceI(dimension)));
} else
if (typeid(DistanceType) == typeid(float)) {
HNSWmetric.reset(dynamic_cast<SpaceInterface<DistanceType> *>(new L2Space(dimension)));
} else {
std::cerr << "HNSW matcher: this type of distance is not handled Yet" << std::endl;
}

HNSWmatcher.reset(new HierarchicalNSW<DistanceType>(HNSWmetric.get(), nbRows, 16, 100) );
HNSWmatcher->setEf(16);

// add first point..
HNSWmatcher->addPoint((void *)(dataset), (size_t) 0);
//...and the other in //
#ifdef OPENMVG_USE_OPENMP
#pragma omp parallel for
#endif
for (int i = 1; i < nbRows; i++) {
HNSWmatcher->addPoint((void *) (dataset + dimension * i), (size_t) i);
}

return true;
};

/**
* Search the nearest Neighbor of the scalar array query.
*
* \param[in] query The query array.
* \param[out] indice The indice of array in the dataset that.
* have been computed as the nearest array.
* \param[out] distance The distance between the two arrays.
*
* \return True if success.
*/
bool SearchNeighbour
(
const Scalar * query,
int * indice,
DistanceType * distance
) override
{
if (HNSWmatcher.get() == nullptr)
return false;
auto result = HNSWmatcher->searchKnn(query, 1).top();
*indice = result.second;
*distance = result.first;
return true;
}

/**
* Search the N nearest Neighbor of the scalar array query.
*
* \param[in] query The query array.
* \param[in] nbQuery The number of query rows.
* \param[out] indices The corresponding (query, neighbor) indices.
* \param[out] distances The distances between the matched arrays.
* \param[in] NN The number of maximal neighbor that will be searched.
*
* \return True if success.
*/
bool SearchNeighbours
(
const Scalar * query, int nbQuery,
IndMatches * pvec_indices,
std::vector<DistanceType> * pvec_distances,
size_t NN
) override
{
if (HNSWmatcher.get() == nullptr)
{
return false;
}
pvec_indices->reserve(nbQuery * NN);
pvec_distances->reserve(nbQuery * NN);
#ifdef OPENMVG_USE_OPENMP
#pragma omp parallel for
#endif
for (int i = 0; i < nbQuery; i++) {
auto result = HNSWmatcher->searchKnn((const void *) (query + dimension_ * i), NN,
[](const std::pair<DistanceType, size_t> &a, const std::pair<DistanceType, size_t> &b) -> bool {
return a.first < b.first;
});
#ifdef OPENMVG_USE_OPENMP
#pragma omp critical
#endif
{
for (const auto & res : result)
{
pvec_indices->emplace_back(i, res.second);
pvec_distances->emplace_back(res.first);
}
}
}
return true;
};

private:
int dimension_;
std::unique_ptr<SpaceInterface<DistanceType>> HNSWmetric;
std::unique_ptr<HierarchicalNSW<DistanceType>> HNSWmatcher;
};

} // namespace matching
} // namespace openMVG

#endif // OPENMVG_MATCHING_MATCHER_HNSW_HPP
1 change: 1 addition & 0 deletions src/openMVG/matching/matcher_type.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ enum EMatcherType : unsigned char
BRUTE_FORCE_L2,
ANN_L2,
CASCADE_HASHING_L2,
HNSW_L2,
BRUTE_FORCE_HAMMING
};

Expand Down
33 changes: 33 additions & 0 deletions src/openMVG/matching/matching_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "openMVG/matching/matcher_brute_force.hpp"
#include "openMVG/matching/matcher_cascade_hashing.hpp"
#include "openMVG/matching/matcher_kdtree_flann.hpp"
#include "openMVG/matching/matcher_hnsw.hpp"

#include "openMVG/numeric/eigen_alias_definition.hpp"

Expand Down Expand Up @@ -118,6 +119,38 @@ TEST(Matching, ArrayMatcher_Kdtree_Flann_Simple__NN)
EXPECT_EQ(IndMatch(0,4), vec_nIndice[4]);
}

TEST(Matching, ArrayMatcher_Hnsw_Simple__NN)
{
const float array[] = {0, 1, 2, 5, 6};
// no 3, because it involve the same dist as 1,1

HNSWMatcher<float> matcher;
EXPECT_TRUE( matcher.Build(array, 5, 1) );

const float query[] = {2};
IndMatches vec_nIndice;
vector<float> vec_fDistance;
const int NN = 5;
EXPECT_TRUE( matcher.SearchNeighbours(query, 1, &vec_nIndice, &vec_fDistance, NN) );

EXPECT_EQ( 5, vec_nIndice.size());
EXPECT_EQ( 5, vec_fDistance.size());

// Check distances:
EXPECT_NEAR( vec_fDistance[0], Square(2.0f-2.0f), 1e-6);
EXPECT_NEAR( vec_fDistance[1], Square(1.0f-2.0f), 1e-6);
EXPECT_NEAR( vec_fDistance[2], Square(0.0f-2.0f), 1e-6);
EXPECT_NEAR( vec_fDistance[3], Square(5.0f-2.0f), 1e-6);
EXPECT_NEAR( vec_fDistance[4], Square(6.0f-2.0f), 1e-6);

// Check indexes:
EXPECT_EQ(IndMatch(0,2), vec_nIndice[0]);
EXPECT_EQ(IndMatch(0,1), vec_nIndice[1]);
EXPECT_EQ(IndMatch(0,0), vec_nIndice[2]);
EXPECT_EQ(IndMatch(0,3), vec_nIndice[3]);
EXPECT_EQ(IndMatch(0,4), vec_nIndice[4]);
}

//-- Test LIMIT case (empty arrays)

TEST(Matching, ArrayMatcherBruteForce_Simple_EmptyArrays)
Expand Down
15 changes: 15 additions & 0 deletions src/openMVG/matching/regions_matcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "openMVG/matching/matcher_brute_force.hpp"
#include "openMVG/matching/matcher_cascade_hashing.hpp"
#include "openMVG/matching/matcher_kdtree_flann.hpp"
#include "openMVG/matching/matcher_hnsw.hpp"
#include "openMVG/matching/metric.hpp"
#include "openMVG/matching/metric_hamming.hpp"

Expand Down Expand Up @@ -84,6 +85,13 @@ std::unique_ptr<RegionsMatcher> RegionMatcherFactory
region_matcher.reset(new matching::RegionsMatcherT<MatcherT>(regions, true));
}
break;
case HNSW_L2:
{
using MetricT = L2<unsigned char>;
using MatcherT = HNSWMatcher<unsigned char, MetricT>;
region_matcher.reset(new matching::RegionsMatcherT<MatcherT>(regions, true));
}
break;
case CASCADE_HASHING_L2:
{
using MetricT = L2<unsigned char>;
Expand Down Expand Up @@ -114,6 +122,13 @@ std::unique_ptr<RegionsMatcher> RegionMatcherFactory
region_matcher.reset(new matching::RegionsMatcherT<MatcherT>(regions, true));
}
break;
case HNSW_L2:
{
using MetricT = L2<float>;
using MatcherT = HNSWMatcher<float, MetricT>;
region_matcher.reset(new matching::RegionsMatcherT<MatcherT>(regions, true));
}
break;
case CASCADE_HASHING_L2:
{
using MetricT = L2<float>;
Expand Down
7 changes: 7 additions & 0 deletions src/software/SfM/main_ComputeMatches.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ int main(int argc, char **argv)
<< " AUTO: auto choice from regions type,\n"
<< " For Scalar based regions descriptor:\n"
<< " BRUTEFORCEL2: L2 BruteForce matching,\n"
<< " HNSWL2: L2 Approximate Matching with Hierarchical Navigable Small World graphs (float only),\n"
<< " ANNL2: L2 Approximate Nearest Neighbor matching,\n"
<< " CASCADEHASHINGL2: L2 Cascade Hashing matching.\n"
<< " FASTCASCADEHASHINGL2: (default)\n"
Expand Down Expand Up @@ -335,6 +336,12 @@ int main(int argc, char **argv)
collectionMatcher.reset(new Matcher_Regions(fDistRatio, BRUTE_FORCE_HAMMING));
}
else
if (sNearestMatchingMethod == "HNSWL2")
{
std::cout << "Using HNSWL2 matcher" << std::endl;
collectionMatcher.reset(new Matcher_Regions(fDistRatio, HNSW_L2));
}
else
if (sNearestMatchingMethod == "ANNL2")
{
std::cout << "Using ANN_L2 matcher" << std::endl;
Expand Down
4 changes: 2 additions & 2 deletions src/third_party/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ if (DEFINED OpenMVG_USE_INTERNAL_EIGEN)
add_subdirectory(eigen)
endif()

list(APPEND directories cmdLine histogram htmlDoc progress vectorGraphics)
list(APPEND directories cmdLine histogram htmlDoc progress vectorGraphics hnswlib)
foreach(inDirectory ${directories})
install(
DIRECTORY ./${inDirectory}
DESTINATION include/openMVG/third_party/
DESTINATION ${CMAKE_INSTALL_PREFIX}/include/openMVG/third_party/
COMPONENT headers
FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h"
)
Expand Down
Loading

0 comments on commit a1565ed

Please sign in to comment.