Skip to main content

cumulus3d_cli/
events.rs

1/*
2 * events.rs
3 *
4 * Copyright (c) 2026 ParkSangWoo
5 *
6 * Author(s):
7 *
8 *      ParkSangWoo <dev.parksangwoo@gmail.com>
9 *
10 *
11 * This file is part of cumulus3d.
12 *
13 * Licensed under the Apache License, Version 2.0 (the "License");
14 * you may not use this file except in compliance with the License.
15 * You may obtain a copy of the License at
16 *
17 *      http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS,
21 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 * See the License for the specific language governing permissions and
23 * limitations under the License.
24 *
25 * SPDX-License-Identifier: Apache-2.0
26 */
27
28//! Pipeline events. Each time a new frame bundle is processed, the result decomposed by stage is emitted.
29//! This file is the shared contract of three modules (session, pipeline, sinks). Adding fields is allowed; changing or removing existing
30//! fields is forbidden.
31
32use cumulus3d_core::io::ply::PointCloud;
33use cumulus3d_core::{ImageId, Reconstruction, Rigid3, Sim3};
34use std::sync::Arc;
35use std::time::{Duration, SystemTime};
36
37/// Header attached to every event.
38#[derive(Clone, Debug)]
39pub struct Meta {
40    /// Session version (a monotonically increasing number of the state that emitted the event).
41    pub version: u64,
42    /// Time of occurrence.
43    pub at: SystemTime,
44}
45
46/// Zone index and position range [lo, hi).
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct ZoneRange {
49    /// Zone index.
50    pub zone: usize,
51    /// First position (inclusive).
52    pub lo: usize,
53    /// End position (exclusive).
54    pub hi: usize,
55}
56
57/// A decoded input frame (one camera image). It is delivered first as [`Event::FrameDecoded`], before processing (features, matching,
58/// registration).
59#[derive(Clone, Debug)]
60pub struct Frame {
61    /// Position index.
62    pub position: usize,
63    /// Camera name (e.g. `camF`).
64    pub camera: String,
65    /// Image name (path relative to `image_root`).
66    pub name: String,
67    /// Image file path.
68    pub path: std::path::PathBuf,
69    /// Width in pixels.
70    pub width: u32,
71    /// Height in pixels.
72    pub height: u32,
73    /// RGB8 pixels (row-major, length `width * height * 3`). Shared between hooks without copying.
74    pub rgb: Arc<[u8]>,
75    /// GPS record of this image (if any).
76    pub gps: Option<cumulus3d_core::io::GpsRecord>,
77}
78
79/// Pipeline events (per-stage results). Every variant carries a [`Meta`].
80#[derive(Clone, Debug)]
81pub enum Event {
82    /// The frame bundle of one position has arrived.
83    FrameIngested {
84        /// Common header.
85        meta: Meta,
86        /// Position index.
87        position: usize,
88        /// Image names of this position.
89        images: Vec<String>,
90    },
91    /// Decoding of one input frame is complete. Comes first, before the position is processed (only when `SessionConfig::decode_frames`
92    /// is on).
93    FrameDecoded {
94        /// Common header.
95        meta: Meta,
96        /// Decoded frame.
97        frame: Arc<Frame>,
98    },
99    /// Session start (once, at the first input). `positions` = total number of known positions. (added by session)
100    Started {
101        /// Common header.
102        meta: Meta,
103        /// Total number of known positions (None if unknown).
104        positions: Option<usize>,
105    },
106    /// One run.log line. If `stamped`, it is written with a `[HH:MM:SS] ` prefix (meta.at local time). (added by session)
107    Log {
108        /// Common header.
109        meta: Meta,
110        /// Line to record.
111        line: String,
112        /// Whether to add the `[HH:MM:SS] ` prefix.
113        stamped: bool,
114    },
115    /// Feature extraction of one image is complete.
116    FeaturesExtracted {
117        /// Common header.
118        meta: Meta,
119        /// Image name.
120        image: String,
121        /// Number of extracted features.
122        count: usize,
123        /// Position index.
124        position: usize,
125        /// Image id.
126        image_id: ImageId,
127    },
128    /// Result of matching new pairs.
129    /// `pairs` = pairs matched at this position, `verified` = pairs with valid geometry, `matched` = pairs for which descriptor matching
130    /// was performed.
131    PairsMatched {
132        /// Common header.
133        meta: Meta,
134        /// Pairs matched at this position.
135        pairs: usize,
136        /// Pairs with valid geometry.
137        verified: usize,
138        /// Position index.
139        position: usize,
140        /// Pairs for which descriptor matching was performed.
141        matched: usize,
142    },
143    /// The first model has been built.
144    ModelInitialized {
145        /// Common header.
146        meta: Meta,
147        /// Number of registered images.
148        registered: usize,
149        /// Position index.
150        position: usize,
151        /// Copy of the relevant model.
152        model: Arc<Reconstruction>,
153    },
154    /// An image has been registered to the model.
155    FrameRegistered {
156        /// Common header.
157        meta: Meta,
158        /// Position index.
159        position: usize,
160        /// Image name.
161        image: String,
162        /// Image id.
163        image_id: ImageId,
164        /// Registered pose (world -> camera).
165        cam_from_world: Rigid3,
166    },
167    /// Registration and triangulation of one position are finished.
168    PositionDone {
169        /// Common header.
170        meta: Meta,
171        /// Position index.
172        position: usize,
173        /// Number of registered images.
174        registered: usize,
175        /// Number of images expected up to this position.
176        expected: usize,
177    },
178    /// The last position of a zone has arrived.
179    /// `model` = copy of the chain model at that point (before alignment).
180    ZoneArrived {
181        /// Common header.
182        meta: Meta,
183        /// Zone range.
184        range: ZoneRange,
185        /// Copy of the relevant model.
186        model: Arc<Reconstruction>,
187    },
188    /// Zone preview point cloud (no BA).
189    /// `model` = preview model (zone 0 is the GPS-aligned one), `dense` = false means densification was not run or failed and `cloud` is
190    /// an empty placeholder.
191    /// `frame` = point-cloud coordinate frame ("gps" | "refined_K" | "unaligned").
192    ZonePreview {
193        /// Common header.
194        meta: Meta,
195        /// Zone range.
196        range: ZoneRange,
197        /// Point cloud (an empty placeholder if there is no densification).
198        cloud: Arc<PointCloud>,
199        /// Copy of the relevant model.
200        model: Arc<Reconstruction>,
201        /// Whether densification was actually performed.
202        dense: bool,
203        /// Point-cloud coordinate frame ("gps" | "refined_K" | "unaligned").
204        frame: String,
205    },
206    /// Refinement job start (background). (added by session)
207    RefineStarted {
208        /// Common header.
209        meta: Meta,
210        /// Zone index.
211        zone: usize,
212    },
213    /// Model right after BA of the refinement job (before GPS alignment). (added by session)
214    ZoneAdjusted {
215        /// Common header.
216        meta: Meta,
217        /// Zone index.
218        zone: usize,
219        /// Copy of the relevant model.
220        model: Arc<Reconstruction>,
221    },
222    /// Zone refined pose finalized (BA + GPS alignment).
223    /// `model` = refined model after BA + GPS alignment.
224    ZoneRefinedPose {
225        /// Common header.
226        meta: Meta,
227        /// Zone range.
228        range: ZoneRange,
229        /// Number of registered images.
230        registered: usize,
231        /// Mean reprojection error (pixels).
232        mean_reproj_px: f64,
233        /// Copy of the relevant model.
234        model: Arc<Reconstruction>,
235    },
236    /// Zone refined point cloud. It replaces the preview of the same zone.
237    /// `dense` = false means `cloud` is an empty placeholder.
238    ZoneRefined {
239        /// Common header.
240        meta: Meta,
241        /// Zone range.
242        range: ZoneRange,
243        /// Point cloud (an empty placeholder if there is no densification).
244        cloud: Arc<PointCloud>,
245        /// Copy of the relevant model.
246        model: Arc<Reconstruction>,
247        /// Whether densification was actually performed.
248        dense: bool,
249    },
250    /// The refined model was adopted as the reference for subsequent registration.
251    BaseAdopted {
252        /// Common header.
253        meta: Meta,
254        /// Zone index.
255        zone: usize,
256    },
257    /// Coordinate-frame transform of an already sent zone (the point cloud is not sent again): new_from_old.
258    Reanchored {
259        /// Common header.
260        meta: Meta,
261        /// Zone index.
262        zone: usize,
263        /// new_from_old transform.
264        transform: Sim3,
265    },
266    /// Snapshot for display (event sequence number and composite point cloud).
267    Snapshot {
268        /// Common header.
269        meta: Meta,
270        /// Event sequence number.
271        index: usize,
272        /// Point cloud (an empty placeholder if there is no densification).
273        cloud: Arc<PointCloud>,
274    },
275    /// All positions processed.
276    AllPositionsDone {
277        /// Common header.
278        meta: Meta,
279    },
280    /// All refinement jobs complete.
281    /// `chain` = final chain model.
282    AllRefinedDone {
283        /// Common header.
284        meta: Meta,
285        /// Final chain model.
286        chain: Option<Arc<Reconstruction>>,
287    },
288    /// Session end summary (the last of all events). `stage_times` = (stage, accumulated time, count) in stage-name order,
289    /// `pipeline_secs` = start to all refinements complete, `chain_stats` = statistics line of the final chain model. (added by session)
290    Finished {
291        /// Common header.
292        meta: Meta,
293        /// Start to all refinements complete (seconds).
294        pipeline_secs: f64,
295        /// (stage, accumulated time, count) in stage-name order.
296        stage_times: Vec<(String, Duration, usize)>,
297        /// Statistics line of the final chain model.
298        chain_stats: Vec<String>,
299    },
300    /// `Command::ResetFrom` was handled. (added by session)
301    Reset {
302        /// Common header.
303        meta: Meta,
304        /// Position index.
305        position: usize,
306    },
307    /// `Command::InvalidateZone` was handled. (added by session)
308    ZoneInvalidated {
309        /// Common header.
310        meta: Meta,
311        /// Zone index.
312        zone: usize,
313    },
314    /// Warning.
315    Warning {
316        /// Common header.
317        meta: Meta,
318        /// Message.
319        message: String,
320    },
321    /// Error (including hook panics).
322    Error {
323        /// Common header.
324        meta: Meta,
325        /// Message.
326        message: String,
327    },
328}
329
330/// Event kind (for hook registration and filtering).
331#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
332pub enum EventKind {
333    /// [`Event::FrameIngested`].
334    FrameIngested,
335    /// [`Event::FrameDecoded`].
336    FrameDecoded,
337    /// [`Event::Started`].
338    Started,
339    /// [`Event::Log`].
340    Log,
341    /// [`Event::RefineStarted`].
342    RefineStarted,
343    /// [`Event::ZoneAdjusted`].
344    ZoneAdjusted,
345    /// [`Event::Finished`].
346    Finished,
347    /// [`Event::Reset`].
348    Reset,
349    /// [`Event::ZoneInvalidated`].
350    ZoneInvalidated,
351    /// [`Event::FeaturesExtracted`].
352    FeaturesExtracted,
353    /// [`Event::PairsMatched`].
354    PairsMatched,
355    /// [`Event::ModelInitialized`].
356    ModelInitialized,
357    /// [`Event::FrameRegistered`].
358    FrameRegistered,
359    /// [`Event::PositionDone`].
360    PositionDone,
361    /// [`Event::ZoneArrived`].
362    ZoneArrived,
363    /// [`Event::ZonePreview`].
364    ZonePreview,
365    /// [`Event::ZoneRefinedPose`].
366    ZoneRefinedPose,
367    /// [`Event::ZoneRefined`].
368    ZoneRefined,
369    /// [`Event::BaseAdopted`].
370    BaseAdopted,
371    /// [`Event::Reanchored`].
372    Reanchored,
373    /// [`Event::Snapshot`].
374    Snapshot,
375    /// [`Event::AllPositionsDone`].
376    AllPositionsDone,
377    /// [`Event::AllRefinedDone`].
378    AllRefinedDone,
379    /// [`Event::Warning`].
380    Warning,
381    /// [`Event::Error`].
382    Error,
383}
384
385impl Event {
386    /// Event kind.
387    pub fn kind(&self) -> EventKind {
388        use Event::*;
389        match self {
390            FrameIngested { .. } => EventKind::FrameIngested,
391            FrameDecoded { .. } => EventKind::FrameDecoded,
392            Started { .. } => EventKind::Started,
393            Log { .. } => EventKind::Log,
394            RefineStarted { .. } => EventKind::RefineStarted,
395            ZoneAdjusted { .. } => EventKind::ZoneAdjusted,
396            Finished { .. } => EventKind::Finished,
397            Reset { .. } => EventKind::Reset,
398            ZoneInvalidated { .. } => EventKind::ZoneInvalidated,
399            FeaturesExtracted { .. } => EventKind::FeaturesExtracted,
400            PairsMatched { .. } => EventKind::PairsMatched,
401            ModelInitialized { .. } => EventKind::ModelInitialized,
402            FrameRegistered { .. } => EventKind::FrameRegistered,
403            PositionDone { .. } => EventKind::PositionDone,
404            ZoneArrived { .. } => EventKind::ZoneArrived,
405            ZonePreview { .. } => EventKind::ZonePreview,
406            ZoneRefinedPose { .. } => EventKind::ZoneRefinedPose,
407            ZoneRefined { .. } => EventKind::ZoneRefined,
408            BaseAdopted { .. } => EventKind::BaseAdopted,
409            Reanchored { .. } => EventKind::Reanchored,
410            Snapshot { .. } => EventKind::Snapshot,
411            AllPositionsDone { .. } => EventKind::AllPositionsDone,
412            AllRefinedDone { .. } => EventKind::AllRefinedDone,
413            Warning { .. } => EventKind::Warning,
414            Error { .. } => EventKind::Error,
415        }
416    }
417    /// Common header.
418    pub fn meta(&self) -> &Meta {
419        use Event::*;
420        match self {
421            FrameIngested { meta, .. }
422            | FeaturesExtracted { meta, .. }
423            | PairsMatched { meta, .. }
424            | ModelInitialized { meta, .. }
425            | FrameRegistered { meta, .. }
426            | PositionDone { meta, .. }
427            | ZoneArrived { meta, .. }
428            | ZonePreview { meta, .. }
429            | ZoneRefinedPose { meta, .. }
430            | ZoneRefined { meta, .. }
431            | BaseAdopted { meta, .. }
432            | Reanchored { meta, .. }
433            | Snapshot { meta, .. }
434            | AllPositionsDone { meta }
435            | AllRefinedDone { meta, .. }
436            | FrameDecoded { meta, .. }
437            | Started { meta, .. }
438            | Log { meta, .. }
439            | RefineStarted { meta, .. }
440            | ZoneAdjusted { meta, .. }
441            | Finished { meta, .. }
442            | Reset { meta, .. }
443            | ZoneInvalidated { meta, .. }
444            | Warning { meta, .. }
445            | Error { meta, .. } => meta,
446        }
447    }
448}
449
450impl Event {
451    /// Event text for timeline.txt (None if there is none). The wording and format are the same as the existing record.
452    pub fn timeline_text(&self) -> Option<String> {
453        use Event::*;
454        Some(match self {
455            Started { positions, .. } => match positions {
456                Some(n) => format!("start NPOS={n}"),
457                None => "start".to_string(),
458            },
459            ModelInitialized { registered, .. } => format!("init_model Registered images: {registered}"),
460            BaseAdopted { zone, .. } => format!("adopt refined {zone} as base"),
461            PositionDone { position, registered, expected, .. } => format!("pos {position} registered {registered}/{expected}"),
462            ZoneArrived { range, .. } => format!("region {} arrived pos[{},{})", range.zone, range.lo, range.hi),
463            ZonePreview { range, .. } => format!("preview {} ready", range.zone),
464            RefineStarted { zone, .. } => format!("refined {zone} ba_start"),
465            ZoneRefinedPose { range, registered, mean_reproj_px, .. } => {
466                format!("refined {} pose_ready Registered images: {registered} Mean reprojection error: {mean_reproj_px:.6}px ", range.zone)
467            }
468            ZoneRefined { range, .. } => format!("refined {} ready", range.zone),
469            AllPositionsDone { .. } => "all positions done".to_string(),
470            AllRefinedDone { .. } => "all refined done".to_string(),
471            _ => return None,
472        })
473    }
474}
475
476/// Commands fed into the reducer.
477#[derive(Clone, Debug)]
478pub enum Command {
479    /// Reprocess from this position (discard later state).
480    ResetFrom(usize),
481    /// Invalidate the zone result and have it rebuilt.
482    InvalidateZone(usize),
483}