Skip to main content

cumulus3d_core/
features.rs

1/*
2 * features.rs
3 *
4 * Copyright (c) 2026 ParkSangWoo
5 *
6 * Author(s):
7 *
8 *      ParkSangWoo <dev.parksangwoo@gmail.com>
9 *
10 *
11 * This file is part of cumulus3d.
12 *
13 * Licensed under the Apache License, Version 2.0 (the "License");
14 * you may not use this file except in compliance with the License.
15 * You may obtain a copy of the License at
16 *
17 *      http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS,
21 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 * See the License for the specific language governing permissions and
23 * limitations under the License.
24 *
25 * Third-party notices: parts of the algorithms, default parameters and data
26 * formats in this file follow other open-source projects. Their copyright
27 * notices and licenses are reproduced in THIRD_PARTY_NOTICES.md.
28 *
29 * SPDX-License-Identifier: Apache-2.0
30 */
31
32//! Feature and matching data types.
33
34use crate::error::{Error, Result};
35use crate::geometry::{Mat3, Rigid3};
36
37/// SIFT descriptor dimension.
38pub const DESCRIPTOR_DIM: usize = 128;
39
40/// Keypoint: pixel coordinates (x, y) at input image resolution + affine shape A = [[a11, a12], [a21, a22]] (f32).
41///
42/// The pixel origin is the top-left corner; the top-left pixel center = (0.5, 0.5).
43/// The columns of A are the keypoint's local coordinate axes mapped into image coordinates.
44#[derive(Clone, Copy, Debug, PartialEq, Default)]
45pub struct Keypoint {
46    /// x pixel coordinate.
47    pub x: f32,
48    /// y pixel coordinate.
49    pub y: f32,
50    /// The (1,1) element of the affine shape A.
51    pub a11: f32,
52    /// The (1,2) element of the affine shape A.
53    pub a12: f32,
54    /// The (2,1) element of the affine shape A.
55    pub a21: f32,
56    /// The (2,2) element of the affine shape A.
57    pub a22: f32,
58}
59
60impl Keypoint {
61    /// A = identity matrix.
62    pub fn new(x: f32, y: f32) -> Self {
63        Self { x, y, a11: 1.0, a12: 0.0, a21: 0.0, a22: 1.0 }
64    }
65    /// (scale, orientation) → A = s·[[cos, −sin], [sin, cos]].
66    pub fn from_scale_orientation(x: f32, y: f32, scale: f32, orientation: f32) -> Self {
67        let (s, c) = orientation.sin_cos();
68        Self { x, y, a11: scale * c, a12: -scale * s, a21: scale * s, a22: scale * c }
69    }
70    /// Interprets a DB row (2, 4 or 6 columns).
71    pub fn from_row(row: &[f32]) -> Result<Self> {
72        match row.len() {
73            2 => Ok(Self::new(row[0], row[1])),
74            4 => Ok(Self::from_scale_orientation(row[0], row[1], row[2], row[3])),
75            6 => Ok(Self { x: row[0], y: row[1], a11: row[2], a12: row[3], a21: row[4], a22: row[5] }),
76            n => Err(Error::Format(format!("키포인트 열 수 {n} 는 지원하지 않음"))),
77        }
78    }
79    /// 6-column row `[x, y, a11, a12, a21, a22]`.
80    pub fn to_row(&self) -> [f32; 6] {
81        [self.x, self.y, self.a11, self.a12, self.a21, self.a22]
82    }
83    /// Scale of the first axis ‖(a11, a21)‖.
84    pub fn scale_x(&self) -> f32 {
85        (self.a11 * self.a11 + self.a21 * self.a21).sqrt()
86    }
87    /// Scale of the second axis ‖(a12, a22)‖.
88    pub fn scale_y(&self) -> f32 {
89        (self.a12 * self.a12 + self.a22 * self.a22).sqrt()
90    }
91    /// Mean scale.
92    pub fn scale(&self) -> f32 {
93        0.5 * (self.scale_x() + self.scale_y())
94    }
95    /// Orientation (radians).
96    pub fn orientation(&self) -> f32 {
97        self.a21.atan2(self.a11)
98    }
99    /// Shear angle (radians).
100    pub fn shear(&self) -> f32 {
101        (-self.a12).atan2(self.a22) - self.orientation()
102    }
103    /// Resolution conversion: x·sx, y·sy, first column (a11,a21)·sx, second column (a12,a22)·sy.
104    pub fn rescale(&mut self, sx: f32, sy: f32) {
105        self.x *= sx;
106        self.y *= sy;
107        self.a11 *= sx;
108        self.a21 *= sx;
109        self.a12 *= sy;
110        self.a22 *= sy;
111    }
112}
113
114/// uint8 descriptor set of one image (N × 128, row-major).
115#[derive(Clone, Debug, PartialEq, Default)]
116pub struct Descriptors {
117    data: Vec<u8>,
118}
119
120impl Descriptors {
121    /// Empty set.
122    pub fn new() -> Self {
123        Self::default()
124    }
125    /// Creates with capacity for n descriptors.
126    pub fn with_capacity(n: usize) -> Self {
127        Self { data: Vec::with_capacity(n * DESCRIPTOR_DIM) }
128    }
129    /// The length must be a multiple of 128.
130    pub fn from_vec(data: Vec<u8>) -> Result<Self> {
131        if !data.len().is_multiple_of(DESCRIPTOR_DIM) {
132            return Err(Error::Format(format!("기술자 바이트 수 {} 가 128 의 배수가 아님", data.len())));
133        }
134        Ok(Self { data })
135    }
136    /// Number of descriptors.
137    pub fn len(&self) -> usize {
138        self.data.len() / DESCRIPTOR_DIM
139    }
140    /// Whether it is empty.
141    pub fn is_empty(&self) -> bool {
142        self.data.is_empty()
143    }
144    /// The i-th descriptor (128 bytes).
145    pub fn row(&self, i: usize) -> &[u8] {
146        &self.data[i * DESCRIPTOR_DIM..(i + 1) * DESCRIPTOR_DIM]
147    }
148    /// The i-th descriptor (mutable).
149    pub fn row_mut(&mut self, i: usize) -> &mut [u8] {
150        &mut self.data[i * DESCRIPTOR_DIM..(i + 1) * DESCRIPTOR_DIM]
151    }
152    /// Appends one descriptor.
153    pub fn push(&mut self, d: &[u8; DESCRIPTOR_DIM]) {
154        self.data.extend_from_slice(d);
155    }
156    /// All bytes, row-major.
157    pub fn as_slice(&self) -> &[u8] {
158        &self.data
159    }
160    /// Converts to a byte vector.
161    pub fn into_vec(self) -> Vec<u8> {
162        self.data
163    }
164    /// Keeps only the first n.
165    pub fn truncate(&mut self, n: usize) {
166        self.data.truncate(n * DESCRIPTOR_DIM);
167    }
168}
169
170/// Pair of keypoint indices in two images (image1 idx, image2 idx).
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
172pub struct FeatureMatch {
173    /// Keypoint index in image 1.
174    pub idx1: u32,
175    /// Keypoint index in image 2.
176    pub idx2: u32,
177}
178
179impl FeatureMatch {
180    /// Creates from an index pair.
181    pub fn new(idx1: u32, idx2: u32) -> Self {
182        Self { idx1, idx2 }
183    }
184    /// Match with the two indices swapped.
185    pub fn swapped(&self) -> Self {
186        Self { idx1: self.idx2, idx2: self.idx1 }
187    }
188}
189
190/// Kind of two-view geometry configuration (DB config value).
191#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
192pub enum TwoViewGeometryConfig {
193    #[default]
194    /// Undefined (not verified or failed).
195    Undefined = 0,
196    /// Degenerate (unusable).
197    Degenerate = 1,
198    /// Calibrated (E model).
199    Calibrated = 2,
200    /// Uncalibrated (F model).
201    Uncalibrated = 3,
202    /// Planar (H model, with baseline).
203    Planar = 4,
204    /// Pure rotation (H model, no baseline).
205    Panoramic = 5,
206    /// Planar or pure rotation.
207    PlanarOrRotation = 6,
208    /// Watermark (fixed pattern at the image border).
209    Watermark = 7,
210    /// Multiple models.
211    Multiple = 8,
212    /// Rig calibrated.
213    CalibratedRig = 9,
214}
215
216impl TwoViewGeometryConfig {
217    /// Integer value -> kind.
218    pub fn from_i32(v: i32) -> Option<Self> {
219        use TwoViewGeometryConfig::*;
220        Some(match v {
221            0 => Undefined,
222            1 => Degenerate,
223            2 => Calibrated,
224            3 => Uncalibrated,
225            4 => Planar,
226            5 => Panoramic,
227            6 => PlanarOrRotation,
228            7 => Watermark,
229            8 => Multiple,
230            9 => CalibratedRig,
231            _ => return None,
232        })
233    }
234    /// Integer value used for storage.
235    pub fn as_i32(self) -> i32 {
236        self as i32
237    }
238}
239
240/// Two-view geometry of one pair. Matrices are in the "image1 -> image2" direction (x2ᵀ F x1 = 0, x2 ~ H x1).
241#[derive(Clone, Debug, PartialEq, Default)]
242pub struct TwoViewGeometry {
243    /// Configuration kind.
244    pub config: TwoViewGeometryConfig,
245    /// Essential matrix E.
246    pub e: Option<Mat3>,
247    /// Fundamental matrix F.
248    pub f: Option<Mat3>,
249    /// Homography H.
250    pub h: Option<Mat3>,
251    /// Relative pose (image1 camera -> image2 camera).
252    pub cam1_to_cam2: Option<Rigid3>,
253    /// Inlier matches.
254    pub inlier_matches: Vec<FeatureMatch>,
255    /// Median triangulation angle (radians). Auxiliary information that is not stored.
256    pub tri_angle: Option<f64>,
257}
258
259impl TwoViewGeometry {
260    /// Direction reversal: F -> Fᵀ, E -> Eᵀ, H -> H⁻¹, pose -> inverse, inlier columns swapped.
261    pub fn inverted(&self) -> Self {
262        Self {
263            config: self.config,
264            e: self.e.map(|m| m.transpose()),
265            f: self.f.map(|m| m.transpose()),
266            h: self.h.and_then(|m| m.try_inverse()),
267            cam1_to_cam2: self.cam1_to_cam2.map(|p| p.inverse()),
268            inlier_matches: self.inlier_matches.iter().map(|m| m.swapped()).collect(),
269            tri_angle: self.tri_angle,
270        }
271    }
272    /// In-place direction reversal (see `inverted`).
273    pub fn invert(&mut self) {
274        *self = self.inverted();
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn keypoint_shape() {
284        let k = Keypoint::from_scale_orientation(1600.5, 1200.25, 2.0, 0.3);
285        assert!((k.scale() - 2.0).abs() < 1e-6);
286        assert!((k.orientation() - 0.3).abs() < 1e-6);
287        assert!(k.shear().abs() < 1e-6);
288        let mut k2 = k;
289        k2.rescale(4000.0 / 3200.0, 3000.0 / 2400.0);
290        assert!((k2.x - 2000.625).abs() < 1e-5 * 2000.0);
291        assert!((k2.y - 1500.3125).abs() < 1e-5 * 1500.0);
292        assert!((k2.scale() - 2.5).abs() < 1e-5);
293        assert!((k2.orientation() - 0.3).abs() < 1e-5);
294        assert_eq!(Keypoint::from_row(&k.to_row()).unwrap(), k);
295        assert!(Keypoint::from_row(&[1.0; 3]).is_err());
296    }
297
298    #[test]
299    fn tvg_invert() {
300        let h = Mat3::new(1.0, 0.1, 5.0, 0.0, 1.2, -3.0, 0.001, 0.0, 1.0);
301        let g = TwoViewGeometry {
302            config: TwoViewGeometryConfig::Calibrated,
303            h: Some(h),
304            f: Some(h),
305            inlier_matches: vec![FeatureMatch::new(1, 2)],
306            ..Default::default()
307        };
308        let gi = g.inverted();
309        assert!((gi.h.unwrap() * h - Mat3::identity()).norm() < 1e-12);
310        assert_eq!(gi.f.unwrap(), h.transpose());
311        assert_eq!(gi.inlier_matches[0], FeatureMatch::new(2, 1));
312        let gii = gi.inverted();
313        assert!((gii.h.unwrap() - h).norm() < 1e-12);
314    }
315}