mod config;
mod inertial;
mod state;
pub use config::SlamConfig;
pub use state::{TrackingResult, TrackingStatus};
use inertial::{AppliedInitialization, InertialState, viba0_accel_bias_prior};
use state::{SystemMode, SystemState};
use crate::tracking::{KeyframePolicy, TrackingLossRecoveryPolicy};
use crate::tracking::local_map::{LocalMapSelectionConfig, select_local_map_points};
use crate::tracking::motion::{InertialPrediction, predict_pose};
use crate::tracking::tracker::{FrameInput, Tracker};
use std::sync::{Arc, Mutex};
use crate::Frame;
use crate::initialization::bootstrap::{
self, BootstrapDecision, MIN_KEYPOINTS_FOR_BOOTSTRAP, MIN_STEREO_POINTS, MIN_VALID_POINTS,
TooFewStereoPoints, evaluate_bootstrap,
};
use crate::initialization::two_view::TwoViewInitConfig;
use crate::loop_closure::place_recognition::Vocabulary;
use crate::loop_closure::{LoopCloser, LoopClosingContext, LoopClosureEvent};
use crate::mapping::keyframe_mapping::{self, KeyframeGrowthResult, KeyframeInsertion};
use crate::mapping::map::{Keyframe, Map, MapInsertion, MapMutationError, MapPoint};
use crate::mapping::{KeyframeJob, LocalMapping};
use crate::pose_conversion::apply_reference_pose_correction;
use crate::sensor_rig::{ImuCalibration, SensorRig};
use kornia_3d::camera::PinholeCamera;
use kornia_3d::pose::Pose3d;
use kornia_image::Image;
use kornia_sensors::imu::ImuMeasurement;
pub struct SlamSystem {
rig: SensorRig,
tracker: Tracker,
two_view_init_config: TwoViewInitConfig,
keyframe_policy: KeyframePolicy,
tracking_loss_recovery: TrackingLossRecoveryPolicy,
stereo_close_depth: Option<f64>,
debug: bool,
debug_messages: Vec<String>,
map: Arc<Mutex<Map>>,
map_publication_gate: Option<Arc<Mutex<()>>>,
inertial: InertialState,
local_mapping: LocalMapping,
loop_closer: LoopCloser,
loop_closure_events: Vec<LoopClosureEvent>,
state: SystemState,
}
impl SlamSystem {
pub fn new(camera: PinholeCamera, config: SlamConfig) -> Self {
Self::with_rig(SensorRig::new(camera), config)
}
pub fn with_rig(rig: SensorRig, config: SlamConfig) -> Self {
let camera = rig.camera.clone();
let map = Arc::new(Mutex::new(Map::new()));
let local_mapping =
LocalMapping::new(config.local_mapping, Arc::clone(&map), camera.clone());
let map_publication_gate = local_mapping.publication_gate();
let loop_closer = LoopCloser::new(config.pgo);
Self {
rig,
tracker: Tracker::new(config.map_projection),
two_view_init_config: config.two_view_init,
keyframe_policy: config.keyframe_policy,
tracking_loss_recovery: config.tracking_loss_recovery,
stereo_close_depth: config.stereo_close_depth_m,
debug: config.debug,
debug_messages: Vec::new(),
map,
map_publication_gate,
local_mapping,
state: SystemState::new(),
inertial: InertialState::new(),
loop_closer,
loop_closure_events: Vec::new(),
}
}
pub fn set_vocabulary(&mut self, vocabulary: Vocabulary) {
self.loop_closer.set_vocabulary(vocabulary);
}
pub fn drain_loop_closure_events(&mut self) -> Vec<LoopClosureEvent> {
std::mem::take(&mut self.loop_closure_events)
}
pub fn set_imu_extrinsics(&mut self, t_bc: Pose3d) {
self.rig.imu = Some(ImuCalibration::new(t_bc));
}
pub fn rig(&self) -> &SensorRig {
&self.rig
}
pub fn process_frame(
&mut self,
mut frame: Frame,
previous_image: Option<&Image<u8, 1>>,
current_image: &Image<u8, 1>,
timestamp_sec: f64,
imu_samples: Vec<ImuMeasurement>,
) -> TrackingResult {
let publication_gate = self.map_publication_gate.clone();
let _publication = publication_gate
.as_ref()
.map(|gate| gate.lock().unwrap_or_else(|poisoned| poisoned.into_inner()));
self.apply_local_mapping_results();
frame.ensure_undistorted(&self.rig.camera);
self.inertial.buffer_samples(imu_samples);
match self.state.mode {
SystemMode::Bootstrap => self.bootstrap_step(frame, timestamp_sec),
SystemMode::ImuInit => {
self.inertial_init_step(frame, previous_image, current_image, timestamp_sec)
}
SystemMode::Tracking => {
self.tracking_step(frame, previous_image, current_image, timestamp_sec)
}
}
}
pub fn with_map_points<R>(&self, f: impl FnOnce(&[MapPoint]) -> R) -> R {
f(self.map.lock().unwrap().map_points())
}
pub fn current_keyframe_idx(&self) -> Option<usize> {
self.state.current_keyframe_idx.and_then(|ki| {
self.map
.lock()
.unwrap()
.get_keyframe(ki)
.map(|kf| kf.frame.idx)
})
}
pub fn num_active_map_points(&self) -> usize {
self.map.lock().unwrap().num_active_map_points()
}
pub fn drain_debug_messages(&mut self) -> Vec<String> {
std::mem::take(&mut self.debug_messages)
}
pub fn set_debug(&mut self, on: bool) {
self.debug = on;
if !on {
self.debug_messages.clear();
}
}
fn apply_local_mapping_results(&mut self) {
let Some(reference_idx) = self.state.current_keyframe_idx else {
let _ = self.local_mapping.drain_results();
return;
};
for result in self.local_mapping.drain_results() {
let Some(correction) = result
.keyframe_corrections
.iter()
.find(|correction| correction.kf_idx == reference_idx)
else {
continue;
};
self.state.pose_world_to_cam = apply_reference_pose_correction(
self.state.pose_world_to_cam,
correction.pose_before,
correction.pose_after,
);
self.state.velocity_world = correction.velocity_world;
self.inertial.bias = correction.imu_bias;
}
}
fn dbg(&mut self, msg: String) {
if self.debug {
self.debug_messages.push(msg);
}
}
fn bootstrap_step(&mut self, curr_frame: Frame, timestamp_sec: f64) -> TrackingResult {
if curr_frame.is_stereo() {
return self.bootstrap_stereo(curr_frame, timestamp_sec);
}
self.bootstrap_mono(curr_frame, timestamp_sec)
}
fn bootstrap_stereo(&mut self, mut curr_frame: Frame, timestamp_sec: f64) -> TrackingResult {
curr_frame.pose_world_to_cam = self.state.pose_world_to_cam;
let curr_idx = curr_frame.idx;
let insertion =
match bootstrap::stereo_initial_map(Keyframe::from_frame(curr_frame), &self.rig.camera)
{
Ok(insertion) => insertion,
Err(TooFewStereoPoints { found }) => {
self.dbg(format!(
"[bootstrap_stereo] frame={curr_idx} skip: only {found} stereo points \
(need >= {MIN_STEREO_POINTS})"
));
return self.frame_result(TrackingStatus::Skipped);
}
};
let published = self.map.lock().unwrap().apply_insertion(insertion);
let added = match published {
Ok(result) => result.landmark_ids.len(),
Err(error) => {
self.dbg(format!(
"[bootstrap_stereo] frame={curr_idx} publication rejected: {error}"
));
return self.frame_result(TrackingStatus::Skipped);
}
};
self.dbg(format!(
"[bootstrap_stereo] frame={curr_idx} metric map created with {added} points",
));
self.state.velocity = None;
self.finish_bootstrap(curr_idx, timestamp_sec);
self.prune_imu_before(timestamp_sec);
self.frame_result(TrackingStatus::KeyframeAccepted)
}
fn bootstrap_mono(&mut self, mut curr_frame: Frame, timestamp_sec: f64) -> TrackingResult {
curr_frame.pose_world_to_cam = self.state.pose_world_to_cam;
let decision = evaluate_bootstrap(
self.state.bootstrap_frame.as_ref(),
&curr_frame,
&self.rig.camera,
&self.two_view_init_config,
);
let two_view_estimate = match decision {
BootstrapDecision::Unusable { keypoints } => {
self.dbg(format!(
"[bootstrap] frame={} skip: too few keypoints ({}, need > {})",
curr_frame.idx, keypoints, MIN_KEYPOINTS_FOR_BOOTSTRAP,
));
self.state.bootstrap_frame = None;
return self.frame_result(TrackingStatus::Skipped);
}
BootstrapDecision::StoreAsReference => {
self.dbg(format!(
"[bootstrap] frame={} stored as reference (awaiting second frame)",
curr_frame.idx,
));
self.state.bootstrap_frame = Some(curr_frame);
self.inertial.bootstrap_timestamp_sec = Some(timestamp_sec);
self.prune_imu_before(timestamp_sec);
return self.frame_result(TrackingStatus::Skipped);
}
BootstrapDecision::Rejected {
reference_idx,
reason,
} => {
self.dbg(format!(
"[bootstrap] frame={} (ref={}) reject: {:?}",
curr_frame.idx, reference_idx, reason,
));
return self.frame_result(TrackingStatus::Skipped);
}
BootstrapDecision::Initialized(estimate) => *estimate,
};
let prev_bootstrap_frame = self
.state
.bootstrap_frame
.take()
.expect("an accepted bootstrap consumes the reference it matched against");
self.dbg(format!(
"[bootstrap] frame={} accept: model={} triangulated={} inliers={}",
curr_frame.idx,
two_view_estimate.model_kind,
two_view_estimate.points3d.len(),
two_view_estimate.estimate.inliers,
));
let estimated_pose = two_view_estimate.estimate.pose;
let prev_pose_world_to_cam = curr_frame.pose_world_to_cam;
self.state.pose_world_to_cam = estimated_pose;
curr_frame.pose_world_to_cam = estimated_pose;
let prev_idx = prev_bootstrap_frame.idx;
let reference_kf = Keyframe::from_frame(prev_bootstrap_frame);
let current_kf = Keyframe::from_frame(curr_frame);
let curr_idx = current_kf.frame.idx;
let insertion =
bootstrap::two_view_initial_map(reference_kf, current_kf, &two_view_estimate);
if let Err(error) = self.publish_initial_map(insertion, [prev_idx, curr_idx]) {
self.dbg(format!(
"[bootstrap] frame={curr_idx} publication rejected: {error}"
));
self.state.reset();
return self.frame_result(TrackingStatus::Skipped);
}
let health = crate::initialization::initial_map_health(&self.map.lock().unwrap());
if health.is_degenerate() {
self.dbg(format!(
"[init_gate] reject: valid_in_both={} median_depth={:.3} (need >= {} and > 0)",
health.valid_in_both, health.median_depth_older_kf, MIN_VALID_POINTS,
));
self.map.lock().unwrap().clear_active();
self.state.reset();
return self.frame_result(TrackingStatus::Skipped);
}
if let Some(kf) = self.map.lock().unwrap().get_keyframe(curr_idx) {
self.state.pose_world_to_cam = kf.frame.pose_world_to_cam;
}
if let Some(prev_ts) = self.inertial.bootstrap_timestamp_sec {
if let Some(edge) = self.inertial.keyframe_edge(
self.rig.imu_noise(),
prev_idx,
curr_idx,
prev_ts,
timestamp_sec,
) {
let published = self.map.lock().unwrap().apply_insertion(MapInsertion {
imu_factors: vec![edge],
..Default::default()
});
if let Err(error) = published {
self.dbg(format!(
"[bootstrap] frame={curr_idx} imu edge rejected: {error}"
));
}
}
self.prune_imu_before(timestamp_sec);
}
self.state.velocity = Some(Pose3d::between(
&prev_pose_world_to_cam,
&self.state.pose_world_to_cam,
));
self.finish_bootstrap(curr_idx, timestamp_sec);
self.frame_result(TrackingStatus::KeyframeAccepted)
}
fn publish_initial_map(
&mut self,
insertion: MapInsertion,
keyframes: [usize; 2],
) -> Result<(), MapMutationError> {
self.map.lock().unwrap().apply_insertion(insertion)?;
let refined = crate::mapping::bundle_adjustment::run_initial_ba(
&mut self.map.lock().unwrap(),
&self.rig.camera,
);
if let Err(error) = refined {
self.dbg(format!("[init_ba] {error}; keeping the unrefined map"));
}
for kf_idx in keyframes {
self.register_place_recognition(kf_idx);
}
Ok(())
}
fn finish_bootstrap(&mut self, kf_idx: usize, timestamp_sec: f64) {
self.state.current_keyframe_idx = Some(kf_idx);
self.state.last_keyframe_idx = Some(kf_idx);
self.state.mode = if self.rig.camera_to_body().is_some() {
self.inertial.schedule.start(kf_idx, timestamp_sec);
SystemMode::ImuInit
} else {
SystemMode::Tracking
};
self.inertial.last_keyframe_timestamp_sec = Some(timestamp_sec);
}
fn frame_result(&self, status: TrackingStatus) -> TrackingResult {
TrackingResult {
pose_world_to_cam: self.state.pose_world_to_cam,
status,
}
}
fn prune_imu_before(&mut self, t: f64) {
self.inertial.prune_before(t);
}
fn inertial_init_step(
&mut self,
frame: Frame,
previous_image: Option<&Image<u8, 1>>,
current_image: &Image<u8, 1>,
timestamp_sec: f64,
) -> TrackingResult {
let result = self.tracking_step(frame, previous_image, current_image, timestamp_sec);
if result.status == TrackingStatus::KeyframeAccepted
&& let Some(start_idx) = self.inertial.schedule.start_kf_idx()
{
let kfs: Vec<usize> = self
.map
.lock()
.unwrap()
.keyframes()
.iter()
.filter(|kf| kf.frame.idx >= start_idx)
.map(|kf| kf.frame.idx)
.collect();
let imu_time: f64 = self
.map
.lock()
.unwrap()
.imu_factors()
.iter()
.filter(|f| f.curr_kf_idx >= start_idx)
.map(|f| f.preintegrated.dt)
.sum();
let gate_msg = format_imu_init_gate(
start_idx,
kfs.first().copied(),
kfs.last().copied(),
kfs.len(),
self.inertial.initializer.config.min_keyframes,
imu_time,
self.inertial.initializer.config.min_time_sec,
);
self.dbg(gate_msg);
}
let due_for_retry = self.inertial.schedule.retry_due(timestamp_sec);
let imu_init_ready = self.inertial.initializer.ready(
&self.map.lock().unwrap(),
self.inertial.schedule.start_kf_idx(),
);
if result.status == TrackingStatus::KeyframeAccepted && due_for_retry && imu_init_ready {
let Some(start_idx) = self.inertial.schedule.start_kf_idx() else {
return result;
};
self.inertial.schedule.record_attempt(timestamp_sec);
let is_mono = !self
.map
.lock()
.unwrap()
.keyframes()
.iter()
.find(|kf| kf.frame.idx >= start_idx)
.map(|kf| kf.frame.is_stereo())
.unwrap_or(false);
let prior_a0 = viba0_accel_bias_prior(is_mono);
let init_result = self.inertial.initializer.try_initialize(
&self.map.lock().unwrap(),
self.rig.camera_to_body(),
self.inertial.bias,
start_idx,
1e2,
prior_a0,
false,
);
match init_result {
Some(init) => {
let applied = self.inertial.apply_initialization(
&mut self.map.lock().unwrap(),
&mut self.state,
init,
start_idx,
);
match applied {
Ok(applied) => self.resume_tracking_after_viba0(timestamp_sec, applied),
Err(error) => {
self.dbg(format!("[imu_init] VIBA0 alignment refused: {error}"));
}
}
}
None => {
self.dbg("[imu_init] VIBA0 rejected: solve failed or invalid scale".into());
}
}
}
result
}
fn resume_tracking_after_viba0(&mut self, timestamp_sec: f64, applied: AppliedInitialization) {
self.state.mode = SystemMode::Tracking;
self.state.imu_init_timestamp_sec = Some(timestamp_sec);
if !self.local_mapping.submit(KeyframeJob {
imu_initialized: true,
imu_t_bc: self.rig.camera_to_body(),
gravity_world: self.inertial.gravity_world,
}) {
self.dbg("[local_mapping] worker is unavailable".into());
}
self.apply_local_mapping_results();
let AppliedInitialization {
scale,
gravity_world: gravity,
gyro_bias: bg,
} = applied;
self.dbg(format!(
"[imu_init] VIBA0 accepted: scale={scale:.4} gravity=({:.3},{:.3},{:.3}) \
gyro_bias=({:.4},{:.4},{:.4})",
gravity.x, gravity.y, gravity.z, bg.x, bg.y, bg.z
));
}
fn tracking_step(
&mut self,
frame: Frame,
previous_image: Option<&Image<u8, 1>>,
current_image: &Image<u8, 1>,
timestamp_sec: f64,
) -> TrackingResult {
let image_size = frame.image_size;
let pose_before = self.state.pose_world_to_cam;
let prev_timestamp = self.state.last_frame_timestamp_sec;
if let Some(kf_idx) = self.state.current_keyframe_idx
&& let Some(kf) = self.map.lock().unwrap().get_keyframe(kf_idx)
{
self.state.velocity_world = kf.velocity_world;
self.inertial.bias = kf.imu_bias;
}
let preintegrated = (self.state.imu_initialized && prev_timestamp > 0.0).then(|| {
self.inertial
.preintegrate_window(self.rig.imu_noise(), prev_timestamp, timestamp_sec)
.0
});
let (candidate_pose, predicted_velocity) = predict_pose(
pose_before,
self.state.velocity,
self.state.velocity_world,
preintegrated
.as_ref()
.map(|preintegrated| InertialPrediction {
preintegrated,
camera_to_body: self.rig.camera_to_body(),
gravity_world: self.inertial.gravity_world,
}),
);
if let Some(velocity_world) = predicted_velocity {
self.state.velocity_world = velocity_world; }
let currently_lost_for = self
.state
.lost_since_sec
.map_or(0.0, |t0| timestamp_sec - t0);
let result = {
let map = self.map.lock().unwrap();
self.tracker.estimate(
FrameInput {
frame: &frame,
previous_image,
current_image,
candidate_pose,
pose_before,
current_keyframe_idx: self.state.current_keyframe_idx,
lost_for_sec: currently_lost_for,
},
&map,
&self.rig.camera,
)
};
let (mut status, matches, tracked_inliers, reject_reason) = match result {
Ok(estimate) => {
self.state.pose_world_to_cam = estimate.pose;
if !self.state.imu_initialized {
self.state.velocity = Some(Pose3d::between(&pose_before, &estimate.pose));
}
(
TrackingStatus::Tracked,
estimate.matches,
estimate.inliers,
None,
)
}
Err(reason) => {
self.state.pose_world_to_cam = candidate_pose;
(TrackingStatus::Skipped, Vec::new(), 0, Some(reason))
}
};
if self.debug {
let msg = match reject_reason {
Some(reason) => format!("[track] frame={} reject: {:?}", frame.idx, reason),
None => format!(
"[track] frame={} ok: matches={} inliers={}",
frame.idx,
matches.len(),
tracked_inliers,
),
};
self.debug_messages.push(msg);
}
if status == TrackingStatus::Tracked {
{
let mut map_guard = self.map.lock().unwrap();
let current_kf = self
.state
.current_keyframe_idx
.and_then(|ki| map_guard.get_keyframe(ki));
let local_indices = select_local_map_points(
&map_guard,
&matches,
current_kf,
LocalMapSelectionConfig::default(),
);
let visible = crate::tracking::local_map::landmarks_in_frustum(
&map_guard,
&local_indices,
&self.rig.camera,
&candidate_pose,
image_size,
);
map_guard.update_observation_counts(&visible, &matches);
}
if self.try_insert_keyframe(&frame, timestamp_sec, tracked_inliers, &matches) {
status = TrackingStatus::KeyframeAccepted;
}
}
if status == TrackingStatus::Skipped {
let policy = &self.tracking_loss_recovery;
let lost_since = *self.state.lost_since_sec.get_or_insert(timestamp_sec);
let recently_lost_for = timestamp_sec - lost_since;
let imu_confident = self.state.imu_initialized
&& self
.state
.imu_init_timestamp_sec
.is_some_and(|t0| timestamp_sec - t0 >= policy.min_imu_confidence_sec);
let grace_period_sec = policy.grace_period_sec(imu_confident);
let map_established =
self.map.lock().unwrap().keyframes().len() > policy.min_keyframes_for_grace;
if !map_established || recently_lost_for >= grace_period_sec {
self.dbg(format!(
"[lost] frame={} giving up after {:.2}s (map_established={}): resetting",
frame.idx, recently_lost_for, map_established,
));
self.tracker.reset_tracks();
self.state.reset();
return self.bootstrap_step(frame, timestamp_sec);
}
} else {
self.state.lost_since_sec = None;
}
self.state.last_frame_timestamp_sec = timestamp_sec;
if let Some(kf_ts) = self.inertial.last_keyframe_timestamp_sec {
self.prune_imu_before(kf_ts.min(timestamp_sec));
}
self.frame_result(status)
}
fn try_insert_keyframe(
&mut self,
frame: &Frame,
timestamp_sec: f64,
tracked_inliers: usize,
matches: &[(usize, usize)],
) -> bool {
let n_ref_map_points = if let Some(ki) = self.state.current_keyframe_idx {
let map = self.map.lock().unwrap();
map.get_keyframe(ki)
.map(|kf| kf.num_associated_points())
.unwrap_or(0)
} else {
0
};
if !self.keyframe_policy.should_insert(
frame.idx,
self.state.last_keyframe_idx,
tracked_inliers,
n_ref_map_points,
) {
return false;
}
if let Some(ki) = self.state.current_keyframe_idx {
let map = self.map.lock().unwrap();
if map.get_keyframe(ki).is_none() {
return false;
}
} else {
return false;
}
let mut curr_kf = Keyframe::from_frame(Frame {
pose_world_to_cam: self.state.pose_world_to_cam,
..frame.clone()
});
if self.state.imu_initialized {
curr_kf.velocity_world = self.state.velocity_world;
curr_kf.imu_bias = self.inertial.bias;
}
let imu_initialized = self.state.imu_initialized;
let is_stereo = curr_kf.frame.is_stereo();
let growth_plan =
keyframe_mapping::prepare_keyframe_growth(&self.map.lock().unwrap(), frame.idx);
let imu_edge = match (
self.state.last_keyframe_idx,
self.inertial.last_keyframe_timestamp_sec,
) {
(Some(prev_kf_idx), Some(prev_ts)) => self.inertial.keyframe_edge(
self.rig.imu_noise(),
prev_kf_idx,
frame.idx,
prev_ts,
timestamp_sec,
),
_ => None,
};
let KeyframeInsertion {
insertion,
close_points: n_close,
} = keyframe_mapping::keyframe_insertion(
curr_kf,
matches,
self.stereo_close_depth,
&self.rig.camera,
imu_edge,
);
let published = self.map.lock().unwrap().apply_insertion(insertion);
if let Err(error) = published {
self.dbg(format!(
"[kf] frame={} publication rejected: {error}",
frame.idx
));
return false;
}
if self.stereo_close_depth.is_some() && is_stereo {
self.dbg(format!(
"[kf_stereo] frame={} close_points={}",
frame.idx, n_close
));
}
self.inertial.last_keyframe_timestamp_sec = Some(timestamp_sec);
self.state.current_keyframe_idx = Some(frame.idx);
self.state.last_keyframe_idx = Some(frame.idx);
let growth = keyframe_mapping::grow_keyframe(
&self.map,
growth_plan,
&self.rig.camera,
self.two_view_init_config.match_config,
&self.two_view_init_config.triangulation_config,
);
self.report_growth(frame.idx, &growth);
if imu_initialized {
self.refine_inertial_init(timestamp_sec);
}
if !self.local_mapping.submit(KeyframeJob {
imu_initialized,
imu_t_bc: self.rig.camera_to_body(),
gravity_world: self.inertial.gravity_world,
}) {
self.dbg("[local_mapping] worker is unavailable".into());
}
self.apply_local_mapping_results();
self.register_place_recognition(frame.idx);
true
}
fn report_growth(&mut self, frame_idx: usize, growth: &KeyframeGrowthResult) {
if !self.debug {
return;
}
for failure in &growth.pair_failures {
self.dbg(format!(
"[kf] frame={frame_idx} pair growth against {} rejected: {}",
failure.neighbor_keyframe_idx, failure.error
));
}
self.dbg(format!(
"[kf] frame={frame_idx} grown={} from {} neighbor kfs",
growth.landmarks_added, growth.neighbor_count
));
self.dbg(format!(
"[fuse] frame={frame_idx} fused={}",
growth.observations_added
));
for error in &growth.fusion_failures {
self.dbg(format!("[fuse] frame={frame_idx} link refused: {error}"));
}
}
fn refine_inertial_init(&mut self, timestamp_sec: f64) {
let (Some(start_idx), Some(refinement)) = (
self.inertial.schedule.start_kf_idx(),
self.inertial.schedule.due_refinement(timestamp_sec),
) else {
return;
};
let stage = refinement.name();
let init_result = self.inertial.initializer.try_initialize(
&self.map.lock().unwrap(),
self.rig.camera_to_body(),
self.inertial.bias,
start_idx,
refinement.prior_g(),
refinement.prior_a(),
true,
);
match init_result {
Some(init) => {
let applied = self.inertial.apply_initialization(
&mut self.map.lock().unwrap(),
&mut self.state,
init,
start_idx,
);
match applied {
Ok(AppliedInitialization {
scale,
gyro_bias: bg,
..
}) => self.dbg(format!(
"[imu_init] {stage} accepted: scale_correction={scale:.4} gyro_bias=({:.4},{:.4},{:.4})",
bg.x, bg.y, bg.z
)),
Err(error) => {
self.dbg(format!("[imu_init] {stage} alignment refused: {error}"));
}
}
}
None => {
self.dbg(format!(
"[imu_init] {stage} rejected: solve failed or invalid scale"
));
}
}
self.inertial.schedule.complete(refinement);
}
fn register_place_recognition(&mut self, kf_idx: usize) {
let context = LoopClosingContext {
pose_world_to_cam: self.state.pose_world_to_cam,
velocity_world: self.state.velocity_world,
current_keyframe_idx: self.state.current_keyframe_idx,
imu_initialized: self.state.imu_initialized,
gravity_world: self.inertial.gravity_world,
};
let outcome = {
let mut map = self.map.lock().unwrap();
self.loop_closer
.on_keyframe(&mut map, &self.rig.camera, kf_idx, context)
};
if let Some(message) = outcome.debug_message {
self.dbg(message);
}
if let Some((corrected_tracking_pose, corrected_velocity)) = outcome.tracking_correction {
self.state.pose_world_to_cam = corrected_tracking_pose;
self.state.velocity_world = corrected_velocity;
}
if outcome.pgo_applied {
self.tracker.reset_tracks();
if self.state.imu_initialized {
if !self.local_mapping.submit(KeyframeJob {
imu_initialized: true,
imu_t_bc: self.rig.camera_to_body(),
gravity_world: self.inertial.gravity_world,
}) {
self.dbg("[local_mapping] worker is unavailable after PGO".into());
}
self.apply_local_mapping_results();
}
}
self.loop_closure_events.extend(outcome.events);
}
}
fn format_imu_init_gate(
start_idx: usize,
first_idx: Option<usize>,
last_idx: Option<usize>,
keyframes: usize,
min_keyframes: usize,
imu_time: f64,
min_time_sec: f64,
) -> String {
format!(
"[imu_init_gate] start_idx={start_idx} first_idx={first_idx:?} last_idx={last_idx:?} kfs={keyframes}/{min_keyframes} imu_time={imu_time:.2}/{min_time_sec:.1}s"
)
}
#[cfg(test)]
mod tests {
use super::format_imu_init_gate;
#[test]
fn formats_compact_imu_init_gate() {
assert_eq!(
format_imu_init_gate(12, Some(12), Some(32), 7, 10, 1.05, 1.0),
"[imu_init_gate] start_idx=12 first_idx=Some(12) last_idx=Some(32) kfs=7/10 imu_time=1.05/1.0s"
);
}
}