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