Skip to main content

rigidity_scenes/
lib.rs

1//! Synthetic scenes with analytically known null spaces.
2//!
3//! These are the project's measuring standard. The degeneracy detector is
4//! not judged by eye but against an answer derived by hand: for every
5//! scene it is known in advance how many degrees of freedom the geometry
6//! fails to determine, and which ones.
7//!
8//! # Convention
9//!
10//! Null spaces are written in `ξ = [ρ; φ]` coordinates and refer to
11//! rotation about the **coordinate origin**. Scenes are built in a
12//! canonical pose where the answer takes a simple form. Moving a scene by
13//! a transform `T` maps the null space to `Adj(T)·v` — checked by its own
14//! test, which incidentally confirms the adjoint implementation.
15//!
16//! # What "known null space" means
17//!
18//! A row of the point-to-plane Jacobian is `[nᵀ | (p × n)ᵀ]`. A vector `v`
19//! is unobservable when `[nᵀ | (p × n)ᵀ]·v = 0` for **every** point of the
20//! scene. That condition is solved by hand below for each surface.
21
22pub mod rng;
23mod surfaces;
24
25use nalgebra::{Vector3, Vector6};
26use rigidity_core::PointCloud;
27
28pub use surfaces::SceneKind;
29
30/// Generation parameters.
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct SceneParams {
33    /// Points per face (or over the whole surface, for the cylinder and
34    /// the sphere).
35    pub points_per_face: usize,
36    /// Characteristic size of the scene, metres.
37    pub scale: f64,
38    /// Standard deviation of isotropic Gaussian noise on positions.
39    ///
40    /// Isotropic rather than along the normal: the latter is a model that
41    /// happens to suit point-to-plane, and is less honest about a real
42    /// sensor.
43    pub noise_sigma: f64,
44    /// Fraction of outliers among the points.
45    pub outlier_ratio: f64,
46    /// How far an outlier strays from the surface, in units of `scale`.
47    pub outlier_extent: f64,
48    /// Generator seed.
49    pub seed: u64,
50}
51
52impl Default for SceneParams {
53    fn default() -> Self {
54        Self {
55            points_per_face: 2_000,
56            scale: 1.0,
57            noise_sigma: 0.0,
58            outlier_ratio: 0.0,
59            outlier_extent: 0.25,
60            seed: 0x1234_5678_9ABC_DEF0,
61        }
62    }
63}
64
65/// A generated scene.
66#[derive(Debug, Clone)]
67pub struct Scene {
68    /// Which surface.
69    pub kind: SceneKind,
70    /// The parameters it was built with.
71    pub params: SceneParams,
72    /// Exact point coordinates in `f64`.
73    ///
74    /// The scene is a mathematical object; the cloud is its in-memory
75    /// representation. Keeping them apart is mandatory, because `f32`
76    /// storage destroys exact identities on curved surfaces. On a cylinder
77    /// `(p × n)_z` is identically zero, yet after quantisation it becomes
78    /// about 3·10⁻⁸ of the scene scale. Analytical claims are checked
79    /// against this field; all processing runs on [`cloud`](Self::cloud).
80    pub points: Vec<Vector3<f64>>,
81    /// The points in working form: `f32` relative to an origin.
82    pub cloud: PointCloud,
83    /// The exact analytical normal at each point.
84    ///
85    /// Analytical, not estimated from neighbours: normal-estimation error
86    /// is a separate source of inaccuracy and must not be mixed into
87    /// geometric degeneracy.
88    pub normals: Vec<Vector3<f64>>,
89    /// The first `inlier_count` points lie on the surface; the rest are
90    /// outliers.
91    ///
92    /// The null-space guarantee applies to inliers only: an outlier is not
93    /// on the surface, and the declared vector does not annihilate its
94    /// Jacobian row.
95    pub inlier_count: usize,
96}
97
98impl Scene {
99    /// Builds the scene.
100    pub fn generate(kind: SceneKind, params: SceneParams) -> Self {
101        surfaces::generate(kind, params)
102    }
103
104    /// The analytical basis of the null space.
105    pub fn nullspace(&self) -> Vec<Vector6<f64>> {
106        self.kind.nullspace()
107    }
108
109    /// Dimension of the null space.
110    pub fn nullspace_dimension(&self) -> usize {
111        self.kind.nullspace_dimension()
112    }
113
114    /// Number of points.
115    pub fn len(&self) -> usize {
116        self.cloud.len()
117    }
118
119    /// Whether the scene is empty.
120    pub fn is_empty(&self) -> bool {
121        self.cloud.is_empty()
122    }
123
124    /// Splits the scene into two partially overlapping clouds.
125    ///
126    /// A fraction `overlap` of the points goes into both clouds; the rest
127    /// is divided evenly. Needed for source/target pairs in registration:
128    /// full overlap is an unrealistic and far too easy case.
129    pub fn split_with_overlap(&self, overlap: f64, seed: u64) -> (PointCloud, PointCloud) {
130        let overlap = overlap.clamp(0.0, 1.0);
131        let exclusive = (1.0 - overlap) * 0.5;
132        let mut rng = rng::Rng::new(seed);
133        let mut source = PointCloud::with_origin(self.cloud.origin());
134        let mut target = PointCloud::with_origin(self.cloud.origin());
135        for i in 0..self.cloud.len() {
136            let point = self.cloud.point(i);
137            let draw = rng.unit();
138            if draw < overlap {
139                source.push(point);
140                target.push(point);
141            } else if draw < overlap + exclusive {
142                source.push(point);
143            } else {
144                target.push(point);
145            }
146        }
147        (source, target)
148    }
149}