Skip to main content

rigidity_scenes/
surfaces.rs

1//! Scene geometry and the derivation of the null spaces.
2
3use nalgebra::{Vector3, Vector6};
4use rigidity_core::PointCloud;
5
6use crate::rng::Rng;
7use crate::{Scene, SceneParams};
8
9/// Basis vector `index` in the ordering `ξ = [ρx, ρy, ρz, φx, φy, φz]`.
10fn basis(index: usize) -> Vector6<f64> {
11    let mut v = Vector6::zeros();
12    v[index] = 1.0;
13    v
14}
15
16/// The set of scenes whose answer is known.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SceneKind {
19    /// A single plane.
20    Plane,
21    /// The lateral surface of a cylinder.
22    Cylinder,
23    /// A sphere.
24    Sphere,
25    /// Two non-parallel planes.
26    TwoPlanes,
27    /// A trihedral corner.
28    Corner,
29    /// A T-shaped weld joint.
30    TeeJoint,
31    /// A long corridor: a floor and two walls.
32    Corridor,
33}
34
35impl SceneKind {
36    /// Every scene.
37    pub const ALL: [SceneKind; 7] = [
38        Self::Plane,
39        Self::Cylinder,
40        Self::Sphere,
41        Self::TwoPlanes,
42        Self::Corner,
43        Self::TeeJoint,
44        Self::Corridor,
45    ];
46
47    /// A name for messages.
48    pub fn name(self) -> &'static str {
49        match self {
50            Self::Plane => "plane",
51            Self::Cylinder => "cylinder",
52            Self::Sphere => "sphere",
53            Self::TwoPlanes => "two planes",
54            Self::Corner => "trihedral corner",
55            Self::TeeJoint => "tee joint",
56            Self::Corridor => "corridor",
57        }
58    }
59
60    /// The analytical basis of the null space in the canonical pose.
61    ///
62    /// The derivation for each scene, from the Jacobian row
63    /// `[nᵀ | (p × n)ᵀ]`:
64    ///
65    /// **Plane** `z = 0`, `n = (0,0,1)`, points `(a, b, 0)`. Here
66    /// `p × n = (b, −a, 0)`, and requiring `ρz + b·φx − a·φy = 0` for all
67    /// `a, b` gives `φx = φy = ρz = 0`. Free: `ρx`, `ρy`, `φz` — the
68    /// in-plane translations and rotation about the normal, three degrees
69    /// of freedom.
70    ///
71    /// **Cylinder** with axis `z`, points `(r cosθ, r sinθ, h)`,
72    /// `n = (cosθ, sinθ, 0)`. Here `p × n = (−h sinθ, h cosθ, 0)`, and
73    /// requiring `cosθ·(ρx + h·φy) + sinθ·(ρy − h·φx) = 0` for all `θ` and
74    /// at least two distinct `h` gives `ρx = ρy = φx = φy = 0`. Free: `ρz`
75    /// and `φz` — sliding along the axis and spinning about it.
76    ///
77    /// **Sphere** centred at the origin: `p = r·n`, hence
78    /// `p × n = r·(n × n) = 0`, so the rotational block of every row
79    /// vanishes identically. The normals cover all directions, so `ρ = 0`.
80    /// Free: all three rotations. Note that the answer depends on the
81    /// sphere's centre coinciding with the centre of rotation; a displaced
82    /// sphere has a different basis.
83    ///
84    /// **Two planes** `z = 0` and `x = 0`. The first gives
85    /// `φx = φy = ρz = 0`, the second `φy = φz = ρx = 0`. Together only
86    /// `ρy` is free: translation along the line of intersection.
87    ///
88    /// **Trihedral corner** adds the plane `y = 0` with the condition
89    /// `ρy = φx = φz = 0`. Nothing is left — full observability.
90    ///
91    /// **Tee joint** is the same pair of orthogonal planes as above, with
92    /// realistic extents. Translation along the seam is free.
93    ///
94    /// **Corridor**: the floor `z = 0` gives `ρz = φx = φy = 0`, and the
95    /// walls `x = ±s` give `ρx = φy = φz = 0`. Translation along the
96    /// corridor is free.
97    pub fn nullspace(self) -> Vec<Vector6<f64>> {
98        match self {
99            Self::Plane => vec![basis(0), basis(1), basis(5)],
100            Self::Cylinder => vec![basis(2), basis(5)],
101            Self::Sphere => vec![basis(3), basis(4), basis(5)],
102            Self::TwoPlanes | Self::TeeJoint | Self::Corridor => vec![basis(1)],
103            Self::Corner => Vec::new(),
104        }
105    }
106
107    /// Dimension of the null space.
108    pub fn nullspace_dimension(self) -> usize {
109        self.nullspace().len()
110    }
111}
112
113/// A rectangular patch of a plane.
114struct Patch {
115    center: Vector3<f64>,
116    u: Vector3<f64>,
117    v: Vector3<f64>,
118    half_u: f64,
119    half_v: f64,
120    normal: Vector3<f64>,
121}
122
123fn axes() -> (Vector3<f64>, Vector3<f64>, Vector3<f64>) {
124    (Vector3::x(), Vector3::y(), Vector3::z())
125}
126
127fn patches(kind: SceneKind, s: f64) -> Vec<Patch> {
128    let (x, y, z) = axes();
129    let patch = |center: Vector3<f64>,
130                 u: Vector3<f64>,
131                 v: Vector3<f64>,
132                 half_u: f64,
133                 half_v: f64,
134                 normal: Vector3<f64>| Patch {
135        center,
136        u,
137        v,
138        half_u,
139        half_v,
140        normal,
141    };
142
143    match kind {
144        SceneKind::Plane => vec![patch(Vector3::zeros(), x, y, s, s, z)],
145        SceneKind::TwoPlanes => vec![
146            patch(Vector3::zeros(), x, y, s, s, z),
147            patch(Vector3::new(0.0, 0.0, s), y, z, s, s, x),
148        ],
149        SceneKind::Corner => vec![
150            patch(Vector3::new(s, s, 0.0), x, y, s, s, z),
151            patch(Vector3::new(0.0, s, s), y, z, s, s, x),
152            patch(Vector3::new(s, 0.0, s), x, z, s, s, y),
153        ],
154        SceneKind::TeeJoint => vec![
155            // Flange and web: the weld seam runs along y.
156            patch(Vector3::zeros(), x, y, s, 4.0 * s, z),
157            patch(Vector3::new(0.0, 0.0, s), y, z, 4.0 * s, s, x),
158        ],
159        SceneKind::Corridor => vec![
160            patch(Vector3::zeros(), x, y, s, 10.0 * s, z),
161            patch(Vector3::new(-s, 0.0, s), y, z, 10.0 * s, s, x),
162            patch(Vector3::new(s, 0.0, s), y, z, 10.0 * s, s, -x),
163        ],
164        SceneKind::Cylinder | SceneKind::Sphere => Vec::new(),
165    }
166}
167
168fn sample_patch(
169    patch: &Patch,
170    count: usize,
171    rng: &mut Rng,
172    points: &mut Vec<Vector3<f64>>,
173    normals: &mut Vec<Vector3<f64>>,
174) {
175    for _ in 0..count {
176        let position = patch.center
177            + patch.u * (rng.symmetric() * patch.half_u)
178            + patch.v * (rng.symmetric() * patch.half_v);
179        points.push(position);
180        normals.push(patch.normal);
181    }
182}
183
184fn sample_cylinder(
185    scale: f64,
186    count: usize,
187    rng: &mut Rng,
188    points: &mut Vec<Vector3<f64>>,
189    normals: &mut Vec<Vector3<f64>>,
190) {
191    let radius = scale * 0.5;
192    for _ in 0..count {
193        let angle = std::f64::consts::TAU * rng.unit();
194        let height = rng.symmetric() * scale;
195        let normal = Vector3::new(angle.cos(), angle.sin(), 0.0);
196        points.push(Vector3::new(radius * normal.x, radius * normal.y, height));
197        normals.push(normal);
198    }
199}
200
201fn sample_sphere(
202    scale: f64,
203    count: usize,
204    rng: &mut Rng,
205    points: &mut Vec<Vector3<f64>>,
206    normals: &mut Vec<Vector3<f64>>,
207) {
208    for _ in 0..count {
209        // Three normal deviates give a uniform direction once normalised,
210        // unlike uniform angles, which crowd points near the poles.
211        let mut direction = Vector3::new(rng.normal(1.0), rng.normal(1.0), rng.normal(1.0));
212        while direction.norm() < 1e-12 {
213            direction = Vector3::new(rng.normal(1.0), rng.normal(1.0), rng.normal(1.0));
214        }
215        let normal = direction.normalize();
216        points.push(normal * scale);
217        normals.push(normal);
218    }
219}
220
221pub(crate) fn generate(kind: SceneKind, params: SceneParams) -> Scene {
222    let mut rng = Rng::new(params.seed);
223    let mut points: Vec<Vector3<f64>> = Vec::new();
224    let mut normals: Vec<Vector3<f64>> = Vec::new();
225
226    match kind {
227        SceneKind::Cylinder => sample_cylinder(
228            params.scale,
229            params.points_per_face,
230            &mut rng,
231            &mut points,
232            &mut normals,
233        ),
234        SceneKind::Sphere => sample_sphere(
235            params.scale,
236            params.points_per_face,
237            &mut rng,
238            &mut points,
239            &mut normals,
240        ),
241        _ => {
242            for patch in patches(kind, params.scale) {
243                sample_patch(
244                    &patch,
245                    params.points_per_face,
246                    &mut rng,
247                    &mut points,
248                    &mut normals,
249                );
250            }
251        }
252    }
253
254    if params.noise_sigma > 0.0 {
255        for point in &mut points {
256            *point += Vector3::new(
257                rng.normal(params.noise_sigma),
258                rng.normal(params.noise_sigma),
259                rng.normal(params.noise_sigma),
260            );
261        }
262    }
263
264    let inlier_count = points.len();
265
266    // An outlier is not a point displaced along its normal but a **wrong
267    // correspondence**: a point matched to the wrong piece of surface. The
268    // distinction is essential. Displacing along the normal changes the
269    // residual but not the Jacobian row: `(p + δn) × n = p × n`.
270    // Degeneracy is a property of the Jacobian alone, so an outlier must
271    // bring a wrong normal with it as well.
272    let outlier_count = (inlier_count as f64 * params.outlier_ratio).round() as usize;
273    if outlier_count > 0 {
274        let extent = params.outlier_extent * params.scale;
275        let (mut min, mut max) = (points[0], points[0]);
276        for point in &points {
277            min = min.inf(point);
278            max = max.sup(point);
279        }
280        for _ in 0..outlier_count {
281            let position = Vector3::new(
282                min.x - extent + rng.unit() * (max.x - min.x + 2.0 * extent),
283                min.y - extent + rng.unit() * (max.y - min.y + 2.0 * extent),
284                min.z - extent + rng.unit() * (max.z - min.z + 2.0 * extent),
285            );
286            let mut direction = Vector3::new(rng.normal(1.0), rng.normal(1.0), rng.normal(1.0));
287            while direction.norm() < 1e-12 {
288                direction = Vector3::new(rng.normal(1.0), rng.normal(1.0), rng.normal(1.0));
289            }
290            points.push(position);
291            normals.push(direction.normalize());
292        }
293    }
294
295    let mut cloud = PointCloud::with_capacity(points.len());
296    for point in &points {
297        cloud.push(*point);
298    }
299
300    Scene {
301        kind,
302        params,
303        points,
304        cloud,
305        normals,
306        inlier_count,
307    }
308}