mirror of
https://github.com/introlab/rtabmap.git
synced 2026-10-04 09:07:47 +08:00
Added VWDictionary tests and doc. Fixed LSH not working (fix from https://github.com/flann-lib/flann/pull/472
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user