use std::collections::{HashMap, HashSet};
use kornia_bow::BoW;
use kornia_bow::orb_slam3::{OrbVocabulary, pack_orb_descriptor};
pub use kornia_bow::BoW as BagOfWords;
pub use kornia_bow::orb_slam3::{OrbVocabulary as Vocabulary, load_orb_slam3_vocabulary};
pub fn compute_bow(vocab: &OrbVocabulary, descriptors: &[[u8; 32]]) -> BoW {
if descriptors.is_empty() {
return BoW(Vec::new());
}
let packed: Vec<_> = descriptors.iter().map(pack_orb_descriptor).collect();
vocab.transform(&packed).unwrap_or(BoW(Vec::new()))
}
#[derive(Debug, Default)]
pub struct KeyFrameDatabase {
inverted: HashMap<u32, Vec<usize>>,
bows: HashMap<usize, BoW>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Candidate {
pub kf_idx: usize,
pub shared_words: usize,
pub score: f32,
}
impl KeyFrameDatabase {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.bows.len()
}
pub fn is_empty(&self) -> bool {
self.bows.is_empty()
}
pub fn add(&mut self, kf_idx: usize, bow: BoW) {
if self.bows.contains_key(&kf_idx) {
self.erase(kf_idx);
}
for &(word, _) in &bow.0 {
self.inverted.entry(word).or_default().push(kf_idx);
}
self.bows.insert(kf_idx, bow);
}
pub fn erase(&mut self, kf_idx: usize) {
let Some(bow) = self.bows.remove(&kf_idx) else {
return;
};
for &(word, _) in &bow.0 {
if let Some(list) = self.inverted.get_mut(&word) {
list.retain(|&k| k != kf_idx);
if list.is_empty() {
self.inverted.remove(&word);
}
}
}
}
pub fn bow(&self, kf_idx: usize) -> Option<&BoW> {
self.bows.get(&kf_idx)
}
pub fn detect_candidates(
&self,
query: &BoW,
exclude: &HashSet<usize>,
min_score: f32,
) -> Vec<Candidate> {
let mut shared: HashMap<usize, usize> = HashMap::new();
for &(word, _) in &query.0 {
if let Some(list) = self.inverted.get(&word) {
for &kf in list {
if exclude.contains(&kf) {
continue;
}
*shared.entry(kf).or_insert(0) += 1;
}
}
}
if shared.is_empty() {
return Vec::new();
}
let max_shared = shared.values().copied().max().unwrap_or(0);
let min_shared = (0.8 * max_shared as f32) as usize;
let mut candidates: Vec<Candidate> = shared
.into_iter()
.filter(|&(_, n)| n > min_shared)
.filter_map(|(kf, n)| {
let bow = self.bows.get(&kf)?;
let score = query.l1_similarity(bow);
(score >= min_score).then_some(Candidate {
kf_idx: kf,
shared_words: n,
score,
})
})
.collect();
candidates.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
candidates
}
pub fn detect_loop_candidates(
&self,
query_kf_idx: usize,
query: &BoW,
covisible: impl IntoIterator<Item = usize>,
) -> Vec<Candidate> {
let mut exclude = HashSet::new();
exclude.insert(query_kf_idx);
let mut min_score = 1.0f32;
for nb in covisible {
exclude.insert(nb);
if let Some(nb_bow) = self.bow(nb) {
min_score = min_score.min(query.l1_similarity(nb_bow));
}
}
self.detect_candidates(query, &exclude, min_score)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bow(entries: &[(u32, f32)]) -> BoW {
BoW(entries.to_vec())
}
#[test]
fn add_erase_roundtrip() {
let mut db = KeyFrameDatabase::new();
db.add(1, bow(&[(10, 0.5), (20, 0.5)]));
assert_eq!(db.len(), 1);
db.erase(1);
assert!(db.is_empty());
assert!(db.inverted.is_empty());
}
#[test]
fn detect_prefers_more_similar_keyframe() {
let mut db = KeyFrameDatabase::new();
db.add(1, bow(&[(10, 0.5), (20, 0.5)]));
db.add(2, bow(&[(10, 0.5), (99, 0.5)]));
let query = bow(&[(10, 0.5), (20, 0.5)]);
let cands = db.detect_candidates(&query, &HashSet::new(), 0.0);
assert_eq!(cands.first().map(|c| c.kf_idx), Some(1));
assert!(cands[0].score >= cands.last().unwrap().score);
}
#[test]
fn detect_excludes_listed_keyframes() {
let mut db = KeyFrameDatabase::new();
db.add(1, bow(&[(10, 1.0)]));
let query = bow(&[(10, 1.0)]);
let exclude: HashSet<usize> = [1].into_iter().collect();
assert!(db.detect_candidates(&query, &exclude, 0.0).is_empty());
}
#[test]
fn detect_loop_excludes_query_and_neighbours() {
let mut db = KeyFrameDatabase::new();
db.add(1, bow(&[(10, 0.5), (20, 0.5)])); db.add(2, bow(&[(10, 0.5), (20, 0.5)]));
let query = bow(&[(10, 0.5), (20, 0.5)]);
let cands = db.detect_loop_candidates(3, &query, [1]);
assert_eq!(cands.len(), 1);
assert_eq!(cands[0].kf_idx, 2);
}
#[test]
fn detect_applies_min_score() {
let mut db = KeyFrameDatabase::new();
db.add(1, bow(&[(10, 1.0)]));
let query = bow(&[(20, 1.0)]); assert!(
db.detect_candidates(&query, &HashSet::new(), 0.5)
.is_empty()
);
}
}