Skip to main content

cumulus3d_align/
cloud.rs

1//! 점군 후처리: Sim3 변환, 정밀 점군 근접 마스킹, 1/N 추출, 스냅샷 합성.
2
3use crate::kdtree::KdTree;
4use rayon::prelude::*;
5use cumulus3d_core::io::PointCloud;
6use cumulus3d_core::{Sim3, Vec3};
7
8/// 점과 법선에 Sim3 적용(점: sRx+t, 법선: Rn). 계산은 f64.
9pub fn transform_cloud(cloud: &mut PointCloud, t: &Sim3) {
10    let r = t.rotation.to_rotation_matrix();
11    let sr = r * t.scale;
12    cloud.positions.par_iter_mut().for_each(|p| {
13        let v = sr * Vec3::new(p[0] as f64, p[1] as f64, p[2] as f64) + t.translation;
14        *p = [v.x as f32, v.y as f32, v.z as f32];
15    });
16    cloud.normals.par_iter_mut().for_each(|n| {
17        let v = r * Vec3::new(n[0] as f64, n[1] as f64, n[2] as f64);
18        *n = [v.x as f32, v.y as f32, v.z as f32];
19    });
20}
21
22/// `mask[i]` 가 true 인 점만 남긴 새 점군.
23pub fn select(cloud: &PointCloud, keep: &[bool]) -> PointCloud {
24    let pick = |v: &Vec<[f32; 3]>| -> Vec<[f32; 3]> {
25        if v.is_empty() {
26            Vec::new()
27        } else {
28            v.iter().zip(keep).filter(|(_, k)| **k).map(|(x, _)| *x).collect()
29        }
30    };
31    PointCloud {
32        positions: pick(&cloud.positions),
33        normals: pick(&cloud.normals),
34        colors: if cloud.colors.is_empty() {
35            Vec::new()
36        } else {
37            cloud.colors.iter().zip(keep).filter(|(_, k)| **k).map(|(c, _)| *c).collect()
38        },
39    }
40}
41
42/// `reference` 와 거리 ≤ radius 인 점을 뺀 점군(정밀 점과 겹치는 초벌 점 제거).
43pub fn remove_near(cloud: &PointCloud, reference: &KdTree, radius: f64) -> PointCloud {
44    let near = reference.any_within_many_f32(&cloud.positions, radius);
45    let keep: Vec<bool> = near.iter().map(|b| !b).collect();
46    select(cloud, &keep)
47}
48
49/// 1/stride 간격 추출(인덱스 0, stride, 2·stride, …). stride ≤ 1 이면 그대로.
50pub fn decimate(cloud: &PointCloud, stride: usize) -> PointCloud {
51    if stride <= 1 {
52        return cloud.clone();
53    }
54    let keep: Vec<bool> = (0..cloud.len()).map(|i| i % stride == 0).collect();
55    select(cloud, &keep)
56}
57
58/// 이어 붙이기. 일부만 법선/색이 있으면 없는 쪽을 (0,0,0) 법선 / 흰색으로 채운다.
59pub fn merge_clouds(clouds: &[&PointCloud]) -> PointCloud {
60    let total: usize = clouds.iter().map(|c| c.len()).sum();
61    let any_n = clouds.iter().any(|c| c.has_normals());
62    let any_c = clouds.iter().any(|c| c.has_colors());
63    let mut out = PointCloud {
64        positions: Vec::with_capacity(total),
65        normals: Vec::with_capacity(if any_n { total } else { 0 }),
66        colors: Vec::with_capacity(if any_c { total } else { 0 }),
67    };
68    for c in clouds {
69        out.positions.extend_from_slice(&c.positions);
70        if any_n {
71            if c.has_normals() {
72                out.normals.extend_from_slice(&c.normals);
73            } else {
74                out.normals.extend(std::iter::repeat_n([0.0f32; 3], c.len()));
75            }
76        }
77        if any_c {
78            if c.has_colors() {
79                out.colors.extend_from_slice(&c.colors);
80            } else {
81                out.colors.extend(std::iter::repeat_n([255u8; 3], c.len()));
82            }
83        }
84    }
85    out
86}
87
88/// 스냅샷 합성 옵션.
89#[derive(Clone, Debug, PartialEq)]
90pub struct SnapshotOptions {
91    /// 정밀 점과 이 거리 이내의 초벌 점 제거 (m).
92    pub mask_radius: f64,
93    /// 최종 1/N 추출.
94    pub stride: usize,
95}
96
97impl Default for SnapshotOptions {
98    fn default() -> Self {
99        Self { mask_radius: 1.5, stride: 6 }
100    }
101}
102
103/// 스냅샷 = 정밀 구역 전부 + (정밀 점과 겹치지 않는) 초벌 구역들, 그 뒤 1/N 추출.
104/// 모든 입력은 이미 같은 좌표계에 있어야 한다.
105pub fn compose_snapshot(fine: &[&PointCloud], coarse: &[&PointCloud], opts: &SnapshotOptions) -> PointCloud {
106    let fine_all = merge_clouds(fine);
107    let masked: Vec<PointCloud> = if fine_all.is_empty() {
108        coarse.iter().map(|c| (*c).clone()).collect()
109    } else {
110        let tree = KdTree::from_f32(&fine_all.positions);
111        coarse.iter().map(|c| remove_near(c, &tree, opts.mask_radius)).collect()
112    };
113    let mut parts: Vec<&PointCloud> = vec![&fine_all];
114    parts.extend(masked.iter());
115    // 설계 결정: 1/N 추출 시점. 마스킹 후 합친 전체에 한 번 적용한다.
116    decimate(&merge_clouds(&parts), opts.stride)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use cumulus3d_core::Quat;
123
124    fn grid(n: usize, step: f32, z: f32) -> PointCloud {
125        let mut c = PointCloud::default();
126        for i in 0..n {
127            for j in 0..n {
128                c.positions.push([i as f32 * step, j as f32 * step, z]);
129                c.normals.push([0.0, 0.0, 1.0]);
130                c.colors.push([i as u8, j as u8, 0]);
131            }
132        }
133        c
134    }
135
136    #[test]
137    fn mask_within_1_5m() {
138        // 정밀: x ∈ [0, 10] 격자(z=0). 초벌: z 높이별 점 — 1.5 m 이내만 제거.
139        let fine = grid(11, 1.0, 0.0);
140        let mut coarse = PointCloud::default();
141        for z in [0.5f32, 1.4, 1.5, 1.6, 3.0] {
142            coarse.positions.push([5.0, 5.0, z]);
143        }
144        coarse.positions.push([20.0, 5.0, 0.0]); // 수평으로 멀리
145        coarse.positions.push([11.4, 5.0, 0.0]); // 가장자리에서 1.4 m
146        let tree = KdTree::from_f32(&fine.positions);
147        let out = remove_near(&coarse, &tree, 1.5);
148        assert_eq!(out.positions, vec![[5.0, 5.0, 1.6], [5.0, 5.0, 3.0], [20.0, 5.0, 0.0]]);
149    }
150
151    #[test]
152    fn transform_points_and_normals() {
153        let mut c = grid(3, 1.0, 2.0);
154        let t = Sim3::new(2.0, Quat::from_axis_angle(&Vec3::x(), std::f64::consts::FRAC_PI_2), Vec3::new(1.0, 0.0, 0.0));
155        let orig = c.clone();
156        transform_cloud(&mut c, &t);
157        for (a, b) in orig.positions.iter().zip(&c.positions) {
158            let e = t.transform_point(&Vec3::new(a[0] as f64, a[1] as f64, a[2] as f64));
159            assert!((e - Vec3::new(b[0] as f64, b[1] as f64, b[2] as f64)).norm() < 1e-5);
160        }
161        // z 법선 → x 축 90° 회전 → −y.
162        let n = c.normals[0];
163        assert!((n[1] + 1.0).abs() < 1e-6 && n[2].abs() < 1e-6);
164    }
165
166    #[test]
167    fn decimate_and_snapshot() {
168        let c = grid(4, 1.0, 0.0);
169        assert_eq!(decimate(&c, 6).len(), 3); // 16 → 0, 6, 12
170        let fine = grid(5, 1.0, 0.0);
171        let mut coarse = grid(10, 1.0, 0.0); // 0..9; x,y ≤ 6 인 점은 정밀(0..4)과 1.5 m 이내
172        coarse.normals.clear();
173        let snap = compose_snapshot(&[&fine], &[&coarse], &SnapshotOptions { mask_radius: 1.5, stride: 1 });
174        let kept_coarse = coarse
175            .positions
176            .iter()
177            .filter(|p| {
178                let dx = (p[0] - 4.0).max(0.0);
179                let dy = (p[1] - 4.0).max(0.0);
180                dx * dx + dy * dy > 2.25
181            })
182            .count();
183        assert_eq!(snap.len(), 25 + kept_coarse);
184        assert_eq!(snap.normals.len(), snap.len());
185        assert_eq!(snap.normals[snap.len() - 1], [0.0; 3]);
186    }
187}