1use crate::ahead::{AheadCore, Lookahead, PositionExtract};
67use crate::densewrap::{dense_model, DenseConfig};
68use crate::events::{Command, Event, Frame, Meta, ZoneRange};
69use crate::intrinsics::IntrinsicsPrior;
70use crate::post::fit_shared_points;
71use crate::util::StageTimes;
72use cumulus3d_align::shared::NameFilter;
73use cumulus3d_align::{align_to_gps, EnuOrigin, ModelAlignerOptions};
74use cumulus3d_ba::{bundle_adjust_with, BaBackend, BaBackendRef, BaConfig};
75use cumulus3d_core::analyzer::ModelStats;
76use cumulus3d_core::io::ply::PointCloud;
77use cumulus3d_core::io::GpsRecord;
78use cumulus3d_core::{Camera, CameraId, FeatureStore, ImageId, MatchGraph, MatchGraphOptions, Reconstruction, Rigid3};
79use cumulus3d_features::{ExtractionOptions, FeatureExtractor, ImageReport, ImageStatus, SiftEngine};
80use cumulus3d_matching::{match_pairs_with, MatcherBackend, PairMatchingOptions, TwoViewBackend};
81use cumulus3d_sfm::{
82 global_mapper, register_images_with, triangulate_points_with, AbsPoseBackend, GlobalSfmOptions, PointTriangulatorOptions,
83 RegistrationOptions, TriangulationBackend, TriangulationScope,
84};
85use rayon::prelude::*;
86use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
87use std::path::PathBuf;
88use std::sync::{Arc, Mutex, MutexGuard};
89use std::thread::JoinHandle;
90use std::time::{Instant, SystemTime};
91
92#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct FrameImage {
95 pub camera: String,
97 pub name: String,
99}
100
101#[derive(Clone, Debug, Default)]
103pub struct FrameSet {
104 pub images: Vec<FrameImage>,
106 pub gps: Vec<GpsRecord>,
108}
109
110impl FrameSet {
111 pub fn new<C: Into<String>, N: Into<String>>(images: impl IntoIterator<Item = (C, N)>) -> Self {
113 Self { images: images.into_iter().map(|(c, n)| FrameImage { camera: c.into(), name: n.into() }).collect(), gps: Vec::new() }
114 }
115}
116
117#[derive(Clone)]
119pub struct SparseBackends {
120 pub two_view: Arc<dyn TwoViewBackend>,
122 pub abs_pose: Arc<dyn AbsPoseBackend>,
124 pub triangulation: Arc<dyn TriangulationBackend>,
126}
127
128impl Default for SparseBackends {
129 fn default() -> Self {
130 Self {
131 two_view: Arc::new(cumulus3d_matching::CpuTwoView),
132 abs_pose: Arc::new(cumulus3d_sfm::CpuAbsPose),
133 triangulation: Arc::new(cumulus3d_sfm::CpuTriangulation),
134 }
135 }
136}
137
138#[derive(Clone)]
140pub struct SessionConfig {
141 pub image_root: PathBuf,
143 pub gps: Vec<GpsRecord>,
145 pub span: usize,
147 pub overlap: usize,
149 pub total_positions: Option<usize>,
151 pub incremental_triangulation: bool,
153 pub fixed_enu_origin: bool,
155 pub seed: Option<u64>,
157 pub extraction: ExtractionOptions,
159 pub sift: Arc<dyn SiftEngine>,
161 pub matcher: Arc<dyn MatcherBackend>,
163 pub ba_backend: Arc<dyn BaBackend>,
165 pub sparse: SparseBackends,
167 pub dense: Option<DenseConfig>,
169 pub history: usize,
171 pub decode_frames: bool,
173 pub stable_display: bool,
178 pub gps_prior_sigma: f64,
181 pub settle_tol: f64,
185 pub settle_max_zone: usize,
187 pub intrinsics_prior: Option<Arc<IntrinsicsPrior>>,
190 pub lookahead: usize,
195 pub preview: bool,
200}
201
202impl SessionConfig {
203 pub fn new(image_root: impl Into<PathBuf>) -> Self {
205 let mut extraction = ExtractionOptions::default();
206 extraction.sift.max_num_features = 8192;
207 extraction.reader.max_image_size = 3200;
208 Self {
209 image_root: image_root.into(),
210 gps: Vec::new(),
211 span: 12,
212 overlap: 2,
213 total_positions: None,
214 incremental_triangulation: false,
215 fixed_enu_origin: false,
216 seed: None,
217 extraction,
218 sift: Arc::new(cumulus3d_features::CpuSift::default()),
219 matcher: Arc::new(cumulus3d_matching::CpuMatcher::default()),
220 ba_backend: Arc::new(cumulus3d_ba::CpuBaBackend),
221 sparse: SparseBackends::default(),
222 dense: None,
223 history: 8,
224 decode_frames: false,
225 stable_display: false,
226 gps_prior_sigma: 1.0,
227 settle_tol: 0.01,
228 settle_max_zone: 3,
229 intrinsics_prior: None,
230 lookahead: 0,
231 preview: false,
232 }
233 }
234}
235
236#[derive(Clone, Debug)]
238pub enum Input {
239 Frames(FrameSet),
241 Batch(Vec<FrameSet>),
248 Command(Command),
250 Poll,
252 Log(String),
254 Finish,
256}
257
258impl From<FrameSet> for Input {
259 fn from(f: FrameSet) -> Self {
260 Input::Frames(f)
261 }
262}
263
264impl From<Command> for Input {
265 fn from(c: Command) -> Self {
266 Input::Command(c)
267 }
268}
269
270impl crate::pipeline::Reducer for Session {
273 type Input = Input;
274 fn step(&mut self, input: Input) -> Vec<Event> {
275 self.apply(input)
276 }
277 fn prelude(&mut self, input: &Input) -> Vec<Event> {
278 let sh = self.shared.clone();
279 let mut evs = Vec::new();
280 {
281 let mut out = Out { sh: &sh, fg: Some(&mut evs), guard: None };
282 match input {
283 Input::Frames(fs) => self.announce(fs, self.frames.len(), &mut out),
284 Input::Batch(sets) => {
285 for (i, fs) in sets.iter().enumerate() {
286 self.announce(fs, self.frames.len() + i, &mut out);
287 }
288 }
289 _ => {}
290 }
291 }
292 evs
293 }
294 fn poll(&mut self) -> Vec<Event> {
295 self.apply(Input::Poll)
296 }
297 fn command(&mut self, c: Command) -> Vec<Event> {
298 self.apply(Input::Command(c))
299 }
300 fn finish(&mut self) -> Vec<Event> {
301 self.apply(Input::Finish)
302 }
303}
304
305pub type EventSink = Arc<dyn Fn(Event) + Send + Sync>;
307
308#[derive(Clone, Debug)]
310pub struct PreviewZone {
311 pub range: ZoneRange,
313 pub model: Arc<Reconstruction>,
315 pub cloud: Option<Arc<PointCloud>>,
317 pub frame: String,
319}
320
321#[derive(Clone, Debug)]
323pub struct RefinedZone {
324 pub range: ZoneRange,
326 pub model: Arc<Reconstruction>,
328 pub cloud: Option<Arc<PointCloud>>,
330 pub done: bool,
332 generation: u64,
333}
334
335struct EmitState {
338 version: u64,
339 sink: Option<EventSink>,
340 inbox: Vec<Event>,
341}
342
343struct Emitter(Mutex<EmitState>);
344
345impl Emitter {
346 fn lock(&self) -> MutexGuard<'_, EmitState> {
347 self.0.lock().unwrap_or_else(|e| e.into_inner())
348 }
349
350 fn emit(&self, make: impl FnOnce(Meta) -> Event, fg: Option<&mut Vec<Event>>) {
352 let mut st = self.lock();
353 st.version += 1;
354 let ev = make(Meta { version: st.version, at: SystemTime::now() });
355 match (&st.sink, fg) {
356 (Some(s), fg) => {
357 let s = s.clone();
358 if let Some(v) = fg {
359 v.push(ev.clone());
360 }
361 s(ev);
362 }
363 (None, Some(v)) => v.push(ev),
364 (None, None) => st.inbox.push(ev),
365 }
366 }
367}
368
369struct AheadGuard(Arc<AheadCore>);
371
372impl Drop for AheadGuard {
373 fn drop(&mut self) {
374 self.0.close();
375 }
376}
377
378struct Shared {
380 cfg: SessionConfig,
381 ahead: AheadGuard,
382 emitter: Emitter,
383 times: StageTimes,
384 bg: Mutex<Background>,
385 start: Mutex<Option<Instant>>,
386}
387
388#[derive(Default)]
389struct Background {
390 jobs: Vec<JoinHandle<()>>,
391 ready: BTreeMap<usize, RefinedZone>,
392 generation: BTreeMap<usize, u64>,
393 settled: Option<usize>,
395 zone_keep: BTreeMap<usize, Arc<HashSet<String>>>,
397 dense_basis: BTreeMap<usize, Arc<Reconstruction>>,
399 finished: BTreeMap<usize, u64>,
401}
402
403struct FinishMark<'a> {
405 sh: &'a Shared,
406 zone: usize,
407 generation: u64,
408}
409
410impl Drop for FinishMark<'_> {
411 fn drop(&mut self) {
412 self.sh.bg().finished.insert(self.zone, self.generation);
413 }
414}
415
416impl Shared {
417 fn bg(&self) -> MutexGuard<'_, Background> {
418 self.bg.lock().unwrap_or_else(|e| e.into_inner())
419 }
420
421 fn generation(&self, zone: usize) -> u64 {
422 self.bg().generation.get(&zone).copied().unwrap_or(0)
423 }
424
425 fn invalidate(&self, zone: usize) {
427 let mut bg = self.bg();
428 *bg.generation.entry(zone).or_insert(0) += 1;
429 bg.ready.remove(&zone);
430 bg.zone_keep.remove(&zone);
431 bg.dense_basis.remove(&zone);
432 }
433
434 fn aligner_options(&self, gps: &[GpsRecord]) -> ModelAlignerOptions {
435 let mut o = ModelAlignerOptions { max_error: 3.0, ..Default::default() };
436 o.ransac.random_seed = self.cfg.seed;
437 if self.cfg.fixed_enu_origin {
438 if let Some(g) = gps.first() {
439 o.origin = EnuOrigin::Explicit { lat: g.lat, lon: g.lon, alt: g.alt };
440 }
441 }
442 o
443 }
444
445 fn wait_refined(&self, k: usize, generation: u64) -> Option<Arc<Reconstruction>> {
449 let t0 = Instant::now();
450 let me = k + 1;
451 loop {
452 if let Some(z) = self.bg().ready.get(&k) {
453 return Some(z.model.clone());
454 }
455 if self.generation(me) != generation || t0.elapsed().as_secs() > 1800 {
456 return None;
457 }
458 std::thread::sleep(std::time::Duration::from_millis(20));
459 }
460 }
461
462 fn wait_finished(&self, k: usize, me: usize, generation: u64) -> bool {
465 let t0 = Instant::now();
466 loop {
467 {
468 let bg = self.bg();
469 if bg.finished.get(&k).is_some_and(|g| *g == bg.generation.get(&k).copied().unwrap_or(0)) {
470 return true;
471 }
472 }
473 if self.generation(me) != generation || t0.elapsed().as_secs() > 1800 {
474 return false;
475 }
476 std::thread::sleep(std::time::Duration::from_millis(20));
477 }
478 }
479
480 fn replace_shown(&self, k: usize, generation: u64, model: &Arc<Reconstruction>, st: &ModelStats) {
485 if !self.wait_finished(k - 1, k, generation) {
486 return;
487 }
488 for j in 0..k {
489 let snap = {
490 let bg = self.bg();
491 let gj = bg.generation.get(&j).copied().unwrap_or(0);
492 bg.ready
493 .get(&j)
494 .filter(|z| z.generation == gj)
495 .map(|z| (z.clone(), gj, bg.zone_keep.get(&j).cloned(), bg.dense_basis.get(&j).cloned()))
496 };
497 let Some((z, gj, keep, basis)) = snap else { continue };
498 let mut out = Out { sh: self, fg: None, guard: Some((j, gj)) };
499 let change = basis.as_ref().map_or(0.0, |b| intrinsics_change(b.cameras(), model.cameras()));
500 let mut basis_new = None;
501 let cloud = z.cloud.as_ref().map(|old| {
502 if change >= self.cfg.settle_tol {
503 if let Some(c) = keep.as_ref().and_then(|keep| self.dense_region("refined", j, model, keep, &mut out)) {
504 out.log(format!("[settle] 구역 {j} 다시 조밀화 (내부값 변화 {:.2}%)", change * 100.0));
505 basis_new = Some(model.clone());
506 return c;
507 }
508 }
509 let by_points = keep
511 .as_ref()
512 .and_then(|keep| {
513 let keep = keep.clone();
514 let filter: NameFilter = Arc::new(move |n: &str| keep.contains(n));
515 fit_shared_points(&z.model, model, Some(filter), MIN_SHARED_PAIRS)
516 })
517 .or_else(|| fit_shared_points(&z.model, model, None, MIN_SHARED_PAIRS));
518 let mut c = (**old).clone();
519 if let Some((f, n)) = by_points {
520 let t = f.sim3;
521 cumulus3d_align::transform_cloud(&mut c, &t);
522 out.log(format!(
523 "[settle] 구역 {j} 점군 이동: 공유 점 {n}, 잔차 중앙 {:.3}m, 축척 {:.4}, 이동 {:.2}m",
524 f.median,
525 t.scale,
526 t.translation.norm()
527 ));
528 } else {
529 let t = keep.as_ref().and_then(|keep| sim3_between(&z.model, model, |n| keep.contains(n)));
530 match t {
531 Some(t) => {
532 cumulus3d_align::transform_cloud(&mut c, &t);
533 out.log(format!(
534 "[settle] 구역 {j} 점군 이동(카메라 중심 대체): 이동 {:.3}m 축척 {:.4}",
535 t.translation.norm(),
536 t.scale
537 ));
538 }
539 None => out.log(format!("[settle] 구역 {j} 점군 이동 생략: 공통 영상 부족")),
540 }
541 }
542 Arc::new(c)
543 });
544 {
545 let mut bg = self.bg();
546 if bg.generation.get(&j).copied().unwrap_or(0) != gj {
547 continue;
548 }
549 let Some(r) = bg.ready.get_mut(&j).filter(|r| r.generation == gj) else { continue };
550 r.model = model.clone();
551 r.cloud = cloud.clone();
552 r.done = true;
553 if let Some(b) = basis_new {
554 bg.dense_basis.insert(j, b);
555 }
556 }
557 let (range, m) = (z.range, model.clone());
558 let (registered, mean_reproj_px) = (st.registered_image_count, st.mean_reprojection_error);
559 out.emit(|meta| Event::ZoneRefinedPose { meta, range, registered, mean_reproj_px, model: m });
560 let m = model.clone();
561 out.emit(|meta| Event::ZoneRefined { meta, range, dense: cloud.is_some(), cloud: cloud.unwrap_or_default(), model: m });
562 }
563 }
564
565 fn align_enu(&self, rec: &mut Reconstruction, what: &str, gps_all: &[GpsRecord], out: &mut Out) -> bool {
566 let names: BTreeSet<String> = rec.registered_images().into_iter().filter_map(|i| rec.image(i).map(|im| im.name.clone())).collect();
567 let gps: Vec<GpsRecord> = gps_all.iter().filter(|g| names.contains(&g.name)).cloned().collect();
568 let opts = self.aligner_options(gps_all);
569 let t0 = Instant::now();
570 let r = align_to_gps(rec, &gps, &opts);
571 out.stage("align", what, t0);
572 match r {
573 Ok(a) => {
574 out.log(format!(
575 "[align] {what}: 공통 {} 인라이어 {} 오차 평균 {:.3}m 중앙 {:.3}m 축척 {:.4}",
576 a.common.len(),
577 a.num_inliers,
578 a.mean_error,
579 a.median_error,
580 a.sim3.scale
581 ));
582 if let Some(g) = &a.gravity {
583 out.log(gravity_note(&format!("align {what}"), g));
584 }
585 true
586 }
587 Err(e) => {
588 out.warn(format!("[align] {what}: 정렬 실패({e}) — 정렬 없이 계속"));
589 false
590 }
591 }
592 }
593
594 fn dense_region(&self, kind: &str, k: usize, model: &Reconstruction, keep: &HashSet<String>, out: &mut Out) -> Option<Arc<PointCloud>> {
596 let dense = self.cfg.dense.as_ref()?;
597 let what = format!("{kind} {k}");
598 let t = Instant::now();
599 let r = dense_model(model, |n| keep.contains(n), &self.cfg.image_root, dense);
600 let total = t.elapsed();
601 match r {
602 Ok(run) => {
603 self.times.add("undistort", run.undistort_time);
604 self.times.add("densify", run.densify_time);
605 let tm = &run.output.timings;
606 out.stamped(format!(
607 "[time] dense {what}: 전체 {:.1}s (왜곡 보정+장면 {:.1}s, 대기 {:.1}s, 조밀화 {:.1}s = 이웃 {:.1} 준비 {:.1} 깊이 {:.1} 필터 {:.1} 융합 {:.1}, 캐시 적중 {})",
608 total.as_secs_f64(),
609 run.undistort_time.as_secs_f64(),
610 run.lock_wait.as_secs_f64(),
611 run.densify_time.as_secs_f64(),
612 tm.neighbors.as_secs_f64(),
613 tm.prepare.as_secs_f64(),
614 tm.depth().as_secs_f64(),
615 tm.filter.as_secs_f64(),
616 tm.fusion.as_secs_f64(),
617 run.output.cache_hits
618 ));
619 out.log(format!("[dense] {kind} {k} frames {} points {}", run.frames, run.output.cloud.len()));
620 let mut cloud = run.output.cloud;
621 if !cloud.is_empty() && cloud.colors.is_empty() {
623 cloud.colors = vec![[255, 255, 255]; cloud.len()];
624 }
625 Some(Arc::new(cloud))
626 }
627 Err(e) => {
628 out.warn(format!("[dense] {kind} {k} 실패: {e}"));
629 None
630 }
631 }
632 }
633
634 fn refine_job(
636 self: Arc<Self>,
637 range: ZoneRange,
638 mut rec: Reconstruction,
639 generation: u64,
640 gps: Arc<Vec<GpsRecord>>,
641 keep: Arc<HashSet<String>>,
642 cam_ids: Arc<BTreeMap<String, CameraId>>,
643 ) {
644 let k = range.zone;
645 let sh = &*self;
646 let mut out = Out { sh, fg: None, guard: Some((k, generation)) };
647 let _finish = FinishMark { sh, zone: k, generation };
648 out.emit(|meta| Event::RefineStarted { meta, zone: k });
649 let mut ba = BaConfig::default();
650 let mut prev: Option<Arc<Reconstruction>> = None;
653 let mut settling = false;
654 if sh.cfg.stable_display {
655 let mut bg = sh.bg();
656 if bg.generation.get(&k).copied().unwrap_or(0) == generation {
657 bg.zone_keep.insert(k, keep.clone());
658 }
659 }
660 if sh.cfg.stable_display {
661 let targets = if sh.cfg.gps_prior_sigma > 0.0 { enu_targets(&rec, &gps) } else { HashMap::new() };
662 if k == 0 {
663 let (src, dst): (Vec<_>, Vec<_>) = targets.iter().filter_map(|(id, g)| rec.projection_center(*id).map(|c| (c, *g))).unzip();
665 if src.len() >= 3 {
666 if let Some((t, g)) = cumulus3d_align::fit_enu_sim3(&rec, &src, &dst) {
667 rec.transform(&t);
668 out.log(format!("[stable] refined 0: ENU 초기 정렬 GPS {} 개, 축척 {:.4}", src.len(), t.scale));
669 if let Some(g) = g {
670 out.log(gravity_note("stable refined 0", &g));
671 }
672 }
673 }
674 settling = true;
675 } else {
676 prev = sh.wait_refined(k - 1, generation);
677 settling = sh.bg().settled.is_none();
678 match (&prev, settling) {
679 (Some(p), false) => {
680 let (n, scale) = anchor_to_shown(&mut rec, p, &mut ba);
681 out.log(format!("[stable] refined {k}: 고정 영상 {n}, 이전 구역에 맞춘 축척 {scale:.4}"));
682 }
683 (Some(p), true) => {
684 let scale = sim3_onto_shown(&mut rec, p).map_or(1.0, |(t, _)| t.scale);
685 out.log(format!("[stable] refined {k}: 정착 전 — 모든 자세·내부값 자유, 이전 구역에 맞춘 축척 {scale:.4}"));
686 }
687 (None, _) => out.warn(format!("[stable] refined {k}: 앞 구역 결과 없음 — 고정 없이 조정")),
688 }
689 }
690 let sigma = sh.cfg.gps_prior_sigma;
691 for (id, g) in &targets {
692 if !ba.constant_poses.contains(id) {
693 ba.position_priors.insert(*id, (*g, sigma));
694 }
695 }
696 if !ba.position_priors.is_empty() {
697 out.log(format!("[stable] refined {k}: GPS 위치 제약 {} 개 (σ {sigma} m)", ba.position_priors.len()));
698 }
699 }
700 if let Some(pr) = &sh.cfg.intrinsics_prior {
701 let pr = pr.resolve(&cam_ids);
702 for (cid, cam) in rec.cameras() {
703 if !ba.constant_cameras.contains(cid) {
704 if let Some(p) = pr.ba_prior(cam) {
705 ba.intrinsics_priors.insert(*cid, p);
706 }
707 }
708 }
709 if !ba.intrinsics_priors.is_empty() {
710 out.log(format!(
711 "[prior] refined {k}: 내부값 사전 카메라 {} 개 (초점 σ {} 상대, 왜곡 σ {})",
712 ba.intrinsics_priors.len(),
713 pr.focal_sigma,
714 pr.dist_sigma
715 ));
716 }
717 }
718 let t0 = Instant::now();
719 let r = bundle_adjust_with(&mut rec, &ba, &*sh.cfg.ba_backend);
720 out.stage("ba", &format!("refined {k}"), t0);
721 match r {
722 Ok(s) => out.log(format!(
723 "[ba] refined {k}: 반복 {} 비용 {:.4e} → {:.4e} ({:?}), RMS {:.3}px",
724 s.num_iterations,
725 s.initial_cost,
726 s.final_cost,
727 s.termination,
728 s.rms_reprojection_error()
729 )),
730 Err(e) => out.warn(format!("[ba] refined {k} 실패: {e}")),
731 }
732 if let (true, Some(p)) = (settling && k > 0, &prev) {
734 match fit_shared_points(&rec, p, None, MIN_SHARED_PAIRS) {
735 Some((f, n)) => {
736 let t = f.sim3;
737 rec.transform(&t);
738 out.log(format!(
739 "[settle] refined {k}: 화면 좌표계 재정렬 공유 점 {n}, 잔차 중앙 {:.3}m, 축척 {:.4}, 이동 {:.2}m",
740 f.median,
741 t.scale,
742 t.translation.norm()
743 ));
744 }
745 None => match sim3_onto_shown(&mut rec, p) {
746 Some((t, _)) => out.log(format!(
747 "[settle] refined {k}: 화면 좌표계 재정렬 (카메라 중심 대체) 축척 {:.4}, 이동 {:.2}m",
748 t.scale,
749 t.translation.norm()
750 )),
751 None => out.warn(format!("[settle] refined {k}: 화면 좌표계 재정렬 생략: 공통 영상 부족")),
752 },
753 }
754 }
755 let adjusted = Arc::new(rec.clone());
756 out.emit(|meta| Event::ZoneAdjusted { meta, zone: k, model: adjusted });
757 if !sh.cfg.stable_display {
758 sh.align_enu(&mut rec, &format!("refined {k}"), &gps, &mut out);
759 }
760 let st = ModelStats::compute(&rec);
761 let mut settle_now = false;
763 if settling {
764 let change = prev.as_ref().map(|p| intrinsics_change(p.cameras(), rec.cameras()));
765 let forced = k >= sh.cfg.settle_max_zone;
766 settle_now = forced || change.is_some_and(|c| c < sh.cfg.settle_tol);
767 let verdict = match (settle_now, forced) {
768 (true, true) => "강제 고정",
769 (true, false) => "고정",
770 _ => "정착 중",
771 };
772 match change {
773 Some(c) => out.log(format!("[settle] refined {k}: 내부값 변화 {:.2}% → {verdict}", c * 100.0)),
774 None => out.log(format!("[settle] refined {k}: → {verdict}")),
775 }
776 }
777 let model = Arc::new(rec);
778 {
779 let mut bg = sh.bg();
780 if bg.generation.get(&k).copied().unwrap_or(0) == generation {
781 if settle_now {
782 bg.settled = Some(k);
783 }
784 bg.ready.insert(k, RefinedZone { range, model: model.clone(), cloud: None, done: false, generation });
785 }
786 }
787 let m = model.clone();
788 out.emit(|meta| Event::ZoneRefinedPose {
789 meta,
790 range,
791 registered: st.registered_image_count,
792 mean_reproj_px: st.mean_reprojection_error,
793 model: m,
794 });
795 let cloud = sh.dense_region("refined", k, &model, &keep, &mut out);
796 {
797 let mut bg = sh.bg();
798 if bg.generation.get(&k).copied().unwrap_or(0) == generation {
799 if let Some(z) = bg.ready.get_mut(&k).filter(|z| z.generation == generation) {
800 z.cloud = cloud.clone();
801 z.done = true;
802 }
803 bg.dense_basis.insert(k, model.clone());
804 }
805 }
806 let m = model.clone();
807 out.emit(|meta| Event::ZoneRefined { meta, range, dense: cloud.is_some(), cloud: cloud.unwrap_or_default(), model: m });
808 if settling && k > 0 {
810 sh.replace_shown(k, generation, &model, &st);
811 }
812 }
813}
814
815struct Out<'a> {
817 sh: &'a Shared,
818 fg: Option<&'a mut Vec<Event>>,
819 guard: Option<(usize, u64)>,
820}
821
822impl Out<'_> {
823 fn emit(&mut self, make: impl FnOnce(Meta) -> Event) {
824 if let Some((k, g)) = self.guard {
825 if self.sh.generation(k) != g {
826 return;
827 }
828 }
829 self.sh.emitter.emit(make, self.fg.as_deref_mut());
830 }
831
832 fn log(&mut self, line: impl Into<String>) {
833 let line = line.into();
834 self.emit(|meta| Event::Log { meta, line, stamped: false });
835 }
836
837 fn stamped(&mut self, line: impl Into<String>) {
838 let line = line.into();
839 self.emit(|meta| Event::Log { meta, line, stamped: true });
840 }
841
842 fn warn(&mut self, line: String) {
844 self.log(line.clone());
845 self.emit(|meta| Event::Warning { meta, message: line });
846 }
847
848 fn stage(&mut self, stage: &str, what: &str, t0: Instant) {
850 let d = t0.elapsed();
851 self.sh.times.add(stage, d);
852 self.stamped(format!("[time] {stage} {what}: {:.3}s", d.as_secs_f64()));
853 }
854}
855
856#[derive(Clone)]
859struct Checkpoint {
860 position: usize,
862 chain: Option<Reconstruction>,
863 adopted: Option<usize>,
864 cam_ids: Arc<BTreeMap<String, CameraId>>,
865 graph: Arc<MatchGraph>,
866 gps_len: usize,
867}
868
869#[derive(Clone)]
871pub struct Session {
872 shared: Arc<Shared>,
873 store: Arc<FeatureStore>,
874 graph: Arc<MatchGraph>,
875 frames: Arc<Vec<Vec<FrameImage>>>,
876 position_of: Arc<HashMap<String, usize>>,
877 ingested: usize,
878 cam_ids: Arc<BTreeMap<String, CameraId>>,
879 gps: Arc<Vec<GpsRecord>>,
880 chain: Option<Reconstruction>,
881 adopted: Option<usize>,
882 arrived: BTreeMap<usize, ZoneRange>,
883 previews: Arc<BTreeMap<usize, PreviewZone>>,
884 checkpoints: VecDeque<Checkpoint>,
885 started: bool,
886 finished: bool,
887 failed: Option<String>,
888 announced: Option<usize>,
890}
891
892pub fn step(session: &Session, frames: FrameSet) -> (Session, Vec<Event>) {
894 session.clone().advance(Input::Frames(frames))
895}
896
897pub fn poll(session: &Session) -> (Session, Vec<Event>) {
899 session.clone().advance(Input::Poll)
900}
901
902pub fn command(session: &Session, cmd: Command) -> (Session, Vec<Event>) {
904 session.clone().advance(Input::Command(cmd))
905}
906
907pub fn finish(session: &Session) -> (Session, Vec<Event>) {
909 session.clone().advance(Input::Finish)
910}
911
912impl Session {
913 pub fn new(cfg: SessionConfig) -> Self {
915 let extractor = Arc::new(FeatureExtractor::with_backend(cfg.sift.clone()));
916 let store = Arc::new(FeatureStore::new());
917 let ahead = AheadGuard(AheadCore::new(
918 extractor,
919 cfg.image_root.clone(),
920 cfg.extraction.clone(),
921 cfg.intrinsics_prior.clone(),
922 cfg.lookahead,
923 Arc::clone(&store),
924 ));
925 let gps = Arc::new(cfg.gps.clone());
926 let shared = Arc::new(Shared {
927 cfg,
928 ahead,
929 emitter: Emitter(Mutex::new(EmitState { version: 0, sink: None, inbox: Vec::new() })),
930 times: StageTimes::default(),
931 bg: Mutex::new(Background::default()),
932 start: Mutex::new(None),
933 });
934 Self {
935 shared,
936 store,
937 graph: Arc::new(MatchGraph::new()),
938 frames: Arc::new(Vec::new()),
939 position_of: Arc::new(HashMap::new()),
940 ingested: 0,
941 cam_ids: Arc::new(BTreeMap::new()),
942 gps,
943 chain: None,
944 adopted: None,
945 arrived: BTreeMap::new(),
946 previews: Arc::new(BTreeMap::new()),
947 checkpoints: VecDeque::new(),
948 started: false,
949 announced: None,
950 finished: false,
951 failed: None,
952 }
953 }
954
955 pub fn set_sink(&self, sink: Option<EventSink>) {
958 let mut st = self.shared.emitter.lock();
959 if let Some(s) = &sink {
960 for ev in std::mem::take(&mut st.inbox) {
961 s(ev);
962 }
963 }
964 st.sink = sink;
965 }
966
967 pub fn emit_external(&self, make: impl FnOnce(Meta) -> Event) {
970 self.shared.emitter.emit(make, None);
971 }
972
973 pub fn advance(mut self, input: Input) -> (Session, Vec<Event>) {
975 let evs = self.apply(input);
976 (self, evs)
977 }
978
979 pub fn apply(&mut self, input: Input) -> Vec<Event> {
982 let sh = self.shared.clone();
983 let mut evs: Vec<Event> = Vec::new();
984 {
985 let mut out = Out { sh: &sh, fg: Some(&mut evs), guard: None };
986 if self.finished {
987 if !matches!(input, Input::Poll) {
988 out.emit(|meta| Event::Warning { meta, message: "세션이 이미 끝남 — 입력 무시".into() });
989 }
990 } else {
991 if !matches!(input, Input::Poll) {
992 self.ensure_started(&mut out);
993 }
994 match input {
995 Input::Frames(fs) => {
996 self.announce(&fs, self.frames.len(), &mut out);
997 if let Some(e) = &self.failed {
998 let message = format!("이전 오류로 중단된 세션 — 프레임 무시 ({e})");
999 out.emit(|meta| Event::Warning { meta, message });
1000 } else if let Err(e) = self.process_frames(fs, &mut out) {
1001 self.failed = Some(e.clone());
1002 out.emit(|meta| Event::Error { meta, message: e });
1003 }
1004 }
1005 Input::Batch(sets) => {
1006 for (i, fs) in sets.iter().enumerate() {
1007 self.announce(fs, self.frames.len() + i, &mut out);
1008 }
1009 if let Some(e) = &self.failed {
1010 let message = format!("이전 오류로 중단된 세션 — 프레임 무시 ({e})");
1011 out.emit(|meta| Event::Warning { meta, message });
1012 } else if let Err(e) = self.process_batch(sets, &mut out) {
1013 self.failed = Some(e.clone());
1014 out.emit(|meta| Event::Error { meta, message: e });
1015 }
1016 }
1017 Input::Command(c) => self.apply_command(c, &mut out),
1018 Input::Poll => {}
1019 Input::Log(line) => out.log(line),
1020 Input::Finish => {
1021 if self.failed.is_none() {
1022 self.finish_session(&mut out);
1023 }
1024 self.finished = true;
1025 }
1026 }
1027 }
1028 }
1029 let bg = std::mem::take(&mut sh.emitter.lock().inbox);
1031 if !bg.is_empty() {
1032 evs.extend(bg);
1033 evs.sort_by_key(|e| e.meta().version);
1034 }
1035 evs
1036 }
1037
1038 pub fn positions(&self) -> usize {
1042 self.frames.len()
1043 }
1044 pub fn frames(&self) -> &[Vec<FrameImage>] {
1046 &self.frames
1047 }
1048 pub fn position_of(&self, name: &str) -> Option<usize> {
1050 self.position_of.get(name).copied()
1051 }
1052 pub fn model(&self) -> Option<&Reconstruction> {
1054 self.chain.as_ref()
1055 }
1056 pub fn adopted(&self) -> Option<usize> {
1058 self.adopted
1059 }
1060 pub fn store(&self) -> &FeatureStore {
1062 &self.store
1063 }
1064 pub fn graph(&self) -> &MatchGraph {
1066 &self.graph
1067 }
1068 pub fn config(&self) -> &SessionConfig {
1070 &self.shared.cfg
1071 }
1072 pub fn arrived_zones(&self) -> Vec<ZoneRange> {
1074 self.arrived.values().copied().collect()
1075 }
1076 pub fn previews(&self) -> &BTreeMap<usize, PreviewZone> {
1078 &self.previews
1079 }
1080 pub fn refined(&self) -> BTreeMap<usize, RefinedZone> {
1082 self.shared.bg().ready.clone()
1083 }
1084 pub fn pending_jobs(&self) -> usize {
1086 self.shared.bg().jobs.iter().filter(|j| !j.is_finished()).count()
1087 }
1088 pub fn version(&self) -> u64 {
1090 self.shared.emitter.lock().version
1091 }
1092 pub fn failed(&self) -> Option<&str> {
1094 self.failed.as_deref()
1095 }
1096 pub fn is_finished(&self) -> bool {
1098 self.finished
1099 }
1100
1101 fn ensure_started(&mut self, out: &mut Out) {
1104 if self.started {
1105 return;
1106 }
1107 self.started = true;
1108 *self.shared.start.lock().unwrap_or_else(|e| e.into_inner()) = Some(Instant::now());
1109 let positions = self.shared.cfg.total_positions;
1110 out.emit(|meta| Event::Started { meta, positions });
1111 }
1112
1113 fn announce(&mut self, fs: &FrameSet, p: usize, out: &mut Out) {
1115 let sh = self.shared.clone();
1116 if !sh.cfg.decode_frames || self.finished || self.failed.is_some() || self.announced.is_some_and(|a| a >= p) {
1117 return;
1118 }
1119 self.ensure_started(out);
1120 self.announced = Some(p);
1121 let decoded: Vec<Result<Frame, String>> = fs
1122 .images
1123 .par_iter()
1124 .map(|im| {
1125 let path = sh.cfg.image_root.join(&im.name);
1126 let img = image::ImageReader::open(&path)
1127 .and_then(|r| r.with_guessed_format())
1128 .map_err(|e| format!("{}: {e}", path.display()))?
1129 .decode()
1130 .map_err(|e| format!("{}: 디코딩 실패: {e}", path.display()))?
1131 .into_rgb8();
1132 let gps = fs.gps.iter().chain(self.gps.iter()).find(|g| g.name == im.name).cloned();
1133 let (width, height) = img.dimensions();
1134 Ok(Frame {
1135 position: p,
1136 camera: im.camera.clone(),
1137 name: im.name.clone(),
1138 path,
1139 width,
1140 height,
1141 rgb: Arc::from(img.into_raw()),
1142 gps,
1143 })
1144 })
1145 .collect();
1146 for d in decoded {
1147 match d {
1148 Ok(f) => {
1149 let frame = Arc::new(f);
1150 out.emit(|meta| Event::FrameDecoded { meta, frame });
1151 }
1152 Err(e) => out.emit(|meta| Event::Warning { meta, message: format!("[frame] {e}") }),
1153 }
1154 }
1155 }
1156
1157 pub fn lookahead(&self) -> Lookahead {
1160 Lookahead(Arc::clone(&self.shared.ahead.0))
1161 }
1162
1163 fn process_frames(&mut self, fs: FrameSet, out: &mut Out) -> Result<(), String> {
1164 self.process_segment(vec![fs], out)
1165 }
1166
1167 fn process_batch(&mut self, sets: Vec<FrameSet>, out: &mut Out) -> Result<(), String> {
1170 let cfg = &self.shared.cfg;
1171 let (span, ovl, total) = (cfg.span, cfg.overlap, cfg.total_positions);
1172 let mut seg: Vec<FrameSet> = Vec::new();
1173 for (p, fs) in (self.frames.len()..).zip(sets) {
1174 seg.push(fs);
1175 let boundary = p + 1 == span + ovl || !zones_ending_at(p, span, ovl, total).is_empty();
1176 if boundary {
1177 self.process_segment(std::mem::take(&mut seg), out)?;
1178 }
1179 }
1180 if !seg.is_empty() {
1181 self.process_segment(seg, out)?;
1182 }
1183 Ok(())
1184 }
1185
1186 fn extract_positions(&mut self, p0: usize, imgs: &[Vec<FrameImage>], out: &mut Out) -> Result<(), String> {
1189 let sh = self.shared.clone();
1190 let ahead = &sh.ahead.0;
1191 let mut results: Vec<Option<PositionExtract>> = Vec::with_capacity(imgs.len());
1192 let mut missing: Vec<usize> = Vec::new();
1193 for (i, im) in imgs.iter().enumerate() {
1194 match ahead.take(im) {
1195 Some(Ok(r)) => results.push(Some(r)),
1196 Some(Err(e)) if e != "cancelled" => return Err(e),
1197 _ => {
1198 results.push(None);
1199 missing.push(i);
1200 }
1201 }
1202 }
1203 if !missing.is_empty() {
1204 let list: Vec<&[FrameImage]> = missing.iter().map(|&i| imgs[i].as_slice()).collect();
1205 let done = ahead.run_now(&self.store, &list)?;
1206 for (&i, r) in missing.iter().zip(done) {
1207 results[i] = Some(r);
1208 }
1209 }
1210 for (i, r) in results.into_iter().enumerate() {
1211 let (p, r) = (p0 + i, r.expect("every position extracted"));
1212 report_extraction(&r.reports, p, out);
1213 for note in &r.notes {
1214 out.log(note.clone());
1215 }
1216 if *self.cam_ids != r.cams {
1217 self.cam_ids = Arc::new(r.cams);
1218 }
1219 sh.times.add("feature", r.elapsed);
1220 out.stamped(format!("[time] feature pos {p}: {:.3}s", r.elapsed.as_secs_f64()));
1221 }
1222 Ok(())
1223 }
1224
1225 fn process_segment(&mut self, sets: Vec<FrameSet>, out: &mut Out) -> Result<(), String> {
1230 let sh = self.shared.clone();
1231 let cfg = &sh.cfg;
1232 let p0 = self.frames.len();
1233 let n = sets.len();
1234 if n == 0 {
1235 return Ok(());
1236 }
1237 let last = p0 + n - 1;
1238 let label = if n == 1 { format!("pos {p0}") } else { format!("pos {p0}..{last}") };
1239 let mut imgs: Vec<Vec<FrameImage>> = Vec::with_capacity(n);
1240 for (i, fs) in sets.into_iter().enumerate() {
1241 let p = p0 + i;
1242 let names: Vec<String> = fs.images.iter().map(|i| i.name.clone()).collect();
1243 {
1244 let pos = Arc::make_mut(&mut self.position_of);
1245 for n in &names {
1246 pos.insert(n.clone(), p);
1247 }
1248 }
1249 Arc::make_mut(&mut self.frames).push(fs.images.clone());
1250 self.ingested += fs.images.len();
1251 if !fs.gps.is_empty() {
1252 Arc::make_mut(&mut self.gps).extend(fs.gps);
1253 }
1254 out.emit(|meta| Event::FrameIngested { meta, position: p, images: names });
1255 imgs.push(fs.images);
1256 }
1257
1258 self.extract_positions(p0, &imgs, out)?;
1260
1261 let per_pos: Vec<Vec<(ImageId, ImageId)>> = (p0..=last)
1263 .map(|p| {
1264 pairs_for(&self.frames, p)
1265 .into_iter()
1266 .filter_map(|(a, b)| Some((self.store.image_id_by_name(&a)?, self.store.image_id_by_name(&b)?)))
1267 .collect()
1268 })
1269 .collect();
1270 let pairs: Vec<(ImageId, ImageId)> = if n == 1 { per_pos[0].clone() } else { per_pos.iter().flatten().copied().collect() };
1271 let mut mopts = PairMatchingOptions::default();
1272 mopts.geometry.ransac.random_seed = cfg.seed;
1273 let t0 = Instant::now();
1274 let st = match_pairs_with(&self.store, &pairs, &mopts, cfg.matcher.as_ref(), cfg.sparse.two_view.as_ref());
1275 out.stage("matching", &format!("{label} ({} pairs)", pairs.len()), t0);
1276 let st = st.map_err(|e| e.to_string())?;
1277 Arc::make_mut(&mut self.graph).update_from_store(&self.store, &MatchGraphOptions::default());
1278 for (i, pp) in per_pos.iter().enumerate() {
1279 let p = p0 + i;
1280 let (np, nv, nm) = if n == 1 {
1281 (pairs.len(), st.num_valid_geometries, st.num_matched)
1282 } else {
1283 let valid =
1284 pp.iter().filter(|(a, b)| self.store.get_two_view(*a, *b).is_some_and(|g| !g.inlier_matches.is_empty())).count();
1285 (pp.len(), valid, pp.len())
1286 };
1287 out.log(format!("[match] pos {p}: 짝 {np} 매칭 {nm} 유효 기하 {nv} (그래프 영상 {})", self.graph.image_ids().len()));
1288 out.emit(|meta| Event::PairsMatched { meta, pairs: np, verified: nv, position: p, matched: nm });
1289 }
1290
1291 let (span, ovl) = (cfg.span, cfg.overlap);
1293 if last + 1 < span + ovl {
1294 self.checkpoint(last);
1295 return Ok(());
1296 }
1297 if last + 1 == span + ovl {
1298 let t0 = Instant::now();
1299 let gopts = GlobalSfmOptions { ba_backend: BaBackendRef::new(cfg.ba_backend.clone()), ..GlobalSfmOptions::script() };
1300 let o = global_mapper(&self.store, &self.graph, &gopts);
1301 out.stage("mapper", &format!("pos {last}"), t0);
1302 let o = o.map_err(|e| e.to_string())?;
1303 if let Some(f) = &o.failure {
1304 out.warn(format!("[mapper] 실패 보고: {f}"));
1305 }
1306 out.log(format!("[mapper] {:?}", o.summary));
1307 let rec = o.reconstruction;
1308 let model = Arc::new(rec.clone());
1309 let registered = rec.registered_image_count();
1310 out.emit(|meta| Event::ModelInitialized { meta, registered, position: last, model });
1311 let ids = rec.registered_images();
1312 self.emit_registered(&rec, &ids, out);
1313 self.chain = Some(rec);
1314 } else {
1315 let mut k = self.adopted.map_or(0, |a| a + 1);
1317 loop {
1318 let r = sh.bg().ready.get(&k).map(|z| z.model.clone());
1319 let Some(r) = r else { break };
1320 self.chain = Some((*r).clone());
1321 self.adopted = Some(k);
1322 out.emit(|meta| Event::BaseAdopted { meta, zone: k });
1323 k += 1;
1324 }
1325 if let Some(base) = &self.chain {
1326 let mut reg = base.clone();
1327 let t0 = Instant::now();
1328 let rr =
1329 register_images_with(&mut reg, &self.store, &self.graph, &RegistrationOptions::default(), cfg.sparse.abs_pose.as_ref());
1330 out.stage("registration", &label, t0);
1331 match rr {
1332 Ok(rep) => {
1333 let new_ids = rep.registered();
1334 out.log(format!("[register] {label}: 시도 {} 등록 {}", rep.attempts.len(), new_ids.len()));
1335 let scope = if cfg.incremental_triangulation {
1336 TriangulationScope::Images(new_ids.clone())
1337 } else {
1338 TriangulationScope::AllRegistered
1339 };
1340 let topts = PointTriangulatorOptions { clear_points: false, scope, ..Default::default() };
1341 let t0 = Instant::now();
1342 let tr = triangulate_points_with(&mut reg, &self.graph, &topts, cfg.sparse.triangulation.as_ref());
1343 out.stage("triangulation", &label, t0);
1344 match tr {
1345 Ok(t) => {
1346 out.log(format!(
1347 "[triangulate] {label}: 생성 {} 연장 {} 완성 {} 병합 {} 필터 {} (점 {})",
1348 t.num_created,
1349 t.num_continued,
1350 t.num_completed,
1351 t.num_merged,
1352 t.num_filtered,
1353 reg.num_points3d()
1354 ));
1355 self.emit_registered(®, &new_ids, out);
1356 self.chain = Some(reg);
1357 }
1358 Err(e) => out.warn(format!("[triangulate] {label} 실패: {e} (이전 모델 유지)")),
1359 }
1360 }
1361 Err(e) => out.warn(format!("[register] {label} 실패: {e}")),
1362 }
1363 }
1364 }
1365 let registered = self.chain.as_ref().map_or(0, |r| r.registered_image_count());
1366 for p in p0..=last {
1367 if p + 1 < span + ovl {
1368 continue;
1369 }
1370 let expected: usize = self.frames[..=p].iter().map(|f| f.len()).sum();
1371 out.emit(|meta| Event::PositionDone { meta, position: p, registered, expected });
1372 }
1373
1374 for z in zones_ending_at(last, span, ovl, cfg.total_positions) {
1376 if !self.arrived.contains_key(&z.zone) {
1377 self.process_zone(z, out);
1378 }
1379 }
1380 self.checkpoint(last);
1381 Ok(())
1382 }
1383
1384 fn emit_registered(&self, rec: &Reconstruction, ids: &[ImageId], out: &mut Out) {
1385 for &id in ids {
1386 let (Some(im), Some(pose)) = (rec.image(id), rec.world_to_cam(id)) else { continue };
1387 let image = im.name.clone();
1388 let position = self.position_of(&image).unwrap_or(usize::MAX);
1389 out.emit(|meta| Event::FrameRegistered { meta, position, image, image_id: id, cam_from_world: pose });
1390 }
1391 }
1392
1393 fn process_zone(&mut self, range: ZoneRange, out: &mut Out) {
1395 let sh = self.shared.clone();
1396 let k = range.zone;
1397 let snap = self.chain.clone().unwrap_or_default();
1398 let m = Arc::new(snap.clone());
1399 out.emit(|meta| Event::ZoneArrived { meta, range, model: m });
1400 if sh.cfg.stable_display {
1401 let keep: Arc<HashSet<String>> =
1402 Arc::new(self.position_of.iter().filter(|(_, p)| **p >= range.lo && **p < range.hi).map(|(n, _)| n.clone()).collect());
1403 let generation = sh.generation(k);
1404 let (sh2, gps, cam_ids) = (sh.clone(), self.gps.clone(), self.cam_ids.clone());
1405 let (snap_job, keep_job) = (snap.clone(), keep.clone());
1406 let h = std::thread::spawn(move || sh2.refine_job(range, snap_job, generation, gps, keep_job, cam_ids));
1407 sh.bg().jobs.push(h);
1408 self.arrived.insert(k, range);
1409 if sh.cfg.preview {
1410 self.sparse_preview(range, &snap, &keep, out);
1411 }
1412 return;
1413 }
1414 let mut preview = snap.clone();
1415 let mut frame = match self.adopted {
1416 Some(j) => format!("refined_{j}"),
1417 None => "unaligned".to_string(),
1418 };
1419 if k == 0 && sh.align_enu(&mut preview, "preview 0", &self.gps, out) {
1420 frame = "gps".to_string();
1421 }
1422 let keep: Arc<HashSet<String>> =
1423 Arc::new(self.position_of.iter().filter(|(_, p)| **p >= range.lo && **p < range.hi).map(|(n, _)| n.clone()).collect());
1424 let generation = sh.generation(k);
1425 {
1426 let (sh2, gps, keep, cam_ids) = (sh.clone(), self.gps.clone(), keep.clone(), self.cam_ids.clone());
1427 let h = std::thread::spawn(move || sh2.refine_job(range, snap, generation, gps, keep, cam_ids));
1428 sh.bg().jobs.push(h);
1429 }
1430 let cloud = sh.dense_region("preview", k, &preview, &keep, out);
1431 let model = Arc::new(preview);
1432 self.arrived.insert(k, range);
1433 Arc::make_mut(&mut self.previews)
1434 .insert(k, PreviewZone { range, model: model.clone(), cloud: cloud.clone(), frame: frame.clone() });
1435 out.emit(|meta| Event::ZonePreview { meta, range, dense: cloud.is_some(), cloud: cloud.unwrap_or_default(), model, frame });
1436 }
1437
1438 fn sparse_preview(&mut self, range: ZoneRange, snap: &Reconstruction, keep: &HashSet<String>, out: &mut Out) {
1446 let sh = self.shared.clone();
1447 let k = range.zone;
1448 let own_lo = if k == 0 { 0 } else { (k * sh.cfg.span + sh.cfg.overlap).max(range.lo) };
1450 let own: HashSet<ImageId> = snap
1451 .registered_images()
1452 .into_iter()
1453 .filter(|id| {
1454 snap.image(*id).is_some_and(|im| keep.contains(&im.name) && self.position_of.get(&im.name).is_some_and(|p| *p >= own_lo))
1455 })
1456 .collect();
1457 let mut cloud = PointCloud::default();
1458 for (_, pt) in snap.points3d() {
1459 let Some(t) = pt.track.iter().find(|t| own.contains(&t.image_id)) else { continue };
1460 cloud.positions.push([pt.xyz.x as f32, pt.xyz.y as f32, pt.xyz.z as f32]);
1461 cloud.colors.push(pt.color);
1462 cloud.keys.push(preview_key(t.image_id, t.point2d_idx));
1463 }
1464 if cloud.is_empty() {
1465 out.log(format!("[preview] 구역 {k}: 표시할 3D 점 없음 — 미리보기 생략"));
1466 return;
1467 }
1468 let shown = sh.bg().ready.range(..k).next_back().map(|(j, z)| (*j, z.model.clone()));
1469 let fit = match &shown {
1470 Some((j, m)) => match fit_shared_points(snap, m, None, MIN_SHARED_PAIRS) {
1471 Some((f, n)) => Some((f.sim3, format!("구역 {j} 과 공유 점 {n}, 잔차 중앙 {:.3}m", f.median))),
1472 None => sim3_between(snap, m, |_| true).map(|t| (t, format!("구역 {j} 과 공유 영상 카메라 중심"))),
1473 },
1474 None => {
1475 let targets = enu_targets(snap, &self.gps);
1476 let (src, dst): (Vec<_>, Vec<_>) =
1477 targets.iter().filter_map(|(id, g)| snap.projection_center(*id).map(|c| (c, *g))).unzip();
1478 (src.len() >= 3)
1479 .then(|| cumulus3d_align::fit_enu_sim3(snap, &src, &dst))
1480 .flatten()
1481 .map(|(t, _)| (t, format!("GPS {} 개 (ENU)", src.len())))
1482 }
1483 };
1484 let Some((t, how)) = fit else {
1485 out.warn(format!("[preview] 구역 {k}: 표시 좌표계에 맞출 기준 없음 — 미리보기 생략"));
1486 return;
1487 };
1488 cumulus3d_align::transform_cloud(&mut cloud, &t);
1489 let mut model = snap.clone();
1490 model.transform(&t);
1491 let (model, cloud) = (Arc::new(model), Arc::new(cloud));
1492 out.log(format!("[preview] 구역 {k}: 희소 점 {} 개를 표시 좌표계에 배치 ({how}, 축척 {:.4})", cloud.len(), t.scale));
1493 let frame = "display".to_string();
1494 Arc::make_mut(&mut self.previews)
1495 .insert(k, PreviewZone { range, model: model.clone(), cloud: Some(cloud.clone()), frame: frame.clone() });
1496 out.emit(|meta| Event::ZonePreview { meta, range, dense: false, cloud, model, frame });
1497 }
1498
1499 fn checkpoint(&mut self, position: usize) {
1500 let n = self.shared.cfg.history;
1501 if n == 0 {
1502 return;
1503 }
1504 self.checkpoints.push_back(Checkpoint {
1505 position,
1506 chain: self.chain.clone(),
1507 adopted: self.adopted,
1508 cam_ids: self.cam_ids.clone(),
1509 graph: self.graph.clone(),
1510 gps_len: self.gps.len(),
1511 });
1512 while self.checkpoints.len() > n {
1513 self.checkpoints.pop_front();
1514 }
1515 }
1516
1517 fn drop_zone(&mut self, k: usize, out: &mut Out) {
1518 self.arrived.remove(&k);
1519 Arc::make_mut(&mut self.previews).remove(&k);
1520 self.shared.invalidate(k);
1521 out.emit(|meta| Event::ZoneInvalidated { meta, zone: k });
1522 }
1523
1524 fn apply_command(&mut self, cmd: Command, out: &mut Out) {
1525 match cmd {
1526 Command::ResetFrom(p) => self.reset_from(p, out),
1527 Command::InvalidateZone(k) => {
1528 let Some(range) = self.arrived.get(&k).copied() else {
1529 out.emit(|meta| Event::Warning {
1530 meta, message: format!("구역 {k} 은 아직 도착하지 않음 — 무효화할 것 없음")
1531 });
1532 return;
1533 };
1534 self.drop_zone(k, out);
1535 self.process_zone(range, out);
1537 }
1538 }
1539 }
1540
1541 fn reset_from(&mut self, p: usize, out: &mut Out) {
1544 let npos = self.frames.len();
1545 if p > npos {
1546 out.emit(|meta| Event::Warning { meta, message: format!("ResetFrom({p}): 처리한 위치 {npos} 보다 큼 — 무시") });
1547 return;
1548 }
1549 let cp = if p == 0 {
1550 None
1551 } else {
1552 match self.checkpoints.iter().find(|c| c.position + 1 == p) {
1553 Some(c) => Some(c.clone()),
1554 None => {
1555 out.emit(|meta| Event::Warning {
1556 meta, message: format!("ResetFrom({p}): 되감기 지점 없음(history 부족) — 무시")
1557 });
1558 return;
1559 }
1560 }
1561 };
1562 let gone: Vec<usize> = self.arrived.iter().filter(|(_, r)| r.hi.saturating_sub(1) >= p).map(|(k, _)| *k).collect();
1563 for k in gone {
1564 self.drop_zone(k, out);
1565 }
1566 let pos = self.position_of.clone();
1567 self.store = Arc::new(self.store.retain_copy(|im| pos.get(&im.name).is_some_and(|q| *q < p)));
1568 Arc::make_mut(&mut self.frames).truncate(p);
1569 Arc::make_mut(&mut self.position_of).retain(|_, q| *q < p);
1570 self.ingested = self.frames.iter().map(|f| f.len()).sum();
1571 self.checkpoints.retain(|c| c.position < p);
1572 match cp {
1573 Some(c) => {
1574 self.chain = c.chain;
1575 self.adopted = c.adopted;
1576 self.cam_ids = c.cam_ids;
1577 self.graph = c.graph;
1578 Arc::make_mut(&mut self.gps).truncate(c.gps_len);
1579 }
1580 None => {
1581 self.chain = None;
1582 self.adopted = None;
1583 self.cam_ids = Arc::new(BTreeMap::new());
1584 self.graph = Arc::new(MatchGraph::new());
1585 self.gps = Arc::new(self.shared.cfg.gps.clone());
1586 }
1587 }
1588 self.shared.ahead.0.reset(Arc::clone(&self.store), (*self.cam_ids).clone());
1589 out.emit(|meta| Event::Reset { meta, position: p });
1590 }
1591
1592 fn finish_session(&mut self, out: &mut Out) {
1593 let sh = self.shared.clone();
1594 let (span, ovl) = (sh.cfg.span, sh.cfg.overlap);
1595 let npos = self.frames.len();
1597 if npos > 0 && npos >= span + ovl {
1598 for z in zones_upto(npos, span, ovl) {
1599 if !self.arrived.contains_key(&z.zone) {
1600 self.process_zone(z, out);
1601 }
1602 }
1603 }
1604 out.emit(|meta| Event::AllPositionsDone { meta });
1605 loop {
1606 let jobs = std::mem::take(&mut sh.bg().jobs);
1607 if jobs.is_empty() {
1608 break;
1609 }
1610 for j in jobs {
1611 let _ = j.join();
1612 }
1613 }
1614 let chain = match (&self.chain, sh.cfg.stable_display) {
1615 (Some(c), true) => {
1616 let mut enu = c.clone();
1620 if sh.align_enu(&mut enu, "final (stable display)", &self.gps, out) {
1621 match display_to_enu_scale(c, &enu) {
1624 Some(s) => out.log(format!("[stable] 표시→ENU 축척 {s:.4} (1.0 이면 표시 좌표계가 ENU 와 같음)")),
1625 None => out.log("[stable] 표시→ENU 축척: 공통 영상 부족".to_string()),
1626 }
1627 Some(Arc::new(enu))
1628 } else {
1629 out.warn(
1630 "[align] final (stable display): GPS 정렬 실패 — 표시 좌표계 모델을 그대로 넘김(final_frame/ 은 표시 좌표계)"
1631 .to_string(),
1632 );
1633 Some(Arc::new(c.clone()))
1634 }
1635 }
1636 (c, _) => c.clone().map(Arc::new),
1637 };
1638 out.emit(|meta| Event::AllRefinedDone { meta, chain });
1639 let pipeline_secs = sh.start.lock().unwrap_or_else(|e| e.into_inner()).map_or(0.0, |t| t.elapsed().as_secs_f64());
1640 let stage_times = sh.times.snapshot().into_iter().map(|(k, (d, n))| (k, d, n)).collect();
1641 let chain_stats = self.chain.as_ref().map(|c| ModelStats::compute(c).lines()).unwrap_or_default();
1642 out.emit(|meta| Event::Finished { meta, pipeline_secs, stage_times, chain_stats });
1643 }
1644}
1645
1646fn report_extraction(rep: &[ImageReport], position: usize, out: &mut Out) {
1647 for r in rep {
1648 match &r.status {
1649 ImageStatus::Extracted { image_id, camera_id, num_features } => {
1650 out.log(format!("[feature] {} id {image_id} cam {camera_id} 특징 {num_features}", r.name));
1651 let (image, image_id, count) = (r.name.clone(), *image_id, *num_features);
1652 out.emit(|meta| Event::FeaturesExtracted { meta, image, count, position, image_id });
1653 }
1654 ImageStatus::AlreadyExists { image_id } => out.log(format!("[feature] {} id {image_id} 이미 있음", r.name)),
1655 ImageStatus::Failed { error } => out.warn(format!("[feature] {} 실패: {error}", r.name)),
1656 }
1657 }
1658}
1659
1660pub(crate) fn fmt_params(p: &[f64]) -> String {
1661 format!("[{}]", p.iter().map(|x| format!("{x:.5}")).collect::<Vec<_>>().join(" "))
1662}
1663
1664fn display_to_enu_scale(display: &Reconstruction, enu: &Reconstruction) -> Option<f64> {
1666 let (mut src, mut dst) = (Vec::new(), Vec::new());
1667 for id in display.registered_images() {
1668 let name = display.image(id)?.name.clone();
1669 let Some(eid) = enu.image_by_name(&name).map(|im| im.image_id) else { continue };
1670 if let (Some(a), Some(b)) = (display.projection_center(id), enu.projection_center(eid)) {
1671 src.push(a);
1672 dst.push(b);
1673 }
1674 }
1675 if src.len() < 3 {
1676 return None;
1677 }
1678 cumulus3d_align::umeyama(&src, &dst, true).map(|t| t.scale)
1679}
1680
1681fn gravity_note(what: &str, g: &cumulus3d_align::GravityResolution) -> String {
1683 use cumulus3d_align::GravityPrior;
1684 let prior = match g.prior {
1685 GravityPrior::Ground { inliers } => format!("지면 평면(점 {inliers} 개)"),
1686 GravityPrior::ViewDirection => "카메라 시선(아래 방향)".to_string(),
1687 GravityPrior::None => "사용 불가 — 회전 그대로".to_string(),
1688 };
1689 format!("[gravity] {what}: GPS 가 거의 일직선(σ2/σ1 {:.3}) — 직선 둘레 회전을 {prior} 로 결정", g.ratio)
1690}
1691
1692fn enu_targets(rec: &Reconstruction, gps: &[GpsRecord]) -> HashMap<ImageId, cumulus3d_core::Vec3> {
1694 let Some(o) = gps.first() else { return HashMap::new() };
1695 let frame = cumulus3d_align::EnuFrame::new(o.lat, o.lon, o.alt);
1696 let by_name: HashMap<&str, &GpsRecord> = gps.iter().map(|g| (g.name.as_str(), g)).collect();
1697 rec.registered_images()
1698 .into_iter()
1699 .filter_map(|id| {
1700 let g = by_name.get(rec.image(id)?.name.as_str())?;
1701 Some((id, frame.lla_to_enu(g.lat, g.lon, g.alt)))
1702 })
1703 .collect()
1704}
1705
1706fn sim3_onto_shown(rec: &mut Reconstruction, shown: &Reconstruction) -> Option<(cumulus3d_core::Sim3, Vec<(ImageId, Rigid3)>)> {
1709 let mut pairs = Vec::new();
1710 for id in rec.registered_images() {
1711 let Some(name) = rec.image(id).map(|im| im.name.clone()) else { continue };
1712 let Some(sid) = shown.image_by_name(&name).map(|im| im.image_id) else { continue };
1713 if let (Some(a), Some(b), Some(pose)) = (rec.projection_center(id), shown.projection_center(sid), shown.world_to_cam(sid)) {
1714 pairs.push((id, a, b, pose));
1715 }
1716 }
1717 let (src, dst): (Vec<_>, Vec<_>) = pairs.iter().map(|(_, a, b, _)| (*a, *b)).unzip();
1718 let t = cumulus3d_align::umeyama(&src, &dst, true)?;
1719 rec.transform(&t);
1720 Some((t, pairs.into_iter().map(|(id, _, _, pose)| (id, pose)).collect()))
1721}
1722
1723fn anchor_to_shown(rec: &mut Reconstruction, shown: &Reconstruction, ba: &mut BaConfig) -> (usize, f64) {
1726 let Some((t, poses)) = sim3_onto_shown(rec, shown) else { return (0, 1.0) };
1727 for (id, pose) in &poses {
1728 if rec.set_world_to_cam(*id, *pose).is_ok() {
1729 ba.constant_poses.insert(*id);
1730 }
1731 }
1732 for (cid, cam) in shown.cameras() {
1733 if let Some(c) = rec.camera_mut(*cid) {
1734 *c = cam.clone();
1735 ba.constant_cameras.insert(*cid);
1736 }
1737 }
1738 (ba.constant_poses.len(), t.scale)
1739}
1740
1741const MIN_SHARED_PAIRS: usize = 20;
1743
1744fn preview_key(image: ImageId, point2d: u32) -> u64 {
1747 ((image as u64) << 32) | 0xFFFF_0000 | (point2d as u64 & 0xFFFF)
1748}
1749
1750fn sim3_between(from: &Reconstruction, to: &Reconstruction, keep: impl Fn(&str) -> bool) -> Option<cumulus3d_core::Sim3> {
1753 let (mut src, mut dst) = (Vec::new(), Vec::new());
1754 for id in from.registered_images() {
1755 let Some(name) = from.image(id).map(|im| im.name.as_str()).filter(|n| keep(n)) else { continue };
1756 let Some(tid) = to.image_by_name(name).map(|im| im.image_id) else { continue };
1757 if let (Some(a), Some(b)) = (from.projection_center(id), to.projection_center(tid)) {
1758 src.push(a);
1759 dst.push(b);
1760 }
1761 }
1762 if src.len() < 3 {
1763 return None;
1764 }
1765 cumulus3d_align::umeyama(&src, &dst, true)
1766}
1767
1768pub fn intrinsics_change(a: &BTreeMap<CameraId, Camera>, b: &BTreeMap<CameraId, Camera>) -> f64 {
1771 let mut worst = 0.0f64;
1772 for (id, ca) in a {
1773 let Some(cb) = b.get(id) else { continue };
1774 if ca.model != cb.model {
1775 return f64::INFINITY;
1776 }
1777 for &s in ca.model.focal_slots() {
1778 let (fa, fb) = (ca.params[s], cb.params[s]);
1779 worst = worst.max(if fa != 0.0 {
1780 ((fb - fa) / fa).abs()
1781 } else if fb != 0.0 {
1782 f64::INFINITY
1783 } else {
1784 0.0
1785 });
1786 }
1787 }
1788 worst
1789}
1790
1791pub fn pairs_for(frames: &[Vec<FrameImage>], p: usize) -> Vec<(String, String)> {
1794 let mut pr = BTreeSet::new();
1795 let Some(cur) = frames.get(p) else { return Vec::new() };
1796 for im in cur {
1797 for d in [1usize, 2, 3, 4, 5, 8, 16] {
1798 if p >= d {
1799 if let Some(o) = frames[p - d].iter().find(|x| x.camera == im.camera) {
1800 pr.insert((o.name.clone(), im.name.clone()));
1801 }
1802 }
1803 }
1804 }
1805 for a in cur {
1806 for fq in &frames[p.saturating_sub(4)..=p] {
1807 for b in fq {
1808 if a.camera == b.camera {
1809 continue;
1810 }
1811 let (x, y) = (a.name.clone(), b.name.clone());
1812 pr.insert(if x <= y { (x, y) } else { (y, x) });
1813 }
1814 }
1815 }
1816 pr.into_iter().collect()
1817}
1818
1819pub fn zones_upto(total: usize, span: usize, ovl: usize) -> Vec<ZoneRange> {
1821 let mut v = Vec::new();
1822 let mut start = 0;
1823 let mut k = 0;
1824 while start < total {
1825 v.push(ZoneRange { zone: k, lo: start.saturating_sub(ovl), hi: (start + span + ovl).min(total) });
1826 k += 1;
1827 start += span.max(1);
1828 }
1829 v
1830}
1831
1832fn zones_ending_at(p: usize, span: usize, ovl: usize, total: Option<usize>) -> Vec<ZoneRange> {
1834 let mut v = Vec::new();
1835 let mut k = 0;
1836 loop {
1837 let start = k * span.max(1);
1838 if start > p || total.is_some_and(|n| start >= n) {
1839 break;
1840 }
1841 let hi = total.map_or(start + span + ovl, |n| (start + span + ovl).min(n));
1842 if hi == p + 1 {
1843 v.push(ZoneRange { zone: k, lo: start.saturating_sub(ovl), hi });
1844 }
1845 k += 1;
1846 }
1847 v
1848}
1849
1850#[cfg(test)]
1851mod tests {
1852 use super::*;
1853
1854 fn frames(n: usize) -> Vec<Vec<FrameImage>> {
1855 (0..n)
1856 .map(|p| {
1857 ["camF", "camR", "camL"]
1858 .iter()
1859 .map(|c| FrameImage { camera: c.to_string(), name: format!("{c}/{c}_{:04}.jpg", p * 3) })
1860 .collect()
1861 })
1862 .collect()
1863 }
1864
1865 #[test]
1866 fn zones_match_regions() {
1867 let z = |v: Vec<ZoneRange>| v.into_iter().map(|r| (r.zone, r.lo, r.hi)).collect::<Vec<_>>();
1868 assert_eq!(z(zones_upto(26, 12, 2)), vec![(0, 0, 14), (1, 10, 26), (2, 22, 26)]);
1869 let mut all = Vec::new();
1871 for p in 0..26 {
1872 all.extend(zones_ending_at(p, 12, 2, Some(26)));
1873 }
1874 all.sort_by_key(|r| r.zone);
1875 assert_eq!(z(all), z(zones_upto(26, 12, 2)));
1876 assert_eq!(z(zones_ending_at(13, 12, 2, None)), vec![(0, 0, 14)]);
1878 assert_eq!(z(zones_ending_at(25, 12, 2, None)), vec![(1, 10, 26)]);
1879 assert!(zones_ending_at(25, 12, 2, Some(40)).iter().any(|r| r.zone == 1));
1880 }
1881
1882 #[test]
1883 fn intrinsics_change_is_max_relative_focal_change() {
1884 use cumulus3d_core::CameraModelKind;
1885 let cam = |id: CameraId, model: CameraModelKind, params: Vec<f64>| Camera::new(id, model, 4000, 3000, params).unwrap();
1886 let single = |id, f: f64| cam(id, CameraModelKind::SingleFocalRadial, vec![f, 2000.0, 1500.0, 0.1]);
1887 let pinhole = |id, fx: f64, fy: f64| cam(id, CameraModelKind::Pinhole, vec![fx, fy, 2000.0, 1500.0]);
1888 let map = |v: Vec<Camera>| v.into_iter().map(|c| (c.camera_id, c)).collect::<BTreeMap<_, _>>();
1889 let a = map(vec![single(1, 1000.0), pinhole(2, 2000.0, 2000.0)]);
1890 assert_eq!(intrinsics_change(&a, &a), 0.0);
1891 let b = map(vec![single(1, 1005.0), pinhole(2, 2000.0, 2040.0), single(3, 1.0)]);
1893 assert!((intrinsics_change(&a, &b) - 0.02).abs() < 1e-12);
1894 let c = map(vec![single(1, 990.0), pinhole(2, 2000.0, 2000.0)]);
1895 assert!((intrinsics_change(&a, &c) - 0.01).abs() < 1e-12);
1896 assert_eq!(intrinsics_change(&a, &map(vec![single(9, 5.0)])), 0.0);
1898 assert!(intrinsics_change(&a, &map(vec![pinhole(1, 1000.0, 1000.0)])).is_infinite());
1899 }
1900
1901 #[test]
1902 fn pair_rule() {
1903 let f = frames(20);
1904 assert_eq!(pairs_for(&f, 0).len(), 3);
1905 let p17 = pairs_for(&f, 17);
1906 assert_eq!(p17.len(), 21 + 27);
1907 assert!(p17.contains(&("camF/camF_0003.jpg".to_string(), "camF/camF_0051.jpg".to_string())));
1908 }
1909}