mirror of
https://github.com/ruvnet/RuView
synced 2026-08-02 19:11:46 +00:00
d781f20e1a
New standalone leaf crate implementing the synthetic-aperture-radar reconstruction primitive a handheld through-wall RF imaging device would need: a stepped-frequency multi-position complex forward measurement simulator, delay-and-sum backprojection reconstruction, point-cloud extraction, and closed-form range/cross-range resolution + antenna-pose coherence-budget formulas checked against the reconstruction's actual behavior in tests/physics_validation.rs. Motivated by comparing this repo against Applied Electrodynamics' "WaveSight" launch. Scoped explicitly below ADR-278's RISE/DiffRadar/ GeRaF reproduction gates: this is the bare measurement-model + backprojection primitive, not a reproduction of any published system or a claim about real hardware capability. Every number is SYNTHETIC/L0 (ADR-282) -- no wideband RF hardware backs this crate. 24 tests (21 unit + 3 integration), 0 failed, clippy-clean. Adds a tutorial walkthrough and MEASURED backprojection benchmark numbers.
117 lines
4.8 KiB
Rust
117 lines
4.8 KiB
Rust
//! Extract a sparse point cloud from a dense [`ReflectivityImage`].
|
|
//!
|
|
//! A `nx * ny * nz` voxel grid is not a useful end product on its own --
|
|
//! real point-cloud consumers (visualization, `ruview-unified`'s
|
|
//! `GaussianMap`, downstream fusion) want a short list of "here is
|
|
//! something" points, not every voxel. This module does simple
|
|
//! threshold + local-maximum extraction: no clustering, no material
|
|
//! classification, no confidence calibration against real data (ADR-283
|
|
//! §5 -- explicitly out of scope for this crate).
|
|
|
|
use crate::geometry::Point3;
|
|
use crate::reconstruct::ReflectivityImage;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// A single detected point: a location and its reconstructed reflectivity
|
|
/// magnitude (relative, not calibrated to any physical unit).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub struct PointCloudPoint {
|
|
/// World-space location, meters.
|
|
pub position: Point3,
|
|
/// Reconstructed reflectivity magnitude at this voxel.
|
|
pub magnitude: f64,
|
|
}
|
|
|
|
/// Extract detected points from `image`: every voxel whose magnitude is
|
|
/// (a) at least `threshold_fraction` of the image's peak magnitude, and
|
|
/// (b) a local maximum among its 6-connected neighbors (so a single broad
|
|
/// blob yields one point, not every voxel inside it).
|
|
///
|
|
/// `threshold_fraction` must be in `(0.0, 1.0]`. A typical value is
|
|
/// `0.5` (a classic radar/SAR "half-power point" style threshold).
|
|
pub fn extract_point_cloud(image: &ReflectivityImage, threshold_fraction: f64) -> Vec<PointCloudPoint> {
|
|
assert!(
|
|
threshold_fraction > 0.0 && threshold_fraction <= 1.0,
|
|
"threshold_fraction must be in (0, 1]"
|
|
);
|
|
let grid = &image.grid;
|
|
let peak = image.magnitude.iter().cloned().fold(0.0_f64, f64::max);
|
|
if peak <= 0.0 {
|
|
return Vec::new();
|
|
}
|
|
let threshold = peak * threshold_fraction;
|
|
|
|
let mag_at = |i: i64, j: i64, k: i64| -> f64 {
|
|
if i < 0 || j < 0 || k < 0 || i as usize >= grid.nx || j as usize >= grid.ny || k as usize >= grid.nz {
|
|
return f64::NEG_INFINITY;
|
|
}
|
|
let linear = grid.linear_index(i as usize, j as usize, k as usize);
|
|
image.magnitude[linear]
|
|
};
|
|
|
|
let mut points = Vec::new();
|
|
for k in 0..grid.nz {
|
|
for j in 0..grid.ny {
|
|
for i in 0..grid.nx {
|
|
let here = mag_at(i as i64, j as i64, k as i64);
|
|
if here < threshold {
|
|
continue;
|
|
}
|
|
let neighbors = [
|
|
mag_at(i as i64 - 1, j as i64, k as i64),
|
|
mag_at(i as i64 + 1, j as i64, k as i64),
|
|
mag_at(i as i64, j as i64 - 1, k as i64),
|
|
mag_at(i as i64, j as i64 + 1, k as i64),
|
|
mag_at(i as i64, j as i64, k as i64 - 1),
|
|
mag_at(i as i64, j as i64, k as i64 + 1),
|
|
];
|
|
if neighbors.iter().all(|&n| here >= n) {
|
|
points.push(PointCloudPoint {
|
|
position: grid.voxel_center(i, j, k),
|
|
magnitude: here,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
points
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::geometry::linear_aperture;
|
|
use crate::measurement::{simulate_measurement, FrequencySweep, ScatteringTarget};
|
|
use crate::reconstruct::{backproject, VoxelGrid};
|
|
|
|
#[test]
|
|
fn single_target_yields_a_single_detected_point() {
|
|
let poses = linear_aperture(Point3::new(-0.5, 0.0, 0.0), Point3::new(0.5, 0.0, 0.0), 21);
|
|
let sweep = FrequencySweep::new(2.0e9, 6.0e9, 32);
|
|
let target = ScatteringTarget::new(Point3::new(0.0, 2.0, 0.0), 1.0);
|
|
let measurement = simulate_measurement(&poses, &sweep, &[target], 0.0, 3);
|
|
let grid = VoxelGrid::new(Point3::new(-0.5, 1.6, -0.5), 0.05, 21, 17, 21);
|
|
let image = backproject(&measurement, &poses, &sweep, &grid);
|
|
|
|
let points = extract_point_cloud(&image, 0.5);
|
|
assert!(!points.is_empty(), "must detect at least the true target");
|
|
let best = points.iter().max_by(|a, b| a.magnitude.partial_cmp(&b.magnitude).unwrap()).unwrap();
|
|
assert!(best.position.distance(&target.position) < 0.1);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_image_yields_no_points() {
|
|
let grid = VoxelGrid::new(Point3::new(0.0, 0.0, 0.0), 0.1, 3, 3, 3);
|
|
let image = ReflectivityImage { grid, magnitude: vec![0.0; grid.len()] };
|
|
assert!(extract_point_cloud(&image, 0.5).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "threshold_fraction")]
|
|
fn rejects_out_of_range_threshold() {
|
|
let grid = VoxelGrid::new(Point3::new(0.0, 0.0, 0.0), 0.1, 2, 2, 2);
|
|
let image = ReflectivityImage { grid, magnitude: vec![1.0; grid.len()] };
|
|
let _ = extract_point_cloud(&image, 1.5);
|
|
}
|
|
}
|