1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/*
* main.rs
*
* Copyright (c) 2026 ParkSangWoo
*
* Author(s):
*
* ParkSangWoo <dev.parksangwoo@gmail.com>
*
*
* This file is part of cumulus3d.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
//! The `cumulus3d` executable.
use clap::{Args, Parser, Subcommand};
use cumulus3d_cli::declare::{Plan, Recon};
use cumulus3d_cli::interop::{self, InteropCmd};
use cumulus3d_cli::stream::{run_stream, StreamConfig};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "cumulus3d", version, about = "한 프로세스 점진 재구성 + 단계별 하위 명령")]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand, Debug)]
enum Cmd {
/// incremental_stream.sh as a single process: position-arrival loop + per-zone preview/refined densification + post-processing.
Stream(Box<StreamArgs>),
/// Run from a plan TOML file: validate the plan (reporting every problem) -> position loop, wait for background jobs,
/// post-processing, all in one go.
Run(RunArgs),
/// Plan tools: print the default plan, validate a plan.
Plan(PlanArgs),
#[command(flatten)]
Interop(InteropCmd),
}
#[derive(Args, Debug)]
struct StreamArgs {
/// Input folder (images/camF|camR|camL/*.jpg, gps_ref.txt).
#[arg(long)]
src: PathBuf,
/// Output folder (deleted and recreated).
#[arg(long)]
out: PathBuf,
#[arg(long, default_value_t = 12)]
span: usize,
#[arg(long, default_value_t = 2)]
overlap: usize,
/// Only frames with file number % stride == 0. Position = index in the selected frame list.
#[arg(long, default_value_t = 3)]
stride: usize,
/// Triangulate only newly registered images (an improvement that can change the result).
#[arg(long)]
incremental_triangulation: bool,
/// Reuse depth maps of overlapping images.
#[arg(long)]
depth_cache: bool,
/// Fix the session ENU origin to the first GPS line.
#[arg(long)]
fixed_enu_origin: bool,
/// PatchMatch backend (cuda). Without a device, densification fails with an error.
#[arg(long, default_value = "cuda")]
pm_backend: String,
/// Densification profile (fast|quality).
#[arg(long, default_value = "fast")]
mvs_profile: String,
/// SIFT backend (cpu|cuda). The result is the same as cpu.
#[arg(long, default_value = "cpu")]
sift_backend: String,
/// Descriptor matching backend (cpu|cuda). The result is the same as cpu.
#[arg(long, default_value = "cpu")]
match_backend: String,
/// Image undistortion backend (cpu|cuda). The result matches cpu up to rounding.
#[arg(long, default_value = "cpu")]
undistort_backend: String,
/// Set --pm-backend/--sift-backend/--match-backend/--undistort-backend all to cuda.
#[arg(long)]
gpu: bool,
/// Number of rayon threads (0 = number of cores).
#[arg(long, default_value_t = 0)]
threads: usize,
/// Only the first N positions (for quick tests).
#[arg(long)]
max_positions: Option<usize>,
/// Save the per-zone sparse model (work/models/*).
#[arg(long)]
save_models: bool,
/// Serialize the preview/refined densification backend calls.
#[arg(long)]
serialize_dense: bool,
/// Maximum undistorted image size (script: 960). Lowering it speeds up densification.
#[arg(long, default_value_t = 960)]
dense_max_image_size: i64,
/// Number of densification source views (<= 32).
#[arg(long, default_value_t = 10)]
number_views: usize,
/// Maximum number of SIFT features (script: 8192).
#[arg(long, default_value_t = 8192)]
max_num_features: usize,
/// Maximum SIFT input image size (default 3200).
#[arg(long, default_value_t = 3200)]
sift_max_image_size: usize,
/// Do not show progress on standard output (only in run.log).
#[arg(long)]
quiet: bool,
/// Skip densification (for development/tests; no post-processing output).
#[arg(long)]
no_dense: bool,
/// Stable display: no preview, each refined zone is adjusted with already shown cameras held fixed and stays where
/// it was shown (local frame); GPS/ENU is applied once to the final outputs.
#[arg(long)]
stable_display: bool,
/// Stable display: the display settles (shown cameras and intrinsics frozen) once the relative focal change between
/// consecutive refined zones is below this (default 0.01 = 1 %).
#[arg(long, default_value_t = 0.01)]
settle_tol: f64,
/// Stable display: settle by force at this zone at the latest (default 3).
#[arg(long, default_value_t = 3)]
settle_max_zone: usize,
/// Soft intrinsics prior: JSON `{"cameras": {"<camera_id>": [params in the camera model's order]}}` (OPENCV:
/// fx fy cx cy k1 k2 p1 p2). Cameras start from these values and the bundle adjustment is pulled towards them.
#[arg(long)]
intrinsics_prior: Option<PathBuf>,
/// Intrinsics prior: relative sigma of the focal lengths (residual (f - f0)/(sigma*f0)).
#[arg(long, default_value_t = 0.005)]
intrinsics_prior_sigma: f64,
/// Intrinsics prior: absolute sigma of the distortion parameters.
#[arg(long, default_value_t = 0.01)]
intrinsics_prior_dist_sigma: f64,
/// Fix the RANSAC seed (two-view geometry, GPS alignment). Without it, it differs from run to run.
#[arg(long)]
seed: Option<u64>,
/// Extract the features of the next N positions ahead while the current one is matched, registered and triangulated
/// (0 = off). The results do not change.
#[arg(long, default_value_t = 0)]
lookahead: usize,
/// Stable display: show a sparse preview of every zone as soon as it closes (placed in the display frame; the refined zone
/// replaces it later).
#[arg(long)]
preview: bool,
/// Live display thinning: keep 1 of N points (key hash); 1 = keep all. Not applied to merged.ply.
#[arg(long, default_value_t = 6, value_parser = parse_live_stride)]
live_stride: usize,
/// Upper bound on the number of densification scales (1 = a single scale at full resolution; unset = profile default).
#[arg(long)]
max_levels: Option<usize>,
/// Fused-cloud smoothing along normals: iterations (0 = off).
#[arg(long)]
fusion_smooth_iters: Option<usize>,
/// Fused-cloud smoothing: neighbour radius (GSD multiple).
#[arg(long)]
fusion_smooth_radius: Option<f64>,
/// Fused-cloud smoothing: range sigma along the normal (GSD multiple).
#[arg(long)]
fusion_smooth_range: Option<f64>,
/// Fused-cloud smoothing: neighbour normal gate (degrees).
#[arg(long)]
fusion_smooth_normal: Option<f64>,
/// Free-space visibility filter: neighbouring views checked per fused point (0 = off).
#[arg(long)]
visibility_views: Option<usize>,
/// Free-space visibility filter: relative depth tolerance.
#[arg(long)]
visibility_tau: Option<f64>,
/// Free-space visibility filter: minimum violating views to remove a point.
#[arg(long)]
visibility_min_violations: Option<usize>,
}
#[derive(Args, Debug)]
struct RunArgs {
/// Plan file (TOML). Relative paths are relative to the working location.
plan: PathBuf,
/// Only validate; do not run.
#[arg(long)]
check: bool,
}
#[derive(Args, Debug)]
struct PlanArgs {
/// Print the default plan (aerial-formation preset) as TOML to standard output.
#[arg(long)]
print_default: bool,
/// Validate the plan file (do not run).
#[arg(long)]
check: Option<PathBuf>,
}
fn parse_live_stride(s: &str) -> Result<usize, String> {
match s.parse::<usize>() {
Ok(n) if n >= 1 => Ok(n),
_ => Err("must be an integer >= 1".into()),
}
}
fn check_plan(path: &PathBuf) -> Result<Recon, String> {
Recon::from_plan(Plan::load(path)?).build().map_err(|e| e.to_string())
}
fn main() {
let cli = Cli::parse();
let r = match cli.cmd {
Cmd::Stream(a) => {
let a = *a;
let mut c = StreamConfig::new(a.src, a.out);
c.span = a.span;
c.overlap = a.overlap;
c.stride = a.stride;
c.incremental_triangulation = a.incremental_triangulation;
c.depth_cache = a.depth_cache;
c.fixed_enu_origin = a.fixed_enu_origin;
c.mvs_profile = a.mvs_profile;
c.pm_backend = a.pm_backend;
c.sift_backend = a.sift_backend;
c.match_backend = a.match_backend;
c.undistort_backend = a.undistort_backend;
if a.gpu {
c.undistort_backend = "cuda".into();
c.pm_backend = "cuda".into();
c.sift_backend = "cuda".into();
c.match_backend = "cuda".into();
}
c.threads = a.threads;
c.max_positions = a.max_positions;
c.save_models = a.save_models;
c.serialize_dense = a.serialize_dense;
c.dense_max_image_size = a.dense_max_image_size;
c.number_views = a.number_views;
c.max_num_features = a.max_num_features;
c.sift_max_image_size = a.sift_max_image_size;
c.echo = !a.quiet;
c.no_dense = a.no_dense;
c.stable_display = a.stable_display;
c.settle_tol = a.settle_tol;
c.settle_max_zone = a.settle_max_zone;
c.intrinsics_prior = a.intrinsics_prior;
c.intrinsics_prior_sigma = a.intrinsics_prior_sigma;
c.intrinsics_prior_dist_sigma = a.intrinsics_prior_dist_sigma;
c.seed = a.seed;
c.max_levels = a.max_levels;
c.live_stride = a.live_stride;
c.lookahead = a.lookahead;
c.preview = a.preview;
c.fusion.smooth_iters = a.fusion_smooth_iters;
c.fusion.smooth_radius = a.fusion_smooth_radius;
c.fusion.smooth_range = a.fusion_smooth_range;
c.fusion.smooth_normal = a.fusion_smooth_normal;
c.fusion.visibility_views = a.visibility_views;
c.fusion.visibility_tau = a.visibility_tau;
c.fusion.visibility_min_violations = a.visibility_min_violations;
run_stream(c)
}
Cmd::Run(a) => check_plan(&a.plan).and_then(|r| if a.check { Ok(()) } else { r.run().map(|_| ()) }),
Cmd::Plan(a) => {
if a.print_default {
print!("{}", Plan::default().to_toml_annotated());
Ok(())
} else if let Some(p) = a.check {
check_plan(&p).map(|r| println!("계획 검사 통과: 위치 {}", r.positions()))
} else {
Err("--print-default 또는 --check <plan.toml> 중 하나가 필요함".into())
}
}
Cmd::Interop(c) => interop::run(c),
};
if let Err(e) = r {
eprintln!("오류: {e}");
std::process::exit(1);
}
}