Skip to main content

braid_mvg/
multi_cam_system.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4#![allow(non_snake_case)]
5
6use std::collections::BTreeMap;
7use std::io::Read;
8
9use na::{Matrix3, Vector3};
10use nalgebra as na;
11
12use na::RealField;
13use serde::{Deserialize, Serialize, de::DeserializeOwned};
14
15use cam_geom::{Ray, coordinate_system::WorldFrame};
16
17use crate::pymvg_support::PymvgMultiCameraSystemV1;
18use crate::{
19    Camera, MvgError, PointWorldFrame, PointWorldFrameWithSumReprojError, Result, UndistortedPixel,
20};
21
22/// A calibrated multi-camera system for 3D computer vision applications.
23///
24/// This structure manages a collection of cameras with known intrinsic and extrinsic
25/// parameters, providing high-level operations for 3D reconstruction, triangulation,
26/// and geometric analysis. It's the primary interface for multi-view geometry
27/// operations in the Braid system.
28///
29/// # Core Capabilities
30///
31/// - **3D Triangulation**: Reconstruct 3D points from 2D observations across cameras
32/// - **Reprojection Analysis**: Compute and analyze reprojection errors for quality assessment
33/// - **Geometric Validation**: Verify camera calibration quality and detect issues
34/// - **Format Conversion**: Import/export from various camera system formats (PyMVG, etc.)
35///
36/// # Mathematical Foundation
37///
38/// The system operates on the principle that multiple cameras observing the same
39/// 3D point provide redundant information that can be used to:
40///
41/// 1. **Triangulate** the 3D position via geometric intersection of viewing rays
42/// 2. **Validate** the reconstruction by reprojecting back to all cameras
43/// 3. **Optimize** camera parameters through bundle adjustment
44///
45/// # Camera Naming
46///
47/// Cameras are identified by string names within the system. This allows for
48/// flexible camera management and easy association of observations with specific cameras.
49///
50/// # Example
51///
52/// ```rust
53/// use braid_mvg::{Camera, MultiCameraSystem, PointWorldFrame, extrinsics, make_default_intrinsics, UndistortedPixel};
54/// use std::collections::BTreeMap;
55/// use nalgebra::{Point3, Point2};
56///
57/// // Create cameras
58/// let camera1 = Camera::new(640, 480,
59///     extrinsics::make_default_extrinsics::<f64>(),
60///     make_default_intrinsics::<f64>())?;
61/// let camera2 = Camera::new(640, 480,
62///     extrinsics::make_default_extrinsics::<f64>(),
63///     make_default_intrinsics::<f64>())?;
64///
65/// // Build multi-camera system
66/// let mut cameras = BTreeMap::new();
67/// cameras.insert("cam1".to_string(), camera1);
68/// cameras.insert("cam2".to_string(), camera2);
69/// let system = MultiCameraSystem::new(cameras);
70///
71/// // Create observations for triangulation as slice of tuples
72/// let observations = vec![
73///     ("cam1".to_string(), UndistortedPixel { coords: Point2::new(320.0, 240.0) }),
74///     ("cam2".to_string(), UndistortedPixel { coords: Point2::new(320.0, 240.0) }),
75/// ];
76///
77/// // Use for 3D triangulation
78/// if let Ok(point_3d) = system.find3d(&observations) {
79///     println!("Reconstructed 3D point: {:?}", point_3d.coords);
80/// }
81/// # Ok::<(), braid_mvg::MvgError>(())
82/// ```
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct MultiCameraSystem<R: RealField + Serialize + Copy> {
85    cams_by_name: BTreeMap<String, Camera<R>>,
86    comment: Option<String>,
87}
88
89impl<R> MultiCameraSystem<R>
90where
91    R: RealField + Serialize + DeserializeOwned + Default + Copy,
92{
93    /// Export the camera system to PyMVG format via a writer.
94    ///
95    /// This method serializes the multi-camera system to PyMVG JSON format,
96    /// writing the result to the provided writer.
97    ///
98    /// # Arguments
99    ///
100    /// * `writer` - Writer to output the PyMVG JSON data
101    ///
102    /// # Returns
103    ///
104    /// `Ok(())` on success, or [`MvgError`] if serialization fails.
105    pub fn to_pymvg_writer<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
106        let sys = self.to_pymvg()?;
107        serde_json::to_writer(writer, &sys)?;
108        Ok(())
109    }
110    /// Create a multi-camera system from PyMVG JSON format.
111    ///
112    /// This constructor deserializes a multi-camera system from PyMVG JSON format,
113    /// reading from the provided reader.
114    ///
115    /// # Arguments
116    ///
117    /// * `reader` - Reader containing PyMVG JSON data
118    ///
119    /// # Returns
120    ///
121    /// A new [`MultiCameraSystem`] instance, or [`MvgError`] if parsing fails.
122    pub fn from_pymvg_json<Rd: Read>(reader: Rd) -> Result<Self> {
123        let pymvg_system: PymvgMultiCameraSystemV1<R> = serde_json::from_reader(reader)?;
124        MultiCameraSystem::from_pymvg(&pymvg_system)
125    }
126}
127
128impl<R: RealField + Default + Serialize + Copy> MultiCameraSystem<R> {
129    /// Create a new multi-camera system from a collection of cameras.
130    ///
131    /// # Arguments
132    ///
133    /// * `cams_by_name` - Map of camera names to [`Camera`] instances
134    ///
135    /// # Returns
136    ///
137    /// A new [`MultiCameraSystem`] instance
138    pub fn new(cams_by_name: BTreeMap<String, Camera<R>>) -> Self {
139        Self::new_inner(cams_by_name, None)
140    }
141
142    /// Get an optional comment describing this camera system.
143    ///
144    /// # Returns
145    ///
146    /// Optional reference to the comment string
147    #[inline]
148    pub fn comment(&self) -> Option<&String> {
149        self.comment.as_ref()
150    }
151
152    /// Get the collection of cameras in this system.
153    #[inline]
154    pub fn cams_by_name(&self) -> &BTreeMap<String, Camera<R>> {
155        &self.cams_by_name
156    }
157
158    /// Create a new multi-camera system with an optional comment.
159    ///
160    /// # Arguments
161    ///
162    /// * `cams_by_name` - Map of camera names to [`Camera`] instances
163    /// * `comment` - Descriptive comment for the camera system
164    ///
165    /// # Returns
166    ///
167    /// A new [`MultiCameraSystem`] instance
168    pub fn new_with_comment(cams_by_name: BTreeMap<String, Camera<R>>, comment: String) -> Self {
169        Self::new_inner(cams_by_name, Some(comment))
170    }
171
172    /// Internal constructor for creating a multi-camera system.
173    ///
174    /// # Arguments
175    ///
176    /// * `cams_by_name` - Map of camera names to [`Camera`] instances
177    /// * `comment` - Optional descriptive comment
178    ///
179    /// # Returns
180    ///
181    /// A new [`MultiCameraSystem`] instance
182    pub fn new_inner(cams_by_name: BTreeMap<String, Camera<R>>, comment: Option<String>) -> Self {
183        Self {
184            cams_by_name,
185            comment,
186        }
187    }
188
189    /// Get a camera by name.
190    ///
191    /// # Arguments
192    ///
193    /// * `name` - Name of the camera to retrieve
194    ///
195    /// # Returns
196    ///
197    /// Optional reference to the [`Camera`], or `None` if not found
198    #[inline]
199    pub fn cam_by_name(&self, name: &str) -> Option<&Camera<R>> {
200        self.cams_by_name.get(name)
201    }
202
203    /// Create a multi-camera system from a PyMVG data structure.
204    ///
205    /// # Arguments
206    ///
207    /// * `pymvg_system` - PyMVG multi-camera system data structure
208    ///
209    /// # Returns
210    ///
211    /// A new [`MultiCameraSystem`] instance, or [`MvgError`] if conversion fails
212    pub fn from_pymvg(pymvg_system: &PymvgMultiCameraSystemV1<R>) -> Result<Self> {
213        let mut cams = BTreeMap::new();
214        if pymvg_system.__pymvg_file_version__ != "1.0" {
215            return Err(MvgError::UnsupportedVersion);
216        }
217        for pymvg_cam in pymvg_system.camera_system.iter() {
218            let (name, cam) = Camera::from_pymvg(pymvg_cam)?;
219            cams.insert(name, cam);
220        }
221        Ok(Self::new(cams))
222    }
223
224    /// Convert this multi-camera system to PyMVG format.
225    ///
226    /// This method converts the camera system to PyMVG data structure format
227    /// for interoperability with PyMVG library and JSON serialization.
228    ///
229    /// # Returns
230    ///
231    /// A [`PymvgMultiCameraSystemV1`] structure, or [`MvgError`] if conversion fails
232    ///
233    /// # Example
234    ///
235    /// ```rust
236    /// use braid_mvg::{MultiCameraSystem, Camera, extrinsics, make_default_intrinsics};
237    /// use std::collections::BTreeMap;
238    ///
239    /// let mut cameras = BTreeMap::new();
240    /// cameras.insert("cam1".to_string(), Camera::new(640, 480,
241    ///     extrinsics::make_default_extrinsics::<f64>(),
242    ///     make_default_intrinsics::<f64>())?);
243    /// let system = MultiCameraSystem::new(cameras);
244    /// let pymvg_system = system.to_pymvg()?;
245    /// # Ok::<(), braid_mvg::MvgError>(())
246    /// ```
247    pub fn to_pymvg(&self) -> Result<PymvgMultiCameraSystemV1<R>> {
248        Ok(PymvgMultiCameraSystemV1 {
249            __pymvg_file_version__: "1.0".to_string(),
250            camera_system: self
251                .cams_by_name
252                .iter()
253                .map(|(name, cam)| cam.to_pymvg(name))
254                .collect(),
255        })
256    }
257
258    /// Find reprojection error of 3D coordinate into pixel coordinates.
259    ///
260    /// Note that this returns the reprojection distance of the *undistorted*
261    /// pixels.
262    pub fn get_reprojection_undistorted_dists(
263        &self,
264        points: &[(String, UndistortedPixel<R>)],
265        this_3d_pt: &PointWorldFrame<R>,
266    ) -> Result<Vec<R>> {
267        let this_dists = points
268            .iter()
269            .map(|(cam_name, orig)| {
270                Ok(na::distance(
271                    &self
272                        .cams_by_name
273                        .get(cam_name)
274                        .ok_or(MvgError::UnknownCamera)?
275                        .project_3d_to_pixel(this_3d_pt)
276                        .coords,
277                    &orig.coords,
278                ))
279            })
280            .collect::<Result<Vec<R>>>()?;
281        Ok(this_dists)
282    }
283
284    /// Find 3D coordinate and cumulative reprojection distance using pixel coordinates from cameras
285    pub fn find3d_and_cum_reproj_dist(
286        &self,
287        points: &[(String, UndistortedPixel<R>)],
288    ) -> Result<PointWorldFrameWithSumReprojError<R>> {
289        let point = self.find3d(points)?;
290        let reproj_dists = self.get_reprojection_undistorted_dists(points, &point)?;
291        Ok(PointWorldFrameWithSumReprojError::new(point, reproj_dists))
292    }
293
294    /// Find 3D coordinate using pixel coordinates from cameras
295    pub fn find3d(&self, points: &[(String, UndistortedPixel<R>)]) -> Result<PointWorldFrame<R>> {
296        if points.len() < 2 {
297            return Err(MvgError::NotEnoughPoints);
298        }
299
300        self.find3d_air(points)
301    }
302
303    fn find3d_air(&self, points: &[(String, UndistortedPixel<R>)]) -> Result<PointWorldFrame<R>> {
304        let mut rays: Vec<Ray<WorldFrame, R>> = Vec::with_capacity(points.len());
305        for (name, xy) in points.iter() {
306            // Get camera.
307            let cam = self.cams_by_name.get(name).ok_or(MvgError::UnknownCamera)?;
308            // Get ray from point `xy` in camera coords.
309            let ray_cam = cam.intrinsics().undistorted_pixel_to_camera(&xy.into());
310            // Convert to world coords.
311            let ray = cam
312                .extrinsics()
313                .ray_camera_to_world(&ray_cam)
314                .to_single_ray();
315            rays.push(ray);
316        }
317
318        let coords = cam_geom::best_intersection_of_rays(&rays)?;
319        Ok(coords.into())
320    }
321
322    /// Apply a similarity transformation to all cameras in the system.
323    ///
324    /// This method applies the same similarity transformation (scale, rotation, translation)
325    /// to all cameras in the multi-camera system. This is commonly used for:
326    /// - Coordinate system alignment between different camera systems
327    /// - Scale recovery in structure-from-motion pipelines
328    /// - Aligning reconstructed coordinates with ground truth
329    ///
330    /// # Mathematical Details
331    ///
332    /// The transformation applies: `X' = s*R*X + t` to all camera positions and orientations.
333    ///
334    /// # Arguments
335    ///
336    /// * `s` - Uniform scale factor (must be positive)
337    /// * `rot` - 3×3 rotation matrix (must be orthogonal with determinant +1)
338    /// * `t` - 3×1 translation vector
339    ///
340    /// # Returns
341    ///
342    /// A new aligned [`MultiCameraSystem`], or [`MvgError`] if transformation fails
343    ///
344    /// # Errors
345    ///
346    /// Returns an error if:
347    /// - The rotation matrix is invalid
348    /// - The scale factor is non-positive
349    /// - Any camera transformation fails
350    ///
351    /// # Example
352    ///
353    /// ```rust
354    /// use braid_mvg::{MultiCameraSystem, Camera, extrinsics, make_default_intrinsics};
355    /// use nalgebra::{Matrix3, Vector3};
356    /// use std::collections::BTreeMap;
357    ///
358    /// let mut cameras = BTreeMap::new();
359    /// cameras.insert("cam1".to_string(), Camera::new(640, 480,
360    ///     extrinsics::make_default_extrinsics::<f64>(),
361    ///     make_default_intrinsics::<f64>())?);
362    /// let system = MultiCameraSystem::new(cameras);
363    ///
364    /// let scale = 2.0;
365    /// let rotation = Matrix3::identity();
366    /// let translation = Vector3::zeros();
367    /// let aligned_system = system.align(scale, rotation, translation)?;
368    /// # Ok::<(), braid_mvg::MvgError>(())
369    /// ```
370    pub fn align(&self, s: R, rot: Matrix3<R>, t: Vector3<R>) -> Result<Self> {
371        let comment = self.comment.clone();
372
373        let mut aligned = BTreeMap::new();
374
375        for (name, orig_cam) in self.cams_by_name.iter() {
376            let cam = orig_cam.align(s, rot, t)?;
377            aligned.insert(name.clone(), cam);
378        }
379
380        Ok(Self {
381            cams_by_name: aligned,
382            comment,
383        })
384    }
385}