1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-30 15:53:03 +04:00

Merge pull request #25708 from kaingwade:flann2annoy

Add interface to Annoy which will replace the FLANN #25708

This PR is to add interface to [Annoy](https://github.com/spotify/annoy) which will replace the FLANN, part of one of the cleanup work of OpenCV 5.0: #24998. 

After it, there will be consecutive patches:

- [ ] Add Annoy based DescriptorMatcher
- [ ] Replace FLANN based code with Annoy and remove FLANN completely

### Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

- [x] I agree to contribute to the project under Apache 2 License.
- [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
- [x] The PR is proposed to the proper branch
- [ ] There is a reference to the original bug report and related work
- [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable
      Patch to opencv_extra has the same branch name.
- [ ] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
WU Jia
2024-10-28 22:04:02 +08:00
committed by GitHub
parent a2fa1d49a4
commit 66a29b422c
7 changed files with 2271 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
Copyright (c) 2013 Spotify AB
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
File diff suppressed because it is too large Load Diff
+242
View File
@@ -0,0 +1,242 @@
// This is from https://code.google.com/p/mman-win32/
//
// Licensed under MIT
#ifndef _MMAN_WIN32_H
#define _MMAN_WIN32_H
#ifndef _WIN32_WINNT // Allow use of features specific to Windows XP or later.
#define _WIN32_WINNT 0x0501 // Change this to the appropriate value to target other versions of Windows.
#endif
#include <sys/types.h>
#include <windows.h>
#include <errno.h>
#include <io.h>
#define PROT_NONE 0
#define PROT_READ 1
#define PROT_WRITE 2
#define PROT_EXEC 4
#define MAP_FILE 0
#define MAP_SHARED 1
#define MAP_PRIVATE 2
#define MAP_TYPE 0xf
#define MAP_FIXED 0x10
#define MAP_ANONYMOUS 0x20
#define MAP_ANON MAP_ANONYMOUS
#define MAP_FAILED ((void *)-1)
/* Flags for msync. */
#define MS_ASYNC 1
#define MS_SYNC 2
#define MS_INVALIDATE 4
#ifndef FILE_MAP_EXECUTE
#define FILE_MAP_EXECUTE 0x0020
#endif
static int __map_mman_error(const DWORD err, const int /*deferr*/)
{
if (err == 0)
return 0;
//TODO: implement
return err;
}
static DWORD __map_mmap_prot_page(const int prot)
{
DWORD protect = 0;
if (prot == PROT_NONE)
return protect;
if ((prot & PROT_EXEC) != 0)
{
protect = ((prot & PROT_WRITE) != 0) ?
PAGE_EXECUTE_READWRITE : PAGE_EXECUTE_READ;
}
else
{
protect = ((prot & PROT_WRITE) != 0) ?
PAGE_READWRITE : PAGE_READONLY;
}
return protect;
}
static DWORD __map_mmap_prot_file(const int prot)
{
DWORD desiredAccess = 0;
if (prot == PROT_NONE)
return desiredAccess;
if ((prot & PROT_READ) != 0)
desiredAccess |= FILE_MAP_READ;
if ((prot & PROT_WRITE) != 0)
desiredAccess |= FILE_MAP_WRITE;
if ((prot & PROT_EXEC) != 0)
desiredAccess |= FILE_MAP_EXECUTE;
return desiredAccess;
}
inline void* mmap(void * /*addr*/, size_t len, int prot, int flags, int fildes, off_t off)
{
HANDLE fm, h;
void * map = MAP_FAILED;
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4293)
#endif
const DWORD dwFileOffsetLow = (sizeof(off_t) <= sizeof(DWORD)) ?
(DWORD)off : (DWORD)(off & 0xFFFFFFFFL);
const DWORD dwFileOffsetHigh = (sizeof(off_t) <= sizeof(DWORD)) ?
(DWORD)0 : (DWORD)((off >> 32) & 0xFFFFFFFFL);
const DWORD protect = __map_mmap_prot_page(prot);
const DWORD desiredAccess = __map_mmap_prot_file(prot);
const off_t maxSize = off + (off_t)len;
const DWORD dwMaxSizeLow = (sizeof(off_t) <= sizeof(DWORD)) ?
(DWORD)maxSize : (DWORD)(maxSize & 0xFFFFFFFFL);
const DWORD dwMaxSizeHigh = (sizeof(off_t) <= sizeof(DWORD)) ?
(DWORD)0 : (DWORD)((maxSize >> 32) & 0xFFFFFFFFL);
#ifdef _MSC_VER
#pragma warning(pop)
#endif
errno = 0;
if (len == 0
/* Unsupported flag combinations */
|| (flags & MAP_FIXED) != 0
/* Usupported protection combinations */
|| prot == PROT_EXEC)
{
errno = EINVAL;
return MAP_FAILED;
}
h = ((flags & MAP_ANONYMOUS) == 0) ?
(HANDLE)_get_osfhandle(fildes) : INVALID_HANDLE_VALUE;
if ((flags & MAP_ANONYMOUS) == 0 && h == INVALID_HANDLE_VALUE)
{
errno = EBADF;
return MAP_FAILED;
}
fm = CreateFileMapping(h, NULL, protect, dwMaxSizeHigh, dwMaxSizeLow, NULL);
if (fm == NULL)
{
errno = __map_mman_error(GetLastError(), EPERM);
return MAP_FAILED;
}
map = MapViewOfFile(fm, desiredAccess, dwFileOffsetHigh, dwFileOffsetLow, len);
CloseHandle(fm);
if (map == NULL)
{
errno = __map_mman_error(GetLastError(), EPERM);
return MAP_FAILED;
}
return map;
}
inline int munmap(void *addr, size_t /*len*/)
{
if (UnmapViewOfFile(addr))
return 0;
errno = __map_mman_error(GetLastError(), EPERM);
return -1;
}
inline int mprotect(void *addr, size_t len, int prot)
{
DWORD newProtect = __map_mmap_prot_page(prot);
DWORD oldProtect = 0;
if (VirtualProtect(addr, len, newProtect, &oldProtect))
return 0;
errno = __map_mman_error(GetLastError(), EPERM);
return -1;
}
inline int msync(void *addr, size_t len, int /*flags*/)
{
if (FlushViewOfFile(addr, len))
return 0;
errno = __map_mman_error(GetLastError(), EPERM);
return -1;
}
inline int mlock(const void *addr, size_t len)
{
if (VirtualLock((LPVOID)addr, len))
return 0;
errno = __map_mman_error(GetLastError(), EPERM);
return -1;
}
inline int munlock(const void *addr, size_t len)
{
if (VirtualUnlock((LPVOID)addr, len))
return 0;
errno = __map_mman_error(GetLastError(), EPERM);
return -1;
}
#if !defined(__MINGW32__)
inline int ftruncate(const int fd, const int64_t size) {
if (fd < 0) {
errno = EBADF;
return -1;
}
HANDLE h = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
LARGE_INTEGER li_start, li_size;
li_start.QuadPart = static_cast<int64_t>(0);
li_size.QuadPart = size;
if (SetFilePointerEx(h, li_start, NULL, FILE_CURRENT) == ~0 ||
SetFilePointerEx(h, li_size, NULL, FILE_BEGIN) == ~0 ||
!SetEndOfFile(h)) {
unsigned long error = GetLastError();
fprintf(stderr, "I/O error while truncating: %lu\n", error);
switch (error) {
case ERROR_INVALID_HANDLE:
errno = EBADF;
break;
default:
errno = EIO;
break;
}
return -1;
}
return 0;
}
#endif
#endif
+1
View File
@@ -9,3 +9,4 @@ endif()
ocv_define_module(features2d opencv_imgproc ${debug_modules} OPTIONAL opencv_flann WRAP java objc python js)
ocv_install_3rdparty_licenses(mscr "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/mscr/chi_table_LICENSE.txt")
ocv_install_3rdparty_licenses(annoylib "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/annoy/LICENSE")
@@ -67,6 +67,13 @@
@{
@defgroup features2d_hal_interface Interface
@}
@defgroup features2d_annoy Approximate Nearest Neighbors Search in Multi-Dimensional Spaces
This section documents OpenCV's interface to the Annoy. Annoy (Approximate Nearest Neighbors Oh Yeah)
is a library to search for points in space that are close to a given query point. It also creates
large read-only file-based data structures that are mmapped into memory so that many processes may
share the same data.
@}
*/
@@ -1179,6 +1186,102 @@ CV_EXPORTS int getNearestPoint( const std::vector<Point2f>& recallPrecisionCurve
//! @}
/****************************************************************************************\
* Approximate Nearest Neighbors *
\****************************************************************************************/
//! @addtogroup features2d_annoy
//! @{
class CV_EXPORTS_W ANNIndex
{
public:
/** @brief Metrics used to calculate the distance between two feature vectors.
*/
enum Distance
{
DIST_EUCLIDEAN,
DIST_MANHATTAN,
DIST_ANGULAR,
DIST_HAMMING,
DIST_DOTPRODUCT
};
virtual ~ANNIndex() = default;
/** @brief Add feature vectors to index.
*
* @param features Matrix containing the feature vectors to index. The size of the matrix is
num_features x feature_dimension.
*/
CV_WRAP virtual void addItems(InputArray features) = 0;
/** @brief Build the index.
*
* @param trees Number of trees in the index. If not provided, the number is determined automatically
* in a way that at most 2x as much memory as the features vectors take is used.
*/
CV_WRAP virtual void build(int trees=-1) = 0;
/** @brief Performs a K-nearest neighbor search for given query vector(s) using the index.
*
* @param query The query vector(s).
* @param indices Matrix that will contain the indices of the K-nearest neighbors found, optional.
* @param dists Matrix that will contain the distances to the K-nearest neighbors found, optional.
* @param knn Number of nearest neighbors to search for.
* @param search_k The maximum number of nodes to inspect, which defaults to trees x knn if not provided.
*/
CV_WRAP virtual void knnSearch(InputArray query, OutputArray indices, OutputArray dists, int knn, int search_k=-1) = 0;
/** @brief Save the index to disk and loads it. After saving, no more vectors can be added.
*
* @param filename Filename of the index to be saved.
* @param prefault If prefault is set to true, it will pre-read the entire file into memory (using mmap
* with MAP_POPULATE). Default is false.
*/
CV_WRAP virtual void save(const String &filename, bool prefault=false) = 0;
/** @brief Loads (mmaps) an index from disk.
*
* @param filename Filename of the index to be loaded.
* @param prefault If prefault is set to true, it will pre-read the entire file into memory (using mmap
* with MAP_POPULATE). Default is false.
*/
CV_WRAP virtual void load(const String &filename, bool prefault=false) = 0;
/** @brief Return the number of trees in the index.
*/
CV_WRAP virtual int getTreeNumber() = 0;
/** @brief Return the number of feature vectors in the index.
*/
CV_WRAP virtual int getItemNumber() = 0;
/** @brief Prepare to build the index in the specified file instead of RAM (execute before adding
* items, no need to save after build)
*
* @param filename Filename of the index to be built.
*/
CV_WRAP virtual bool setOnDiskBuild(const String &filename) = 0;
/** @brief Initialize the random number generator with the given seed. Only necessary to pass this
* before adding the items. Will have no effect after calling build() or load().
*
* @param seed The given seed of the random number generator. Its value should be within the range of uint32_t.
*/
CV_WRAP virtual void setSeed(int seed) = 0;
/** @brief Creates an instance of annoy index class with given parameters
*
* @param dim The dimension of the feature vector.
* @param distType Metric to calculate the distance between two feature vectors, can be DIST_EUCLIDEAN,
DIST_MANHATTAN, DIST_ANGULAR, DIST_HAMMING, or DIST_DOTPRODUCT.
*/
CV_WRAP static Ptr<ANNIndex> create(int dim, ANNIndex::Distance distType=ANNIndex::DIST_EUCLIDEAN);
};
//! @} features2d_annoy
} /* namespace cv */
#endif
+264
View File
@@ -0,0 +1,264 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html
#include "precomp.hpp"
#include "../3rdparty/annoy/annoylib.h"
#include <opencv2/core/utils/logger.hpp>
namespace cv
{
struct Random
{
static const uint64 default_seed = 0xffffffff;
#if __cplusplus < 201103L
typedef uint64 seed_type;
#endif
RNG rng;
Random(uint64 seed = default_seed)
{
rng.state = seed;
}
inline int flip()
{
// Draw random 0 or 1
return rng.next() & 1;
}
inline size_t index(size_t n)
{
// Draw random integer between 0 and n-1 where n is at most the number of data points you have
return rng(unsigned(n));
}
inline void set_seed(uint64 seed)
{
rng.state = seed;
}
};
template <typename DataType, typename DistanceType>
class ANNIndexImpl : public ANNIndex
{
public:
ANNIndexImpl(int dimension) : dim(dimension)
{
index = makePtr<::cvannoy::AnnoyIndex<int, DataType, DistanceType, Random, ::cvannoy::AnnoyIndexSingleThreadedBuildPolicy>>(dimension);
}
void addItems(InputArray _dataset) CV_OVERRIDE
{
CV_Assert(!_dataset.empty());
Mat features = _dataset.getMat();
CV_Assert(features.cols == dim);
CV_Assert(features.type() == cv::DataType<DataType>::type);
int num = features.rows;
char* msg = nullptr;
if (!index->add_item(0, features.ptr<DataType>(0), &msg))
{
if (msg)
{
String errorMsg = msg;
free(msg);
CV_Error(Error::StsError, errorMsg);
}
else
{
CV_Error(Error::StsError, "Fail to add an item.");
}
}
for (int i = 1; i < num; ++i)
index->add_item(i, features.ptr<DataType>(i));
}
void build(int trees) CV_OVERRIDE
{
if (index->get_n_items() <= 0)
CV_Error(Error::StsError, "No items added. Please add items before building the index.");
if (trees <= 0)
trees = -1;
char* msg = nullptr;
if (!index->build(trees, -1, &msg))
{
if (msg)
{
String errorMsg = msg;
free(msg);
CV_Error(Error::StsError, errorMsg);
}
else
{
CV_Error(Error::StsError, "Fail to build the index.");
}
}
}
void knnSearch(InputArray _query, OutputArray _indices, OutputArray _dists, int knn, int search_k) CV_OVERRIDE
{
CV_Assert(!_query.empty() && _query.isContinuous());
Mat query = _query.getMat(), indices, dists;
CV_Assert(query.type() == cv::DataType<DataType>::type);
CV_Assert(knn > 0 && knn <= index->get_n_items());
int numQuery = query.rows;
if (_indices.needed())
{
indices = _indices.getMat();
if (!indices.isContinuous() || indices.type() != CV_32S ||
indices.rows != numQuery || indices.cols != knn)
{
if (!indices.isContinuous())
_indices.release();
_indices.create(numQuery, knn, CV_32S);
indices = _indices.getMat();
}
}
else
indices.create(numQuery, knn, CV_32S);
if (_dists.needed())
{
dists = _dists.getMat();
if (!dists.isContinuous() || dists.type() != cv::DataType<DataType>::type ||
dists.rows != numQuery || dists.cols != knn)
{
if (!_dists.isContinuous())
_dists.release();
_dists.create(numQuery, knn, cv::DataType<DataType>::type);
dists = _dists.getMat();
}
}
else
dists.create(numQuery, knn, cv::DataType<DataType>::type);
auto processBatch = [&](const Range& range)
{
std::vector<int> nns;
std::vector<DataType> distances;
for (int i = range.start; i < range.end; ++i)
{
index->get_nns_by_vector(query.ptr<DataType>(i), knn, search_k, &nns, &distances);
std::copy(nns.begin(), nns.end(), indices.ptr<int>(i));
std::copy(distances.begin(), distances.end(), dists.ptr<DataType>(i));
nns.clear();
distances.clear();
}
};
parallel_for_(Range(0, numQuery), processBatch);
}
void save(const String &filename, bool prefault) CV_OVERRIDE
{
char* msg = nullptr;
if (!index->save(filename.c_str(), prefault, &msg))
{
if (msg)
{
String errorMsg = msg;
free(msg);
CV_Error(Error::StsError, errorMsg);
}
else
{
CV_Error(Error::StsError, "Fail to save the index.");
}
}
}
void load(const String &filename, bool prefault) CV_OVERRIDE
{
char* msg = nullptr;
if (!index->load(filename.c_str(), prefault, &msg))
{
if (msg)
{
String errorMsg = msg;
free(msg);
CV_Error(Error::StsError, errorMsg);
}
else
{
CV_Error(Error::StsError, "Fail to load the index.");
}
}
}
int getTreeNumber() CV_OVERRIDE
{
return index->get_n_trees();
}
int getItemNumber() CV_OVERRIDE
{
return index->get_n_items();
}
bool setOnDiskBuild(const String &filename) CV_OVERRIDE
{
char* msg = nullptr;
if (index->on_disk_build(filename.c_str(), &msg))
return true;
else
{
if (msg)
{
String errorMsg = msg;
CV_LOG_ERROR(NULL, errorMsg);
free(msg);
}
else
{
CV_LOG_ERROR(NULL, "Cannot set build on disk.");
}
return false;
}
}
void setSeed(int seed) CV_OVERRIDE
{
index->set_seed(static_cast<uint32_t>(seed));
}
private:
int dim;
Ptr<::cvannoy::AnnoyIndex<int, DataType, DistanceType, Random, ::cvannoy::AnnoyIndexSingleThreadedBuildPolicy>> index;
};
Ptr<ANNIndex> ANNIndex::create(int dim, ANNIndex::Distance distType)
{
switch (distType)
{
case ANNIndex::DIST_EUCLIDEAN:
return makePtr<ANNIndexImpl<float, ::cvannoy::Euclidean>>(dim);
break;
case ANNIndex::DIST_MANHATTAN:
return makePtr<ANNIndexImpl<float, ::cvannoy::Manhattan>>(dim);
break;
case ANNIndex::DIST_ANGULAR:
return makePtr<ANNIndexImpl<float, ::cvannoy::Angular>>(dim);
break;
case ANNIndex::DIST_HAMMING:
return makePtr<ANNIndexImpl<uchar, ::cvannoy::Hamming>>(dim);
break;
case ANNIndex::DIST_DOTPRODUCT:
return makePtr<ANNIndexImpl<float, ::cvannoy::DotProduct>>(dim);
break;
default:
CV_Error(Error::StsBadArg, "Unknown/unsupported distance type");
}
};
}
@@ -330,4 +330,55 @@ TEST(Features2d_FLANN_Saved, regression) { CV_FlannSavedIndexTest test; test.saf
#endif
class CV_AnnoyTest : public NearestNeighborTest
{
public:
CV_AnnoyTest() : NearestNeighborTest(), index(NULL) {}
protected:
void createModel(const Mat& data) CV_OVERRIDE
{
index = ANNIndex::create(data.cols);
index->addItems(data);
index->build();
}
int findNeighbors(Mat& points, Mat& neighbors) CV_OVERRIDE
{
Mat distances(points.rows, neighbors.cols, CV_32FC1);
int knn = 1;
index->knnSearch(points, neighbors, distances, knn);
Mat neighbors1(neighbors.size(), CV_32SC1);
for(int i = 0; i < points.rows; i++)
{
float* fltPtr = points.ptr<float>(i);
vector<float> query(fltPtr, fltPtr + points.cols);
vector<int> indices(neighbors1.cols, 0);
vector<float> dists(distances.cols, 0);
index->knnSearch(query, indices, dists, knn);
vector<int>::const_iterator it = indices.begin();
for(int j = 0; it != indices.end(); ++it, j++)
neighbors1.at<int>(i, j) = *it;
}
if (cvtest::norm(neighbors, neighbors1, NORM_L1) > 0)
{
ts->printf(cvtest::TS::LOG, "bad accuray of find nearest neighbors\n");
ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
}
return ts->get_err_code();
}
void releaseModel() CV_OVERRIDE {}
Ptr<ANNIndex> index;
};
TEST(Features2d_ANNIndex, regression) {CV_AnnoyTest test; test.safe_run();}
}} // namespace