Skip to main content

flydra_mvg/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::collections::BTreeMap;
5use std::io::{Read, Write};
6use std::path::PathBuf;
7
8use serde::de::DeserializeOwned;
9
10use num_traits::{One, Zero};
11
12use nalgebra as na;
13use nalgebra::{
14    DMatrix, DefaultAllocator, Dyn, Matrix3, OMatrix, RealField, U1, U2, U3, U4, Vector3, Vector5,
15    allocator::Allocator, geometry::Point3,
16};
17
18use cam_geom::ExtrinsicParameters;
19use opencv_ros_camera::{Distortion, RosOpenCvIntrinsics};
20
21use braid_mvg::{
22    Camera, DistortedPixel, MultiCameraSystem, MvgError, PointWorldFrame,
23    PointWorldFrameMaybeWithSumReprojError, PointWorldFrameWithSumReprojError, UndistortedPixel,
24    WorldCoordAndUndistorted2D, rq_decomposition, vec_sum,
25};
26
27mod fermats_least_time;
28
29pub mod flydra_xml_support;
30
31use crate::flydra_xml_support::{FlydraDistortionModel, SingleCameraCalibration};
32
33const AIR_REFRACTION: f64 = 1.0003;
34
35#[derive(thiserror::Error, Debug)]
36pub enum FlydraMvgError {
37    #[error("xml error: {0}")]
38    SerdeXmlError(#[from] serde_xml_rs::Error),
39    #[error("cannot convert to or from flydra xml: {msg}")]
40    FailedFlydraXmlConversion { msg: String },
41    #[error("MVG error: {0}")]
42    MvgError(#[from] braid_mvg::MvgError),
43    #[error("IO error: {0}")]
44    Io(#[from] std::io::Error),
45    #[error("not implemented operation in braid_mvg")]
46    NotImplemented,
47    #[error("no valid root found")]
48    NoValidRootFound,
49    #[error("No non-linear parameter file {0} found")]
50    NoNonlinearParameters(PathBuf),
51}
52
53pub type Result<T> = std::result::Result<T, FlydraMvgError>;
54
55// MultiCameraIter -------------------------------------------------------
56
57/// implements an `Iterator` which returns cameras as `MultiCamera`s.
58pub struct MultiCameraIter<'a, R: RealField + Copy + Default + serde::Serialize> {
59    name_iter: CamNameIter<'a, R>,
60    flydra_system: &'a FlydraMultiCameraSystem<R>,
61}
62
63impl<R: RealField + Copy + Default + serde::Serialize> Iterator for MultiCameraIter<'_, R> {
64    type Item = MultiCamera<R>;
65    fn next(&mut self) -> Option<Self::Item> {
66        self.name_iter
67            .next()
68            .map(|name| self.flydra_system.cam_by_name(name).unwrap())
69    }
70}
71
72// CamNameIter -------------------------------------------------------
73
74/// implements an `Iterator` which returns camera names as `&str`s.
75pub struct CamNameIter<'a, R: RealField + Copy + Default + serde::Serialize>(
76    std::collections::btree_map::Keys<'a, String, Camera<R>>,
77);
78
79impl<'a, R: RealField + Copy + Default + serde::Serialize> Iterator for CamNameIter<'a, R> {
80    type Item = &'a str;
81    fn next(&mut self) -> Option<Self::Item> {
82        self.0.next().map(AsRef::as_ref)
83    }
84}
85
86// RealField and f64 inter conversion ----------------------------------------------------
87
88trait Point3ToR<R: RealField> {
89    fn to_r(self) -> Point3<R>;
90}
91
92impl<R: RealField> Point3ToR<R> for Point3<f64> {
93    fn to_r(self) -> Point3<R> {
94        Point3::new(
95            na::convert(self[0]),
96            na::convert(self[1]),
97            na::convert(self[2]),
98        )
99    }
100}
101
102trait Vector3ToR<R: RealField> {
103    fn to_r(self) -> Vector3<R>;
104}
105
106impl<R: RealField> Vector3ToR<R> for Vector3<f64> {
107    fn to_r(self) -> Vector3<R> {
108        Vector3::new(
109            na::convert(self[0]),
110            na::convert(self[1]),
111            na::convert(self[2]),
112        )
113    }
114}
115
116trait Point3ToF64 {
117    fn to_f64(self) -> Point3<f64>;
118}
119
120impl<R: RealField> Point3ToF64 for &Point3<R> {
121    fn to_f64(self) -> Point3<f64> {
122        let x: f64 = self[0].to_subset().unwrap();
123        let y: f64 = self[1].to_subset().unwrap();
124        let z: f64 = self[2].to_subset().unwrap();
125        Point3::new(x, y, z)
126    }
127}
128
129trait Vector3ToF64<T> {
130    fn to_f64(self) -> nalgebra::Vector3<f64>;
131}
132
133impl<T> Vector3ToF64<T> for &nalgebra::Vector3<T>
134where
135    T: RealField,
136{
137    fn to_f64(self) -> nalgebra::Vector3<f64> {
138        nalgebra::Vector3::new(
139            self[0].to_subset().unwrap(),
140            self[1].to_subset().unwrap(),
141            self[2].to_subset().unwrap(),
142        )
143    }
144}
145
146// RayCamera -------------------------------------------------------
147
148/// defines operations with Ray type
149///
150/// Rays can be easier to work with when the camera system may have water as
151/// rays are defined from an origin (typically the camera center) in a
152/// direction rather than a point in 3D space, which may be on the other side
153/// of a refractive boundary.
154trait RayCamera<R: RealField + Copy> {
155    fn project_pixel_to_ray(&self, pt: &UndistortedPixel<R>) -> parry3d_f64::query::Ray;
156    fn project_distorted_pixel_to_ray(&self, pt2d: &DistortedPixel<R>) -> parry3d_f64::query::Ray;
157    fn project_ray_to_distorted_pixel(&self, ray: &parry3d_f64::query::Ray) -> DistortedPixel<R>;
158    fn project_ray_to_pixel(&self, ray: &parry3d_f64::query::Ray) -> UndistortedPixel<R>;
159}
160
161impl<R: RealField + Copy + Default + serde::Serialize> RayCamera<R> for Camera<R> {
162    fn project_pixel_to_ray(&self, pt: &UndistortedPixel<R>) -> parry3d_f64::query::Ray {
163        let dist = na::convert(1.0);
164        let p2 = self.project_pixel_to_3d_with_dist(pt, dist);
165        let ray_origin = *self.extrinsics().camcenter();
166        let ray_dir = p2.coords - ray_origin;
167        let ray_origin = ray_origin.to_f64();
168        let ray_dir = ray_dir.to_f64();
169        parry3d_f64::query::Ray::new(ray_origin, ray_dir)
170    }
171
172    fn project_distorted_pixel_to_ray(&self, pt2d: &DistortedPixel<R>) -> parry3d_f64::query::Ray {
173        let undistorted = self.intrinsics().undistort(&pt2d.into());
174        self.project_pixel_to_ray(&undistorted.into())
175    }
176
177    fn project_ray_to_distorted_pixel(&self, ray: &parry3d_f64::query::Ray) -> DistortedPixel<R> {
178        let camcenter = self.extrinsics().camcenter().to_f64();
179        debug_assert!(ray.origin == camcenter);
180        let pt3d = PointWorldFrame::<R> {
181            coords: (ray.origin + ray.dir).to_r(),
182        };
183        self.project_3d_to_distorted_pixel(&pt3d)
184    }
185
186    fn project_ray_to_pixel(&self, ray: &parry3d_f64::query::Ray) -> UndistortedPixel<R> {
187        debug_assert!(ray.origin == self.extrinsics().camcenter().to_f64());
188        let pt3d = PointWorldFrame::<R> {
189            coords: (ray.origin + ray.dir).to_r(),
190        };
191        self.project_3d_to_pixel(&pt3d)
192    }
193}
194
195// MultiCamera -------------------------------------------------------
196
197/// A camera which may be looking at water
198///
199/// Note that we specifically do not have the methods
200/// `project_distorted_pixel_to_3d_with_dist` and `project_pixel_to_3d_with_dist`
201/// because these are dangerous in the sense that depending on `dist`, the
202/// resulting pixel may be subject to refraction. Instead, we have only the
203/// ray based methods.
204#[derive(Clone, Debug)]
205pub struct MultiCamera<R: RealField + Copy + Default + serde::Serialize> {
206    water: Option<R>,
207    name: String,
208    cam: Camera<R>,
209}
210
211impl<R: RealField + Copy + Default + serde::Serialize> MultiCamera<R> {
212    pub fn to_cam(self) -> Camera<R> {
213        self.cam
214    }
215
216    #[inline]
217    pub fn project_pixel_to_ray(&self, pt: &UndistortedPixel<R>) -> parry3d_f64::query::Ray {
218        self.cam.project_pixel_to_ray(pt)
219    }
220
221    #[inline]
222    pub fn project_distorted_pixel_to_ray(
223        &self,
224        pt: &DistortedPixel<R>,
225    ) -> parry3d_f64::query::Ray {
226        self.cam.project_distorted_pixel_to_ray(pt)
227    }
228
229    #[inline]
230    pub fn project_ray_to_pixel(&self, ray: &parry3d_f64::query::Ray) -> UndistortedPixel<R> {
231        self.cam.project_ray_to_pixel(ray)
232    }
233
234    #[inline]
235    pub fn project_ray_to_distorted_pixel(
236        &self,
237        ray: &parry3d_f64::query::Ray,
238    ) -> DistortedPixel<R> {
239        self.cam.project_ray_to_distorted_pixel(ray)
240    }
241
242    /// projects a 3D point to a ray
243    ///
244    /// If the point is under water, the ray is in the direction the camera
245    /// sees it (not the straight-line direction).
246    pub fn project_3d_to_ray(&self, pt3d: &PointWorldFrame<R>) -> parry3d_f64::query::Ray {
247        let camcenter = self.extrinsics().camcenter();
248
249        let dir: Vector3<R> = if let Some(n2) = self.water
250            && pt3d.coords[2] < na::convert(0.0)
251        {
252            // this is tag "laksdfjasl".
253            let n1 = na::convert(AIR_REFRACTION);
254
255            let camcenter_z0 =
256                Point3::from(Vector3::new(camcenter[0], camcenter[1], na::convert(0.0)));
257            let shifted_pt = pt3d.coords - camcenter_z0; // origin under cam at surface. cam at (0,0,z).
258            let theta = shifted_pt[1].atan2(shifted_pt[0]); // angles to points
259            let r = (shifted_pt[0].powi(2) + shifted_pt[1].powi(2)).sqrt(); // horizontal dist
260            let depth = -shifted_pt[2];
261            let height = camcenter[2];
262
263            let water_roots_eps = na::convert(1e-5);
264            let root_params =
265                fermats_least_time::RootParams::new(n1, n2, height, r, depth, water_roots_eps);
266            let r0 = match fermats_least_time::find_fastest_path_fermat(&root_params) {
267                Ok(r0) => r0,
268                Err(e) => {
269                    tracing::error!(
270                        "find_fastest_path_fermat {} with parameters: {:?}",
271                        e,
272                        root_params,
273                    );
274                    panic!("find_fastest_path_fermat {e} with parameters: {root_params:?}");
275                }
276            };
277
278            let shifted_water_surface_pt =
279                Vector3::new(r0 * theta.cos(), r0 * theta.sin(), na::convert(0.0));
280
281            let water_surface_pt = shifted_water_surface_pt + camcenter_z0.coords;
282            water_surface_pt - camcenter.coords
283        } else {
284            pt3d.coords - camcenter
285        };
286        debug_assert!(
287            na::Matrix::norm(&dir) > R::default_epsilon(),
288            "pt3d is at camcenter"
289        );
290        parry3d_f64::query::Ray::new(camcenter.to_f64(), dir.to_f64())
291    }
292
293    #[expect(
294        non_snake_case,
295        reason = "uppercase to match mathematical notation for a matrix"
296    )]
297    pub fn linearize_numerically_at(
298        &self,
299        center: &PointWorldFrame<R>,
300        delta: R,
301    ) -> Result<OMatrix<R, U2, U3>> {
302        let zero = na::convert(0.0);
303
304        let dx = Vector3::<R>::new(delta, zero, zero);
305        let dy = Vector3::<R>::new(zero, delta, zero);
306        let dz = Vector3::<R>::new(zero, zero, delta);
307
308        let center_x = PointWorldFrame {
309            coords: center.coords + dx,
310        };
311        let center_y = PointWorldFrame {
312            coords: center.coords + dy,
313        };
314        let center_z = PointWorldFrame {
315            coords: center.coords + dz,
316        };
317
318        let F = self.project_3d_to_pixel(center).coords;
319        let Fx = self.project_3d_to_pixel(&center_x).coords;
320        let Fy = self.project_3d_to_pixel(&center_y).coords;
321        let Fz = self.project_3d_to_pixel(&center_z).coords;
322
323        let dF_dx = (Fx - F) / delta;
324        let dF_dy = (Fy - F) / delta;
325        let dF_dz = (Fz - F) / delta;
326
327        Ok(OMatrix::<R, U2, U3>::new(
328            dF_dx[0], dF_dy[0], dF_dz[0], dF_dx[1], dF_dy[1], dF_dz[1],
329        ))
330    }
331
332    pub fn project_3d_to_pixel(&self, pt3d: &PointWorldFrame<R>) -> UndistortedPixel<R> {
333        let ray = self.project_3d_to_ray(pt3d); // This handles water correctly
334        // (i.e. a 3D point is not necessarily seen with the ray direct from the cam center
335        // to that 3D point).
336
337        // From here, we use normal camera stuff (no need to know about water).
338        let coords: Point3<R> = (ray.origin + ray.dir).to_r();
339        let pt_air =
340            cam_geom::Points::<cam_geom::WorldFrame, _, _, _>::new(coords.coords.transpose());
341
342        use opencv_ros_camera::CameraExt;
343        let pt_undistorted = self.cam.as_ref().world_to_undistorted_pixel(&pt_air);
344
345        pt_undistorted.into()
346    }
347
348    pub fn project_3d_to_distorted_pixel(&self, pt3d: &PointWorldFrame<R>) -> DistortedPixel<R>
349    where
350        DefaultAllocator: Allocator<U1, U2>,
351    {
352        let undistorted = self.project_3d_to_pixel(pt3d);
353        let u2: opencv_ros_camera::UndistortedPixels<R, U1, _> = (&undistorted).into();
354        self.cam.intrinsics().distort(&u2).into()
355    }
356
357    #[inline]
358    pub fn extrinsics(&self) -> &ExtrinsicParameters<R> {
359        self.cam.extrinsics()
360    }
361
362    /// Return the intrinsic parameters, but probably does not do what you want.
363    ///
364    /// Commenting out for now. Probably does not do what you think it does. In particular,
365    /// since we may have a refractive boundary, cannot just simply map coordinates between
366    /// 2d pixel coordinate and 3D ray, because after the boundary, the ray will be different.
367    pub fn do_not_use_intrinsics(&self) -> &RosOpenCvIntrinsics<R> {
368        self.cam.intrinsics()
369    }
370
371    pub fn undistort(&self, a: &braid_mvg::DistortedPixel<R>) -> braid_mvg::UndistortedPixel<R> {
372        let a2: cam_geom::Pixels<R, U1, _> = a.into();
373        let b1: opencv_ros_camera::UndistortedPixels<R, U1, _> =
374            self.cam.intrinsics().undistort(&a2);
375        b1.into()
376    }
377
378    #[inline]
379    pub fn width(&self) -> usize {
380        self.cam.width()
381    }
382
383    #[inline]
384    pub fn height(&self) -> usize {
385        self.cam.height()
386    }
387
388    #[inline]
389    pub fn name(&self) -> &str {
390        &self.name
391    }
392}
393
394// FlydraMultiCameraSystem ----------------------------------------------------
395
396#[derive(Clone, Debug)]
397pub struct FlydraMultiCameraSystem<R: RealField + Copy + serde::Serialize> {
398    system: MultiCameraSystem<R>,
399    water: Option<R>,
400}
401
402impl<R: RealField + Copy + Default + serde::Serialize> FlydraMultiCameraSystem<R> {
403    pub fn from_system(system: MultiCameraSystem<R>, water: Option<R>) -> Self {
404        FlydraMultiCameraSystem { system, water }
405    }
406
407    pub fn has_refractive_boundary(&self) -> bool {
408        self.water.is_some()
409    }
410
411    pub fn water(&self) -> Option<R> {
412        self.water
413    }
414
415    pub fn to_system(self) -> MultiCameraSystem<R> {
416        self.system
417    }
418
419    pub fn system(&self) -> &MultiCameraSystem<R> {
420        &self.system
421    }
422
423    pub fn new(cams_by_name: BTreeMap<String, Camera<R>>, water: Option<R>) -> Self {
424        let system = MultiCameraSystem::new(cams_by_name);
425
426        FlydraMultiCameraSystem { system, water }
427    }
428
429    pub fn len(&self) -> usize {
430        self.system.cams_by_name().len()
431    }
432
433    pub fn is_empty(&self) -> bool {
434        self.system.cams_by_name().is_empty()
435    }
436
437    pub fn cam_by_name(&self, name: &str) -> Option<MultiCamera<R>> {
438        self.system.cam_by_name(name).map(|cam| MultiCamera {
439            water: self.water,
440            name: name.to_string(),
441            cam: cam.clone(),
442        })
443    }
444
445    pub fn cam_names(&self) -> CamNameIter<'_, R> {
446        CamNameIter(self.system.cams_by_name().keys())
447    }
448
449    pub fn cameras<'a>(&'a self) -> MultiCameraIter<'a, R> {
450        let name_iter = self.cam_names();
451        MultiCameraIter {
452            name_iter,
453            flydra_system: self,
454        }
455    }
456
457    pub fn find3d_and_cum_reproj_dist_distorted(
458        &self,
459        points: &[(String, DistortedPixel<R>)],
460    ) -> Result<PointWorldFrameWithSumReprojError<R>> {
461        use crate::PointWorldFrameMaybeWithSumReprojError::*;
462
463        let x = self.find3d_distorted(points)?;
464        let (pt, upoints) = x.wc_and_upoints();
465        match pt {
466            WithSumReprojError(wsre) => Ok(wsre),
467            Point(point) => {
468                let reproj_dists = self.get_reprojection_undistorted_dists(&upoints, &point)?;
469                Ok(PointWorldFrameWithSumReprojError::new(point, reproj_dists))
470            }
471        }
472    }
473
474    /// Find 3D coordinate using pixel coordinates from cameras
475    ///
476    /// If the system has water, two evaluations are done: one
477    /// for the case of the 3D point being under water, the other
478    /// for the case of the 3D point being in air. The evaluation
479    /// with the lowest mean reprojection error is selected.
480    pub fn find3d(
481        &self,
482        points: &[(String, UndistortedPixel<R>)],
483    ) -> Result<PointWorldFrameMaybeWithSumReprojError<R>> {
484        if points.len() < 2 {
485            return Err(MvgError::NotEnoughPoints.into());
486        }
487
488        use crate::PointWorldFrameMaybeWithSumReprojError::*;
489
490        match self.water {
491            Some(n2) => {
492                // TODO: would it be possible to have a 3d reconstruction with
493                // lower reprojection error when it was z<0 but with the air
494                // based calculation? This would seem problematic...
495                let opt_water_3d_pt = match self.find3d_water(points, n2) {
496                    Ok(water_3d_pt) => Some(water_3d_pt),
497                    Err(FlydraMvgError::MvgError(MvgError::CamGeomError { .. })) => None,
498                    Err(e) => {
499                        return Err(e);
500                    }
501                };
502                let air_3d_pt = self.find3d_air(points)?;
503
504                let air_dists = self.get_reprojection_undistorted_dists(points, &air_3d_pt)?;
505                let air_dist_sum = vec_sum(&air_dists);
506
507                if let Some(water_3d_pt) = opt_water_3d_pt {
508                    let water_dists =
509                        self.get_reprojection_undistorted_dists(points, &water_3d_pt)?;
510                    let water_dist_sum = vec_sum(&water_dists);
511                    if water_dist_sum < air_dist_sum {
512                        Ok(WithSumReprojError(PointWorldFrameWithSumReprojError::new(
513                            water_3d_pt,
514                            water_dists,
515                        )))
516                    } else {
517                        Ok(WithSumReprojError(PointWorldFrameWithSumReprojError::new(
518                            air_3d_pt, air_dists,
519                        )))
520                    }
521                } else {
522                    Ok(WithSumReprojError(PointWorldFrameWithSumReprojError::new(
523                        air_3d_pt, air_dists,
524                    )))
525                }
526            }
527            None => Ok(Point(self.system.find3d(points)?)),
528        }
529    }
530
531    pub fn find3d_distorted(
532        &self,
533        points: &[(String, DistortedPixel<R>)],
534    ) -> Result<WorldCoordAndUndistorted2D<R>> {
535        let upoints: Vec<(String, UndistortedPixel<R>)> = points
536            .iter()
537            .filter_map(|(name, pt)| {
538                self.cam_by_name(name)
539                    .map(|cam| (name.clone(), cam.undistort(pt)))
540            })
541            .collect();
542        if upoints.len() != points.len() {
543            return Err(MvgError::UnknownCamera.into());
544        }
545        Ok(WorldCoordAndUndistorted2D::new(
546            self.find3d(&upoints)?,
547            upoints,
548        ))
549    }
550
551    fn find3d_water(
552        &self,
553        points: &[(String, UndistortedPixel<R>)],
554        n2: R,
555    ) -> Result<PointWorldFrame<R>> {
556        use cam_geom::{Ray, WorldFrame};
557        let z0 = parry3d_f64::shape::HalfSpace::new(Vector3::z_axis());
558
559        let mut rays: Vec<Ray<WorldFrame, _>> = Vec::with_capacity(points.len());
560
561        for (name, xy) in points.iter() {
562            let cam = self.cam_by_name(name).ok_or(MvgError::UnknownCamera)?;
563            let air_ray = cam.project_pixel_to_ray(xy);
564            let solid = false; // will intersect either side of plane
565
566            let opt_surface_pt_toi: Option<f64> = parry3d_f64::query::RayCast::cast_local_ray(
567                &z0,
568                &air_ray,
569                f64::max_value().unwrap(),
570                solid,
571            );
572
573            let air_ray_origin = air_ray.origin.to_r();
574            let air_ray_dir = air_ray.dir.to_r();
575
576            if let Some(toi) = opt_surface_pt_toi {
577                let toi: R = na::convert(toi);
578                let surface_pt: Point3<R> = air_ray_origin + air_ray_dir * toi;
579
580                // closest point to camera on water surface, assumes water at z==0
581                let camcenter = &air_ray_origin;
582                let camcenter_z0: Point3<R> =
583                    Point3::from(Vector3::new(camcenter[0], camcenter[1], na::convert(0.0)));
584
585                let surface_pt_cam = surface_pt - camcenter_z0;
586
587                // Get underwater line from water surface (using Snell's Law).
588                let y = surface_pt_cam[1];
589                let x = surface_pt_cam[0];
590                let pt_angle = y.atan2(x);
591                let pt_horiz_dist = (x * x + y * y).sqrt(); // horizontal distance from camera to water surface
592                let theta_air = pt_horiz_dist.atan2(camcenter[2]);
593
594                // sin(theta_water)/sin(theta_air) = sin(n_air)/sin(n_water)
595                let n_air = na::convert(AIR_REFRACTION);
596                let sin_theta_water = theta_air.sin() * n_air / n2;
597                let theta_water = sin_theta_water.asin();
598                let horiz_dist_at_depth_1 = theta_water.tan();
599                let horiz_dist_cam_depth_1 = horiz_dist_at_depth_1 + pt_horiz_dist; // total horizontal distance
600                let deep_pt_cam: Vector3<R> = Vector3::new(
601                    horiz_dist_cam_depth_1 * pt_angle.cos(),
602                    horiz_dist_cam_depth_1 * pt_angle.sin(),
603                    na::convert(-1.0),
604                );
605                let deep_pt = deep_pt_cam + camcenter_z0.coords;
606                let water_ray_dir = deep_pt - surface_pt.coords;
607                rays.push(Ray::new(
608                    surface_pt.coords.transpose(),
609                    water_ray_dir.transpose(),
610                ));
611            }
612        }
613        let pt = cam_geom::best_intersection_of_rays(&rays).map_err(braid_mvg::MvgError::from)?;
614        Ok(pt.into())
615    }
616
617    /// Find 3D coordinate using pixel coordinates from cameras
618    fn find3d_air(&self, points: &[(String, UndistortedPixel<R>)]) -> Result<PointWorldFrame<R>> {
619        Ok(self.system.find3d(points)?)
620    }
621
622    /// Find reprojection error of 3D coordinate into pixel coordinates
623    pub fn get_reprojection_undistorted_dists(
624        &self,
625        points: &[(String, UndistortedPixel<R>)],
626        this_3d_pt: &PointWorldFrame<R>,
627    ) -> Result<Vec<R>> {
628        let this_dists = points
629            .iter()
630            .map(|(cam_name, orig)| {
631                Ok(na::distance(
632                    &self
633                        .cam_by_name(cam_name)
634                        .ok_or(MvgError::UnknownCamera)?
635                        .project_3d_to_pixel(this_3d_pt)
636                        .coords,
637                    &orig.coords,
638                ))
639            })
640            .collect::<Result<Vec<R>>>()?;
641        Ok(this_dists)
642    }
643
644    pub fn from_flydra_reconstructor(
645        recon: &flydra_xml_support::FlydraReconstructor<R>,
646    ) -> Result<Self> {
647        let water = recon.water;
648        let mut cams = BTreeMap::new();
649        for flydra_cam in recon.cameras.iter() {
650            let (name, cam) = Camera::from_flydra(flydra_cam)?;
651            cams.insert(name, cam);
652        }
653        let _ = recon.minimum_eccentricity;
654        Ok(Self::new(cams, water))
655    }
656
657    pub fn to_flydra_reconstructor(&self) -> Result<flydra_xml_support::FlydraReconstructor<R>> {
658        let cameras: Result<Vec<flydra_xml_support::SingleCameraCalibration<R>>> = self
659            .system
660            .cams_by_name()
661            .iter()
662            .map(|(name, cam)| {
663                let flydra_cam: flydra_xml_support::SingleCameraCalibration<R> =
664                    cam.to_flydra(name)?;
665                Ok(flydra_cam)
666            })
667            .collect();
668        let cameras = cameras?;
669        let water = self.water;
670
671        Ok(flydra_xml_support::FlydraReconstructor {
672            cameras,
673            comment: self.system.comment().cloned(),
674            water,
675            minimum_eccentricity: na::convert(0.0),
676        })
677    }
678}
679
680fn loadtxt_3x4<R>(p: impl AsRef<std::path::Path>) -> Result<OMatrix<R, U3, U4>>
681where
682    R: RealField + Copy + serde::Serialize + DeserializeOwned + Default,
683{
684    let mat = loadtxt_dyn::<R>(p)?;
685    if mat.nrows() != 3 || mat.ncols() != 4 {
686        return Err(MvgError::ParseError.into());
687    }
688    Ok(OMatrix::<R, U3, U4>::from_column_slice(mat.as_slice()))
689}
690
691fn loadtxt_dyn<R>(p: impl AsRef<std::path::Path>) -> Result<OMatrix<R, Dyn, Dyn>>
692where
693    R: RealField + Copy + serde::Serialize + DeserializeOwned + Default,
694{
695    let buf = std::fs::read_to_string(p.as_ref()).map_err(braid_mvg::MvgError::from)?;
696    let lines: Vec<&str> = buf.trim().split("\n").collect();
697    let lines: Vec<&str> = lines
698        .into_iter()
699        .filter(|line| !line.trim().starts_with('#'))
700        .collect();
701    let mut result = Vec::new();
702    let mut n_cols = None;
703    for line in lines.iter() {
704        let mut this_line: Vec<R> = Vec::new();
705        for val_str in line.split_ascii_whitespace() {
706            let val: f64 = val_str.parse().map_err(|_| MvgError::ParseError)?;
707            this_line.push(na::convert(val));
708        }
709        if n_cols.is_none() {
710            n_cols = Some(this_line.len());
711        }
712        if n_cols != Some(this_line.len()) {
713            return Err(MvgError::ParseError.into());
714        }
715        result.push(this_line);
716    }
717    let n_rows = result.len();
718    if n_rows < 1 {
719        return Err(MvgError::ParseError.into());
720    }
721    let n_cols = n_cols.unwrap();
722
723    let mut rmat = OMatrix::<R, Dyn, Dyn>::zeros(n_rows, n_cols);
724    for (i, this_row) in result.into_iter().enumerate() {
725        for (j, this_el) in this_row.into_iter().enumerate() {
726            rmat[(i, j)] = this_el;
727        }
728    }
729    Ok(rmat)
730}
731
732fn loadrad<R>(p: impl AsRef<std::path::Path>) -> Result<FlydraDistortionModel<R>>
733where
734    R: RealField + Copy + serde::Serialize + DeserializeOwned + Default,
735{
736    let buf = std::fs::read_to_string(p.as_ref())?;
737    let lines: Vec<&str> = buf.trim().split("\n").collect();
738    let mut vars = BTreeMap::new();
739    for line in lines.into_iter() {
740        let line = line.trim();
741        if line.is_empty() {
742            continue;
743        }
744        let parts: Vec<_> = line.split("=").collect();
745        if parts.len() != 2 {
746            return Err(MvgError::ParseError.into());
747        }
748        let (varname, valstr) = (parts[0], parts[1]);
749        let val: f64 = valstr.trim().parse().map_err(|_| MvgError::ParseError)?;
750        let val: R = na::convert(val);
751        vars.insert(varname.trim().to_string(), val);
752    }
753
754    Ok(FlydraDistortionModel {
755        fc1: vars["K11"],
756        fc2: vars["K22"],
757        cc1: vars["K13"],
758        cc2: vars["K23"],
759        alpha_c: na::convert(0.0),
760        k1: vars["kc1"],
761        k2: vars["kc2"],
762        p1: vars["kc3"],
763        p2: vars["kc4"],
764        k3: na::convert(0.0),
765        fc1p: None,
766        fc2p: None,
767        cc1p: None,
768        cc2p: None,
769    })
770}
771
772pub struct McscDirData<R>
773where
774    R: RealField + Copy + serde::Serialize + DeserializeOwned + Default,
775{
776    pub cameras: Vec<SingleCameraCalibration<R>>,
777    pub points4cals: Vec<DMatrix<f64>>,
778}
779
780/// Read the results of an MCSC calibration from a directory.
781///
782/// If `require_radfiles` is true, then an error will be returned if any camera
783/// does not have a corresponding .rad file. If false, then cameras without .rad
784/// files will be treated as having a linear distortion model.
785pub fn read_mcsc_dir<R, P: AsRef<std::path::Path>>(
786    mcsc_dir: P,
787    require_radfiles: bool,
788) -> Result<McscDirData<R>>
789where
790    R: RealField + Copy + serde::Serialize + DeserializeOwned + Default,
791{
792    let mcsc_dir = std::path::PathBuf::from(mcsc_dir.as_ref());
793    let cam_order_fname = mcsc_dir.join("camera_order.txt");
794    let cam_order = std::fs::read_to_string(cam_order_fname)?;
795    let cam_ids: Vec<&str> = cam_order.trim().split("\n").collect();
796
797    let res_dat = mcsc_dir.join("Res.dat");
798    let res_dat_buf = std::fs::read_to_string(res_dat)?;
799    let res_lines: Vec<&str> = res_dat_buf.trim().split("\n").collect();
800    assert_eq!(cam_ids.len(), res_lines.len());
801    let mut cameras = Vec::new();
802    let mut points4cals = Vec::new();
803    for (i, (cam_id, res_row)) in cam_ids.iter().zip(res_lines.iter()).enumerate() {
804        let wh: Vec<&str> = res_row.split(" ").collect();
805        assert_eq!(wh.len(), 2);
806        let w = wh[0].parse().unwrap();
807        let h = wh[1].parse().unwrap();
808
809        let pmat_fname = mcsc_dir.join(format!("camera{}.Pmat.cal", (i + 1)));
810        let pmat = loadtxt_3x4(&pmat_fname)?; // 3 rows x 4 columns
811
812        let rad_fname = mcsc_dir.join(format!("basename{}.rad", (i + 1)));
813        let non_linear_parameters = if !rad_fname.exists() {
814            // No .rad file exists.
815            if require_radfiles {
816                return Err(FlydraMvgError::NoNonlinearParameters(rad_fname));
817            } else {
818                FlydraDistortionModel::linear(&pmat)
819            }
820        } else {
821            loadrad(&rad_fname)?
822        };
823
824        let cam = SingleCameraCalibration {
825            cam_id: cam_id.to_string(),
826            calibration_matrix: pmat,
827            resolution: (w, h),
828            scale_factor: None,
829            non_linear_parameters,
830        };
831        cameras.push(cam);
832
833        let points4cal_fname = mcsc_dir.join(format!("cam{}.points4cal.dat", (i + 1)));
834        if points4cal_fname.exists() {
835            let points4cal = loadtxt_dyn(&points4cal_fname)?;
836            points4cals.push(points4cal);
837        }
838    }
839
840    Ok(McscDirData {
841        cameras,
842        points4cals,
843    })
844}
845
846impl<R> FlydraMultiCameraSystem<R>
847where
848    R: RealField + Copy + serde::Serialize + DeserializeOwned + Default,
849{
850    pub fn from_mcsc_dir<P>(mcsc_dir: P, require_radfiles: bool) -> Result<Self>
851    where
852        P: AsRef<std::path::Path>,
853    {
854        let McscDirData { cameras, .. } = read_mcsc_dir(mcsc_dir, require_radfiles)?;
855        let recon = flydra_xml_support::FlydraReconstructor {
856            cameras,
857            ..Default::default()
858        };
859        FlydraMultiCameraSystem::from_flydra_reconstructor(&recon)
860    }
861
862    pub fn from_flydra_xml<Rd: Read>(reader: Rd) -> Result<Self> {
863        let recon: flydra_xml_support::FlydraReconstructor<R> = serde_xml_rs::from_reader(reader)?;
864        FlydraMultiCameraSystem::from_flydra_reconstructor(&recon)
865    }
866
867    pub fn to_flydra_xml<W: Write>(&self, mut writer: W) -> Result<()> {
868        let recon = self.to_flydra_reconstructor()?;
869        let buf = flydra_xml_support::serialize_recon(&recon).map_err(|_e| MvgError::Io {
870            source: std::io::ErrorKind::Other.into(),
871        })?;
872        writer.write_all(buf.as_bytes())?;
873        Ok(())
874    }
875
876    /// Read a calibration from a path.
877    pub fn from_path<P>(cal_fname: P, require_radfiles: bool) -> Result<Self>
878    where
879        P: AsRef<std::path::Path>,
880    {
881        let cal_fname = cal_fname.as_ref();
882
883        if cal_fname.is_dir() {
884            return Self::from_mcsc_dir(cal_fname, require_radfiles);
885        }
886
887        let cal_file = std::fs::File::open(cal_fname)?;
888
889        if cal_fname.extension() == Some(std::ffi::OsStr::new("json"))
890            || cal_fname.extension() == Some(std::ffi::OsStr::new("pymvg"))
891        {
892            // Assume any .json or .pymvg file is a pymvg file.
893            let system = braid_mvg::MultiCameraSystem::from_pymvg_json(cal_file)?;
894            Ok(Self::from_system(system, None))
895        } else {
896            // Otherwise, assume it is a flydra xml file.
897            Ok(Self::from_flydra_xml(cal_file)?)
898        }
899    }
900}
901
902// FlydraCamera ----------------------------------------------
903
904/// A helper trait to implement conversions to and from `braid_mvg::Camera`
905pub trait FlydraCamera<R: RealField + Copy + serde::Serialize> {
906    fn to_flydra(&self, name: &str) -> Result<SingleCameraCalibration<R>>;
907    fn from_flydra(cam: &SingleCameraCalibration<R>) -> Result<(String, Camera<R>)>;
908}
909impl<R: RealField + Copy + serde::Serialize> FlydraCamera<R> for Camera<R> {
910    fn to_flydra(&self, name: &str) -> Result<SingleCameraCalibration<R>> {
911        let cam_id = name.to_string();
912        let k = self.intrinsics().k;
913        let distortion = &self.intrinsics().distortion;
914        let alpha_c = k[(0, 1)] / k[(0, 0)];
915
916        let non_linear_parameters = FlydraDistortionModel {
917            fc1: k[(0, 0)],
918            fc2: k[(1, 1)],
919            cc1: k[(0, 2)],
920            cc2: k[(1, 2)],
921            alpha_c,
922            k1: distortion.radial1(),
923            k2: distortion.radial2(),
924            p1: distortion.tangential1(),
925            p2: distortion.tangential2(),
926            k3: distortion.radial3(),
927            fc1p: None,
928            fc2p: None,
929            cc1p: None,
930            cc2p: None,
931        };
932        let calibration_matrix = *self.linear_part_as_pmat();
933        Ok(SingleCameraCalibration {
934            cam_id,
935            calibration_matrix,
936            resolution: (self.width(), self.height()),
937            scale_factor: None,
938            non_linear_parameters,
939        })
940    }
941
942    fn from_flydra(cam: &SingleCameraCalibration<R>) -> Result<(String, Camera<R>)> {
943        // We allow a relatively large epsilon here because, due to a bug, we
944        // have saved many calibrations with cam.non_linear_parameters.alpha_c
945        // set to zero where the skew in k is not quite zero. In theory, this
946        // epsilon should be really low.
947        let epsilon = 0.03;
948        from_flydra_with_limited_skew(cam, epsilon)
949    }
950}
951
952#[expect(non_snake_case)]
953pub fn from_flydra_with_limited_skew<R: RealField + Copy + serde::Serialize>(
954    cam: &SingleCameraCalibration<R>,
955    epsilon: f64,
956) -> Result<(String, Camera<R>)> {
957    let one: R = One::one();
958    let zero: R = Zero::zero();
959
960    let name = cam.cam_id.clone();
961    let m = cam.calibration_matrix.remove_column(3);
962    let (rquat, k) = rq_decomposition(m)?;
963
964    let k22: R = k[(2, 2)];
965    let k = k * (one / k22); // normalize
966    let p = OMatrix::<R, U3, U4>::new(
967        k[(0, 0)],
968        k[(0, 1)],
969        k[(0, 2)],
970        zero,
971        k[(1, 0)],
972        k[(1, 1)],
973        k[(1, 2)],
974        zero,
975        k[(2, 0)],
976        k[(2, 1)],
977        k[(2, 2)],
978        zero,
979    );
980
981    // (Ab)use PyMVG's rectification to do coordinate transform
982    // for MCSC's undistortion.
983
984    // The intrinsic parameters used for 3D -> 2D.
985    let ex = p[(0, 0)];
986    let bx = p[(0, 2)];
987    let Sx = p[(0, 3)];
988    let ey = p[(1, 1)];
989    let by = p[(1, 2)];
990    let Sy = p[(1, 3)];
991
992    // Parameters used to define undistortion coordinates.
993    let fx = cam.non_linear_parameters.fc1;
994    let fy = cam.non_linear_parameters.fc2;
995    let cx = cam.non_linear_parameters.cc1;
996    let cy = cam.non_linear_parameters.cc2;
997
998    let expected_alpha_c = k[(0, 1)] / k[(0, 0)];
999
1000    let skew_diff = (expected_alpha_c - cam.non_linear_parameters.alpha_c).abs();
1001    if skew_diff > na::convert(epsilon) {
1002        return Err(FlydraMvgError::FailedFlydraXmlConversion {
1003            msg: format!("skew difference {skew_diff:} too large"),
1004        });
1005    }
1006
1007    if let Some(fc1p) = cam.non_linear_parameters.fc1p
1008        && fc1p != cam.non_linear_parameters.fc1
1009    {
1010        return Err(FlydraMvgError::NotImplemented);
1011    }
1012    if let Some(fc2p) = cam.non_linear_parameters.fc2p
1013        && fc2p != cam.non_linear_parameters.fc2
1014    {
1015        return Err(FlydraMvgError::NotImplemented);
1016    }
1017    if let Some(cc1p) = cam.non_linear_parameters.cc1p
1018        && cc1p != cam.non_linear_parameters.cc1
1019    {
1020        return Err(FlydraMvgError::NotImplemented);
1021    }
1022    if let Some(cc2p) = cam.non_linear_parameters.cc2p
1023        && cc2p != cam.non_linear_parameters.cc2
1024    {
1025        return Err(FlydraMvgError::NotImplemented);
1026    }
1027    if let Some(scale_factor) = cam.scale_factor
1028        && scale_factor != one
1029    {
1030        return Err(FlydraMvgError::NotImplemented);
1031    }
1032
1033    // This craziness abuses the rectification matrix of the ROS/OpenCV
1034    // model to compensate for the issue that the intrinsic parameters used
1035    // in the MultiCamSelfCal (MCSC) distortion correction are independent
1036    // from the intrinsic parameters of the linear camera model. With this
1037    // abuse, we allow storing the MCSC calibration results in a compatible
1038    // way with ROS/OpenCV.
1039    //
1040    // Potential bug warning: it could be that the math used to work out
1041    // this matrix form had has a bug in which it was assumed that skew was
1042    // always zero. (This goes especially for entry [0,1].)
1043    #[rustfmt::skip]
1044        let rect_t = {
1045            Matrix3::new(
1046            ex/fx,     zero, (bx+Sx-cx)/fx,
1047             zero,    ey/fy, (by+Sy-cy)/fy,
1048             zero,     zero,           one)
1049        };
1050    let rect = rect_t.transpose();
1051    let i = &cam.non_linear_parameters;
1052    let k3 = zero;
1053    let distortion = Vector5::new(i.k1, i.k2, i.p1, i.p2, k3);
1054    #[rustfmt::skip]
1055        let k = {
1056            Matrix3::new(
1057            fx, i.alpha_c*fx, cx,
1058            zero, fy, cy,
1059            zero, zero, one)
1060        };
1061    let distortion = Distortion::from_opencv_vec(distortion);
1062    let intrinsics = RosOpenCvIntrinsics::from_components(p, k, distortion, rect)
1063        .map_err(braid_mvg::MvgError::from)?;
1064    let camcenter = pmat2cam_center(&cam.calibration_matrix);
1065
1066    let extrinsics = ExtrinsicParameters::from_rotation_and_camcenter(rquat, camcenter);
1067    let cam2 = Camera::new(cam.resolution.0, cam.resolution.1, extrinsics, intrinsics)?;
1068
1069    Ok((name, cam2))
1070}
1071
1072/// helper function (duplicated from braid_mvg)
1073#[expect(clippy::many_single_char_names)]
1074fn pmat2cam_center<R: RealField + Copy>(p: &OMatrix<R, U3, U4>) -> Point3<R> {
1075    let x = (*p).remove_column(0).determinant();
1076    let y = -(*p).remove_column(1).determinant();
1077    let z = (*p).remove_column(2).determinant();
1078    let w = -(*p).remove_column(3).determinant();
1079    Point3::from(Vector3::new(x / w, y / w, z / w))
1080}