use std::path::PathBuf;
use terrio::crs::SpatialRef;
use terrio::pointcloud::filters::{
self, BandDimension, CovarianceFeaturesOptions, DbscanOptions, DimRange, EigenvaluesOptions, FeatureMode, Neighbourhood,
NnDistanceMode, OverlayOptions,
};
use terrio::pointcloud::{self, Dimension, PointCloud, Values};
use terrio::{FieldDef, FieldType, FieldValue, Geometry, Layer, Raster, RasterConfig, RasterDataset};
fn data(name: &str) -> Option<String> {
let dir = PathBuf::from(std::env::var_os("TERRIO_TEST_DATA")?);
let p = dir.join("pointcloud").join(name);
assert!(p.exists(), "{} is missing from TERRIO_TEST_DATA", p.display());
Some(p.to_string_lossy().into_owned())
}
fn points(xyz: &[[f64; 3]]) -> PointCloud {
let col = |k: usize| Values::F64(xyz.iter().map(|p| p[k]).collect());
pointcloud::from_dimensions(
vec![
Dimension::new("x", col(0)),
Dimension::new("y", col(1)),
Dimension::new("z", col(2)),
Dimension::new("intensity", Values::U16((0..xyz.len() as u16).collect())),
Dimension::new("classification", Values::U8(vec![1; xyz.len()])),
],
SpatialRef::default(),
)
}
fn plane(n: usize) -> Vec<[f64; 3]> {
(0..n * n)
.map(|i| {
let (x, y) = ((i % n) as f64, (i / n) as f64);
[x, y, 0.1 * x + 0.2 * y]
})
.collect()
}
fn col(c: &PointCloud, name: &str) -> Vec<f64> {
c.dim(name).unwrap_or_else(|| panic!("no {name}")).to_f64()
}
#[test]
fn clusters_by_density() {
let mut xyz: Vec<[f64; 3]> = (0..10).map(|i| [i as f64 * 0.5, 0.0, 0.0]).collect();
xyz.push([20.0, 0.0, 0.0]);
xyz.extend((0..10).map(|i| [40.0 + i as f64 * 0.5, 0.0, 0.0]));
let c = filters::dbscan(&points(&xyz), &DbscanOptions { min_points: 3, ..Default::default() }).unwrap();
let ids = c.dim("cluster_id").unwrap();
assert!(matches!(ids.values, Values::I64(_)));
assert_eq!(ids.to_f64(), [vec![0.0; 10], vec![-1.0], vec![1.0; 10]].concat());
let o = DbscanOptions { min_points: 30, dimensions: vec!["X".into()], ..Default::default() };
assert!(col(&filters::dbscan(&points(&xyz), &o).unwrap(), "cluster_id").iter().all(|v| *v == -1.0));
let o = DbscanOptions { dimensions: vec!["Nope".into()], ..Default::default() };
assert!(filters::dbscan(&points(&xyz), &o).is_err());
}
#[test]
fn computes_covariance_features() {
let c = points(&plane(20));
let o = CovarianceFeaturesOptions { feature_set: vec!["all".into(), "Density".into()], ..Default::default() };
assert!(filters::covariance_features(&c, &o).is_err(), "Density needs OptimalKNN and OptimalRadius");
let o = CovarianceFeaturesOptions {
feature_set: vec!["Linearity".into(), "planarity".into(), " SCATTERING ".into(), "DemantkeVerticality".into(), "none".into()],
mode: FeatureMode::Raw,
..Default::default()
};
let f = filters::covariance_features(&c, &o).unwrap();
assert!(f.dim("verticality").is_none() && f.dim("none").is_none());
let interior = |i: usize| (3..17).contains(&(i % 20)) && (3..17).contains(&(i / 20));
let (scattering, demantke) = (col(&f, "scattering"), col(&f, "demantke_verticality"));
let normal_z = 1.0 / (1.0f64 + 0.01 + 0.04).sqrt();
for i in (0..400).filter(|&i| interior(i)) {
assert!(scattering[i].abs() < 1e-9, "point {i}: {}", scattering[i]);
assert!((demantke[i] - (1.0 - normal_z)).abs() < 1e-7, "point {i}: {}", demantke[i]);
}
let line: Vec<[f64; 3]> = (0..50).map(|i| [i as f64, 2.0 * i as f64, 0.5 * i as f64]).collect();
let f = filters::covariance_features(&points(&line), &CovarianceFeaturesOptions::default()).unwrap();
assert!(col(&f, "linearity").iter().all(|v| (v - 1.0).abs() < 1e-6));
assert!(FeatureMode::parse("SQRT").is_ok() && FeatureMode::parse("cube").is_err());
}
#[test]
fn computes_eigenvalues_and_ranks() {
let c = points(&plane(15));
let o = EigenvaluesOptions { normalize: true, ..Default::default() };
let e = filters::eigenvalues(&c, &o).unwrap();
let sums: Vec<f64> = (0..c.len()).map(|i| (0..3).map(|k| e.dim(&format!("eigenvalue{k}")).unwrap().get(i)).sum()).collect();
assert!(sums.iter().all(|s| (s - 1.0).abs() < 1e-12));
assert!(col(&e, "eigenvalue0").iter().all(|v| v.abs() < 1e-9));
let o = EigenvaluesOptions { neighbours: Neighbourhood { knn: 8, stride: 1, radius: Some(0.5), min_k: 3 }, normalize: false };
assert!(col(&filters::eigenvalues(&c, &o).unwrap(), "eigenvalue2").iter().all(|v| *v == 0.0));
assert!(col(&filters::estimate_rank(&c, 8, 0.01).unwrap(), "rank").iter().all(|v| *v == 2.0));
let coplanar = col(&filters::approximate_coplanar(&c, 8, 25.0, 6.0).unwrap(), "coplanar");
assert!(coplanar.iter().filter(|v| **v == 1.0).count() > 150);
}
#[test]
fn measures_nearest_neighbours() {
let mut xyz: Vec<[f64; 3]> = (0..100).map(|i| [(i % 10) as f64, (i / 10) as f64, 0.0]).collect();
xyz.push([100.0, 100.0, 0.0]);
let c = points(&xyz);
let lof = filters::lof(&c, 5).unwrap();
let factors = col(&lof, "local_outlier_factor");
let max = factors.iter().copied().fold(f64::MIN, f64::max);
assert_eq!(factors[100], max, "the lone point is the most outlying");
assert!(lof.dim("nndistance").is_some() && lof.dim("local_reachability_distance").is_some());
let d = col(&filters::nndistance(&c, 4, NnDistanceMode::Kth).unwrap(), "nndistance");
assert_eq!(d[55], 1.0);
let d = col(&filters::nndistance(&c, 8, NnDistanceMode::Avg).unwrap(), "nndistance");
assert!((d[55] - (4.0 + 4.0 * 2f64.sqrt()) / 8.0).abs() < 1e-12);
let density = col(&filters::radial_density(&c, 1.1).unwrap(), "radial_density");
let pdal_pi = 314_159.0 / 100_000.0;
assert!((density[55] - 5.0 / (4.0 / 3.0 * pdal_pi * 1.1f64.powi(3))).abs() < 1e-12);
assert_eq!(col(&filters::reciprocity(&c, 4).unwrap(), "reciprocity")[100], 100.0);
let m = col(&filters::miniball(&c, 8).unwrap(), "miniball");
assert!(m[55].abs() < 1e-12, "a grid point is at the centre of its 8 neighbours");
assert!(NnDistanceMode::parse("AVG").is_ok() && NnDistanceMode::parse("max").is_err());
}
#[test]
fn smooths_elevations() {
let xyz = vec![[0.0, 0.0, 1.0], [0.5, 0.0, 2.0], [0.0, 0.5, 3.0], [0.2, 0.2, 100.0], [10.0, 10.0, 7.0]];
let c = filters::zsmooth(&points(&xyz), "Smoothed", 1.0, 50.0).unwrap();
let s = col(&c, "smoothed");
assert_eq!(s[3], 2.0, "the median of 1, 2 and 3");
assert_eq!(s[4], 7.0, "a point alone keeps its z");
let low = col(&filters::zsmooth(&points(&xyz), "Smoothed", 1.0, 0.0).unwrap(), "smoothed");
assert_eq!(low[3], 1.0);
assert!(filters::zsmooth(&points(&xyz), "Z", 1.0, 50.0).is_err());
}
#[test]
fn balances_skewness() {
let mut xyz: Vec<[f64; 3]> = (0..200).map(|i| [i as f64, 0.0, ((i * 37) % 11) as f64 * 0.01]).collect();
xyz.extend((0..40).map(|i| [i as f64, 1.0, 10.0 + i as f64]));
let c = filters::skewness_balancing(&points(&xyz), 2, 1, false).unwrap();
let z = col(&c, "z");
assert!(z.windows(2).all(|w| w[0] <= w[1]));
let classes = col(&c, "classification");
let ground = classes.iter().take_while(|c| **c == 2.0).count();
assert!(ground > 0 && ground < 200 && classes[ground..].iter().all(|c| *c == 1.0), "{ground}");
assert!(filters::skewness_balancing(&points(&xyz), 2, 2, false).is_err());
}
#[test]
fn selects_and_orders_points() {
let xyz: Vec<[f64; 3]> = (0..20).map(|i| [i as f64, (i % 3) as f64, ((i * 7) % 20) as f64]).collect();
let c = points(&xyz);
assert_eq!(col(&filters::head(&c, 3, false), "x"), [0.0, 1.0, 2.0]);
assert_eq!(filters::head(&c, 3, true).len(), 17);
assert_eq!(col(&filters::tail(&c, 2, false), "x"), [18.0, 19.0]);
assert_eq!(filters::tail(&c, 50, false).len(), 20);
assert_eq!(filters::tail(&c, 50, true).len(), 0);
let s = filters::sort(&c, &["Z".into()], true).unwrap();
assert!(col(&s, "z").windows(2).all(|w| w[0] >= w[1]));
let s = filters::sort(&c, &["X".into(), "Y".into()], false).unwrap();
assert_eq!(col(&s, "x")[..7], [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0]);
assert_eq!(col(&filters::locate(&c, "Z", false).unwrap(), "z"), [19.0]);
assert_eq!(col(&filters::locate(&c, "z", true).unwrap(), "x"), [0.0]);
let mut xyz = xyz;
xyz.push([0.0, 0.0, 1000.0]);
let c = points(&xyz);
assert_eq!(filters::iqr(&c, "Z", 1.5).unwrap().len(), 20);
assert_eq!(filters::mad(&c, "Z", 2.0, 1.4862).unwrap().len(), 20);
assert!(filters::sort(&c, &[], false).is_err() && filters::locate(&c, "nope", true).is_err());
}
fn raster() -> RasterDataset {
let cfg = RasterConfig { cols: 10, rows: 10, bands: 2, x_min: 0.0, y_min: 0.0, cell_size: 1.0, ..Default::default() };
let mut data = Vec::new();
for band in 0..2 {
for row in 0..10 {
for c in 0..10 {
let v = c as f64 + 10.0 * (9 - row) as f64;
data.push(if band == 0 { v } else { 100.0 - v });
}
}
}
RasterDataset::new(Raster::from_data(cfg, data).unwrap(), true)
}
#[test]
fn keeps_points_near_a_raster_surface() {
let mut xyz: Vec<[f64; 3]> = (0..100).map(|i| [(i % 10) as f64 + 0.5, (i / 10) as f64 + 0.5, i as f64 + (i % 4) as f64]).collect();
xyz.push([20.0, 20.0, 0.0]);
let c = points(&xyz);
let kept = filters::dem(&c, &raster(), &DimRange::parse("Z[0:1.5]").unwrap(), 1).unwrap();
assert_eq!(kept.len(), 50, "z up to 1.5 above the surface");
assert!(filters::dem(&c, &raster(), &DimRange::parse("Z[0:1]").unwrap(), 3).is_err());
}
#[test]
fn colorizes_points_from_raster_bands() {
let xyz = vec![[0.5, 9.5, 0.0], [3.2, 0.1, 0.0], [10.5, 5.0, 0.0]];
let dims = BandDimension::parse_list("Red:1:2, Shade:2").unwrap();
let c = filters::colorization(&points(&xyz), &raster(), &dims).unwrap();
assert!(matches!(c.dim("red").unwrap().values, Values::F64(_)));
assert_eq!(col(&c, "red"), [180.0, 6.0, 0.0]);
assert_eq!(col(&c, "shade"), [10.0, 97.0, 0.0]);
let rgb = BandDimension::rgb();
assert!(filters::colorization(&points(&xyz), &raster(), &rgb).is_err(), "no third band");
}
#[test]
fn overlays_polygon_attributes() {
let mut layer = Layer::new("zones");
layer.add_field(FieldDef::new("name", FieldType::Text));
layer.add_field(FieldDef::new("zone", FieldType::Integer));
let square = |x0: f64, holes: Vec<Vec<wbvector::Coord>>| {
let ring = [(x0, 0.0), (x0 + 10.0, 0.0), (x0 + 10.0, 10.0), (x0, 10.0), (x0, 0.0)];
Geometry::polygon(ring.iter().map(|&(x, y)| wbvector::Coord::xy(x, y)).collect(), holes)
};
let hole: Vec<wbvector::Coord> =
[(2.0, 2.0), (4.0, 2.0), (4.0, 4.0), (2.0, 4.0), (2.0, 2.0)].iter().map(|&(x, y)| wbvector::Coord::xy(x, y)).collect();
for (k, g) in [square(0.0, vec![hole]), square(5.0, vec![])].into_iter().enumerate() {
let attributes = vec![FieldValue::Text(format!("{}7", k + 1)), FieldValue::Integer(k as i64 + 3)];
layer.push(terrio::Feature { fid: k as u64, geometry: Some(g), attributes });
}
let xyz = vec![[1.0, 1.0, 0.0], [3.0, 3.0, 0.0], [7.0, 5.0, 0.0], [12.0, 5.0, 0.0], [30.0, 5.0, 0.0], [0.0, 5.0, 0.0]];
let c = points(&xyz);
let o = OverlayOptions { dimension: "Classification".into(), column: None };
assert_eq!(col(&filters::overlay(&c, &layer, &o).unwrap(), "classification"), [3.0, 1.0, 3.0, 4.0, 1.0, 3.0]);
let o = OverlayOptions { dimension: "Intensity".into(), column: Some("NAME".into()) };
assert_eq!(col(&filters::overlay(&c, &layer, &o).unwrap(), "intensity"), [17.0, 1.0, 17.0, 27.0, 4.0, 17.0]);
assert!(filters::overlay(&c, &layer, &OverlayOptions { dimension: "Nope".into(), column: None }).is_err());
assert!(filters::overlay(&c, &layer, &OverlayOptions { dimension: "Intensity".into(), column: Some("x".into()) }).is_err());
}
fn autzen() -> Option<PointCloud> {
Some(pointcloud::read_points(&data("autzen_trim.las")?, &Default::default()).unwrap())
}
#[test]
fn neighbourhood_filters_as_pdal_does_on_autzen() {
let Some(c) = autzen() else { return };
let sum = |c: &PointCloud, name: &str| col(c, name).iter().sum::<f64>();
let o = DbscanOptions { eps: 3.0, min_points: 10, ..Default::default() };
let ids = col(&filters::dbscan(&c, &o).unwrap(), "cluster_id");
assert_eq!((ids.iter().copied().fold(f64::MIN, f64::max), ids.iter().filter(|v| **v == -1.0).count()), (359.0, 95_169));
let e = filters::eigenvalues(&c, &EigenvaluesOptions::default()).unwrap();
assert_eq!(e.dim("eigenvalue2").unwrap().get(0), 16.185_960_628_220_393);
let f = filters::covariance_features(&c, &CovarianceFeaturesOptions::default()).unwrap();
assert_eq!(f.dim("linearity").unwrap().get(0), 0.677_748_811_127_886_5);
assert_eq!(f.dim("verticality").unwrap().get(0), 0.032_007_386_653_545_196);
let lof = filters::lof(&c, 10).unwrap();
assert_eq!(lof.dim("local_outlier_factor").unwrap().get(0), 1.031_589_756_323_879_4);
assert_eq!(sum(&filters::reciprocity(&c, 8).unwrap(), "reciprocity"), 1_061_100.0);
assert_eq!(col(&filters::estimate_rank(&c, 8, 0.01).unwrap(), "rank").iter().filter(|r| **r == 3.0).count(), 37_754);
assert_eq!(col(&filters::approximate_coplanar(&c, 8, 25.0, 6.0).unwrap(), "coplanar").iter().filter(|r| **r == 1.0).count(), 75_369);
assert_eq!(filters::iqr(&c, "Z", 1.5).unwrap().len(), 82_454);
assert_eq!(filters::mad(&c, "Z", 2.0, 1.4862).unwrap().len(), 80_067);
let ground = filters::skewness_balancing(&c, 2, 1, false).unwrap();
assert_eq!(col(&ground, "classification").iter().filter(|v| **v == 2.0).count(), 102_234);
}