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}