cam_geom/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![deny(rust_2018_idioms, unsafe_code, missing_docs)]
3#![doc = include_str!("../README.md")]
4
5//! # Examples
6//!
7//! ## Example - projecting 3D world coordinates to 2D pixel coordinates.
8//!
9//! ```
10//! use cam_geom::*;
11//! use nalgebra::{Matrix2x3, Unit, Vector3};
12//!
13//! // Create two points in the world coordinate frame.
14//! let world_coords = Points::new(Matrix2x3::new(
15//!     1.0, 0.0, 0.0, // point 1
16//!     0.0, 1.0, 0.0, // point 2
17//! ));
18//!
19//! // perspective parameters - focal length of 100, no skew, pixel center at (640,480)
20//! let intrinsics = IntrinsicParametersPerspective::from(PerspectiveParams {
21//!     fx: 100.0,
22//!     fy: 100.0,
23//!     skew: 0.0,
24//!     cx: 640.0,
25//!     cy: 480.0,
26//! });
27//!
28//! // Set extrinsic parameters - camera at (10,0,0), looking at (0,0,0), up (0,0,1)
29//! let camcenter = Vector3::new(10.0, 0.0, 0.0);
30//! let lookat = Vector3::new(0.0, 0.0, 0.0);
31//! let up = Unit::new_normalize(Vector3::new(0.0, 0.0, 1.0));
32//! let pose = ExtrinsicParameters::from_view(&camcenter, &lookat, &up);
33//!
34//! // Create a `Camera` with both intrinsic and extrinsic parameters.
35//! let camera = Camera::new(intrinsics, pose);
36//!
37//! // Project the original 3D coordinates to 2D pixel coordinates.
38//! let pixel_coords = camera.world_to_pixel(&world_coords);
39//!
40//! // Print the results.
41//! for i in 0..world_coords.data.nrows() {
42//!     let wc = world_coords.data.row(i);
43//!     let pix = pixel_coords.data.row(i);
44//!     println!("{} -> {}", wc, pix);
45//! }
46//! ```
47//!
48//! This will print:
49//!
50//! ```text
51//!   ┌       ┐
52//!   │ 1 0 0 │
53//!   └       ┘
54//!
55//!  ->
56//!   ┌         ┐
57//!   │ 640 480 │
58//!   └         ┘
59//!
60//!
61//!
62//!   ┌       ┐
63//!   │ 0 1 0 │
64//!   └       ┘
65//!
66//!  ->
67//!   ┌         ┐
68//!   │ 650 480 │
69//!   └         ┘
70//! ```
71//!
72//!
73//! ## Example - intersection of rays
74//!
75//! ```
76//! use cam_geom::*;
77//! use nalgebra::RowVector3;
78//!
79//! // Create the first ray.
80//!     let ray1 = Ray::<WorldFrame, _>::new(
81//!     RowVector3::new(1.0, 0.0, 0.0), // origin
82//!     RowVector3::new(0.0, 1.0, 0.0), // direction
83//! );
84//!
85//! // Create the second ray.
86//! let ray2 = Ray::<WorldFrame, _>::new(
87//!     RowVector3::new(0.0, 1.0, 0.0), // origin
88//!     RowVector3::new(1.0, 0.0, 0.0), // direction
89//! );
90//!
91//! // Compute the best intersection.
92//! let result = best_intersection_of_rays(&[ray1, ray2]).unwrap();
93//!
94//! // Print the result.
95//! println!("result: {}", result.data);
96//! ```
97//!
98//! This will print:
99//!
100//! ```text
101//! result:
102//!   ┌       ┐
103//!   │ 1 1 0 │
104//!   └       ┘
105//! ```
106
107#[cfg(not(feature = "std"))]
108extern crate core as std;
109
110#[cfg(feature = "serde-serialize")]
111use serde::{Deserialize, Serialize};
112
113use nalgebra::{
114    allocator::Allocator,
115    storage::{Owned, Storage},
116    DefaultAllocator, Dim, DimName, Isometry3, Matrix, Point3, RealField, SMatrix, Vector3, U1, U2,
117    U3,
118};
119
120#[cfg(feature = "std")]
121pub mod intrinsic_test_utils;
122
123mod intrinsics_perspective;
124pub use intrinsics_perspective::{IntrinsicParametersPerspective, PerspectiveParams};
125
126mod intrinsics_orthographic;
127pub use intrinsics_orthographic::{IntrinsicParametersOrthographic, OrthographicParams};
128
129mod extrinsics;
130pub use extrinsics::ExtrinsicParameters;
131
132mod camera;
133pub use camera::Camera;
134
135/// Defines the different possible types of ray bundles.
136pub mod ray_bundle_types;
137
138#[cfg(feature = "alloc")]
139mod ray_intersection;
140#[cfg(feature = "alloc")]
141pub use ray_intersection::best_intersection_of_rays;
142
143pub mod linearize;
144
145/// All possible errors.
146#[cfg_attr(feature = "std", derive(Debug))]
147#[non_exhaustive]
148pub enum Error {
149    /// Invalid input.
150    InvalidInput,
151    /// Singular Value Decomposition did not converge.
152    SvdFailed,
153    /// At least two rays are needed to compute their intersection.
154    MinimumTwoRaysNeeded,
155    /// Invalid rotation matrix
156    InvalidRotationMatrix,
157}
158
159#[cfg(feature = "std")]
160impl std::error::Error for Error {}
161
162#[cfg(feature = "std")]
163impl std::fmt::Display for Error {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        std::fmt::Debug::fmt(self, f)
166    }
167}
168
169/// 2D pixel locations on the image sensor.
170///
171/// These pixels are "distorted" - with barrel and pincushion distortion - if
172/// the camera model incorporates such. (Undistorted pixels are handled
173/// internally within the camera model.)
174///
175/// This is a newtype wrapping an `nalgebra::Matrix`.
176#[derive(Clone)]
177pub struct Pixels<R: RealField, NPTS: Dim, STORAGE> {
178    /// The matrix storing pixel locations.
179    pub data: nalgebra::Matrix<R, NPTS, U2, STORAGE>,
180}
181
182impl<R: RealField, NPTS: Dim, STORAGE> Pixels<R, NPTS, STORAGE> {
183    /// Create a new Pixels instance
184    #[inline]
185    pub fn new(data: nalgebra::Matrix<R, NPTS, U2, STORAGE>) -> Self {
186        Self { data }
187    }
188}
189
190/// A coordinate system in which points and rays can be defined.
191pub trait CoordinateSystem {}
192
193/// Implementations of [`CoordinateSystem`](trait.CoordinateSystem.html).
194pub mod coordinate_system {
195
196    #[cfg(feature = "serde-serialize")]
197    use serde::{Deserialize, Serialize};
198
199    /// Coordinates in the camera coordinate system.
200    ///
201    /// The camera center is at (0,0,0) at looking at (0,0,1) with up as
202    /// (0,-1,0) in this coordinate frame.
203    #[derive(Debug, Clone, PartialEq)]
204    #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
205    pub struct CameraFrame {}
206    impl crate::CoordinateSystem for CameraFrame {}
207
208    /// Coordinates in the world coordinate system.
209    ///
210    /// The camera center is may be located at an arbitrary position and pointed
211    /// in an arbitrary direction in this coordinate frame.
212    #[derive(Debug, Clone, PartialEq)]
213    #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
214    pub struct WorldFrame {}
215    impl crate::CoordinateSystem for WorldFrame {}
216}
217pub use coordinate_system::{CameraFrame, WorldFrame};
218
219/// 3D points. Can be in any [`CoordinateSystem`](trait.CoordinateSystem.html).
220///
221/// This is a newtype wrapping an `nalgebra::Matrix`.
222pub struct Points<Coords: CoordinateSystem, R: RealField, NPTS: Dim, STORAGE> {
223    coords: std::marker::PhantomData<Coords>,
224    /// The matrix storing point locations.
225    pub data: nalgebra::Matrix<R, NPTS, U3, STORAGE>,
226}
227
228#[cfg(feature = "std")]
229impl<Coords: CoordinateSystem, R: RealField, NPTS: Dim, STORAGE: std::fmt::Debug> std::fmt::Debug
230    for Points<Coords, R, NPTS, STORAGE>
231{
232    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
233        f.debug_struct("Points")
234            .field("coords", &self.coords)
235            .field("data", &self.data)
236            .finish()
237    }
238}
239
240impl<Coords, R, NPTS, STORAGE> Points<Coords, R, NPTS, STORAGE>
241where
242    Coords: CoordinateSystem,
243    R: RealField,
244    NPTS: Dim,
245{
246    /// Create a new Points instance from the underlying storage.
247    #[inline]
248    pub fn new(data: nalgebra::Matrix<R, NPTS, U3, STORAGE>) -> Self {
249        Self {
250            coords: std::marker::PhantomData,
251            data,
252        }
253    }
254}
255
256/// 3D rays. Can be in any [`CoordinateSystem`](trait.CoordinateSystem.html).
257///
258/// Any given `RayBundle` will have a particular bundle type, which implements
259/// the [`Bundle`](trait.Bundle.html) trait.
260#[derive(Debug, Clone, PartialEq)]
261#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
262pub struct RayBundle<Coords, BType, R, NPTS, StorageMultiple>
263where
264    Coords: CoordinateSystem,
265    BType: Bundle<R>,
266    R: RealField,
267    NPTS: Dim,
268    StorageMultiple: Storage<R, NPTS, U3>,
269{
270    coords: std::marker::PhantomData<Coords>,
271    /// The matrix storing the ray data.
272    pub data: Matrix<R, NPTS, U3, StorageMultiple>,
273    bundle_type: BType,
274}
275
276impl<Coords, BType, R, NPTS, StorageMultiple> RayBundle<Coords, BType, R, NPTS, StorageMultiple>
277where
278    Coords: CoordinateSystem,
279    BType: Bundle<R>,
280    R: RealField,
281    NPTS: DimName,
282    StorageMultiple: Storage<R, NPTS, U3>,
283    DefaultAllocator: Allocator<NPTS, U3>,
284{
285    /// get directions of each ray in bundle
286    #[inline]
287    pub fn directions(&self) -> Matrix<R, NPTS, U3, Owned<R, NPTS, U3>> {
288        self.bundle_type.directions(&self.data)
289    }
290
291    /// get centers (origins) of each ray in bundle
292    #[inline]
293    pub fn centers(&self) -> Matrix<R, NPTS, U3, Owned<R, NPTS, U3>> {
294        self.bundle_type.centers(&self.data)
295    }
296}
297
298impl<Coords, BType, R> RayBundle<Coords, BType, R, U1, Owned<R, U1, U3>>
299where
300    Coords: CoordinateSystem,
301    BType: Bundle<R>,
302    R: RealField,
303{
304    /// Return the single ray from the RayBundle with exactly one ray.
305    #[inline]
306    pub fn to_single_ray(&self) -> Ray<Coords, R> {
307        self.bundle_type.to_single_ray(&self.data)
308    }
309}
310
311impl<Coords, R, NPTS, StorageMultiple>
312    RayBundle<Coords, crate::ray_bundle_types::SharedOriginRayBundle<R>, R, NPTS, StorageMultiple>
313where
314    Coords: CoordinateSystem,
315    R: RealField,
316    NPTS: Dim,
317    StorageMultiple: Storage<R, NPTS, U3>,
318{
319    /// Create a new RayBundle instance in which all rays share origin at zero.
320    ///
321    /// The number of points allocated is given by the `npts` parameter, which
322    /// should agree with the `NPTS` type. The coordinate system is given by the
323    /// `Coords` type.
324    pub fn new_shared_zero_origin(data: Matrix<R, NPTS, U3, StorageMultiple>) -> Self {
325        let bundle_type = crate::ray_bundle_types::SharedOriginRayBundle::new_shared_zero_origin();
326        Self::new(bundle_type, data)
327    }
328}
329
330impl<Coords, R, NPTS, StorageMultiple>
331    RayBundle<
332        Coords,
333        crate::ray_bundle_types::SharedDirectionRayBundle<R>,
334        R,
335        NPTS,
336        StorageMultiple,
337    >
338where
339    Coords: CoordinateSystem,
340    R: RealField,
341    NPTS: Dim,
342    StorageMultiple: Storage<R, NPTS, U3>,
343{
344    /// Create a new RayBundle instance in which all rays share +z direction.
345    ///
346    /// The number of points allocated is given by the `npts` parameter, which
347    /// should agree with the `NPTS` type. The coordinate system is given by the
348    /// `Coords` type.
349    pub fn new_shared_plusz_direction(data: Matrix<R, NPTS, U3, StorageMultiple>) -> Self {
350        let bundle_type =
351            crate::ray_bundle_types::SharedDirectionRayBundle::new_plusz_shared_direction();
352        Self::new(bundle_type, data)
353    }
354}
355
356impl<Coords, BType, R, NPTS, StorageMultiple> RayBundle<Coords, BType, R, NPTS, StorageMultiple>
357where
358    Coords: CoordinateSystem,
359    BType: Bundle<R>,
360    R: RealField,
361    NPTS: Dim,
362    StorageMultiple: Storage<R, NPTS, U3>,
363{
364    /// Create a new RayBundle instance from the underlying storage.
365    ///
366    /// The coordinate system is given by the `Coords` type and the bundle type
367    /// (e.g. shared origin or shared direction) is given by the `BType`.
368    #[inline]
369    fn new(bundle_type: BType, data: nalgebra::Matrix<R, NPTS, U3, StorageMultiple>) -> Self {
370        Self {
371            coords: std::marker::PhantomData,
372            data,
373            bundle_type,
374        }
375    }
376
377    /// get a 3D point on the ray, obtained by adding the direction(s) to the origin(s)
378    ///
379    /// The distance of the point from the ray bundle center is not definted and
380    /// can be arbitrary.
381    #[inline]
382    pub fn point_on_ray(&self) -> Points<Coords, R, NPTS, Owned<R, NPTS, U3>>
383    where
384        DefaultAllocator: Allocator<NPTS, U3>,
385    {
386        self.bundle_type.point_on_ray(&self.data)
387    }
388
389    /// get a 3D point on the ray at a defined distance from the origin(s)
390    #[inline]
391    pub fn point_on_ray_at_distance(
392        &self,
393        distance: R,
394    ) -> Points<Coords, R, NPTS, Owned<R, NPTS, U3>>
395    where
396        DefaultAllocator: Allocator<NPTS, U3>,
397    {
398        self.bundle_type
399            .point_on_ray_at_distance(&self.data, distance)
400    }
401
402    #[inline]
403    fn to_pose<OutFrame>(
404        &self,
405        pose: Isometry3<R>,
406    ) -> RayBundle<OutFrame, BType, R, NPTS, Owned<R, NPTS, U3>>
407    where
408        R: RealField,
409        NPTS: Dim,
410        OutFrame: CoordinateSystem,
411        DefaultAllocator: Allocator<NPTS, U3>,
412    {
413        self.bundle_type.to_pose(pose, &self.data)
414    }
415}
416
417/// A single ray. Can be in any [`CoordinateSystem`](trait.CoordinateSystem.html).
418///
419/// A `RayBundle` with only one ray can be converted to this with
420/// `RayBundle::to_single_ray()`.
421pub struct Ray<Coords, R: RealField> {
422    /// The center (origin) of the ray.
423    pub center: SMatrix<R, 1, 3>,
424    /// The direction of the ray.
425    pub direction: SMatrix<R, 1, 3>,
426    c: std::marker::PhantomData<Coords>,
427}
428
429impl<Coords, R: RealField> Ray<Coords, R> {
430    /// Create a new ray from center (origin) and direction.
431    #[inline]
432    pub fn new(center: SMatrix<R, 1, 3>, direction: SMatrix<R, 1, 3>) -> Self {
433        Self {
434            center,
435            direction,
436            c: std::marker::PhantomData,
437        }
438    }
439}
440
441/// Specifies operations which any RayBundle must implement.
442pub trait Bundle<R>
443where
444    R: RealField,
445{
446    /// Return a single ray from a `RayBundle` with exactly one ray.
447    fn to_single_ray<Coords>(&self, self_data: &SMatrix<R, 1, 3>) -> Ray<Coords, R>
448    where
449        Coords: CoordinateSystem;
450
451    /// Get directions of each ray in bundle.
452    ///
453    /// This can be inefficient, because when not every ray has a different
454    /// direction (which is the case for the `SharedDirectionRayBundle` type),
455    /// this will nevertheless copy the single direction `NPTS` times.
456    fn directions<NPTS, StorageIn>(
457        &self,
458        self_data: &Matrix<R, NPTS, U3, StorageIn>,
459    ) -> Matrix<R, NPTS, U3, Owned<R, NPTS, U3>>
460    where
461        NPTS: DimName,
462        StorageIn: Storage<R, NPTS, U3>,
463        DefaultAllocator: Allocator<NPTS, U3>;
464
465    /// Get centers of each ray in bundle.
466    ///
467    /// This can be inefficient, because when not every ray has a different
468    /// center (which is the case for the `SharedOriginRayBundle` type),
469    /// this will nevertheless copy the single center `NPTS` times.
470    fn centers<NPTS, StorageIn>(
471        &self,
472        self_data: &Matrix<R, NPTS, U3, StorageIn>,
473    ) -> Matrix<R, NPTS, U3, Owned<R, NPTS, U3>>
474    where
475        NPTS: DimName,
476        StorageIn: Storage<R, NPTS, U3>,
477        DefaultAllocator: Allocator<NPTS, U3>;
478
479    /// Return points on on the input rays.
480    ///
481    /// The distance of the point from the ray bundle center is not definted and
482    /// can be arbitrary.
483    fn point_on_ray<NPTS, StorageIn, OutFrame>(
484        &self,
485        self_data: &Matrix<R, NPTS, U3, StorageIn>,
486    ) -> Points<OutFrame, R, NPTS, Owned<R, NPTS, U3>>
487    where
488        Self: Sized,
489        NPTS: Dim,
490        StorageIn: Storage<R, NPTS, U3>,
491        OutFrame: CoordinateSystem,
492        DefaultAllocator: Allocator<NPTS, U3>;
493
494    /// Return points on on the input rays at a defined distance from the origin(s).
495    fn point_on_ray_at_distance<NPTS, StorageIn, OutFrame>(
496        &self,
497        self_data: &Matrix<R, NPTS, U3, StorageIn>,
498        distance: R,
499    ) -> Points<OutFrame, R, NPTS, Owned<R, NPTS, U3>>
500    where
501        Self: Sized,
502        NPTS: Dim,
503        StorageIn: Storage<R, NPTS, U3>,
504        OutFrame: CoordinateSystem,
505        DefaultAllocator: Allocator<NPTS, U3>;
506
507    /// Convert the input rays by the pose given.
508    fn to_pose<NPTS, StorageIn, OutFrame>(
509        &self,
510        pose: Isometry3<R>,
511        self_data: &Matrix<R, NPTS, U3, StorageIn>,
512    ) -> RayBundle<OutFrame, Self, R, NPTS, Owned<R, NPTS, U3>>
513    where
514        Self: Sized,
515        R: RealField,
516        NPTS: Dim,
517        StorageIn: Storage<R, NPTS, U3>,
518        OutFrame: CoordinateSystem,
519        DefaultAllocator: Allocator<NPTS, U3>;
520}
521
522/// A geometric model of camera coordinates to pixels (and vice versa).
523pub trait IntrinsicParameters<R>: std::fmt::Debug + Clone
524where
525    R: RealField,
526{
527    /// What type of ray bundle is returned when projecting pixels to rays.
528    type BundleType;
529
530    /// project pixels to camera coords
531    fn pixel_to_camera<IN, NPTS>(
532        &self,
533        pixels: &Pixels<R, NPTS, IN>,
534    ) -> RayBundle<coordinate_system::CameraFrame, Self::BundleType, R, NPTS, Owned<R, NPTS, U3>>
535    where
536        Self::BundleType: Bundle<R>,
537        IN: Storage<R, NPTS, U2>,
538        NPTS: Dim,
539        DefaultAllocator: Allocator<U1, U2>, // needed to make life easy for implementors
540        DefaultAllocator: Allocator<NPTS, U2>, // needed to make life easy for implementors
541        DefaultAllocator: Allocator<NPTS, U3>;
542
543    /// project camera coords to pixel coordinates
544    fn camera_to_pixel<IN, NPTS>(
545        &self,
546        camera: &Points<coordinate_system::CameraFrame, R, NPTS, IN>,
547    ) -> Pixels<R, NPTS, Owned<R, NPTS, U2>>
548    where
549        IN: Storage<R, NPTS, U3>,
550        NPTS: Dim,
551        DefaultAllocator: Allocator<NPTS, U2>;
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557    use nalgebra::convert;
558
559    #[cfg(not(feature = "std"))]
560    compile_error!("tests require std");
561
562    #[test]
563    fn rays_shared_origin() {
564        // Create rays in world coorindates all with a shared origin at zero.
565        let b1 =
566            RayBundle::<WorldFrame, _, _, _, _>::new_shared_zero_origin(SMatrix::<_, 2, 3>::new(
567                1.0, 2.0, 3.0, // ray 1
568                4.0, 5.0, 6.0, // ray 2
569            ));
570
571        // Get points on rays at a specific distance.
572        let actual_dist1 = b1.point_on_ray_at_distance(1.0).data;
573
574        {
575            // Manually compte what these points should be.
576            let r1m = (1.0_f64 + 4.0 + 9.0).sqrt();
577            let r2m = (16.0_f64 + 25.0 + 36.0).sqrt();
578            let expected = SMatrix::<_, 2, 3>::new(
579                1.0 / r1m,
580                2.0 / r1m,
581                3.0 / r1m, // ray 1
582                4.0 / r2m,
583                5.0 / r2m,
584                6.0 / r2m, // ray 2
585            );
586
587            // Check the points vs the manually computed versions.
588            approx::assert_abs_diff_eq!(actual_dist1, expected, epsilon = 1e-10);
589        }
590
591        // Get points on rays at a specific distance.
592        let actual_dist10 = b1.point_on_ray_at_distance(10.0).data;
593
594        // Get points on rays at arbitrary distance.
595        let actual = b1.point_on_ray().data;
596
597        for i in 0..actual_dist1.nrows() {
598            assert_on_line(actual_dist1.row(i), actual_dist10.row(i), actual.row(i));
599        }
600    }
601
602    #[test]
603    fn rays_shared_direction() {
604        // Create rays in world coorindates all with a shared direction (+z).
605        let b1 = RayBundle::<WorldFrame, _, _, _, _>::new_shared_plusz_direction(
606            SMatrix::<_, 2, 3>::new(
607                1.0, 2.0, 0.0, // ray 1
608                3.0, 4.0, 0.0, // ray 2
609            ),
610        );
611
612        // Get points on rays at a specific distance.
613        let actual_dist10 = b1.point_on_ray_at_distance(10.0).data;
614
615        {
616            // Manually compte what these points should be.
617            let expected_dist10 = SMatrix::<_, 2, 3>::new(
618                1.0, 2.0, 10.0, // ray 1
619                3.0, 4.0, 10.0, // ray 2
620            );
621
622            // Check the points vs the manually computed versions.
623            approx::assert_abs_diff_eq!(actual_dist10, expected_dist10, epsilon = 1e-10);
624        }
625
626        // Get points on rays at a specific distance.
627        let actual_dist0 = b1.point_on_ray_at_distance(0.0).data;
628
629        {
630            // Manually compte what these points should be.
631            let expected_dist0 = SMatrix::<_, 2, 3>::new(
632                1.0, 2.0, 0.0, // ray 1
633                3.0, 4.0, 0.0, // ray 2
634            );
635
636            // Check the points vs the manually computed versions.
637            approx::assert_abs_diff_eq!(actual_dist0, expected_dist0, epsilon = 1e-10);
638        }
639
640        // Get points on rays at arbitrary distance.
641        let actual = b1.point_on_ray().data;
642
643        for i in 0..actual_dist0.nrows() {
644            assert_on_line(actual_dist0.row(i), actual_dist10.row(i), actual.row(i));
645        }
646    }
647
648    fn assert_on_line<R, S1, S2, S3>(
649        line_a: Matrix<R, U1, U3, S1>,
650        line_b: Matrix<R, U1, U3, S2>,
651        test_pt: Matrix<R, U1, U3, S3>,
652    ) where
653        R: RealField,
654        S1: Storage<R, U1, U3>,
655        S2: Storage<R, U1, U3>,
656        S3: Storage<R, U1, U3>,
657    {
658        let dir = &line_b - &line_a;
659        let testx = &test_pt - &line_a;
660        let mag_dir = (dir[0].clone() * dir[0].clone()
661            + dir[1].clone() * dir[1].clone()
662            + dir[2].clone() * dir[2].clone())
663        .sqrt();
664        let mag_testx = (testx[0].clone() * testx[0].clone()
665            + testx[1].clone() * testx[1].clone()
666            + testx[2].clone() * testx[2].clone())
667        .sqrt();
668        let scale = mag_dir / mag_testx;
669
670        for j in 0..3 {
671            approx::assert_abs_diff_eq!(
672                testx[j].clone() * scale.clone(),
673                dir[j].clone(),
674                epsilon = convert(1e-10)
675            );
676        }
677    }
678
679    #[test]
680    #[cfg(feature = "serde-serialize")]
681    fn test_ray_bundle_serde() {
682        let expected =
683            RayBundle::<WorldFrame, _, _, _, _>::new_shared_plusz_direction(
684                SMatrix::<_, 2, 3>::new(
685                    1.0, 2.0, 0.0, // ray 1
686                    3.0, 4.0, 0.0, // ray 2
687                ),
688            );
689
690        let buf = serde_json::to_string(&expected).unwrap();
691        let actual: RayBundle<_, _, _, _, _> = serde_json::from_str(&buf).unwrap();
692        assert!(expected == actual);
693    }
694}