Skip to main content

cumulus3d_core/
graph.rs

1//! 대응 그래프.
2//!
3//! 영상과 짝을 증분으로 추가할 수 있어, 새 영상이 들어올 때 전체를 다시 구성할 필요가 없다.
4//! 2D 점별 대응은 작은 Vec 로 보관한다(평탄화 생략, 의미 동일).
5
6use crate::features::{FeatureMatch, TwoViewGeometry, TwoViewGeometryConfig};
7use crate::ids::*;
8use crate::store::FeatureStore;
9use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
10
11/// 대응 하나: (영상 id, 2D 점 인덱스).
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
13pub struct Correspondence {
14    /// 영상 id.
15    pub image_id: ImageId,
16    /// 2D 점 인덱스.
17    pub point2d_idx: Point2DIdx,
18}
19
20impl Correspondence {
21    /// (영상, 2D 점)으로 생성.
22    pub fn new(image_id: ImageId, point2d_idx: Point2DIdx) -> Self {
23        Self { image_id, point2d_idx }
24    }
25}
26
27#[derive(Clone, Debug, Default)]
28struct GraphImage {
29    /// 대응이 하나라도 있는 2D 점 수.
30    num_observations: usize,
31    /// 짝 매칭 수 합.
32    num_correspondences: usize,
33    corrs: Vec<Vec<Correspondence>>,
34}
35
36#[derive(Clone, Debug)]
37struct GraphPair {
38    num_correspondences: usize,
39    /// 작은 id → 큰 id 방향, 인라이어 목록 제외.
40    geometry: TwoViewGeometry,
41}
42
43/// 저장소로부터 구성할 때의 옵션.
44#[derive(Clone, Debug)]
45pub struct MatchGraphOptions {
46    /// 짝으로 쓰기 위한 최소 인라이어 매칭 수.
47    pub min_num_matches: usize,
48    /// 워터마크 짝을 무시할지.
49    pub ignore_watermarks: bool,
50    /// 비면 전체 영상.
51    pub image_names: HashSet<String>,
52    /// 거짓이면 "사용 가능한 짝"으로 연결된 영상만 적재.
53    pub keep_all_images: bool,
54}
55
56impl Default for MatchGraphOptions {
57    fn default() -> Self {
58        Self { min_num_matches: 15, ignore_watermarks: false, image_names: HashSet::new(), keep_all_images: false }
59    }
60}
61
62/// 짝 추가 결과 통계(경고 수).
63#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub struct AddPairStats {
65    /// 추가한 대응 수.
66    pub num_added: usize,
67    /// 인덱스가 범위를 벗어나 버린 매칭 수.
68    pub num_out_of_range: usize,
69    /// 중복이라 버린 매칭 수.
70    pub num_duplicates: usize,
71    /// 자기 자신과의 짝이었는지.
72    pub self_pair: bool,
73}
74
75/// 대응 그래프.
76#[derive(Clone, Debug, Default)]
77pub struct MatchGraph {
78    images: HashMap<ImageId, GraphImage>,
79    pairs: HashMap<PairId, GraphPair>,
80    /// 증분 갱신용 저장소 로그 위치.
81    store_cursor: usize,
82}
83
84impl MatchGraph {
85    /// 빈 그래프.
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    /// 노드 추가(2D 점 수만큼 빈 대응 목록). 이미 있으면 무시.
91    pub fn add_image(&mut self, image_id: ImageId, num_points2d: usize) {
92        self.images.entry(image_id).or_insert_with(|| GraphImage {
93            corrs: vec![Vec::new(); num_points2d],
94            ..Default::default()
95        });
96    }
97    /// 영상 노드가 있는지.
98    pub fn exists_image(&self, image_id: ImageId) -> bool {
99        self.images.contains_key(&image_id)
100    }
101    /// 영상 노드 수.
102    pub fn num_images(&self) -> usize {
103        self.images.len()
104    }
105    /// 짝 수.
106    pub fn pair_count(&self) -> usize {
107        self.pairs.len()
108    }
109    /// 영상 id 목록(오름차순).
110    pub fn image_ids(&self) -> Vec<ImageId> {
111        let mut v: Vec<_> = self.images.keys().copied().collect();
112        v.sort();
113        v
114    }
115    /// 영상의 2D 점 수.
116    pub fn num_points2d(&self, image_id: ImageId) -> usize {
117        self.images.get(&image_id).map_or(0, |i| i.corrs.len())
118    }
119    /// 대응이 하나라도 있는 2D 점 수.
120    pub fn observation_count_of(&self, image_id: ImageId) -> usize {
121        self.images.get(&image_id).map_or(0, |i| i.num_observations)
122    }
123    /// 영상이 속한 짝들의 매칭 수 합.
124    pub fn match_count_of(&self, image_id: ImageId) -> usize {
125        self.images.get(&image_id).map_or(0, |i| i.num_correspondences)
126    }
127    /// 두 영상 사이 매칭 수.
128    pub fn match_count_between(&self, id1: ImageId, id2: ImageId) -> usize {
129        pair_id_of(id1, id2).ok().and_then(|p| self.pairs.get(&p)).map_or(0, |p| p.num_correspondences)
130    }
131    /// 두 영상 사이 짝이 있는지.
132    pub fn exists_image_pair(&self, id1: ImageId, id2: ImageId) -> bool {
133        pair_id_of(id1, id2).is_ok_and(|p| self.pairs.contains_key(&p))
134    }
135    /// (짝 id, 매칭 수), 짝 id 오름차순.
136    pub fn image_pairs(&self) -> Vec<(PairId, usize)> {
137        let mut v: Vec<_> = self.pairs.iter().map(|(k, p)| (*k, p.num_correspondences)).collect();
138        v.sort();
139        v
140    }
141
142    /// 짝 추가. 매칭은 (id1 의 인덱스, id2 의 인덱스).
143    /// 두 영상 노드가 있어야 한다(없으면 아무것도 안 함). 기하는 작은 id → 큰 id 방향으로 저장.
144    pub fn insert_two_view(&mut self, id1: ImageId, id2: ImageId, tvg: &TwoViewGeometry) -> AddPairStats {
145        let mut st = AddPairStats::default();
146        if id1 == id2 {
147            st.self_pair = true;
148            return st;
149        }
150        let Ok(pid) = pair_id_of(id1, id2) else { return st };
151        if !self.images.contains_key(&id1) || !self.images.contains_key(&id2) || self.pairs.contains_key(&pid) {
152            return st;
153        }
154        let n1 = self.images[&id1].corrs.len();
155        let n2 = self.images[&id2].corrs.len();
156        let mut count = 0usize;
157        for m in &tvg.inlier_matches {
158            if m.idx1 as usize >= n1 || m.idx2 as usize >= n2 {
159                st.num_out_of_range += 1;
160                continue;
161            }
162            let c2 = Correspondence::new(id2, m.idx2);
163            {
164                let im1 = self.images.get_mut(&id1).expect("존재");
165                let list = &mut im1.corrs[m.idx1 as usize];
166                if list.contains(&c2) {
167                    st.num_duplicates += 1;
168                    continue;
169                }
170                if list.is_empty() {
171                    im1.num_observations += 1;
172                }
173                list.push(c2);
174            }
175            {
176                let im2 = self.images.get_mut(&id2).expect("존재");
177                let list = &mut im2.corrs[m.idx2 as usize];
178                if list.is_empty() {
179                    im2.num_observations += 1;
180                }
181                list.push(Correspondence::new(id1, m.idx1));
182            }
183            count += 1;
184        }
185        self.images.get_mut(&id1).expect("존재").num_correspondences += count;
186        self.images.get_mut(&id2).expect("존재").num_correspondences += count;
187        let mut geometry = TwoViewGeometry { inlier_matches: Vec::new(), ..tvg.clone() };
188        if swap_image_pair(id1, id2) {
189            geometry.invert();
190        }
191        self.pairs.insert(pid, GraphPair { num_correspondences: count, geometry });
192        st.num_added = count;
193        st
194    }
195
196    fn usable(tvg: &TwoViewGeometry, opts: &MatchGraphOptions) -> bool {
197        tvg.inlier_matches.len() >= opts.min_num_matches
198            && !(opts.ignore_watermarks && tvg.config == TwoViewGeometryConfig::Watermark)
199    }
200
201    /// 저장소에서 구성.
202    pub fn from_store(store: &FeatureStore, opts: &MatchGraphOptions) -> Self {
203        let mut g = Self::new();
204        g.update_from_store(store, opts);
205        g
206    }
207
208    /// 저장소에 새로 기록된 짝만 반영(증분). 새 영상 노드도 필요 시 추가.
209    /// 반환: 새로 추가된 짝 수.
210    pub fn update_from_store(&mut self, store: &FeatureStore, opts: &MatchGraphOptions) -> usize {
211        let (new_pairs, cursor) = store.two_view_geometries_since(self.store_cursor);
212        self.store_cursor = cursor;
213        let name_ok = |id: ImageId| -> bool {
214            opts.image_names.is_empty() || store.image(id).is_some_and(|im| opts.image_names.contains(&im.name))
215        };
216        if opts.keep_all_images {
217            for im in store.images() {
218                if name_ok(im.image_id) {
219                    self.add_image(im.image_id, store.num_keypoints(im.image_id));
220                }
221            }
222        }
223        // 짝 id 순서로 결정적 처리, 중복 기록 제거.
224        let mut by_pid: BTreeMap<PairId, _> = BTreeMap::new();
225        for (pid, t) in new_pairs {
226            by_pid.insert(pid, t);
227        }
228        let mut added = 0;
229        for (pid, t) in by_pid {
230            if self.pairs.contains_key(&pid) || !Self::usable(&t, opts) {
231                continue;
232            }
233            let (a, b) = images_of_pair(pid);
234            if !name_ok(a) || !name_ok(b) || a == b {
235                continue;
236            }
237            for id in [a, b] {
238                if !self.images.contains_key(&id) {
239                    self.add_image(id, store.num_keypoints(id));
240                }
241            }
242            self.insert_two_view(a, b, &t);
243            added += 1;
244        }
245        added
246    }
247
248    /// (영상, 2D 점)의 직접 대응.
249    pub fn find_correspondences(&self, image_id: ImageId, point2d_idx: Point2DIdx) -> &[Correspondence] {
250        self.images
251            .get(&image_id)
252            .and_then(|i| i.corrs.get(point2d_idx as usize))
253            .map_or(&[], |v| v.as_slice())
254    }
255    /// (영상, 2D 점)에 대응이 있는지.
256    pub fn has_correspondences(&self, image_id: ImageId, point2d_idx: Point2DIdx) -> bool {
257        !self.find_correspondences(image_id, point2d_idx).is_empty()
258    }
259    /// 두 뷰 관측: 대응이 정확히 1개이고 상대도 정확히 1개.
260    pub fn in_two_view_track(&self, image_id: ImageId, point2d_idx: Point2DIdx) -> bool {
261        let c = self.find_correspondences(image_id, point2d_idx);
262        c.len() == 1 && self.find_correspondences(c[0].image_id, c[0].point2d_idx).len() == 1
263    }
264    /// 두 영상 사이 매칭: 영상1 2D 점 오름차순.
265    pub fn matches_between(&self, id1: ImageId, id2: ImageId) -> Vec<FeatureMatch> {
266        let mut out = Vec::new();
267        if let Some(im) = self.images.get(&id1) {
268            for (k, list) in im.corrs.iter().enumerate() {
269                for c in list {
270                    if c.image_id == id2 {
271                        out.push(FeatureMatch::new(k as u32, c.point2d_idx));
272                    }
273                }
274            }
275        }
276        out
277    }
278    /// 추이적 대응(너비 우선, 깊이 t). 시작 관측과 이미 본 관측 제외.
279    pub fn transitive_matches(
280        &self,
281        image_id: ImageId,
282        point2d_idx: Point2DIdx,
283        depth: usize,
284    ) -> Vec<Correspondence> {
285        let start = Correspondence::new(image_id, point2d_idx);
286        let mut seen: HashSet<Correspondence> = HashSet::from([start]);
287        let mut out = Vec::new();
288        let mut q = VecDeque::from([(start, 0usize)]);
289        while let Some((c, d)) = q.pop_front() {
290            if d >= depth {
291                continue;
292            }
293            for n in self.find_correspondences(c.image_id, c.point2d_idx) {
294                if seen.insert(*n) {
295                    out.push(*n);
296                    q.push_back((*n, d + 1));
297                }
298            }
299        }
300        out
301    }
302    /// (id1 → id2) 방향 두 뷰 기하(인라이어 제외).
303    pub fn two_view_geometry(&self, id1: ImageId, id2: ImageId) -> Option<TwoViewGeometry> {
304        let pid = pair_id_of(id1, id2).ok()?;
305        let g = &self.pairs.get(&pid)?.geometry;
306        Some(if swap_image_pair(id1, id2) { g.inverted() } else { g.clone() })
307    }
308    /// (id1 → id2) 방향 기하로 갱신.
309    pub fn replace_two_view(&mut self, id1: ImageId, id2: ImageId, tvg: &TwoViewGeometry) -> bool {
310        let Ok(pid) = pair_id_of(id1, id2) else { return false };
311        let Some(p) = self.pairs.get_mut(&pid) else { return false };
312        let mut g = TwoViewGeometry { inlier_matches: Vec::new(), ..tvg.clone() };
313        if swap_image_pair(id1, id2) {
314            g.invert();
315        }
316        p.geometry = g;
317        true
318    }
319
320    /// 합집합-찾기 트랙(연결 성분). `pair_filter(작은 id, 큰 id)` 가 참인 짝만 사용.
321    /// 각 성분은 (영상, 점) 오름차순, 성분 목록은 첫 원소 오름차순. 길이 1 성분은 없다.
322    pub fn union_find_tracks<F>(&self, pair_filter: F) -> Vec<Vec<Correspondence>>
323    where
324        F: Fn(ImageId, ImageId) -> bool,
325    {
326        let mut index: HashMap<Correspondence, usize> = HashMap::new();
327        let mut nodes: Vec<Correspondence> = Vec::new();
328        let mut parent: Vec<usize> = Vec::new();
329        fn find(p: &mut [usize], mut x: usize) -> usize {
330            while p[x] != x {
331                p[x] = p[p[x]];
332                x = p[x];
333            }
334            x
335        }
336        let mut get = |c: Correspondence, nodes: &mut Vec<Correspondence>, parent: &mut Vec<usize>| -> usize {
337            *index.entry(c).or_insert_with(|| {
338                nodes.push(c);
339                parent.push(parent.len());
340                parent.len() - 1
341            })
342        };
343        let mut pairs: Vec<PairId> = self.pairs.keys().copied().collect();
344        pairs.sort();
345        for pid in pairs {
346            let (a, b) = images_of_pair(pid);
347            if !pair_filter(a, b) {
348                continue;
349            }
350            for m in self.matches_between(a, b) {
351                let x = get(Correspondence::new(a, m.idx1), &mut nodes, &mut parent);
352                let y = get(Correspondence::new(b, m.idx2), &mut nodes, &mut parent);
353                let (rx, ry) = (find(&mut parent, x), find(&mut parent, y));
354                if rx != ry {
355                    // 사전식으로 큰 쪽 루트를 작은 쪽 아래로.
356                    if nodes[rx] < nodes[ry] {
357                        parent[ry] = rx;
358                    } else {
359                        parent[rx] = ry;
360                    }
361                }
362            }
363        }
364        let mut comps: BTreeMap<usize, Vec<Correspondence>> = BTreeMap::new();
365        for (i, node) in nodes.iter().enumerate() {
366            let r = find(&mut parent, i);
367            comps.entry(r).or_default().push(*node);
368        }
369        let mut out: Vec<Vec<Correspondence>> = comps
370            .into_values()
371            .filter(|v| v.len() > 1)
372            .map(|mut v| {
373                v.sort();
374                v
375            })
376            .collect();
377        out.sort();
378        out
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use crate::camera::{Camera, CameraModelKind};
386    use crate::features::Keypoint;
387    use crate::geometry::{Quat, Rigid3, Vec3};
388
389    fn tvg(m: &[(u32, u32)]) -> TwoViewGeometry {
390        TwoViewGeometry {
391            config: TwoViewGeometryConfig::Calibrated,
392            inlier_matches: m.iter().map(|&(a, b)| FeatureMatch::new(a, b)).collect(),
393            ..Default::default()
394        }
395    }
396
397    #[test]
398    fn add_pair_rules() {
399        let mut g = MatchGraph::new();
400        g.add_image(1, 10);
401        g.add_image(2, 10);
402        g.add_image(3, 10);
403        let st = g.insert_two_view(1, 2, &tvg(&[(0, 0), (1, 1), (1, 1), (20, 2), (2, 30), (3, 3)]));
404        assert_eq!(st.num_added, 3);
405        assert_eq!(st.num_duplicates, 1);
406        assert_eq!(st.num_out_of_range, 2);
407        assert!(g.insert_two_view(3, 3, &tvg(&[(0, 0)])).self_pair);
408        assert_eq!(g.match_count_between(2, 1), 3);
409        assert_eq!(g.observation_count_of(1), 3);
410        assert_eq!(g.match_count_of(2), 3);
411        // reversed direction pair
412        g.insert_two_view(3, 2, &tvg(&[(5, 0), (6, 1)]));
413        assert_eq!(g.observation_count_of(2), 3);
414        assert_eq!(g.match_count_of(2), 5);
415        assert_eq!(g.matches_between(2, 3), vec![FeatureMatch::new(0, 5), FeatureMatch::new(1, 6)]);
416        assert!(g.in_two_view_track(1, 3));
417        assert!(!g.in_two_view_track(1, 0)); // 2:0 has two corrs
418        let t = g.transitive_matches(1, 0, 2);
419        assert_eq!(t, vec![Correspondence::new(2, 0), Correspondence::new(3, 5)]);
420        assert_eq!(g.transitive_matches(1, 0, 1).len(), 1);
421    }
422
423    #[test]
424    fn geometry_direction() {
425        let mut g = MatchGraph::new();
426        g.add_image(1, 1);
427        g.add_image(2, 1);
428        let pose = Rigid3::new(Quat::from_axis_angle(&Vec3::new(1.0, 2.0, 3.0), 0.7), Vec3::new(0.3, -0.2, 1.0));
429        let t = TwoViewGeometry { cam1_to_cam2: Some(pose), ..tvg(&[(0, 0)]) };
430        g.insert_two_view(2, 1, &t);
431        let back = g.two_view_geometry(2, 1).unwrap().cam1_to_cam2.unwrap();
432        assert!(back.rotation.angular_distance(&pose.rotation) < 1e-12);
433        let inv = g.two_view_geometry(1, 2).unwrap().cam1_to_cam2.unwrap();
434        let id = inv * pose;
435        assert!(id.rotation.angular_distance(&Quat::IDENTITY) < 1e-12);
436        assert!(id.translation.norm() < 1e-12);
437    }
438
439    #[test]
440    fn incremental_from_store_and_tracks() {
441        let s = FeatureStore::new();
442        let c = s.add_camera(Camera::from_focal(CameraModelKind::OpenCv, 100.0, 100, 100)).unwrap();
443        let ids: Vec<_> = (0..4).map(|k| s.add_image(&format!("{k}.jpg"), c).unwrap()).collect();
444        for &i in &ids {
445            s.set_keypoints(i, vec![Keypoint::new(0.5, 0.5); 20]);
446        }
447        let full: Vec<(u32, u32)> = (0..15).map(|k| (k, k)).collect();
448        s.put_two_view(ids[0], ids[1], &tvg(&full)).unwrap();
449        s.put_two_view(ids[1], ids[2], &tvg(&full)).unwrap();
450        s.put_two_view(ids[2], ids[3], &tvg(&full[..14])).unwrap(); // too few
451        let opts = MatchGraphOptions::default();
452        let mut g = MatchGraph::from_store(&s, &opts);
453        assert_eq!(g.num_images(), 3);
454        assert_eq!(g.pair_count(), 2);
455        // incremental: new pair arrives
456        s.put_two_view(ids[3], ids[0], &tvg(&full)).unwrap();
457        assert_eq!(g.update_from_store(&s, &opts), 1);
458        assert_eq!(g.update_from_store(&s, &opts), 0);
459        assert_eq!(g.num_images(), 4);
460        let tracks = g.union_find_tracks(|_, _| true);
461        assert_eq!(tracks.len(), 15);
462        assert!(tracks.iter().all(|t| t.len() == 4));
463        let tracks = g.union_find_tracks(|a, b| !(a == ids[1] && b == ids[2]));
464        assert!(tracks.iter().all(|t| t.len() == 3));
465    }
466}