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}