Skip to main content

cumulus3d_cli/
session.rs

1//! 상태 값과 리듀서: [`Session`] + [`step`] / [`poll`] / [`command`] / [`finish`].
2//!
3//! 위치 하나의 프레임 묶음([`FrameSet`])이 들어올 때마다 특징 → 새 짝 매칭 → (첫 모델 / 이어 등록 + 삼각측량)
4//! → 구역 완료 시 초벌(전경) 조밀화 + 정밀(배경 스레드: BA → GPS ENU 정렬 → 조밀화)을 수행하고,
5//! 그 과정을 단계별 [`Event`] 로 내보낸다. 파일·로그는 쓰지 않는다(run.log 줄도 [`Event::Log`] 로 나간다).
6//!
7//! # 상태와 공유
8//! `Session` 은 값이다. `step(&s, frames)` 는 새 `Session` 을 돌려주고 `s` 는 그대로 쓸 수 있다.
9//! 모델·영상 목록·대응 그래프 등은 `Arc`/쓰기 시 복사로 공유한다. 단, 특징 저장소는 추가 전용 공유 자료라
10//! 같은 이전 값에서 서로 다른 프레임으로 두 번 `step` 하면 저장소가 섞인다(되감기는 [`Command::ResetFrom`] 으로).
11//! 배경 정밀 작업 결과(정밀 모델·점군)와 이벤트 버전 번호도 세션 계열 전체가 공유한다.
12//!
13//! # 이벤트 전달
14//! - 기본: 전경 이벤트는 그 호출의 반환 벡터로, 배경 정밀 작업 이벤트는 내부 수신함에 쌓였다가 다음
15//!   `step`/`poll`/`command`/`finish` 반환 벡터 앞쪽으로 나간다.
16//! - [`Session::set_sink`] 로 수신기를 두면 **모든** 이벤트(전경·배경)가 낸 즉시 버전 순으로 수신기에 전달된다.
17//!   이때 반환 벡터의 전경 이벤트는 이미 전달된 것의 사본이다(다시 전달하지 말 것). 수신기는 막히면 안 된다
18//!   (내부 잠금 안에서 불린다).
19//! - `meta.version` 은 세션 계열 전체에서 단조 증가하며 낸 순서(= timeline 순서)를 준다.
20//!
21//! # run.log·timeline 재현 규칙(출력 훅용)
22//! - timeline 문구는 [`Event::timeline_text`] 가 주며, run.log 에도 `[HH:MM:SS] 문구` 로 같은 자리에 쓴다.
23//! - [`Event::Log`] 는 그 자리의 run.log 한 줄(`stamped` 이면 `[HH:MM:SS] ` 머리).
24//! - [`Event::Warning`]/[`Event::Error`] 는 같은 내용의 `Log` 와 함께 나가므로 run.log 에 따로 쓰지 않는다.
25//! - [`Event::Finished`] 의 단계 시간에는 후처리("post") 시간이 없다(후처리는 출력 훅이 한다).
26
27use crate::densewrap::{dense_model, DenseConfig};
28use crate::events::{Command, Event, Meta, ZoneRange};
29use crate::util::StageTimes;
30use cumulus3d_align::{align_to_gps, EnuOrigin, ModelAlignerOptions};
31use cumulus3d_ba::{bundle_adjust, BaConfig};
32use cumulus3d_core::analyzer::ModelStats;
33use cumulus3d_core::io::ply::PointCloud;
34use cumulus3d_core::io::GpsRecord;
35use cumulus3d_core::{CameraId, FeatureStore, ImageId, MatchGraph, MatchGraphOptions, Reconstruction};
36use cumulus3d_features::{CameraMode, ExtractionOptions, FeatureExtractor, ImageReport, ImageStatus, SiftEngine};
37use cumulus3d_matching::{match_pairs, MatcherBackend, PairMatchingOptions};
38use cumulus3d_sfm::{
39    global_mapper, register_images, triangulate_points, GlobalSfmOptions, PointTriangulatorOptions, RegistrationOptions,
40    TriangulationScope,
41};
42use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
43use std::path::PathBuf;
44use std::sync::{Arc, Mutex, MutexGuard};
45use std::thread::JoinHandle;
46use std::time::{Instant, SystemTime};
47
48/// 위치 하나의 영상 하나.
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct FrameImage {
51    /// 카메라 이름(예: `camF`). 같은 이름은 같은 카메라 내부값을 쓴다.
52    pub camera: String,
53    /// 영상 이름(`image_root` 기준 상대 경로, 예: `camF/camF_0000.jpg`).
54    pub name: String,
55}
56
57/// 위치 하나의 프레임 묶음(카메라마다 한 장).
58#[derive(Clone, Debug, Default)]
59pub struct FrameSet {
60    /// 카메라별 영상.
61    pub images: Vec<FrameImage>,
62    /// 이 묶음과 함께 들어온 GPS 기록(설정의 `gps` 뒤에 덧붙는다).
63    pub gps: Vec<GpsRecord>,
64}
65
66impl FrameSet {
67    /// (카메라, 이름) 목록으로.
68    pub fn new<C: Into<String>, N: Into<String>>(images: impl IntoIterator<Item = (C, N)>) -> Self {
69        Self { images: images.into_iter().map(|(c, n)| FrameImage { camera: c.into(), name: n.into() }).collect(), gps: Vec::new() }
70    }
71}
72
73/// 세션 설정.
74#[derive(Clone)]
75pub struct SessionConfig {
76    /// 입력 영상 폴더(영상 이름의 기준).
77    pub image_root: PathBuf,
78    /// GPS 기록(정렬 순서 = 이 순서).
79    pub gps: Vec<GpsRecord>,
80    /// 구역 크기·겹침(위치 단위).
81    pub span: usize,
82    /// 구역 겹침(위치 수).
83    pub overlap: usize,
84    /// 알려진 전체 위치 수. 있으면 마지막 구역들이 마지막 위치에서 닫히고, 없으면 [`finish`] 에서 닫힌다.
85    pub total_positions: Option<usize>,
86    /// 새로 등록된 영상만 삼각측량.
87    pub incremental_triangulation: bool,
88    /// 첫 GPS 기록으로 ENU 원점 고정(기본: 정렬마다 첫 공통 기록).
89    pub fixed_enu_origin: bool,
90    /// 두 뷰 기하·GPS 정렬 RANSAC 시드.
91    pub seed: Option<u64>,
92    /// 특징 추출 옵션.
93    pub extraction: ExtractionOptions,
94    /// SIFT 백엔드.
95    pub sift: Arc<dyn SiftEngine>,
96    /// 기술자 매칭 백엔드.
97    pub matcher: Arc<dyn MatcherBackend>,
98    /// 조밀화 설정(None = 조밀화 생략, 점군 이벤트는 `dense: false`).
99    pub dense: Option<DenseConfig>,
100    /// [`Command::ResetFrom`] 용으로 보관할 위치별 되감기 지점 수(0 = 되감기 불가, 메모리 절약).
101    pub history: usize,
102}
103
104impl SessionConfig {
105    /// 기본값: 구역 12 / 겹침 2, SIFT 최대 8192 특징·입력 3200, CPU SIFT·매칭, 조밀화 없음, 되감기 8.
106    pub fn new(image_root: impl Into<PathBuf>) -> Self {
107        let mut extraction = ExtractionOptions::default();
108        extraction.sift.max_num_features = 8192;
109        extraction.reader.max_image_size = 3200;
110        Self {
111            image_root: image_root.into(),
112            gps: Vec::new(),
113            span: 12,
114            overlap: 2,
115            total_positions: None,
116            incremental_triangulation: false,
117            fixed_enu_origin: false,
118            seed: None,
119            extraction,
120            sift: Arc::new(cumulus3d_features::CpuSift::default()),
121            matcher: Arc::new(cumulus3d_matching::CpuMatcher::default()),
122            dense: None,
123            history: 8,
124        }
125    }
126}
127
128/// 리듀서 입력.
129#[derive(Clone, Debug)]
130pub enum Input {
131    /// 다음 위치의 프레임 묶음.
132    Frames(FrameSet),
133    /// 상태를 바꾸는 명령.
134    Command(Command),
135    /// 배경 이벤트 수거만.
136    Poll,
137    /// run.log 에 한 줄(`Event::Log`).
138    Log(String),
139    /// 남은 구역 닫기 → 배경 작업 대기 → 요약. 이후 입력은 경고만 낸다.
140    Finish,
141}
142
143impl From<FrameSet> for Input {
144    fn from(f: FrameSet) -> Self {
145        Input::Frames(f)
146    }
147}
148
149impl From<Command> for Input {
150    fn from(c: Command) -> Self {
151        Input::Command(c)
152    }
153}
154
155/// [`crate::pipeline::Pipeline`] 이 구동하는 리듀서로서의 세션. `finish` 는 남은 구역을 닫고 배경 정밀 작업을 모두 기다린다.
156impl crate::pipeline::Reducer for Session {
157    type Input = Input;
158    fn step(&mut self, input: Input) -> Vec<Event> {
159        self.apply(input)
160    }
161    fn poll(&mut self) -> Vec<Event> {
162        self.apply(Input::Poll)
163    }
164    fn command(&mut self, c: Command) -> Vec<Event> {
165        self.apply(Input::Command(c))
166    }
167    fn finish(&mut self) -> Vec<Event> {
168        self.apply(Input::Finish)
169    }
170}
171
172/// 즉시 전달 수신기.
173pub type EventSink = Arc<dyn Fn(Event) + Send + Sync>;
174
175/// 구역 초벌 결과.
176#[derive(Clone, Debug)]
177pub struct PreviewZone {
178    /// 구역 범위.
179    pub range: ZoneRange,
180    /// 초벌 모델(구역 0 은 GPS 정렬본).
181    pub model: Arc<Reconstruction>,
182    /// 초벌 조밀 점군(조밀화를 안 했으면 None).
183    pub cloud: Option<Arc<PointCloud>>,
184    /// 점군 좌표계("gps" | "refined_K" | "unaligned").
185    pub frame: String,
186}
187
188/// 구역 정밀 결과(자세 확정 시점부터 있다; 점군은 조밀화가 끝나야 생긴다).
189#[derive(Clone, Debug)]
190pub struct RefinedZone {
191    /// 구역 범위.
192    pub range: ZoneRange,
193    /// BA + GPS 정렬된 모델.
194    pub model: Arc<Reconstruction>,
195    /// 정밀 조밀 점군(조밀화가 끝나기 전·생략 시 None).
196    pub cloud: Option<Arc<PointCloud>>,
197    /// 정밀 조밀화까지 끝났는지.
198    pub done: bool,
199    generation: u64,
200}
201
202// ---------------------------------------------------------------- 이벤트 발행
203
204struct EmitState {
205    version: u64,
206    sink: Option<EventSink>,
207    inbox: Vec<Event>,
208}
209
210struct Emitter(Mutex<EmitState>);
211
212impl Emitter {
213    fn lock(&self) -> MutexGuard<'_, EmitState> {
214        self.0.lock().unwrap_or_else(|e| e.into_inner())
215    }
216
217    /// 버전·시각을 붙여 낸다. `fg` 가 있으면 반환 벡터에도 넣는다.
218    fn emit(&self, make: impl FnOnce(Meta) -> Event, fg: Option<&mut Vec<Event>>) {
219        let mut st = self.lock();
220        st.version += 1;
221        let ev = make(Meta { version: st.version, at: SystemTime::now() });
222        match (&st.sink, fg) {
223            (Some(s), fg) => {
224                let s = s.clone();
225                if let Some(v) = fg {
226                    v.push(ev.clone());
227                }
228                s(ev);
229            }
230            (None, Some(v)) => v.push(ev),
231            (None, None) => st.inbox.push(ev),
232        }
233    }
234}
235
236/// 세션 계열이 공유하는 불변 설정 + 배경 상태.
237struct Shared {
238    cfg: SessionConfig,
239    extractor: FeatureExtractor,
240    emitter: Emitter,
241    times: StageTimes,
242    bg: Mutex<Background>,
243    start: Mutex<Option<Instant>>,
244}
245
246#[derive(Default)]
247struct Background {
248    jobs: Vec<JoinHandle<()>>,
249    ready: BTreeMap<usize, RefinedZone>,
250    generation: BTreeMap<usize, u64>,
251}
252
253impl Shared {
254    fn bg(&self) -> MutexGuard<'_, Background> {
255        self.bg.lock().unwrap_or_else(|e| e.into_inner())
256    }
257
258    fn generation(&self, zone: usize) -> u64 {
259        self.bg().generation.get(&zone).copied().unwrap_or(0)
260    }
261
262    /// 구역 결과 폐기: 세대를 올려 진행 중 작업의 이후 결과·이벤트를 버린다.
263    fn invalidate(&self, zone: usize) {
264        let mut bg = self.bg();
265        *bg.generation.entry(zone).or_insert(0) += 1;
266        bg.ready.remove(&zone);
267    }
268
269    fn aligner_options(&self, gps: &[GpsRecord]) -> ModelAlignerOptions {
270        let mut o = ModelAlignerOptions { max_error: 3.0, ..Default::default() };
271        o.ransac.random_seed = self.cfg.seed;
272        if self.cfg.fixed_enu_origin {
273            if let Some(g) = gps.first() {
274                o.origin = EnuOrigin::Explicit { lat: g.lat, lon: g.lon, alt: g.alt };
275            }
276        }
277        o
278    }
279
280    /// 등록 영상의 GPS 로 ENU 정렬. 실패하면 모델 그대로 두고 경고(설계 결정: 멈추지 않음).
281    fn align_enu(&self, rec: &mut Reconstruction, what: &str, gps_all: &[GpsRecord], out: &mut Out) -> bool {
282        let names: BTreeSet<String> = rec.registered_images().into_iter().filter_map(|i| rec.image(i).map(|im| im.name.clone())).collect();
283        let gps: Vec<GpsRecord> = gps_all.iter().filter(|g| names.contains(&g.name)).cloned().collect();
284        let opts = self.aligner_options(gps_all);
285        let t0 = Instant::now();
286        let r = align_to_gps(rec, &gps, &opts);
287        out.stage("align", what, t0);
288        match r {
289            Ok(a) => {
290                out.log(format!(
291                    "[align] {what}: 공통 {} 인라이어 {} 오차 평균 {:.3}m 중앙 {:.3}m 축척 {:.4}",
292                    a.common.len(),
293                    a.num_inliers,
294                    a.mean_error,
295                    a.median_error,
296                    a.sim3.scale
297                ));
298                true
299            }
300            Err(e) => {
301                out.warn(format!("[align] {what}: 정렬 실패({e}) — 정렬 없이 계속"));
302                false
303            }
304        }
305    }
306
307    /// 조밀화(구역 밖 영상 제외). 조밀화가 꺼져 있거나 실패하면 None.
308    fn dense_region(&self, kind: &str, k: usize, model: &Reconstruction, keep: &HashSet<String>, out: &mut Out) -> Option<Arc<PointCloud>> {
309        let dense = self.cfg.dense.as_ref()?;
310        let what = format!("{kind} {k}");
311        let t = Instant::now();
312        let r = dense_model(model, |n| keep.contains(n), &self.cfg.image_root, dense);
313        let total = t.elapsed();
314        match r {
315            Ok(run) => {
316                self.times.add("undistort", run.undistort_time);
317                self.times.add("densify", run.densify_time);
318                let tm = &run.output.timings;
319                out.stamped(format!(
320                    "[time] dense {what}: 전체 {:.1}s (왜곡 보정+장면 {:.1}s, 대기 {:.1}s, 조밀화 {:.1}s = 이웃 {:.1} 준비 {:.1} 깊이 {:.1} 필터 {:.1} 융합 {:.1}, 캐시 적중 {})",
321                    total.as_secs_f64(),
322                    run.undistort_time.as_secs_f64(),
323                    run.lock_wait.as_secs_f64(),
324                    run.densify_time.as_secs_f64(),
325                    tm.neighbors.as_secs_f64(),
326                    tm.prepare.as_secs_f64(),
327                    tm.depth().as_secs_f64(),
328                    tm.filter.as_secs_f64(),
329                    tm.fusion.as_secs_f64(),
330                    run.output.cache_hits
331                ));
332                out.log(format!("[dense] {kind} {k} frames {} points {}", run.frames, run.output.cloud.len()));
333                let mut cloud = run.output.cloud;
334                // PLY 왕복과 같은 값: 색이 없으면 흰색.
335                if !cloud.is_empty() && cloud.colors.is_empty() {
336                    cloud.colors = vec![[255, 255, 255]; cloud.len()];
337                }
338                Some(Arc::new(cloud))
339            }
340            Err(e) => {
341                out.warn(format!("[dense] {kind} {k} 실패: {e}"));
342                None
343            }
344        }
345    }
346
347    /// 배경 정밀 작업: 사본 → BA → ENU 정렬 → (채택 가능) → 조밀화.
348    fn refine_job(self: Arc<Self>, range: ZoneRange, mut rec: Reconstruction, generation: u64, gps: Arc<Vec<GpsRecord>>, keep: Arc<HashSet<String>>) {
349        let k = range.zone;
350        let sh = &*self;
351        let mut out = Out { sh, fg: None, guard: Some((k, generation)) };
352        out.emit(|meta| Event::RefineStarted { meta, zone: k });
353        let t0 = Instant::now();
354        let r = bundle_adjust(&mut rec, &BaConfig::default());
355        out.stage("ba", &format!("refined {k}"), t0);
356        match r {
357            Ok(s) => out.log(format!(
358                "[ba] refined {k}: 반복 {} 비용 {:.4e} → {:.4e} ({:?}), RMS {:.3}px",
359                s.num_iterations,
360                s.initial_cost,
361                s.final_cost,
362                s.termination,
363                s.rms_reprojection_error()
364            )),
365            Err(e) => out.warn(format!("[ba] refined {k} 실패: {e}")),
366        }
367        let adjusted = Arc::new(rec.clone());
368        out.emit(|meta| Event::ZoneAdjusted { meta, zone: k, model: adjusted });
369        sh.align_enu(&mut rec, &format!("refined {k}"), &gps, &mut out);
370        let st = ModelStats::compute(&rec);
371        let model = Arc::new(rec);
372        {
373            let mut bg = sh.bg();
374            if bg.generation.get(&k).copied().unwrap_or(0) == generation {
375                bg.ready.insert(k, RefinedZone { range, model: model.clone(), cloud: None, done: false, generation });
376            }
377        }
378        let m = model.clone();
379        out.emit(|meta| Event::ZoneRefinedPose {
380            meta,
381            range,
382            registered: st.registered_image_count,
383            mean_reproj_px: st.mean_reprojection_error,
384            model: m,
385        });
386        let cloud = sh.dense_region("refined", k, &model, &keep, &mut out);
387        if let Some(z) = sh.bg().ready.get_mut(&k).filter(|z| z.generation == generation) {
388            z.cloud = cloud.clone();
389            z.done = true;
390        }
391        out.emit(|meta| Event::ZoneRefined {
392            meta,
393            range,
394            dense: cloud.is_some(),
395            cloud: cloud.unwrap_or_default(),
396            model,
397        });
398    }
399}
400
401/// 이벤트 출구(전경: 반환 벡터, 배경: 수신기/수신함). 배경은 구역 세대가 바뀌면 이벤트를 버린다.
402struct Out<'a> {
403    sh: &'a Shared,
404    fg: Option<&'a mut Vec<Event>>,
405    guard: Option<(usize, u64)>,
406}
407
408impl Out<'_> {
409    fn emit(&mut self, make: impl FnOnce(Meta) -> Event) {
410        if let Some((k, g)) = self.guard {
411            if self.sh.generation(k) != g {
412                return;
413            }
414        }
415        self.sh.emitter.emit(make, self.fg.as_deref_mut());
416    }
417
418    fn log(&mut self, line: impl Into<String>) {
419        let line = line.into();
420        self.emit(|meta| Event::Log { meta, line, stamped: false });
421    }
422
423    fn stamped(&mut self, line: impl Into<String>) {
424        let line = line.into();
425        self.emit(|meta| Event::Log { meta, line, stamped: true });
426    }
427
428    /// run.log 줄 + 같은 내용의 경고.
429    fn warn(&mut self, line: String) {
430        self.log(line.clone());
431        self.emit(|meta| Event::Warning { meta, message: line });
432    }
433
434    /// 단계 시간 누적 + `[time] 단계 대상: 초` 줄.
435    fn stage(&mut self, stage: &str, what: &str, t0: Instant) {
436        let d = t0.elapsed();
437        self.sh.times.add(stage, d);
438        self.stamped(format!("[time] {stage} {what}: {:.3}s", d.as_secs_f64()));
439    }
440}
441
442// ---------------------------------------------------------------- 상태 값
443
444#[derive(Clone)]
445struct Checkpoint {
446    /// 이 위치까지 처리한 직후의 상태.
447    position: usize,
448    chain: Option<Reconstruction>,
449    adopted: Option<usize>,
450    cam_ids: Arc<BTreeMap<String, CameraId>>,
451    graph: Arc<MatchGraph>,
452    gps_len: usize,
453}
454
455/// 점진 재구성 상태 값.
456#[derive(Clone)]
457pub struct Session {
458    shared: Arc<Shared>,
459    store: Arc<FeatureStore>,
460    graph: Arc<MatchGraph>,
461    frames: Arc<Vec<Vec<FrameImage>>>,
462    position_of: Arc<HashMap<String, usize>>,
463    ingested: usize,
464    cam_ids: Arc<BTreeMap<String, CameraId>>,
465    gps: Arc<Vec<GpsRecord>>,
466    chain: Option<Reconstruction>,
467    adopted: Option<usize>,
468    arrived: BTreeMap<usize, ZoneRange>,
469    previews: Arc<BTreeMap<usize, PreviewZone>>,
470    checkpoints: VecDeque<Checkpoint>,
471    started: bool,
472    finished: bool,
473    failed: Option<String>,
474}
475
476/// 다음 프레임 묶음을 처리한 새 상태와 이벤트.
477pub fn step(session: &Session, frames: FrameSet) -> (Session, Vec<Event>) {
478    session.clone().advance(Input::Frames(frames))
479}
480
481/// 배경 정밀 작업 이벤트 수거(수신기가 없을 때). 상태는 바뀌지 않는다.
482pub fn poll(session: &Session) -> (Session, Vec<Event>) {
483    session.clone().advance(Input::Poll)
484}
485
486/// 명령 처리.
487pub fn command(session: &Session, cmd: Command) -> (Session, Vec<Event>) {
488    session.clone().advance(Input::Command(cmd))
489}
490
491/// 남은 구역 닫기 → 배경 작업 대기 → 요약.
492pub fn finish(session: &Session) -> (Session, Vec<Event>) {
493    session.clone().advance(Input::Finish)
494}
495
496impl Session {
497    /// 설정으로 빈 세션을 만든다.
498    pub fn new(cfg: SessionConfig) -> Self {
499        let extractor = FeatureExtractor::with_backend(cfg.sift.clone());
500        let gps = Arc::new(cfg.gps.clone());
501        let shared = Arc::new(Shared {
502            cfg,
503            extractor,
504            emitter: Emitter(Mutex::new(EmitState { version: 0, sink: None, inbox: Vec::new() })),
505            times: StageTimes::default(),
506            bg: Mutex::new(Background::default()),
507            start: Mutex::new(None),
508        });
509        Self {
510            shared,
511            store: Arc::new(FeatureStore::new()),
512            graph: Arc::new(MatchGraph::new()),
513            frames: Arc::new(Vec::new()),
514            position_of: Arc::new(HashMap::new()),
515            ingested: 0,
516            cam_ids: Arc::new(BTreeMap::new()),
517            gps,
518            chain: None,
519            adopted: None,
520            arrived: BTreeMap::new(),
521            previews: Arc::new(BTreeMap::new()),
522            checkpoints: VecDeque::new(),
523            started: false,
524            finished: false,
525            failed: None,
526        }
527    }
528
529    /// 즉시 전달 수신기 설정/해제(세션 계열 공유). 설정 시 쌓여 있던 배경 이벤트를 먼저 전달한다.
530    pub fn set_sink(&self, sink: Option<EventSink>) {
531        let mut st = self.shared.emitter.lock();
532        if let Some(s) = &sink {
533            for ev in std::mem::take(&mut st.inbox) {
534                s(ev);
535            }
536        }
537        st.sink = sink;
538    }
539
540    /// 외부(예: 훅 실행기)에서 이 세션의 버전 순서로 이벤트를 낸다. 수신기가 없으면 수신함에 쌓인다.
541    pub fn emit_external(&self, make: impl FnOnce(Meta) -> Event) {
542        self.shared.emitter.emit(make, None);
543    }
544
545    /// 상태 전이(소유 버전, 복사 없음). `step`/`poll`/`command`/`finish` 는 이것의 얇은 감쌈.
546    pub fn advance(mut self, input: Input) -> (Session, Vec<Event>) {
547        let evs = self.apply(input);
548        (self, evs)
549    }
550
551    /// 제자리 상태 전이. 반환 이벤트는 버전 순(그동안 배경 작업이 낸 이벤트도 제자리에 끼워 넣는다).
552    pub fn apply(&mut self, input: Input) -> Vec<Event> {
553        let sh = self.shared.clone();
554        let mut evs: Vec<Event> = Vec::new();
555        {
556            let mut out = Out { sh: &sh, fg: Some(&mut evs), guard: None };
557            if self.finished {
558                if !matches!(input, Input::Poll) {
559                    out.emit(|meta| Event::Warning { meta, message: "세션이 이미 끝남 — 입력 무시".into() });
560                }
561            } else {
562                if !self.started && !matches!(input, Input::Poll) {
563                    self.started = true;
564                    *sh.start.lock().unwrap_or_else(|e| e.into_inner()) = Some(Instant::now());
565                    let positions = sh.cfg.total_positions;
566                    out.emit(|meta| Event::Started { meta, positions });
567                }
568                match input {
569                    Input::Frames(fs) => {
570                        if let Some(e) = &self.failed {
571                            let message = format!("이전 오류로 중단된 세션 — 프레임 무시 ({e})");
572                            out.emit(|meta| Event::Warning { meta, message });
573                        } else if let Err(e) = self.process_frames(fs, &mut out) {
574                            self.failed = Some(e.clone());
575                            out.emit(|meta| Event::Error { meta, message: e });
576                        }
577                    }
578                    Input::Command(c) => self.apply_command(c, &mut out),
579                    Input::Poll => {}
580                    Input::Log(line) => out.log(line),
581                    Input::Finish => {
582                        if self.failed.is_none() {
583                            self.finish_session(&mut out);
584                        }
585                        self.finished = true;
586                    }
587                }
588            }
589        }
590        // 이 호출 동안(과 그 전에) 배경 작업이 수신함에 낸 이벤트를 버전 순으로 합친다.
591        let bg = std::mem::take(&mut sh.emitter.lock().inbox);
592        if !bg.is_empty() {
593            evs.extend(bg);
594            evs.sort_by_key(|e| e.meta().version);
595        }
596        evs
597    }
598
599    // ------------------------------------------------------------ 조회
600
601    /// 처리한 위치 수.
602    pub fn positions(&self) -> usize {
603        self.frames.len()
604    }
605    /// 위치별 프레임 묶음.
606    pub fn frames(&self) -> &[Vec<FrameImage>] {
607        &self.frames
608    }
609    /// 영상 이름의 위치.
610    pub fn position_of(&self, name: &str) -> Option<usize> {
611        self.position_of.get(name).copied()
612    }
613    /// 현재 체인 모델(첫 모델 전에는 None).
614    pub fn model(&self) -> Option<&Reconstruction> {
615        self.chain.as_ref()
616    }
617    /// 체인의 기준으로 채택된 마지막 정밀 구역.
618    pub fn adopted(&self) -> Option<usize> {
619        self.adopted
620    }
621    /// 특징 저장소.
622    pub fn store(&self) -> &FeatureStore {
623        &self.store
624    }
625    /// 대응 그래프.
626    pub fn graph(&self) -> &MatchGraph {
627        &self.graph
628    }
629    /// 세션 설정.
630    pub fn config(&self) -> &SessionConfig {
631        &self.shared.cfg
632    }
633    /// 도착(처리)한 구역들.
634    pub fn arrived_zones(&self) -> Vec<ZoneRange> {
635        self.arrived.values().copied().collect()
636    }
637    /// 구역별 초벌 결과.
638    pub fn previews(&self) -> &BTreeMap<usize, PreviewZone> {
639        &self.previews
640    }
641    /// 구역별 정밀 결과(자세 확정된 것, 배경 작업과 공유되는 현재 값).
642    pub fn refined(&self) -> BTreeMap<usize, RefinedZone> {
643        self.shared.bg().ready.clone()
644    }
645    /// 아직 끝나지 않은 배경 정밀 작업 수.
646    pub fn pending_jobs(&self) -> usize {
647        self.shared.bg().jobs.iter().filter(|j| !j.is_finished()).count()
648    }
649    /// 마지막으로 발급한 이벤트 버전.
650    pub fn version(&self) -> u64 {
651        self.shared.emitter.lock().version
652    }
653    /// 치명 오류(특징 추출·매칭·첫 모델 실패). 있으면 이후 프레임은 무시된다.
654    pub fn failed(&self) -> Option<&str> {
655        self.failed.as_deref()
656    }
657    /// `Finish` 를 처리했는지.
658    pub fn is_finished(&self) -> bool {
659        self.finished
660    }
661
662    // ------------------------------------------------------------ 처리
663
664    fn process_frames(&mut self, fs: FrameSet, out: &mut Out) -> Result<(), String> {
665        let sh = self.shared.clone();
666        let cfg = &sh.cfg;
667        let p = self.frames.len();
668        let names: Vec<String> = fs.images.iter().map(|i| i.name.clone()).collect();
669        {
670            let pos = Arc::make_mut(&mut self.position_of);
671            for n in &names {
672                pos.insert(n.clone(), p);
673            }
674        }
675        Arc::make_mut(&mut self.frames).push(fs.images.clone());
676        self.ingested += fs.images.len();
677        if !fs.gps.is_empty() {
678            Arc::make_mut(&mut self.gps).extend(fs.gps);
679        }
680        out.emit(|meta| Event::FrameIngested { meta, position: p, images: names });
681
682        // 1) 특징: 첫 묶음은 폴더당 카메라, 이후 같은 카메라 id.
683        let t0 = Instant::now();
684        let r = self.extract(p, &fs.images, out);
685        out.stage("feature", &format!("pos {p}"), t0);
686        r?;
687
688        // 2) 새 영상이 낀 짝만 매칭.
689        let pairs: Vec<(ImageId, ImageId)> = pairs_for(&self.frames, p)
690            .into_iter()
691            .filter_map(|(a, b)| Some((self.store.image_id_by_name(&a)?, self.store.image_id_by_name(&b)?)))
692            .collect();
693        let mut mopts = PairMatchingOptions::default();
694        mopts.geometry.ransac.random_seed = cfg.seed;
695        let t0 = Instant::now();
696        let st = match_pairs(&self.store, &pairs, &mopts, cfg.matcher.as_ref());
697        out.stage("matching", &format!("pos {p} ({} pairs)", pairs.len()), t0);
698        let st = st.map_err(|e| e.to_string())?;
699        Arc::make_mut(&mut self.graph).update_from_store(&self.store, &MatchGraphOptions::default());
700        out.log(format!(
701            "[match] pos {p}: 짝 {} 매칭 {} 유효 기하 {} (그래프 영상 {})",
702            pairs.len(),
703            st.num_matched,
704            st.num_valid_geometries,
705            self.graph.image_ids().len()
706        ));
707        let (np, nv, nm) = (pairs.len(), st.num_valid_geometries, st.num_matched);
708        out.emit(|meta| Event::PairsMatched { meta, pairs: np, verified: nv, position: p, matched: nm });
709
710        // 3) 등록.
711        let (span, ovl) = (cfg.span, cfg.overlap);
712        if p + 1 < span + ovl {
713            self.checkpoint(p);
714            return Ok(());
715        }
716        if p + 1 == span + ovl {
717            let t0 = Instant::now();
718            let o = global_mapper(&self.store, &self.graph, &GlobalSfmOptions::script());
719            out.stage("mapper", &format!("pos {p}"), t0);
720            let o = o.map_err(|e| e.to_string())?;
721            if let Some(f) = &o.failure {
722                out.warn(format!("[mapper] 실패 보고: {f}"));
723            }
724            out.log(format!("[mapper] {:?}", o.summary));
725            let rec = o.reconstruction;
726            let model = Arc::new(rec.clone());
727            let registered = rec.registered_image_count();
728            out.emit(|meta| Event::ModelInitialized { meta, registered, position: p, model });
729            let ids = rec.registered_images();
730            self.emit_registered(&rec, &ids, out);
731            self.chain = Some(rec);
732        } else {
733            // 새 정밀 모델이 나왔으면 그 위에서 이어 등록.
734            let mut k = self.adopted.map_or(0, |a| a + 1);
735            loop {
736                let r = sh.bg().ready.get(&k).map(|z| z.model.clone());
737                let Some(r) = r else { break };
738                self.chain = Some((*r).clone());
739                self.adopted = Some(k);
740                out.emit(|meta| Event::BaseAdopted { meta, zone: k });
741                k += 1;
742            }
743            if let Some(base) = &self.chain {
744                let mut reg = base.clone();
745                let t0 = Instant::now();
746                let rr = register_images(&mut reg, &self.store, &self.graph, &RegistrationOptions::default());
747                out.stage("registration", &format!("pos {p}"), t0);
748                match rr {
749                    Ok(rep) => {
750                        let new_ids = rep.registered();
751                        out.log(format!("[register] pos {p}: 시도 {} 등록 {}", rep.attempts.len(), new_ids.len()));
752                        let scope = if cfg.incremental_triangulation {
753                            TriangulationScope::Images(new_ids.clone())
754                        } else {
755                            TriangulationScope::AllRegistered
756                        };
757                        let topts = PointTriangulatorOptions { clear_points: false, scope, ..Default::default() };
758                        let t0 = Instant::now();
759                        let tr = triangulate_points(&mut reg, &self.graph, &topts);
760                        out.stage("triangulation", &format!("pos {p}"), t0);
761                        match tr {
762                            Ok(t) => {
763                                out.log(format!(
764                                    "[triangulate] pos {p}: 생성 {} 연장 {} 완성 {} 병합 {} 필터 {} (점 {})",
765                                    t.num_created,
766                                    t.num_continued,
767                                    t.num_completed,
768                                    t.num_merged,
769                                    t.num_filtered,
770                                    reg.num_points3d()
771                                ));
772                                self.emit_registered(&reg, &new_ids, out);
773                                self.chain = Some(reg);
774                            }
775                            Err(e) => out.warn(format!("[triangulate] pos {p} 실패: {e} (이전 모델 유지)")),
776                        }
777                    }
778                    Err(e) => out.warn(format!("[register] pos {p} 실패: {e}")),
779                }
780            }
781        }
782        let registered = self.chain.as_ref().map_or(0, |r| r.registered_image_count());
783        let expected = self.ingested;
784        out.emit(|meta| Event::PositionDone { meta, position: p, registered, expected });
785
786        // 4) 구역 완료 → 초벌(전경) + 정밀(배경).
787        for z in zones_ending_at(p, span, ovl, cfg.total_positions) {
788            if !self.arrived.contains_key(&z.zone) {
789                self.process_zone(z, out);
790            }
791        }
792        self.checkpoint(p);
793        Ok(())
794    }
795
796    fn extract(&mut self, p: usize, images: &[FrameImage], out: &mut Out) -> Result<(), String> {
797        let sh = self.shared.clone();
798        let cfg = &sh.cfg;
799        if self.cam_ids.is_empty() {
800            let names: Vec<String> = images.iter().map(|i| i.name.clone()).collect();
801            let mut o = cfg.extraction.clone();
802            o.reader.camera_mode = CameraMode::PerFolder;
803            let rep = sh.extractor.extract_files(&self.store, &cfg.image_root, &names, &o).map_err(|e| e.to_string())?;
804            report_extraction(&rep, p, out);
805            let cams = Arc::make_mut(&mut self.cam_ids);
806            for im in images {
807                let s = self.store.image_by_name(&im.name).ok_or_else(|| format!("{} 첫 영상 특징 추출 실패", im.camera))?;
808                cams.insert(im.camera.clone(), s.camera_id);
809            }
810        } else {
811            for im in images {
812                let mut o = cfg.extraction.clone();
813                let known = self.cam_ids.get(&im.camera).copied();
814                o.reader.camera_mode = known.map_or(CameraMode::PerFolder, CameraMode::Existing);
815                let rep = sh.extractor.extract_files(&self.store, &cfg.image_root, std::slice::from_ref(&im.name), &o).map_err(|e| e.to_string())?;
816                report_extraction(&rep, p, out);
817                if known.is_none() {
818                    if let Some(s) = self.store.image_by_name(&im.name) {
819                        Arc::make_mut(&mut self.cam_ids).insert(im.camera.clone(), s.camera_id);
820                    }
821                }
822            }
823        }
824        Ok(())
825    }
826
827    fn emit_registered(&self, rec: &Reconstruction, ids: &[ImageId], out: &mut Out) {
828        for &id in ids {
829            let (Some(im), Some(pose)) = (rec.image(id), rec.world_to_cam(id)) else { continue };
830            let image = im.name.clone();
831            let position = self.position_of(&image).unwrap_or(usize::MAX);
832            out.emit(|meta| Event::FrameRegistered { meta, position, image, image_id: id, cam_from_world: pose });
833        }
834    }
835
836    /// 구역 도착: 초벌(전경, 구역 0 만 GPS 정렬) + 정밀 작업(배경) 시작.
837    fn process_zone(&mut self, range: ZoneRange, out: &mut Out) {
838        let sh = self.shared.clone();
839        let k = range.zone;
840        let snap = self.chain.clone().unwrap_or_default();
841        let m = Arc::new(snap.clone());
842        out.emit(|meta| Event::ZoneArrived { meta, range, model: m });
843        let mut preview = snap.clone();
844        let mut frame = match self.adopted {
845            Some(j) => format!("refined_{j}"),
846            None => "unaligned".to_string(),
847        };
848        if k == 0 && sh.align_enu(&mut preview, "preview 0", &self.gps, out) {
849            frame = "gps".to_string();
850        }
851        let keep: Arc<HashSet<String>> =
852            Arc::new(self.position_of.iter().filter(|(_, p)| **p >= range.lo && **p < range.hi).map(|(n, _)| n.clone()).collect());
853        let generation = sh.generation(k);
854        {
855            let (sh2, gps, keep) = (sh.clone(), self.gps.clone(), keep.clone());
856            let h = std::thread::spawn(move || sh2.refine_job(range, snap, generation, gps, keep));
857            sh.bg().jobs.push(h);
858        }
859        let cloud = sh.dense_region("preview", k, &preview, &keep, out);
860        let model = Arc::new(preview);
861        self.arrived.insert(k, range);
862        Arc::make_mut(&mut self.previews).insert(k, PreviewZone { range, model: model.clone(), cloud: cloud.clone(), frame: frame.clone() });
863        out.emit(|meta| Event::ZonePreview { meta, range, dense: cloud.is_some(), cloud: cloud.unwrap_or_default(), model, frame });
864    }
865
866    fn checkpoint(&mut self, position: usize) {
867        let n = self.shared.cfg.history;
868        if n == 0 {
869            return;
870        }
871        self.checkpoints.push_back(Checkpoint {
872            position,
873            chain: self.chain.clone(),
874            adopted: self.adopted,
875            cam_ids: self.cam_ids.clone(),
876            graph: self.graph.clone(),
877            gps_len: self.gps.len(),
878        });
879        while self.checkpoints.len() > n {
880            self.checkpoints.pop_front();
881        }
882    }
883
884    fn drop_zone(&mut self, k: usize, out: &mut Out) {
885        self.arrived.remove(&k);
886        Arc::make_mut(&mut self.previews).remove(&k);
887        self.shared.invalidate(k);
888        out.emit(|meta| Event::ZoneInvalidated { meta, zone: k });
889    }
890
891    fn apply_command(&mut self, cmd: Command, out: &mut Out) {
892        match cmd {
893            Command::ResetFrom(p) => self.reset_from(p, out),
894            Command::InvalidateZone(k) => {
895                let Some(range) = self.arrived.get(&k).copied() else {
896                    out.emit(|meta| Event::Warning { meta, message: format!("구역 {k} 은 아직 도착하지 않음 — 무효화할 것 없음") });
897                    return;
898                };
899                self.drop_zone(k, out);
900                // 현재 체인으로 다시 만든다.
901                self.process_zone(range, out);
902            }
903        }
904    }
905
906    /// 위치 p 이후 상태 폐기: 저장소·그래프·체인을 위치 p−1 직후로 되돌리고 그 뒤에 닫힌 구역을 무효화.
907    fn reset_from(&mut self, p: usize, out: &mut Out) {
908        let npos = self.frames.len();
909        if p > npos {
910            out.emit(|meta| Event::Warning { meta, message: format!("ResetFrom({p}): 처리한 위치 {npos} 보다 큼 — 무시") });
911            return;
912        }
913        let cp = if p == 0 {
914            None
915        } else {
916            match self.checkpoints.iter().find(|c| c.position + 1 == p) {
917                Some(c) => Some(c.clone()),
918                None => {
919                    out.emit(|meta| Event::Warning { meta, message: format!("ResetFrom({p}): 되감기 지점 없음(history 부족) — 무시") });
920                    return;
921                }
922            }
923        };
924        let gone: Vec<usize> = self.arrived.iter().filter(|(_, r)| r.hi.saturating_sub(1) >= p).map(|(k, _)| *k).collect();
925        for k in gone {
926            self.drop_zone(k, out);
927        }
928        let pos = self.position_of.clone();
929        self.store = Arc::new(self.store.retain_copy(|im| pos.get(&im.name).is_some_and(|q| *q < p)));
930        Arc::make_mut(&mut self.frames).truncate(p);
931        Arc::make_mut(&mut self.position_of).retain(|_, q| *q < p);
932        self.ingested = self.frames.iter().map(|f| f.len()).sum();
933        self.checkpoints.retain(|c| c.position < p);
934        match cp {
935            Some(c) => {
936                self.chain = c.chain;
937                self.adopted = c.adopted;
938                self.cam_ids = c.cam_ids;
939                self.graph = c.graph;
940                Arc::make_mut(&mut self.gps).truncate(c.gps_len);
941            }
942            None => {
943                self.chain = None;
944                self.adopted = None;
945                self.cam_ids = Arc::new(BTreeMap::new());
946                self.graph = Arc::new(MatchGraph::new());
947                self.gps = Arc::new(self.shared.cfg.gps.clone());
948            }
949        }
950        out.emit(|meta| Event::Reset { meta, position: p });
951    }
952
953    fn finish_session(&mut self, out: &mut Out) {
954        let sh = self.shared.clone();
955        let (span, ovl) = (sh.cfg.span, sh.cfg.overlap);
956        // 전체 수를 몰랐으면 남은 구역을 마지막 위치에서 닫는다.
957        let npos = self.frames.len();
958        if npos > 0 && npos >= span + ovl {
959            for z in zones_upto(npos, span, ovl) {
960                if !self.arrived.contains_key(&z.zone) {
961                    self.process_zone(z, out);
962                }
963            }
964        }
965        out.emit(|meta| Event::AllPositionsDone { meta });
966        loop {
967            let jobs = std::mem::take(&mut sh.bg().jobs);
968            if jobs.is_empty() {
969                break;
970            }
971            for j in jobs {
972                let _ = j.join();
973            }
974        }
975        let chain = self.chain.clone().map(Arc::new);
976        out.emit(|meta| Event::AllRefinedDone { meta, chain });
977        let pipeline_secs = sh.start.lock().unwrap_or_else(|e| e.into_inner()).map_or(0.0, |t| t.elapsed().as_secs_f64());
978        let stage_times = sh.times.snapshot().into_iter().map(|(k, (d, n))| (k, d, n)).collect();
979        let chain_stats = self.chain.as_ref().map(|c| ModelStats::compute(c).lines()).unwrap_or_default();
980        out.emit(|meta| Event::Finished { meta, pipeline_secs, stage_times, chain_stats });
981    }
982}
983
984fn report_extraction(rep: &[ImageReport], position: usize, out: &mut Out) {
985    for r in rep {
986        match &r.status {
987            ImageStatus::Extracted { image_id, camera_id, num_features } => {
988                out.log(format!("[feature] {} id {image_id} cam {camera_id} 특징 {num_features}", r.name));
989                let (image, image_id, count) = (r.name.clone(), *image_id, *num_features);
990                out.emit(|meta| Event::FeaturesExtracted { meta, image, count, position, image_id });
991            }
992            ImageStatus::AlreadyExists { image_id } => out.log(format!("[feature] {} id {image_id} 이미 있음", r.name)),
993            ImageStatus::Failed { error } => out.warn(format!("[feature] {} 실패: {error}", r.name)),
994        }
995    }
996}
997
998/// 짝 규칙: 같은 카메라 위치 간격 1..5, 8, 16 / 다른 카메라 위치 차 0..4. 위치 p 의 영상이 낀 짝만, 정렬된 이름 짝.
999pub fn pairs_for(frames: &[Vec<FrameImage>], p: usize) -> Vec<(String, String)> {
1000    let mut pr = BTreeSet::new();
1001    let Some(cur) = frames.get(p) else { return Vec::new() };
1002    for im in cur {
1003        for d in [1usize, 2, 3, 4, 5, 8, 16] {
1004            if p >= d {
1005                if let Some(o) = frames[p - d].iter().find(|x| x.camera == im.camera) {
1006                    pr.insert((o.name.clone(), im.name.clone()));
1007                }
1008            }
1009        }
1010    }
1011    for a in cur {
1012        for fq in &frames[p.saturating_sub(4)..=p] {
1013            for b in fq {
1014                if a.camera == b.camera {
1015                    continue;
1016                }
1017                let (x, y) = (a.name.clone(), b.name.clone());
1018                pr.insert(if x <= y { (x, y) } else { (y, x) });
1019            }
1020        }
1021    }
1022    pr.into_iter().collect()
1023}
1024
1025/// 구역: start = 0, span, …; [start−ovl, start+span+ovl) ∩ [0, total). 완료 위치 = hi−1.
1026pub fn zones_upto(total: usize, span: usize, ovl: usize) -> Vec<ZoneRange> {
1027    let mut v = Vec::new();
1028    let mut start = 0;
1029    let mut k = 0;
1030    while start < total {
1031        v.push(ZoneRange { zone: k, lo: start.saturating_sub(ovl), hi: (start + span + ovl).min(total) });
1032        k += 1;
1033        start += span.max(1);
1034    }
1035    v
1036}
1037
1038/// 위치 p 에서 닫히는 구역(전체 수를 모르면 잘리지 않은 구역만).
1039fn zones_ending_at(p: usize, span: usize, ovl: usize, total: Option<usize>) -> Vec<ZoneRange> {
1040    let mut v = Vec::new();
1041    let mut k = 0;
1042    loop {
1043        let start = k * span.max(1);
1044        if start > p || total.is_some_and(|n| start >= n) {
1045            break;
1046        }
1047        let hi = total.map_or(start + span + ovl, |n| (start + span + ovl).min(n));
1048        if hi == p + 1 {
1049            v.push(ZoneRange { zone: k, lo: start.saturating_sub(ovl), hi });
1050        }
1051        k += 1;
1052    }
1053    v
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058    use super::*;
1059
1060    fn frames(n: usize) -> Vec<Vec<FrameImage>> {
1061        (0..n)
1062            .map(|p| {
1063                ["camF", "camR", "camL"]
1064                    .iter()
1065                    .map(|c| FrameImage { camera: c.to_string(), name: format!("{c}/{c}_{:04}.jpg", p * 3) })
1066                    .collect()
1067            })
1068            .collect()
1069    }
1070
1071    #[test]
1072    fn zones_match_regions() {
1073        let z = |v: Vec<ZoneRange>| v.into_iter().map(|r| (r.zone, r.lo, r.hi)).collect::<Vec<_>>();
1074        assert_eq!(z(zones_upto(26, 12, 2)), vec![(0, 0, 14), (1, 10, 26), (2, 22, 26)]);
1075        // 위치별로 닫히는 구역을 모으면 같은 목록.
1076        let mut all = Vec::new();
1077        for p in 0..26 {
1078            all.extend(zones_ending_at(p, 12, 2, Some(26)));
1079        }
1080        all.sort_by_key(|r| r.zone);
1081        assert_eq!(z(all), z(zones_upto(26, 12, 2)));
1082        // 전체 수를 모르면 잘리지 않은 구역만 위치에서 닫힌다.
1083        assert_eq!(z(zones_ending_at(13, 12, 2, None)), vec![(0, 0, 14)]);
1084        assert_eq!(z(zones_ending_at(25, 12, 2, None)), vec![(1, 10, 26)]);
1085        assert!(zones_ending_at(25, 12, 2, Some(40)).iter().any(|r| r.zone == 1));
1086    }
1087
1088    #[test]
1089    fn pair_rule() {
1090        let f = frames(20);
1091        assert_eq!(pairs_for(&f, 0).len(), 3);
1092        let p17 = pairs_for(&f, 17);
1093        assert_eq!(p17.len(), 21 + 27);
1094        assert!(p17.contains(&("camF/camF_0003.jpg".to_string(), "camF/camF_0051.jpg".to_string())));
1095    }
1096}