use crate::kernels::features::{feature_words, DESCRIPTOR_WORDS};
use std::collections::{HashMap, HashSet};
pub const MIN_FEATURES: usize = 8;
pub const QUERY_WORDS: usize = 48;
pub const POSTING_SPREAD: usize = 48;
pub const RETRIEVAL_BUDGET: usize = 96;
pub const MAX_CANDIDATES: usize = 12;
pub const MIN_VOTES: u32 = 3;
pub const DEFAULT_MIN_GAP: u32 = 3;
pub const BUDGET_DIAGNOSTIC: &str = "RELOCALIZATION_BUDGET";
#[derive(Default)]
struct Posting {
frames: Vec<u32>,
ids: Vec<u32>,
}
fn words(descriptors: &[[u32; DESCRIPTOR_WORDS]]) -> Vec<u32> {
let even: Vec<[u32; DESCRIPTOR_WORDS]> = descriptors.iter().step_by(2).copied().collect();
feature_words(&even)
}
#[derive(Clone, Debug, PartialEq)]
pub struct Retrieval {
pub candidates: Vec<String>,
pub budget_warning: bool,
frame: u32,
min_gap: u32,
}
impl Retrieval {
pub fn admits(&self, keyframe_frame: u32) -> bool {
keyframe_frame.abs_diff(self.frame) >= self.min_gap
}
}
#[derive(Default)]
pub struct KeyframeIndex {
ids: Vec<String>,
interned: HashMap<String, u32>,
postings: HashMap<String, HashMap<u32, Posting>>,
warned_layers: HashSet<String>,
}
impl KeyframeIndex {
pub fn new() -> Self {
Self::default()
}
fn intern(&mut self, id: &str) -> u32 {
if let Some(&i) = self.interned.get(id) {
return i;
}
let i = self.ids.len() as u32;
self.ids.push(id.to_string());
self.interned.insert(id.to_string(), i);
i
}
pub fn add(
&mut self,
id: &str,
layer: &str,
frame: u32,
descriptors: &[[u32; DESCRIPTOR_WORDS]],
) {
let interned = self.intern(id);
let postings = self.postings.entry(layer.to_string()).or_default();
for word in words(descriptors) {
let Posting { frames, ids } = postings.entry(word).or_default();
let mut at = frames.len();
while at > 0 && frames[at - 1] > frame {
at -= 1;
}
if at > 0 && frames[at - 1] == frame {
ids[at - 1] = interned;
} else {
frames.insert(at, frame);
ids.insert(at, interned);
}
}
}
pub fn retrieve(
&mut self,
layer: &str,
frame: u32,
descriptors: &[[u32; DESCRIPTOR_WORDS]],
exclude: Option<&str>,
min_gap: Option<u32>,
) -> Retrieval {
let mut retrieval = Retrieval {
candidates: Vec::new(),
budget_warning: false,
frame,
min_gap: min_gap.unwrap_or(DEFAULT_MIN_GAP),
};
if descriptors.len() < MIN_FEATURES {
return retrieval;
}
let all = words(descriptors);
let step = (all.len() / QUERY_WORDS).max(1);
let sampled = all.iter().step_by(step).take(QUERY_WORDS);
let exclude = exclude.and_then(|id| self.interned.get(id).copied());
let mut votes: Vec<(u32, u32)> = Vec::new();
let mut slot: HashMap<u32, usize> = HashMap::new();
let mut vote = |id: u32| match slot.get(&id) {
Some(&i) => votes[i].1 += 1,
None => {
slot.insert(id, votes.len());
votes.push((id, 1));
}
};
let mut truncated = false;
let postings = self.postings.get(layer);
for word in sampled {
let ids = postings
.and_then(|p| p.get(word))
.map_or(&[][..], |p| p.ids.as_slice());
if ids.len() > RETRIEVAL_BUDGET {
truncated = true;
}
let mut seen = HashSet::new();
for &id in ids.iter().take(POSTING_SPREAD) {
if Some(id) == exclude {
continue;
}
seen.insert(id);
vote(id);
}
if ids.len() > POSTING_SPREAD {
for &id in ids[ids.len() - POSTING_SPREAD..].iter().rev() {
if Some(id) == exclude || !seen.insert(id) {
continue;
}
vote(id);
}
}
}
if truncated && self.warned_layers.insert(layer.to_string()) {
retrieval.budget_warning = true;
}
votes.sort_by(|a, b| b.1.cmp(&a.1));
retrieval.candidates = votes
.into_iter()
.take(MAX_CANDIDATES)
.filter(|&(_, count)| count >= MIN_VOTES)
.map(|(id, _)| self.ids[id as usize].clone())
.collect();
retrieval
}
}
#[cfg(test)]
mod tests {
use super::*;
fn descriptor(w: u32) -> [u32; DESCRIPTOR_WORDS] {
let mut d = [0u32; DESCRIPTOR_WORDS];
for k in 0..4 {
d[k * 2] = (w + 1000 * k as u32) & 4095;
}
d
}
fn features(words: &[u32]) -> Vec<[u32; DESCRIPTOR_WORDS]> {
words
.iter()
.flat_map(|&w| [descriptor(w), descriptor(4000)])
.collect()
}
#[test]
fn a_query_with_too_few_features_finds_nothing_and_never_warns() {
let mut index = KeyframeIndex::new();
index.add("L/1", "L", 1, &features(&[1, 2, 3, 4]));
let r = index.retrieve("L", 50, &features(&[1, 2, 3, 4])[..7], None, None);
assert_eq!(r.candidates, Vec::<String>::new());
assert!(!r.budget_warning);
}
#[test]
fn a_keyframe_needs_three_votes_and_only_its_own_layer_counts() {
let mut index = KeyframeIndex::new();
index.add("L/1", "L", 1, &features(&[1, 2, 3, 4]));
index.add("L/2", "L", 2, &features(&[1, 2, 90, 91]));
index.add("M/3", "M", 3, &features(&[1, 2, 3, 4]));
let r = index.retrieve("L", 50, &features(&[1, 2, 3, 4]), None, None);
assert_eq!(r.candidates, ["L/1", "L/2"]);
let none = index.retrieve("L", 50, &features(&[200, 201, 202, 203]), None, None);
assert!(none.candidates.is_empty());
}
#[test]
fn exclude_gets_no_vote() {
let mut index = KeyframeIndex::new();
index.add("L/1", "L", 1, &features(&[1, 2, 3, 4]));
index.add("L/2", "L", 2, &features(&[1, 2, 3, 4]));
let r = index.retrieve("L", 50, &features(&[1, 2, 3, 4]), Some("L/1"), None);
assert_eq!(r.candidates, ["L/2"]);
}
#[test]
fn ties_keep_first_vote_order_and_at_most_twelve_come_back() {
let mut index = KeyframeIndex::new();
for f in (0..20u32).rev() {
index.add(&format!("L/{f:02}"), "L", f, &features(&[1, 2, 3, 4]));
}
let r = index.retrieve("L", 500, &features(&[1, 2, 3, 4]), None, None);
let expected: Vec<String> = (0..12).map(|f| format!("L/{f:02}")).collect();
assert_eq!(r.candidates, expected);
}
#[test]
fn postings_stay_in_frame_order_whatever_order_keyframes_arrive_in() {
let mut index = KeyframeIndex::new();
for frame in [30u32, 10, 50, 20] {
index.add(
&format!("body/{frame}"),
"body",
frame,
&features(&[1, 2, 3, 4]),
);
}
let postings = &index.postings["body"];
assert!(!postings.is_empty());
for Posting { frames, ids } in postings.values() {
assert_eq!(frames, &[10, 20, 30, 50]);
let named: Vec<&str> = ids
.iter()
.map(|&i| index.ids[i as usize].as_str())
.collect();
assert_eq!(named, ["body/10", "body/20", "body/30", "body/50"]);
}
}
#[test]
fn a_later_keyframe_at_the_same_frame_replaces_the_earlier_one() {
let mut index = KeyframeIndex::new();
index.add("L/a", "L", 7, &features(&[1, 2, 3, 4]));
index.add("L/b", "L", 7, &features(&[1, 2, 3, 4]));
let r = index.retrieve("L", 50, &features(&[1, 2, 3, 4]), None, None);
assert_eq!(r.candidates, ["L/b"]);
}
#[test]
fn a_long_posting_votes_for_its_latest_keyframes_too_and_warns_once_per_layer() {
let mut index = KeyframeIndex::new();
for f in 0..120u32 {
index.add(&format!("L/{f:03}"), "L", f, &features(&[1, 2, 3, 4]));
index.add(&format!("M/{f:03}"), "M", f, &features(&[1, 2, 3, 4]));
}
let first = index.retrieve("L", 500, &features(&[1, 2, 3, 4]), None, None);
assert!(
first.budget_warning,
"a 120-long posting overran the budget of 96"
);
assert_eq!(first.candidates.len(), 12);
assert_eq!(first.candidates[0], "L/000");
let again = index.retrieve("L", 500, &features(&[1, 2, 3, 4]), None, None);
assert!(!again.budget_warning, "warned once per layer");
assert!(
index
.retrieve("M", 500, &features(&[1, 2, 3, 4]), None, None)
.budget_warning
);
let latest = index.retrieve("L", 500, &features(&[1, 2, 3, 4]), Some("L/000"), None);
assert!(!latest.candidates.contains(&"L/000".to_string()));
}
#[test]
fn the_latest_keyframes_of_a_long_posting_are_found() {
let mut index = KeyframeIndex::new();
for f in 0..60u32 {
index.add(&format!("L/{f:03}"), "L", f, &features(&[1, 2, 3, 4]));
}
index.add("L/999", "L", 999, &features(&[1, 2, 3, 4, 5, 6]));
let r = index.retrieve("L", 2000, &features(&[1, 2, 3, 4, 5, 6]), None, None);
assert_eq!(r.candidates[0], "L/999");
assert!(!r.budget_warning);
}
#[test]
fn admits_keeps_odometry_neighbours_out() {
let mut index = KeyframeIndex::new();
let r = index.retrieve("L", 10, &[], None, None);
assert!(!r.admits(8) && !r.admits(12) && r.admits(7) && r.admits(13));
let wide = index.retrieve("L", 10, &[], None, Some(6));
assert!(!wide.admits(5) && wide.admits(4) && wide.admits(16));
}
}