Skip to main content

cumulus3d_core/
reconstruction.rs

1/*
2 * reconstruction.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//! Sparse reconstruction data structures.
33//!
34//! Copy cost: images and 3D points are held in `Arc` and cloned with `Arc::make_mut` (copy-on-write) on modification.
35//! So `Reconstruction::clone()` only increments reference counts, and afterwards only the images/points that actually change are copied.
36//! The invariants (`check_invariants`) are maintained by every public mutating operation.
37
38use crate::camera::Camera;
39use crate::error::{Error, Result};
40use crate::geometry::{max_triangulation_angle, Rigid3, Sim3, Vec2, Vec3};
41use crate::ids::*;
42use rayon::prelude::*;
43use std::collections::{BTreeMap, HashMap};
44use std::sync::Arc;
45
46/// Track element (image id, 2D point index).
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
48pub struct TrackEntry {
49    /// Image id.
50    pub image_id: ImageId,
51    /// 2D point index within the image.
52    pub point2d_idx: Point2DIdx,
53}
54
55impl TrackEntry {
56    /// Creates from (image, 2D point).
57    pub fn new(image_id: ImageId, point2d_idx: Point2DIdx) -> Self {
58        Self { image_id, point2d_idx }
59    }
60}
61
62/// 2D point of an image: pixel coordinates + linked 3D point id (`INVALID_POINT3D_ID` if none).
63#[derive(Clone, Copy, Debug, PartialEq)]
64pub struct Point2D {
65    /// Pixel coordinates.
66    pub xy: Vec2,
67    /// Linked 3D point id.
68    pub point3d_id: Point3DId,
69}
70
71impl Point2D {
72    /// Creates a 2D point without a link.
73    pub fn new(xy: Vec2) -> Self {
74        Self { xy, point3d_id: INVALID_POINT3D_ID }
75    }
76    /// Whether it is linked to a 3D point.
77    pub fn has_point3d(&self) -> bool {
78        self.point3d_id != INVALID_POINT3D_ID
79    }
80}
81
82/// 3D point. error is the mean pixel reprojection error, −1 = none.
83#[derive(Clone, Debug, PartialEq)]
84pub struct Point3D {
85    /// World coordinates.
86    pub xyz: Vec3,
87    /// RGB color.
88    pub color: [u8; 3],
89    /// Mean reprojection error (pixels, −1 = none).
90    pub error: f64,
91    /// Observation track.
92    pub track: Vec<TrackEntry>,
93}
94
95impl Point3D {
96    /// Creates with an empty track, black color and no error.
97    pub fn new(xyz: Vec3) -> Self {
98        Self { xyz, color: [0, 0, 0], error: -1.0, track: Vec::new() }
99    }
100    /// Whether an error is recorded.
101    pub fn has_error(&self) -> bool {
102        self.error != -1.0
103    }
104    /// Track length.
105    pub fn track_len(&self) -> usize {
106        self.track.len()
107    }
108}
109
110/// Rig: reference sensor (pose = identity) + optional rig_to_sensor per non-reference sensor.
111#[derive(Clone, Debug, PartialEq)]
112pub struct Rig {
113    /// rig id.
114    pub rig_id: RigId,
115    /// Reference sensor.
116    pub ref_sensor_id: Option<SensorKey>,
117    /// Non-reference sensors (kept sorted). None = uncalibrated.
118    pub sensors: BTreeMap<SensorKey, Option<Rigid3>>,
119}
120
121impl Rig {
122    /// Empty rig.
123    pub fn new(rig_id: RigId) -> Self {
124        Self { rig_id, ref_sensor_id: None, sensors: BTreeMap::new() }
125    }
126    /// Single-camera rig (rig id = camera id).
127    pub fn trivial(camera_id: CameraId) -> Self {
128        Self { rig_id: camera_id, ref_sensor_id: Some(SensorKey::camera(camera_id)), sensors: BTreeMap::new() }
129    }
130    /// Number of sensors (including the reference).
131    pub fn num_sensors(&self) -> usize {
132        self.sensors.len() + self.ref_sensor_id.is_some() as usize
133    }
134    /// Whether it is the reference sensor.
135    pub fn is_reference_sensor(&self, s: SensorKey) -> bool {
136        self.ref_sensor_id == Some(s)
137    }
138    /// Whether the sensor is in the rig.
139    pub fn has_sensor(&self, s: SensorKey) -> bool {
140        self.is_reference_sensor(s) || self.sensors.contains_key(&s)
141    }
142    /// rig_to_sensor. Identity for the reference sensor, None if uncalibrated or absent.
143    pub fn rig_to_sensor(&self, s: SensorKey) -> Option<Rigid3> {
144        if self.is_reference_sensor(s) {
145            Some(Rigid3::identity())
146        } else {
147            self.sensors.get(&s).copied().flatten()
148        }
149    }
150}
151
152/// Frame: group of data of a rig at the same time + optional world_to_rig.
153#[derive(Clone, Debug, PartialEq)]
154pub struct Frame {
155    /// Frame id.
156    pub frame_id: FrameId,
157    /// Rig id it belongs to.
158    pub rig_id: RigId,
159    data_ids: Vec<SensorDataKey>,
160    /// World -> rig pose (undetermined if absent).
161    pub world_to_rig: Option<Rigid3>,
162}
163
164impl Frame {
165    /// Frame without data or pose.
166    pub fn new(frame_id: FrameId, rig_id: RigId) -> Self {
167        Self { frame_id, rig_id, data_ids: Vec::new(), world_to_rig: None }
168    }
169    /// Adds while keeping the sorted state (duplicates ignored).
170    pub fn attach_data(&mut self, d: SensorDataKey) {
171        if let Err(pos) = self.data_ids.binary_search(&d) {
172            self.data_ids.insert(pos, d);
173        }
174    }
175    /// List of data keys (sorted).
176    pub fn data_ids(&self) -> &[SensorDataKey] {
177        &self.data_ids
178    }
179    /// Whether a pose exists.
180    pub fn has_pose(&self) -> bool {
181        self.world_to_rig.is_some()
182    }
183    /// Image ids of the camera data (in data order).
184    pub fn image_ids(&self) -> impl Iterator<Item = ImageId> + '_ {
185        self.data_ids.iter().filter(|d| d.sensor_id.sensor_type == SensorKind::Camera).map(|d| d.id as ImageId)
186    }
187}
188
189/// Image. The pose is not stored but derived from the frame.
190#[derive(Clone, Debug, PartialEq)]
191pub struct Image {
192    /// Image id.
193    pub image_id: ImageId,
194    /// Must not contain whitespace (text format delimiter).
195    pub name: String,
196    /// Camera id.
197    pub camera_id: CameraId,
198    /// Frame id it belongs to.
199    pub frame_id: FrameId,
200    points2d: Vec<Point2D>,
201    num_points3d: usize,
202}
203
204impl Image {
205    /// Creates from 2D point coordinates (no links, frame unassigned).
206    pub fn new(image_id: ImageId, name: impl Into<String>, camera_id: CameraId, points: impl IntoIterator<Item = Vec2>) -> Self {
207        Self {
208            image_id,
209            name: name.into(),
210            camera_id,
211            frame_id: INVALID_FRAME_ID,
212            points2d: points.into_iter().map(Point2D::new).collect(),
213            num_points3d: 0,
214        }
215    }
216    /// All 2D points.
217    pub fn points2d(&self) -> &[Point2D] {
218        &self.points2d
219    }
220    /// The idx-th 2D point.
221    pub fn point2d(&self, idx: Point2DIdx) -> &Point2D {
222        &self.points2d[idx as usize]
223    }
224    /// Number of 2D points.
225    pub fn num_points2d(&self) -> usize {
226        self.points2d.len()
227    }
228    /// Number of 2D points linked to a 3D point (cached).
229    pub fn num_points3d(&self) -> usize {
230        self.num_points3d
231    }
232}
233
234/// How to update the point error after an observation filter.
235#[derive(Clone, Copy, Debug, PartialEq, Eq)]
236pub enum FilterErrorUpdate {
237    /// No update.
238    None,
239    /// (sum of inlier errors) / (track length before deletion)
240    SumOverOriginalLength,
241    /// Mean error of the remaining observations
242    MeanOfRemaining,
243}
244
245/// Normalization options.
246#[derive(Clone, Copy, Debug, PartialEq)]
247pub struct NormalizeOptions {
248    /// If true, scale 1 (translation only).
249    pub fixed_scale: bool,
250    /// Extent size after normalization.
251    pub extent: f64,
252    /// Lower quantile for computing the extent.
253    pub p0: f64,
254    /// Upper quantile for computing the extent.
255    pub p1: f64,
256    /// If true, use the projection centers of registered images; if false, use the 3D points.
257    pub use_images: bool,
258}
259
260impl Default for NormalizeOptions {
261    fn default() -> Self {
262        Self { fixed_scale: false, extent: 10.0, p0: 0.1, p1: 0.9, use_images: true }
263    }
264}
265
266/// Sparse reconstruction.
267#[derive(Clone, Debug, Default)]
268pub struct Reconstruction {
269    cameras: BTreeMap<CameraId, Camera>,
270    rigs: BTreeMap<RigId, Rig>,
271    frames: BTreeMap<FrameId, Frame>,
272    images: BTreeMap<ImageId, Arc<Image>>,
273    points3d: BTreeMap<Point3DId, Arc<Point3D>>,
274    /// Registration order.
275    registered_frames: Vec<FrameId>,
276    registered_image_count: usize,
277    max_point3d_id: Point3DId,
278}
279
280fn nf<T>(what: impl std::fmt::Display) -> Result<T> {
281    Err(Error::NotFound(what.to_string()))
282}
283
284impl Reconstruction {
285    /// Empty reconstruction.
286    pub fn new() -> Self {
287        Self::default()
288    }
289
290    // ================= Queries =================
291
292    /// All cameras (in id order).
293    pub fn cameras(&self) -> &BTreeMap<CameraId, Camera> {
294        &self.cameras
295    }
296    /// Camera lookup.
297    pub fn camera(&self, id: CameraId) -> Option<&Camera> {
298        self.cameras.get(&id)
299    }
300    /// Modifies camera intrinsics (independent of the invariants).
301    pub fn camera_mut(&mut self, id: CameraId) -> Option<&mut Camera> {
302        self.cameras.get_mut(&id)
303    }
304    /// All rigs (in id order).
305    pub fn rigs(&self) -> &BTreeMap<RigId, Rig> {
306        &self.rigs
307    }
308    /// Rig lookup.
309    pub fn rig(&self, id: RigId) -> Option<&Rig> {
310        self.rigs.get(&id)
311    }
312    /// Mutable rig lookup.
313    pub fn rig_mut(&mut self, id: RigId) -> Option<&mut Rig> {
314        self.rigs.get_mut(&id)
315    }
316    /// All frames (in id order).
317    pub fn frames(&self) -> &BTreeMap<FrameId, Frame> {
318        &self.frames
319    }
320    /// Frame lookup.
321    pub fn frame(&self, id: FrameId) -> Option<&Frame> {
322        self.frames.get(&id)
323    }
324    /// Images (ascending by id).
325    pub fn images(&self) -> impl Iterator<Item = &Image> + '_ {
326        self.images.values().map(|a| &**a)
327    }
328    /// Image lookup.
329    pub fn image(&self, id: ImageId) -> Option<&Image> {
330        self.images.get(&id).map(|a| &**a)
331    }
332    /// Image ids (ascending).
333    pub fn image_ids(&self) -> impl Iterator<Item = ImageId> + '_ {
334        self.images.keys().copied()
335    }
336    /// Whether the image exists.
337    pub fn exists_image(&self, id: ImageId) -> bool {
338        self.images.contains_key(&id)
339    }
340    /// Looks up an image by name.
341    pub fn image_by_name(&self, name: &str) -> Option<&Image> {
342        self.images().find(|im| im.name == name)
343    }
344    /// 3D points (ascending by id).
345    pub fn points3d(&self) -> impl Iterator<Item = (Point3DId, &Point3D)> + '_ {
346        self.points3d.iter().map(|(k, v)| (*k, &**v))
347    }
348    /// 3D point lookup.
349    pub fn point3d(&self, id: Point3DId) -> Option<&Point3D> {
350        self.points3d.get(&id).map(|a| &**a)
351    }
352    /// List of 3D point ids (ascending).
353    pub fn point3d_ids(&self) -> Vec<Point3DId> {
354        self.points3d.keys().copied().collect()
355    }
356    /// Whether the 3D point exists.
357    pub fn exists_point3d(&self, id: Point3DId) -> bool {
358        self.points3d.contains_key(&id)
359    }
360    /// Number of cameras.
361    pub fn num_cameras(&self) -> usize {
362        self.cameras.len()
363    }
364    /// Number of rigs.
365    pub fn num_rigs(&self) -> usize {
366        self.rigs.len()
367    }
368    /// Number of frames.
369    pub fn num_frames(&self) -> usize {
370        self.frames.len()
371    }
372    /// Number of images.
373    pub fn num_images(&self) -> usize {
374        self.images.len()
375    }
376    /// Number of 3D points.
377    pub fn num_points3d(&self) -> usize {
378        self.points3d.len()
379    }
380    /// Number of registered frames.
381    pub fn registered_frame_count(&self) -> usize {
382        self.registered_frames.len()
383    }
384    /// Number of registered images.
385    pub fn registered_image_count(&self) -> usize {
386        self.registered_image_count
387    }
388    /// Maximum 3D point id issued/observed so far.
389    pub fn max_point3d_id(&self) -> Point3DId {
390        self.max_point3d_id
391    }
392    /// Registered frames (in registration order).
393    pub fn registered_frames(&self) -> &[FrameId] {
394        &self.registered_frames
395    }
396    /// Registered images: registered frame order -> data order within the frame.
397    pub fn registered_images(&self) -> Vec<ImageId> {
398        let mut out = Vec::with_capacity(self.registered_image_count);
399        for fid in &self.registered_frames {
400            if let Some(f) = self.frames.get(fid) {
401                out.extend(f.image_ids().filter(|i| self.images.contains_key(i)));
402            }
403        }
404        out
405    }
406    /// Whether the frame is registered.
407    pub fn is_frame_registered(&self, frame_id: FrameId) -> bool {
408        self.registered_frames.contains(&frame_id)
409    }
410    /// Whether the image's frame is registered.
411    pub fn is_image_registered(&self, image_id: ImageId) -> bool {
412        self.image(image_id).is_some_and(|im| self.is_frame_registered(im.frame_id))
413    }
414
415    /// world_to_cam of the image. None if there is no pose.
416    pub fn world_to_cam(&self, image_id: ImageId) -> Option<Rigid3> {
417        let im = self.image(image_id)?;
418        let frame = self.frames.get(&im.frame_id)?;
419        let world_to_rig = frame.world_to_rig?;
420        let rig = self.rigs.get(&frame.rig_id)?;
421        let s = SensorKey::camera(im.camera_id);
422        if rig.is_reference_sensor(s) {
423            Some(world_to_rig)
424        } else {
425            Some(rig.rig_to_sensor(s)?.compose(&world_to_rig))
426        }
427    }
428    /// Whether the image has a pose.
429    pub fn has_pose(&self, image_id: ImageId) -> bool {
430        self.image(image_id).and_then(|im| self.frames.get(&im.frame_id)).is_some_and(|f| f.has_pose())
431    }
432    /// Projection center C = −Rᵀ t.
433    pub fn projection_center(&self, image_id: ImageId) -> Option<Vec3> {
434        self.world_to_cam(image_id).map(|p| p.center())
435    }
436
437    // ================= Adding =================
438
439    /// Adds a camera (error if the id is duplicated).
440    pub fn add_camera(&mut self, camera: Camera) -> Result<()> {
441        if self.cameras.contains_key(&camera.camera_id) {
442            return Err(Error::AlreadyExists(format!("카메라 {}", camera.camera_id)));
443        }
444        if !camera.verify_params() {
445            return Err(Error::InvalidArgument(format!("카메라 {} 파라미터 개수", camera.camera_id)));
446        }
447        self.cameras.insert(camera.camera_id, camera);
448        Ok(())
449    }
450    /// Adds a camera together with a single-camera rig (rig id = camera id). Skipped if the rig already exists.
451    pub fn add_camera_own_rig(&mut self, camera: Camera) -> Result<()> {
452        let id = camera.camera_id;
453        self.add_camera(camera)?;
454        self.rigs.entry(id).or_insert_with(|| Rig::trivial(id));
455        Ok(())
456    }
457    /// Adds a rig (error if the id is duplicated).
458    pub fn add_rig(&mut self, rig: Rig) -> Result<()> {
459        if self.rigs.contains_key(&rig.rig_id) {
460            return Err(Error::AlreadyExists(format!("rig {}", rig.rig_id)));
461        }
462        self.rigs.insert(rig.rig_id, rig);
463        Ok(())
464    }
465    /// Adds a frame (not registered).
466    pub fn add_frame(&mut self, frame: Frame) -> Result<()> {
467        if self.frames.contains_key(&frame.frame_id) {
468            return Err(Error::AlreadyExists(format!("프레임 {}", frame.frame_id)));
469        }
470        if !self.rigs.contains_key(&frame.rig_id) {
471            return nf(format!("rig {}", frame.rig_id));
472        }
473        self.frames.insert(frame.frame_id, frame);
474        Ok(())
475    }
476    /// Adds an image. The camera must exist, and if frame_id is valid that frame must exist.
477    /// Existing 3D links of the 2D points are cleared (links are created only by adding 3D points).
478    pub fn add_image(&mut self, mut image: Image) -> Result<()> {
479        if self.images.contains_key(&image.image_id) {
480            return Err(Error::AlreadyExists(format!("영상 {}", image.image_id)));
481        }
482        if !self.cameras.contains_key(&image.camera_id) {
483            return nf(format!("카메라 {}", image.camera_id));
484        }
485        if image.frame_id != INVALID_FRAME_ID && !self.frames.contains_key(&image.frame_id) {
486            return nf(format!("프레임 {}", image.frame_id));
487        }
488        for p in image.points2d.iter_mut() {
489            p.point3d_id = INVALID_POINT3D_ID;
490        }
491        image.num_points3d = 0;
492        self.images.insert(image.image_id, Arc::new(image));
493        Ok(())
494    }
495    /// Adds an image + trivial frame (frame id = image id, rig id = camera id). Creates the rig if absent.
496    pub fn add_image_own_frame(&mut self, mut image: Image, world_to_cam: Option<Rigid3>) -> Result<()> {
497        if !self.cameras.contains_key(&image.camera_id) {
498            return nf(format!("카메라 {}", image.camera_id));
499        }
500        if self.images.contains_key(&image.image_id) {
501            return Err(Error::AlreadyExists(format!("영상 {}", image.image_id)));
502        }
503        let cam = image.camera_id;
504        self.rigs.entry(cam).or_insert_with(|| Rig::trivial(cam));
505        let mut frame = Frame::new(image.image_id, cam);
506        frame.attach_data(SensorDataKey::image(cam, image.image_id));
507        frame.world_to_rig = world_to_cam;
508        self.add_frame(frame)?;
509        image.frame_id = image.image_id;
510        self.add_image(image)
511    }
512
513    // ================= Pose and registration =================
514
515    /// Sets/removes a frame pose. The pose of a registered frame cannot be removed.
516    pub fn set_frame_pose(&mut self, frame_id: FrameId, world_to_rig: Option<Rigid3>) -> Result<()> {
517        if world_to_rig.is_none() && self.is_frame_registered(frame_id) {
518            return Err(Error::InvalidArgument(format!("등록된 프레임 {frame_id} 의 자세 제거")));
519        }
520        match self.frames.get_mut(&frame_id) {
521            Some(f) => {
522                f.world_to_rig = world_to_rig;
523                Ok(())
524            }
525            None => nf(format!("프레임 {frame_id}")),
526        }
527    }
528    /// Sets the frame pose from the image's world_to_cam (back-computes world_to_rig for a non-reference sensor).
529    pub fn set_world_to_cam(&mut self, image_id: ImageId, world_to_cam: Rigid3) -> Result<()> {
530        let im = self.image(image_id).ok_or_else(|| Error::NotFound(format!("영상 {image_id}")))?;
531        let (fid, s) = (im.frame_id, SensorKey::camera(im.camera_id));
532        let frame = self.frames.get(&fid).ok_or_else(|| Error::NotFound(format!("프레임 {fid}")))?;
533        let rig = self.rigs.get(&frame.rig_id).ok_or_else(|| Error::NotFound(format!("rig {}", frame.rig_id)))?;
534        let world_to_rig = if rig.is_reference_sensor(s) {
535            world_to_cam
536        } else {
537            let rig_to_cam =
538                rig.rig_to_sensor(s).ok_or_else(|| Error::InvalidArgument(format!("영상 {image_id} 센서가 rig 에서 미보정")))?;
539            rig_to_cam.inverse().compose(&world_to_cam)
540        };
541        self.frames.get_mut(&fid).expect("checked").world_to_rig = Some(world_to_rig);
542        Ok(())
543    }
544    /// Registers a frame (pose required). Ok(false) if already registered.
545    pub fn register_frame(&mut self, frame_id: FrameId) -> Result<bool> {
546        let f = self.frames.get(&frame_id).ok_or_else(|| Error::NotFound(format!("프레임 {frame_id}")))?;
547        if !f.has_pose() {
548            return Err(Error::InvalidArgument(format!("자세 없는 프레임 {frame_id} 등록")));
549        }
550        if self.is_frame_registered(frame_id) {
551            return Ok(false);
552        }
553        let n = f.image_ids().filter(|i| self.images.contains_key(i)).count();
554        self.registered_frames.push(frame_id);
555        self.registered_image_count += n;
556        Ok(true)
557    }
558    /// Registers the frame the image belongs to (`register_frame`).
559    pub fn register_image(&mut self, image_id: ImageId) -> Result<bool> {
560        let fid = self.image(image_id).ok_or_else(|| Error::NotFound(format!("영상 {image_id}")))?.frame_id;
561        self.register_frame(fid)
562    }
563    /// Deregisters a frame: deletes each image's observations in ascending 2D point index order ->
564    /// decreases the registered image count -> removes the pose -> removes from the registration list. Ok(false) if not registered.
565    pub fn deregister_frame(&mut self, frame_id: FrameId) -> Result<bool> {
566        let Some(pos) = self.registered_frames.iter().position(|f| *f == frame_id) else {
567            return Ok(false);
568        };
569        let image_ids: Vec<ImageId> = self.frames[&frame_id].image_ids().filter(|i| self.images.contains_key(i)).collect();
570        for &iid in &image_ids {
571            let n = self.images[&iid].points2d.len();
572            for idx in 0..n {
573                if self.images[&iid].points2d[idx].has_point3d() {
574                    self.delete_observation(iid, idx as Point2DIdx)?;
575                }
576            }
577        }
578        self.registered_image_count -= image_ids.len();
579        if let Some(f) = self.frames.get_mut(&frame_id) {
580            f.world_to_rig = None;
581        }
582        self.registered_frames.remove(pos);
583        Ok(true)
584    }
585    /// Deregisters the frame the image belongs to (`deregister_frame`).
586    pub fn deregister_image(&mut self, image_id: ImageId) -> Result<bool> {
587        let fid = self.image(image_id).ok_or_else(|| Error::NotFound(format!("영상 {image_id}")))?.frame_id;
588        self.deregister_frame(fid)
589    }
590
591    // ================= 3D points and observations =================
592
593    fn link(&mut self, e: TrackEntry, pid: Point3DId) {
594        let im = Arc::make_mut(self.images.get_mut(&e.image_id).expect("검증됨"));
595        let p = &mut im.points2d[e.point2d_idx as usize];
596        if !p.has_point3d() {
597            im.num_points3d += 1;
598        }
599        p.point3d_id = pid;
600    }
601    fn unlink(&mut self, e: TrackEntry, pid: Point3DId) {
602        if let Some(a) = self.images.get_mut(&e.image_id) {
603            if a.points2d.get(e.point2d_idx as usize).is_some_and(|p| p.point3d_id == pid) {
604                let im = Arc::make_mut(a);
605                im.points2d[e.point2d_idx as usize].point3d_id = INVALID_POINT3D_ID;
606                im.num_points3d -= 1;
607            }
608        }
609    }
610    fn check_element(&self, e: &TrackEntry, pid: Point3DId) -> Result<()> {
611        let im = self.image(e.image_id).ok_or_else(|| Error::NotFound(format!("영상 {}", e.image_id)))?;
612        let p = im
613            .points2d
614            .get(e.point2d_idx as usize)
615            .ok_or_else(|| Error::InvalidArgument(format!("영상 {} 의 2D 점 {} 범위 밖", e.image_id, e.point2d_idx)))?;
616        if p.has_point3d() && p.point3d_id != pid {
617            return Err(Error::Invariant(format!("영상 {} 2D 점 {} 은 이미 3D 점 {} 에 연결됨", e.image_id, e.point2d_idx, p.point3d_id)));
618        }
619        Ok(())
620    }
621
622    /// Adds a new 3D point (id = max id + 1). Error if a 2D point of a track element is linked to another point.
623    pub fn add_point3d(&mut self, xyz: Vec3, track: Vec<TrackEntry>, color: [u8; 3]) -> Result<Point3DId> {
624        // Design decision: the first id of an empty reconstruction. In the "max id + 1" rule the initial maximum is 0 -> first id
625        // 1.
626        let id = self.max_point3d_id + 1;
627        self.add_point3d_with_id(id, Point3D { xyz, color, error: -1.0, track })?;
628        Ok(id)
629    }
630    /// Adds a 3D point with the given id (for file reading and the global mapper). Updates the max id.
631    pub fn add_point3d_with_id(&mut self, id: Point3DId, point: Point3D) -> Result<()> {
632        if id == INVALID_POINT3D_ID {
633            return Err(Error::InvalidArgument("무효 3D 점 id".into()));
634        }
635        if self.points3d.contains_key(&id) {
636            return Err(Error::AlreadyExists(format!("3D 점 {id}")));
637        }
638        for e in &point.track {
639            self.check_element(e, id)?;
640        }
641        for e in point.track.clone() {
642            self.link(e, id);
643        }
644        self.points3d.insert(id, Arc::new(point));
645        self.max_point3d_id = self.max_point3d_id.max(id);
646        Ok(())
647    }
648    /// Adds an observation to an existing 3D point.
649    pub fn add_observation(&mut self, point3d_id: Point3DId, e: TrackEntry) -> Result<()> {
650        if !self.points3d.contains_key(&point3d_id) {
651            return nf(format!("3D 점 {point3d_id}"));
652        }
653        self.check_element(&e, point3d_id)?;
654        if self.image(e.image_id).expect("검증됨").points2d[e.point2d_idx as usize].has_point3d() {
655            return Err(Error::Invariant(format!("관측 ({}, {}) 중복", e.image_id, e.point2d_idx)));
656        }
657        self.link(e, point3d_id);
658        Arc::make_mut(self.points3d.get_mut(&point3d_id).expect("검증됨")).track.push(e);
659        Ok(())
660    }
661    /// Deletes an observation: if track length <= 2 the whole 3D point is deleted, otherwise only that element is removed.
662    /// Returns: true if the whole point was deleted.
663    pub fn delete_observation(&mut self, image_id: ImageId, point2d_idx: Point2DIdx) -> Result<bool> {
664        let im = self.image(image_id).ok_or_else(|| Error::NotFound(format!("영상 {image_id}")))?;
665        let pid =
666            im.points2d.get(point2d_idx as usize).ok_or_else(|| Error::InvalidArgument(format!("2D 점 {point2d_idx} 범위 밖")))?.point3d_id;
667        if pid == INVALID_POINT3D_ID {
668            return Err(Error::InvalidArgument(format!("영상 {image_id} 2D 점 {point2d_idx} 에 3D 점 없음")));
669        }
670        let e = TrackEntry::new(image_id, point2d_idx);
671        if self.points3d[&pid].track.len() <= 2 {
672            self.delete_point3d(pid)?;
673            return Ok(true);
674        }
675        let p = Arc::make_mut(self.points3d.get_mut(&pid).expect("연결 불변식"));
676        if let Some(pos) = p.track.iter().position(|t| *t == e) {
677            p.track.remove(pos);
678        }
679        self.unlink(e, pid);
680        Ok(false)
681    }
682    /// Deletes a 3D point (unlinks all observations).
683    pub fn delete_point3d(&mut self, id: Point3DId) -> Result<()> {
684        let p = self.points3d.remove(&id).ok_or_else(|| Error::NotFound(format!("3D 점 {id}")))?;
685        for e in &p.track {
686            self.unlink(*e, id);
687        }
688        Ok(())
689    }
690    /// Merges two 3D points: track-length-weighted mean position and color (color truncated), tracks concatenated (a then b), new id.
691    pub fn merge_points3d(&mut self, a: Point3DId, b: Point3DId) -> Result<Point3DId> {
692        if a == b {
693            return Err(Error::InvalidArgument("같은 점 병합".into()));
694        }
695        let pa = self.points3d.get(&a).ok_or_else(|| Error::NotFound(format!("3D 점 {a}")))?.clone();
696        let pb = self.points3d.get(&b).ok_or_else(|| Error::NotFound(format!("3D 점 {b}")))?.clone();
697        let (wa, wb) = (pa.track.len() as f64, pb.track.len() as f64);
698        let w = (wa + wb).max(1.0);
699        let xyz = (pa.xyz * wa + pb.xyz * wb) / w;
700        let mut color = [0u8; 3];
701        for (k, c) in color.iter_mut().enumerate() {
702            *c = ((pa.color[k] as f64 * wa + pb.color[k] as f64 * wb) / w) as u8;
703        }
704        let mut track = pa.track.clone();
705        track.extend_from_slice(&pb.track);
706        self.delete_point3d(a)?;
707        self.delete_point3d(b)?;
708        // Design decision: the error of the merged point is set to −1 (none). A later stage recomputes it.
709        self.add_point3d(xyz, track, color)
710    }
711    /// Sets a 3D point position.
712    pub fn set_point3d_xyz(&mut self, id: Point3DId, xyz: Vec3) -> Result<()> {
713        let p = self.points3d.get_mut(&id).ok_or_else(|| Error::NotFound(format!("3D 점 {id}")))?;
714        Arc::make_mut(p).xyz = xyz;
715        Ok(())
716    }
717    /// Sets a 3D point error.
718    pub fn set_point3d_error(&mut self, id: Point3DId, error: f64) -> Result<()> {
719        let p = self.points3d.get_mut(&id).ok_or_else(|| Error::NotFound(format!("3D 점 {id}")))?;
720        Arc::make_mut(p).error = error;
721        Ok(())
722    }
723    /// Sets a 3D point color.
724    pub fn set_point3d_color(&mut self, id: Point3DId, color: [u8; 3]) -> Result<()> {
725        let p = self.points3d.get_mut(&id).ok_or_else(|| Error::NotFound(format!("3D 점 {id}")))?;
726        Arc::make_mut(p).color = color;
727        Ok(())
728    }
729    /// Deletes all 3D points (unlinks observations).
730    pub fn delete_all_points3d(&mut self) {
731        self.points3d.clear();
732        for a in self.images.values_mut() {
733            if a.num_points3d > 0 {
734                let im = Arc::make_mut(a);
735                for p in im.points2d.iter_mut() {
736                    p.point3d_id = INVALID_POINT3D_ID;
737                }
738                im.num_points3d = 0;
739            }
740        }
741    }
742
743    // ================= Statistics =================
744
745    /// Sum over registered images of "number of 2D points linked to a 3D point".
746    pub fn total_observations(&self) -> usize {
747        self.registered_images().iter().map(|i| self.images[i].num_points3d).sum()
748    }
749    /// Mean track length of the 3D points.
750    pub fn mean_track_len(&self) -> f64 {
751        if self.points3d.is_empty() {
752            0.0
753        } else {
754            self.total_observations() as f64 / self.points3d.len() as f64
755        }
756    }
757    /// Mean number of observations per registered image.
758    pub fn mean_obs_per_registered_image(&self) -> f64 {
759        if self.registered_image_count == 0 {
760            0.0
761        } else {
762            self.total_observations() as f64 / self.registered_image_count as f64
763        }
764    }
765    /// Mean of the stored point errors that are not −1 (not recomputed). 0 if none.
766    pub fn mean_reproj_error(&self) -> f64 {
767        let (mut s, mut n) = (0.0, 0usize);
768        for p in self.points3d.values() {
769            if p.has_error() {
770                s += p.error;
771                n += 1;
772            }
773        }
774        if n == 0 {
775            0.0
776        } else {
777            s / n as f64
778        }
779    }
780
781    // ================= Reprojection error =================
782
783    /// Per-image (world_to_cam, camera) cache.
784    fn pose_cache(&self) -> HashMap<ImageId, (Rigid3, &Camera)> {
785        let mut m = HashMap::with_capacity(self.images.len());
786        for im in self.images() {
787            if let (Some(p), Some(c)) = (self.world_to_cam(im.image_id), self.cameras.get(&im.camera_id)) {
788                m.insert(im.image_id, (p, c));
789            }
790        }
791        m
792    }
793
794    /// Pixel reprojection squared error. f64::MAX if projection fails (depth < ε).
795    pub fn squared_reprojection_error(pose: &Rigid3, camera: &Camera, xy: &Vec2, xyz: &Vec3) -> f64 {
796        match camera.cam_to_img(&pose.transform_point(xyz)) {
797            Some(p) => (p - xy).norm_squared(),
798            None => f64::MAX,
799        }
800    }
801
802    fn compute_point_error(&self, cache: &HashMap<ImageId, (Rigid3, &Camera)>, p: &Point3D) -> f64 {
803        if p.track.is_empty() {
804            return 0.0;
805        }
806        let mut s = 0.0;
807        for e in &p.track {
808            let sq = match cache.get(&e.image_id) {
809                Some((pose, cam)) => {
810                    let xy = self.images[&e.image_id].points2d[e.point2d_idx as usize].xy;
811                    Self::squared_reprojection_error(pose, cam, &xy, &p.xyz)
812                }
813                None => f64::MAX,
814            };
815            s += sq.sqrt();
816        }
817        s / p.track.len() as f64
818    }
819
820    /// Recomputes the error of every 3D point as the mean pixel reprojection error.
821    pub fn update_point3d_errors(&mut self) {
822        let cache = self.pose_cache();
823        let errs: Vec<(Point3DId, f64)> = self.points3d.par_iter().map(|(id, p)| (*id, self.compute_point_error(&cache, p))).collect();
824        drop(cache);
825        for (id, e) in errs {
826            let p = self.points3d.get_mut(&id).expect("존재");
827            if p.error != e {
828                Arc::make_mut(p).error = e;
829            }
830        }
831    }
832    /// Mean pixel reprojection error of one point (not stored).
833    pub fn point3d_reprojection_error(&self, id: Point3DId) -> Option<f64> {
834        let p = self.point3d(id)?;
835        let mut cache = HashMap::new();
836        for e in &p.track {
837            if let (Some(pose), Some(im)) = (self.world_to_cam(e.image_id), self.image(e.image_id)) {
838                cache.insert(e.image_id, (pose, &self.cameras[&im.camera_id]));
839            }
840        }
841        Some(self.compute_point_error(&cache, p))
842    }
843
844    // ================= Filters =================
845
846    /// General observation filter: for each observation computes `error_fn(camera, world_to_cam, 2D coordinates, 3D point)`
847    /// and marks those exceeding max_error (including NaN) for deletion. Points with track length < 2 are deleted immediately;
848    /// if the number marked is (length − 1) or more the whole point is deleted, otherwise only those observations.
849    /// The observation error of an image without a pose is +∞. Returns: number of deleted observations.
850    pub fn filter_observations<F>(
851        &mut self,
852        point_ids: Option<&[Point3DId]>,
853        max_error: f64,
854        error_fn: F,
855        update: FilterErrorUpdate,
856    ) -> usize
857    where
858        F: Fn(&Camera, &Rigid3, &Vec2, &Vec3) -> f64 + Sync,
859    {
860        let ids: Vec<Point3DId> = match point_ids {
861            Some(v) => v.iter().copied().filter(|i| self.points3d.contains_key(i)).collect(),
862            None => self.point3d_ids(),
863        };
864        let cache = self.pose_cache();
865        // (point id, per-observation errors)
866        let evals: Vec<(Point3DId, Vec<f64>)> = ids
867            .par_iter()
868            .map(|id| {
869                let p = &self.points3d[id];
870                let errs = p
871                    .track
872                    .iter()
873                    .map(|e| match cache.get(&e.image_id) {
874                        Some((pose, cam)) => {
875                            let xy = self.images[&e.image_id].points2d[e.point2d_idx as usize].xy;
876                            error_fn(cam, pose, &xy, &p.xyz)
877                        }
878                        None => f64::INFINITY,
879                    })
880                    .collect();
881                (*id, errs)
882            })
883            .collect();
884        drop(cache);
885        self.apply_observation_filter(evals, max_error, update)
886    }
887
888    /// [`filter_observations`](Self::filter_observations) 의 삭제 단계: 미리 계산한 관측별 오차
889    /// (`evals` = (점 id, 트랙 순서의 오차))로 같은 규칙을 적용한다. 오차는 GPU 등 바깥에서 한꺼번에
890    /// 계산할 수 있다. 없는 점이나 트랙 길이와 오차 수가 다른 항목은 건너뛴다. 반환: 삭제된 관측 수.
891    pub fn apply_observation_filter(&mut self, evals: Vec<(Point3DId, Vec<f64>)>, max_error: f64, update: FilterErrorUpdate) -> usize {
892        let mut removed = 0usize;
893        for (id, errs) in evals {
894            let Some(p) = self.points3d.get(&id) else { continue };
895            let track = p.track.clone();
896            let len = track.len();
897            if errs.len() != len {
898                continue;
899            }
900            if len < 2 {
901                removed += len;
902                self.delete_point3d(id).expect("존재");
903                continue;
904            }
905            let bad: Vec<usize> = (0..len).filter(|&k| errs[k].is_nan() || errs[k] > max_error).collect();
906            if bad.len() >= len - 1 {
907                removed += len;
908                self.delete_point3d(id).expect("존재");
909                continue;
910            }
911            let mut point_deleted = false;
912            for &k in &bad {
913                removed += 1;
914                if self.delete_observation(track[k].image_id, track[k].point2d_idx).expect("연결 불변식") {
915                    point_deleted = true;
916                    break;
917                }
918            }
919            if point_deleted {
920                continue;
921            }
922            let inl: Vec<f64> = (0..len).filter(|k| !bad.contains(k)).map(|k| errs[k]).collect();
923            let new_err = match update {
924                FilterErrorUpdate::None => None,
925                FilterErrorUpdate::SumOverOriginalLength => Some(inl.iter().sum::<f64>() / len as f64),
926                FilterErrorUpdate::MeanOfRemaining => Some(inl.iter().sum::<f64>() / inl.len() as f64),
927            };
928            if let Some(e) = new_err {
929                self.set_point3d_error(id, e).expect("존재");
930            }
931        }
932        removed
933    }
934
935    /// Pixel reprojection error filter. Returns: number of deleted observations.
936    pub fn filter_observations_with_large_reprojection_error(
937        &mut self,
938        max_reproj_error_px: f64,
939        point_ids: Option<&[Point3DId]>,
940    ) -> usize {
941        self.filter_observations(
942            point_ids,
943            max_reproj_error_px,
944            |cam, pose, xy, xyz| Self::squared_reprojection_error(pose, cam, xy, xyz).sqrt(),
945            FilterErrorUpdate::SumOverOriginalLength,
946        )
947    }
948
949    /// Triangulation angle filter: keep if any track image pair has angle >= min_tri_angle_deg, otherwise delete the point.
950    /// Returns: number of deleted observations.
951    pub fn filter_points3d_with_small_triangulation_angle(&mut self, min_tri_angle_deg: f64, point_ids: Option<&[Point3DId]>) -> usize {
952        let min_rad = min_tri_angle_deg.to_radians();
953        let ids: Vec<Point3DId> = match point_ids {
954            Some(v) => v.iter().copied().filter(|i| self.points3d.contains_key(i)).collect(),
955            None => self.point3d_ids(),
956        };
957        let centers: HashMap<ImageId, Vec3> = self.pose_cache().into_iter().map(|(k, (p, _))| (k, p.center())).collect();
958        let to_delete: Vec<(Point3DId, usize)> = ids
959            .par_iter()
960            .filter_map(|id| {
961                let p = &self.points3d[id];
962                let cs: Vec<Vec3> = p.track.iter().filter_map(|e| centers.get(&e.image_id).copied()).collect();
963                if max_triangulation_angle(&cs, &p.xyz) >= min_rad {
964                    None
965                } else {
966                    Some((*id, p.track.len()))
967                }
968            })
969            .collect();
970        let mut removed = 0;
971        for (id, n) in to_delete {
972            removed += n;
973            self.delete_point3d(id).expect("존재");
974        }
975        removed
976    }
977
978    // ================= Transform and normalization =================
979
980    /// Applies a similarity transform: points, frame poses, rig sensor translations (times s).
981    pub fn transform(&mut self, t: &Sim3) {
982        for p in self.points3d.values_mut() {
983            let m = Arc::make_mut(p);
984            m.xyz = t.transform_point(&m.xyz);
985        }
986        for f in self.frames.values_mut() {
987            if let Some(p) = f.world_to_rig {
988                f.world_to_rig = Some(t.transform_pose(&p));
989            }
990        }
991        for r in self.rigs.values_mut() {
992            for v in r.sensors.values_mut().flatten() {
993                v.translation *= t.scale;
994            }
995        }
996    }
997
998    /// Normalization. Returns the applied Sim3; None if fewer than 2 registered frames.
999    pub fn normalize(&mut self, opts: &NormalizeOptions) -> Option<Sim3> {
1000        if self.registered_frames.len() < 2 {
1001            return None;
1002        }
1003        let coords: Vec<Vec3> = if opts.use_images {
1004            self.registered_images().iter().filter_map(|i| self.projection_center(*i)).collect()
1005        } else {
1006            self.points3d.values().map(|p| p.xyz).collect()
1007        };
1008        if coords.is_empty() {
1009            return None;
1010        }
1011        let n = coords.len();
1012        let e = n - 1;
1013        let lo = e.min((opts.p0 * e as f64).floor() as usize);
1014        let hi = e.min((opts.p1 * e as f64).ceil() as usize);
1015        let mut bmin = Vec3::zeros();
1016        let mut bmax = Vec3::zeros();
1017        let mut center = Vec3::zeros();
1018        for ax in 0..3 {
1019            let mut v: Vec<f64> = coords.iter().map(|c| c[ax]).collect();
1020            v.sort_by(|a, b| a.total_cmp(b));
1021            bmin[ax] = v[lo];
1022            bmax[ax] = v[hi];
1023            let slice = &v[lo..=hi];
1024            center[ax] = slice.iter().sum::<f64>() / slice.len() as f64;
1025        }
1026        let diag = (bmax - bmin).norm();
1027        let s = if opts.fixed_scale || diag < f64::EPSILON { 1.0 } else { opts.extent / diag };
1028        let t = Sim3::new(s, crate::geometry::Quat::IDENTITY, -s * center);
1029        self.transform(&t);
1030        Some(t)
1031    }
1032
1033    // ================= Image deletion (image_deleter) =================
1034
1035    /// Deregisters the frames the images in an id list belong to. Returns: list of warning messages.
1036    pub fn deregister_images_by_id(&mut self, ids: &[ImageId]) -> Vec<String> {
1037        let mut warnings = Vec::new();
1038        for &id in ids {
1039            match self.image(id).map(|im| im.frame_id) {
1040                None => warnings.push(format!("Image with ID {id} does not exist")),
1041                Some(fid) => match self.deregister_frame(fid) {
1042                    Ok(true) => {}
1043                    _ => warnings.push(format!("Image with ID {id} is not registered")),
1044                },
1045            }
1046        }
1047        warnings
1048    }
1049    /// Deregisters the images in a name list. Returns: list of warning messages.
1050    pub fn deregister_images_by_name(&mut self, names: &[&str]) -> Vec<String> {
1051        let mut warnings = Vec::new();
1052        for &name in names {
1053            match self.image_by_name(name).map(|im| im.image_id) {
1054                None => warnings.push(format!("Image with name {name} does not exist")),
1055                Some(id) => warnings.extend(self.deregister_images_by_id(&[id])),
1056            }
1057        }
1058        warnings
1059    }
1060
1061    /// Deletes unregistered frames and images, and also deletes unused cameras and rigs.
1062    pub fn remove_unregistered(&mut self) {
1063        let unreg: Vec<ImageId> = self.images.keys().copied().filter(|i| !self.is_image_registered(*i)).collect();
1064        for &iid in &unreg {
1065            let n = self.images[&iid].points2d.len();
1066            for idx in 0..n {
1067                if self.images[&iid].points2d[idx].has_point3d() {
1068                    let _ = self.delete_observation(iid, idx as Point2DIdx);
1069                }
1070            }
1071            self.images.remove(&iid);
1072        }
1073        let reg: std::collections::HashSet<FrameId> = self.registered_frames.iter().copied().collect();
1074        self.frames.retain(|k, _| reg.contains(k));
1075        let used_cams: std::collections::HashSet<CameraId> = self.images.values().map(|im| im.camera_id).collect();
1076        let used_rigs: std::collections::HashSet<RigId> = self.frames.values().map(|f| f.rig_id).collect();
1077        self.cameras.retain(|k, _| used_cams.contains(k));
1078        self.rigs.retain(|k, _| used_rigs.contains(k));
1079    }
1080
1081    // ================= Color =================
1082
1083    /// Color extraction: for each registered image gets a sampler via `load(image)`,
1084    /// passes each observation's (x − 0.5, y − 0.5), and rounds the per-point mean. Points with no samples are black.
1085    pub fn extract_colors<S, F>(&mut self, mut load: F)
1086    where
1087        F: FnMut(&Image) -> Option<S>,
1088        S: Fn(f64, f64) -> Option<[f64; 3]>,
1089    {
1090        let mut acc: HashMap<Point3DId, ([f64; 3], usize)> = HashMap::new();
1091        for iid in self.registered_images() {
1092            let im = &self.images[&iid];
1093            let Some(sampler) = load(im) else { continue };
1094            for p in im.points2d.iter().filter(|p| p.has_point3d()) {
1095                if let Some(c) = sampler(p.xy.x - 0.5, p.xy.y - 0.5) {
1096                    let a = acc.entry(p.point3d_id).or_insert(([0.0; 3], 0));
1097                    for (dst, v) in a.0.iter_mut().zip(c) {
1098                        *dst += v;
1099                    }
1100                    a.1 += 1;
1101                }
1102            }
1103        }
1104        for (id, p) in self.points3d.iter_mut() {
1105            let color = match acc.get(id) {
1106                Some((s, n)) => s.map(|v| (v / *n as f64).round().clamp(0.0, 255.0) as u8),
1107                None => [0, 0, 0],
1108            };
1109            if p.color != color {
1110                Arc::make_mut(p).color = color;
1111            }
1112        }
1113    }
1114
1115    // ================= Checks =================
1116
1117    /// Invariant check: bidirectional links, per-image link count cache, registered image count.
1118    pub fn check_invariants(&self) -> Result<()> {
1119        let bad = |m: String| Err(Error::Invariant(m));
1120        for (pid, p) in &self.points3d {
1121            for e in &p.track {
1122                let Some(im) = self.image(e.image_id) else { return bad(format!("점 {pid}: 영상 {} 없음", e.image_id)) };
1123                match im.points2d.get(e.point2d_idx as usize) {
1124                    Some(q) if q.point3d_id == *pid => {}
1125                    _ => return bad(format!("점 {pid}: ({}, {}) 역연결 불일치", e.image_id, e.point2d_idx)),
1126                }
1127            }
1128        }
1129        for im in self.images() {
1130            let mut n = 0;
1131            for (idx, q) in im.points2d.iter().enumerate() {
1132                if q.has_point3d() {
1133                    n += 1;
1134                    let Some(p) = self.point3d(q.point3d_id) else {
1135                        return bad(format!("영상 {} 2D {idx}: 3D 점 {} 없음", im.image_id, q.point3d_id));
1136                    };
1137                    if !p.track.contains(&TrackEntry::new(im.image_id, idx as u32)) {
1138                        return bad(format!("영상 {} 2D {idx}: 트랙에 없음", im.image_id));
1139                    }
1140                }
1141            }
1142            if n != im.num_points3d {
1143                return bad(format!("영상 {} 연결 수 캐시 {} != {}", im.image_id, im.num_points3d, n));
1144            }
1145        }
1146        if self.registered_images().len() != self.registered_image_count {
1147            return bad("등록 영상 수 불일치".into());
1148        }
1149        if let Some(m) = self.points3d.keys().next_back() {
1150            if *m > self.max_point3d_id {
1151                return bad("최대 3D 점 id 불일치".into());
1152            }
1153        }
1154        Ok(())
1155    }
1156}
1157
1158/// Bilinear interpolation from a u8 RGB (row-major, 3-channel) image. (x, y) are coordinates where the pixel center is an integer.
1159/// Returns None if out of range.
1160pub fn bilinear_rgb(data: &[u8], width: usize, height: usize, x: f64, y: f64) -> Option<[f64; 3]> {
1161    if width == 0 || height == 0 || !(x >= 0.0 && y >= 0.0 && x <= (width - 1) as f64 && y <= (height - 1) as f64) {
1162        return None;
1163    }
1164    let x0 = x.floor() as usize;
1165    let y0 = y.floor() as usize;
1166    let x1 = (x0 + 1).min(width - 1);
1167    let y1 = (y0 + 1).min(height - 1);
1168    let dx = x - x0 as f64;
1169    let dy = y - y0 as f64;
1170    let px = |xx: usize, yy: usize, c: usize| data[(yy * width + xx) * 3 + c] as f64;
1171    let mut out = [0.0; 3];
1172    for (c, o) in out.iter_mut().enumerate() {
1173        *o = (1.0 - dy) * ((1.0 - dx) * px(x0, y0, c) + dx * px(x1, y0, c)) + dy * ((1.0 - dx) * px(x0, y1, c) + dx * px(x1, y1, c));
1174    }
1175    Some(out)
1176}
1177
1178#[cfg(test)]
1179pub(crate) mod tests {
1180    use super::*;
1181    use crate::camera::CameraModelKind;
1182    use crate::geometry::Quat;
1183
1184    /// One camera, n images (cameras lined up along the x axis), m 2D points per image (all the same coordinate set).
1185    pub(crate) fn line_scene(n: u32, m: usize) -> Reconstruction {
1186        let mut r = Reconstruction::new();
1187        let mut cam = Camera::from_focal(CameraModelKind::Pinhole, 100.0, 200, 200);
1188        cam.camera_id = 1;
1189        r.add_camera_own_rig(cam).unwrap();
1190        for i in 1..=n {
1191            let pts = (0..m).map(|k| Vec2::new(10.0 + k as f64, 20.0));
1192            let pose = Rigid3::new(Quat::IDENTITY, Vec3::new(-(i as f64), 0.0, 0.0));
1193            r.add_image_own_frame(Image::new(i, format!("im{i}.jpg"), 1, pts), Some(pose)).unwrap();
1194            r.register_image(i).unwrap();
1195        }
1196        r
1197    }
1198
1199    #[test]
1200    fn add_delete_observations_and_invariants() {
1201        let mut r = line_scene(4, 5);
1202        let t = |i, k| TrackEntry::new(i, k);
1203        let p2 = r.add_point3d(Vec3::new(0.0, 0.0, 5.0), vec![t(1, 0), t(2, 0)], [1, 2, 3]).unwrap();
1204        let p3 = r.add_point3d(Vec3::new(0.0, 0.0, 5.0), vec![t(1, 1), t(2, 1), t(3, 1)], [0; 3]).unwrap();
1205        let p4 = r.add_point3d(Vec3::new(0.0, 0.0, 5.0), vec![t(1, 2), t(2, 2), t(3, 2), t(4, 2)], [0; 3]).unwrap();
1206        assert_eq!((p2, p3, p4), (1, 2, 3));
1207        r.check_invariants().unwrap();
1208        // conflicting link
1209        assert!(r.add_point3d(Vec3::zeros(), vec![t(1, 0), t(3, 0)], [0; 3]).is_err());
1210        r.check_invariants().unwrap();
1211        assert_eq!(r.image(1).unwrap().num_points3d(), 3);
1212        r.add_observation(p2, t(3, 0)).unwrap();
1213        assert!(r.add_observation(p2, t(3, 0)).is_err());
1214        assert_eq!(r.point3d(p2).unwrap().track.len(), 3);
1215        assert!(!r.delete_observation(3, 0).unwrap());
1216        assert!(r.delete_observation(1, 0).unwrap()); // len 2 → point deleted
1217        assert!(!r.exists_point3d(p2));
1218        assert_eq!(r.image(2).unwrap().num_points3d(), 2);
1219        r.check_invariants().unwrap();
1220        // merge
1221        let m = r.merge_points3d(p3, p4).unwrap();
1222        assert_eq!(m, 4);
1223        assert_eq!(r.point3d(m).unwrap().track.len(), 7);
1224        r.check_invariants().unwrap();
1225        // cheap clone + copy on write
1226        let snapshot = r.clone();
1227        r.delete_point3d(m).unwrap();
1228        assert!(snapshot.exists_point3d(m));
1229        assert_eq!(snapshot.image(1).unwrap().num_points3d(), 2);
1230        assert_eq!(r.image(1).unwrap().num_points3d(), 0);
1231        snapshot.check_invariants().unwrap();
1232        r.check_invariants().unwrap();
1233    }
1234
1235    #[test]
1236    fn image_deleter_rule() {
1237        let mut r = line_scene(4, 5);
1238        let t = |i, k| TrackEntry::new(i, k);
1239        let a = r.add_point3d(Vec3::new(1.0, 0.0, 5.0), vec![t(1, 0), t(2, 0)], [0; 3]).unwrap();
1240        let b = r.add_point3d(Vec3::new(1.0, 0.0, 5.0), vec![t(1, 1), t(2, 1), t(3, 1)], [0; 3]).unwrap();
1241        let c = r.add_point3d(Vec3::new(1.0, 0.0, 5.0), vec![t(1, 2), t(2, 2), t(3, 2), t(4, 2)], [0; 3]).unwrap();
1242        let d = r.add_point3d(Vec3::new(1.0, 0.0, 5.0), vec![t(2, 3), t(3, 3)], [0; 3]).unwrap();
1243        for (id, e) in [(a, 1.0), (b, 2.0), (c, 3.0), (d, 4.0)] {
1244            r.set_point3d_error(id, e).unwrap();
1245        }
1246        let w = r.deregister_images_by_id(&[1, 99]);
1247        assert_eq!(w.len(), 1);
1248        assert!(!r.exists_point3d(a));
1249        assert_eq!(r.point3d(b).unwrap().track.len(), 2);
1250        assert_eq!(r.point3d(c).unwrap().track.len(), 3);
1251        assert_eq!(r.point3d(d).unwrap().track.len(), 2);
1252        assert_eq!(r.point3d(b).unwrap().error, 2.0);
1253        assert_eq!(r.point3d(c).unwrap().error, 3.0);
1254        assert!(!r.is_image_registered(1));
1255        assert!(!r.registered_images().contains(&1));
1256        assert_eq!(r.registered_image_count(), 3);
1257        assert_eq!(r.num_images(), 4);
1258        // second delete: warning only
1259        assert_eq!(r.deregister_images_by_name(&["im1.jpg"]).len(), 1);
1260        r.check_invariants().unwrap();
1261        r.remove_unregistered();
1262        assert_eq!(r.num_images(), 3);
1263        r.check_invariants().unwrap();
1264    }
1265
1266    #[test]
1267    fn filters() {
1268        let r = line_scene(3, 4);
1269        let t = |i, k| TrackEntry::new(i, k);
1270        // project a point at (0.5,0,10) in camera of image 1: x=0.5+... set 2D obs to exact projection for images
1271        let x = Vec3::new(2.0, 0.0, 10.0);
1272        let mut obs = Vec::new();
1273        for i in 1..=3u32 {
1274            let pose = r.world_to_cam(i).unwrap();
1275            let px = r.camera(1).unwrap().cam_to_img(&(pose * x)).unwrap();
1276            obs.push(px);
1277        }
1278        // overwrite image 2D coords via re-creating scene
1279        let mut r2 = Reconstruction::new();
1280        r2.add_camera_own_rig(r.camera(1).unwrap().clone()).unwrap();
1281        for i in 1..=3u32 {
1282            let mut pts = vec![obs[(i - 1) as usize]; 4];
1283            pts[1].x += 4.01; // obs idx 1 is 4.01 px off
1284            pts[2].x += 3.99;
1285            r2.add_image_own_frame(Image::new(i, format!("{i}"), 1, pts), r.world_to_cam(i)).unwrap();
1286            r2.register_image(i).unwrap();
1287        }
1288        let r = &mut r2;
1289        let good = r.add_point3d(x, vec![t(1, 0), t(2, 0), t(3, 0)], [0; 3]).unwrap();
1290        let one_bad = r.add_point3d(x, vec![t(1, 1), t(2, 2), t(3, 3)], [0; 3]);
1291        // t(2,2) is 3.99 off → inlier; t(1,1) 4.01 → bad
1292        let one_bad = one_bad.unwrap();
1293        let two_bad = r.add_point3d(x, vec![t(2, 1), t(3, 1), t(1, 3)], [0; 3]).unwrap();
1294        let removed = r.filter_observations_with_large_reprojection_error(4.0, None);
1295        assert_eq!(removed, 1 + 3);
1296        assert!(r.exists_point3d(good));
1297        assert_eq!(r.point3d(one_bad).unwrap().track.len(), 2);
1298        assert!((r.point3d(one_bad).unwrap().error - 3.99 / 3.0).abs() < 1e-9);
1299        assert!(!r.exists_point3d(two_bad));
1300        r.check_invariants().unwrap();
1301        // triangulation angle: centers at x=1,2,3, point at (2,0,10): max angle between c1,c3
1302        let ang = crate::geometry::triangulation_angle(&Vec3::new(1.0, 0.0, 0.0), &Vec3::new(3.0, 0.0, 0.0), &x).to_degrees();
1303        // one_bad keeps only images 2 and 3 so its maximum angle is small -> delete first (2 observations).
1304        assert_eq!(r.filter_points3d_with_small_triangulation_angle(ang - 1e-9, None), 2);
1305        assert!(r.exists_point3d(good));
1306        assert_eq!(r.filter_points3d_with_small_triangulation_angle(ang + 1e-9, None), 3);
1307        assert_eq!(r.num_points3d(), 0);
1308        r.check_invariants().unwrap();
1309    }
1310
1311    #[test]
1312    fn errors_and_normalize() {
1313        let mut r = line_scene(11, 1);
1314        r.update_point3d_errors();
1315        let opts = NormalizeOptions::default();
1316        let t = r.normalize(&opts).unwrap();
1317        let centers: Vec<Vec3> = r.registered_images().iter().map(|i| r.projection_center(*i).unwrap()).collect();
1318        // n=11 → lo=1, hi=9 → x from 2..10 originally, diag 8 → s=10/8
1319        assert!((t.scale - 10.0 / 8.0).abs() < 1e-12);
1320        let mut xs: Vec<f64> = centers.iter().map(|c| c.x).collect();
1321        xs.sort_by(|a, b| a.total_cmp(b));
1322        assert!(((xs[9] - xs[1]) - 10.0).abs() < 1e-9);
1323        let mean: f64 = xs[1..=9].iter().sum::<f64>() / 9.0;
1324        assert!(mean.abs() < 1e-9);
1325    }
1326
1327    #[test]
1328    fn rig_pose_derivation() {
1329        let mut r = Reconstruction::new();
1330        for c in [1u32, 2] {
1331            let mut cam = Camera::from_focal(CameraModelKind::Pinhole, 100.0, 200, 200);
1332            cam.camera_id = c;
1333            r.add_camera(cam).unwrap();
1334        }
1335        let mut rig = Rig::new(7);
1336        rig.ref_sensor_id = Some(SensorKey::camera(1));
1337        let rig_to_c2 = Rigid3::new(Quat::from_axis_angle(&Vec3::y(), 0.2), Vec3::new(0.5, 0.0, 0.0));
1338        rig.sensors.insert(SensorKey::camera(2), Some(rig_to_c2));
1339        r.add_rig(rig).unwrap();
1340        let mut f = Frame::new(3, 7);
1341        f.attach_data(SensorDataKey::image(2, 11));
1342        f.attach_data(SensorDataKey::image(1, 10));
1343        r.add_frame(f).unwrap();
1344        let mut im = Image::new(10, "a", 1, []);
1345        im.frame_id = 3;
1346        r.add_image(im).unwrap();
1347        let mut im = Image::new(11, "b", 2, []);
1348        im.frame_id = 3;
1349        r.add_image(im).unwrap();
1350        let want = Rigid3::new(Quat::from_axis_angle(&Vec3::x(), 0.1), Vec3::new(1.0, 2.0, 3.0));
1351        r.set_world_to_cam(11, want).unwrap();
1352        let got = r.world_to_cam(11).unwrap();
1353        assert!((got.translation - want.translation).norm() < 1e-12);
1354        r.register_frame(3).unwrap();
1355        assert_eq!(r.registered_image_count(), 2);
1356        assert_eq!(r.registered_images(), vec![10, 11]);
1357        let s = Sim3::new(2.0, Quat::from_axis_angle(&Vec3::z(), 0.4), Vec3::new(1.0, 1.0, 1.0));
1358        let c_before = r.projection_center(11).unwrap();
1359        r.transform(&s);
1360        assert!((r.projection_center(11).unwrap() - s.transform_point(&c_before)).norm() < 1e-10);
1361    }
1362
1363    #[test]
1364    fn bilinear() {
1365        let data = vec![0u8, 0, 0, 100, 100, 100, 200, 200, 200, 255, 255, 255];
1366        let c = bilinear_rgb(&data, 2, 2, 0.5, 0.5).unwrap();
1367        assert!((c[0] - 138.75).abs() < 1e-9);
1368        assert!(bilinear_rgb(&data, 2, 2, 1.5, 0.0).is_none());
1369    }
1370}