re_components/
pinhole.rs

1use arrow2_convert::{ArrowDeserialize, ArrowField, ArrowSerialize};
2
3use super::{mat::Mat3x3, Vec2D};
4
5/// Camera perspective projection (a.k.a. intrinsics).
6///
7/// This component is a "mono-component". See [the crate level docs](crate) for details.
8///
9/// ```
10/// use re_components::Pinhole;
11/// use arrow2_convert::field::ArrowField;
12/// use arrow2::datatypes::{DataType, Field};
13///
14/// assert_eq!(
15///     Pinhole::data_type(),
16///     DataType::Struct(vec![
17///         Field::new(
18///             "image_from_cam",
19///             DataType::FixedSizeList(
20///                 Box::new(Field::new("item", DataType::Float32, false)),
21///                 9
22///             ),
23///             false,
24///         ),
25///         Field::new(
26///             "resolution",
27///             DataType::FixedSizeList(
28///                 Box::new(Field::new("item", DataType::Float32, false)),
29///                 2
30///             ),
31///             true,
32///         ),
33///     ]),
34/// );
35/// ```
36#[derive(Copy, Clone, Debug, PartialEq, ArrowField, ArrowSerialize, ArrowDeserialize)]
37#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
38pub struct Pinhole {
39    /// Column-major projection matrix.
40    ///
41    /// Child from parent.
42    /// Image coordinates from camera view coordinates.
43    ///
44    /// Example:
45    /// ```text
46    /// [[1496.1, 0.0,    0.0], // col 0
47    ///  [0.0,    1496.1, 0.0], // col 1
48    ///  [980.5,  744.5,  1.0]] // col 2
49    /// ```
50    pub image_from_cam: Mat3x3,
51
52    /// Pixel resolution (usually integers) of child image space. Width and height.
53    ///
54    /// Example:
55    /// ```text
56    /// [1920.0, 1440.0]
57    /// ```
58    ///
59    /// [`Self::image_from_cam`] project onto the space spanned by `(0,0)` and `resolution - 1`.
60    pub resolution: Option<Vec2D>,
61}
62
63impl re_log_types::LegacyComponent for Pinhole {
64    #[inline]
65    fn legacy_name() -> re_log_types::ComponentName {
66        "rerun.pinhole".into()
67    }
68}
69
70impl Pinhole {
71    /// Field of View on the Y axis, i.e. the angle between top and bottom (in radians).
72    #[inline]
73    pub fn fov_y(&self) -> Option<f32> {
74        self.resolution
75            .map(|resolution| 2.0 * (0.5 * resolution[1] / self.image_from_cam[1][1]).atan())
76    }
77
78    /// X & Y focal length in pixels.
79    ///
80    /// [see definition of intrinsic matrix](https://en.wikipedia.org/wiki/Camera_resectioning#Intrinsic_parameters)
81    #[inline]
82    pub fn focal_length_in_pixels(&self) -> Vec2D {
83        [self.image_from_cam[0][0], self.image_from_cam[1][1]].into()
84    }
85
86    /// Focal length.
87    #[inline]
88    pub fn focal_length(&self) -> Option<f32> {
89        // Use only the first element of the focal length vector, as we don't support non-square pixels.
90        self.resolution.map(|r| self.image_from_cam[0][0] / r[0])
91    }
92
93    /// Principal point of the pinhole camera,
94    /// i.e. the intersection of the optical axis and the image plane.
95    ///
96    /// [see definition of intrinsic matrix](https://en.wikipedia.org/wiki/Camera_resectioning#Intrinsic_parameters)
97    #[cfg(feature = "glam")]
98    #[inline]
99    pub fn principal_point(&self) -> glam::Vec2 {
100        glam::vec2(self.image_from_cam[2][0], self.image_from_cam[2][1])
101    }
102
103    #[inline]
104    #[cfg(feature = "glam")]
105    pub fn resolution(&self) -> Option<glam::Vec2> {
106        self.resolution.map(|r| r.into())
107    }
108
109    #[inline]
110    pub fn aspect_ratio(&self) -> Option<f32> {
111        self.resolution.map(|r| r[0] / r[1])
112    }
113
114    /// Project camera-space coordinates into pixel coordinates,
115    /// returning the same z/depth.
116    #[cfg(feature = "glam")]
117    #[inline]
118    pub fn project(&self, pixel: glam::Vec3) -> glam::Vec3 {
119        ((pixel.truncate() * glam::Vec2::from(self.focal_length_in_pixels())) / pixel.z
120            + self.principal_point())
121        .extend(pixel.z)
122    }
123
124    /// Given pixel coordinates and a world-space depth,
125    /// return a position in the camera space.
126    ///
127    /// The returned z is the same as the input z (depth).
128    #[cfg(feature = "glam")]
129    #[inline]
130    pub fn unproject(&self, pixel: glam::Vec3) -> glam::Vec3 {
131        ((pixel.truncate() - self.principal_point()) * pixel.z
132            / glam::Vec2::from(self.focal_length_in_pixels()))
133        .extend(pixel.z)
134    }
135}
136
137re_log_types::component_legacy_shim!(Pinhole);
138
139#[test]
140fn test_pinhole_roundtrip() {
141    use arrow2::array::Array;
142    use arrow2_convert::{deserialize::TryIntoCollection, serialize::TryIntoArrow};
143
144    let pinholes_in = vec![
145        Pinhole {
146            image_from_cam: [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]].into(),
147            resolution: None,
148        },
149        Pinhole {
150            image_from_cam: [[21.0, 22.0, 23.0], [24.0, 25.0, 26.0], [27.0, 28.0, 29.0]].into(),
151            resolution: Some([123.0, 456.0].into()),
152        },
153    ];
154    let array: Box<dyn Array> = pinholes_in.try_into_arrow().unwrap();
155    let pinholes_out: Vec<Pinhole> = TryIntoCollection::try_into_collection(array).unwrap();
156    assert_eq!(pinholes_in, pinholes_out);
157}