1use 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
55pub 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
72pub 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
86trait 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
146trait 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#[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 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 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; let theta = shifted_pt[1].atan2(shifted_pt[0]); let r = (shifted_pt[0].powi(2) + shifted_pt[1].powi(2)).sqrt(); 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(¢er_x).coords;
320 let Fy = self.project_3d_to_pixel(¢er_y).coords;
321 let Fz = self.project_3d_to_pixel(¢er_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); 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 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#[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 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 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; 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 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 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(); let theta_air = pt_horiz_dist.atan2(camcenter[2]);
593
594 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; 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 fn find3d_air(&self, points: &[(String, UndistortedPixel<R>)]) -> Result<PointWorldFrame<R>> {
619 Ok(self.system.find3d(points)?)
620 }
621
622 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
780pub 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)?; let rad_fname = mcsc_dir.join(format!("basename{}.rad", (i + 1)));
813 let non_linear_parameters = if !rad_fname.exists() {
814 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 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 let system = braid_mvg::MultiCameraSystem::from_pymvg_json(cal_file)?;
894 Ok(Self::from_system(system, None))
895 } else {
896 Ok(Self::from_flydra_xml(cal_file)?)
898 }
899 }
900}
901
902pub 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 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); 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 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 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 #[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#[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}