use kornia_3d::camera::PinholeCamera;
use kornia_3d::pose::Pose3d;
pub use kornia_3d::pose::TwoViewError;
use kornia_3d::pose::{TriangulationConfig, TwoViewEstimator, TwoViewModel};
use kornia_algebra::Vec3F64;
use kornia_imgproc::features::{OrbFeatures, OrbMatchConfig, match_orb_descriptors};
use crate::tracking::pose_estimation::Estimate;
#[derive(Debug, Clone)]
pub struct TwoViewAcceptanceConfig {
pub min_matches: usize,
pub min_inliers: usize,
pub min_triangulated: usize,
}
#[derive(Debug, Clone)]
pub struct TwoViewInitConfig {
pub match_config: OrbMatchConfig,
pub triangulation_config: TriangulationConfig,
pub acceptance_config: TwoViewAcceptanceConfig,
}
impl Default for TwoViewInitConfig {
fn default() -> Self {
Self {
match_config: OrbMatchConfig {
nn_ratio: 0.6,
th_low: 50,
check_orientation: true,
histo_length: 30,
},
triangulation_config: TriangulationConfig {
cheirality_ambiguity_max: 0.75,
..TriangulationConfig::default()
},
acceptance_config: TwoViewAcceptanceConfig {
min_matches: 100,
min_inliers: 30,
min_triangulated: 50,
},
}
}
}
#[derive(Debug)]
pub enum TwoViewRejectReason {
LowMatches,
EstimationFailed(TwoViewError),
LowTriangulated,
LowInliers,
LowParallax,
}
#[derive(Debug, Clone)]
pub struct TwoViewEstimate {
pub estimate: Estimate,
pub points3d: Vec<Vec3F64>,
pub inlier_indices: Vec<usize>,
pub median_depth: Option<f64>,
pub model_kind: char,
}
pub fn try_initialize_two_view(
ref_features: &OrbFeatures,
ref_pose: &Pose3d,
curr_features: &OrbFeatures,
camera: &PinholeCamera,
config: &TwoViewInitConfig,
) -> Result<TwoViewEstimate, TwoViewRejectReason> {
let acceptance = &config.acceptance_config;
let matches = match_orb_descriptors(
&ref_features.orientations,
&ref_features.descriptors,
&curr_features.orientations,
&curr_features.descriptors,
config.match_config,
);
if matches.len() < acceptance.min_matches {
return Err(TwoViewRejectReason::LowMatches);
}
let (reference_pts, current_pts) = camera.undistort_matched_pairs(
&ref_features.keypoints_xy,
&curr_features.keypoints_xy,
&matches,
);
let k = camera.intrinsic_matrix();
let estimator = TwoViewEstimator::builder()
.triangulation(config.triangulation_config.clone())
.build();
let result = estimator
.estimate(&reference_pts, ¤t_pts, &k, &k)
.map_err(TwoViewRejectReason::EstimationFailed)?;
let model_kind = match result.model {
TwoViewModel::Homography(_) => 'H',
_ => 'F',
};
if result.points3d.len() < acceptance.min_triangulated {
return Err(TwoViewRejectReason::LowTriangulated);
}
if result.inlier_indices.len() < acceptance.min_inliers {
return Err(TwoViewRejectReason::LowInliers);
}
let median_parallax_deg = result.median_parallax_deg(&reference_pts, ¤t_pts, camera);
if median_parallax_deg < config.triangulation_config.min_parallax_deg {
return Err(TwoViewRejectReason::LowParallax);
}
let mut t_scaled = result.translation;
let mut depths: Vec<f64> = result
.points3d
.iter()
.map(|p| p.z)
.filter(|&z| z > 0.0)
.collect();
let median_depth = median_in_place(&mut depths).filter(|&d| d > 1e-6);
if let Some(md) = median_depth {
t_scaled /= md;
}
let pose = Pose3d::new(
result.rotation * ref_pose.rotation,
result.rotation * ref_pose.translation + t_scaled,
);
let inliers = result.inlier_indices.len();
Ok(TwoViewEstimate {
estimate: Estimate {
pose,
matches,
inliers,
},
points3d: result.points3d,
inlier_indices: result.inlier_indices,
median_depth,
model_kind,
})
}
fn median_in_place(values: &mut [f64]) -> Option<f64> {
if values.is_empty() {
return None;
}
let mid = values.len() / 2;
values.select_nth_unstable_by(mid, |a, b| a.total_cmp(b));
Some(values[mid])
}