use std::collections::HashMap;
use crate::error::{Result, VisionError};
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct VoxelConfig {
pub voxel_size: [f64; 3],
pub point_cloud_range: [f64; 6],
pub max_points_per_voxel: usize,
pub max_voxels: usize,
}
impl Default for VoxelConfig {
fn default() -> Self {
Self {
voxel_size: [0.1, 0.1, 0.2],
point_cloud_range: [-50.0, -50.0, -5.0, 50.0, 50.0, 3.0],
max_points_per_voxel: 32,
max_voxels: 20_000,
}
}
}
#[derive(Debug, Clone)]
pub struct Voxel {
pub indices: [usize; 3],
pub points: Vec<[f64; 3]>,
pub mean_point: [f64; 3],
pub n_points: usize,
}
#[derive(Debug, Clone)]
pub struct VoxelizationResult {
pub voxels: Vec<Voxel>,
pub n_voxels: usize,
pub grid_shape: [usize; 3],
}
pub fn voxelize(points: &[[f64; 3]], config: &VoxelConfig) -> Result<VoxelizationResult> {
let [vx, vy, vz] = config.voxel_size;
if vx <= 0.0 || vy <= 0.0 || vz <= 0.0 {
return Err(VisionError::InvalidParameter(
"voxel_size dimensions must be positive".into(),
));
}
let [x_min, y_min, z_min, x_max, y_max, z_max] = config.point_cloud_range;
let nx = ((x_max - x_min) / vx).ceil() as usize;
let ny = ((y_max - y_min) / vy).ceil() as usize;
let nz = ((z_max - z_min) / vz).ceil() as usize;
if nx == 0 || ny == 0 || nz == 0 {
return Err(VisionError::InvalidParameter(
"point_cloud_range produces a zero-dimension grid".into(),
));
}
let mut map: HashMap<[usize; 3], Vec<[f64; 3]>> = HashMap::new();
for &[x, y, z] in points {
if x < x_min || x >= x_max || y < y_min || y >= y_max || z < z_min || z >= z_max {
continue;
}
let ix = ((x - x_min) / vx).floor() as usize;
let iy = ((y - y_min) / vy).floor() as usize;
let iz = ((z - z_min) / vz).floor() as usize;
if ix >= nx || iy >= ny || iz >= nz {
continue;
}
let entry = map.entry([ix, iy, iz]).or_default();
if entry.len() < config.max_points_per_voxel {
entry.push([x, y, z]);
}
}
let mut voxel_keys: Vec<[usize; 3]> = map.keys().copied().collect();
voxel_keys.sort_unstable();
let n_kept = voxel_keys.len().min(config.max_voxels);
voxel_keys.truncate(n_kept);
let mut voxels = Vec::with_capacity(n_kept);
for key in voxel_keys {
let pts = map.remove(&key).unwrap_or_default();
let n = pts.len();
let mean = centroid(&pts);
voxels.push(Voxel {
indices: key,
points: pts,
mean_point: mean,
n_points: n,
});
}
let n_voxels = voxels.len();
Ok(VoxelizationResult {
voxels,
n_voxels,
grid_shape: [nx, ny, nz],
})
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct PillarConfig {
pub voxel_cfg: VoxelConfig,
pub max_points_per_pillar: usize,
pub n_features: usize,
}
impl Default for PillarConfig {
fn default() -> Self {
Self {
voxel_cfg: VoxelConfig::default(),
max_points_per_pillar: 32,
n_features: 9,
}
}
}
#[derive(Debug, Clone)]
pub struct PillarFeatures {
pub features: Vec<Vec<f64>>,
pub coords: Vec<[usize; 2]>,
}
pub fn extract_pillar_features(
points: &[[f64; 4]],
config: &PillarConfig,
) -> Result<PillarFeatures> {
let vcfg = &config.voxel_cfg;
let [vx, vy, _vz] = vcfg.voxel_size;
if vx <= 0.0 || vy <= 0.0 {
return Err(VisionError::InvalidParameter(
"voxel_size x/y must be positive".into(),
));
}
let [x_min, y_min, z_min, x_max, y_max, z_max] = vcfg.point_cloud_range;
let nx = ((x_max - x_min) / vx).ceil() as usize;
let ny = ((y_max - y_min) / vy).ceil() as usize;
if nx == 0 || ny == 0 {
return Err(VisionError::InvalidParameter(
"point_cloud_range produces a zero-dimension grid".into(),
));
}
let mut pillar_map: HashMap<[usize; 2], Vec<[f64; 4]>> = HashMap::new();
for &[x, y, z, r] in points {
if x < x_min || x >= x_max || y < y_min || y >= y_max || z < z_min || z >= z_max {
continue;
}
let ix = ((x - x_min) / vx).floor() as usize;
let iy = ((y - y_min) / vy).floor() as usize;
if ix >= nx || iy >= ny {
continue;
}
let entry = pillar_map.entry([ix, iy]).or_default();
if entry.len() < config.max_points_per_pillar {
entry.push([x, y, z, r]);
}
}
let mut keys: Vec<[usize; 2]> = pillar_map.keys().copied().collect();
keys.sort_unstable();
let n_kept = keys.len().min(vcfg.max_voxels);
keys.truncate(n_kept);
let feat_len = config.max_points_per_pillar * config.n_features;
let mut features: Vec<Vec<f64>> = Vec::with_capacity(n_kept);
let mut coords: Vec<[usize; 2]> = Vec::with_capacity(n_kept);
for key in keys {
let pts = pillar_map.remove(&key).unwrap_or_default();
let n = pts.len() as f64;
let (sum_x, sum_y, sum_z) = pts.iter().fold((0.0, 0.0, 0.0), |acc, p| {
(acc.0 + p[0], acc.1 + p[1], acc.2 + p[2])
});
let (cx, cy, cz) = if n > 0.0 {
(sum_x / n, sum_y / n, sum_z / n)
} else {
(0.0, 0.0, 0.0)
};
let [ix, iy] = key;
let px = x_min + (ix as f64 + 0.5) * vx;
let py = y_min + (iy as f64 + 0.5) * vy;
let mut row = vec![0.0_f64; feat_len];
for (i, &[x, y, z, r]) in pts.iter().enumerate() {
let base = i * config.n_features;
if base + config.n_features > row.len() {
break;
}
row[base] = x;
row[base + 1] = y;
row[base + 2] = z;
row[base + 3] = r;
row[base + 4] = x - cx;
row[base + 5] = y - cy;
row[base + 6] = z - cz;
row[base + 7] = x - px;
row[base + 8] = y - py;
}
features.push(row);
coords.push(key);
}
Ok(PillarFeatures { features, coords })
}
pub fn estimate_normals(points: &[[f64; 3]], k_neighbors: usize) -> Result<Vec<[f64; 3]>> {
if k_neighbors < 3 {
return Err(VisionError::InvalidParameter(
"k_neighbors must be at least 3 to define a plane".into(),
));
}
let n = points.len();
if n < k_neighbors {
return Err(VisionError::InvalidParameter(format!(
"Not enough points ({n}) for k_neighbors={k_neighbors}"
)));
}
let mut normals = Vec::with_capacity(n);
for i in 0..n {
let pi = points[i];
let mut dists: Vec<(f64, usize)> = points
.iter()
.enumerate()
.map(|(j, &pj)| {
let dx = pi[0] - pj[0];
let dy = pi[1] - pj[1];
let dz = pi[2] - pj[2];
(dx * dx + dy * dy + dz * dz, j)
})
.collect();
dists.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let neighbors: Vec<[f64; 3]> = dists[..k_neighbors]
.iter()
.map(|&(_, j)| points[j])
.collect();
let mean = centroid(&neighbors);
let mut cov = [[0.0_f64; 3]; 3];
for &nb in &neighbors {
let d = [nb[0] - mean[0], nb[1] - mean[1], nb[2] - mean[2]];
for r in 0..3 {
for c in 0..3 {
cov[r][c] += d[r] * d[c];
}
}
}
let scale = 1.0 / neighbors.len() as f64;
for cov_row in &mut cov {
for cov_val in cov_row.iter_mut() {
*cov_val *= scale;
}
}
let (eigenvalues, eigenvectors) = jacobi_eigen_3x3(cov)?;
let min_idx = eigenvalues
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
let n_vec = eigenvectors[min_idx];
let len = (n_vec[0] * n_vec[0] + n_vec[1] * n_vec[1] + n_vec[2] * n_vec[2]).sqrt();
let normal = if len > 1e-12 {
[n_vec[0] / len, n_vec[1] / len, n_vec[2] / len]
} else {
[0.0, 0.0, 1.0]
};
normals.push(normal);
}
Ok(normals)
}
fn centroid(pts: &[[f64; 3]]) -> [f64; 3] {
if pts.is_empty() {
return [0.0, 0.0, 0.0];
}
let n = pts.len() as f64;
let (sx, sy, sz) = pts.iter().fold((0.0, 0.0, 0.0), |acc, &p| {
(acc.0 + p[0], acc.1 + p[1], acc.2 + p[2])
});
[sx / n, sy / n, sz / n]
}
fn jacobi_eigen_3x3(a_in: [[f64; 3]; 3]) -> Result<([f64; 3], [[f64; 3]; 3])> {
let mut a = a_in;
let mut v = [[0.0_f64; 3]; 3];
for (i, v_row) in v.iter_mut().enumerate() {
v_row[i] = 1.0;
}
const MAX_ITER: usize = 100;
const EPS: f64 = 1e-12;
for _ in 0..MAX_ITER {
let mut max_val = 0.0_f64;
let mut p = 0;
let mut q = 1;
for (r, a_row) in a.iter().enumerate() {
for (c, &a_val) in a_row.iter().enumerate().skip(r + 1) {
if a_val.abs() > max_val {
max_val = a_val.abs();
p = r;
q = c;
}
}
}
if max_val < EPS {
break;
}
let theta = if (a[q][q] - a[p][p]).abs() < EPS {
std::f64::consts::FRAC_PI_4
} else {
0.5 * ((2.0 * a[p][q]) / (a[q][q] - a[p][p])).atan()
};
let (s, cs) = theta.sin_cos();
let mut a2 = a;
a2[p][p] = cs * cs * a[p][p] - 2.0 * s * cs * a[p][q] + s * s * a[q][q];
a2[q][q] = s * s * a[p][p] + 2.0 * s * cs * a[p][q] + cs * cs * a[q][q];
a2[p][q] = 0.0;
a2[q][p] = 0.0;
let third = 3 - p - q; let r = third;
a2[p][r] = cs * a[p][r] - s * a[q][r];
a2[r][p] = a2[p][r];
a2[q][r] = s * a[p][r] + cs * a[q][r];
a2[r][q] = a2[q][r];
a = a2;
for v_row in &mut v {
let vi_p = v_row[p];
let vi_q = v_row[q];
v_row[p] = cs * vi_p - s * vi_q;
v_row[q] = s * vi_p + cs * vi_q;
}
}
Ok(([a[0][0], a[1][1], a[2][2]], v))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_uniform_cloud(n: usize) -> Vec<[f64; 3]> {
(0..n)
.map(|i| {
let t = i as f64;
[t * 0.5 - 10.0, t * 0.3 - 5.0, t * 0.1 - 1.0]
})
.collect()
}
#[test]
fn test_voxel_config_defaults() {
let cfg = VoxelConfig::default();
assert_eq!(cfg.voxel_size, [0.1, 0.1, 0.2]);
assert_eq!(cfg.max_points_per_voxel, 32);
assert_eq!(cfg.max_voxels, 20_000);
}
#[test]
fn test_pillar_config_defaults() {
let cfg = PillarConfig::default();
assert_eq!(cfg.max_points_per_pillar, 32);
assert_eq!(cfg.n_features, 9);
}
#[test]
fn test_voxelization_basic() {
let pts: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[0.05, 0.05, 0.05], [1.0, 0.0, 0.0], ];
let cfg = VoxelConfig::default();
let res = voxelize(&pts, &cfg).expect("voxelize should succeed");
assert_eq!(res.n_voxels, 2);
}
#[test]
fn test_voxelization_filters_out_of_range() {
let pts: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0], [100.0, 0.0, 0.0], ];
let cfg = VoxelConfig::default();
let res = voxelize(&pts, &cfg).expect("voxelize should succeed");
assert_eq!(res.n_voxels, 1);
}
#[test]
fn test_voxelization_max_voxels_cap() {
let n = 500;
let pts: Vec<[f64; 3]> = (0..n).map(|i| [i as f64 * 0.2 - 10.0, 0.0, 0.0]).collect();
let cfg = VoxelConfig {
max_voxels: 10,
..Default::default()
};
let res = voxelize(&pts, &cfg).expect("voxelize should succeed");
assert!(res.n_voxels <= 10);
}
#[test]
fn test_voxelization_max_points_per_voxel() {
let pts: Vec<[f64; 3]> = (0..100).map(|_| [0.0, 0.0, 0.0]).collect();
let cfg = VoxelConfig {
max_points_per_voxel: 5,
..Default::default()
};
let res = voxelize(&pts, &cfg).expect("voxelize should succeed");
assert_eq!(res.n_voxels, 1);
assert_eq!(res.voxels[0].n_points, 5);
}
#[test]
fn test_pillar_feature_shape() {
let pts: Vec<[f64; 4]> = (0..50)
.map(|i| {
let t = i as f64 * 0.3 - 5.0;
[t, t * 0.5, 0.0, 0.8]
})
.collect();
let cfg = PillarConfig::default();
let pf = extract_pillar_features(&pts, &cfg).expect("pillar extraction should succeed");
let expected_len = cfg.max_points_per_pillar * cfg.n_features;
for row in &pf.features {
assert_eq!(row.len(), expected_len);
}
assert_eq!(pf.features.len(), pf.coords.len());
}
#[test]
fn test_pillar_n_pillars_bounded() {
let pts: Vec<[f64; 4]> = (0..1000)
.map(|i| {
let x = (i % 50) as f64 * 0.5 - 12.0;
let y = (i / 50) as f64 * 0.5 - 5.0;
[x, y, 0.0, 1.0]
})
.collect();
let cfg = PillarConfig {
voxel_cfg: VoxelConfig {
max_voxels: 50,
..Default::default()
},
..Default::default()
};
let pf = extract_pillar_features(&pts, &cfg).expect("pillar extraction should succeed");
assert!(pf.features.len() <= 50);
}
#[test]
fn test_normals_unit_length() {
let pts: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
[0.5, 0.5, 0.0],
[0.2, 0.8, 0.0],
];
let normals = estimate_normals(&pts, 4).expect("normal estimation should succeed");
assert_eq!(normals.len(), pts.len());
for n in &normals {
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!((len - 1.0).abs() < 1e-9, "normal length = {len}");
}
}
#[test]
fn test_normals_z_axis() {
let pts: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
[0.5, 0.5, 0.0],
];
let normals = estimate_normals(&pts, 4).expect("normal estimation should succeed");
for n in &normals {
assert!(n[2].abs() > 0.9, "expected Z-dominant normal, got {n:?}");
}
}
#[test]
fn test_voxel_grid_shape() {
let cfg = VoxelConfig {
voxel_size: [1.0, 1.0, 1.0],
point_cloud_range: [0.0, 0.0, 0.0, 10.0, 10.0, 5.0],
..Default::default()
};
let pts: Vec<[f64; 3]> = vec![[5.0, 5.0, 2.0]];
let res = voxelize(&pts, &cfg).expect("voxelize should succeed");
assert_eq!(res.grid_shape, [10, 10, 5]);
}
#[test]
fn test_centroid_mean_point() {
let pts: Vec<[f64; 3]> = vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 2.0, 0.0]];
let cfg = VoxelConfig {
voxel_size: [5.0, 5.0, 5.0],
point_cloud_range: [-5.0, -5.0, -5.0, 5.0, 5.0, 5.0],
..Default::default()
};
let res = voxelize(&pts, &cfg).expect("voxelize should succeed");
assert_eq!(res.n_voxels, 1);
let mean = res.voxels[0].mean_point;
assert!((mean[0] - 1.0).abs() < 1e-9);
assert!((mean[1] - 2.0 / 3.0).abs() < 1e-9);
}
}