1use 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
14pub struct PostInput<'a> {
16 pub out: &'a Path,
18 pub span: usize,
20 pub ovl: usize,
22 pub events: Vec<(f64, String)>,
24 pub previews: &'a BTreeMap<usize, Reconstruction>,
26 pub refined: &'a BTreeMap<usize, Reconstruction>,
28 pub preview_ply: &'a BTreeMap<usize, PathBuf>,
30 pub refined_ply: &'a BTreeMap<usize, PathBuf>,
32 pub position: &'a (dyn Fn(&str) -> Option<usize> + Sync),
34}
35
36fn 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
41struct Fit {
43 sim3: Sim3,
44 median: f64,
45 inliers: usize,
46}
47
48fn 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
85fn 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#[derive(Clone, Copy, Debug, Default, PartialEq)]
104pub struct ZoneTimes {
105 pub arrived: Option<f64>,
107 pub preview_ready: Option<f64>,
109 pub refined_pose: Option<f64>,
111 pub refined_ready: Option<f64>,
113}
114
115pub struct ZoneCloud<'a> {
117 pub file: String,
119 pub cloud: &'a PointCloud,
121}
122
123pub struct PostZones<'a> {
125 pub out: &'a Path,
127 pub preview: BTreeMap<usize, ZoneCloud<'a>>,
129 pub refined: BTreeMap<usize, ZoneCloud<'a>>,
131 pub preview_models: BTreeMap<usize, &'a Reconstruction>,
133 pub refined_models: BTreeMap<usize, &'a Reconstruction>,
135 pub times: BTreeMap<usize, ZoneTimes>,
137 pub windows: BTreeMap<usize, (usize, usize)>,
139 pub position: &'a (dyn Fn(&str) -> Option<usize> + Sync),
141}
142
143pub 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
185pub 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 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 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 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 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 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}