Skip to main content

braid_mvg/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Camera geometry and multi-view geometry (MVG) types and algorithms for the
5//! [Braid](https://strawlab.org/braid) tracking system.
6//!
7//! This crate provides camera modeling, geometric transformations, and
8//! multi-camera system support for 3D computer vision applications. It's
9//! specifically designed for use in the Braid multi-camera tracking system but
10//! can be used for general computer vision tasks.
11//!
12//! ## Features
13//!
14//! - Camera modeling with intrinsic and extrinsic parameters based on
15//!   [`cam-geom`](https://docs.rs/cam-geom)
16//! - Lens distortion correction using OpenCV-compatible models based on
17//!   [`opencv-ros-camera`](https://docs.rs/opencv-ros-camera)
18//! - Multi-camera system management and calibration
19//! - 3D point triangulation from multiple camera views
20//! - Point alignment algorithms (Kabsch-Umeyama, robust Arun)
21//! - Coordinate frame transformations between world, camera, and pixel spaces
22//! - [rerun.io](https://rerun.io) integration for 3D visualization (optional)
23//!
24//! ## Core Types
25//!
26//! - [`Camera`]: Individual camera with intrinsics and extrinsics
27//! - [`MultiCameraSystem`]: Collection of calibrated cameras
28//! - [`DistortedPixel`], [`UndistortedPixel`]: Pixel coordinate types
29//! - [`PointWorldFrame`], [`PointCameraFrame`]: 3D point types in different
30//!   coordinate systems
31//!
32//! ## Coordinate Systems
33//!
34//! The crate uses three main coordinate systems:
35//! - **World Frame**: Global 3D coordinate system
36//! - **Camera Frame**: 3D coordinates relative to individual cameras
37//! - **Pixel Coordinates**: 2D image coordinates (distorted and undistorted)
38//!
39//! ## Example
40//!
41//! This example demonstrates a complete round-trip workflow: projecting a 3D point
42//! to 2D pixel coordinates in each camera, then reconstructing the 3D point from
43//! these 2D observations and comparing it to the original.
44//!
45//! ```rust
46//! use braid_mvg::{Camera, MultiCameraSystem, PointWorldFrame, extrinsics, make_default_intrinsics};
47//! use std::collections::BTreeMap;
48//! use nalgebra::Point3;
49//!
50//! // Create a multi-camera system with two cameras for triangulation
51//!
52//! // Camera 1: use default extrinsics (positioned at (1,2,3))
53//! let extrinsics1 = extrinsics::make_default_extrinsics::<f64>();
54//! let camera1 = Camera::new(640, 480, extrinsics1, make_default_intrinsics()).unwrap();
55//!
56//! // Camera 2: positioned with sufficient baseline for triangulation
57//! let translation2 = nalgebra::Point3::new(3.0, 2.0, 3.0);
58//! let rotation2 = nalgebra::UnitQuaternion::identity();
59//! let extrinsics2 = extrinsics::from_rquat_translation(rotation2, translation2);
60//! let camera2 = Camera::new(640, 480, extrinsics2, make_default_intrinsics()).unwrap();
61//!
62//! // Build the multi-camera system
63//! let mut cameras = BTreeMap::new();
64//! cameras.insert("cam1".to_string(), camera1);
65//! cameras.insert("cam2".to_string(), camera2);
66//! let system = MultiCameraSystem::new(cameras);
67//!
68//! // Define an original 3D point in world coordinates
69//! // Place it in front of both cameras at a reasonable distance
70//! let original_point = PointWorldFrame {
71//!     coords: Point3::new(2.0, 2.0, 8.0)
72//! };
73//! println!("Original 3D point: {:?}", original_point.coords);
74//!
75//! // Step 1: Project the 3D point to 2D pixels in each camera
76//! let mut observations = Vec::new();
77//! println!("\nProjecting to 2D pixels:");
78//! for (cam_name, camera) in system.cams_by_name() {
79//!     let pixel = camera.project_3d_to_pixel(&original_point);
80//!     println!("  {}: pixel ({:.2}, {:.2})", cam_name, pixel.coords.x, pixel.coords.y);
81//!     observations.push((cam_name.clone(), pixel));
82//! }
83//!
84//! // Step 2: Reconstruct the 3D point from the 2D observations
85//! println!("\nReconstructing 3D point from 2D observations...");
86//! let reconstructed_point = system.find3d(&observations)
87//!     .expect("Triangulation should succeed with good observations");
88//!
89//! // Step 3: Compare original and reconstructed points
90//! println!("Reconstructed 3D point: {:?}", reconstructed_point.coords);
91//!
92//! let error = (original_point.coords - reconstructed_point.coords).norm();
93//! println!("3D reconstruction error: {error:.2e}");
94//!
95//! // With perfect cameras and no noise, reconstruction should be very accurate
96//! assert!(error < 1e-6, "Reconstruction error too large: {error:.2e}");
97//!
98//! // Verify that reprojection works correctly
99//! println!("\nVerifying reprojection accuracy:");
100//! for (cam_name, camera) in system.cams_by_name() {
101//!     let reprojected_pixel = camera.project_3d_to_pixel(&reconstructed_point);
102//!     let original_pixel = camera.project_3d_to_pixel(&original_point);
103//!     let pixel_error = (reprojected_pixel.coords - original_pixel.coords).norm();
104//!     println!("  {cam_name}: reprojection error {pixel_error:.2e} pixels");
105//!     assert!(pixel_error < 1e-6, "Reprojection error too large for {cam_name}");
106//! }
107//!
108//! println!("āœ“ Round-trip 3D→2D→3D reconstruction successful!");
109//! ```
110#![deny(rust_2018_idioms)]
111#![warn(missing_docs)]
112#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
113use thiserror::Error;
114
115use nalgebra as na;
116use nalgebra::geometry::{Point2, Point3};
117use nalgebra::{Dim, RealField, U1, U2, U3};
118
119use cam_geom::ExtrinsicParameters;
120use opencv_ros_camera::{Distortion, RosOpenCvIntrinsics};
121
122/// Error types that can occur during multi-view geometry operations.
123#[derive(Error, Debug)]
124pub enum MvgError {
125    /// Unknown or unsupported lens distortion model encountered.
126    ///
127    /// This error occurs when trying to work with a camera distortion model
128    /// that is not supported by the current implementation.
129    #[error("unknown distortion model")]
130    UnknownDistortionModel,
131    /// Rectification matrix is not supported by the current implementation.
132    ///
133    /// Rectification matrices are used in stereo vision but are not fully
134    /// supported by all operations in this crate.
135    #[error("rectification matrix not supported")]
136    RectificationMatrixNotSupported,
137    /// Insufficient points provided for the geometric operation.
138    ///
139    /// Many operations like triangulation require a minimum number of observations.
140    /// For example, 3D triangulation needs at least 2 camera views.
141    #[error("not enough points")]
142    NotEnoughPoints,
143    /// Invalid matrix or array dimensions for the operation.
144    ///
145    /// This occurs when input data has incompatible dimensions for the
146    /// requested mathematical operation.
147    #[error("invalid shape")]
148    InvalidShape,
149    /// Camera name not found in the multi-camera system.
150    ///
151    /// Thrown when referencing a camera by name that doesn't exist in the
152    /// current [`MultiCameraSystem`].
153    #[error("unknown camera")]
154    UnknownCamera,
155    /// Singular Value Decomposition failed during matrix operations.
156    ///
157    /// This can occur during camera calibration or 3D reconstruction when
158    /// the input data is degenerate or ill-conditioned.
159    #[error("SVD failed")]
160    SvdFailed,
161    /// Generic parsing error for configuration files or data formats.
162    #[error("Parsing error")]
163    ParseError,
164    /// Invalid rotation matrix (not orthogonal or determinant ≠ 1).
165    ///
166    /// Rotation matrices must be orthogonal with determinant +1 to represent
167    /// valid 3D rotations.
168    #[error("invalid rotation matrix")]
169    InvalidRotationMatrix,
170    /// Unsupported file format or schema version.
171    #[error("unsupported version")]
172    UnsupportedVersion,
173    /// Invalid rectification matrix parameters.
174    #[error("invalid rect matrix")]
175    InvalidRectMatrix,
176    /// Unsupported camera or parameter type.
177    #[error("unsupported type")]
178    UnsupportedType,
179    /// Rerun.io does not support this camera intrinsics model.
180    ///
181    /// Only available when the `rerun-io` feature is enabled.
182    /// Some complex distortion models cannot be exported to rerun.io format.
183    #[cfg(feature = "rerun-io")]
184    #[error("rerun does not support this model of camera intrinsics")]
185    RerunUnsupportedIntrinsics,
186    /// Multiple valid mathematical roots found where only one was expected.
187    ///
188    /// This can occur in polynomial root-finding algorithms used in
189    /// geometric computations.
190    #[error("multiple valid roots found")]
191    MultipleValidRootsFound,
192    /// No valid mathematical root found for the equation.
193    ///
194    /// This indicates that the geometric problem has no solution with
195    /// the given constraints.
196    #[error("no valid root found")]
197    NoValidRootFound,
198    /// I/O error during file operations.
199    #[error("IO error: {source}")]
200    Io {
201        /// The underlying I/O error.
202        #[from]
203        source: std::io::Error,
204    },
205    /// YAML serialization/deserialization error.
206    #[error("serde_yaml error: {source}")]
207    SerdeYaml {
208        /// The underlying YAML parsing error.
209        #[from]
210        source: serde_yaml::Error,
211    },
212    /// JSON serialization/deserialization error.
213    #[error("serde_json error: {source}")]
214    SerdeJson {
215        /// The underlying JSON parsing error.
216        #[from]
217        source: serde_json::Error,
218    },
219    /// SVG rendering or processing error.
220    #[error("SvgError: {}", error)]
221    SvgError {
222        /// The SVG error message.
223        error: &'static str,
224    },
225    /// Pseudo-inverse calculation error.
226    #[error("PinvError: {}", error)]
227    PinvError {
228        /// The pseudo-inverse error message.
229        error: String,
230    },
231    /// Error from the [`cam-geom`](https://docs.rs/cam-geom) crate.
232    #[error("cam_geom::Error: {source}")]
233    CamGeomError {
234        /// The underlying cam-geom error.
235        #[from]
236        source: cam_geom::Error,
237    },
238    /// Error from the [`opencv-ros-camera`](https://docs.rs/opencv-ros-camera) crate.
239    #[error("opencv_ros_camera::Error: {source}")]
240    OpencvRosError {
241        /// The underlying opencv-ros-camera error.
242        #[from]
243        source: opencv_ros_camera::Error,
244    },
245}
246
247/// Convenience type alias for results in multi-view geometry operations.
248pub type Result<M> = std::result::Result<M, MvgError>;
249
250pub mod pymvg_support;
251
252/// Camera intrinsic parameter utilities and operations.
253///
254/// This module centers around a convenience additions to
255/// [`opencv_ros_camera::RosOpenCvIntrinsics`].
256pub mod intrinsics;
257
258/// Camera extrinsic parameter utilities and factory functions.
259///
260/// This module provides functions for creating camera extrinsic parameters,
261/// centered around a convenience additions to
262/// [`cam_geom::ExtrinsicParameters`].`
263pub mod extrinsics;
264
265/// Point cloud alignment algorithms and utilities.
266///
267/// This module implements various algorithms for aligning point clouds and
268/// coordinate systems, including Kabsch-Umeyama and robust Arun methods.
269/// These are commonly used in camera calibration and 3D reconstruction.
270pub mod align_points;
271
272/// Integration with [rerun.io](https://rerun.io) for 3D visualization.
273///
274/// This module provides conversion utilities between braid-mvg types and
275/// rerun.io data structures, enabling 3D visualization of camera systems,
276/// point clouds, and tracking results.
277///
278/// **Note**: This module is only available when the `rerun-io` feature is enabled.
279#[cfg(feature = "rerun-io")]
280#[cfg_attr(docsrs, doc(cfg(feature = "rerun-io")))]
281pub mod rerun_io;
282
283mod camera;
284pub use crate::camera::{Camera, rq_decomposition};
285
286mod multi_cam_system;
287pub use crate::multi_cam_system::MultiCameraSystem;
288
289/// A 2D pixel coordinate in the distorted image space.
290///
291/// This represents pixel coordinates as they appear in the raw camera image,
292/// including the effects of lens distortion.
293#[derive(Debug, Clone)]
294pub struct DistortedPixel<R: RealField + Copy> {
295    /// The 2D pixel coordinates (x, y) in the distorted image.
296    pub coords: Point2<R>,
297}
298
299impl<R, IN> From<&cam_geom::Pixels<R, U1, IN>> for DistortedPixel<R>
300where
301    R: RealField + Copy,
302    IN: nalgebra::storage::Storage<R, U1, U2>,
303{
304    fn from(orig: &cam_geom::Pixels<R, U1, IN>) -> Self {
305        DistortedPixel {
306            coords: Point2::new(orig.data[(0, 0)], orig.data[(0, 1)]),
307        }
308    }
309}
310
311impl<R, IN> From<cam_geom::Pixels<R, U1, IN>> for DistortedPixel<R>
312where
313    R: RealField + Copy,
314    IN: nalgebra::storage::Storage<R, U1, U2>,
315{
316    fn from(orig: cam_geom::Pixels<R, U1, IN>) -> Self {
317        let orig_ref = &orig;
318        orig_ref.into()
319    }
320}
321
322impl<R> From<&DistortedPixel<R>> for cam_geom::Pixels<R, U1, na::storage::Owned<R, U1, U2>>
323where
324    R: RealField + Copy,
325    na::DefaultAllocator: na::allocator::Allocator<U1, U2>,
326{
327    fn from(orig: &DistortedPixel<R>) -> Self {
328        Self {
329            data: na::OMatrix::<R, U1, U2>::from_row_slice(&[orig.coords[0], orig.coords[1]]),
330        }
331    }
332}
333
334impl<R: RealField + Copy> DistortedPixel<R> {
335    /// Extract a single distorted pixel from a collection of pixels.
336    ///
337    /// This method allows you to extract one pixel coordinate from a larger
338    /// collection of pixel coordinates, such as those returned by batch
339    /// projection operations.
340    ///
341    /// # Arguments
342    ///
343    /// * `pixels` - A collection of pixel coordinates
344    /// * `i` - The index of the pixel to extract (0-based)
345    ///
346    /// # Example
347    ///
348    /// ```rust
349    /// use braid_mvg::DistortedPixel;
350    /// use cam_geom::Pixels;
351    /// use nalgebra::{Point2, OMatrix, U2};
352    ///
353    /// // This example would work with actual cam_geom::Pixels data
354    /// // let pixels = /* some cam_geom::Pixels instance */;
355    /// // let first_pixel = DistortedPixel::from_pixels(&pixels, 0);
356    /// ```
357    pub fn from_pixels<NPTS, IN>(pixels: &cam_geom::Pixels<R, NPTS, IN>, i: usize) -> Self
358    where
359        NPTS: Dim,
360        IN: nalgebra::storage::Storage<R, NPTS, U2>,
361    {
362        DistortedPixel {
363            coords: Point2::new(pixels.data[(i, 0)], pixels.data[(i, 1)]),
364        }
365    }
366}
367
368/// A 2D pixel coordinate in the undistorted (rectified) image space.
369///
370/// This represents pixel coordinates after lens distortion has been removed,
371/// corresponding to an ideal pinhole camera model.
372#[derive(Debug, Clone)]
373pub struct UndistortedPixel<R: RealField + Copy> {
374    /// The 2D pixel coordinates (x, y) in the undistorted image.
375    pub coords: Point2<R>,
376}
377
378impl<R, IN> From<&opencv_ros_camera::UndistortedPixels<R, U1, IN>> for UndistortedPixel<R>
379where
380    R: RealField + Copy,
381    IN: nalgebra::storage::Storage<R, U1, U2>,
382{
383    fn from(orig: &opencv_ros_camera::UndistortedPixels<R, U1, IN>) -> Self {
384        UndistortedPixel {
385            coords: Point2::new(orig.data[(0, 0)], orig.data[(0, 1)]),
386        }
387    }
388}
389
390impl<R, IN> From<opencv_ros_camera::UndistortedPixels<R, U1, IN>> for UndistortedPixel<R>
391where
392    R: RealField + Copy,
393    IN: nalgebra::storage::Storage<R, U1, U2>,
394{
395    fn from(orig: opencv_ros_camera::UndistortedPixels<R, U1, IN>) -> Self {
396        let orig_ref = &orig;
397        orig_ref.into()
398    }
399}
400
401impl<R> From<&UndistortedPixel<R>>
402    for opencv_ros_camera::UndistortedPixels<R, U1, na::storage::Owned<R, U1, U2>>
403where
404    R: RealField + Copy,
405    na::DefaultAllocator: na::allocator::Allocator<U1, U2>,
406{
407    fn from(orig: &UndistortedPixel<R>) -> Self {
408        Self {
409            data: na::OMatrix::<R, U1, U2>::from_row_slice(&[orig.coords[0], orig.coords[1]]),
410        }
411    }
412}
413
414/// A 3D point in the camera coordinate frame.
415///
416/// This represents a 3D point in the coordinate system of a specific camera.
417#[derive(Debug, Clone)]
418pub struct PointCameraFrame<R: RealField + Copy> {
419    /// The 3D coordinates (x, y, z) in the camera reference frame.
420    pub coords: Point3<R>,
421}
422
423impl<R, IN> From<&cam_geom::Points<cam_geom::coordinate_system::CameraFrame, R, U1, IN>>
424    for PointCameraFrame<R>
425where
426    R: RealField + Copy,
427    IN: nalgebra::storage::Storage<R, U1, U3>,
428{
429    fn from(orig: &cam_geom::Points<cam_geom::coordinate_system::CameraFrame, R, U1, IN>) -> Self {
430        PointCameraFrame {
431            coords: Point3::new(orig.data[(0, 0)], orig.data[(0, 1)], orig.data[(0, 2)]),
432        }
433    }
434}
435
436impl<R, IN> From<cam_geom::Points<cam_geom::coordinate_system::CameraFrame, R, U1, IN>>
437    for PointCameraFrame<R>
438where
439    R: RealField + Copy,
440    IN: nalgebra::storage::Storage<R, U1, U3>,
441{
442    fn from(orig: cam_geom::Points<cam_geom::coordinate_system::CameraFrame, R, U1, IN>) -> Self {
443        let orig_ref = &orig;
444        orig_ref.into()
445    }
446}
447
448impl<R> From<&PointCameraFrame<R>>
449    for cam_geom::Points<
450        cam_geom::coordinate_system::CameraFrame,
451        R,
452        U1,
453        na::storage::Owned<R, U1, U3>,
454    >
455where
456    R: RealField + Copy,
457    na::DefaultAllocator: na::allocator::Allocator<U1, U2>,
458{
459    fn from(orig: &PointCameraFrame<R>) -> Self {
460        Self::new(na::OMatrix::<R, U1, U3>::new(
461            orig.coords[0],
462            orig.coords[1],
463            orig.coords[2],
464        ))
465    }
466}
467
468/// A 3D point in the world coordinate frame.
469///
470/// This represents a 3D point in a global coordinate system that is independent
471/// of any specific camera.
472#[derive(Debug, Clone)]
473pub struct PointWorldFrame<R: RealField + Copy> {
474    /// The 3D coordinates (x, y, z) in the world reference frame.
475    pub coords: Point3<R>,
476}
477
478impl From<&[f64; 3]> for PointWorldFrame<f64> {
479    fn from(orig: &[f64; 3]) -> Self {
480        PointWorldFrame {
481            coords: Point3::new(orig[0], orig[1], orig[2]),
482        }
483    }
484}
485
486impl From<[f64; 3]> for PointWorldFrame<f64> {
487    fn from(orig: [f64; 3]) -> Self {
488        PointWorldFrame {
489            coords: Point3::new(orig[0], orig[1], orig[2]),
490        }
491    }
492}
493
494impl<R, IN> From<&cam_geom::Points<cam_geom::coordinate_system::WorldFrame, R, U1, IN>>
495    for PointWorldFrame<R>
496where
497    R: RealField + Copy,
498    IN: nalgebra::storage::Storage<R, U1, U3>,
499{
500    fn from(orig: &cam_geom::Points<cam_geom::coordinate_system::WorldFrame, R, U1, IN>) -> Self {
501        PointWorldFrame {
502            coords: Point3::new(orig.data[(0, 0)], orig.data[(0, 1)], orig.data[(0, 2)]),
503        }
504    }
505}
506
507impl<R, IN> From<cam_geom::Points<cam_geom::coordinate_system::WorldFrame, R, U1, IN>>
508    for PointWorldFrame<R>
509where
510    R: RealField + Copy,
511    IN: nalgebra::storage::Storage<R, U1, U3>,
512{
513    fn from(orig: cam_geom::Points<cam_geom::coordinate_system::WorldFrame, R, U1, IN>) -> Self {
514        let orig_ref = &orig;
515        orig_ref.into()
516    }
517}
518
519impl<R> From<&PointWorldFrame<R>>
520    for cam_geom::Points<
521        cam_geom::coordinate_system::WorldFrame,
522        R,
523        U1,
524        na::storage::Owned<R, U1, U3>,
525    >
526where
527    R: RealField + Copy,
528    na::DefaultAllocator: na::allocator::Allocator<U1, U2>,
529{
530    fn from(orig: &PointWorldFrame<R>) -> Self {
531        Self::new(na::OMatrix::<R, U1, U3>::new(
532            orig.coords[0],
533            orig.coords[1],
534            orig.coords[2],
535        ))
536    }
537}
538
539/// Compute the sum of elements in a vector.
540pub fn vec_sum<R: RealField + Copy>(vec: &[R]) -> R {
541    vec.iter().fold(na::convert(0.0), |acc, i| acc + *i)
542}
543
544/// A 3D world point with associated reprojection error statistics.
545///
546/// This structure extends [`PointWorldFrame`] with additional information about
547/// how well the reconstructed 3D point reprojects back to the original 2D
548/// observations in each camera. This is useful for quality assessment and
549/// outlier detection in 3D reconstruction.
550///
551/// # Reprojection Error
552///
553/// Reprojection error measures how far the reconstructed 3D point projects
554/// from the original 2D observations when projected back into each camera.
555/// Lower values indicate better reconstruction quality.
556///
557/// # Fields
558///
559/// - `point`: The reconstructed 3D point in world coordinates
560/// - `cum_reproj_dist`: Sum of reprojection distances across all cameras
561/// - `mean_reproj_dist`: Average reprojection distance per camera
562/// - `reproj_dists`: Individual reprojection distance for each camera
563///
564/// # Example
565///
566/// ```rust
567/// use braid_mvg::{PointWorldFrame, PointWorldFrameWithSumReprojError};
568/// use nalgebra::Point3;
569///
570/// let point = PointWorldFrame { coords: Point3::new(1.0, 2.0, 3.0) };
571/// let errors = vec![0.5, 0.3, 0.8]; // reprojection errors for 3 cameras
572///
573/// let point_with_error = PointWorldFrameWithSumReprojError::new(point, errors);
574/// println!("Mean reprojection error: {:.3}", point_with_error.mean_reproj_dist);
575/// ```
576#[derive(Debug, Clone)]
577pub struct PointWorldFrameWithSumReprojError<R: RealField + Copy> {
578    /// The reconstructed 3D point in world coordinates.
579    pub point: PointWorldFrame<R>,
580    /// Sum of reprojection distances from all cameras.
581    pub cum_reproj_dist: R,
582    /// Average reprojection distance per camera.
583    pub mean_reproj_dist: R,
584    /// Individual reprojection distances for each camera.
585    pub reproj_dists: Vec<R>,
586}
587
588impl<R: RealField + Copy> PointWorldFrameWithSumReprojError<R> {
589    /// Create a new point with reprojection error statistics.
590    ///
591    /// This constructor automatically computes the cumulative and mean
592    /// reprojection errors from the individual camera errors.
593    ///
594    /// # Arguments
595    ///
596    /// * `point` - The 3D point in world coordinates
597    /// * `reproj_dists` - Vector of reprojection distances, one per camera
598    ///
599    /// # Returns
600    ///
601    /// A new instance with computed error statistics
602    ///
603    /// # Example
604    ///
605    /// ```rust
606    /// use braid_mvg::{PointWorldFrame, PointWorldFrameWithSumReprojError};
607    /// use nalgebra::Point3;
608    ///
609    /// let point = PointWorldFrame { coords: Point3::new(0.0, 0.0, 5.0) };
610    /// let errors = vec![0.1, 0.2, 0.15]; // errors from 3 cameras
611    ///
612    /// let result = PointWorldFrameWithSumReprojError::new(point, errors);
613    /// assert!((result.mean_reproj_dist - 0.15f64).abs() < 1e-10);
614    /// assert!((result.cum_reproj_dist - 0.45f64).abs() < 1e-10);
615    /// ```
616    pub fn new(point: PointWorldFrame<R>, reproj_dists: Vec<R>) -> Self {
617        let cum_reproj_dist = vec_sum(&reproj_dists);
618        let n_cams: R = na::convert(reproj_dists.len() as f64);
619        let mean_reproj_dist = cum_reproj_dist / n_cams;
620        Self {
621            point,
622            cum_reproj_dist,
623            mean_reproj_dist,
624            reproj_dists,
625        }
626    }
627}
628
629/// A 3D world point that may or may not include reprojection error information.
630///
631/// This enum allows functions to return either a simple 3D point or a point
632/// with additional reprojection error statistics, depending on the operation
633/// performed and the level of detail requested.
634#[derive(Debug, Clone)]
635pub enum PointWorldFrameMaybeWithSumReprojError<R: RealField + Copy> {
636    /// A simple 3D point without error information.
637    Point(PointWorldFrame<R>),
638    /// A 3D point with reprojection error statistics.
639    WithSumReprojError(PointWorldFrameWithSumReprojError<R>),
640}
641
642impl<R: RealField + Copy> PointWorldFrameMaybeWithSumReprojError<R> {
643    /// Extract the 3D point coordinates regardless of the variant type.
644    ///
645    /// This method provides a uniform way to get the 3D point coordinates
646    /// whether the enum contains a simple point or a point with error statistics.
647    pub fn point(self) -> PointWorldFrame<R> {
648        use crate::PointWorldFrameMaybeWithSumReprojError::*;
649        match self {
650            Point(pt) => pt,
651            WithSumReprojError(pto) => pto.point,
652        }
653    }
654}
655
656/// A combined data structure containing a 3D world point and its 2D camera observations.
657///
658/// This structure packages together a 3D point (possibly with reprojection errors)
659/// and the corresponding 2D undistorted pixel observations from each camera.
660/// This is useful for algorithms that need to work with both the 3D structure
661/// and the 2D observations simultaneously.
662///
663/// # Use Cases
664///
665/// - Bundle adjustment optimization
666/// - Outlier detection and filtering
667/// - Tracking and correspondence validation
668/// - Quality assessment of triangulation results
669///
670/// # Example
671///
672/// ```rust
673/// use braid_mvg::{WorldCoordAndUndistorted2D, PointWorldFrame,
674///                 PointWorldFrameMaybeWithSumReprojError, UndistortedPixel};
675/// use nalgebra::{Point2, Point3};
676///
677/// let point = PointWorldFrame { coords: Point3::new(1.0, 2.0, 3.0) };
678/// let maybe_point = PointWorldFrameMaybeWithSumReprojError::Point(point);
679///
680/// let observations = vec![
681///     ("cam1".to_string(), UndistortedPixel { coords: Point2::new(320.0, 240.0) }),
682///     ("cam2".to_string(), UndistortedPixel { coords: Point2::new(340.0, 250.0) }),
683/// ];
684///
685/// let combined = WorldCoordAndUndistorted2D::new(maybe_point, observations);
686/// ```
687#[derive(Debug, Clone)]
688pub struct WorldCoordAndUndistorted2D<R: RealField + Copy> {
689    /// The 3D world point (possibly with reprojection error information).
690    wc: PointWorldFrameMaybeWithSumReprojError<R>,
691    /// The 2D undistorted pixel observations from each camera.
692    /// Each entry is a (camera_name, pixel_coordinate) pair.
693    upoints: Vec<(String, UndistortedPixel<R>)>,
694}
695
696impl<R: RealField + Copy> WorldCoordAndUndistorted2D<R> {
697    /// Create a new combined data structure.
698    ///
699    /// # Arguments
700    ///
701    /// * `wc` - The 3D world coordinates (possibly with error information)
702    /// * `upoints` - Vector of (camera_name, undistorted_pixel) pairs
703    ///
704    /// # Returns
705    ///
706    /// A new instance combining the 3D and 2D information
707    pub fn new(
708        wc: PointWorldFrameMaybeWithSumReprojError<R>,
709        upoints: Vec<(String, UndistortedPixel<R>)>,
710    ) -> Self {
711        Self { wc, upoints }
712    }
713
714    /// Extract just the 3D point coordinates.
715    ///
716    /// # Returns
717    ///
718    /// The [`PointWorldFrame`] containing the 3D coordinates
719    pub fn point(self) -> PointWorldFrame<R> {
720        self.wc.point()
721    }
722
723    /// Decompose into the constituent 3D and 2D components.
724    ///
725    /// # Returns
726    ///
727    /// A tuple containing:
728    /// - The 3D world coordinates (possibly with error information)
729    /// - The vector of 2D observations from each camera
730    pub fn wc_and_upoints(
731        self,
732    ) -> (
733        PointWorldFrameMaybeWithSumReprojError<R>,
734        Vec<(String, UndistortedPixel<R>)>,
735    ) {
736        (self.wc, self.upoints)
737    }
738}
739
740/// Create default camera intrinsic parameters for testing and prototyping.
741///
742/// These parameters are not suitable for real applications - always perform
743/// proper camera calibration for production use.
744pub fn make_default_intrinsics<R: RealField + Copy>() -> RosOpenCvIntrinsics<R> {
745    let cx = na::convert(320.0);
746    let cy = na::convert(240.0);
747    let fx = na::convert(1000.0);
748    let skew = na::convert(0.0);
749    let fy = fx;
750    RosOpenCvIntrinsics::from_params(fx, skew, fy, cx, cy)
751}
752
753#[cfg(test)]
754mod tests {
755    use crate::*;
756
757    fn get_test_intrinsics() -> Vec<(String, RosOpenCvIntrinsics<f64>)> {
758        use na::Vector5;
759        let mut result = Vec::new();
760
761        for (name, dist) in &[
762            (
763                "linear",
764                Distortion::from_opencv_vec(Vector5::new(0.0, 0.0, 0.0, 0.0, 0.0)),
765            ),
766            (
767                "d1",
768                Distortion::from_opencv_vec(Vector5::new(0.1001, 0.2002, 0.3003, 0.4004, 0.5005)),
769            ),
770        ] {
771            for skew in &[0, 10] {
772                let fx = 100.0;
773                let fy = 100.0;
774                let cx = 320.0;
775                let cy = 240.0;
776
777                let cam = RosOpenCvIntrinsics::from_params_with_distortion(
778                    fx,
779                    *skew as f64,
780                    fy,
781                    cx,
782                    cy,
783                    dist.clone(),
784                );
785                result.push((format!("dist-{name}_skew{skew}"), cam));
786            }
787        }
788
789        result.push(("default".to_string(), make_default_intrinsics()));
790
791        result
792    }
793
794    pub(crate) fn get_test_cameras() -> Vec<(String, Camera<f64>)> {
795        let mut result = Vec::new();
796
797        use na::core::OMatrix;
798        use na::core::dimension::U4;
799
800        #[rustfmt::skip]
801        let pmat = OMatrix::<f64,U3,U4>::new(100.0, 0.0, 0.0, 0.01,
802            0.0, 100.0, 0.0, 0.01,
803            320.0, 240.0, 1.0, 0.01);
804        let cam = crate::Camera::from_pmat(640, 480, &pmat).expect("generate test cam from pmat");
805        result.insert(0, ("from-pmat-1".to_string(), cam));
806
807        let extrinsics = crate::extrinsics::make_default_extrinsics();
808        for (int_name, intrinsics) in get_test_intrinsics().into_iter() {
809            let name = format!("cam-{int_name}");
810            let cam = Camera::new(640, 480, extrinsics.clone(), intrinsics).unwrap();
811            result.push((name, cam));
812        }
813        result.push(("default-cam".to_string(), Camera::default()));
814
815        let mut result2 = vec![];
816        for (name, cam) in result {
817            if &name == "cam-dist-linear_skew0" {
818                result2.push((name, cam));
819            }
820        }
821        result2
822    }
823}