Skip to main content

cumulus3d_cli/
stream.rs

1//! `cumulus3d stream`: 입력 폴더의 위치를 하나씩 [`Session`] 리듀서에 넣고([`Pipeline`]),
2//! 기본 출력 훅([`crate::sinks`])이 timeline.txt·run.log·점군·스냅샷 파일을 만든다.
3//! 계산(특징 → 짝 매칭 → 첫 모델/이어 등록 → 구역별 초벌·정밀)은 [`crate::session`] 에 있다.
4
5use crate::densewrap::{make_match_backend, make_pm_backend, make_sift_backend, parse_profile, DenseConfig};
6use crate::pipeline::Pipeline;
7use crate::session::{pairs_for, zones_upto, FrameImage, FrameSet, Input, Session, SessionConfig};
8use crate::sinks::{DefaultSinks, SinkOptions};
9use cumulus3d_core::io::{read_gps_file, GpsRecord};
10use cumulus3d_dense::DepthMapCache;
11use std::collections::{BTreeMap, HashMap};
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, Mutex};
14
15/// 카메라 폴더(스크립트 순서).
16pub const CAMS: [&str; 3] = ["camF", "camR", "camL"];
17
18/// 스트림 설정. 기본값 = 스크립트 동작.
19#[derive(Clone, Debug)]
20pub struct StreamConfig {
21    /// 입력 폴더(`images/<cam>/`, `gps_ref.txt`).
22    pub src: PathBuf,
23    /// 출력 폴더.
24    pub out: PathBuf,
25    /// 구역 크기(위치 수).
26    pub span: usize,
27    /// 구역 겹침(위치 수).
28    pub overlap: usize,
29    /// 프레임 간격(번호 % stride == 0 만 사용).
30    pub stride: usize,
31    /// 새로 등록된 영상만 삼각측량(개선, 기본 끔).
32    pub incremental_triangulation: bool,
33    /// 겹침 영상 깊이맵 재사용(개선, 기본 끔).
34    pub depth_cache: bool,
35    /// 첫 GPS 줄로 세션 ENU 원점 고정(개선, 기본 끔 = 정렬마다 첫 공통 기록).
36    pub fixed_enu_origin: bool,
37    /// PatchMatch 백엔드 이름(cuda).
38    pub pm_backend: String,
39    /// 조밀화 프로파일(fast|quality).
40    pub mvs_profile: String,
41    /// SIFT 백엔드(cpu|cuda).
42    pub sift_backend: String,
43    /// 기술자 매칭 백엔드(cpu|cuda).
44    pub match_backend: String,
45    /// rayon 스레드 수(0 = 코어 수).
46    pub threads: usize,
47    /// 앞 N 위치만 처리.
48    pub max_positions: Option<usize>,
49    /// 구역별 희소 모델 저장.
50    pub save_models: bool,
51    /// 초벌·정밀 조밀화 백엔드 호출 직렬화.
52    pub serialize_dense: bool,
53    /// 왜곡 보정 최대 크기(스크립트 960).
54    pub dense_max_image_size: i64,
55    /// 조밀화 원천 뷰 수.
56    pub number_views: usize,
57    /// SIFT 최대 특징 수(스크립트 8192).
58    pub max_num_features: usize,
59    /// SIFT 입력 최대 크기(기본 3200).
60    pub sift_max_image_size: usize,
61    /// 표준 출력에도 run.log 내용을 보임.
62    pub echo: bool,
63    /// 조밀화 생략(개발·시험용).
64    pub no_dense: bool,
65    /// 두 뷰 기하·GPS 정렬 RANSAC 시드(None = 실행마다 다름).
66    pub seed: Option<u64>,
67}
68
69impl StreamConfig {
70    /// 기본 설정(구역 12, 겹침 2, stride 3).
71    pub fn new(src: PathBuf, out: PathBuf) -> Self {
72        Self {
73            src,
74            out,
75            span: 12,
76            overlap: 2,
77            stride: 3,
78            incremental_triangulation: false,
79            depth_cache: false,
80            fixed_enu_origin: false,
81            pm_backend: "cuda".into(),
82            mvs_profile: "fast".into(),
83            sift_backend: "cpu".into(),
84            match_backend: "cpu".into(),
85            threads: 0,
86            max_positions: None,
87            save_models: false,
88            serialize_dense: false,
89            dense_max_image_size: 960,
90            number_views: 10,
91            max_num_features: 8192,
92            sift_max_image_size: 3200,
93            echo: true,
94            no_dense: false,
95            seed: None,
96        }
97    }
98}
99
100/// 위치 ↔ 영상 이름. 위치 = 선택된 프레임 목록의 순번(파일 번호 / stride 가 아님).
101#[derive(Clone, Debug)]
102pub struct Layout {
103    /// 위치별 입력 파일의 프레임 번호.
104    pub frames: Vec<u32>,
105    /// 영상 이름 → 위치.
106    pub pos_of: HashMap<String, usize>,
107}
108
109impl Layout {
110    /// `images/camF/camF_NNNN.jpg` 중 번호 % stride == 0 인 것(번호 오름차순).
111    pub fn discover(src: &Path, stride: usize) -> Result<Self, String> {
112        let dir = src.join("images").join(CAMS[0]);
113        let rd = std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
114        let mut frames = Vec::new();
115        for e in rd.flatten() {
116            let n = e.file_name().to_string_lossy().to_string();
117            let Some(stem) = n.strip_prefix(&format!("{}_", CAMS[0])).and_then(|s| s.strip_suffix(".jpg")) else { continue };
118            if let Ok(i) = stem.parse::<u32>() {
119                if (i as usize).is_multiple_of(stride.max(1)) {
120                    frames.push(i);
121                }
122            }
123        }
124        frames.sort_unstable();
125        frames.dedup();
126        let mut l = Self { frames, pos_of: HashMap::new() };
127        for p in 0..l.frames.len() {
128            for c in CAMS {
129                let n = l.name(c, p);
130                l.pos_of.insert(n, p);
131            }
132        }
133        Ok(l)
134    }
135
136    /// 카메라·위치의 영상 이름(`cam/cam_NNNN.jpg`).
137    pub fn name(&self, cam: &str, p: usize) -> String {
138        format!("{cam}/{cam}_{:04}.jpg", self.frames[p])
139    }
140
141    /// 영상 이름의 위치.
142    pub fn position(&self, name: &str) -> Option<usize> {
143        self.pos_of.get(name).copied()
144    }
145}
146
147/// 위치 p 의 프레임 묶음(카메라 순서 camF, camR, camL).
148pub fn frame_set(layout: &Layout, p: usize) -> FrameSet {
149    FrameSet::new(CAMS.iter().map(|c| (c.to_string(), layout.name(c, p))))
150}
151
152/// 스크립트와 같은 짝 규칙: 같은 카메라 간격 1..5, 8, 16 / 다른 카메라 위치 차 0..4. 정렬된 이름 짝.
153pub fn pairs_for_position(layout: &Layout, p: usize) -> Vec<(String, String)> {
154    let frames: Vec<Vec<FrameImage>> = (0..=p).map(|q| frame_set(layout, q).images).collect();
155    pairs_for(&frames, p)
156}
157
158/// 구역: start = 0, SPAN, …; [start−OVL, start+SPAN+OVL) ∩ [0, NPOS). 완료 위치 = hi−1.
159pub fn regions(npos: usize, span: usize, ovl: usize) -> Vec<(usize, usize, usize)> {
160    zones_upto(npos, span, ovl).into_iter().map(|z| (z.zone, z.lo, z.hi)).collect()
161}
162
163/// 스트림 설정 → 세션 설정. `gps` = 선택된 영상의 GPS 기록(파일 순서), `dense` = None 이면 조밀화 생략.
164pub fn session_config(cfg: &StreamConfig, npos: usize, gps: Vec<GpsRecord>, dense: Option<DenseConfig>) -> Result<SessionConfig, String> {
165    let mut s = SessionConfig::new(cfg.src.join("images"));
166    s.gps = gps;
167    s.span = cfg.span;
168    s.overlap = cfg.overlap;
169    s.total_positions = Some(npos);
170    s.incremental_triangulation = cfg.incremental_triangulation;
171    s.fixed_enu_origin = cfg.fixed_enu_origin;
172    s.seed = cfg.seed;
173    s.extraction.sift.max_num_features = cfg.max_num_features;
174    s.extraction.reader.max_image_size = cfg.sift_max_image_size;
175    s.sift = make_sift_backend(&cfg.sift_backend)?;
176    s.matcher = Arc::from(make_match_backend(&cfg.match_backend)?);
177    s.dense = dense;
178    // CLI 는 되감지 않는다(메모리 절약).
179    s.history = 0;
180    Ok(s)
181}
182
183/// 스트림 실행: 세션 리듀서 + 파이프라인 + 기본 출력 훅. 출력 폴더를 새로 만든다.
184pub fn run_stream(cfg: StreamConfig) -> Result<(), String> {
185    if cfg.threads > 0 {
186        // 이미 만들어졌으면(테스트 등) 무시.
187        let _ = rayon::ThreadPoolBuilder::new().num_threads(cfg.threads).build_global();
188    }
189    // 백엔드가 없으면 조밀화 단계에서 오류로 기록한다.
190    let backend = make_pm_backend(&cfg.pm_backend);
191    let profile = parse_profile(&cfg.mvs_profile)?;
192    let out = cfg.out.clone();
193    if out.exists() {
194        std::fs::remove_dir_all(&out).map_err(|e| format!("{}: {e}", out.display()))?;
195    }
196    let sinks = DefaultSinks::new(&out, &SinkOptions { echo: cfg.echo, save_models: cfg.save_models }).map_err(|e| e.to_string())?;
197
198    let mut layout = Layout::discover(&cfg.src, cfg.stride)?;
199    if let Some(m) = cfg.max_positions {
200        layout.frames.truncate(m);
201        let frames = layout.frames.len();
202        layout.pos_of.retain(|_, p| *p < frames);
203    }
204    let npos = layout.frames.len();
205    let gps_path = cfg.src.join("gps_ref.txt");
206    let gps_all: Vec<GpsRecord> = read_gps_file(&gps_path)
207        .map_err(|e| format!("{}: {e}", gps_path.display()))?
208        .into_iter()
209        .filter(|g| layout.pos_of.contains_key(&g.name))
210        .collect();
211
212    let dense = (!cfg.no_dense).then(|| {
213        let mut dense = DenseConfig::new(backend, profile);
214        dense.undistort.max_image_size = cfg.dense_max_image_size;
215        dense.densify.neighbors.num_views = cfg.number_views;
216        if cfg.serialize_dense {
217            dense.lock = Some(Arc::new(Mutex::new(())));
218        }
219        if cfg.depth_cache {
220            dense.depth_cache = Some(Arc::new(DepthMapCache::new()));
221        }
222        dense
223    });
224    let session = Session::new(session_config(&cfg, npos, gps_all, dense)?);
225    // 출력 훅은 하나(on_any)라 비동기여도 이벤트 순서대로 처리된다.
226    let mut pl = Pipeline::new(session).on_any(sinks.into_hook());
227
228    let (span, ovl) = (cfg.span, cfg.overlap);
229    let mut reg_end: BTreeMap<usize, Vec<(usize, usize, usize)>> = BTreeMap::new();
230    for (k, lo, hi) in regions(npos, span, ovl) {
231        reg_end.entry(hi - 1).or_default().push((k, lo, hi));
232    }
233    pl.push(Input::Log(format!(
234        "regions: {}",
235        reg_end
236            .iter()
237            .map(|(e, v)| format!("{e} ->{}", v.iter().map(|(k, lo, hi)| format!(" {k}:{lo}:{hi}")).collect::<String>()))
238            .collect::<Vec<_>>()
239            .join(", ")
240    )));
241    pl.push(Input::Log(format!(
242        "설정: span {span} overlap {ovl} stride {} 증분삼각측량 {} 깊이캐시 {} 고정원점 {} pm {}({})/sift {}/match {} 직렬조밀화 {} 왜곡보정최대 {} 뷰 {} 스레드 {}",
243        cfg.stride,
244        cfg.incremental_triangulation,
245        cfg.depth_cache,
246        cfg.fixed_enu_origin,
247        cfg.pm_backend,
248        cfg.mvs_profile,
249        cfg.sift_backend,
250        cfg.match_backend,
251        cfg.serialize_dense,
252        cfg.dense_max_image_size,
253        cfg.number_views,
254        rayon::current_num_threads()
255    )));
256    for p in 0..npos {
257        pl.push(frame_set(&layout, p).into());
258        if let Some(e) = pl.reducer().failed() {
259            // 치명 오류: 후처리 없이 멈춘다(훅 큐는 파이프라인을 버릴 때 비워진다).
260            return Err(e.to_string());
261        }
262    }
263    let _ = pl.finish();
264    Ok(())
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn regions_match_script() {
273        // NPOS=26, SPAN 12, OVL 2 → [0,14), [10,26), [22,26)
274        assert_eq!(regions(26, 12, 2), vec![(0, 0, 14), (1, 10, 26), (2, 22, 26)]);
275        assert_eq!(regions(40, 12, 2), vec![(0, 0, 14), (1, 10, 26), (2, 22, 38), (3, 34, 40)]);
276    }
277
278    #[test]
279    fn pairs_rule() {
280        let frames: Vec<u32> = (0..20).map(|i| i * 3).collect();
281        let mut l = Layout { frames, pos_of: HashMap::new() };
282        l.pos_of.insert("x".into(), 0);
283        let p0 = pairs_for_position(&l, 0);
284        // 위치 0: 다른 카메라끼리 3짝.
285        assert_eq!(p0.len(), 3);
286        let p17 = pairs_for_position(&l, 17);
287        // 같은 카메라 7간격 × 3 + 다른 카메라: 6 순서쌍 × 5 위치 중 q=p 는 중복 → 3 + 6×4 = 27.
288        assert_eq!(p17.len(), 21 + 27);
289        assert!(p17.contains(&("camF/camF_0003.jpg".to_string(), "camF/camF_0051.jpg".to_string())));
290    }
291}