Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,747 @@
//! 3D volumetric data structure for medical imaging.
//!
//! The `Volume` struct provides a container for 3D image data with spatial
//! metadata (spacing, origin, affine transform).
use crate::nifti::transform::{self, Affine4};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
/// A 3D volume of medical imaging data.
///
/// Data is stored in row-major order (C-order): z varies slowest, x varies fastest.
/// The layout is `data[z * (y_size * x_size) + y * x_size + x]`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Volume {
/// Voxel data in row-major order
data: Vec<f64>,
/// Dimensions [x, y, z]
shape: [usize; 3],
/// Voxel spacing in mm [dx, dy, dz]
spacing: [f64; 3],
/// Origin in world coordinates [ox, oy, oz]
origin: [f64; 3],
/// 4x4 affine transformation matrix (voxel to world)
affine: Affine4,
}
impl Volume {
/// Create a new volume from raw data and metadata.
///
/// # Arguments
/// * `data` - Voxel data in row-major order
/// * `shape` - Dimensions [x, y, z]
/// * `spacing` - Voxel spacing in mm [dx, dy, dz]
/// * `origin` - Origin in world coordinates [ox, oy, oz]
/// * `affine` - 4x4 affine transformation matrix
///
/// # Panics
/// Panics if data length doesn't match shape.
pub fn new(
data: Vec<f64>,
shape: [usize; 3],
spacing: [f64; 3],
origin: [f64; 3],
affine: Affine4,
) -> Self {
let expected_len = shape[0] * shape[1] * shape[2];
assert_eq!(
data.len(),
expected_len,
"Data length {} doesn't match shape {:?} (expected {})",
data.len(),
shape,
expected_len
);
Self {
data,
shape,
spacing,
origin,
affine,
}
}
/// Create a volume filled with zeros.
pub fn zeros(shape: [usize; 3]) -> Self {
let size = shape[0] * shape[1] * shape[2];
let spacing = [1.0, 1.0, 1.0];
let origin = [0.0, 0.0, 0.0];
let affine = transform::from_spacing_origin(spacing, origin);
Self {
data: vec![0.0; size],
shape,
spacing,
origin,
affine,
}
}
/// Create a volume filled with a constant value.
pub fn filled(shape: [usize; 3], value: f64) -> Self {
let size = shape[0] * shape[1] * shape[2];
let spacing = [1.0, 1.0, 1.0];
let origin = [0.0, 0.0, 0.0];
let affine = transform::from_spacing_origin(spacing, origin);
Self {
data: vec![value; size],
shape,
spacing,
origin,
affine,
}
}
/// Get the volume dimensions [x, y, z].
pub fn shape(&self) -> [usize; 3] {
self.shape
}
/// Get the voxel spacing in mm [dx, dy, dz].
pub fn spacing(&self) -> [f64; 3] {
self.spacing
}
/// Get the origin in world coordinates [ox, oy, oz].
pub fn origin(&self) -> [f64; 3] {
self.origin
}
/// Get the affine transformation matrix.
pub fn affine(&self) -> Affine4 {
self.affine
}
/// Get a reference to the raw voxel data.
pub fn data(&self) -> &[f64] {
&self.data
}
/// Get a mutable reference to the raw voxel data.
pub fn data_mut(&mut self) -> &mut [f64] {
&mut self.data
}
/// Get the total number of voxels.
pub fn num_voxels(&self) -> usize {
self.shape[0] * self.shape[1] * self.shape[2]
}
/// Convert flat index to 3D coordinates (x, y, z).
pub fn index_to_coords(&self, index: usize) -> [usize; 3] {
let x = index % self.shape[0];
let y = (index / self.shape[0]) % self.shape[1];
let z = index / (self.shape[0] * self.shape[1]);
[x, y, z]
}
/// Convert 3D coordinates to flat index.
pub fn coords_to_index(&self, x: usize, y: usize, z: usize) -> usize {
z * (self.shape[0] * self.shape[1]) + y * self.shape[0] + x
}
/// Get voxel value at coordinates (x, y, z).
///
/// Returns None if coordinates are out of bounds.
pub fn get(&self, x: usize, y: usize, z: usize) -> Option<f64> {
if x < self.shape[0] && y < self.shape[1] && z < self.shape[2] {
Some(self.data[self.coords_to_index(x, y, z)])
} else {
None
}
}
/// Set voxel value at coordinates (x, y, z).
///
/// Returns false if coordinates are out of bounds.
pub fn set(&mut self, x: usize, y: usize, z: usize, value: f64) -> bool {
if x < self.shape[0] && y < self.shape[1] && z < self.shape[2] {
let idx = self.coords_to_index(x, y, z);
self.data[idx] = value;
true
} else {
false
}
}
/// Get voxel value at coordinates without bounds checking.
///
/// # Safety
/// Caller must ensure coordinates are valid.
pub unsafe fn get_unchecked(&self, x: usize, y: usize, z: usize) -> f64 {
let idx = self.coords_to_index(x, y, z);
// SAFETY: caller guarantees coordinates are valid
unsafe { *self.data.get_unchecked(idx) }
}
/// Set voxel value at coordinates without bounds checking.
///
/// # Safety
/// Caller must ensure coordinates are valid.
pub unsafe fn set_unchecked(&mut self, x: usize, y: usize, z: usize, value: f64) {
let idx = self.coords_to_index(x, y, z);
// SAFETY: caller guarantees coordinates are valid
unsafe { *self.data.get_unchecked_mut(idx) = value }
}
/// Convert voxel coordinates to world coordinates.
pub fn voxel_to_world(&self, voxel: [f64; 3]) -> [f64; 3] {
transform::voxel_to_world(&self.affine, voxel)
}
/// Convert world coordinates to voxel coordinates.
pub fn world_to_voxel(&self, world: [f64; 3]) -> Option<[f64; 3]> {
transform::world_to_voxel(&self.affine, world)
}
/// Get the bounding box in world coordinates.
///
/// Returns (min_corner, max_corner).
pub fn world_bounds(&self) -> ([f64; 3], [f64; 3]) {
let corners = [
[0.0, 0.0, 0.0],
[self.shape[0] as f64, 0.0, 0.0],
[0.0, self.shape[1] as f64, 0.0],
[0.0, 0.0, self.shape[2] as f64],
[self.shape[0] as f64, self.shape[1] as f64, 0.0],
[self.shape[0] as f64, 0.0, self.shape[2] as f64],
[0.0, self.shape[1] as f64, self.shape[2] as f64],
[
self.shape[0] as f64,
self.shape[1] as f64,
self.shape[2] as f64,
],
];
let world_corners: Vec<_> = corners.iter().map(|&c| self.voxel_to_world(c)).collect();
let min = [
world_corners
.iter()
.map(|c| c[0])
.fold(f64::INFINITY, f64::min),
world_corners
.iter()
.map(|c| c[1])
.fold(f64::INFINITY, f64::min),
world_corners
.iter()
.map(|c| c[2])
.fold(f64::INFINITY, f64::min),
];
let max = [
world_corners
.iter()
.map(|c| c[0])
.fold(f64::NEG_INFINITY, f64::max),
world_corners
.iter()
.map(|c| c[1])
.fold(f64::NEG_INFINITY, f64::max),
world_corners
.iter()
.map(|c| c[2])
.fold(f64::NEG_INFINITY, f64::max),
];
(min, max)
}
/// Get minimum voxel value.
pub fn min(&self) -> f64 {
self.data
.par_iter()
.copied()
.reduce(|| f64::INFINITY, f64::min)
}
/// Get maximum voxel value.
pub fn max(&self) -> f64 {
self.data
.par_iter()
.copied()
.reduce(|| f64::NEG_INFINITY, f64::max)
}
/// Get mean voxel value.
pub fn mean(&self) -> f64 {
let sum: f64 = self.data.par_iter().sum();
sum / self.data.len() as f64
}
/// Get standard deviation of voxel values.
pub fn std(&self) -> f64 {
let mean = self.mean();
let variance: f64 = self
.data
.par_iter()
.map(|&v| (v - mean).powi(2))
.sum::<f64>()
/ self.data.len() as f64;
variance.sqrt()
}
/// Get unique label values (for segmentation volumes).
pub fn unique_labels(&self) -> Vec<i64> {
use std::collections::BTreeSet;
let labels: BTreeSet<i64> = self.data.par_iter().map(|&v| v.round() as i64).collect();
labels.into_iter().collect()
}
/// Apply a function to each voxel value (in-place).
pub fn apply<F>(&mut self, f: F)
where
F: Fn(f64) -> f64 + Sync + Send,
{
self.data.par_iter_mut().for_each(|v| *v = f(*v));
}
/// Apply a function to each voxel, creating a new volume.
pub fn map<F>(&self, f: F) -> Self
where
F: Fn(f64) -> f64 + Sync + Send,
{
let new_data: Vec<f64> = self.data.par_iter().map(|&v| f(v)).collect();
Self {
data: new_data,
shape: self.shape,
spacing: self.spacing,
origin: self.origin,
affine: self.affine,
}
}
/// Threshold the volume: values >= threshold become 1.0, others become 0.0.
pub fn threshold(&self, threshold: f64) -> Self {
self.map(|v| if v >= threshold { 1.0 } else { 0.0 })
}
/// Create a binary mask where values equal to the label are 1.0.
pub fn mask_label(&self, label: i64) -> Self {
self.map(|v| if v.round() as i64 == label { 1.0 } else { 0.0 })
}
/// Extract a sub-volume (crop).
///
/// # Arguments
/// * `start` - Start coordinates [x, y, z]
/// * `size` - Size of sub-volume [sx, sy, sz]
pub fn crop(&self, start: [usize; 3], size: [usize; 3]) -> Self {
let [sx, sy, sz] = size;
let mut new_data = vec![0.0; sx * sy * sz];
for z in 0..sz {
for y in 0..sy {
for x in 0..sx {
let src_idx = self.coords_to_index(start[0] + x, start[1] + y, start[2] + z);
let dst_idx = z * (sx * sy) + y * sx + x;
new_data[dst_idx] = self.data[src_idx];
}
}
}
// Adjust origin for the crop
let new_origin = self.voxel_to_world([start[0] as f64, start[1] as f64, start[2] as f64]);
Self {
data: new_data,
shape: size,
spacing: self.spacing,
origin: new_origin,
affine: self.affine, // Affine stays the same, origin adjusted
}
}
/// Set spacing and update the affine accordingly.
pub fn set_spacing(&mut self, spacing: [f64; 3]) {
self.spacing = spacing;
self.affine = transform::from_spacing_origin(spacing, self.origin);
}
/// Set origin and update the affine accordingly.
pub fn set_origin(&mut self, origin: [f64; 3]) {
self.origin = origin;
self.affine = transform::from_spacing_origin(self.spacing, origin);
}
/// Set the affine transformation and extract spacing/origin.
pub fn set_affine(&mut self, affine: Affine4) {
self.affine = affine;
self.spacing = transform::get_spacing(&affine);
self.origin = transform::get_origin(&affine);
}
/// Trilinear interpolation at a floating-point voxel coordinate.
///
/// Returns None if the coordinate is outside the volume.
pub fn interpolate(&self, x: f64, y: f64, z: f64) -> Option<f64> {
if x < 0.0
|| y < 0.0
|| z < 0.0
|| x >= (self.shape[0] - 1) as f64
|| y >= (self.shape[1] - 1) as f64
|| z >= (self.shape[2] - 1) as f64
{
return None;
}
let x0 = x.floor() as usize;
let y0 = y.floor() as usize;
let z0 = z.floor() as usize;
let x1 = x0 + 1;
let y1 = y0 + 1;
let z1 = z0 + 1;
let xd = x - x0 as f64;
let yd = y - y0 as f64;
let zd = z - z0 as f64;
// Get 8 corner values
let c000 = self.get(x0, y0, z0)?;
let c100 = self.get(x1, y0, z0)?;
let c010 = self.get(x0, y1, z0)?;
let c110 = self.get(x1, y1, z0)?;
let c001 = self.get(x0, y0, z1)?;
let c101 = self.get(x1, y0, z1)?;
let c011 = self.get(x0, y1, z1)?;
let c111 = self.get(x1, y1, z1)?;
// Interpolate along x
let c00 = c000 * (1.0 - xd) + c100 * xd;
let c01 = c001 * (1.0 - xd) + c101 * xd;
let c10 = c010 * (1.0 - xd) + c110 * xd;
let c11 = c011 * (1.0 - xd) + c111 * xd;
// Interpolate along y
let c0 = c00 * (1.0 - yd) + c10 * yd;
let c1 = c01 * (1.0 - yd) + c11 * yd;
// Interpolate along z
Some(c0 * (1.0 - zd) + c1 * zd)
}
/// Sample the volume at a world coordinate using trilinear interpolation.
pub fn sample_world(&self, world: [f64; 3]) -> Option<f64> {
let voxel = self.world_to_voxel(world)?;
self.interpolate(voxel[0], voxel[1], voxel[2])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zeros() {
let vol = Volume::zeros([10, 20, 30]);
assert_eq!(vol.shape(), [10, 20, 30]);
assert_eq!(vol.num_voxels(), 6000);
assert_eq!(vol.min(), 0.0);
assert_eq!(vol.max(), 0.0);
}
#[test]
fn test_filled() {
let vol = Volume::filled([5, 5, 5], 42.0);
assert_eq!(vol.shape(), [5, 5, 5]);
assert_eq!(vol.min(), 42.0);
assert_eq!(vol.max(), 42.0);
}
#[test]
fn test_get_set() {
let mut vol = Volume::zeros([10, 10, 10]);
vol.set(5, 5, 5, 42.0);
assert_eq!(vol.get(5, 5, 5), Some(42.0));
assert_eq!(vol.get(0, 0, 0), Some(0.0));
assert_eq!(vol.get(100, 100, 100), None);
}
#[test]
fn test_set_returns_false_out_of_bounds() {
let mut vol = Volume::zeros([10, 10, 10]);
assert!(!vol.set(100, 0, 0, 1.0));
assert!(!vol.set(0, 100, 0, 1.0));
assert!(!vol.set(0, 0, 100, 1.0));
}
#[test]
fn test_set_returns_true_in_bounds() {
let mut vol = Volume::zeros([10, 10, 10]);
assert!(vol.set(5, 5, 5, 1.0));
}
#[test]
fn test_coords_conversion() {
let vol = Volume::zeros([10, 20, 30]);
// Test round-trip
for z in 0..5 {
for y in 0..5 {
for x in 0..5 {
let idx = vol.coords_to_index(x, y, z);
let [rx, ry, rz] = vol.index_to_coords(idx);
assert_eq!([x, y, z], [rx, ry, rz]);
}
}
}
}
#[test]
fn test_coords_to_index_calculation() {
let vol = Volume::zeros([10, 20, 30]);
// First element
assert_eq!(vol.coords_to_index(0, 0, 0), 0);
// Last x in first row
assert_eq!(vol.coords_to_index(9, 0, 0), 9);
// First element of second row
assert_eq!(vol.coords_to_index(0, 1, 0), 10);
// First element of second slice
assert_eq!(vol.coords_to_index(0, 0, 1), 200);
}
#[test]
fn test_statistics() {
let vol = Volume::filled([10, 10, 10], 5.0);
assert_eq!(vol.mean(), 5.0);
assert_eq!(vol.std(), 0.0);
}
#[test]
fn test_statistics_mixed_values() {
let mut vol = Volume::zeros([10, 10, 10]);
// Set half the values to 1.0
for i in 0..500 {
let [x, y, z] = vol.index_to_coords(i);
vol.set(x, y, z, 1.0);
}
assert_eq!(vol.mean(), 0.5);
assert!(vol.min() == 0.0);
assert!(vol.max() == 1.0);
}
#[test]
fn test_interpolate() {
let mut vol = Volume::zeros([10, 10, 10]);
vol.set(0, 0, 0, 0.0);
vol.set(1, 0, 0, 1.0);
// Interpolate at midpoint
let val = vol.interpolate(0.5, 0.0, 0.0).unwrap();
assert!((val - 0.5).abs() < 1e-10);
}
#[test]
fn test_interpolate_out_of_bounds() {
let vol = Volume::zeros([10, 10, 10]);
assert_eq!(vol.interpolate(-1.0, 0.0, 0.0), None);
assert_eq!(vol.interpolate(0.0, -1.0, 0.0), None);
assert_eq!(vol.interpolate(0.0, 0.0, -1.0), None);
assert_eq!(vol.interpolate(10.0, 0.0, 0.0), None);
assert_eq!(vol.interpolate(0.0, 10.0, 0.0), None);
assert_eq!(vol.interpolate(0.0, 0.0, 10.0), None);
}
#[test]
fn test_threshold() {
let mut vol = Volume::zeros([5, 5, 5]);
vol.set(0, 0, 0, 0.5);
vol.set(1, 0, 0, 1.5);
vol.set(2, 0, 0, 2.5);
let thresholded = vol.threshold(1.0);
assert_eq!(thresholded.get(0, 0, 0), Some(0.0));
assert_eq!(thresholded.get(1, 0, 0), Some(1.0));
assert_eq!(thresholded.get(2, 0, 0), Some(1.0));
}
#[test]
fn test_mask_label() {
let mut vol = Volume::zeros([5, 5, 5]);
vol.set(0, 0, 0, 0.0);
vol.set(1, 0, 0, 1.0);
vol.set(2, 0, 0, 2.0);
let mask = vol.mask_label(1);
assert_eq!(mask.get(0, 0, 0), Some(0.0));
assert_eq!(mask.get(1, 0, 0), Some(1.0));
assert_eq!(mask.get(2, 0, 0), Some(0.0));
}
#[test]
fn test_unique_labels() {
let mut vol = Volume::zeros([5, 5, 5]);
vol.set(0, 0, 0, 0.0);
vol.set(1, 0, 0, 1.0);
vol.set(2, 0, 0, 2.0);
vol.set(3, 0, 0, 1.0);
let labels = vol.unique_labels();
assert_eq!(labels, vec![0, 1, 2]);
}
#[test]
fn test_crop() {
let mut vol = Volume::zeros([10, 10, 10]);
vol.set(5, 5, 5, 42.0);
let cropped = vol.crop([4, 4, 4], [3, 3, 3]);
assert_eq!(cropped.shape(), [3, 3, 3]);
assert_eq!(cropped.get(1, 1, 1), Some(42.0)); // (5,5,5) - (4,4,4) = (1,1,1)
}
#[test]
fn test_map() {
let vol = Volume::filled([5, 5, 5], 2.0);
let doubled = vol.map(|v| v * 2.0);
assert_eq!(doubled.get(0, 0, 0), Some(4.0));
}
#[test]
fn test_apply() {
let mut vol = Volume::filled([5, 5, 5], 2.0);
vol.apply(|v| v * 2.0);
assert_eq!(vol.get(0, 0, 0), Some(4.0));
}
#[test]
fn test_voxel_to_world() {
let spacing = [2.0, 2.0, 2.0];
let origin = [10.0, 20.0, 30.0];
let affine = transform::from_spacing_origin(spacing, origin);
let vol = Volume::new(vec![0.0; 1000], [10, 10, 10], spacing, origin, affine);
let world = vol.voxel_to_world([5.0, 5.0, 5.0]);
assert_eq!(world, [20.0, 30.0, 40.0]);
}
#[test]
fn test_world_to_voxel() {
let spacing = [2.0, 2.0, 2.0];
let origin = [10.0, 20.0, 30.0];
let affine = transform::from_spacing_origin(spacing, origin);
let vol = Volume::new(vec![0.0; 1000], [10, 10, 10], spacing, origin, affine);
let voxel = vol.world_to_voxel([20.0, 30.0, 40.0]).unwrap();
assert!((voxel[0] - 5.0).abs() < 1e-10);
assert!((voxel[1] - 5.0).abs() < 1e-10);
assert!((voxel[2] - 5.0).abs() < 1e-10);
}
#[test]
fn test_set_spacing() {
let mut vol = Volume::zeros([10, 10, 10]);
vol.set_spacing([2.0, 3.0, 4.0]);
assert_eq!(vol.spacing(), [2.0, 3.0, 4.0]);
}
#[test]
fn test_set_origin() {
let mut vol = Volume::zeros([10, 10, 10]);
vol.set_origin([10.0, 20.0, 30.0]);
assert_eq!(vol.origin(), [10.0, 20.0, 30.0]);
}
#[test]
fn test_set_affine() {
let mut vol = Volume::zeros([10, 10, 10]);
let new_affine = transform::from_spacing_origin([2.0, 3.0, 4.0], [5.0, 10.0, 15.0]);
vol.set_affine(new_affine);
let spacing = vol.spacing();
assert!((spacing[0] - 2.0).abs() < 1e-10);
assert!((spacing[1] - 3.0).abs() < 1e-10);
assert!((spacing[2] - 4.0).abs() < 1e-10);
}
#[test]
fn test_world_bounds() {
let spacing = [1.0, 1.0, 1.0];
let origin = [0.0, 0.0, 0.0];
let affine = transform::from_spacing_origin(spacing, origin);
let vol = Volume::new(vec![0.0; 1000], [10, 10, 10], spacing, origin, affine);
let (min, max) = vol.world_bounds();
assert_eq!(min, [0.0, 0.0, 0.0]);
assert_eq!(max, [10.0, 10.0, 10.0]);
}
#[test]
fn test_sample_world() {
let spacing = [1.0, 1.0, 1.0];
let origin = [0.0, 0.0, 0.0];
let affine = transform::from_spacing_origin(spacing, origin);
let mut vol = Volume::new(vec![0.0; 1000], [10, 10, 10], spacing, origin, affine);
vol.set(5, 5, 5, 42.0);
let value = vol.sample_world([5.0, 5.0, 5.0]).unwrap();
assert!((value - 42.0).abs() < 1e-10);
}
#[test]
fn test_data_access() {
let vol = Volume::filled([5, 5, 5], 3.0);
let data = vol.data();
assert_eq!(data.len(), 125);
assert_eq!(data[0], 3.0);
}
#[test]
fn test_data_mut_access() {
let mut vol = Volume::filled([5, 5, 5], 3.0);
let data = vol.data_mut();
data[0] = 10.0;
assert_eq!(vol.get(0, 0, 0), Some(10.0));
}
#[test]
fn test_num_voxels() {
let vol = Volume::zeros([10, 20, 30]);
assert_eq!(vol.num_voxels(), 6000);
}
#[test]
fn test_clone() {
let vol1 = Volume::filled([5, 5, 5], 2.0);
let vol2 = vol1.clone();
assert_eq!(vol2.get(0, 0, 0), Some(2.0));
assert_eq!(vol2.shape(), vol1.shape());
}
#[test]
#[should_panic(expected = "doesn't match shape")]
fn test_new_panics_on_size_mismatch() {
let spacing = [1.0, 1.0, 1.0];
let origin = [0.0, 0.0, 0.0];
let affine = transform::from_spacing_origin(spacing, origin);
Volume::new(vec![0.0; 100], [10, 10, 10], spacing, origin, affine);
}
#[test]
fn test_new_correct_size() {
let spacing = [1.0, 1.0, 1.0];
let origin = [0.0, 0.0, 0.0];
let affine = transform::from_spacing_origin(spacing, origin);
let vol = Volume::new(vec![0.0; 1000], [10, 10, 10], spacing, origin, affine);
assert_eq!(vol.num_voxels(), 1000);
}
#[test]
fn test_unsafe_get_set() {
let mut vol = Volume::zeros([10, 10, 10]);
unsafe {
vol.set_unchecked(5, 5, 5, 42.0);
let value = vol.get_unchecked(5, 5, 5);
assert_eq!(value, 42.0);
}
}
}