Skip to main content

cumulus3d_core/
features.rs

1//! 특징·매칭 자료형.
2
3use crate::error::{Error, Result};
4use crate::geometry::{Mat3, Rigid3};
5
6/// SIFT 기술자 차원.
7pub const DESCRIPTOR_DIM: usize = 128;
8
9/// 키포인트: 입력 영상 해상도 픽셀 좌표 (x, y) + 아핀 형상 A = [[a11, a12], [a21, a22]] (f32).
10///
11/// 픽셀 원점은 좌상단 모서리, 좌상단 화소 중심 = (0.5, 0.5).
12/// A 의 열은 키포인트 국소 좌표축을 영상 좌표로 보낸 벡터.
13#[derive(Clone, Copy, Debug, PartialEq, Default)]
14pub struct Keypoint {
15    /// x 픽셀 좌표.
16    pub x: f32,
17    /// y 픽셀 좌표.
18    pub y: f32,
19    /// 아핀 형상 A 의 (1,1) 원소.
20    pub a11: f32,
21    /// 아핀 형상 A 의 (1,2) 원소.
22    pub a12: f32,
23    /// 아핀 형상 A 의 (2,1) 원소.
24    pub a21: f32,
25    /// 아핀 형상 A 의 (2,2) 원소.
26    pub a22: f32,
27}
28
29impl Keypoint {
30    /// A = 단위행렬.
31    pub fn new(x: f32, y: f32) -> Self {
32        Self { x, y, a11: 1.0, a12: 0.0, a21: 0.0, a22: 1.0 }
33    }
34    /// (scale, orientation) → A = s·[[cos, −sin], [sin, cos]].
35    pub fn from_scale_orientation(x: f32, y: f32, scale: f32, orientation: f32) -> Self {
36        let (s, c) = orientation.sin_cos();
37        Self { x, y, a11: scale * c, a12: -scale * s, a21: scale * s, a22: scale * c }
38    }
39    /// DB 행(열 수 2, 4, 6) 해석.
40    pub fn from_row(row: &[f32]) -> Result<Self> {
41        match row.len() {
42            2 => Ok(Self::new(row[0], row[1])),
43            4 => Ok(Self::from_scale_orientation(row[0], row[1], row[2], row[3])),
44            6 => Ok(Self { x: row[0], y: row[1], a11: row[2], a12: row[3], a21: row[4], a22: row[5] }),
45            n => Err(Error::Format(format!("키포인트 열 수 {n} 는 지원하지 않음"))),
46        }
47    }
48    /// 6열 행 `[x, y, a11, a12, a21, a22]`.
49    pub fn to_row(&self) -> [f32; 6] {
50        [self.x, self.y, self.a11, self.a12, self.a21, self.a22]
51    }
52    /// 첫 축 스케일 ‖(a11, a21)‖.
53    pub fn scale_x(&self) -> f32 {
54        (self.a11 * self.a11 + self.a21 * self.a21).sqrt()
55    }
56    /// 둘째 축 스케일 ‖(a12, a22)‖.
57    pub fn scale_y(&self) -> f32 {
58        (self.a12 * self.a12 + self.a22 * self.a22).sqrt()
59    }
60    /// 평균 스케일.
61    pub fn scale(&self) -> f32 {
62        0.5 * (self.scale_x() + self.scale_y())
63    }
64    /// 방향(라디안).
65    pub fn orientation(&self) -> f32 {
66        self.a21.atan2(self.a11)
67    }
68    /// 전단 각(라디안).
69    pub fn shear(&self) -> f32 {
70        (-self.a12).atan2(self.a22) - self.orientation()
71    }
72    /// 해상도 환산: x·sx, y·sy, 첫 열(a11,a21)·sx, 둘째 열(a12,a22)·sy.
73    pub fn rescale(&mut self, sx: f32, sy: f32) {
74        self.x *= sx;
75        self.y *= sy;
76        self.a11 *= sx;
77        self.a21 *= sx;
78        self.a12 *= sy;
79        self.a22 *= sy;
80    }
81}
82
83/// 영상 하나의 uint8 기술자 묶음(N × 128, 행 우선).
84#[derive(Clone, Debug, PartialEq, Default)]
85pub struct Descriptors {
86    data: Vec<u8>,
87}
88
89impl Descriptors {
90    /// 빈 묶음.
91    pub fn new() -> Self {
92        Self::default()
93    }
94    /// n 개 용량으로 생성.
95    pub fn with_capacity(n: usize) -> Self {
96        Self { data: Vec::with_capacity(n * DESCRIPTOR_DIM) }
97    }
98    /// 길이가 128 의 배수여야 한다.
99    pub fn from_vec(data: Vec<u8>) -> Result<Self> {
100        if !data.len().is_multiple_of(DESCRIPTOR_DIM) {
101            return Err(Error::Format(format!("기술자 바이트 수 {} 가 128 의 배수가 아님", data.len())));
102        }
103        Ok(Self { data })
104    }
105    /// 기술자 개수.
106    pub fn len(&self) -> usize {
107        self.data.len() / DESCRIPTOR_DIM
108    }
109    /// 비었는지.
110    pub fn is_empty(&self) -> bool {
111        self.data.is_empty()
112    }
113    /// i 번째 기술자(128 바이트).
114    pub fn row(&self, i: usize) -> &[u8] {
115        &self.data[i * DESCRIPTOR_DIM..(i + 1) * DESCRIPTOR_DIM]
116    }
117    /// i 번째 기술자(가변).
118    pub fn row_mut(&mut self, i: usize) -> &mut [u8] {
119        &mut self.data[i * DESCRIPTOR_DIM..(i + 1) * DESCRIPTOR_DIM]
120    }
121    /// 기술자 하나 추가.
122    pub fn push(&mut self, d: &[u8; DESCRIPTOR_DIM]) {
123        self.data.extend_from_slice(d);
124    }
125    /// 행 우선 바이트 전체.
126    pub fn as_slice(&self) -> &[u8] {
127        &self.data
128    }
129    /// 바이트 벡터로 변환.
130    pub fn into_vec(self) -> Vec<u8> {
131        self.data
132    }
133    /// 앞 n 개만 남김.
134    pub fn truncate(&mut self, n: usize) {
135        self.data.truncate(n * DESCRIPTOR_DIM);
136    }
137}
138
139/// 두 영상 키포인트 인덱스 쌍 (영상1 idx, 영상2 idx).
140#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
141pub struct FeatureMatch {
142    /// 영상1 키포인트 인덱스.
143    pub idx1: u32,
144    /// 영상2 키포인트 인덱스.
145    pub idx2: u32,
146}
147
148impl FeatureMatch {
149    /// 인덱스 쌍으로 생성.
150    pub fn new(idx1: u32, idx2: u32) -> Self {
151        Self { idx1, idx2 }
152    }
153    /// 두 인덱스를 맞바꾼 매칭.
154    pub fn swapped(&self) -> Self {
155        Self { idx1: self.idx2, idx2: self.idx1 }
156    }
157}
158
159/// 두 뷰 기하 구성 종류(DB config 값).
160#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
161pub enum TwoViewGeometryConfig {
162    #[default]
163    /// 미정(검증 안 됨·실패).
164    Undefined = 0,
165    /// 퇴화(쓸 수 없음).
166    Degenerate = 1,
167    /// 보정됨(E 모델).
168    Calibrated = 2,
169    /// 비보정(F 모델).
170    Uncalibrated = 3,
171    /// 평면(H 모델, 기선 있음).
172    Planar = 4,
173    /// 순수 회전(H 모델, 기선 없음).
174    Panoramic = 5,
175    /// 평면 또는 순수 회전.
176    PlanarOrRotation = 6,
177    /// 워터마크(영상 테두리 고정 패턴).
178    Watermark = 7,
179    /// 여러 모델.
180    Multiple = 8,
181    /// rig 보정.
182    CalibratedRig = 9,
183}
184
185impl TwoViewGeometryConfig {
186    /// 정수 값 → 종류.
187    pub fn from_i32(v: i32) -> Option<Self> {
188        use TwoViewGeometryConfig::*;
189        Some(match v {
190            0 => Undefined,
191            1 => Degenerate,
192            2 => Calibrated,
193            3 => Uncalibrated,
194            4 => Planar,
195            5 => Panoramic,
196            6 => PlanarOrRotation,
197            7 => Watermark,
198            8 => Multiple,
199            9 => CalibratedRig,
200            _ => return None,
201        })
202    }
203    /// 저장용 정수 값.
204    pub fn as_i32(self) -> i32 {
205        self as i32
206    }
207}
208
209/// 짝 하나의 두 뷰 기하. 행렬은 "영상1 → 영상2" 방향(x2ᵀ F x1 = 0, x2 ~ H x1).
210#[derive(Clone, Debug, PartialEq, Default)]
211pub struct TwoViewGeometry {
212    /// 구성 종류.
213    pub config: TwoViewGeometryConfig,
214    /// 본질 행렬 E.
215    pub e: Option<Mat3>,
216    /// 기초 행렬 F.
217    pub f: Option<Mat3>,
218    /// 호모그래피 H.
219    pub h: Option<Mat3>,
220    /// 상대 자세(영상1 카메라 → 영상2 카메라).
221    pub cam1_to_cam2: Option<Rigid3>,
222    /// 인라이어 매칭.
223    pub inlier_matches: Vec<FeatureMatch>,
224    /// 삼각측량 각 중앙값(라디안). 저장되지 않는 부가 정보.
225    pub tri_angle: Option<f64>,
226}
227
228impl TwoViewGeometry {
229    /// 방향 반전: F → Fᵀ, E → Eᵀ, H → H⁻¹, 자세 → 역, 인라이어 열 교환.
230    pub fn inverted(&self) -> Self {
231        Self {
232            config: self.config,
233            e: self.e.map(|m| m.transpose()),
234            f: self.f.map(|m| m.transpose()),
235            h: self.h.and_then(|m| m.try_inverse()),
236            cam1_to_cam2: self.cam1_to_cam2.map(|p| p.inverse()),
237            inlier_matches: self.inlier_matches.iter().map(|m| m.swapped()).collect(),
238            tri_angle: self.tri_angle,
239        }
240    }
241    /// 제자리 방향 반전(`inverted` 참고).
242    pub fn invert(&mut self) {
243        *self = self.inverted();
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn keypoint_shape() {
253        let k = Keypoint::from_scale_orientation(1600.5, 1200.25, 2.0, 0.3);
254        assert!((k.scale() - 2.0).abs() < 1e-6);
255        assert!((k.orientation() - 0.3).abs() < 1e-6);
256        assert!(k.shear().abs() < 1e-6);
257        let mut k2 = k;
258        k2.rescale(4000.0 / 3200.0, 3000.0 / 2400.0);
259        assert!((k2.x - 2000.625).abs() < 1e-5 * 2000.0);
260        assert!((k2.y - 1500.3125).abs() < 1e-5 * 1500.0);
261        assert!((k2.scale() - 2.5).abs() < 1e-5);
262        assert!((k2.orientation() - 0.3).abs() < 1e-5);
263        assert_eq!(Keypoint::from_row(&k.to_row()).unwrap(), k);
264        assert!(Keypoint::from_row(&[1.0; 3]).is_err());
265    }
266
267    #[test]
268    fn tvg_invert() {
269        let h = Mat3::new(1.0, 0.1, 5.0, 0.0, 1.2, -3.0, 0.001, 0.0, 1.0);
270        let g = TwoViewGeometry {
271            config: TwoViewGeometryConfig::Calibrated,
272            h: Some(h),
273            f: Some(h),
274            inlier_matches: vec![FeatureMatch::new(1, 2)],
275            ..Default::default()
276        };
277        let gi = g.inverted();
278        assert!((gi.h.unwrap() * h - Mat3::identity()).norm() < 1e-12);
279        assert_eq!(gi.f.unwrap(), h.transpose());
280        assert_eq!(gi.inlier_matches[0], FeatureMatch::new(2, 1));
281        let gii = gi.inverted();
282        assert!((gii.h.unwrap() - h).norm() < 1e-12);
283    }
284}