use std::collections::HashSet;
use kornia_3d::camera::PinholeCamera;
use crate::Frame;
use crate::initialization::two_view::{
TwoViewEstimate, TwoViewInitConfig, TwoViewRejectReason, try_initialize_two_view,
};
use crate::mapping::Map;
use crate::mapping::growth::{accepted_pair_claims, stereo_seeds};
use crate::mapping::map::{
Keyframe, LandmarkSeed, LandmarkTarget, MapInsertion, ObservationKey, ObservationLink,
};
use kornia_algebra::Vec3F64;
pub(crate) const MIN_KEYPOINTS_FOR_BOOTSTRAP: usize = 100;
pub(crate) const MIN_STEREO_POINTS: usize = 50;
pub(crate) const MIN_VALID_POINTS: usize = 50;
pub(crate) enum BootstrapDecision {
Unusable { keypoints: usize },
StoreAsReference,
Rejected {
reference_idx: usize,
reason: TwoViewRejectReason,
},
Initialized(Box<TwoViewEstimate>),
}
pub(crate) fn evaluate_bootstrap(
reference: Option<&Frame>,
current: &Frame,
camera: &PinholeCamera,
config: &TwoViewInitConfig,
) -> BootstrapDecision {
let keypoints = current.features.keypoints_xy.len();
if keypoints <= MIN_KEYPOINTS_FOR_BOOTSTRAP {
return BootstrapDecision::Unusable { keypoints };
}
let Some(reference) = reference else {
return BootstrapDecision::StoreAsReference;
};
match try_initialize_two_view(
&reference.features,
&reference.pose_world_to_cam,
¤t.features,
camera,
config,
) {
Ok(estimate) => BootstrapDecision::Initialized(Box::new(estimate)),
Err(reason) => BootstrapDecision::Rejected {
reference_idx: reference.idx,
reason,
},
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct TooFewStereoPoints {
pub(crate) found: usize,
}
pub(crate) fn stereo_initial_map(
keyframe: Keyframe,
camera: &PinholeCamera,
) -> Result<MapInsertion, TooFewStereoPoints> {
let landmarks = stereo_seeds(&keyframe.frame, camera, f64::INFINITY, &[]);
if landmarks.len() < MIN_STEREO_POINTS {
return Err(TooFewStereoPoints {
found: landmarks.len(),
});
}
Ok(MapInsertion {
keyframes: vec![keyframe],
landmarks,
..Default::default()
})
}
pub(crate) fn two_view_initial_map(
reference: Keyframe,
current: Keyframe,
estimate: &TwoViewEstimate,
) -> MapInsertion {
let depth_scale = estimate.median_depth.filter(|&d| d > 1e-6).unwrap_or(1.0);
let reference_pose_inv = reference.frame.pose_world_to_cam.inverse();
let reference_kf_idx = reference.frame.idx;
let current_kf_idx = current.frame.idx;
let matches = &estimate.estimate.matches;
let mut candidates: Vec<(Vec3F64, [u8; 3], usize, usize)> = Vec::new();
for (p_cam, &match_idx) in estimate.points3d.iter().zip(estimate.inlier_indices.iter()) {
let Some(&(ref_desc_idx, curr_desc_idx)) = matches.get(match_idx) else {
continue;
};
if ref_desc_idx >= reference.map_point_by_desc_idx.len()
|| curr_desc_idx >= current.map_point_by_desc_idx.len()
{
continue;
}
if current
.frame
.features
.descriptors
.get(curr_desc_idx)
.or_else(|| reference.frame.features.descriptors.get(ref_desc_idx))
.is_none()
{
continue;
}
let color = current
.frame
.keypoint_colors
.get(curr_desc_idx)
.copied()
.unwrap_or([128; 3]);
let position = reference_pose_inv.transform_point(&(*p_cam / depth_scale));
candidates.push((position, color, ref_desc_idx, curr_desc_idx));
}
let claims: Vec<(usize, usize)> = candidates
.iter()
.map(|&(_, _, ref_desc_idx, curr_desc_idx)| (ref_desc_idx, curr_desc_idx))
.collect();
let mut request = MapInsertion::default();
for index in accepted_pair_claims(&claims) {
let (position, color, ref_desc_idx, curr_desc_idx) = candidates[index];
let new_index = request.landmarks.len();
request.landmarks.push(LandmarkSeed {
position,
color,
reference: ObservationKey {
keyframe_idx: current_kf_idx,
feature_idx: curr_desc_idx,
},
});
request.observations.push(ObservationLink {
observation: ObservationKey {
keyframe_idx: reference_kf_idx,
feature_idx: ref_desc_idx,
},
landmark: LandmarkTarget::New(new_index),
});
}
request.keyframes = vec![reference, current];
request
}
#[derive(Debug, Clone, Copy, Default)]
pub struct InitialMapHealth {
pub valid_in_both: usize,
pub median_depth_older_kf: f64,
}
impl InitialMapHealth {
pub fn is_degenerate(&self) -> bool {
self.valid_in_both < MIN_VALID_POINTS || self.median_depth_older_kf <= 0.0
}
}
pub fn initial_map_health(map: &Map) -> InitialMapHealth {
let keyframes = map.keyframes();
let n = keyframes.len();
if n < 2 {
return InitialMapHealth::default();
}
let kf_older = &keyframes[n - 2];
let kf_newer = &keyframes[n - 1];
let pose_older = kf_older.frame.pose_world_to_cam;
let pose_newer = kf_newer.frame.pose_world_to_cam;
let mut seen: HashSet<usize> = HashSet::new();
for mp_idx in kf_older
.map_point_by_desc_idx
.iter()
.chain(kf_newer.map_point_by_desc_idx.iter())
.flatten()
{
seen.insert(*mp_idx);
}
let mut depths_older: Vec<f64> = Vec::with_capacity(seen.len());
let mut valid_in_both = 0usize;
for idx in seen {
let Some(mp) = map.map_points().get(idx) else {
continue;
};
if mp.culled {
continue;
}
let z_older = pose_older.transform_point(&mp.position).z;
let z_newer = pose_newer.transform_point(&mp.position).z;
if z_older > 0.0 && z_newer > 0.0 {
valid_in_both += 1;
depths_older.push(z_older);
}
}
let median_depth = if depths_older.is_empty() {
0.0
} else {
let mid = depths_older.len() / 2;
depths_older.select_nth_unstable_by(mid, |a, b| a.total_cmp(b));
depths_older[mid]
};
InitialMapHealth {
valid_in_both,
median_depth_older_kf: median_depth,
}
}
#[cfg(test)]
mod tests {
use super::{
BootstrapDecision, MIN_KEYPOINTS_FOR_BOOTSTRAP, MIN_STEREO_POINTS, evaluate_bootstrap,
stereo_initial_map, two_view_initial_map,
};
use crate::Frame;
use crate::initialization::two_view::TwoViewEstimate;
use crate::mapping::map::{Keyframe, LandmarkTarget};
use crate::tracking::pose_estimation::Estimate;
use kornia_3d::camera::PinholeCamera;
use kornia_3d::pose::Pose3d;
use kornia_algebra::Vec3F64;
use kornia_image::ImageSize;
use kornia_imgproc::features::OrbFeatures;
fn frame(idx: usize, keypoints: usize) -> Frame {
Frame {
idx,
features: OrbFeatures {
keypoints_xy: (0..keypoints).map(|i| [i as f32, i as f32]).collect(),
orientations: vec![0.0; keypoints],
descriptors: vec![[0u8; 32]; keypoints],
octaves: vec![0; keypoints],
},
pose_world_to_cam: Pose3d::IDENTITY,
image_size: ImageSize {
width: 640,
height: 480,
},
keypoint_colors: Vec::new(),
u_right: Vec::new(),
depth: Vec::new(),
keypoints_undist: Vec::new(),
}
}
fn camera() -> PinholeCamera {
PinholeCamera {
fx: 400.0,
fy: 400.0,
cx: 320.0,
cy: 240.0,
k1: 0.0,
k2: 0.0,
p1: 0.0,
p2: 0.0,
}
}
#[test]
fn a_feature_poor_frame_is_unusable_with_or_without_a_reference() {
let poor = frame(1, MIN_KEYPOINTS_FOR_BOOTSTRAP);
let reference = frame(0, 500);
for held in [None, Some(&reference)] {
assert!(matches!(
evaluate_bootstrap(held, &poor, &camera(), &Default::default()),
BootstrapDecision::Unusable { keypoints } if keypoints == MIN_KEYPOINTS_FOR_BOOTSTRAP
));
}
}
#[test]
fn the_keypoint_threshold_is_exclusive() {
assert!(matches!(
evaluate_bootstrap(
None,
&frame(1, MIN_KEYPOINTS_FOR_BOOTSTRAP),
&camera(),
&Default::default()
),
BootstrapDecision::Unusable { .. }
));
assert!(matches!(
evaluate_bootstrap(
None,
&frame(1, MIN_KEYPOINTS_FOR_BOOTSTRAP + 1),
&camera(),
&Default::default()
),
BootstrapDecision::StoreAsReference
));
}
#[test]
fn a_usable_frame_without_a_reference_becomes_the_reference() {
assert!(matches!(
evaluate_bootstrap(None, &frame(7, 500), &camera(), &Default::default()),
BootstrapDecision::StoreAsReference
));
}
#[test]
fn two_view_failure_reports_the_reference_it_failed_against() {
let reference = frame(3, 500);
let current = frame(4, 500);
match evaluate_bootstrap(Some(&reference), ¤t, &camera(), &Default::default()) {
BootstrapDecision::Rejected { reference_idx, .. } => {
assert_eq!(reference_idx, 3, "the held reference must be identified");
}
_ => panic!("degenerate identical features cannot initialize"),
}
}
#[test]
fn evaluation_leaves_both_frames_untouched() {
let reference = frame(3, 500);
let current = frame(4, 500);
let reference_before = reference.features.keypoints_xy.len();
let current_before = current.features.keypoints_xy.len();
let _ = evaluate_bootstrap(Some(&reference), ¤t, &camera(), &Default::default());
assert_eq!(reference.features.keypoints_xy.len(), reference_before);
assert_eq!(current.features.keypoints_xy.len(), current_before);
assert_eq!(reference.pose_world_to_cam, Pose3d::IDENTITY);
}
fn stereo_frame(idx: usize, valid_depths: usize) -> Frame {
let mut frame = frame(idx, valid_depths + 5);
frame.depth = (0..valid_depths + 5)
.map(|i| if i < valid_depths { 2.0 } else { -1.0 })
.collect();
frame
}
#[test]
fn a_stereo_frame_needs_enough_valid_depths_to_seed_a_map() {
let too_few = stereo_frame(1, MIN_STEREO_POINTS - 1);
let error = stereo_initial_map(Keyframe::from_frame(too_few), &camera()).unwrap_err();
assert_eq!(error.found, MIN_STEREO_POINTS - 1);
let insertion = stereo_initial_map(
Keyframe::from_frame(stereo_frame(1, MIN_STEREO_POINTS)),
&camera(),
)
.unwrap();
assert_eq!(insertion.keyframes.len(), 1);
assert_eq!(insertion.landmarks.len(), MIN_STEREO_POINTS);
assert!(insertion.observations.is_empty());
}
#[test]
fn a_two_view_map_keeps_the_first_claim_on_each_feature_and_normalizes_depth() {
let reference = Keyframe::from_frame(frame(3, 500));
let current = Keyframe::from_frame(frame(4, 500));
let estimate = TwoViewEstimate {
estimate: Estimate {
pose: Pose3d::IDENTITY,
matches: vec![(10, 20), (10, 21), (11, 999), (12, 22)],
inliers: 4,
},
points3d: vec![
Vec3F64::new(0.0, 0.0, 4.0),
Vec3F64::new(0.0, 0.0, 4.0),
Vec3F64::new(0.0, 0.0, 4.0),
Vec3F64::new(0.0, 0.0, 8.0),
],
inlier_indices: vec![0, 1, 2, 3],
median_depth: Some(4.0),
model_kind: 'F',
};
let insertion = two_view_initial_map(reference, current, &estimate);
assert_eq!(insertion.keyframes.len(), 2);
let features: Vec<usize> = insertion
.landmarks
.iter()
.map(|seed| seed.reference.feature_idx)
.collect();
assert_eq!(features, vec![20, 22]);
assert!(
insertion
.landmarks
.iter()
.all(|seed| seed.reference.keyframe_idx == 4)
);
assert_eq!(insertion.landmarks[0].position.z, 1.0);
assert_eq!(insertion.landmarks[1].position.z, 2.0);
assert!(insertion.observations.iter().all(|link| {
link.observation.keyframe_idx == 3 && matches!(link.landmark, LandmarkTarget::New(_))
}));
}
}