1use crate::blocks::{DiffStream, Heartbeat, LiveStream};
59use crate::events::{Event, EventKind, ZoneRange};
60use crate::pipeline::{Pipeline, Reducer};
61use crate::post::{self, PostZones, ZoneCloud, ZoneTimes};
62use crate::util::Logger;
63use cumulus3d_core::interop::{write_model_binary, ImageOrder};
64use cumulus3d_core::io::{write_ply, PlyLayout, PointCloud};
65use cumulus3d_core::Reconstruction;
66use std::collections::{BTreeMap, HashMap};
67use std::fs::File;
68use std::io::Write;
69use std::path::{Path, PathBuf};
70use std::sync::{Arc, Mutex};
71use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
72
73pub type Hook = Box<dyn FnMut(&Event) + Send>;
75
76pub const TIMELINE_TABLE: [(EventKind, &str); 11] = [
78 (EventKind::Started, "start NPOS={positions}"),
79 (EventKind::ModelInitialized, "init_model Registered images: {registered}"),
80 (EventKind::BaseAdopted, "adopt refined {zone} as base"),
81 (EventKind::PositionDone, "pos {position} registered {registered}/{expected}"),
82 (EventKind::ZoneArrived, "region {zone} arrived pos[{lo},{hi})"),
83 (EventKind::ZonePreview, "preview {zone} ready"),
84 (EventKind::RefineStarted, "refined {zone} ba_start"),
85 (
86 EventKind::ZoneRefinedPose,
87 "refined {zone} pose_ready Registered images: {registered} Mean reprojection error: {mean_reproj_px:.6}px ",
88 ),
89 (EventKind::ZoneRefined, "refined {zone} ready"),
90 (EventKind::AllPositionsDone, "all positions done"),
91 (EventKind::AllRefinedDone, "all refined done"),
92];
93
94pub trait Sink: Send {
96 fn kinds(&self) -> Vec<EventKind>;
98 fn handle(&mut self, e: &Event);
100}
101
102pub fn hooks<S: Sink + 'static>(sink: S) -> Vec<(EventKind, Hook)> {
104 let kinds = sink.kinds();
105 let shared = Arc::new(Mutex::new(sink));
106 kinds
107 .into_iter()
108 .map(|k| {
109 let s = Arc::clone(&shared);
110 let h: Hook = Box::new(move |e: &Event| s.lock().unwrap_or_else(|p| p.into_inner()).handle(e));
111 (k, h)
112 })
113 .collect()
114}
115
116pub fn any_hook<S: Sink + 'static>(mut sink: S) -> Hook {
118 let kinds = sink.kinds();
119 Box::new(move |e: &Event| {
120 if kinds.contains(&e.kind()) {
121 sink.handle(e)
122 }
123 })
124}
125
126fn epoch_secs(t: SystemTime) -> f64 {
127 t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs_f64()
128}
129
130fn zone_file(kind: &str, r: &ZoneRange) -> String {
131 format!("{kind}_{:02}_pos{}-{}.ply", r.zone, r.lo, r.hi)
132}
133
134#[derive(Clone)]
138pub struct RunLog {
139 inner: Arc<RunLogInner>,
140}
141
142struct RunLogInner {
143 logger: Logger,
144 extra_times: Mutex<Vec<(String, Duration)>>,
146}
147
148impl RunLog {
149 pub fn open(path: Option<&Path>, echo: bool) -> std::io::Result<Self> {
151 Ok(Self { inner: Arc::new(RunLogInner { logger: Logger::new(path, echo)?, extra_times: Mutex::new(Vec::new()) }) })
152 }
153 pub fn logger(&self) -> &Logger {
155 &self.inner.logger
156 }
157 pub fn line(&self, s: &str) {
159 self.inner.logger.line(s)
160 }
161 pub fn stamped_at(&self, at: SystemTime, s: &str) {
163 self.inner.logger.stamped_at(at, s)
164 }
165 pub fn add_time(&self, stage: &str, d: Duration) {
167 if let Ok(mut v) = self.inner.extra_times.lock() {
168 v.push((stage.to_string(), d));
169 }
170 }
171 fn take_times(&self) -> Vec<(String, Duration)> {
172 self.inner.extra_times.lock().map(|mut v| std::mem::take(&mut *v)).unwrap_or_default()
173 }
174}
175
176pub struct TimelineSink {
180 file: File,
181}
182
183impl TimelineSink {
184 pub fn create(path: &Path) -> std::io::Result<Self> {
186 Ok(Self { file: File::create(path)? })
187 }
188
189 pub fn line(e: &Event) -> Option<String> {
191 let s = e.timeline_text()?;
192 let t = e.meta().at.duration_since(UNIX_EPOCH).unwrap_or_default();
193 Some(format!("{}.{:09} {s}", t.as_secs(), t.subsec_nanos()))
194 }
195}
196
197impl Sink for TimelineSink {
198 fn kinds(&self) -> Vec<EventKind> {
199 TIMELINE_TABLE.iter().map(|(k, _)| *k).collect()
200 }
201 fn handle(&mut self, e: &Event) {
202 if let Some(l) = Self::line(e) {
203 let _ = writeln!(self.file, "{l}");
204 let _ = self.file.flush();
205 }
206 }
207}
208
209pub struct RunLogSink {
216 log: RunLog,
217 done: Option<PathBuf>,
218}
219
220impl RunLogSink {
221 pub fn new(log: RunLog, done: Option<PathBuf>) -> Self {
223 Self { log, done }
224 }
225}
226
227impl Sink for RunLogSink {
228 fn kinds(&self) -> Vec<EventKind> {
229 let mut v: Vec<EventKind> = TIMELINE_TABLE.iter().map(|(k, _)| *k).collect();
230 v.extend([EventKind::Log, EventKind::Finished]);
231 v
232 }
233 fn handle(&mut self, e: &Event) {
234 if let Some(s) = e.timeline_text() {
235 self.log.stamped_at(e.meta().at, &s);
236 return;
237 }
238 match e {
239 Event::Log { meta, line, stamped } => {
240 if *stamped {
241 self.log.stamped_at(meta.at, line)
242 } else {
243 self.log.line(line)
244 }
245 }
246 Event::Finished { pipeline_secs, stage_times, chain_stats, .. } => {
247 let mut m: BTreeMap<String, (Duration, usize)> = BTreeMap::new();
248 for (s, d, n) in stage_times {
249 let x = m.entry(s.clone()).or_default();
250 x.0 += *d;
251 x.1 += n;
252 }
253 for (s, d) in self.log.take_times() {
254 let x = m.entry(s).or_default();
255 x.0 += d;
256 x.1 += 1;
257 }
258 self.log.line(&format!("== 단계별 시간 (파이프라인 {pipeline_secs:.1}s, 배경 스레드와 겹친 누적)"));
259 for (stage, (d, n)) in m {
260 self.log.line(&format!("{stage:<14} {:>9.2}s ({n}회)", d.as_secs_f64()));
261 }
262 if !chain_stats.is_empty() {
263 self.log.line("== 최종 체인 모델");
264 for l in chain_stats {
265 self.log.line(l);
266 }
267 }
268 if let Some(p) = &self.done {
269 if let Err(err) = std::fs::write(p, b"") {
270 self.log.line(&format!("{}: {err}", p.display()));
271 }
272 }
273 }
274 _ => {}
275 }
276 }
277}
278
279pub struct ZonePlySink {
284 full: PathBuf,
285 log: Option<RunLog>,
286}
287
288impl ZonePlySink {
289 pub fn new(full: PathBuf, log: Option<RunLog>) -> Self {
291 Self { full, log }
292 }
293
294 fn write(&self, kind: &str, r: &ZoneRange, c: &PointCloud) {
295 let path = self.full.join(kind).join(zone_file(kind, r));
296 let res = path
297 .parent()
298 .map_or(Ok(()), std::fs::create_dir_all)
299 .map_err(|e| e.to_string())
300 .and_then(|_| write_ply(&path, c, PlyLayout::XyzNormalRgb).map_err(|e| e.to_string()));
301 if let (Err(e), Some(l)) = (res, &self.log) {
302 l.line(&format!("[dense] {kind} {} PLY 쓰기 실패: {e}", r.zone));
303 }
304 }
305}
306
307impl Sink for ZonePlySink {
308 fn kinds(&self) -> Vec<EventKind> {
309 vec![EventKind::ZonePreview, EventKind::ZoneRefined]
310 }
311 fn handle(&mut self, e: &Event) {
312 match e {
313 Event::ZonePreview { range, cloud, dense: true, .. } => self.write("preview", range, cloud),
314 Event::ZoneRefined { range, cloud, dense: true, .. } => self.write("refined", range, cloud),
315 _ => {}
316 }
317 }
318}
319
320pub struct ModelSink {
326 dir: PathBuf,
327 log: Option<RunLog>,
328}
329
330impl ModelSink {
331 pub fn new(dir: PathBuf, log: Option<RunLog>) -> Self {
333 Self { dir, log }
334 }
335
336 fn save(&self, rec: &Reconstruction, name: &str) {
337 let d = self.dir.join(name);
338 let _ = std::fs::create_dir_all(&d);
339 if let (Err(e), Some(l)) = (write_model_binary(rec, &d, ImageOrder::Registration), &self.log) {
340 l.line(&format!("모델 저장 실패 {}: {e}", d.display()));
341 }
342 }
343}
344
345impl Sink for ModelSink {
346 fn kinds(&self) -> Vec<EventKind> {
347 vec![EventKind::ZoneArrived, EventKind::ZonePreview, EventKind::ZoneAdjusted, EventKind::ZoneRefinedPose, EventKind::AllRefinedDone]
348 }
349 fn handle(&mut self, e: &Event) {
350 match e {
351 Event::ZoneArrived { range, model, .. } => self.save(model, &format!("snap_{}", range.zone)),
352 Event::ZonePreview { range, model, .. } => self.save(model, &format!("preview_{}", range.zone)),
353 Event::ZoneAdjusted { zone, model, .. } => self.save(model, &format!("ba_{zone}")),
354 Event::ZoneRefinedPose { range, model, .. } => self.save(model, &format!("refined_{}", range.zone)),
355 Event::AllRefinedDone { chain: Some(c), .. } => self.save(c, "chain"),
356 _ => {}
357 }
358 }
359}
360
361pub struct SnapshotSink {
371 out: PathBuf,
372 log: RunLog,
373 t0: Option<SystemTime>,
374 ranges: BTreeMap<usize, ZoneRange>,
375 times: BTreeMap<usize, Stamps>,
376 preview: BTreeMap<usize, Arc<PointCloud>>,
377 refined: BTreeMap<usize, Arc<PointCloud>>,
378 preview_models: BTreeMap<usize, Arc<Reconstruction>>,
379 refined_models: BTreeMap<usize, Arc<Reconstruction>>,
380 positions: HashMap<String, usize>,
381 live: post::LiveDisplay,
382 live_count: usize,
383 diff: Option<DiffStream>,
385 last_display: Option<(PointCloud, Vec<usize>)>,
386 beat: Option<Heartbeat>,
388 files: bool,
391}
392
393#[derive(Clone, Copy, Default)]
395struct Stamps {
396 arrived: Option<SystemTime>,
397 preview: Option<SystemTime>,
398 pose: Option<SystemTime>,
399 refined: Option<SystemTime>,
400}
401
402impl SnapshotSink {
403 pub fn new(out: PathBuf, log: RunLog) -> Self {
405 Self {
406 out,
407 log,
408 t0: None,
409 ranges: BTreeMap::new(),
410 times: BTreeMap::new(),
411 preview: BTreeMap::new(),
412 refined: BTreeMap::new(),
413 preview_models: BTreeMap::new(),
414 refined_models: BTreeMap::new(),
415 positions: HashMap::new(),
416 live: post::LiveDisplay::default(),
417 live_count: 0,
418 diff: None,
419 last_display: None,
420 beat: None,
421 files: true,
422 }
423 }
424
425 pub fn with_files(mut self, on: bool) -> Self {
427 self.files = on;
428 self
429 }
430
431 fn beat(&mut self, at: SystemTime, f: impl FnOnce(&mut Heartbeat)) {
433 let Some(b) = self.beat.as_mut() else { return };
434 f(b);
435 let t = epoch_secs(at) - epoch_secs(self.t0.unwrap_or(at));
436 if let Err(e) = b.write((t * 10.0).round() / 10.0) {
437 self.log.line(&format!("[live] status.json 실패: {e}"));
438 }
439 }
440
441 pub fn with_live_stride(mut self, stride: usize) -> Self {
443 self.live = post::LiveDisplay::new(post::LiveOptions { stride, ..post::LiveOptions::default() });
444 self
445 }
446
447 pub fn with_live_stream(mut self, cfg: LiveStream) -> Self {
449 let dir = self.out.join("live").join("stream");
450 self.beat = Some(Heartbeat { dir: dir.clone(), stage: "registration", ..Default::default() });
451 self.diff = Some(DiffStream::new(dir, cfg));
452 self
453 }
454
455 fn drop_zone(&mut self, k: usize) {
456 self.ranges.remove(&k);
457 self.times.remove(&k);
458 self.preview.remove(&k);
459 self.refined.remove(&k);
460 self.preview_models.remove(&k);
461 self.refined_models.remove(&k);
462 self.live.forget(k);
463 }
464
465 fn write_live(&mut self, k: usize, at: SystemTime, kind: &str) {
468 if !self.files && self.diff.is_none() {
469 return;
470 }
471 let t = Instant::now();
472 let clouds: BTreeMap<usize, &PointCloud> = self.refined.iter().map(|(j, c)| (*j, &**c)).collect();
473 let models: BTreeMap<usize, &Reconstruction> = self.refined_models.iter().map(|(j, m)| (*j, &**m)).collect();
474 self.live.set_relocate(!self.preview_models.is_empty());
477 let (cloud, zones) = self.live.compose_zoned(&clouds, &models);
478 let dir = self.out.join("live");
479 let rel = epoch_secs(at) - epoch_secs(self.t0.unwrap_or(at));
480 self.live_count += 1;
481 if !self.files {
482 self.write_stream(k, rel, kind, cloud, zones);
483 return;
484 }
485 let name = format!("display_{:02}_{rel:06.1}s_refined_{k}.ply", self.live_count);
486 let tmp = dir.join(".latest.ply.tmp");
487 let r = std::fs::create_dir_all(&dir)
488 .map_err(|e| e.to_string())
489 .and_then(|_| write_ply(dir.join(&name), &cloud, PlyLayout::XyzRgbNormal).map_err(|e| e.to_string()))
490 .and_then(|_| write_ply(&tmp, &cloud, PlyLayout::XyzRgbNormal).map_err(|e| e.to_string()))
491 .and_then(|_| std::fs::rename(&tmp, dir.join("latest.ply")).map_err(|e| e.to_string()));
492 self.log.add_time("live", t.elapsed());
493 match r {
494 Ok(()) => {
495 self.log.line(&format!("[live] {name}: 구역 {} 개, 점 {}, {:.2}s", clouds.len(), cloud.len(), t.elapsed().as_secs_f64()))
496 }
497 Err(e) => self.log.line(&format!("[live] {name} 실패: {e}")),
498 }
499 if self.diff.is_some() {
500 self.write_stream(k, rel, kind, cloud, zones);
501 }
502 }
503
504 fn write_stream(&mut self, zone: usize, t_rel: f64, kind: &str, cloud: PointCloud, zones: Vec<usize>) {
506 let t = Instant::now();
507 let Some(bs) = self.diff.as_mut() else { return };
508 let r = bs.write_event(t_rel, zone, kind, &cloud, &zones);
509 self.log.add_time("live_stream", t.elapsed());
510 match r {
511 Ok(st) => self.log.line(&format!(
512 "[live] stream seq {}: 추가 {} 이동 {} 제거 {}, 점 {}, {:.2} MB, {:.2}s",
513 bs.events_written(),
514 st.n_add,
515 st.n_move,
516 st.n_remove,
517 st.points,
518 st.bytes as f64 / 1e6,
519 t.elapsed().as_secs_f64()
520 )),
521 Err(e) => self.log.line(&format!("[live] stream {kind} 구역 {zone} 실패: {e}")),
522 }
523 self.last_display = Some((cloud, zones));
524 }
525
526 pub fn finish_with(&mut self, final_model: Option<&Reconstruction>) {
529 if !self.files {
530 return;
531 }
532 if self.preview_models.is_empty() && !self.refined.is_empty() {
533 let t0 = epoch_secs(self.t0.unwrap_or(UNIX_EPOCH));
534 let rel = |t: Option<SystemTime>| t.map(|t| epoch_secs(t) - t0);
535 let times: BTreeMap<usize, ZoneTimes> = self
536 .times
537 .iter()
538 .map(|(k, s)| {
539 (
540 *k,
541 ZoneTimes {
542 arrived: rel(s.arrived),
543 preview_ready: None,
544 refined_pose: rel(s.pose),
545 refined_ready: rel(s.refined),
546 },
547 )
548 })
549 .collect();
550 let files: BTreeMap<usize, String> =
551 self.refined.keys().filter_map(|k| self.ranges.get(k).map(|r| (*k, zone_file("refined", r)))).collect();
552 let rf = self.refined.iter().filter_map(|(k, c)| files.get(k).map(|f| (*k, ZoneCloud { file: f.clone(), cloud: c }))).collect();
553 let display = self.refined_models.values().next_back().map(|m| &**m);
554 let t = Instant::now();
555 let models: BTreeMap<usize, &Reconstruction> = self.refined_models.iter().map(|(k, m)| (*k, &**m)).collect();
556 let r = post::run_stable(&self.out, &rf, &models, ×, display, final_model, self.log.logger());
557 self.log.add_time("post", t.elapsed());
558 if let Err(e) = r {
559 self.log.line(&format!("[post] 실패: {e}"));
560 }
561 let _ = std::fs::copy(self.out.join("timeline.txt"), self.out.join("snapshots").join("timeline.txt"));
562 return;
563 }
564 self.finish();
565 }
566
567 pub fn finish(&mut self) {
570 if !self.files {
571 return;
572 }
573 let t0 = epoch_secs(self.t0.unwrap_or(UNIX_EPOCH));
574 let rel = |t: Option<SystemTime>| t.map(|t| epoch_secs(t) - t0);
575 let times: BTreeMap<usize, ZoneTimes> = self
576 .times
577 .iter()
578 .map(|(k, s)| {
579 (
580 *k,
581 ZoneTimes {
582 arrived: rel(s.arrived),
583 preview_ready: rel(s.preview),
584 refined_pose: rel(s.pose),
585 refined_ready: rel(s.refined),
586 },
587 )
588 })
589 .collect();
590 let windows = self
591 .ranges
592 .iter()
593 .map(|(k, r)| {
594 let hi = k.checked_sub(1).and_then(|j| self.ranges.get(&j)).map_or(r.hi, |p| p.hi);
595 (*k, (r.lo, hi))
596 })
597 .collect();
598 let zc = |kind: &str, m: &BTreeMap<usize, Arc<PointCloud>>| -> BTreeMap<usize, (String, Arc<PointCloud>)> {
599 m.iter().filter_map(|(k, c)| self.ranges.get(k).map(|r| (*k, (zone_file(kind, r), Arc::clone(c))))).collect()
600 };
601 let (pv, rf) = (zc("preview", &self.preview), zc("refined", &self.refined));
602 let positions = &self.positions;
603 let pos = move |n: &str| positions.get(n).copied();
604 let z = PostZones {
605 out: &self.out,
606 preview: pv.iter().map(|(k, (f, c))| (*k, ZoneCloud { file: f.clone(), cloud: c })).collect(),
607 refined: rf.iter().map(|(k, (f, c))| (*k, ZoneCloud { file: f.clone(), cloud: c })).collect(),
608 preview_models: self.preview_models.iter().map(|(k, m)| (*k, &**m)).collect(),
609 refined_models: self.refined_models.iter().map(|(k, m)| (*k, &**m)).collect(),
610 times,
611 windows,
612 position: &pos,
613 };
614 let t = Instant::now();
615 let r = post::run_zones(&z, self.log.logger());
616 self.log.add_time("post", t.elapsed());
617 if let Err(e) = r {
618 self.log.line(&format!("[post] 실패: {e}"));
619 }
620 let _ = std::fs::copy(self.out.join("timeline.txt"), self.out.join("snapshots").join("timeline.txt"));
621 }
622}
623
624impl Sink for SnapshotSink {
625 fn kinds(&self) -> Vec<EventKind> {
626 vec![
627 EventKind::Started,
628 EventKind::FrameIngested,
629 EventKind::FeaturesExtracted,
630 EventKind::ZoneArrived,
631 EventKind::ZonePreview,
632 EventKind::PositionDone,
633 EventKind::RefineStarted,
634 EventKind::ZoneRefinedPose,
635 EventKind::ZoneRefined,
636 EventKind::Reset,
637 EventKind::ZoneInvalidated,
638 EventKind::AllRefinedDone,
639 ]
640 }
641
642 fn handle(&mut self, e: &Event) {
643 let at = e.meta().at;
644 if matches!(e, Event::Started { .. }) || self.t0.is_none() {
645 self.t0 = Some(at);
646 }
647 match e {
648 Event::Started { positions, .. } => self.beat(at, |b| b.positions_total = *positions),
649 Event::PositionDone { position, registered, .. } => {
650 let (p, r) = (*position + 1, *registered);
651 self.beat(at, |b| {
652 b.stage = "triangulation";
653 b.positions = p;
654 b.images = r;
655 });
656 }
657 Event::RefineStarted { zone, .. } => self.beat(at, |b| {
658 b.stage = "ba";
659 b.zone = *zone;
660 }),
661 Event::FrameIngested { position, images, .. } => {
662 for n in images {
663 self.positions.insert(n.clone(), *position);
664 }
665 }
666 Event::FeaturesExtracted { image, position, .. } => {
667 self.positions.insert(image.clone(), *position);
668 }
669 Event::ZoneArrived { range, .. } => {
670 self.ranges.insert(range.zone, *range);
671 self.times.entry(range.zone).or_default().arrived.get_or_insert(at);
672 }
673 Event::ZonePreview { range, cloud, model, dense, .. } => {
674 self.ranges.entry(range.zone).or_insert(*range);
675 self.times.entry(range.zone).or_default().preview.get_or_insert(at);
676 self.preview_models.insert(range.zone, Arc::clone(model));
677 if *dense {
678 self.preview.insert(range.zone, Arc::clone(cloud));
679 }
680 }
681 Event::ZoneRefinedPose { range, model, .. } => {
682 self.beat(at, |b| {
683 b.stage = "dense";
684 b.zone = range.zone;
685 });
686 self.ranges.entry(range.zone).or_insert(*range);
687 self.times.entry(range.zone).or_default().pose.get_or_insert(at);
688 self.refined_models.insert(range.zone, Arc::clone(model));
689 self.live.forget(range.zone);
690 }
691 Event::ZoneRefined { range, cloud, dense, .. } => {
692 self.ranges.entry(range.zone).or_insert(*range);
693 self.times.entry(range.zone).or_default().refined.get_or_insert(at);
694 if *dense {
695 let kind = if self.refined.contains_key(&range.zone) { "replace" } else { "refined" };
697 self.refined.insert(range.zone, Arc::clone(cloud));
698 self.write_live(range.zone, at, kind);
699 }
700 }
701 Event::Reset { position, .. } => {
702 let ks: Vec<usize> = self.ranges.values().filter(|r| r.hi > *position).map(|r| r.zone).collect();
703 for k in ks {
704 self.drop_zone(k);
705 }
706 }
707 Event::ZoneInvalidated { zone, .. } => self.drop_zone(*zone),
708 Event::AllRefinedDone { chain, .. } => {
709 self.beat(at, |b| b.stage = "done");
710 if let Some((cloud, zones)) = self.last_display.take() {
712 let rel = epoch_secs(at) - epoch_secs(self.t0.unwrap_or(at));
713 let zone = self.refined.keys().next_back().copied().unwrap_or(0);
714 self.write_stream(zone, rel, "final", cloud, zones);
715 }
716 self.finish_with(chain.as_deref())
717 }
718 _ => {}
719 }
720 }
721}
722
723#[derive(Clone, Debug, Default)]
727pub struct SinkOptions {
728 pub echo: bool,
730 pub save_models: bool,
732}
733
734#[derive(Clone, Copy, Debug, PartialEq)]
736pub struct SinkSet {
737 pub timeline: bool,
739 pub run_log: bool,
741 pub zone_ply: bool,
743 pub models: bool,
745 pub snapshots: bool,
747 pub live_stream: Option<LiveStream>,
749 pub live_stride: usize,
751 pub live_files: bool,
753}
754
755impl SinkSet {
756 pub fn all() -> Self {
758 Self {
759 timeline: true,
760 run_log: true,
761 zone_ply: true,
762 models: true,
763 snapshots: true,
764 live_stream: None,
765 live_stride: post::LiveOptions::default().stride,
766 live_files: true,
767 }
768 }
769}
770
771pub struct DefaultSinks {
773 sinks: Vec<Box<dyn Sink>>,
774 log: RunLog,
775}
776
777impl DefaultSinks {
778 pub fn new(out: &Path, opts: &SinkOptions) -> std::io::Result<Self> {
781 Self::with_set(out, opts.echo, SinkSet { models: opts.save_models, ..SinkSet::all() })
782 }
783
784 pub fn with_set(out: &Path, echo: bool, set: SinkSet) -> std::io::Result<Self> {
788 let dirs: &[&str] = if set.zone_ply { &["full/preview", "full/refined", "work"] } else { &["work"] };
789 for d in dirs {
790 std::fs::create_dir_all(out.join(d))?;
791 }
792 let run_log = out.join("run.log");
793 let log = RunLog::open(set.run_log.then_some(run_log.as_path()), echo)?;
794 let mut sinks: Vec<Box<dyn Sink>> = Vec::new();
795 if set.timeline {
796 sinks.push(Box::new(TimelineSink::create(&out.join("timeline.txt"))?));
797 }
798 sinks.push(Box::new(RunLogSink::new(log.clone(), set.run_log.then(|| out.join("DONE")))));
799 if set.zone_ply {
800 sinks.push(Box::new(ZonePlySink::new(out.join("full"), Some(log.clone()))));
801 }
802 if set.models {
803 sinks.push(Box::new(ModelSink::new(out.join("work").join("models"), Some(log.clone()))));
804 }
805 if set.snapshots {
806 let mut snap = SnapshotSink::new(out.to_path_buf(), log.clone()).with_live_stride(set.live_stride).with_files(set.live_files);
807 if let Some(cfg) = set.live_stream {
808 snap = snap.with_live_stream(cfg);
809 }
810 sinks.push(Box::new(snap));
811 }
812 Ok(Self { sinks, log })
813 }
814
815 pub fn run_log(&self) -> RunLog {
817 self.log.clone()
818 }
819
820 pub fn into_hook(self) -> Hook {
822 any_hook(self)
823 }
824}
825
826impl Sink for DefaultSinks {
827 fn kinds(&self) -> Vec<EventKind> {
828 let mut v: Vec<EventKind> = Vec::new();
829 for s in &self.sinks {
830 for k in s.kinds() {
831 if !v.contains(&k) {
832 v.push(k);
833 }
834 }
835 }
836 v
837 }
838 fn handle(&mut self, e: &Event) {
839 let k = e.kind();
840 for s in &mut self.sinks {
841 if s.kinds().contains(&k) {
842 s.handle(e);
843 }
844 }
845 }
846}
847
848pub fn default_sinks(out: &Path, opts: &SinkOptions) -> std::io::Result<Vec<(EventKind, Hook)>> {
850 Ok(hooks(DefaultSinks::new(out, opts)?))
851}
852
853pub fn attach<R: Reducer>(p: Pipeline<R>, out: &Path, opts: &SinkOptions) -> std::io::Result<Pipeline<R>> {
855 Ok(p.on_any(DefaultSinks::new(out, opts)?.into_hook()))
856}
857
858#[cfg(test)]
859mod tests {
860 use super::*;
861 use crate::events::Meta;
862
863 fn meta(s: u64, n: u32) -> Meta {
864 Meta { version: 0, at: UNIX_EPOCH + Duration::new(s, n) }
865 }
866 fn r(zone: usize, lo: usize, hi: usize) -> ZoneRange {
867 ZoneRange { zone, lo, hi }
868 }
869
870 #[test]
871 fn heartbeat_status_json_follows_events() {
872 let dir = std::env::temp_dir().join(format!("c3d_status_{}", std::process::id()));
873 let _ = std::fs::remove_dir_all(&dir);
874 let log = RunLog::open(None, false).unwrap();
875 let cfg = LiveStream { enu: false, origin_gps: None };
876 let mut s = SnapshotSink::new(dir.clone(), log).with_live_stream(cfg);
877 let read = || crate::util::Json::parse(&std::fs::read_to_string(dir.join("live/stream/status.json")).unwrap()).unwrap();
878 s.handle(&Event::Started { meta: meta(100, 0), positions: Some(26) });
879 s.handle(&Event::PositionDone { meta: meta(102, 500_000_000), position: 14, registered: 44, expected: 45 });
880 let j = read();
881 assert_eq!(
882 j.dump(),
883 crate::util::Json::obj(vec![
884 ("t", crate::util::Json::Num("2.5".into())),
885 ("stage", crate::util::Json::Str("triangulation".into())),
886 ("zone", crate::util::Json::Int(0)),
887 ("positions", crate::util::Json::Int(15)),
888 ("positions_total", crate::util::Json::Int(26)),
889 ("images", crate::util::Json::Int(44)),
890 ])
891 .dump()
892 );
893 s.handle(&Event::RefineStarted { meta: meta(103, 0), zone: 1 });
894 let j = read();
895 assert_eq!(j.get("stage").map(|v| v.dump()), Some("\"ba\"".into()));
896 assert_eq!(j.get("zone").map(|v| v.dump()), Some("1".into()));
897 assert!(!dir.join("live/stream/status.json.tmp").exists());
898 let _ = std::fs::remove_dir_all(&dir);
899 }
900
901 #[test]
902 fn timeline_table() {
903 let m = || meta(1_700_000_000, 500);
905 let model = Arc::new(Reconstruction::new());
906 let cloud = Arc::new(PointCloud::default());
907 let cases: Vec<(Event, &str)> = vec![
908 (Event::Started { meta: m(), positions: Some(26) }, "start NPOS=26"),
909 (Event::ModelInitialized { meta: m(), registered: 42, position: 13, model: model.clone() }, "init_model Registered images: 42"),
910 (Event::BaseAdopted { meta: m(), zone: 0 }, "adopt refined 0 as base"),
911 (Event::PositionDone { meta: m(), position: 14, registered: 44, expected: 45 }, "pos 14 registered 44/45"),
912 (Event::ZoneArrived { meta: m(), range: r(1, 10, 26), model: model.clone() }, "region 1 arrived pos[10,26)"),
913 (
914 Event::ZonePreview {
915 meta: m(),
916 range: r(1, 10, 26),
917 cloud: cloud.clone(),
918 model: model.clone(),
919 dense: true,
920 frame: "gps".into(),
921 },
922 "preview 1 ready",
923 ),
924 (Event::RefineStarted { meta: m(), zone: 1 }, "refined 1 ba_start"),
925 (
926 Event::ZoneRefinedPose { meta: m(), range: r(1, 10, 26), registered: 48, mean_reproj_px: 0.25095, model: model.clone() },
927 "refined 1 pose_ready Registered images: 48 Mean reprojection error: 0.250950px ",
928 ),
929 (Event::ZoneRefined { meta: m(), range: r(1, 10, 26), cloud, model, dense: true }, "refined 1 ready"),
930 (Event::AllPositionsDone { meta: m() }, "all positions done"),
931 (Event::AllRefinedDone { meta: m(), chain: None }, "all refined done"),
932 ];
933 assert_eq!(cases.len(), TIMELINE_TABLE.len());
934 for ((e, want), (k, _)) in cases.iter().zip(TIMELINE_TABLE.iter()) {
935 assert_eq!(e.kind(), *k);
936 assert_eq!(TimelineSink::line(e).unwrap(), format!("1700000000.000000500 {want}"));
937 }
938 assert!(TimelineSink::line(&Event::Warning { meta: m(), message: "x".into() }).is_none());
939 }
940}