Added VWDictionary tests and doc. Fixed LSH not working (fix from https://github.com/flann-lib/flann/pull/472

This commit is contained in:
matlabbe
2025-12-22 21:07:25 -08:00
parent c815c12fc7
commit f7cb330e31
9 changed files with 1238 additions and 59 deletions
+419 -13
View File
@@ -43,18 +43,52 @@ class DBDriver;
class VisualWord; class VisualWord;
class FlannIndex; class FlannIndex;
/**
* @class VWDictionary
* @brief Manages a dictionary of visual words for visual place recognition and loop closure detection.
*
* The VWDictionary class maintains a collection of visual words (feature descriptors) and provides
* efficient nearest neighbor search capabilities. It supports both incremental and fixed dictionary modes:
* - Incremental mode: New visual words are added dynamically as new images are processed
* - Fixed mode: A pre-computed dictionary is loaded from a file
*
* The class uses various nearest neighbor search strategies (FLANN, brute force, GPU-accelerated)
* to match descriptors efficiently. It tracks word references to signatures (images) and manages
* unused words for memory optimization.
*
* @note Visual words are identified by unique integer IDs starting from VWDictionary::ID_START
*/
class RTABMAP_CORE_EXPORT VWDictionary class RTABMAP_CORE_EXPORT VWDictionary
{ {
public: public:
/**
* @enum NNStrategy
* @brief Nearest neighbor search strategies for descriptor matching
*/
enum NNStrategy{ enum NNStrategy{
kNNFlannNaive, kNNFlannNaive, ///< FLANN naive search (exhaustive)
kNNFlannKdTree, kNNFlannKdTree, ///< FLANN kd-tree index (fast for high-dimensional descriptors)
kNNFlannLSH, kNNFlannLSH, ///< FLANN Locality-Sensitive Hashing (ideal for binary descriptors)
kNNBruteForce, kNNBruteForce, ///< Brute force CPU search
kNNBruteForceGPU, kNNBruteForceGPU, ///< Brute force GPU-accelerated search (requires CUDA)
kNNUndef}; kNNUndef ///< Undefined strategy
};
/**
* @brief Starting ID for visual words (typically 1)
*/
static const int ID_START; static const int ID_START;
/**
* @brief Invalid visual word ID (typically 0)
*/
static const int ID_INVALID; static const int ID_INVALID;
/**
* @brief Get the name of a nearest neighbor strategy
* @param strategy The strategy enum value
* @return String representation of the strategy name
*/
static std::string nnStrategyName(NNStrategy strategy) static std::string nnStrategyName(NNStrategy strategy)
{ {
switch(strategy) { switch(strategy) {
@@ -74,85 +108,457 @@ public:
} }
public: public:
/**
* @brief Constructor
* @param parameters Optional parameters map to configure the dictionary
*/
VWDictionary(const ParametersMap & parameters = ParametersMap()); VWDictionary(const ParametersMap & parameters = ParametersMap());
/**
* @brief Destructor
*
* Clears all visual words and releases resources.
*/
virtual ~VWDictionary(); virtual ~VWDictionary();
/**
* @brief Parse and apply parameters from a parameters map
* @param parameters Parameters map containing configuration values
*/
virtual void parseParameters(const ParametersMap & parameters); virtual void parseParameters(const ParametersMap & parameters);
/**
* @brief Update the search index with newly added words
*
* This method rebuilds the nearest neighbor search index (FLANN, etc.)
* to include any words that were added but not yet indexed.
*/
virtual void update(); virtual void update();
/**
* @brief Add new visual words from descriptors
* @param descriptors Matrix of descriptors (one row per descriptor)
* @param signatureId ID of the signature (image) these descriptors belong to
* @return List of visual word IDs that were added or matched
*
* For each descriptor, either matches it to an existing visual word
* (using nearest neighbor search with NNDR ratio) or creates a new visual word.
*
* @note If the dictionary is not incremental (fixed), NNDR is not applied
* and the closest existing visual word ID is always returned. If the dictionary is
* incremental, a new visual word is created if NNDR validation
* passes; otherwise, a reference to an existing visual word is added.
*/
virtual std::list<int> addNewWords( virtual std::list<int> addNewWords(
const cv::Mat & descriptors, const cv::Mat & descriptors,
int signatureId); int signatureId);
/**
* @brief Add an existing visual word to the dictionary
* @param vw Pointer to the visual word to add (ownership is transferred)
*
* @note The dictionary takes ownership of the VisualWord object
*/
virtual void addWord(VisualWord * vw); virtual void addWord(VisualWord * vw);
/**
* @brief Find nearest neighbor visual word IDs for a list of visual words
* @param vws List of visual words to match
* @return Vector of visual word IDs (one per input visual word)
*
* @note If the dictionary is not incremental (fixed), NNDR is not applied
* and the closest visual word ID is always returned. If the dictionary is
* incremental, a valid visual word ID is returned only if NNDR validation passes.
*/
std::vector<int> findNN(const std::list<VisualWord *> & vws) const; std::vector<int> findNN(const std::list<VisualWord *> & vws) const;
/**
* @brief Find nearest neighbor visual word IDs for descriptors
* @param descriptors Matrix of descriptors (one row per descriptor)
* @return Vector of visual word IDs (one per descriptor)
*
* @note If the dictionary is not incremental (fixed), NNDR is not applied
* and the closest visual word ID is always returned. If the dictionary is
* incremental, a valid visual word ID is returned only if NNDR validation passes.
*/
std::vector<int> findNN(const cv::Mat & descriptors) const; std::vector<int> findNN(const cv::Mat & descriptors) const;
/**
* @brief Add a reference from a visual word to a signature
* @param wordId ID of the visual word
* @param signatureId ID of the signature (image)
*
* Tracks which signatures use which visual words. If the word was unused,
* it is removed from the unused words list.
*/
void addWordRef(int wordId, int signatureId); void addWordRef(int wordId, int signatureId);
/**
* @brief Remove all references from a visual word to a signature
* @param wordId ID of the visual word
* @param signatureId ID of the signature (image)
*
* If the word has no more references after this operation,
* it is added to the unused words list.
*/
void removeAllWordRef(int wordId, int signatureId); void removeAllWordRef(int wordId, int signatureId);
/**
* @brief Get a visual word by ID
* @param id Visual word ID
* @return Pointer to the visual word, or nullptr if not found
*/
const VisualWord * getWord(int id) const; const VisualWord * getWord(int id) const;
/**
* @brief Get an unused visual word by ID
* @param id Visual word ID
* @return Pointer to the unused visual word, or nullptr if not found or not unused
*
* @note Ownership of the returned visual word still belongs to the dictionary.
* To transfer ownership to the caller, removeWords() must be called on this word.
*/
VisualWord * getUnusedWord(int id) const; VisualWord * getUnusedWord(int id) const;
/**
* @brief Set the last word ID (used when loading from database)
* @param id Last word ID
*/
void setLastWordId(int id) {_lastWordId = id;} void setLastWordId(int id) {_lastWordId = id;}
/**
* @brief Get all visual words
* @return Map of visual word ID to VisualWord pointer
*/
const std::map<int, VisualWord *> & getVisualWords() const {return _visualWords;} const std::map<int, VisualWord *> & getVisualWords() const {return _visualWords;}
/**
* @brief Get the Nearest Neighbor Distance Ratio (NNDR) threshold
* @return NNDR ratio value
*
* The NNDR ratio is used to determine if a descriptor matches an existing
* visual word. If the ratio of distances to the first and second nearest
* neighbors is below this threshold, a match is accepted.
*
* @note The NNDR method was introduced in "Distinctive Image Features
* from Scale-Invariant Keypoints" by David Lowe (IJCV 2004).
*/
float getNndrRatio() const {return _nndrRatio;} float getNndrRatio() const {return _nndrRatio;}
/**
* @brief Get the count of words not yet indexed in the search tree
* @return Number of words waiting to be indexed
*/
unsigned int getNotIndexedWordsCount() const {return (int)_notIndexedWords.size();} unsigned int getNotIndexedWordsCount() const {return (int)_notIndexedWords.size();}
/**
* @brief Get the ID of the last indexed word
* @return Last indexed word ID, or 0 if no words are indexed
*/
int getLastIndexedWordId() const; int getLastIndexedWordId() const;
/**
* @brief Get the total number of active word-to-signature references
* @return Total count of active references
*/
int getTotalActiveReferences() const {return _totalActiveReferences;} int getTotalActiveReferences() const {return _totalActiveReferences;}
/**
* @brief Get the count of words currently indexed in the search tree
* @return Number of indexed words
*/
unsigned int getIndexedWordsCount() const; unsigned int getIndexedWordsCount() const;
/**
* @brief Get the memory used by the search index
* @return Memory usage in kilobytes
*/
unsigned int getIndexMemoryUsed() const; // KB unsigned int getIndexMemoryUsed() const; // KB
/**
* @brief Get the total memory used by the dictionary
* @return Memory usage in bytes
*/
unsigned long getMemoryUsed() const; //Bytes unsigned long getMemoryUsed() const; //Bytes
bool setNNStrategy(NNStrategy strategy); // Return true if the search tree has been re-initialized
/**
* @brief Set the nearest neighbor search strategy
* @param strategy The strategy to use
* @return true if the search tree was re-initialized (strategy changed), false otherwise
*
* Changing the strategy will rebuild the search index if words are already indexed.
*/
bool setNNStrategy(NNStrategy strategy);
/**
* @brief Get the current nearest neighbor search strategy
* @return The current NNStrategy
*/
NNStrategy getNNStrategy() const {return _strategy;}
/**
* @brief Check if the dictionary is in incremental mode
* @return true if incremental, false if fixed
*/
bool isIncremental() const {return _incrementalDictionary;} bool isIncremental() const {return _incrementalDictionary;}
/**
* @brief Check if FLANN index is updated incrementally
* @return true if incremental FLANN updates are enabled
*/
bool isIncrementalFlann() const {return _incrementalFlann;} bool isIncrementalFlann() const {return _incrementalFlann;}
/**
* @brief Set the dictionary to incremental mode
*
* In incremental mode, new visual words can be added dynamically.
* This cannot be called if a fixed dictionary is already loaded.
*/
void setIncrementalDictionary(); void setIncrementalDictionary();
/**
* @brief Set the dictionary to fixed mode and load from file
* @param dictionaryPath Path to the dictionary file (.txt or .db format)
*
* Loads a pre-computed dictionary from a file. The dictionary file format in txt format
* should be: one line per visual word, with word ID followed by descriptor values.
* This cannot be called if words are already in the dictionary.
*/
void setFixedDictionary(const std::string & dictionaryPath); void setFixedDictionary(const std::string & dictionaryPath);
/**
* @brief Check if the dictionary has been modified since last save
* @return true if modified, false otherwise
*/
bool isModified() const; bool isModified() const;
/**
* @brief Serialize the search index to a byte vector
* @return Serialized index data
*/
std::vector<unsigned char> serializeIndex() const; std::vector<unsigned char> serializeIndex() const;
void deserializeIndex(const std::vector<unsigned char> & data);
void deserializeIndex(const unsigned char * data, size_t size); /**
* @brief Deserialize the search index from a byte vector
* @param data Serialized index data
* @return true if deserialization was successful, false otherwise
*/
bool deserializeIndex(const std::vector<unsigned char> & data);
/**
* @brief Deserialize the search index from raw bytes
* @param data Pointer to serialized index data
* @param size Size of the data in bytes
* @return true if deserialization was successful, false otherwise
*/
bool deserializeIndex(const unsigned char * data, size_t size);
/**
* @brief Export the dictionary to files
* @param fileNameReferences Path to file for word-to-signature references
* @param fileNameDescriptors Path to file for visual word descriptors
*
* Exports the dictionary in a format that can be loaded later.
*/
void exportDictionary(const char * fileNameReferences, const char * fileNameDescriptors) const; void exportDictionary(const char * fileNameReferences, const char * fileNameDescriptors) const;
/**
* @brief Clear all visual words and reset the dictionary
* @param printWarningsIfNotEmpty If true, print warnings if dictionary is not empty
*
* Deletes all visual words and releases all resources.
*/
void clear(bool printWarningsIfNotEmpty = true); void clear(bool printWarningsIfNotEmpty = true);
/**
* @brief Get all unused visual words
* @return Vector of pointers to unused visual words
*
* Unused words are visual words that have no references to any signatures.
*
* @note Ownership of the returned visual words still belongs to the dictionary.
* To transfer ownership to the caller, removeWords() must be called on these words.
*/
std::vector<VisualWord *> getUnusedWords() const; std::vector<VisualWord *> getUnusedWords() const;
/**
* @brief Get IDs of all unused visual words
* @return Vector of unused word IDs
*/
std::vector<int> getUnusedWordIds() const; std::vector<int> getUnusedWordIds() const;
/**
* @brief Get the count of unused visual words
* @return Number of unused words
*/
unsigned int getUnusedWordsSize() const {return (int)_unusedWords.size();} unsigned int getUnusedWordsSize() const {return (int)_unusedWords.size();}
/**
* @brief Remove words from the dictionary
* @param words Vector of visual word pointers to remove
*
* @note The caller is responsible for deleting the VisualWord objects
*/
void removeWords(const std::vector<VisualWord*> & words); // caller must delete the words void removeWords(const std::vector<VisualWord*> & words); // caller must delete the words
/**
* @brief Delete all unused visual words
*
* Removes and deletes visual words that have no references to any signatures.
*/
void deleteUnusedWords(); void deleteUnusedWords();
public: public:
/**
* @brief Convert binary descriptors to 32-bit float format
* @param descriptorsIn Input descriptors (CV_8UC1 for binary, or CV_32FC1 for float)
* @param byteToFloat Conversion mode:
* - If true: Simple type conversion from CV_8UC1 to CV_32FC1 using OpenCV's convertTo().
* Each byte value becomes a float value (output dimensions unchanged).
* - If false: Bit-by-bit expansion for binary descriptors (e.g., ORB, BRIEF).
* Each input byte (8 bits) is expanded into 8 float values (0.0f or 1.0f),
* one per bit. Output has 8x the number of columns (e.g., 32 bytes -> 256 floats).
* @return Descriptors in 32-bit float format (CV_32FC1)
*/
static cv::Mat convertBinTo32F(const cv::Mat & descriptorsIn, bool byteToFloat = true); static cv::Mat convertBinTo32F(const cv::Mat & descriptorsIn, bool byteToFloat = true);
/**
* @brief Convert 32-bit float descriptors to binary format
* @param descriptorsIn Input descriptors (CV_32FC1)
* @param byteToFloat Conversion mode:
* - If true: Simple type conversion from CV_32FC1 to CV_8UC1 using OpenCV's convertTo().
* Each float value becomes a byte value (output dimensions unchanged).
* - If false: Bit-by-bit packing for binary descriptors.
* Each group of 8 float values (0.0f or 1.0f) is packed into 1 byte (8 bits),
* one bit per float. Input must have columns divisible by 8.
* Output has 1/8 the number of columns (e.g., 256 floats -> 32 bytes).
* @return Descriptors in binary format (CV_8UC1)
*/
static cv::Mat convert32FToBin(const cv::Mat & descriptorsIn, bool byteToFloat = true); static cv::Mat convert32FToBin(const cv::Mat & descriptorsIn, bool byteToFloat = true);
protected: protected:
/**
* @brief Get the next available visual word ID
* @return Next unique word ID
*/
int getNextId(); int getNextId();
protected: protected:
/**
* @brief Map of visual word ID to VisualWord pointer
* @note All visual words (used and unused) are stored here
*/
std::map<int, VisualWord *> _visualWords; //<id,VisualWord*> std::map<int, VisualWord *> _visualWords; //<id,VisualWord*>
int _totalActiveReferences; // keep track of all references for updating the common signature
/**
* @brief Total count of active word-to-signature references
* @note Used to track all references for updating common signatures
*/
int _totalActiveReferences;
private: private:
/**
* @brief Whether the dictionary is in incremental mode
*/
bool _incrementalDictionary; bool _incrementalDictionary;
/**
* @brief Whether FLANN index is updated incrementally
*/
bool _incrementalFlann; bool _incrementalFlann;
/**
* @brief Rebalancing factor for FLANN index updates
*/
float _rebalancingFactor; float _rebalancingFactor;
/**
* @brief Whether to convert descriptors from byte to float format
*/
bool _byteToFloat; bool _byteToFloat;
/**
* @brief Nearest Neighbor Distance Ratio threshold
*
* @note The NNDR method was introduced in "Distinctive Image Features
* from Scale-Invariant Keypoints" by David Lowe (IJCV 2004).
*/
float _nndrRatio; float _nndrRatio;
/**
* @brief Path to the pre-computed dictionary file (.txt or .db)
*/
std::string _dictionaryPath; // a pre-computed dictionary (.txt or .db) std::string _dictionaryPath; // a pre-computed dictionary (.txt or .db)
/**
* @brief Path to a new dictionary file to load
*/
std::string _newDictionaryPath; // a pre-computed dictionary (.txt or .db) std::string _newDictionaryPath; // a pre-computed dictionary (.txt or .db)
/**
* @brief Whether new words should be compared together before adding
*/
bool _newWordsComparedTogether; bool _newWordsComparedTogether;
/**
* @brief Whether to include checksum when serializing index
*/
bool _serializeWithChecksum; bool _serializeWithChecksum;
/**
* @brief ID of the last visual word added
*/
int _lastWordId; int _lastWordId;
/**
* @brief Whether to use L1 distance metric instead of L2
*/
bool useDistanceL1_; bool useDistanceL1_;
/**
* @brief FLANN index for fast nearest neighbor search
*/
FlannIndex * _flannIndex; FlannIndex * _flannIndex;
/**
* @brief Data matrix for the search tree
*/
cv::Mat _dataTree; cv::Mat _dataTree;
/**
* @brief Whether the dictionary has been modified since last save
*/
bool _modified; bool _modified;
/**
* @brief Current nearest neighbor search strategy
*/
NNStrategy _strategy; NNStrategy _strategy;
/**
* @brief Map from search index position to visual word ID
*/
std::map<int ,int> _mapIndexId; std::map<int ,int> _mapIndexId;
/**
* @brief Map from visual word ID to search index position
*/
std::map<int ,int> _mapIdIndex; std::map<int ,int> _mapIdIndex;
std::map<int, VisualWord*> _unusedWords; //<id,VisualWord*>, note that these words stay in _visualWords
std::set<int> _notIndexedWords; // Words that are not indexed in the dictionary /**
std::set<int> _removedIndexedWords; // Words not anymore in the dictionary but still indexed in the dictionary * @brief Map of unused visual words (words with no references)
* @note These words remain in _visualWords but are marked as unused
*/
std::map<int, VisualWord*> _unusedWords; //<id,VisualWord*>
/**
* @brief Set of word IDs that are not yet indexed in the search tree
*/
std::set<int> _notIndexedWords;
/**
* @brief Set of word IDs that were removed from dictionary but still indexed
*/
std::set<int> _removedIndexedWords;
}; };
} // namespace rtabmap } // namespace rtabmap
+8 -1
View File
@@ -3647,7 +3647,14 @@ void DBDriverSqlite3::loadQuery(VWDictionary & dictionary, bool lastStateOnly) c
if(dataSize>4 && data) if(dataSize>4 && data)
{ {
UDEBUG("A flann index was saved in the database (size=%ld).", dataSize); UDEBUG("A flann index was saved in the database (size=%ld).", dataSize);
dictionary.deserializeIndex((const unsigned char*)data, dataSize); if(!dictionary.deserializeIndex((const unsigned char*)data, dataSize))
{
UERROR("Failed to deserialize dictionary's index! See previous logs for reason.");
}
else
{
UINFO("Sucessfully loaded dictionary's index.");
}
} }
else { else {
UDEBUG("No flann index was saved in the database."); UDEBUG("No flann index was saved in the database.");
+13 -3
View File
@@ -304,6 +304,7 @@ void FlannIndex::buildIndex(
break; break;
case FLANN_INDEX_LSH: case FLANN_INDEX_LSH:
UASSERT(features.type() == CV_8UC1); UASSERT(features.type() == CV_8UC1);
UASSERT_MSG(features.cols >= 8, "LSH requires a minimum of 8 dimensions to provide valid results.");
params = rtflann::LshIndexParams(12, 20, 2); params = rtflann::LshIndexParams(12, 20, 2);
break; break;
default: default:
@@ -719,10 +720,11 @@ void FlannIndex::knnSearch(
UERROR("Flann index not yet created!"); UERROR("Flann index not yet created!");
return; return;
} }
indices.create(query.rows, knn, sizeof(size_t)==8?CV_64F:CV_32S);
dists.create(query.rows, knn, featuresType_ == CV_8UC1?CV_32S:CV_32F);
rtflann::Matrix<size_t> indicesF((size_t*)indices.data, indices.rows, indices.cols); dists = cv::Mat(query.rows, knn, featuresType_ == CV_8UC1?CV_32S:CV_32F, cv::Scalar(-1));
std::vector<size_t> indicesBuffer(query.rows * knn, std::numeric_limits<size_t>::max());
rtflann::Matrix<size_t> indicesF((size_t*)indicesBuffer.data(), query.rows, knn);
rtflann::SearchParams params = rtflann::SearchParams(checks, eps, sorted); rtflann::SearchParams params = rtflann::SearchParams(checks, eps, sorted);
@@ -749,6 +751,14 @@ void FlannIndex::knnSearch(
((rtflann::Index<rtflann::L2<float> >*)index_)->knnSearch(queryF, indicesF, distsF, knn, params); ((rtflann::Index<rtflann::L2<float> >*)index_)->knnSearch(queryF, indicesF, distsF, knn, params);
} }
} }
indices.create(query.rows, knn, CV_32S);
int * ptr = indices.ptr<int>();
for(size_t i=0 ; i<indicesBuffer.size(); i+=2)
{
ptr[i] = indicesBuffer[i] == std::numeric_limits<size_t>::max()?-1:(int)indicesBuffer[i];
ptr[i+1] = indicesBuffer[i+1] == std::numeric_limits<size_t>::max()?-1:(int)indicesBuffer[i+1];
}
} }
void FlannIndex::radiusSearch( void FlannIndex::radiusSearch(
+19 -25
View File
@@ -661,6 +661,8 @@ void VWDictionary::update()
ULOGGER_DEBUG("_mapIndexId.size() = %d, words.size()=%d, _dim=%d",_mapIndexId.size(), _visualWords.size(), dim); ULOGGER_DEBUG("_mapIndexId.size() = %d, words.size()=%d, _dim=%d",_mapIndexId.size(), _visualWords.size(), dim);
ULOGGER_DEBUG("copying data = %f s", timer.ticks()); ULOGGER_DEBUG("copying data = %f s", timer.ticks());
if(_strategy < kNNBruteForce)
{
_flannIndex->buildIndex( _flannIndex->buildIndex(
_strategy == kNNFlannNaive ? FlannIndex::FLANN_INDEX_LINEAR: _strategy == kNNFlannNaive ? FlannIndex::FLANN_INDEX_LINEAR:
_strategy == kNNFlannLSH ? FlannIndex::FLANN_INDEX_LSH: _strategy == kNNFlannLSH ? FlannIndex::FLANN_INDEX_LSH:
@@ -671,6 +673,7 @@ void VWDictionary::update()
ULOGGER_DEBUG("Time to create kd tree = %f s", timer.ticks()); ULOGGER_DEBUG("Time to create kd tree = %f s", timer.ticks());
} }
} }
}
UDEBUG("Dictionary updated! (size=%d added=%d removed=%d)", UDEBUG("Dictionary updated! (size=%d added=%d removed=%d)",
_dataTree.rows, _notIndexedWords.size(), _removedIndexedWords.size()); _dataTree.rows, _notIndexedWords.size(), _removedIndexedWords.size());
} }
@@ -697,38 +700,38 @@ std::vector<unsigned char> VWDictionary::serializeIndex() const
return _flannIndex->serializeIndex(_serializeWithChecksum); return _flannIndex->serializeIndex(_serializeWithChecksum);
} }
void VWDictionary::deserializeIndex(const std::vector<unsigned char> & data) bool VWDictionary::deserializeIndex(const std::vector<unsigned char> & data)
{ {
deserializeIndex(data.data(), data.size()); return deserializeIndex(data.data(), data.size());
} }
void VWDictionary::deserializeIndex(const unsigned char * data, size_t size) bool VWDictionary::deserializeIndex(const unsigned char * data, size_t size)
{ {
if(data== NULL || size == 0) if(data== NULL || size == 0)
{ {
UWARN("Trying to deserialize empty data, aborting."); UWARN("Trying to deserialize empty data, aborting.");
return; return false;
} }
UDEBUG("Loading flann index... (data size=%ld bytes)", size); UDEBUG("Loading flann index... (data size=%ld bytes)", size);
if(_strategy >= kNNBruteForce) { if(_strategy >= kNNBruteForce) {
//ignore //ignore
return; return false;
} }
if(_flannIndex->isBuilt()) { if(_flannIndex->isBuilt()) {
UERROR("Flann index is already built, cannot deserialize data!"); UERROR("Flann index is already built, cannot deserialize data!");
return; return false;
} }
if(_visualWords.empty()) { if(_visualWords.empty()) {
UERROR("Descriptors should be added before deserializing flann index! See VWDictionary::addWord()"); UERROR("Descriptors should be added before deserializing flann index! See VWDictionary::addWord()");
return; return false;
} }
if(!(_removedIndexedWords.empty() && _visualWords.size() == _notIndexedWords.size())) { if(!(_removedIndexedWords.empty() && _visualWords.size() == _notIndexedWords.size())) {
UERROR("State of dictionary not as expected before deserializing. (removed words=%ld, words=%ld, not indexed=%ld)", UERROR("State of dictionary not as expected before deserializing. (removed words=%ld, words=%ld, not indexed=%ld)",
_removedIndexedWords.size(), _visualWords.size(), _notIndexedWords.size()); _removedIndexedWords.size(), _visualWords.size(), _notIndexedWords.size());
return; return false;
} }
std::map<int, int> mapIndexId; std::map<int, int> mapIndexId;
@@ -818,9 +821,11 @@ void VWDictionary::deserializeIndex(const unsigned char * data, size_t size)
else { else {
UWARN("Failed deserializing flann index data (error: %s), the index will be rebuilt on next update.", errorMsg.c_str()); UWARN("Failed deserializing flann index data (error: %s), the index will be rebuilt on next update.", errorMsg.c_str());
_flannIndex->release(); // reset to initial state _flannIndex->release(); // reset to initial state
return false;
} }
ULOGGER_DEBUG("Time to load flann index = %f s", timer.ticks()); ULOGGER_DEBUG("Time to load flann index = %f s", timer.ticks());
return true;
} }
void VWDictionary::clear(bool printWarningsIfNotEmpty) void VWDictionary::clear(bool printWarningsIfNotEmpty)
@@ -1075,14 +1080,9 @@ std::list<int> VWDictionary::addNewWords(
for(int j=0; j<dists.cols; ++j) for(int j=0; j<dists.cols; ++j)
{ {
float d = dists.at<float>(i,j); float d = dists.at<float>(i,j);
int index; int index = results.at<int>(i, j);
if (sizeof(size_t) == 8) if(index<0) {
{ continue;
index = *((size_t*)&results.at<double>(i, j));
}
else
{
index = *((size_t*)&results.at<int>(i, j));
} }
int id = uValue(_mapIndexId, index); int id = uValue(_mapIndexId, index);
if(d >= 0.0f && id != 0) if(d >= 0.0f && id != 0)
@@ -1440,15 +1440,9 @@ std::vector<int> VWDictionary::findNN(const cv::Mat & queryIn) const
for(int j=0; j<dists.cols; ++j) for(int j=0; j<dists.cols; ++j)
{ {
float d = dists.at<float>(i,j); float d = dists.at<float>(i,j);
int index; int index = results.at<int>(i, j);
if(index < 0) {
if (sizeof(size_t) == 8) continue;
{
index = *((size_t*)&results.at<double>(i, j));
}
else
{
index = *((size_t*)&results.at<int>(i, j));
} }
int id = uValue(_mapIndexId, index); int id = uValue(_mapIndexId, index);
if(d >= 0.0f && id != 0) if(d >= 0.0f && id != 0)
+4 -3
View File
@@ -107,7 +107,7 @@ public:
capacity_(capacity_) capacity_(capacity_)
{ {
// reserving capacity to prevent memory re-allocations // reserving capacity to prevent memory re-allocations
dist_index_.resize(capacity_, DistIndex(std::numeric_limits<DistanceType>::max(),-1)); dist_index_.resize(capacity_, DistIndex(std::numeric_limits<DistanceType>::max(),std::numeric_limits<size_t>::max()));
clear(); clear();
} }
@@ -210,7 +210,7 @@ public:
KNNResultSet(int capacity) : capacity_(capacity) KNNResultSet(int capacity) : capacity_(capacity)
{ {
// reserving capacity to prevent memory re-allocations // reserving capacity to prevent memory re-allocations
dist_index_.resize(capacity_, DistIndex(std::numeric_limits<DistanceType>::max(),-1)); dist_index_.resize(capacity_, DistIndex(std::numeric_limits<DistanceType>::max(),std::numeric_limits<size_t>::max()));
clear(); clear();
} }
@@ -252,7 +252,8 @@ public:
#endif #endif
{ {
// Check for duplicate indices // Check for duplicate indices
for (size_t j = i - 1; dist_index_[j].dist_ == dist && j--;) { // https://github.com/flann-lib/flann/pull/472
for (size_t j = i; j-- && dist_index_[j].dist_ == dist;) {
if (dist_index_[j].index_ == index) { if (dist_index_[j].index_ == index) {
return; return;
} }
+11 -6
View File
@@ -52,11 +52,16 @@ target_link_libraries(test_util3d_surface gtest_main rtabmap_core)
gtest_discover_tests(test_util3d_surface) gtest_discover_tests(test_util3d_surface)
#VisualWord.h #VisualWord.h
add_executable(VisualWordTests VisualWordTests.cpp) add_executable(test_visualword test_visualword.cpp)
target_link_libraries(VisualWordTests gtest_main rtabmap_core) target_link_libraries(test_visualword gtest_main rtabmap_core)
gtest_discover_tests(VisualWordTests) gtest_discover_tests(test_visualword)
#VWDictionary.h
add_executable(test_vwdictionary test_vwdictionary.cpp)
target_link_libraries(test_vwdictionary gtest_main rtabmap_core)
gtest_discover_tests(test_vwdictionary)
#Transform.h #Transform.h
add_executable(TransformTests TransformTests.cpp) add_executable(test_transform test_transform.cpp)
target_link_libraries(TransformTests gtest_main rtabmap_core) target_link_libraries(test_transform gtest_main rtabmap_core)
gtest_discover_tests(TransformTests) gtest_discover_tests(test_transform)
+756
View File
@@ -0,0 +1,756 @@
#include <gtest/gtest.h>
#include <opencv2/core.hpp>
#include "rtabmap/core/VWDictionary.h"
#include "rtabmap/core/VisualWord.h"
#include "rtabmap/core/Parameters.h"
#include "rtabmap/utilite/ULogger.h"
#include "rtabmap/utilite/UFile.h"
#include <vector>
#include <list>
#include <iterator>
#include <fstream>
#include <cstdio>
using namespace rtabmap;
class VWDictionaryTest : public ::testing::Test {
protected:
void SetUp() override {
// Create a dictionary with default parameters
dict = new VWDictionary();
}
void TearDown() override {
delete dict;
}
VWDictionary* dict;
};
TEST_F(VWDictionaryTest, Constructor)
{
EXPECT_TRUE(dict != nullptr);
EXPECT_TRUE(dict->isIncremental());
EXPECT_EQ(dict->getVisualWords().size(), 0u);
EXPECT_EQ(dict->getTotalActiveReferences(), 0);
EXPECT_EQ(dict->getIndexedWordsCount(), 0u);
}
TEST_F(VWDictionaryTest, AddNewWords_Incremental)
{
// Test incremental mode - NNDR is applied, new words created if NNDR fails
// Test with all NNStrategy values
VWDictionary::NNStrategy strategies[] = {
VWDictionary::kNNFlannNaive,
VWDictionary::kNNFlannKdTree,
VWDictionary::kNNFlannLSH,
VWDictionary::kNNBruteForce,
VWDictionary::kNNBruteForceGPU
};
// That will mke logic below works with numbers chosen
ParametersMap params;
params.insert(ParametersPair(Parameters::kKpNndrRatio(), "0.4"));
dict->parseParameters(params);
ULogger::setType(ULogger::kTypeConsole);
ULogger::setLevel(ULogger::kDebug);
for(VWDictionary::NNStrategy strategy : strategies)
{
// Reset dictionary for each strategy
dict->clear();
dict->setNNStrategy(strategy);
EXPECT_TRUE(dict->isIncremental());
if(strategy == VWDictionary::kNNBruteForceGPU)
{
#if CV_MAJOR_VERSION < 3
#ifdef HAVE_OPENCV_GPU
if(!cv::gpu::getCudaEnabledDeviceCount())
{
strategy = VWDictionary::kNNBruteForce;
}
#else
strategy = VWDictionary::kNNBruteForce;
#endif
#else
#ifdef HAVE_OPENCV_CUDAFEATURES2D
if(!cv::cuda::getCudaEnabledDeviceCount())
{
strategy = VWDictionary::kNNBruteForce;
}
#else
strategy = VWDictionary::kNNBruteForce;
#endif
#endif
}
EXPECT_EQ(dict->getNNStrategy(), strategy);
// Add initial words to dictionary (2D descriptors)
// Word 1: (0, 0)
// Word 2: (15, 0)
// Word 3: (0, 255)
// Using dimension 8 to support LSH
cv::Mat initialDescriptors = (cv::Mat_<float>(3, 8) <<
0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
15.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 255.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
// Convert to binary if using LSH strategy
if(strategy == VWDictionary::kNNFlannLSH)
{
// Convert float descriptors to binary
initialDescriptors = VWDictionary::convert32FToBin(initialDescriptors, true);
std::cout << initialDescriptors << std::endl;
}
std::list<int> addedIds = dict->addNewWords(initialDescriptors, 1);
dict->update();
unsigned int initialWordCount = dict->getVisualWords().size();
EXPECT_FALSE(addedIds.empty()) << "Strategy: " << VWDictionary::nnStrategyName(strategy);
EXPECT_EQ(initialWordCount, 3u) << "Strategy: " << VWDictionary::nnStrategyName(strategy);
EXPECT_EQ(addedIds.back(), dict->getVisualWords().rbegin()->first) << "Strategy: " << VWDictionary::nnStrategyName(strategy);
// Get the maximum initial word ID
int maxInitialId = dict->getVisualWords().rbegin()->first;
// Create query descriptors with known distances
// Query 1: (1, 0) - very close to Word 1 (0,0), far from others
// Distance to Word 1: sqrt(1^2 + 0^2) ≈ 1 (LSH 1)
// Distance to Word 2: sqrt(6^2 + 0^2) ≈ 6 (LSH 4)
// Ratio: 1 / 6 ≈ 0.16 < NNDR threshold (typically 0.4) - should PASS NNDR
// LSH Ratio: 1/4 = 0.25 < NNDR
//
// Query 2: (9, 0) - "equidistant" from Word 1 and Word 2
// Distance to Word 1: 9.0 (LSH 1)
// Distance to Word 2: 6.0 (LSH 2)
// Ratio: 36 / 81 = 0.44 > NNDR threshold - should FAIL NNDR (new word created)
// LSH Ratio: 1/2 = 0.5 > NNDR
cv::Mat queryDescriptors = (cv::Mat_<float>(2, 8) <<
1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, // Should match Word 1 (passes NNDR)
9.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); // Should create new word (fails NNDR)
// Convert to binary if using LSH strategy
if(strategy == VWDictionary::kNNFlannLSH)
{
// Convert float descriptors to binary
queryDescriptors = VWDictionary::convert32FToBin(queryDescriptors, true);
std::cout << queryDescriptors << std::endl;
}
int signatureId = 2;
std::list<int> wordIds = dict->addNewWords(queryDescriptors, signatureId);
// In incremental mode, valid word IDs are returned only if NNDR validation passes
// Otherwise, new words are created
EXPECT_EQ(wordIds.size(), 2u) << "Strategy: " << VWDictionary::nnStrategyName(strategy);
// First query should match existing word (NNDR passed)
int firstId = *wordIds.begin();
EXPECT_EQ(firstId, VWDictionary::ID_START) << "Strategy: " << VWDictionary::nnStrategyName(strategy);
EXPECT_NE(dict->getWord(firstId), nullptr) << "Strategy: " << VWDictionary::nnStrategyName(strategy);
// Second query should create a new word (NNDR failed)
int secondId = *std::next(wordIds.begin());
EXPECT_GT(secondId, maxInitialId) << "Strategy: " << VWDictionary::nnStrategyName(strategy); // Should be a new word ID
EXPECT_NE(dict->getWord(secondId), nullptr) << "Strategy: " << VWDictionary::nnStrategyName(strategy);
// Total words should increase by 1 (one new word created)
EXPECT_EQ(dict->getVisualWords().size(), initialWordCount + 1) << "Strategy: " << VWDictionary::nnStrategyName(strategy);
}
}
TEST_F(VWDictionaryTest, AddNewWords_Fixed)
{
// Test fixed mode - NNDR is not applied, closest match is always returned
// Create a temporary dictionary file
std::string dictFile = "test_vwdictionary_fixed_dict.txt";
// Write dictionary file in expected format:
// First line: dimension
// Subsequent lines: word_id descriptor_value1 descriptor_value2 ...
std::ofstream file(dictFile);
ASSERT_TRUE(file.is_open());
// Write header with dimension
file << "2" << std::endl;
// Write words: Word 1: (0, 0), Word 2: (10, 5), Word 3: (0, 100)
file << "1 0.0 0.0" << std::endl;
file << "2 10 0.0" << std::endl;
file << "3 0.0 100.0" << std::endl;
file.close();
// Load fixed dictionary from file
dict->setFixedDictionary(dictFile);
EXPECT_FALSE(dict->isIncremental());
dict->update();
unsigned int initialWordCount = dict->getVisualWords().size();
EXPECT_EQ(initialWordCount, 3u);
// Create query descriptors with known distances
// Query 1: (0.5, 0.5) - closest to Word 1 (0,0), distance ≈ 0.707
// Query 2: (5.1, 0) - slighlty closer to Word 2 (10,5) than Word 1 (0,0), with distances 4.9 and 5.0 respectively
cv::Mat queryDescriptors = (cv::Mat_<float>(2, 2) <<
0.5f, 0.5f, // Closest to Word 1
5.1f, 0.0f); // Closest to Word2 but would not pass NNDR (4.9/5 = 0.98 > 0.8 default NNDR)
int signatureId = 2;
std::list<int> wordIds = dict->addNewWords(queryDescriptors, signatureId);
// In fixed mode, closest visual word ID is always returned (no NNDR check)
EXPECT_EQ(wordIds.size(), 2u);
// First query should match Word 1 (closest match: distance 0.707 to Word 1 vs 9.513 to Word 2)
int firstId = *wordIds.begin();
EXPECT_EQ(firstId, VWDictionary::ID_START);
EXPECT_NE(dict->getWord(firstId), nullptr);
// Second query should also match an existing word (closest match, no NNDR check)
// Query (5.1, 0) is slighlty closer to Word 2
int secondId = *std::next(wordIds.begin());
EXPECT_EQ(secondId, 2);
EXPECT_NE(dict->getWord(secondId), nullptr);
// In fixed mode, no new words should be created
EXPECT_EQ(dict->getVisualWords().size(), initialWordCount);
// Cleanup: remove temporary dictionary file
UFile::erase(dictFile);
}
TEST_F(VWDictionaryTest, AddWord)
{
cv::Mat descriptor = (cv::Mat_<float>(1, 64) <<
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0);
VisualWord* word = new VisualWord(100, descriptor, 10);
dict->addWord(word);
EXPECT_EQ(dict->getVisualWords().size(), 1u);
const VisualWord* retrieved = dict->getWord(100);
EXPECT_NE(retrieved, nullptr);
EXPECT_EQ(retrieved->id(), 100);
}
TEST_F(VWDictionaryTest, FindNN_Incremental)
{
// Test incremental mode - NNDR is applied
EXPECT_TRUE(dict->isIncremental());
// Add initial words to dictionary (2D descriptors)
// Word 1: (0, 0)
// Word 2: (10, 0)
// Word 3: (0, 100)
cv::Mat initialDescriptors = (cv::Mat_<float>(3, 2) <<
0.0f, 0.0f,
10.0f, 0.0f,
0.0f, 100.0f);
dict->addNewWords(initialDescriptors, 1);
dict->update();
// Create query descriptors with known distances
// Query 1: (0.5, 0.5) - very close to Word 1 (0,0), far from others
// Distance to Word 1: sqrt(0.5^2 + 0.5^2) ≈ 0.707
// Distance to Word 2: sqrt(9.5^2 + 0.5^2) ≈ 9.513
// Ratio: 0.707 / 9.513 ≈ 0.074 < NNDR threshold (typically 0.8) - should PASS NNDR
//
// Query 2: (5, 0) - equidistant from Word 1 and Word 2
// Distance to Word 1: 5.0
// Distance to Word 2: 5.0
// Ratio: 5.0 / 5.0 = 1.0 > NNDR threshold - should FAIL NNDR (return ID_INVALID)
cv::Mat queryDescriptors = (cv::Mat_<float>(2, 2) <<
0.5f, 0.5f, // Should match Word 1 (passes NNDR)
5.0f, 0.0f); // Should fail NNDR (returns ID_INVALID)
std::vector<int> matches = dict->findNN(queryDescriptors);
EXPECT_EQ(matches.size(), 2u);
// First query should match existing word (NNDR passed)
int firstId = matches[0];
EXPECT_EQ(firstId, VWDictionary::ID_START);
EXPECT_NE(dict->getWord(firstId), nullptr);
// Second query should fail NNDR (returns ID_INVALID)
int secondId = matches[1];
EXPECT_EQ(secondId, VWDictionary::ID_INVALID);
}
TEST_F(VWDictionaryTest, FindNN_Fixed)
{
// Test fixed mode - NNDR is not applied, closest match is always returned
// Create a temporary dictionary file
std::string dictFile = "test_vwdictionary_fixed_dict_findnn.txt";
// Write dictionary file in expected format:
// First line: dimension
// Subsequent lines: word_id descriptor_value1 descriptor_value2 ...
std::ofstream file(dictFile);
ASSERT_TRUE(file.is_open());
// Write header with dimension
file << "2" << std::endl;
// Write words: Word 1: (0, 0), Word 2: (10, 0), Word 3: (0, 100)
file << "1 0.0 0.0" << std::endl;
file << "2 10 0.0" << std::endl;
file << "3 0.0 100.0" << std::endl;
file.close();
// Load fixed dictionary from file
dict->setFixedDictionary(dictFile);
EXPECT_FALSE(dict->isIncremental());
dict->update();
unsigned int initialWordCount = dict->getVisualWords().size();
EXPECT_EQ(initialWordCount, 3u);
// Create query descriptors with known distances
// Query 1: (0.5, 0.5) - closest to Word 1 (0,0), distance ≈ 0.707
// Query 2: (5.1, 0) - slightly closer to Word 2 (10,0) than Word 1 (0,0), with distances 4.9 and 5.0 respectively
cv::Mat queryDescriptors = (cv::Mat_<float>(2, 2) <<
0.5f, 0.5f, // Closest to Word 1
5.1f, 0.0f); // Closest to Word 2 but would not pass NNDR (4.9/5 = 0.98 > 0.8 default NNDR)
std::vector<int> matches = dict->findNN(queryDescriptors);
EXPECT_EQ(matches.size(), 2u);
// First query should match Word 1 (closest match: distance 0.707 to Word 1 vs 9.513 to Word 2)
int firstId = matches[0];
EXPECT_EQ(firstId, VWDictionary::ID_START);
EXPECT_NE(dict->getWord(firstId), nullptr);
// Second query should also match an existing word (closest match, no NNDR check)
// Query (5.1, 0) is slightly closer to Word 2
int secondId = matches[1];
EXPECT_EQ(secondId, 2);
EXPECT_NE(dict->getWord(secondId), nullptr);
// Cleanup: remove temporary dictionary file
UFile::erase(dictFile);
}
TEST_F(VWDictionaryTest, AddWordRef)
{
cv::Mat descriptor = cv::Mat::ones(1, 32, CV_32F);
std::list<int> wordIds = dict->addNewWords(descriptor, 1);
ASSERT_EQ(wordIds.size(), 1u);
int wordId = wordIds.front();
dict->addWordRef(wordId, 2);
dict->addWordRef(wordId, 3);
EXPECT_EQ(dict->getTotalActiveReferences(), 3); // 1 from addNewWords + 2 from addWordRef
const VisualWord* word = dict->getWord(wordId);
EXPECT_NE(word, nullptr);
EXPECT_EQ(word->getTotalReferences(), 3);
}
TEST_F(VWDictionaryTest, RemoveAllWordRef)
{
cv::Mat descriptor = cv::Mat::ones(1, 32, CV_32F);
std::list<int> wordIds = dict->addNewWords(descriptor, 1);
ASSERT_EQ(wordIds.size(), 1u);
int wordId = wordIds.front();
dict->addWordRef(wordId, 2);
dict->addWordRef(wordId, 3);
EXPECT_EQ(dict->getTotalActiveReferences(), 3);
dict->removeAllWordRef(wordId, 1);
EXPECT_EQ(dict->getTotalActiveReferences(), 2);
dict->removeAllWordRef(wordId, 2);
dict->removeAllWordRef(wordId, 3);
// Word should now be unused
EXPECT_EQ(dict->getTotalActiveReferences(), 0);
EXPECT_EQ(dict->getUnusedWordsSize(), 1u);
}
TEST_F(VWDictionaryTest, GetWord)
{
cv::Mat descriptor = cv::Mat::ones(1, 32, CV_32F);
std::list<int> wordIds = dict->addNewWords(descriptor, 1);
ASSERT_EQ(wordIds.size(), 1u);
int wordId = wordIds.front();
const VisualWord* word = dict->getWord(wordId);
EXPECT_NE(word, nullptr);
EXPECT_EQ(word->id(), wordId);
EXPECT_EQ(word->getDescriptor().cols, 32);
// Test with invalid ID
const VisualWord* invalid = dict->getWord(99999);
EXPECT_EQ(invalid, nullptr);
}
TEST_F(VWDictionaryTest, GetUnusedWords)
{
cv::Mat descriptor = cv::Mat::ones(1, 32, CV_32F);
std::list<int> wordIds = dict->addNewWords(descriptor, 1);
ASSERT_EQ(wordIds.size(), 1u);
int wordId = wordIds.front();
// Initially word has a reference, so it's not unused
EXPECT_EQ(dict->getUnusedWordsSize(), 0u);
// Remove all references
dict->removeAllWordRef(wordId, 1);
EXPECT_EQ(dict->getUnusedWordsSize(), 1u);
std::vector<VisualWord*> unused = dict->getUnusedWords();
EXPECT_EQ(unused.size(), 1u);
EXPECT_EQ(unused[0]->id(), wordId);
std::vector<int> unusedIds = dict->getUnusedWordIds();
EXPECT_EQ(unusedIds.size(), 1u);
EXPECT_EQ(unusedIds[0], wordId);
}
TEST_F(VWDictionaryTest, ConvertBinTo32F_ByteToFloat)
{
// Test byteToFloat = true (simple conversion)
cv::Mat input(2, 10, CV_8UC1);
cv::randu(input, cv::Scalar(0), cv::Scalar(255));
cv::Mat output = VWDictionary::convertBinTo32F(input, true);
EXPECT_EQ(output.type(), CV_32FC1);
EXPECT_EQ(output.rows, 2);
EXPECT_EQ(output.cols, 10); // Same dimensions
}
TEST_F(VWDictionaryTest, ConvertBinTo32F_BitExpansion)
{
// Test byteToFloat = false (bit expansion)
cv::Mat input(1, 4, CV_8UC1);
input.at<unsigned char>(0, 0) = 0b10101010; // 170
input.at<unsigned char>(0, 1) = 0b01010101; // 85
input.at<unsigned char>(0, 2) = 0b11110000; // 240
input.at<unsigned char>(0, 3) = 0b00001111; // 15
cv::Mat output = VWDictionary::convertBinTo32F(input, false);
EXPECT_EQ(output.type(), CV_32FC1);
EXPECT_EQ(output.rows, 1);
EXPECT_EQ(output.cols, 32); // 4 bytes * 8 bits = 32 floats
// Check first byte expansion (10101010)
EXPECT_FLOAT_EQ(output.at<float>(0, 0), 0.0f); // bit 0
EXPECT_FLOAT_EQ(output.at<float>(0, 1), 1.0f); // bit 1
EXPECT_FLOAT_EQ(output.at<float>(0, 2), 0.0f); // bit 2
EXPECT_FLOAT_EQ(output.at<float>(0, 3), 1.0f); // bit 3
}
TEST_F(VWDictionaryTest, Convert32FToBin_ByteToFloat)
{
// Test byteToFloat = true (simple conversion)
cv::Mat input(2, 10, CV_32FC1);
cv::randu(input, cv::Scalar(0), cv::Scalar(255));
cv::Mat output = VWDictionary::convert32FToBin(input, true);
EXPECT_EQ(output.type(), CV_8UC1);
EXPECT_EQ(output.rows, 2);
EXPECT_EQ(output.cols, 10); // Same dimensions
}
TEST_F(VWDictionaryTest, Convert32FToBin_BitPacking)
{
// Test byteToFloat = false (bit packing)
cv::Mat input(1, 32, CV_32FC1);
// Set first 8 floats to represent 10101010
input.at<float>(0, 0) = 0.0f; // bit 0
input.at<float>(0, 1) = 1.0f; // bit 1
input.at<float>(0, 2) = 0.0f; // bit 2
input.at<float>(0, 3) = 1.0f; // bit 3
input.at<float>(0, 4) = 0.0f; // bit 4
input.at<float>(0, 5) = 1.0f; // bit 5
input.at<float>(0, 6) = 0.0f; // bit 6
input.at<float>(0, 7) = 1.0f; // bit 7
// Rest set to 0
for(int i = 8; i < 32; ++i) {
input.at<float>(0, i) = 0.0f;
}
cv::Mat output = VWDictionary::convert32FToBin(input, false);
EXPECT_EQ(output.type(), CV_8UC1);
EXPECT_EQ(output.rows, 1);
EXPECT_EQ(output.cols, 4); // 32 floats / 8 = 4 bytes
EXPECT_EQ(output.at<unsigned char>(0, 0), 0b10101010);
}
TEST_F(VWDictionaryTest, ConvertRoundTrip)
{
// Test round trip conversion with bit expansion
cv::Mat original(1, 4, CV_8UC1);
cv::randu(original, cv::Scalar(0), cv::Scalar(255));
cv::Mat expanded = VWDictionary::convertBinTo32F(original, false);
cv::Mat packed = VWDictionary::convert32FToBin(expanded, false);
EXPECT_EQ(packed.rows, original.rows);
EXPECT_EQ(packed.cols, original.cols);
EXPECT_EQ(packed.type(), original.type());
for(int i = 0; i < original.cols; ++i) {
EXPECT_EQ(packed.at<unsigned char>(0, i), original.at<unsigned char>(0, i));
}
}
TEST_F(VWDictionaryTest, SetNNStrategy)
{
// Test resetting same strategy
bool reinit = dict->setNNStrategy(dict->getNNStrategy());
EXPECT_FALSE(reinit);
// Test changing strategy
reinit = dict->setNNStrategy(VWDictionary::kNNBruteForce);
EXPECT_TRUE(reinit);
// Add some words and index them
cv::Mat descriptors(5, 32, CV_32F);
cv::randu(descriptors, cv::Scalar(0), cv::Scalar(1));
dict->addNewWords(descriptors, 1);
dict->update();
// Change strategy should reinitialize
reinit = dict->setNNStrategy(VWDictionary::kNNFlannKdTree);
EXPECT_TRUE(reinit);
}
TEST_F(VWDictionaryTest, NNStrategyName)
{
EXPECT_EQ(VWDictionary::nnStrategyName(VWDictionary::kNNFlannNaive), "FLANN NAIVE");
EXPECT_EQ(VWDictionary::nnStrategyName(VWDictionary::kNNFlannKdTree), "FLANN KD-TREE");
EXPECT_EQ(VWDictionary::nnStrategyName(VWDictionary::kNNFlannLSH), "FLANN LSH");
EXPECT_EQ(VWDictionary::nnStrategyName(VWDictionary::kNNBruteForce), "BRUTE FORCE");
EXPECT_EQ(VWDictionary::nnStrategyName(VWDictionary::kNNBruteForceGPU), "BRUTE FORCE GPU");
EXPECT_EQ(VWDictionary::nnStrategyName(VWDictionary::kNNUndef), "Unknown");
}
TEST_F(VWDictionaryTest, IncrementalDictionary)
{
EXPECT_TRUE(dict->isIncremental());
dict->setIncrementalDictionary();
EXPECT_TRUE(dict->isIncremental());
}
TEST_F(VWDictionaryTest, GetNndrRatio)
{
float ratio = dict->getNndrRatio();
EXPECT_GT(ratio, 0.0f);
EXPECT_LE(ratio, 1.0f);
}
TEST_F(VWDictionaryTest, Update)
{
// Add words without updating index
cv::Mat descriptors(3, 32, CV_32F);
cv::randu(descriptors, cv::Scalar(0), cv::Scalar(1));
dict->addNewWords(descriptors, 1);
unsigned int notIndexed = dict->getNotIndexedWordsCount();
EXPECT_GT(notIndexed, 0u);
dict->update();
// After update, words should be indexed
EXPECT_GT(dict->getIndexedWordsCount(), 0u);
}
TEST_F(VWDictionaryTest, Clear)
{
// Add some words
cv::Mat descriptors(5, 32, CV_32F);
cv::randu(descriptors, cv::Scalar(0), cv::Scalar(1));
dict->addNewWords(descriptors, 1);
EXPECT_GT(dict->getVisualWords().size(), 0u);
dict->clear();
EXPECT_EQ(dict->getVisualWords().size(), 0u);
EXPECT_EQ(dict->getTotalActiveReferences(), 0);
EXPECT_EQ(dict->getIndexedWordsCount(), 0u);
}
TEST_F(VWDictionaryTest, MemoryUsed)
{
unsigned long memBefore = dict->getMemoryUsed();
// Add some words
cv::Mat descriptors(10, 128, CV_32F);
cv::randu(descriptors, cv::Scalar(0), cv::Scalar(1));
dict->addNewWords(descriptors, 1);
dict->update();
unsigned long memAfter = dict->getMemoryUsed();
EXPECT_GT(memAfter, memBefore);
unsigned int indexMem = dict->getIndexMemoryUsed();
EXPECT_GT(indexMem, 0u);
}
TEST_F(VWDictionaryTest, SerializeDeserializeIndex)
{
// Add words and build index
cv::Mat descriptors(5, 32, CV_32F);
cv::randu(descriptors, cv::Scalar(0), cv::Scalar(1));
dict->addNewWords(descriptors, 1);
dict->update();
// Serialize
std::vector<unsigned char> data = dict->serializeIndex();
EXPECT_GT(data.size(), 0u);
// Create new dictionary and deserialize
VWDictionary dict2;
cv::Mat descriptors2(5, 32, CV_32F);
cv::randu(descriptors2, cv::Scalar(0), cv::Scalar(1));
dict2.addNewWords(descriptors2, 1);
// Deserialize should fail because we are not using the same descriptors
bool success = dict2.deserializeIndex(data);
EXPECT_FALSE(success);
success = dict2.deserializeIndex(data.data(), data.size());
EXPECT_FALSE(success);
// Same descriptors
VWDictionary dict3;
dict3.addNewWords(descriptors, 1);
success = dict3.deserializeIndex(data);
EXPECT_TRUE(success);
// Index should be loaded
EXPECT_GT(dict3.getIndexedWordsCount(), 0u);
// Should fail if index is already built
success = dict3.deserializeIndex(data);
EXPECT_FALSE(success);
// raw bytes
VWDictionary dict4;
dict4.addNewWords(descriptors, 1);
success = dict4.deserializeIndex(data.data(), data.size());
EXPECT_TRUE(success);
// Index should be loaded
EXPECT_GT(dict4.getIndexedWordsCount(), 0u);
// Should fail if index is already built
success = dict4.deserializeIndex(data.data(), data.size());
EXPECT_FALSE(success);
}
TEST_F(VWDictionaryTest, IsModified)
{
EXPECT_TRUE(dict->isModified());
// Adding words should keep it modified
cv::Mat descriptors(3, 32, CV_32F);
cv::randu(descriptors, cv::Scalar(0), cv::Scalar(1));
dict->addNewWords(descriptors, 1);
EXPECT_TRUE(dict->isModified());
}
TEST_F(VWDictionaryTest, SetLastWordId)
{
dict->setLastWordId(100);
// Add a word - should get ID > 100
cv::Mat descriptor = cv::Mat::ones(1, 32, CV_32F);
std::list<int> wordIds = dict->addNewWords(descriptor, 1);
ASSERT_EQ(wordIds.size(), 1u);
EXPECT_GT(wordIds.front(), 100);
}
TEST_F(VWDictionaryTest, DeleteUnusedWords)
{
// Add words
cv::Mat descriptors(3, 32, CV_32F);
cv::randu(descriptors, cv::Scalar(0), cv::Scalar(1));
std::list<int> wordIds = dict->addNewWords(descriptors, 1);
ASSERT_EQ(wordIds.size(), 3u);
// Remove references to make words unused
for(int id : wordIds) {
dict->removeAllWordRef(id, 1);
}
EXPECT_EQ(dict->getUnusedWordsSize(), 3u);
// Delete unused words
dict->deleteUnusedWords();
EXPECT_EQ(dict->getUnusedWordsSize(), 0u);
EXPECT_EQ(dict->getVisualWords().size(), 0u);
}
TEST_F(VWDictionaryTest, FindNNWithVisualWords)
{
// Add words
cv::Mat descriptors(5, 32, CV_32F);
cv::randu(descriptors, cv::Scalar(0), cv::Scalar(1));
std::list<int> wordIds = dict->addNewWords(descriptors, 1);
dict->update();
// Create list of visual words to match (create new words with same descriptors)
std::list<VisualWord*> vws;
for(int id : wordIds) {
const VisualWord* word = dict->getWord(id);
// Create a new VisualWord with the same descriptor for testing
VisualWord* testWord = new VisualWord(id + 1000, word->getDescriptor().clone());
vws.push_back(testWord);
}
// That will force to accept close matches
ParametersMap params;
params.insert(ParametersPair(Parameters::kKpNndrRatio(), "1"));
dict->parseParameters(params);
std::vector<int> matches = dict->findNN(vws);
EXPECT_EQ(matches.size(), vws.size());
// Each word should match itself
int i =0;
for(VisualWord * vw: vws) {
EXPECT_EQ(vw->id()-1000, matches[i++]);
}
// Cleanup
for(VisualWord* vw : vws) {
delete vw;
}
}