use nalgebra::{Vector3, Vector6};
use rayon::prelude::*;
use rigidity_core::PointCloud;
use rigidity_core::icp::{IcpConfig, Kernel, register, surface};
use rigidity_core::lie::Se3;
use rigidity_core::normals::estimate_normals;
use rigidity_core::observability::{
Conditioning, Correspondence, Observability, ObservabilityCriteria, analyse,
};
use rigidity_scenes::rng::Rng;
use rigidity_scenes::{Scene, SceneKind, SceneParams};
use rigidity_spatial::KdTree;
#[derive(Debug, Clone, Copy)]
pub struct TrialConfig {
pub trials: usize,
pub points_per_face: usize,
pub scale: f64,
pub noise_sigma: f64,
pub tolerance: f64,
pub initial_translation: f64,
pub initial_rotation: f64,
pub estimated_normals: bool,
pub seed: u64,
}
impl Default for TrialConfig {
fn default() -> Self {
Self {
trials: 1_000,
points_per_face: 800,
scale: 1.0,
noise_sigma: 1e-3,
tolerance: 1e-4,
initial_translation: 0.02,
initial_rotation: 0.01,
estimated_normals: false,
seed: 0x7E57_5EED,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DirectionOutcome {
pub index: usize,
pub predicted: f64,
pub empirical: f64,
pub bias: f64,
pub observability: Observability,
}
impl DirectionOutcome {
pub fn ratio(&self) -> f64 {
self.empirical / self.predicted
}
}
#[derive(Debug, Clone)]
pub struct SceneOutcome {
pub kind: SceneKind,
pub estimated_normals: bool,
pub converged: usize,
pub trials: usize,
pub condition_number: f64,
pub directions: Vec<DirectionOutcome>,
}
fn random_direction(rng: &mut Rng) -> Vector3<f64> {
loop {
let candidate = Vector3::new(rng.normal(1.0), rng.normal(1.0), rng.normal(1.0));
if candidate.norm() > 1e-9 {
return candidate.normalize();
}
}
}
fn build_normals(
cloud: &PointCloud,
analytic: &[Vector3<f64>],
estimate: bool,
) -> Vec<Vector3<f64>> {
if estimate {
let tree = KdTree::build(cloud).expect("the tree builds");
estimate_normals(cloud, &tree, 16)
} else {
analytic.to_vec()
}
}
pub fn run(kind: SceneKind, config: &TrialConfig) -> SceneOutcome {
let scene = Scene::generate(
kind,
SceneParams {
points_per_face: config.points_per_face,
scale: config.scale,
..SceneParams::default()
},
);
let target_normals = build_normals(&scene.cloud, &scene.normals, config.estimated_normals);
let tree = KdTree::build(&scene.cloud).expect("the tree builds");
let prediction = analyse(scene.len(), Kernel::Squared, |index| {
Some(Correspondence {
point: scene.cloud.point(index),
normal: target_normals[index],
residual: 0.0,
})
})
.expect("the scene is non-empty");
let conditioning: &Conditioning = &prediction.conditioning;
let criteria = ObservabilityCriteria {
noise_sigma: config.noise_sigma,
tolerance: config.tolerance,
};
let states = conditioning.classify(&criteria);
let predicted = conditioning.uncertainty(config.noise_sigma);
let truth = Se3::exp(&Vector6::new(
0.031 * config.scale,
-0.017 * config.scale,
0.024 * config.scale,
0.021,
-0.013,
0.018,
));
let icp = IcpConfig {
kernel: Kernel::Squared,
max_correspondence_distance: 0.5 * config.scale,
max_iterations: 60,
..IcpConfig::default()
};
let outcomes: Vec<Option<([f64; 6], bool)>> = (0..config.trials)
.into_par_iter()
.map(|trial| {
let mut rng = Rng::new(config.seed ^ (trial as u64).wrapping_mul(0x9E37_79B9));
let inverse = truth.inverse();
let rotation = *inverse.rotation().matrix();
let mut source = PointCloud::with_capacity(scene.len());
for index in 0..scene.len() {
let jitter = Vector3::new(
rng.normal(config.noise_sigma),
rng.normal(config.noise_sigma),
rng.normal(config.noise_sigma),
);
source.push(inverse.transform_point(&(scene.points[index] + jitter)));
}
let source_normals = if config.estimated_normals {
let source_tree = KdTree::build(&source).ok()?;
estimate_normals(&source, &source_tree, 16)
} else {
scene.normals.iter().map(|n| rotation * n).collect()
};
let offset = random_direction(&mut rng) * config.initial_translation * config.scale;
let turn = random_direction(&mut rng) * config.initial_rotation;
let start = Se3::exp(&Vector6::new(
offset.x, offset.y, offset.z, turn.x, turn.y, turn.z,
)) * truth;
let result = register(
&surface(&source, &source_normals),
&surface(&scene.cloud, &target_normals),
&tree,
start,
&icp,
);
let error = (truth * result.pose.inverse()).log();
let mut components = [0.0f64; 6];
for (index, slot) in components.iter_mut().enumerate() {
*slot = conditioning.component(index, error);
}
if components.iter().any(|v| !v.is_finite()) {
return None;
}
Some((components, result.converged))
})
.collect();
let samples: Vec<[f64; 6]> = outcomes.iter().flatten().map(|(c, _)| *c).collect();
let converged = outcomes.iter().flatten().filter(|(_, ok)| *ok).count();
let count = samples.len().max(1) as f64;
let directions = (0..6)
.map(|index| {
let mean: f64 = samples.iter().map(|c| c[index]).sum::<f64>() / count;
let variance: f64 = samples
.iter()
.map(|c| (c[index] - mean) * (c[index] - mean))
.sum::<f64>()
/ count;
DirectionOutcome {
index,
predicted: predicted[index],
empirical: variance.sqrt(),
bias: mean,
observability: states[index],
}
})
.collect();
SceneOutcome {
kind,
estimated_normals: config.estimated_normals,
converged,
trials: samples.len(),
condition_number: conditioning.condition_number(),
directions,
}
}
pub fn run_all(config: &TrialConfig) -> Vec<SceneOutcome> {
SceneKind::ALL
.iter()
.map(|kind| run(*kind, config))
.collect()
}