use std::error::Error;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand, ValueEnum};
use rigidity_core::lie::Se3;
use rigidity_core::observability::Analysis;
use rigidity_pipeline::{
PrepareParams, RegisterParams, ReportParams, analyse_cloud, analyse_registration, prepare,
register_pair, transform_cloud,
};
use rigidity_scenes::{Scene, SceneKind, SceneParams};
#[derive(Parser)]
#[command(name = "rigidity", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Scene {
#[arg(short, long, value_enum, default_value_t = Kind::Corridor)]
kind: Kind,
#[arg(short, long, default_value_t = 20_000)]
points: usize,
#[arg(short, long, default_value_t = 1.0)]
scale: f64,
#[arg(long, default_value_t = 0.0)]
noise: f64,
#[arg(long, value_delimiter = ',')]
shift: Option<Vec<f64>>,
#[arg(long, value_delimiter = ',')]
turn: Option<Vec<f64>>,
#[arg(short, long)]
out: PathBuf,
},
Analyse {
cloud: PathBuf,
#[command(flatten)]
common: Common,
},
Register {
source: PathBuf,
target: PathBuf,
#[command(flatten)]
common: Common,
#[arg(long, default_value_t = 0.5)]
max_distance: f64,
#[arg(long, default_value_t = 0.1)]
huber: f64,
#[arg(long)]
out: Option<PathBuf>,
},
}
#[derive(clap::Args)]
struct Common {
#[arg(long, default_value_t = 0.05)]
voxel: f64,
#[arg(long, default_value_t = 16)]
neighbours: usize,
#[arg(long, default_value_t = 0.01)]
noise: f64,
#[arg(long, default_value_t = 0.001)]
tolerance: f64,
#[arg(long, default_value_t = 1.0)]
calibration: f64,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
enum Kind {
Plane,
Cylinder,
Sphere,
TwoPlanes,
Corner,
TeeJoint,
Corridor,
}
impl From<Kind> for SceneKind {
fn from(kind: Kind) -> Self {
match kind {
Kind::Plane => SceneKind::Plane,
Kind::Cylinder => SceneKind::Cylinder,
Kind::Sphere => SceneKind::Sphere,
Kind::TwoPlanes => SceneKind::TwoPlanes,
Kind::Corner => SceneKind::Corner,
Kind::TeeJoint => SceneKind::TeeJoint,
Kind::Corridor => SceneKind::Corridor,
}
}
}
impl Common {
fn prepare(&self) -> PrepareParams {
PrepareParams {
voxel: self.voxel,
neighbours: self.neighbours,
}
}
fn report(&self) -> ReportParams {
ReportParams {
noise: self.noise,
tolerance: self.tolerance,
calibration: self.calibration,
}
}
}
fn print_report(analysis: &Analysis, common: &Common) {
if common.calibration != 1.0 {
println!("empirical correction: ×{:.0}\n", common.calibration);
}
print!("{}", analysis.describe(&common.report().criteria()));
}
fn run() -> Result<(), Box<dyn Error>> {
match Cli::parse().command {
Command::Scene {
kind,
points,
scale,
noise,
shift,
turn,
out,
} => {
let scene = Scene::generate(
kind.into(),
SceneParams {
points_per_face: points,
scale,
noise_sigma: noise,
..SceneParams::default()
},
);
let offset = shift.unwrap_or_else(|| vec![0.0; 3]);
let angles = turn.unwrap_or_else(|| vec![0.0; 3]);
let motion = Se3::exp(&nalgebra::Vector6::new(
offset[0],
offset[1],
offset[2],
angles[0].to_radians(),
angles[1].to_radians(),
angles[2].to_radians(),
));
let written = if motion == Se3::identity() {
scene.cloud.clone()
} else {
println!(
"scene translated by [{:+.4}, {:+.4}, {:+.4}] m and rotated by \
[{:+.3}, {:+.3}, {:+.3}]°",
offset[0], offset[1], offset[2], angles[0], angles[1], angles[2]
);
transform_cloud(&scene.cloud, &motion)
};
rigidity_io::write(&written, &out)?;
println!("scene \"{}\": {} points", scene.kind.name(), scene.len());
println!(
"unobservable degrees of freedom by construction: {}",
scene.nullspace_dimension()
);
println!("written: {}", out.display());
}
Command::Analyse { cloud, common } => {
let prepared = prepare(&cloud, &common.prepare())?;
println!("points after downsampling: {}\n", prepared.len());
print_report(&analyse_cloud(&prepared)?, &common);
}
Command::Register {
source,
target,
common,
max_distance,
huber,
out,
} => {
let moving = prepare(&source, &common.prepare())?;
let fixed = prepare(&target, &common.prepare())?;
println!(
"source {} points, target {} points\n",
moving.len(),
fixed.len()
);
let params = RegisterParams {
max_distance,
huber,
..RegisterParams::default()
};
let result = register_pair(&moving, &fixed, ¶ms);
let translation = result.pose.translation();
let rotation = result.pose.rotation().log();
println!(
"translation: x = {:+8.4} m y = {:+8.4} m z = {:+8.4} m",
translation.x, translation.y, translation.z
);
println!(
"rotation: roll {:+7.3}° pitch {:+7.3}° yaw {:+7.3}°",
rotation.x.to_degrees(),
rotation.y.to_degrees(),
rotation.z.to_degrees()
);
println!(
"RMSE: {:.5} m correspondences: {} iterations: {} converged: {}\n",
result.rmse, result.correspondences, result.iterations, result.converged
);
let analysis = analyse_registration(&moving, &fixed, &result.pose, ¶ms)?;
print_report(&analysis, &common);
if let Some(path) = out {
rigidity_io::write(&transform_cloud(&moving.cloud, &result.pose), &path)?;
println!("\ntransformed source written: {}", path.display());
}
}
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("error: {message}");
ExitCode::FAILURE
}
}
}