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}