Skip to main content

cumulus3d_core/
store.rs

1/*
2 * store.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//! In-memory feature store (images, cameras, keypoints, descriptors, matches, two-view geometry). Can be saved to and loaded from a
33//! binary file.
34//!
35//! Thread-safe (RwLock). Large data (keypoints, descriptors, matches, geometry) are shared via `Arc`, so lookups do not copy.
36//! Pair data is always stored in the "smaller id -> larger id" direction and inverted on return for opposite-direction lookups.
37
38use crate::camera::{Camera, CameraModelKind};
39use crate::error::{Error, Result};
40use crate::features::{Descriptors, FeatureMatch, Keypoint, TwoViewGeometry, TwoViewGeometryConfig};
41use crate::geometry::{Mat3, Rigid3, Vec3};
42use crate::ids::*;
43use crate::io::binary::{check_count, ReadLe, WriteLe};
44use std::collections::{BTreeMap, HashMap};
45use std::io::{BufReader, BufWriter, Read, Write};
46use std::path::Path;
47use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
48
49/// Image row of the store.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct StoreImage {
52    /// Image id.
53    pub image_id: ImageId,
54    /// Image name (unique).
55    pub name: String,
56    /// Camera id.
57    pub camera_id: CameraId,
58}
59
60/// Prior position (WGS84 latitude/longitude (degrees), altitude (m)).
61#[derive(Clone, Copy, Debug, PartialEq)]
62pub struct PosePrior {
63    /// (latitude, longitude, altitude) or a position depending on the coordinate system.
64    pub position: Vec3,
65    /// 0 = WGS84.
66    pub coordinate_system: i32,
67    /// Gravity direction (optional).
68    pub gravity: Option<Vec3>,
69}
70
71#[derive(Default)]
72struct Inner {
73    cameras: BTreeMap<CameraId, Camera>,
74    images: BTreeMap<ImageId, StoreImage>,
75    name_to_id: HashMap<String, ImageId>,
76    keypoints: HashMap<ImageId, Arc<Vec<Keypoint>>>,
77    descriptors: HashMap<ImageId, Arc<Descriptors>>,
78    pose_priors: HashMap<ImageId, PosePrior>,
79    matches: HashMap<PairId, Arc<Vec<FeatureMatch>>>,
80    two_view: HashMap<PairId, Arc<TwoViewGeometry>>,
81    /// Two-view geometry write log (for incremental consumption). The same pair may appear twice if written again.
82    tvg_log: Vec<PairId>,
83}
84
85/// In-memory feature store.
86#[derive(Default)]
87pub struct FeatureStore {
88    inner: RwLock<Inner>,
89}
90
91fn swap_matches(m: &[FeatureMatch]) -> Vec<FeatureMatch> {
92    m.iter().map(|x| x.swapped()).collect()
93}
94
95impl FeatureStore {
96    /// Empty store.
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    fn r(&self) -> RwLockReadGuard<'_, Inner> {
102        self.inner.read().unwrap_or_else(|e| e.into_inner())
103    }
104    fn w(&self) -> RwLockWriteGuard<'_, Inner> {
105        self.inner.write().unwrap_or_else(|e| e.into_inner())
106    }
107
108    // ---------- Cameras ----------
109
110    /// Adds a camera. If `camera_id` is the invalid value, issues "max id + 1" (1 for an empty store).
111    pub fn add_camera(&self, mut camera: Camera) -> Result<CameraId> {
112        let mut g = self.w();
113        if camera.camera_id == INVALID_CAMERA_ID {
114            camera.camera_id = g.cameras.keys().next_back().map_or(1, |k| k + 1);
115        } else if g.cameras.contains_key(&camera.camera_id) {
116            return Err(Error::AlreadyExists(format!("카메라 {}", camera.camera_id)));
117        }
118        let id = camera.camera_id;
119        g.cameras.insert(id, camera);
120        Ok(id)
121    }
122    /// Overwrites an existing camera (error if absent).
123    pub fn update_camera(&self, camera: Camera) -> Result<()> {
124        let mut g = self.w();
125        match g.cameras.get_mut(&camera.camera_id) {
126            Some(c) => {
127                *c = camera;
128                Ok(())
129            }
130            None => Err(Error::NotFound(format!("카메라 {}", camera.camera_id))),
131        }
132    }
133    /// Camera lookup (copy).
134    pub fn camera(&self, id: CameraId) -> Option<Camera> {
135        self.r().cameras.get(&id).cloned()
136    }
137    /// All cameras (in id order).
138    pub fn cameras(&self) -> Vec<Camera> {
139        self.r().cameras.values().cloned().collect()
140    }
141    /// Number of cameras.
142    pub fn num_cameras(&self) -> usize {
143        self.r().cameras.len()
144    }
145
146    // ---------- Images ----------
147
148    /// Adds an image (name unique, camera must exist). The id is "max id + 1" (1 for an empty store).
149    pub fn add_image(&self, name: &str, camera_id: CameraId) -> Result<ImageId> {
150        let mut g = self.w();
151        let id = g.images.keys().next_back().map_or(1, |k| k + 1);
152        Self::add_image_locked(&mut g, id, name, camera_id)
153    }
154    /// Adds an image with the given id.
155    pub fn add_image_with_id(&self, image_id: ImageId, name: &str, camera_id: CameraId) -> Result<ImageId> {
156        Self::add_image_locked(&mut self.w(), image_id, name, camera_id)
157    }
158    fn add_image_locked(g: &mut Inner, image_id: ImageId, name: &str, camera_id: CameraId) -> Result<ImageId> {
159        if image_id as u64 >= MAX_NUM_IMAGES {
160            return Err(Error::InvalidArgument(format!("영상 id {image_id} 가 상한 이상")));
161        }
162        if !g.cameras.contains_key(&camera_id) {
163            return Err(Error::NotFound(format!("카메라 {camera_id}")));
164        }
165        if g.name_to_id.contains_key(name) {
166            return Err(Error::AlreadyExists(format!("영상 이름 {name}")));
167        }
168        if g.images.contains_key(&image_id) {
169            return Err(Error::AlreadyExists(format!("영상 {image_id}")));
170        }
171        g.name_to_id.insert(name.to_string(), image_id);
172        g.images.insert(image_id, StoreImage { image_id, name: name.to_string(), camera_id });
173        Ok(image_id)
174    }
175    /// Image lookup.
176    pub fn image(&self, id: ImageId) -> Option<StoreImage> {
177        self.r().images.get(&id).cloned()
178    }
179    /// Name -> image id.
180    pub fn image_id_by_name(&self, name: &str) -> Option<ImageId> {
181        self.r().name_to_id.get(name).copied()
182    }
183    /// Looks up an image by name.
184    pub fn image_by_name(&self, name: &str) -> Option<StoreImage> {
185        let g = self.r();
186        g.name_to_id.get(name).and_then(|id| g.images.get(id)).cloned()
187    }
188    /// All images (ascending by id).
189    pub fn images(&self) -> Vec<StoreImage> {
190        self.r().images.values().cloned().collect()
191    }
192    /// List of image ids (ascending).
193    pub fn image_ids(&self) -> Vec<ImageId> {
194        self.r().images.keys().copied().collect()
195    }
196    /// Number of images.
197    pub fn num_images(&self) -> usize {
198        self.r().images.len()
199    }
200    /// Whether the image exists.
201    pub fn exists_image(&self, id: ImageId) -> bool {
202        self.r().images.contains_key(&id)
203    }
204
205    /// Sets the image prior position.
206    pub fn set_pose_prior(&self, image_id: ImageId, prior: PosePrior) {
207        self.w().pose_priors.insert(image_id, prior);
208    }
209    /// Gets the image prior position.
210    pub fn pose_prior(&self, image_id: ImageId) -> Option<PosePrior> {
211        self.r().pose_priors.get(&image_id).copied()
212    }
213
214    // ---------- Features ----------
215
216    /// Writes keypoints (overwrites).
217    pub fn set_keypoints(&self, image_id: ImageId, kps: Vec<Keypoint>) {
218        self.w().keypoints.insert(image_id, Arc::new(kps));
219    }
220    /// Writes descriptors (overwrites).
221    pub fn set_descriptors(&self, image_id: ImageId, d: Descriptors) {
222        self.w().descriptors.insert(image_id, Arc::new(d));
223    }
224    /// Keypoint lookup (shared).
225    pub fn keypoints(&self, image_id: ImageId) -> Option<Arc<Vec<Keypoint>>> {
226        self.r().keypoints.get(&image_id).cloned()
227    }
228    /// Descriptor lookup (shared).
229    pub fn descriptors(&self, image_id: ImageId) -> Option<Arc<Descriptors>> {
230        self.r().descriptors.get(&image_id).cloned()
231    }
232    /// Whether keypoints exist.
233    pub fn exists_keypoints(&self, image_id: ImageId) -> bool {
234        self.r().keypoints.contains_key(&image_id)
235    }
236    /// Whether descriptors exist.
237    pub fn exists_descriptors(&self, image_id: ImageId) -> bool {
238        self.r().descriptors.contains_key(&image_id)
239    }
240    /// Number of keypoints of the image (0 if absent).
241    pub fn num_keypoints(&self, image_id: ImageId) -> usize {
242        self.r().keypoints.get(&image_id).map_or(0, |k| k.len())
243    }
244    /// Maximum of the per-image keypoint counts.
245    pub fn largest_keypoint_count(&self) -> usize {
246        self.r().keypoints.values().map(|k| k.len()).max().unwrap_or(0)
247    }
248
249    // ---------- Matches ----------
250
251    /// Writes raw matches (overwrites). Takes index pairs in (id1, id2) order.
252    pub fn write_matches(&self, id1: ImageId, id2: ImageId, matches: &[FeatureMatch]) -> Result<()> {
253        let pid = pair_id_of(id1, id2)?;
254        let v = if swap_image_pair(id1, id2) { swap_matches(matches) } else { matches.to_vec() };
255        self.w().matches.insert(pid, Arc::new(v));
256        Ok(())
257    }
258    /// Raw match lookup in the (id1, id2) direction.
259    pub fn read_matches(&self, id1: ImageId, id2: ImageId) -> Option<Arc<Vec<FeatureMatch>>> {
260        let pid = pair_id_of(id1, id2).ok()?;
261        let m = self.r().matches.get(&pid).cloned()?;
262        if swap_image_pair(id1, id2) {
263            Some(Arc::new(swap_matches(&m)))
264        } else {
265            Some(m)
266        }
267    }
268    /// Whether raw matches exist.
269    pub fn exists_matches(&self, id1: ImageId, id2: ImageId) -> bool {
270        pair_id_of(id1, id2).is_ok_and(|p| self.r().matches.contains_key(&p))
271    }
272    /// Deletes raw matches.
273    pub fn delete_matches(&self, id1: ImageId, id2: ImageId) {
274        if let Ok(p) = pair_id_of(id1, id2) {
275            self.w().matches.remove(&p);
276        }
277    }
278
279    /// Writes two-view geometry (overwrites). Takes geometry in the (id1 -> id2) direction.
280    pub fn put_two_view(&self, id1: ImageId, id2: ImageId, tvg: &TwoViewGeometry) -> Result<()> {
281        let pid = pair_id_of(id1, id2)?;
282        let v = if swap_image_pair(id1, id2) { tvg.inverted() } else { tvg.clone() };
283        let mut g = self.w();
284        g.two_view.insert(pid, Arc::new(v));
285        g.tvg_log.push(pid);
286        Ok(())
287    }
288    /// Two-view geometry in the (id1 -> id2) direction.
289    pub fn get_two_view(&self, id1: ImageId, id2: ImageId) -> Option<Arc<TwoViewGeometry>> {
290        let pid = pair_id_of(id1, id2).ok()?;
291        let t = self.r().two_view.get(&pid).cloned()?;
292        if swap_image_pair(id1, id2) {
293            Some(Arc::new(t.inverted()))
294        } else {
295            Some(t)
296        }
297    }
298    /// Whether two-view geometry exists.
299    pub fn contains_two_view(&self, id1: ImageId, id2: ImageId) -> bool {
300        pair_id_of(id1, id2).is_ok_and(|p| self.r().two_view.contains_key(&p))
301    }
302    /// Deletes two-view geometry.
303    pub fn remove_two_view(&self, id1: ImageId, id2: ImageId) {
304        if let Ok(p) = pair_id_of(id1, id2) {
305            self.w().two_view.remove(&p);
306        }
307    }
308    /// All stored two-view geometries (ascending by pair id, stored direction = smaller id -> larger id).
309    pub fn two_view_geometries(&self) -> Vec<(PairId, Arc<TwoViewGeometry>)> {
310        let g = self.r();
311        let mut v: Vec<_> = g.two_view.iter().map(|(k, t)| (*k, t.clone())).collect();
312        v.sort_by_key(|x| x.0);
313        v
314    }
315    /// Pairs recorded after log position `since` and the new log position. For incremental correspondence graph updates.
316    pub fn two_view_geometries_since(&self, since: usize) -> (Vec<(PairId, Arc<TwoViewGeometry>)>, usize) {
317        let g = self.r();
318        let end = g.tvg_log.len();
319        let mut out = Vec::new();
320        for &pid in &g.tvg_log[since.min(end)..] {
321            if let Some(t) = g.two_view.get(&pid) {
322                out.push((pid, t.clone()));
323            }
324        }
325        (out, end)
326    }
327    /// Number of stored two-view geometries.
328    pub fn num_two_view_geometries(&self) -> usize {
329        self.r().two_view.len()
330    }
331    /// Number of pairs with stored raw matches.
332    pub fn num_matched_pairs(&self) -> usize {
333        self.r().matches.len()
334    }
335
336    /// New store keeping only images for which `keep` is true. All cameras, image ids, pair data and geometry record order are kept as is.
337    /// Large data (keypoints, descriptors, matches, geometry) are shared via `Arc`, so copying is cheap. For rewinding in incremental
338    /// processing.
339    pub fn retain_copy(&self, keep: impl Fn(&StoreImage) -> bool) -> FeatureStore {
340        let g = self.r();
341        let images: BTreeMap<ImageId, StoreImage> = g.images.iter().filter(|(_, im)| keep(im)).map(|(k, v)| (*k, v.clone())).collect();
342        let pair_ok = |pid: &PairId| {
343            let (a, b) = images_of_pair(*pid);
344            images.contains_key(&a) && images.contains_key(&b)
345        };
346        let inner = Inner {
347            cameras: g.cameras.clone(),
348            name_to_id: images.values().map(|im| (im.name.clone(), im.image_id)).collect(),
349            keypoints: g.keypoints.iter().filter(|(k, _)| images.contains_key(k)).map(|(k, v)| (*k, v.clone())).collect(),
350            descriptors: g.descriptors.iter().filter(|(k, _)| images.contains_key(k)).map(|(k, v)| (*k, v.clone())).collect(),
351            pose_priors: g.pose_priors.iter().filter(|(k, _)| images.contains_key(k)).map(|(k, v)| (*k, *v)).collect(),
352            matches: g.matches.iter().filter(|(k, _)| pair_ok(k)).map(|(k, v)| (*k, v.clone())).collect(),
353            two_view: g.two_view.iter().filter(|(k, _)| pair_ok(k)).map(|(k, v)| (*k, v.clone())).collect(),
354            tvg_log: g.tvg_log.iter().copied().filter(|k| pair_ok(k)).collect(),
355            images,
356        };
357        FeatureStore { inner: RwLock::new(inner) }
358    }
359
360    // ---------- Save/Load ----------
361
362    const MAGIC: &'static [u8; 8] = b"C3DFS\0v1";
363    /// Marker used up to 0.3.0. Accepted only when reading (the format is the same).
364    const LEGACY_MAGIC: &'static [u8; 8] = &[b'S', b'K', b'Y', b'F', b'S', 0, b'v', b'1'];
365
366    /// Saves in the own binary format (little-endian).
367    pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
368        let g = self.r();
369        let mut w = BufWriter::new(std::fs::File::create(path)?);
370        w.write_all(Self::MAGIC)?;
371        w.put_u64(g.cameras.len() as u64)?;
372        for c in g.cameras.values() {
373            w.put_u32(c.camera_id)?;
374            w.put_i32(c.model.id())?;
375            w.put_u64(c.width)?;
376            w.put_u64(c.height)?;
377            w.put_u8(c.focal_from_prior as u8)?;
378            w.put_u64(c.params.len() as u64)?;
379            w.put_f64s(&c.params)?;
380        }
381        w.put_u64(g.images.len() as u64)?;
382        for im in g.images.values() {
383            w.put_u32(im.image_id)?;
384            w.put_u32(im.camera_id)?;
385            w.put_u64(im.name.len() as u64)?;
386            w.write_all(im.name.as_bytes())?;
387        }
388        let mut ids: Vec<_> = g.keypoints.keys().copied().collect();
389        ids.sort();
390        w.put_u64(ids.len() as u64)?;
391        for id in ids {
392            let k = &g.keypoints[&id];
393            w.put_u32(id)?;
394            w.put_u64(k.len() as u64)?;
395            for kp in k.iter() {
396                for x in kp.to_row() {
397                    w.put_f32(x)?;
398                }
399            }
400        }
401        let mut ids: Vec<_> = g.descriptors.keys().copied().collect();
402        ids.sort();
403        w.put_u64(ids.len() as u64)?;
404        for id in ids {
405            let d = &g.descriptors[&id];
406            w.put_u32(id)?;
407            w.put_u64(d.len() as u64)?;
408            w.write_all(d.as_slice())?;
409        }
410        let mut ids: Vec<_> = g.pose_priors.keys().copied().collect();
411        ids.sort();
412        w.put_u64(ids.len() as u64)?;
413        for id in ids {
414            let p = &g.pose_priors[&id];
415            w.put_u32(id)?;
416            w.put_f64s(p.position.as_slice())?;
417            w.put_i32(p.coordinate_system)?;
418            match p.gravity {
419                Some(gv) => {
420                    w.put_u8(1)?;
421                    w.put_f64s(gv.as_slice())?;
422                }
423                None => w.put_u8(0)?,
424            }
425        }
426        let mut pids: Vec<_> = g.matches.keys().copied().collect();
427        pids.sort();
428        w.put_u64(pids.len() as u64)?;
429        for pid in pids {
430            let m = &g.matches[&pid];
431            w.put_u64(pid)?;
432            write_match_list(&mut w, m)?;
433        }
434        let mut pids: Vec<_> = g.two_view.keys().copied().collect();
435        pids.sort();
436        w.put_u64(pids.len() as u64)?;
437        for pid in pids {
438            let t = &g.two_view[&pid];
439            w.put_u64(pid)?;
440            w.put_i32(t.config.as_i32())?;
441            for m in [&t.e, &t.f, &t.h] {
442                match m {
443                    Some(m) => {
444                        w.put_u8(1)?;
445                        for r in 0..3 {
446                            for c in 0..3 {
447                                w.put_f64(m[(r, c)])?;
448                            }
449                        }
450                    }
451                    None => w.put_u8(0)?,
452                }
453            }
454            match &t.cam1_to_cam2 {
455                Some(p) => {
456                    w.put_u8(1)?;
457                    w.put_f64s(&p.to_params())?;
458                }
459                None => w.put_u8(0)?,
460            }
461            write_match_list(&mut w, &t.inlier_matches)?;
462        }
463        w.flush()?;
464        Ok(())
465    }
466
467    /// Reads the `save` format.
468    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
469        let mut r = BufReader::new(std::fs::File::open(path)?);
470        let mut magic = [0u8; 8];
471        r.read_exact(&mut magic)?;
472        if &magic != Self::MAGIC && &magic != Self::LEGACY_MAGIC {
473            return Err(Error::Format("특징 저장소 파일 표지가 다름".into()));
474        }
475        let mut g = Inner::default();
476        for _ in 0..check_count(r.get_u64()?, "카메라")? {
477            let id = r.get_u32()?;
478            let model = CameraModelKind::from_id(r.get_i32()?)?;
479            let width = r.get_u64()?;
480            let height = r.get_u64()?;
481            let prior = r.get_u8()? != 0;
482            let n = check_count(r.get_u64()?, "파라미터")?;
483            let mut params = Vec::with_capacity(n);
484            for _ in 0..n {
485                params.push(r.get_f64()?);
486            }
487            let mut c = Camera::new(id, model, width, height, params)?;
488            c.focal_from_prior = prior;
489            g.cameras.insert(id, c);
490        }
491        for _ in 0..check_count(r.get_u64()?, "영상")? {
492            let image_id = r.get_u32()?;
493            let camera_id = r.get_u32()?;
494            let n = check_count(r.get_u64()?, "이름")?;
495            let name = String::from_utf8(r.get_vec_u8(n)?).map_err(|_| Error::Format("이름 UTF-8 아님".into()))?;
496            g.name_to_id.insert(name.clone(), image_id);
497            g.images.insert(image_id, StoreImage { image_id, name, camera_id });
498        }
499        for _ in 0..check_count(r.get_u64()?, "키포인트 묶음")? {
500            let id = r.get_u32()?;
501            let n = check_count(r.get_u64()?, "키포인트")?;
502            let mut v = Vec::with_capacity(n);
503            for _ in 0..n {
504                let mut row = [0f32; 6];
505                for x in row.iter_mut() {
506                    *x = r.get_f32()?;
507                }
508                v.push(Keypoint::from_row(&row)?);
509            }
510            g.keypoints.insert(id, Arc::new(v));
511        }
512        for _ in 0..check_count(r.get_u64()?, "기술자 묶음")? {
513            let id = r.get_u32()?;
514            let n = check_count(r.get_u64()?, "기술자")?;
515            let d = Descriptors::from_vec(r.get_vec_u8(n * crate::features::DESCRIPTOR_DIM)?)?;
516            g.descriptors.insert(id, Arc::new(d));
517        }
518        for _ in 0..check_count(r.get_u64()?, "사전 위치")? {
519            let id = r.get_u32()?;
520            let p = r.get_f64_array::<3>()?;
521            let cs = r.get_i32()?;
522            let gravity = if r.get_u8()? != 0 { Some(Vec3::from(r.get_f64_array::<3>()?)) } else { None };
523            g.pose_priors.insert(id, PosePrior { position: Vec3::from(p), coordinate_system: cs, gravity });
524        }
525        for _ in 0..check_count(r.get_u64()?, "매칭 짝")? {
526            let pid = r.get_u64()?;
527            g.matches.insert(pid, Arc::new(read_match_list(&mut r)?));
528        }
529        for _ in 0..check_count(r.get_u64()?, "두 뷰 기하")? {
530            let pid = r.get_u64()?;
531            let config =
532                TwoViewGeometryConfig::from_i32(r.get_i32()?).ok_or_else(|| Error::Format("두 뷰 기하 config 값이 잘못됨".into()))?;
533            let mut mats = [None, None, None];
534            for m in mats.iter_mut() {
535                if r.get_u8()? != 0 {
536                    let a = r.get_f64_array::<9>()?;
537                    *m = Some(Mat3::from_row_slice(&a));
538                }
539            }
540            let cam1_to_cam2 = if r.get_u8()? != 0 { Some(Rigid3::from_params(&r.get_f64_array::<7>()?)) } else { None };
541            let inlier_matches = read_match_list(&mut r)?;
542            let [e, f, h] = mats;
543            g.two_view.insert(pid, Arc::new(TwoViewGeometry { config, e, f, h, cam1_to_cam2, inlier_matches, tri_angle: None }));
544            g.tvg_log.push(pid);
545        }
546        Ok(Self { inner: RwLock::new(g) })
547    }
548}
549
550fn write_match_list(w: &mut impl Write, m: &[FeatureMatch]) -> Result<()> {
551    w.put_u64(m.len() as u64)?;
552    for x in m {
553        w.put_u32(x.idx1)?;
554        w.put_u32(x.idx2)?;
555    }
556    Ok(())
557}
558
559fn read_match_list(r: &mut impl Read) -> Result<Vec<FeatureMatch>> {
560    let n = check_count(r.get_u64()?, "매칭")?;
561    let mut v = Vec::with_capacity(n);
562    for _ in 0..n {
563        let a = r.get_u32()?;
564        let b = r.get_u32()?;
565        v.push(FeatureMatch::new(a, b));
566    }
567    Ok(v)
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn store_basic_and_roundtrip() {
576        let s = FeatureStore::new();
577        let mut cam = Camera::from_focal(CameraModelKind::OpenCv, 1000.0, 2000, 1000);
578        cam.focal_from_prior = true;
579        let c1 = s.add_camera(cam.clone()).unwrap();
580        assert_eq!(c1, 1);
581        let c2 = s.add_camera(cam).unwrap();
582        assert_eq!(c2, 2);
583        let i1 = s.add_image("camF/0.jpg", c1).unwrap();
584        let i2 = s.add_image("camR/0.jpg", c2).unwrap();
585        assert!(s.add_image("camF/0.jpg", c1).is_err());
586        assert_eq!(s.image_id_by_name("camR/0.jpg"), Some(i2));
587        s.set_keypoints(i1, vec![Keypoint::new(1.5, 2.5), Keypoint::from_scale_orientation(3.0, 4.0, 2.0, 0.5)]);
588        s.set_keypoints(i2, vec![Keypoint::new(0.5, 0.5)]);
589        let mut d = Descriptors::new();
590        d.push(&[7u8; 128]);
591        s.set_descriptors(i1, d);
592        s.set_pose_prior(i1, PosePrior { position: Vec3::new(37.5, 127.0, 50.0), coordinate_system: 0, gravity: None });
593        // reversed write
594        s.write_matches(i2, i1, &[FeatureMatch::new(0, 1)]).unwrap();
595        assert_eq!(s.read_matches(i1, i2).unwrap()[0], FeatureMatch::new(1, 0));
596        assert_eq!(s.read_matches(i2, i1).unwrap()[0], FeatureMatch::new(0, 1));
597        let h = Mat3::new(2.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0);
598        let tvg = TwoViewGeometry {
599            config: TwoViewGeometryConfig::PlanarOrRotation,
600            h: Some(h),
601            cam1_to_cam2: Some(Rigid3::new(crate::geometry::Quat::from_axis_angle(&Vec3::y(), 0.3), Vec3::new(1.0, 0.0, 0.0))),
602            inlier_matches: vec![FeatureMatch::new(0, 1)],
603            ..Default::default()
604        };
605        s.put_two_view(i2, i1, &tvg).unwrap();
606        let back = s.get_two_view(i2, i1).unwrap();
607        assert!((back.h.unwrap() - h).norm() < 1e-12);
608        let fwd = s.get_two_view(i1, i2).unwrap();
609        assert_eq!(fwd.inlier_matches[0], FeatureMatch::new(1, 0));
610        let p = fwd.cam1_to_cam2.unwrap() * tvg.cam1_to_cam2.unwrap();
611        assert!(p.rotation.angular_distance(&crate::geometry::Quat::IDENTITY) < 1e-7);
612        let (since, pos) = s.two_view_geometries_since(0);
613        assert_eq!(since.len(), 1);
614        assert_eq!(s.two_view_geometries_since(pos).0.len(), 0);
615
616        let dir = std::env::temp_dir().join(format!("cumulus3d_store_{}", std::process::id()));
617        std::fs::create_dir_all(&dir).unwrap();
618        let path = dir.join("store.bin");
619        s.save(&path).unwrap();
620        let l = FeatureStore::load(&path).unwrap();
621        assert_eq!(l.cameras(), s.cameras());
622        assert_eq!(l.images(), s.images());
623        assert_eq!(l.keypoints(i1), s.keypoints(i1));
624        assert_eq!(l.descriptors(i1), s.descriptors(i1));
625        assert_eq!(l.pose_prior(i1), s.pose_prior(i1));
626        assert_eq!(l.read_matches(i1, i2), s.read_matches(i1, i2));
627        assert_eq!(l.get_two_view(i1, i2), s.get_two_view(i1, i2));
628        // Files written with the old marker can also be read.
629        let mut bytes = std::fs::read(&path).unwrap();
630        bytes[..8].copy_from_slice(FeatureStore::LEGACY_MAGIC);
631        let legacy = dir.join("legacy.bin");
632        std::fs::write(&legacy, &bytes).unwrap();
633        assert_eq!(FeatureStore::load(&legacy).unwrap().images(), s.images());
634        std::fs::remove_dir_all(&dir).ok();
635    }
636
637    #[test]
638    fn store_concurrent_adds() {
639        let s = Arc::new(FeatureStore::new());
640        s.add_camera(Camera::from_focal(CameraModelKind::OpenCv, 1.0, 10, 10)).unwrap();
641        std::thread::scope(|sc| {
642            for t in 0..8 {
643                let s = s.clone();
644                sc.spawn(move || {
645                    for k in 0..50 {
646                        let id = s.add_image(&format!("t{t}/{k}"), 1).unwrap();
647                        s.set_keypoints(id, vec![Keypoint::new(0.5, 0.5); k]);
648                    }
649                });
650            }
651        });
652        assert_eq!(s.num_images(), 400);
653        assert_eq!(s.largest_keypoint_count(), 49);
654    }
655
656    #[test]
657    fn retain_copy_drops_images_and_pairs() {
658        let s = FeatureStore::new();
659        let c = s.add_camera(Camera::from_focal(CameraModelKind::OpenCv, 100.0, 64, 48)).unwrap();
660        let ids: Vec<ImageId> = (0..3).map(|i| s.add_image(&format!("cam/{i}.jpg"), c).unwrap()).collect();
661        for &i in &ids {
662            s.set_keypoints(i, vec![Keypoint::new(1.0, 1.0)]);
663        }
664        let tvg = TwoViewGeometry { inlier_matches: vec![FeatureMatch::new(0, 0)], ..Default::default() };
665        s.put_two_view(ids[0], ids[1], &tvg).unwrap();
666        s.put_two_view(ids[1], ids[2], &tvg).unwrap();
667        s.write_matches(ids[0], ids[2], &[FeatureMatch::new(0, 0)]).unwrap();
668        let r = s.retain_copy(|im| im.image_id != ids[2]);
669        assert_eq!(r.num_images(), 2);
670        assert_eq!(r.num_cameras(), 1);
671        assert!(r.image_id_by_name("cam/2.jpg").is_none());
672        assert!(r.contains_two_view(ids[0], ids[1]));
673        assert!(!r.contains_two_view(ids[1], ids[2]));
674        assert!(!r.exists_matches(ids[0], ids[2]));
675        assert_eq!(r.two_view_geometries_since(0).1, 1);
676        // The new image id is the remaining max id + 1.
677        assert_eq!(r.add_image("cam/9.jpg", c).unwrap(), ids[2]);
678        // The original store is unchanged.
679        assert_eq!(s.num_images(), 3);
680    }
681}