braid_mvg/
rerun_io.rs

1// Copyright 2016-2025 Andrew D. Straw.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT
5// or http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8use nalgebra::{self as na, RealField};
9use opencv_ros_camera::RosOpenCvIntrinsics;
10
11use crate::MvgError;
12
13// conversion helpers -----------
14
15trait ToF32 {
16    fn f32(&self) -> f32;
17}
18
19impl<R> ToF32 for R
20where
21    R: RealField,
22{
23    #[inline]
24    fn f32(&self) -> f32 {
25        <R as simba::scalar::SupersetOf<f32>>::to_subset(self).unwrap()
26    }
27}
28
29trait ToR<T: RealField> {
30    #[allow(non_snake_case)]
31    fn R(&self) -> T;
32}
33
34impl<T: RealField> ToR<T> for f32 {
35    #[inline]
36    fn R(&self) -> T {
37        na::convert(*self as f64)
38    }
39}
40
41impl<T: RealField> ToR<T> for f64 {
42    #[inline]
43    fn R(&self) -> T {
44        na::convert(*self)
45    }
46}
47
48// extrinsics -----------
49
50fn rr_translation_and_rotation<R: RealField>(
51    extrinsics: &cam_geom::ExtrinsicParameters<R>,
52) -> (
53    re_types::components::Translation3D,
54    re_types::components::RotationQuat,
55) {
56    use re_types::{components::RotationQuat, datatypes::Quaternion};
57    let r = &extrinsics.pose().rotation.coords;
58    // convert from nalgebra to rerun
59    let rquat = RotationQuat(Quaternion::from_xyzw([
60        r.x.f32(),
61        r.y.f32(),
62        r.z.f32(),
63        r.w.f32(),
64    ]));
65    let t = extrinsics.translation();
66    let translation = re_types::datatypes::Vec3D([t[0].f32(), t[1].f32(), t[2].f32()]).into();
67    (translation, rquat)
68}
69
70/// Trait for converting camera extrinsic parameters to rerun.io Transform3D format.
71///
72/// This trait provides a standardized way to convert camera pose information
73/// into rerun.io's Transform3D archetype for 3D visualization. It handles the
74/// coordinate system conversions and data format transformations required
75/// by the rerun.io ecosystem.
76///
77/// # Example
78///
79/// ```rust
80/// use braid_mvg::rerun_io::AsRerunTransform3D;
81/// use braid_mvg::extrinsics;
82///
83/// let extrinsics = extrinsics::make_default_extrinsics::<f64>();
84/// let transform = extrinsics.as_rerun_transform3d();
85/// // Can now be logged to rerun.io
86/// ```
87pub trait AsRerunTransform3D {
88    /// Convert the camera extrinsics to a rerun.io Transform3D.
89    ///
90    /// # Returns
91    ///
92    /// An object that implements `Into<re_types::archetypes::Transform3D>`,
93    /// suitable for logging to rerun.io for 3D visualization.
94    fn as_rerun_transform3d(&self) -> impl Into<re_types::archetypes::Transform3D>;
95}
96
97impl AsRerunTransform3D for cam_geom::ExtrinsicParameters<f64> {
98    fn as_rerun_transform3d(&self) -> impl Into<re_types::archetypes::Transform3D> {
99        use re_types::components::TransformRelation;
100        let (t, r) = rr_translation_and_rotation(self);
101        re_types::archetypes::Transform3D::from_translation_rotation(t, r)
102            .with_relation(TransformRelation::ChildFromParent)
103    }
104}
105
106#[test]
107fn test_rerun_transform3d() {
108    use nalgebra::{Matrix3xX, Vector3};
109    use re_types::external::glam::{self, Affine3A};
110
111    // You can log this to rerun with the `export-rerun-log` example.
112
113    // Create extrinsic parameters
114    let cc = Vector3::new(3.0, 2.0, 1.0);
115    let lookat = Vector3::new(0.0, 0.0, 0.0);
116    let up = Vector3::new(0.0, 0.0, 1.0);
117    let up_unit = na::core::Unit::new_normalize(up);
118
119    let extrinsics = cam_geom::ExtrinsicParameters::from_view(&cc, &lookat, &up_unit);
120
121    #[rustfmt::skip]
122    let points3d = Matrix3xX::<f64>::from_column_slice(
123        &[
124        0.0, 0.0, 0.0,
125        1.0, 0.0, 0.0,
126        1.0, 1.0, 0.0,
127        0.0, 1.0, 0.0,
128        0.0, 0.0, 1.0,
129        1.0, 0.0, 1.0,
130        1.0, 1.0, 1.0,
131        0.0, 1.0, 1.0,
132    ]);
133
134    // Transform points using cam_geom
135    let world = cam_geom::Points::new(points3d.transpose());
136    let cam = extrinsics.world_to_camera(&world);
137
138    // Transform points using rerun/glam
139    let (rrt, rrq) = rr_translation_and_rotation(&extrinsics);
140    let glam_t: Affine3A = rrt.into();
141    let glam_r: Affine3A = rrq.try_into().unwrap();
142    for i in 0..points3d.ncols() {
143        let (x, y, z) = (points3d[(0, i)], points3d[(1, i)], points3d[(2, i)]);
144        let pt = glam::Vec3::new(x as f32, y as f32, z as f32);
145        let p2 = glam_t.transform_point3(glam_r.transform_point3(pt));
146
147        // Now test transformed point
148        approx::assert_relative_eq!(p2.x as f64, cam.data[(i, 0)], epsilon = 1e-6);
149        approx::assert_relative_eq!(p2.y as f64, cam.data[(i, 1)], epsilon = 1e-6);
150        approx::assert_relative_eq!(p2.z as f64, cam.data[(i, 2)], epsilon = 1e-6);
151    }
152}
153
154// intrinsics -----------
155
156fn pinhole_projection_component<R: RealField>(
157    cam: &RosOpenCvIntrinsics<R>,
158) -> Result<re_types::components::PinholeProjection, MvgError> {
159    // Check if camera can be exactly represented in re_types.
160    if !cam.distortion.is_linear() {
161        return Err(MvgError::RerunUnsupportedIntrinsics);
162    }
163    // re_types does not model all possible intrinsic matrices, raise error.
164    for loc in [(0, 1), (1, 0), (2, 0), (2, 1)] {
165        if cam.p[loc].clone().abs() > 1e-16.R() {
166            return Err(MvgError::RerunUnsupportedIntrinsics);
167        }
168    }
169    if (cam.p[(2, 2)].clone() - na::one()).abs() > 1e-16.R() {
170        return Err(MvgError::RerunUnsupportedIntrinsics);
171    }
172
173    let fx = cam.p[(0, 0)].f32();
174    let fy = cam.p[(1, 1)].f32();
175    let cx = cam.p[(0, 2)].f32();
176    let cy = cam.p[(1, 2)].f32();
177
178    Ok(
179        re_types::components::PinholeProjection::from_focal_length_and_principal_point(
180            (fx, fy),
181            (cx, cy),
182        ),
183    )
184}
185
186/// Convert cam-geom intrinsic parameters to a rerun.io Pinhole archetype.
187///
188/// This function converts camera intrinsic parameters from the cam-geom format
189/// to rerun.io's Pinhole archetype format for 3D visualization. It performs
190/// validation to ensure the camera model is compatible with rerun.io's
191/// simplified pinhole representation.
192///
193/// # Limitations
194///
195/// - Only supports linear (undistorted) camera models
196/// - Requires standard pinhole projection matrix format
197/// - Does not support rectification matrices or complex distortion models
198///
199/// # Arguments
200///
201/// * `intrinsics` - Camera intrinsic parameters in cam-geom format
202/// * `width` - Image width in pixels
203/// * `height` - Image height in pixels
204///
205/// # Returns
206///
207/// A rerun.io `Pinhole` archetype on success, or [`MvgError::RerunUnsupportedIntrinsics`]
208/// if the camera model is not compatible with rerun.io.
209///
210/// # Errors
211///
212/// Returns [`MvgError::RerunUnsupportedIntrinsics`] if:
213/// - The camera has lens distortion
214/// - The projection matrix has non-standard structure
215/// - The camera model cannot be represented as a simple pinhole camera
216///
217/// # Example
218///
219/// ```rust
220/// use braid_mvg::rerun_io::cam_geom_to_rr_pinhole_archetype;
221/// use cam_geom::IntrinsicParametersPerspective;
222///
223/// let intrinsics = cam_geom::PerspectiveParams {fx: 100.0, fy: 100.0, cx: 320.0, cy: 240.0, skew: 0.0};
224/// let pinhole = cam_geom_to_rr_pinhole_archetype(&intrinsics.into(), 640, 480);
225/// // Can now be logged to rerun.io
226/// ```
227pub fn cam_geom_to_rr_pinhole_archetype<R: RealField>(
228    intrinsics: &cam_geom::IntrinsicParametersPerspective<R>,
229    width: usize,
230    height: usize,
231) -> Result<re_types::archetypes::Pinhole, MvgError> {
232    let i = intrinsics;
233    if i.skew().f32().abs() > 1e-10 {
234        return Err(MvgError::RerunUnsupportedIntrinsics);
235    }
236    let image_from_camera =
237        re_types::components::PinholeProjection::from_focal_length_and_principal_point(
238            (i.fx().f32(), i.fy().f32()),
239            (i.cx().f32(), i.cy().f32()),
240        );
241    let resolution: re_types::datatypes::Vec2D = (width as f32, height as f32).into();
242    Ok(re_types::archetypes::Pinhole::new(image_from_camera).with_resolution(resolution))
243}
244
245#[cfg(test)]
246fn opencv_intrinsics<R: RealField>(
247    orig: &re_types::components::PinholeProjection,
248) -> RosOpenCvIntrinsics<R> {
249    let m = orig.0;
250    let col0 = m.col(0);
251    let col1 = m.col(1);
252    let col2 = m.col(2);
253    let fx = col0[0].R();
254    let skew = col1[0].R();
255    let fy = col1[1].R();
256    let cx = col2[0].R();
257    let cy = col2[1].R();
258    RosOpenCvIntrinsics::from_params(fx, skew, fy, cx, cy)
259}
260
261impl<R: RealField + Copy> crate::Camera<R> {
262    /// return [re_types::components::Resolution]
263    fn rr_resolution_component(&self) -> re_types::components::Resolution {
264        re_types::components::Resolution(re_types::datatypes::Vec2D::new(
265            self.width() as f32,
266            self.height() as f32,
267        ))
268    }
269
270    /// Return a [`re_types::archetypes::Pinhole`]
271    ///
272    /// The conversion will not succeed if the camera cannot be represented
273    /// exactly in re_types.
274    pub fn rr_pinhole_archetype(&self) -> Result<re_types::archetypes::Pinhole, MvgError> {
275        let pinhole_projection = pinhole_projection_component(self.intrinsics())?;
276        let resolution = self.rr_resolution_component();
277        Ok(re_types::archetypes::Pinhole::new(pinhole_projection).with_resolution(resolution))
278    }
279}
280
281#[test]
282fn test_intrinsics_rerun_roundtrip() {
283    // create camera, including intrinsics
284    let orig = crate::Camera::<f64>::default();
285    // convert to re_types
286    let rr = pinhole_projection_component(orig.intrinsics()).unwrap();
287    // convert back from re_types
288    let intrinsics = opencv_intrinsics::<f64>(&rr);
289    // compare original intrinsics with converted
290    assert_eq!(orig.intrinsics(), &intrinsics);
291}
292
293#[test]
294fn test_intrinsics_rerun() {
295    use cam_geom::Points;
296    use nalgebra::{OMatrix, U3, U7};
297
298    // create camera, including intrinsics
299    let orig = crate::Camera::<f64>::default();
300    // convert to re_types
301    let rr = pinhole_projection_component(orig.intrinsics()).unwrap();
302
303    let orig_intrinsics = orig.intrinsics();
304
305    #[rustfmt::skip]
306    let pts = Points::new(
307        OMatrix::<f64, U7, U3>::from_row_slice(
308            &[0.0,  0.0, 1.0,
309            1.0,  0.0, 1.0,
310            0.0,  1.0, 1.0,
311            1.0,  1.0, 1.0,
312            -1.0,  0.0, 1.0,
313            0.0, -1.0, 1.0,
314            -1.0, -1.0, 1.0]
315        )
316    );
317
318    let pixels_orig = orig_intrinsics.camera_to_undistorted_pixel(&pts);
319
320    for (pt3d, px_orig) in pts.data.row_iter().zip(pixels_orig.data.row_iter()) {
321        let pt3d_v2 =
322            re_types::external::glam::Vec3::new(pt3d[0] as f32, pt3d[1] as f32, pt3d[2] as f32);
323        let pix_rr = rr.project(pt3d_v2);
324        approx::assert_relative_eq!(pix_rr.x, px_orig[0] as f32, epsilon = 1e-10);
325        approx::assert_relative_eq!(pix_rr.y, px_orig[1] as f32, epsilon = 1e-10);
326    }
327}