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