Skip to main content

cumulus3d_cli/
densewrap.rs

1/*
2 * densewrap.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//! Shared densification path: exclude images outside the zone -> undistortion -> scene conversion -> densify.
29//! Used by both the stream and the `densify` subcommand.
30
31use cumulus3d_core::{ImageId, Reconstruction};
32use cumulus3d_dense::{
33    undistort_with, DenseOutput, DenseScene, DensifyOptions, DepthMapCache, ImageBuffer, MvsProfile, PatchMatchBackend, SceneOptions,
34    UndistortBackend, UndistortCache, UndistortOptions,
35};
36use std::collections::BTreeMap;
37use std::path::{Path, PathBuf};
38use std::sync::{Arc, Mutex};
39use std::time::{Duration, Instant};
40
41/// PatchMatch backend selection. Currently only `cuda` exists. Without a device, an error string.
42pub fn make_pm_backend(name: &str) -> Result<Arc<dyn PatchMatchBackend>, String> {
43    match name {
44        "cuda" => cumulus3d_cuda::CudaPatchMatch::try_default()
45            .map(|b| Arc::new(b) as Arc<dyn PatchMatchBackend>)
46            .map_err(|e| format!("--pm-backend cuda: {e} (조밀화에는 CUDA 장치가 필요)")),
47        other => Err(format!("알 수 없는 --pm-backend {other} (cuda)")),
48    }
49}
50
51/// Image undistortion backend selection (cpu|cuda). `None` = the CPU path.
52pub fn make_undistort_backend(name: &str) -> Result<Option<Arc<dyn UndistortBackend>>, String> {
53    match name {
54        "cpu" => Ok(None),
55        "cuda" => cumulus3d_cuda::CudaUndistort::try_default()
56            .map(|b| Some(Arc::new(b) as Arc<dyn UndistortBackend>))
57            .map_err(|e| format!("--undistort-backend cuda: {e}")),
58        other => Err(format!("알 수 없는 --undistort-backend {other} (cpu|cuda)")),
59    }
60}
61
62/// Interpretation of `--mvs-profile`.
63pub fn parse_profile(s: &str) -> Result<MvsProfile, String> {
64    MvsProfile::parse(s).ok_or_else(|| format!("알 수 없는 --mvs-profile {s} (fast|quality)"))
65}
66
67/// Bundle adjustment backend selection (cpu|cuda). cuda prints a warning and solves again on the CPU if it fails at run time.
68pub fn make_ba_backend(name: &str) -> Result<Arc<dyn cumulus3d_ba::BaBackend>, String> {
69    match name {
70        "cpu" => Ok(Arc::new(cumulus3d_ba::CpuBaBackend)),
71        "cuda" => cumulus3d_cuda::CudaBundleAdjuster::try_default()
72            .map(|b| Arc::new(b) as Arc<dyn cumulus3d_ba::BaBackend>)
73            .map_err(|e| format!("--ba-backend cuda: {e}")),
74        other => Err(format!("알 수 없는 --ba-backend {other} (cpu|cuda)")),
75    }
76}
77
78/// SIFT backend selection (cpu|cuda).
79pub fn make_sift_backend(name: &str) -> Result<Arc<dyn cumulus3d_features::SiftEngine>, String> {
80    match name {
81        "cpu" => Ok(Arc::new(cumulus3d_features::CpuSift::default())),
82        "cuda" => cumulus3d_cuda::CudaSift::try_default()
83            .map(|b| Arc::new(b) as Arc<dyn cumulus3d_features::SiftEngine>)
84            .map_err(|e| format!("--sift-backend cuda: {e}")),
85        other => Err(format!("알 수 없는 --sift-backend {other} (cpu|cuda)")),
86    }
87}
88
89/// Descriptor matching backend selection (cpu|cuda).
90pub fn make_match_backend(name: &str) -> Result<Box<dyn cumulus3d_matching::MatcherBackend>, String> {
91    match name {
92        "cpu" => Ok(Box::new(cumulus3d_matching::CpuMatcher::default())),
93        "cuda" => cumulus3d_cuda::CudaMatcher::try_default()
94            .map(|b| Box::new(b) as Box<dyn cumulus3d_matching::MatcherBackend>)
95            .map_err(|e| format!("--match-backend cuda: {e}")),
96        other => Err(format!("알 수 없는 --match-backend {other} (cpu|cuda)")),
97    }
98}
99
100/// Sparse-stage (two-view geometric verification, registration, triangulation) backend selection (cpu|cuda). With cuda the
101/// three stages share one device.
102pub fn make_sparse_backend(name: &str) -> Result<crate::session::SparseBackends, String> {
103    match name {
104        "cpu" => Ok(crate::session::SparseBackends::default()),
105        "cuda" => cumulus3d_cuda::CudaSparse::try_default()
106            .map(|b| {
107                let b = Arc::new(b);
108                crate::session::SparseBackends { two_view: b.clone(), abs_pose: b.clone(), triangulation: b }
109            })
110            .map_err(|e| format!("--sparse-backend cuda: {e}")),
111        other => Err(format!("알 수 없는 --sparse-backend {other} (cpu|cuda)")),
112    }
113}
114
115/// Densification settings.
116#[derive(Clone)]
117pub struct DenseConfig {
118    /// Undistortion options.
119    pub undistort: UndistortOptions,
120    /// Dense scene conversion options.
121    pub scene: SceneOptions,
122    /// Densification (PatchMatch, filter, fusion) options.
123    pub densify: DensifyOptions,
124    /// Score fusion settings (if present, score fusion is used instead of `densify.fusion.mode`).
125    pub score: Option<cumulus3d_dense::fusion_score::ScoreFusionOptions>,
126    /// Backend (if it could not be created, that error; reported in the densification stage).
127    pub backend: Result<Arc<dyn PatchMatchBackend>, String>,
128    /// `--serialize-dense`: one backend call at a time.
129    pub lock: Option<Arc<Mutex<()>>>,
130    /// Depth map cache (`--depth-cache`).
131    pub depth_cache: Option<Arc<DepthMapCache>>,
132    /// Undistortion result cache.
133    pub undistort_cache: Arc<UndistortCache>,
134    /// Image undistortion backend (None = CPU).
135    pub undistort_backend: Option<Arc<dyn UndistortBackend>>,
136}
137
138impl DenseConfig {
139    /// Undistortion max_image_size 960, profile defaults.
140    pub fn new(backend: Result<Arc<dyn PatchMatchBackend>, String>, profile: MvsProfile) -> Self {
141        Self {
142            undistort: UndistortOptions::pipeline(),
143            scene: SceneOptions::default(),
144            densify: DensifyOptions::with_profile(profile),
145            score: None,
146            backend,
147            lock: None,
148            depth_cache: None,
149            undistort_cache: Arc::new(UndistortCache::new()),
150            undistort_backend: None,
151        }
152    }
153}
154
155/// Densification result summary.
156pub struct DenseRun {
157    /// Number of images used for densification.
158    pub frames: usize,
159    /// Densification input scene.
160    pub scene: DenseScene,
161    /// Densification result (point cloud etc.).
162    pub output: DenseOutput,
163    /// Undistortion time.
164    pub undistort_time: Duration,
165    /// Densification time.
166    pub densify_time: Duration,
167    /// Time spent waiting for the backend serialization lock.
168    pub lock_wait: Duration,
169}
170
171/// Densify after removing registered images for which `keep` is false (image_deleter). The input image before undistortion is
172/// `image_root/name`.
173pub fn dense_model(model: &Reconstruction, keep: impl Fn(&str) -> bool, image_root: &Path, cfg: &DenseConfig) -> Result<DenseRun, String> {
174    let backend = cfg.backend.clone()?;
175    let t0 = Instant::now();
176    let mut m = model.clone();
177    let del: Vec<String> =
178        m.registered_images().into_iter().filter_map(|id| m.image(id).map(|im| im.name.clone())).filter(|n| !keep(n)).collect();
179    let del_ref: Vec<&str> = del.iter().map(|s| s.as_str()).collect();
180    m.deregister_images_by_name(&del_ref);
181    let root: PathBuf = image_root.to_path_buf();
182    let und = undistort_with(
183        &m,
184        &cfg.undistort,
185        &cfg.undistort_cache,
186        |im| ImageBuffer::load(root.join(&im.name)).ok().map(Arc::new),
187        cfg.undistort_backend.as_deref(),
188    )
189    .map_err(|e| format!("왜곡 보정 실패: {e}"))?;
190    let images: BTreeMap<ImageId, Arc<ImageBuffer>> = und.images;
191    let frames = images.len();
192    let scene = DenseScene::from_reconstruction(&und.reconstruction, &images, &cfg.scene).map_err(|e| format!("장면 변환 실패: {e}"))?;
193    drop(images);
194    let undistort_time = t0.elapsed();
195    let tw = Instant::now();
196    let guard = cfg.lock.as_ref().map(|l| l.lock().unwrap_or_else(|p| p.into_inner()));
197    let lock_wait = tw.elapsed();
198    let t1 = Instant::now();
199    let output =
200        cumulus3d_dense::densify::densify_with(&scene, &cfg.densify, cfg.score.as_ref(), backend.as_ref(), cfg.depth_cache.as_deref())
201            .map_err(|e| format!("조밀화 실패: {e}"));
202    drop(guard);
203    let output = output?;
204    Ok(DenseRun { frames, scene, output, undistort_time, densify_time: t1.elapsed(), lock_wait })
205}