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 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
{
public:
/**
* @enum NNStrategy
* @brief Nearest neighbor search strategies for descriptor matching
*/
enum NNStrategy{
kNNFlannNaive,
kNNFlannKdTree,
kNNFlannLSH,
kNNBruteForce,
kNNBruteForceGPU,
kNNUndef};
kNNFlannNaive, ///< FLANN naive search (exhaustive)
kNNFlannKdTree, ///< FLANN kd-tree index (fast for high-dimensional descriptors)
kNNFlannLSH, ///< FLANN Locality-Sensitive Hashing (ideal for binary descriptors)
kNNBruteForce, ///< Brute force CPU search
kNNBruteForceGPU, ///< Brute force GPU-accelerated search (requires CUDA)
kNNUndef ///< Undefined strategy
};
/**
* @brief Starting ID for visual words (typically 1)
*/
static const int ID_START;
/**
* @brief Invalid visual word ID (typically 0)
*/
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)
{
switch(strategy) {
@@ -74,85 +108,457 @@ public:
}
public:
/**
* @brief Constructor
* @param parameters Optional parameters map to configure the dictionary
*/
VWDictionary(const ParametersMap & parameters = ParametersMap());
/**
* @brief Destructor
*
* Clears all visual words and releases resources.
*/
virtual ~VWDictionary();
/**
* @brief Parse and apply parameters from a parameters map
* @param parameters Parameters map containing configuration values
*/
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();
/**
* @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(
const cv::Mat & descriptors,
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);
/**
* @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;
/**
* @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;
/**
* @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);
/**
* @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);
/**
* @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;
/**
* @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;
/**
* @brief Set the last word ID (used when loading from database)
* @param id Last word 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;}
/**
* @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;}
/**
* @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();}
/**
* @brief Get the ID of the last indexed word
* @return Last indexed word ID, or 0 if no words are indexed
*/
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;}
/**
* @brief Get the count of words currently indexed in the search tree
* @return Number of indexed words
*/
unsigned int getIndexedWordsCount() const;
/**
* @brief Get the memory used by the search index
* @return Memory usage in kilobytes
*/
unsigned int getIndexMemoryUsed() const; // KB
/**
* @brief Get the total memory used by the dictionary
* @return Memory usage in 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;}
/**
* @brief Check if FLANN index is updated incrementally
* @return true if incremental FLANN updates are enabled
*/
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();
/**
* @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);
/**
* @brief Check if the dictionary has been modified since last save
* @return true if modified, false otherwise
*/
bool isModified() const;
/**
* @brief Serialize the search index to a byte vector
* @return Serialized index data
*/
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;
/**
* @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);
/**
* @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;
/**
* @brief Get IDs of all unused visual words
* @return Vector of unused word IDs
*/
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();}
/**
* @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
/**
* @brief Delete all unused visual words
*
* Removes and deletes visual words that have no references to any signatures.
*/
void deleteUnusedWords();
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);
/**
* @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);
protected:
/**
* @brief Get the next available visual word ID
* @return Next unique word ID
*/
int getNextId();
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*>
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:
/**
* @brief Whether the dictionary is in incremental mode
*/
bool _incrementalDictionary;
/**
* @brief Whether FLANN index is updated incrementally
*/
bool _incrementalFlann;
/**
* @brief Rebalancing factor for FLANN index updates
*/
float _rebalancingFactor;
/**
* @brief Whether to convert descriptors from byte to float format
*/
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;
/**
* @brief Path to the pre-computed dictionary file (.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)
/**
* @brief Whether new words should be compared together before adding
*/
bool _newWordsComparedTogether;
/**
* @brief Whether to include checksum when serializing index
*/
bool _serializeWithChecksum;
/**
* @brief ID of the last visual word added
*/
int _lastWordId;
/**
* @brief Whether to use L1 distance metric instead of L2
*/
bool useDistanceL1_;
/**
* @brief FLANN index for fast nearest neighbor search
*/
FlannIndex * _flannIndex;
/**
* @brief Data matrix for the search tree
*/
cv::Mat _dataTree;
/**
* @brief Whether the dictionary has been modified since last save
*/
bool _modified;
/**
* @brief Current nearest neighbor search strategy
*/
NNStrategy _strategy;
/**
* @brief Map from search index position to visual word ID
*/
std::map<int ,int> _mapIndexId;
/**
* @brief Map from visual word ID to search index position
*/
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