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)]
81#[non_exhaustive]
82pub enum Event {
83 /// The frame bundle of one position has arrived.
84 FrameIngested {
85 /// Common header.
86 meta: Meta,
87 /// Position index.
88 position: usize,
89 /// Image names of this position.
90 images: Vec<String>,
91 },
92 /// Decoding of one input frame is complete. Comes first, before the position is processed (only when `SessionConfig::decode_frames`
93 /// is on).
94 FrameDecoded {
95 /// Common header.
96 meta: Meta,
97 /// Decoded frame.
98 frame: Arc<Frame>,
99 },
100 /// Session start (once, at the first input). `positions` = total number of known positions. (added by session)
101 Started {
102 /// Common header.
103 meta: Meta,
104 /// Total number of known positions (None if unknown).
105 positions: Option<usize>,
106 },
107 /// One run.log line. If `stamped`, it is written with a `[HH:MM:SS] ` prefix (meta.at local time). (added by session)
108 Log {
109 /// Common header.
110 meta: Meta,
111 /// Line to record.
112 line: String,
113 /// Whether to add the `[HH:MM:SS] ` prefix.
114 stamped: bool,
115 },
116 /// Feature extraction of one image is complete.
117 FeaturesExtracted {
118 /// Common header.
119 meta: Meta,
120 /// Image name.
121 image: String,
122 /// Number of extracted features.
123 count: usize,
124 /// Position index.
125 position: usize,
126 /// Image id.
127 image_id: ImageId,
128 },
129 /// Result of matching new pairs.
130 /// `pairs` = pairs matched at this position, `verified` = pairs with valid geometry, `matched` = pairs for which descriptor matching
131 /// was performed.
132 PairsMatched {
133 /// Common header.
134 meta: Meta,
135 /// Pairs matched at this position.
136 pairs: usize,
137 /// Pairs with valid geometry.
138 verified: usize,
139 /// Position index.
140 position: usize,
141 /// Pairs for which descriptor matching was performed.
142 matched: usize,
143 },
144 /// The first model has been built.
145 ModelInitialized {
146 /// Common header.
147 meta: Meta,
148 /// Number of registered images.
149 registered: usize,
150 /// Position index.
151 position: usize,
152 /// Copy of the relevant model.
153 model: Arc<Reconstruction>,
154 },
155 /// An image has been registered to the model.
156 FrameRegistered {
157 /// Common header.
158 meta: Meta,
159 /// Position index.
160 position: usize,
161 /// Image name.
162 image: String,
163 /// Image id.
164 image_id: ImageId,
165 /// Registered pose (world -> camera).
166 cam_from_world: Rigid3,
167 },
168 /// Registration and triangulation of one position are finished.
169 PositionDone {
170 /// Common header.
171 meta: Meta,
172 /// Position index.
173 position: usize,
174 /// Number of registered images.
175 registered: usize,
176 /// Number of images expected up to this position.
177 expected: usize,
178 },
179 /// The last position of a zone has arrived.
180 /// `model` = copy of the chain model at that point (before alignment).
181 ZoneArrived {
182 /// Common header.
183 meta: Meta,
184 /// Zone range.
185 range: ZoneRange,
186 /// Copy of the relevant model.
187 model: Arc<Reconstruction>,
188 },
189 /// Zone preview point cloud (no BA).
190 /// `model` = preview model (zone 0 is the GPS-aligned one), `dense` = false means densification was not run or failed and `cloud` is
191 /// an empty placeholder.
192 /// `frame` = point-cloud coordinate frame ("gps" | "refined_K" | "unaligned").
193 ZonePreview {
194 /// Common header.
195 meta: Meta,
196 /// Zone range.
197 range: ZoneRange,
198 /// Point cloud (an empty placeholder if there is no densification).
199 cloud: Arc<PointCloud>,
200 /// Copy of the relevant model.
201 model: Arc<Reconstruction>,
202 /// Whether densification was actually performed.
203 dense: bool,
204 /// Point-cloud coordinate frame ("gps" | "refined_K" | "unaligned").
205 frame: String,
206 },
207 /// Refinement job start (background). (added by session)
208 RefineStarted {
209 /// Common header.
210 meta: Meta,
211 /// Zone index.
212 zone: usize,
213 },
214 /// Model right after BA of the refinement job (before GPS alignment). (added by session)
215 ZoneAdjusted {
216 /// Common header.
217 meta: Meta,
218 /// Zone index.
219 zone: usize,
220 /// Copy of the relevant model.
221 model: Arc<Reconstruction>,
222 },
223 /// Zone refined pose finalized (BA + GPS alignment).
224 /// `model` = refined model after BA + GPS alignment.
225 ZoneRefinedPose {
226 /// Common header.
227 meta: Meta,
228 /// Zone range.
229 range: ZoneRange,
230 /// Number of registered images.
231 registered: usize,
232 /// Mean reprojection error (pixels).
233 mean_reproj_px: f64,
234 /// Copy of the relevant model.
235 model: Arc<Reconstruction>,
236 },
237 /// Zone refined point cloud. It replaces the preview of the same zone.
238 /// `dense` = false means `cloud` is an empty placeholder.
239 ZoneRefined {
240 /// Common header.
241 meta: Meta,
242 /// Zone range.
243 range: ZoneRange,
244 /// Point cloud (an empty placeholder if there is no densification).
245 cloud: Arc<PointCloud>,
246 /// Copy of the relevant model.
247 model: Arc<Reconstruction>,
248 /// Whether densification was actually performed.
249 dense: bool,
250 },
251 /// The refined model was adopted as the reference for subsequent registration.
252 BaseAdopted {
253 /// Common header.
254 meta: Meta,
255 /// Zone index.
256 zone: usize,
257 },
258 /// Coordinate-frame transform of an already sent zone (the point cloud is not sent again): new_from_old.
259 Reanchored {
260 /// Common header.
261 meta: Meta,
262 /// Zone index.
263 zone: usize,
264 /// new_from_old transform.
265 transform: Sim3,
266 },
267 /// Snapshot for display (event sequence number and composite point cloud).
268 Snapshot {
269 /// Common header.
270 meta: Meta,
271 /// Event sequence number.
272 index: usize,
273 /// Point cloud (an empty placeholder if there is no densification).
274 cloud: Arc<PointCloud>,
275 },
276 /// All positions processed.
277 AllPositionsDone {
278 /// Common header.
279 meta: Meta,
280 },
281 /// All refinement jobs complete.
282 /// `chain` = final chain model.
283 AllRefinedDone {
284 /// Common header.
285 meta: Meta,
286 /// Final chain model.
287 chain: Option<Arc<Reconstruction>>,
288 },
289 /// Session end summary (the last of all events). `stage_times` = (stage, accumulated time, count) in stage-name order,
290 /// `pipeline_secs` = start to all refinements complete, `chain_stats` = statistics line of the final chain model. (added by session)
291 Finished {
292 /// Common header.
293 meta: Meta,
294 /// Start to all refinements complete (seconds).
295 pipeline_secs: f64,
296 /// (stage, accumulated time, count) in stage-name order.
297 stage_times: Vec<(String, Duration, usize)>,
298 /// Statistics line of the final chain model.
299 chain_stats: Vec<String>,
300 },
301 /// `Command::ResetFrom` was handled. (added by session)
302 Reset {
303 /// Common header.
304 meta: Meta,
305 /// Position index.
306 position: usize,
307 },
308 /// `Command::InvalidateZone` was handled. (added by session)
309 ZoneInvalidated {
310 /// Common header.
311 meta: Meta,
312 /// Zone index.
313 zone: usize,
314 },
315 /// Incremental display update computed by the library (opt-in, see `NodeBuilder::display_deltas` and
316 /// [`crate::live::DisplayFeed`]): the point-keyed difference between the previous and the new composed display cloud after
317 /// a zone result. Emitted right after the event that caused it.
318 DisplayDelta {
319 /// Common header (of the causing event).
320 meta: Meta,
321 /// Adds, moves and removes with dense slot ids (apply: removes, adds, moves).
322 delta: Arc<crate::live::DisplayDelta>,
323 /// Zone whose result caused the delta.
324 zone: usize,
325 /// Why it was emitted.
326 kind: crate::live::DeltaKind,
327 /// Seconds since the session start.
328 t: f64,
329 /// Coordinate frame of the display points.
330 frame: crate::live::DisplayFrame,
331 },
332 /// Warning.
333 Warning {
334 /// Common header.
335 meta: Meta,
336 /// Message.
337 message: String,
338 },
339 /// Error (including hook panics).
340 Error {
341 /// Common header.
342 meta: Meta,
343 /// Message.
344 message: String,
345 },
346}
347
348/// Event kind (for hook registration and filtering).
349#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
350#[non_exhaustive]
351pub enum EventKind {
352 /// [`Event::FrameIngested`].
353 FrameIngested,
354 /// [`Event::FrameDecoded`].
355 FrameDecoded,
356 /// [`Event::Started`].
357 Started,
358 /// [`Event::Log`].
359 Log,
360 /// [`Event::RefineStarted`].
361 RefineStarted,
362 /// [`Event::ZoneAdjusted`].
363 ZoneAdjusted,
364 /// [`Event::Finished`].
365 Finished,
366 /// [`Event::Reset`].
367 Reset,
368 /// [`Event::ZoneInvalidated`].
369 ZoneInvalidated,
370 /// [`Event::FeaturesExtracted`].
371 FeaturesExtracted,
372 /// [`Event::PairsMatched`].
373 PairsMatched,
374 /// [`Event::ModelInitialized`].
375 ModelInitialized,
376 /// [`Event::FrameRegistered`].
377 FrameRegistered,
378 /// [`Event::PositionDone`].
379 PositionDone,
380 /// [`Event::ZoneArrived`].
381 ZoneArrived,
382 /// [`Event::ZonePreview`].
383 ZonePreview,
384 /// [`Event::ZoneRefinedPose`].
385 ZoneRefinedPose,
386 /// [`Event::ZoneRefined`].
387 ZoneRefined,
388 /// [`Event::BaseAdopted`].
389 BaseAdopted,
390 /// [`Event::Reanchored`].
391 Reanchored,
392 /// [`Event::Snapshot`].
393 Snapshot,
394 /// [`Event::AllPositionsDone`].
395 AllPositionsDone,
396 /// [`Event::AllRefinedDone`].
397 AllRefinedDone,
398 /// [`Event::DisplayDelta`].
399 DisplayDelta,
400 /// [`Event::Warning`].
401 Warning,
402 /// [`Event::Error`].
403 Error,
404}
405
406impl Event {
407 /// Event kind.
408 pub fn kind(&self) -> EventKind {
409 use Event::*;
410 match self {
411 FrameIngested { .. } => EventKind::FrameIngested,
412 FrameDecoded { .. } => EventKind::FrameDecoded,
413 Started { .. } => EventKind::Started,
414 Log { .. } => EventKind::Log,
415 RefineStarted { .. } => EventKind::RefineStarted,
416 ZoneAdjusted { .. } => EventKind::ZoneAdjusted,
417 Finished { .. } => EventKind::Finished,
418 Reset { .. } => EventKind::Reset,
419 ZoneInvalidated { .. } => EventKind::ZoneInvalidated,
420 FeaturesExtracted { .. } => EventKind::FeaturesExtracted,
421 PairsMatched { .. } => EventKind::PairsMatched,
422 ModelInitialized { .. } => EventKind::ModelInitialized,
423 FrameRegistered { .. } => EventKind::FrameRegistered,
424 PositionDone { .. } => EventKind::PositionDone,
425 ZoneArrived { .. } => EventKind::ZoneArrived,
426 ZonePreview { .. } => EventKind::ZonePreview,
427 ZoneRefinedPose { .. } => EventKind::ZoneRefinedPose,
428 ZoneRefined { .. } => EventKind::ZoneRefined,
429 BaseAdopted { .. } => EventKind::BaseAdopted,
430 Reanchored { .. } => EventKind::Reanchored,
431 Snapshot { .. } => EventKind::Snapshot,
432 AllPositionsDone { .. } => EventKind::AllPositionsDone,
433 AllRefinedDone { .. } => EventKind::AllRefinedDone,
434 DisplayDelta { .. } => EventKind::DisplayDelta,
435 Warning { .. } => EventKind::Warning,
436 Error { .. } => EventKind::Error,
437 }
438 }
439 /// Common header.
440 pub fn meta(&self) -> &Meta {
441 use Event::*;
442 match self {
443 FrameIngested { meta, .. }
444 | FeaturesExtracted { meta, .. }
445 | PairsMatched { meta, .. }
446 | ModelInitialized { meta, .. }
447 | FrameRegistered { meta, .. }
448 | PositionDone { meta, .. }
449 | ZoneArrived { meta, .. }
450 | ZonePreview { meta, .. }
451 | ZoneRefinedPose { meta, .. }
452 | ZoneRefined { meta, .. }
453 | BaseAdopted { meta, .. }
454 | Reanchored { meta, .. }
455 | Snapshot { meta, .. }
456 | AllPositionsDone { meta }
457 | AllRefinedDone { meta, .. }
458 | FrameDecoded { meta, .. }
459 | Started { meta, .. }
460 | Log { meta, .. }
461 | RefineStarted { meta, .. }
462 | ZoneAdjusted { meta, .. }
463 | Finished { meta, .. }
464 | Reset { meta, .. }
465 | ZoneInvalidated { meta, .. }
466 | DisplayDelta { meta, .. }
467 | Warning { meta, .. }
468 | Error { meta, .. } => meta,
469 }
470 }
471}
472
473impl Event {
474 /// Event text for timeline.txt (None if there is none). The wording and format are the same as the existing record.
475 pub fn timeline_text(&self) -> Option<String> {
476 use Event::*;
477 Some(match self {
478 Started { positions, .. } => match positions {
479 Some(n) => format!("start NPOS={n}"),
480 None => "start".to_string(),
481 },
482 ModelInitialized { registered, .. } => format!("init_model Registered images: {registered}"),
483 BaseAdopted { zone, .. } => format!("adopt refined {zone} as base"),
484 PositionDone { position, registered, expected, .. } => format!("pos {position} registered {registered}/{expected}"),
485 ZoneArrived { range, .. } => format!("region {} arrived pos[{},{})", range.zone, range.lo, range.hi),
486 ZonePreview { range, .. } => format!("preview {} ready", range.zone),
487 RefineStarted { zone, .. } => format!("refined {zone} ba_start"),
488 ZoneRefinedPose { range, registered, mean_reproj_px, .. } => {
489 format!("refined {} pose_ready Registered images: {registered} Mean reprojection error: {mean_reproj_px:.6}px ", range.zone)
490 }
491 ZoneRefined { range, .. } => format!("refined {} ready", range.zone),
492 AllPositionsDone { .. } => "all positions done".to_string(),
493 AllRefinedDone { .. } => "all refined done".to_string(),
494 _ => return None,
495 })
496 }
497}
498
499/// Commands fed into the reducer.
500#[derive(Clone, Debug)]
501pub enum Command {
502 /// Reprocess from this position (discard later state).
503 ResetFrom(usize),
504 /// Invalidate the zone result and have it rebuilt.
505 InvalidateZone(usize),
506}