Skip to main content

cumulus3d_cli/
post.rs

1//! 후처리: 스크립트 끝의 파이썬 부분(초벌 정렬·사건별 스냅샷·manifest)과 incremental_reanchor.py 를
2//! align 크레이트로 수행한다. 모델은 메모리의 Reconstruction 을 그대로 쓴다.
3
4use crate::util::{py_float, Json, Logger};
5use cumulus3d_align::{compose_snapshot, robust_umeyama, shared_point_correspondences, transform_cloud, KdTree, SharedPointOptions, SnapshotOptions};
6use cumulus3d_align::shared::NameFilter;
7use cumulus3d_align::RobustUmeyamaOptions;
8use cumulus3d_core::io::{read_ply, write_ply, PlyLayout, PointCloud};
9use cumulus3d_core::{Reconstruction, Sim3, Vec3};
10use std::collections::{BTreeMap, BTreeSet, HashMap};
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13
14/// 후처리 입력(PLY 경로·사건 문자열 기반).
15pub struct PostInput<'a> {
16    /// 출력 폴더.
17    pub out: &'a Path,
18    /// 구역 크기(위치 수).
19    pub span: usize,
20    /// 구역 겹침(위치 수).
21    pub ovl: usize,
22    /// (첫 사건 기준 초, 사건 문자열).
23    pub events: Vec<(f64, String)>,
24    /// 구역별 초벌 모델.
25    pub previews: &'a BTreeMap<usize, Reconstruction>,
26    /// 구역별 정밀 모델.
27    pub refined: &'a BTreeMap<usize, Reconstruction>,
28    /// 구역별 초벌 PLY 경로.
29    pub preview_ply: &'a BTreeMap<usize, PathBuf>,
30    /// 구역별 정밀 PLY 경로.
31    pub refined_ply: &'a BTreeMap<usize, PathBuf>,
32    /// 영상 이름 → 위치.
33    pub position: &'a (dyn Fn(&str) -> Option<usize> + Sync),
34}
35
36/// 파이썬 `tof`: 첫 단어열이 일치하거나 접두가 같은 첫 사건 시각.
37fn tof(events: &[(f64, String)], pat: &str) -> Option<f64> {
38    events.iter().find(|(_, s)| s.split(" pos[").next() == Some(pat) || s.starts_with(pat)).map(|(t, _)| *t)
39}
40
41/// 견고 Umeyama 결과(s, R, t, 잔차 중앙, 인라이어 수).
42struct Fit {
43    sim3: Sim3,
44    median: f64,
45    inliers: usize,
46}
47
48/// 두 모델의 공유 3D 점(같은 이름 영상·같은 2D 인덱스)으로 a_to_b 를 맞춘다.
49fn corr_points(a: &Reconstruction, b: &Reconstruction, filter: Option<NameFilter>) -> (Vec<Vec3>, Vec<Vec3>) {
50    let opts = SharedPointOptions { position_range: None, name_filter: filter };
51    let pairs = shared_point_correspondences(a, b, &opts);
52    let mut pa = Vec::with_capacity(pairs.len());
53    let mut pb = Vec::with_capacity(pairs.len());
54    for (i, j) in pairs {
55        if let (Some(x), Some(y)) = (a.point3d(i), b.point3d(j)) {
56            pa.push(x.xyz);
57            pb.push(y.xyz);
58        }
59    }
60    (pa, pb)
61}
62
63fn fit(pa: &[Vec3], pb: &[Vec3]) -> Option<Fit> {
64    let r = robust_umeyama(pa, pb, &RobustUmeyamaOptions::default())?;
65    Some(Fit { sim3: r.sim3, median: r.median_residual, inliers: r.num_inliers })
66}
67
68fn transformed(c: &PointCloud, t: &Sim3) -> PointCloud {
69    let mut c = c.clone();
70    transform_cloud(&mut c, t);
71    c
72}
73
74fn write(path: &Path, c: &PointCloud) -> Result<(), String> {
75    if let Some(d) = path.parent() {
76        std::fs::create_dir_all(d).map_err(|e| e.to_string())?;
77    }
78    write_ply(path, c, PlyLayout::XyzRgbNormal).map_err(|e| format!("{}: {e}", path.display()))
79}
80
81fn basename(p: &Path) -> String {
82    p.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_default()
83}
84
85/// 앞 구름의 점들에 대한 뒤 구름(1/20 추출) 최근접 거리 중 < 5 m 의 중앙값.
86fn overlap_median(prev: &PointCloud, cur: &PointCloud) -> Option<(f64, usize)> {
87    if prev.is_empty() {
88        return None;
89    }
90    let tree = KdTree::from_f32(&prev.positions);
91    let q: Vec<[f64; 3]> = cur.positions.iter().step_by(20).map(|p| [p[0] as f64, p[1] as f64, p[2] as f64]).collect();
92    let mut d: Vec<f64> = tree.nearest_many(&q).into_iter().flatten().map(|(_, d2)| d2.sqrt()).filter(|d| *d < 5.0).collect();
93    if d.is_empty() {
94        return None;
95    }
96    let n = d.len();
97    d.sort_by(f64::total_cmp);
98    let m = if n % 2 == 1 { d[n / 2] } else { 0.5 * (d[n / 2 - 1] + d[n / 2]) };
99    Some((m, n))
100}
101
102/// 구역별 사건 시각(첫 사건 기준 초). 없으면 None.
103#[derive(Clone, Copy, Debug, Default, PartialEq)]
104pub struct ZoneTimes {
105    /// `region K arrived`.
106    pub arrived: Option<f64>,
107    /// `preview K ready`.
108    pub preview_ready: Option<f64>,
109    /// `refined K pose_ready`.
110    pub refined_pose: Option<f64>,
111    /// `refined K ready`.
112    pub refined_ready: Option<f64>,
113}
114
115/// 구역 점군 하나: 출력 파일 이름(full/ 과 같은 이름)과 점군.
116pub struct ZoneCloud<'a> {
117    /// 출력 파일 이름.
118    pub file: String,
119    /// 점군.
120    pub cloud: &'a PointCloud,
121}
122
123/// 메모리 입력형 후처리(이벤트로 모은 구역 결과). [`run`] 과 기본 훅이 함께 쓴다.
124pub struct PostZones<'a> {
125    /// 출력 폴더.
126    pub out: &'a Path,
127    /// 조밀 점군이 있는 초벌 구역.
128    pub preview: BTreeMap<usize, ZoneCloud<'a>>,
129    /// 조밀 점군이 있는 정밀 구역.
130    pub refined: BTreeMap<usize, ZoneCloud<'a>>,
131    /// 초벌 모델(구역 0 은 GPS 정렬본).
132    pub preview_models: BTreeMap<usize, &'a Reconstruction>,
133    /// 정밀 모델(BA + GPS 정렬).
134    pub refined_models: BTreeMap<usize, &'a Reconstruction>,
135    /// 구역별 사건 시각.
136    pub times: BTreeMap<usize, ZoneTimes>,
137    /// 구역 K(≥ 1) 초벌 정렬에 쓰는 위치 구간 [lo, hi) = 앞 구역과의 겹침.
138    pub windows: BTreeMap<usize, (usize, usize)>,
139    /// 영상 이름 → 위치.
140    pub position: &'a (dyn Fn(&str) -> Option<usize> + Sync),
141}
142
143/// 후처리 전체(PLY 경로·사건 문자열 입력). 요약은 run.log(+표준 출력)로.
144pub fn run(inp: &PostInput, log: &Logger) -> Result<(), String> {
145    let ev = &inp.events;
146    let mut pv: BTreeMap<usize, PointCloud> = BTreeMap::new();
147    for (k, p) in inp.preview_ply {
148        pv.insert(*k, read_ply(p).map_err(|e| format!("{}: {e}", p.display()))?);
149    }
150    let mut rf: BTreeMap<usize, PointCloud> = BTreeMap::new();
151    for (k, p) in inp.refined_ply {
152        rf.insert(*k, read_ply(p).map_err(|e| format!("{}: {e}", p.display()))?);
153    }
154    let mut times: BTreeMap<usize, ZoneTimes> = BTreeMap::new();
155    for k in pv.keys() {
156        let t = times.entry(*k).or_default();
157        t.preview_ready = tof(ev, &format!("preview {k} ready"));
158        t.arrived = tof(ev, &format!("region {k} arrived"));
159    }
160    for k in rf.keys() {
161        let t = times.entry(*k).or_default();
162        t.refined_ready = tof(ev, &format!("refined {k} ready"));
163        t.refined_pose = tof(ev, &format!("refined {k} pose_ready"));
164    }
165    let windows = pv
166        .keys()
167        .map(|k| {
168            let start = k * inp.span;
169            (*k, (start.saturating_sub(inp.ovl), start + inp.ovl))
170        })
171        .collect();
172    let z = PostZones {
173        out: inp.out,
174        preview: pv.iter().map(|(k, c)| (*k, ZoneCloud { file: basename(&inp.preview_ply[k]), cloud: c })).collect(),
175        refined: rf.iter().map(|(k, c)| (*k, ZoneCloud { file: basename(&inp.refined_ply[k]), cloud: c })).collect(),
176        preview_models: inp.previews.iter().map(|(k, m)| (*k, m)).collect(),
177        refined_models: inp.refined.iter().map(|(k, m)| (*k, m)).collect(),
178        times,
179        windows,
180        position: inp.position,
181    };
182    run_zones(&z, log)
183}
184
185/// 후처리 본체(메모리 입력): 초벌 정렬 → aligned/ → 사건별 스냅샷·manifest → 재고정(final_frame/).
186pub fn run_zones(z: &PostZones, log: &Logger) -> Result<(), String> {
187    let out = z.out;
188    let pv: BTreeMap<usize, &PointCloud> = z.preview.iter().map(|(k, c)| (*k, c.cloud)).collect();
189    let rf: BTreeMap<usize, &PointCloud> = z.refined.iter().map(|(k, c)| (*k, c.cloud)).collect();
190    let tm = |k: &usize| z.times.get(k).copied().unwrap_or_default();
191    let t_p: BTreeMap<usize, Option<f64>> = pv.keys().map(|k| (*k, tm(k).preview_ready)).collect();
192    let t_r: BTreeMap<usize, Option<f64>> = rf.keys().map(|k| (*k, tm(k).refined_ready)).collect();
193    let t_rp: BTreeMap<usize, Option<f64>> = rf.keys().map(|k| (*k, tm(k).refined_pose)).collect();
194    let t_a: BTreeMap<usize, Option<f64>> = pv.keys().map(|k| (*k, tm(k).arrived)).collect();
195
196    // --- 초벌 정렬: 그 시점에 자세가 준비된 최신 정밀 모델(없으면 GPS 정렬된 preview_0)에 공유 점으로.
197    let mut align_rows: Vec<Json> = Vec::new();
198    let mut pframe: BTreeMap<usize, String> = BTreeMap::new();
199    let mut pv_aligned: BTreeMap<usize, PointCloud> = BTreeMap::new();
200    for (&k, cloud) in &pv {
201        let (s, med, nk, reference, t): (f64, f64, usize, String, Sim3);
202        if k == 0 {
203            (s, med, nk, reference, t) = (1.0, 0.0, 0, "GPS(preview_0)".into(), Sim3::identity());
204            pframe.insert(k, "preview_0".into());
205        } else {
206            let tpk = t_p[&k].unwrap_or(f64::INFINITY);
207            let cand = rf.keys().copied().filter(|j| t_rp.get(j).copied().flatten().is_some_and(|x| x <= tpk)).max();
208            let (b, refname, frame) = match cand.and_then(|j| z.refined_models.get(&j).copied().map(|m| (m, j))) {
209                Some((m, j)) => (Some(m), format!("refined_{j}"), format!("refined_{j}")),
210                None => (z.preview_models.get(&0).copied(), "preview_0(GPS)".to_string(), "preview_0".to_string()),
211            };
212            let a = z.preview_models.get(&k).copied();
213            let res = match (a, b) {
214                (Some(a), Some(b)) => {
215                    let (lo, hi) = z.windows.get(&k).copied().unwrap_or((0, 0));
216                    let pos = z.position;
217                    // 위치 함수는 'static 이 아니라서 구간 이름 집합을 미리 만든다.
218                    let names: BTreeSet<String> =
219                        a.images().map(|im| im.name.clone()).filter(|n| pos(n).is_some_and(|p| p >= lo && p < hi)).collect();
220                    let filt: NameFilter = Arc::new(move |n: &str| names.contains(n));
221                    let (mut pa, mut pb) = corr_points(a, b, Some(filt));
222                    if pa.len() < 20 {
223                        (pa, pb) = corr_points(a, b, None);
224                    }
225                    fit(&pa, &pb)
226                }
227                _ => None,
228            };
229            match res {
230                Some(f) => {
231                    (s, med, nk, reference, t) = (f.sim3.scale, f.median, f.inliers, refname, f.sim3);
232                    pframe.insert(k, frame);
233                }
234                None => {
235                    // 설계 결정: 대응이 없으면 실패로 끝내지 않고 정렬 없이 두고 표시한다.
236                    log.line(&format!("[post] 초벌 {k} 정렬 실패(공유 점 부족) — 원래 좌표 유지"));
237                    (s, med, nk, reference, t) = (1.0, 0.0, 0, format!("{refname}(실패)"), Sim3::identity());
238                    pframe.insert(k, frame);
239                }
240            }
241        }
242        align_rows.push(Json::obj(vec![
243            ("region", Json::Int(k as i64)),
244            ("reference", Json::Str(reference)),
245            ("pairs", Json::Int(nk as i64)),
246            ("fit_median_m", Json::f(med, 2)),
247            ("scale", Json::f(s, 3)),
248        ]));
249        let c = transformed(cloud, &t);
250        write(&out.join("aligned/preview").join(z.preview[&k].file.clone()), &c)?;
251        pv_aligned.insert(k, c);
252    }
253    for (k, c) in &rf {
254        write(&out.join("aligned/refined").join(z.refined[k].file.clone()), c)?;
255    }
256
257    // --- 사건 순서별 스냅샷.
258    let mut evs: Vec<(f64, &str, usize)> = Vec::new();
259    for k in pv.keys() {
260        evs.push((t_p[k].unwrap_or(f64::INFINITY), "preview", *k));
261    }
262    for k in rf.keys() {
263        evs.push((t_r[k].unwrap_or(f64::INFINITY), "refined", *k));
264    }
265    evs.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(b.1)).then(a.2.cmp(&b.2)));
266    let snap_opts = SnapshotOptions::default();
267    let snapdir = out.join("snapshots");
268    std::fs::create_dir_all(&snapdir).map_err(|e| e.to_string())?;
269    let mut shown_p: BTreeSet<usize> = BTreeSet::new();
270    let mut shown_r: BTreeSet<usize> = BTreeSet::new();
271    struct Ev {
272        i: usize,
273        t: f64,
274        kind: &'static str,
275        k: usize,
276        n: usize,
277        refined: Vec<usize>,
278        preview_only: Vec<usize>,
279    }
280    let mut man: Vec<Ev> = Vec::new();
281    let snap_name = |i: usize, t: f64, kind: &str, k: usize| snapdir.join(format!("event_{i:02}_{t:06.1}s_{kind}_{k}.ply"));
282    for (i, &(t, kind, k)) in evs.iter().enumerate() {
283        let i = i + 1;
284        if kind == "preview" {
285            shown_p.insert(k);
286        } else {
287            shown_r.insert(k);
288        }
289        let fine: Vec<&PointCloud> = shown_r.iter().map(|j| rf[j]).collect();
290        let po: Vec<usize> = shown_p.difference(&shown_r).copied().collect();
291        let coarse: Vec<&PointCloud> = po.iter().map(|j| &pv_aligned[j]).collect();
292        let a = compose_snapshot(&fine, &coarse, &snap_opts);
293        write(&snap_name(i, t, kind, k), &a)?;
294        let kind: &'static str = if kind == "preview" { "preview" } else { "refined" };
295        man.push(Ev { i, t, kind, k, n: a.len(), refined: shown_r.iter().copied().collect(), preview_only: po });
296    }
297    let r1 = |x: Option<f64>| Json::of(x, 1);
298    let rows: Vec<Json> = pv
299        .keys()
300        .map(|k| {
301            let ta = t_a[k].unwrap_or(0.0);
302            let tp = t_p[k].unwrap_or(0.0);
303            let trp = t_rp.get(k).copied().flatten();
304            let tr = t_r.get(k).copied().flatten();
305            Json::obj(vec![
306                ("region", Json::Int(*k as i64)),
307                ("arrived_s", Json::f(ta, 1)),
308                ("preview_ready_s", Json::f(tp, 1)),
309                ("preview_latency_s", Json::f(tp - ta, 1)),
310                ("refined_pose_s", r1(trp)),
311                ("refined_ready_s", r1(tr)),
312                ("refined_latency_s", r1(tr.map(|x| x - ta))),
313            ])
314        })
315        .collect();
316    let ev_json = |e: &Ev, frame: Option<&str>, n: usize| {
317        let mut v = vec![
318            ("event", Json::Int(e.i as i64)),
319            ("time_s", Json::f(e.t, 1)),
320            ("kind", Json::Str(e.kind.into())),
321            ("region", Json::Int(e.k as i64)),
322            ("points_1of6", Json::Int(n as i64)),
323            ("refined", Json::ints(&e.refined)),
324            ("preview_only", Json::ints(&e.preview_only)),
325        ];
326        if let Some(f) = frame {
327            v.push(("frame", Json::Str(f.into())));
328        }
329        Json::obj(v)
330    };
331    let manifest = |events: Vec<Json>, reanchor: Option<Json>| {
332        let mut v = vec![("timeline", Json::Arr(rows.clone())), ("events", Json::Arr(events)), ("align", Json::Arr(align_rows.clone()))];
333        if let Some(r) = reanchor {
334            v.push(("reanchor", r));
335        }
336        Json::obj(v)
337    };
338    let mpath = snapdir.join("manifest.json");
339    std::fs::write(&mpath, manifest(man.iter().map(|e| ev_json(e, None, e.n)).collect(), None).dump()).map_err(|e| e.to_string())?;
340    log.line("== 구역별 시간(초, 시작 기준)");
341    for r in &rows {
342        log.line(&r.py_repr());
343    }
344    log.line("== 정렬");
345    for a in &align_rows {
346        log.line(&a.py_repr());
347    }
348    log.line("== 사건 순서");
349    for m in &man {
350        log.line(&format!("{} {} {} {} {}", m.i, py_float(m.t, 1), m.kind, m.k, m.n));
351    }
352    let ks: Vec<usize> = rf.keys().copied().collect();
353    for w in ks.windows(2) {
354        match overlap_median(rf[&w[0]], rf[&w[1]]) {
355            Some((m, _)) => log.line(&format!("정밀 {}-{} 겹침 차 중앙 {m:.2}m", w[0], w[1])),
356            None => log.line(&format!("정밀 {}-{} 겹침 없음", w[0], w[1])),
357        }
358    }
359
360    // --- 재기준(incremental_reanchor.py): 새 정밀 모델이 나올 때마다 이미 보낸 구역을 그 좌표계로.
361    if rf.is_empty() {
362        return Ok(());
363    }
364    let mut models: BTreeMap<String, &Reconstruction> = BTreeMap::new();
365    if let Some(m) = z.preview_models.get(&0).copied() {
366        models.insert("preview_0".into(), m);
367    }
368    for (k, m) in &z.refined_models {
369        models.insert(format!("refined_{k}"), m);
370    }
371    let mut cache: HashMap<(String, String), Sim3> = HashMap::new();
372    let mut order: Vec<(String, String)> = Vec::new();
373    let mut tf = |a: &str, b: &str| -> Sim3 {
374        if a == b {
375            return Sim3::identity();
376        }
377        let key = (a.to_string(), b.to_string());
378        if let Some(t) = cache.get(&key) {
379            return *t;
380        }
381        let t = match (models.get(a), models.get(b)) {
382            (Some(ma), Some(mb)) => {
383                let (pa, pb) = corr_points(ma, mb, None);
384                match fit(&pa, &pb) {
385                    Some(f) => {
386                        log.line(&format!(
387                            "{a} -> {b}: 점쌍 {}, 잔차 중앙 {:.3}m, 축척 {:.4}, 이동 {:.2}m",
388                            f.inliers,
389                            f.median,
390                            f.sim3.scale,
391                            f.sim3.translation.norm()
392                        ));
393                        f.sim3
394                    }
395                    None => {
396                        log.line(&format!("{a} -> {b}: 점쌍 부족({}), 항등 사용", pa.len()));
397                        Sim3::identity()
398                    }
399                }
400            }
401            _ => Sim3::identity(),
402        };
403        cache.insert(key.clone(), t);
404        order.push(key);
405        t
406    };
407    for f in std::fs::read_dir(&snapdir).map_err(|e| e.to_string())?.flatten() {
408        let n = f.file_name().to_string_lossy().to_string();
409        if n.starts_with("event_") && n.ends_with(".ply") {
410            let _ = std::fs::remove_file(f.path());
411        }
412    }
413    let mut cur = "preview_0".to_string();
414    let mut ev2: Vec<Json> = Vec::new();
415    for e in &man {
416        if e.kind == "refined" {
417            cur = format!("refined_{}", e.k);
418        }
419        let rs: Vec<PointCloud> = e.refined.iter().map(|j| transformed(rf[j], &tf(&format!("refined_{j}"), &cur))).collect();
420        let ps: Vec<PointCloud> = e
421            .preview_only
422            .iter()
423            .map(|j| transformed(&pv_aligned[j], &tf(pframe.get(j).map(|s| s.as_str()).unwrap_or("preview_0"), &cur)))
424            .collect();
425        let a = compose_snapshot(&rs.iter().collect::<Vec<_>>(), &ps.iter().collect::<Vec<_>>(), &snap_opts);
426        write(&snap_name(e.i, e.t, e.kind, e.k), &a)?;
427        log.line(&format!("{} {} {} {} 좌표계 {cur} 점 {}", e.i, py_float(e.t, 1), e.kind, e.k, a.len()));
428        ev2.push(ev_json(e, Some(&cur), a.len()));
429    }
430    let last = format!("refined_{}", rf.keys().max().copied().unwrap_or(0));
431    let mut final_rf: BTreeMap<usize, PointCloud> = BTreeMap::new();
432    for (k, c) in &rf {
433        let c2 = transformed(c, &tf(&format!("refined_{k}"), &last));
434        write(&out.join("final_frame/refined").join(z.refined[k].file.clone()), &c2)?;
435        final_rf.insert(*k, c2);
436    }
437    for (k, c) in &pv_aligned {
438        let fr = pframe.get(k).cloned().unwrap_or_else(|| "preview_0".into());
439        write(&out.join("final_frame/preview").join(z.preview[k].file.clone()), &transformed(c, &tf(&fr, &last)))?;
440    }
441    for w in ks.windows(2) {
442        match overlap_median(&final_rf[&w[0]], &final_rf[&w[1]]) {
443            Some((m, n)) => log.line(&format!("최종 좌표계 정밀 {}-{} 겹침 차 중앙 {m:.2}m (겹침 점 {n})", w[0], w[1])),
444            None => log.line(&format!("최종 좌표계 정밀 {}-{} 겹침 없음", w[0], w[1])),
445        }
446    }
447    let re = Json::Obj(
448        order
449            .iter()
450            .map(|(a, b)| {
451                let t = cache[&(a.clone(), b.clone())];
452                (format!("{a}->{b}"), Json::obj(vec![("scale", Json::f(t.scale, 4)), ("shift_m", Json::f(t.translation.norm(), 2))]))
453            })
454            .collect(),
455    );
456    std::fs::write(&mpath, manifest(ev2, Some(re)).dump()).map_err(|e| e.to_string())?;
457    Ok(())
458}