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,264 @@
//! FreeSurfer curvature file reading
//!
//! Reads .curv files containing per-vertex curvature values.
//! Common curvature files:
//! - lh.curv / rh.curv - mean curvature
//! - lh.sulc / rh.sulc - sulcal depth
//! - lh.thickness / rh.thickness - cortical thickness
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use byteorder::{BigEndian, ReadBytesExt};
use serde::{Deserialize, Serialize};
use crate::error::{AnatomyError, Result};
use crate::surface::Hemisphere;
/// FreeSurfer new curvature file magic number
const NEW_CURV_MAGIC: i32 = -1;
/// Per-vertex curvature data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Curvature {
/// Per-vertex values
pub values: Vec<f32>,
/// Curvature type (e.g., "curv", "sulc", "thickness")
pub curv_type: String,
/// Hemisphere
pub hemisphere: Hemisphere,
/// Number of faces (from file header)
pub n_faces: usize,
}
impl Curvature {
/// Number of vertices
pub fn n_vertices(&self) -> usize {
self.values.len()
}
/// Get min and max values
pub fn range(&self) -> (f32, f32) {
let min = self.values.iter().copied().fold(f32::MAX, f32::min);
let max = self.values.iter().copied().fold(f32::MIN, f32::max);
(min, max)
}
/// Get mean value
pub fn mean(&self) -> f32 {
if self.values.is_empty() {
return 0.0;
}
self.values.iter().sum::<f32>() / self.values.len() as f32
}
/// Normalize values to [0, 1] range
pub fn normalize(&self) -> Vec<f32> {
let (min, max) = self.range();
let range = max - min;
if range.abs() < 1e-10 {
return vec![0.5; self.values.len()];
}
self.values.iter().map(|v| (v - min) / range).collect()
}
/// Get values suitable for colormapping (clamped and normalized)
///
/// # Arguments
///
/// * `vmin` - Minimum value for colormap
/// * `vmax` - Maximum value for colormap
pub fn colormap_values(&self, vmin: f32, vmax: f32) -> Vec<f32> {
let range = vmax - vmin;
if range.abs() < 1e-10 {
return vec![0.5; self.values.len()];
}
self.values
.iter()
.map(|v| (v.clamp(vmin, vmax) - vmin) / range)
.collect()
}
}
/// Read a FreeSurfer curvature file
///
/// # Arguments
///
/// * `path` - Path to the curvature file
/// * `curv_type` - Type of curvature (e.g., "curv", "sulc")
/// * `hemisphere` - Hemisphere
///
/// # Returns
///
/// Curvature data with per-vertex values
///
/// # Example
///
/// ```ignore
/// use rtx_neuro_anatomy::curvature::{read_curvature, Hemisphere};
///
/// let curv = read_curvature(
/// "/path/to/subject/surf/lh.curv",
/// "curv",
/// Hemisphere::Left,
/// )?;
/// println!("Mean curvature: {}", curv.mean());
/// ```
pub fn read_curvature(
path: impl AsRef<Path>,
curv_type: &str,
hemisphere: Hemisphere,
) -> Result<Curvature> {
let path = path.as_ref();
let file = File::open(path).map_err(|_| AnatomyError::CurvatureNotFound {
path: path.to_path_buf(),
})?;
let mut reader = BufReader::new(file);
// Read magic number (3 bytes as i24 big-endian)
let b1 = reader.read_u8()? as i32;
let b2 = reader.read_u8()? as i32;
let b3 = reader.read_u8()? as i32;
let magic = (b1 << 16) | (b2 << 8) | b3;
// Check for new format
if magic == (NEW_CURV_MAGIC & 0xFFFFFF) {
// New format
read_curvature_new(&mut reader, curv_type, hemisphere)
} else {
// Old format: magic is actually n_vertices
let n_vertices = magic as usize;
read_curvature_old(&mut reader, n_vertices, curv_type, hemisphere)
}
}
/// Read new format curvature file
fn read_curvature_new(
reader: &mut BufReader<File>,
curv_type: &str,
hemisphere: Hemisphere,
) -> Result<Curvature> {
// Read header
let n_vertices = reader.read_i32::<BigEndian>()? as usize;
let n_faces = reader.read_i32::<BigEndian>()? as usize;
let _vals_per_vertex = reader.read_i32::<BigEndian>()?;
// Read values
let mut values = Vec::with_capacity(n_vertices);
for _ in 0..n_vertices {
values.push(reader.read_f32::<BigEndian>()?);
}
Ok(Curvature {
values,
curv_type: curv_type.to_string(),
hemisphere,
n_faces,
})
}
/// Read old format curvature file
fn read_curvature_old(
reader: &mut BufReader<File>,
n_vertices: usize,
curv_type: &str,
hemisphere: Hemisphere,
) -> Result<Curvature> {
// Read face count from remaining header
let n_faces = reader.read_i24::<BigEndian>()? as usize;
// Read values (stored as i16, scale by 100)
let mut values = Vec::with_capacity(n_vertices);
for _ in 0..n_vertices {
let raw = reader.read_i16::<BigEndian>()? as f32;
values.push(raw / 100.0);
}
Ok(Curvature {
values,
curv_type: curv_type.to_string(),
hemisphere,
n_faces,
})
}
/// Get curvature filename
pub fn curvature_filename(hemisphere: &Hemisphere, curv_type: &str) -> String {
format!("{}.{}", hemisphere.prefix(), curv_type)
}
#[cfg(test)]
mod tests {
use super::*;
use byteorder::WriteBytesExt;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_test_curvature_new() -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
// Write magic (0xFFFFFF as 3 bytes)
file.write_all(&[0xFF, 0xFF, 0xFF]).unwrap();
// Write header
file.write_i32::<BigEndian>(4).unwrap(); // n_vertices
file.write_i32::<BigEndian>(2).unwrap(); // n_faces
file.write_i32::<BigEndian>(1).unwrap(); // vals_per_vertex
// Write values
file.write_f32::<BigEndian>(0.1).unwrap();
file.write_f32::<BigEndian>(0.2).unwrap();
file.write_f32::<BigEndian>(0.3).unwrap();
file.write_f32::<BigEndian>(0.4).unwrap();
file.flush().unwrap();
file
}
#[test]
fn test_read_curvature_new() {
let file = create_test_curvature_new();
let curv = read_curvature(file.path(), "curv", Hemisphere::Left).unwrap();
assert_eq!(curv.n_vertices(), 4);
assert_eq!(curv.n_faces, 2);
assert!((curv.values[0] - 0.1).abs() < 1e-6);
assert!((curv.values[3] - 0.4).abs() < 1e-6);
}
#[test]
fn test_curvature_range() {
let curv = Curvature {
values: vec![-0.5, 0.0, 0.5, 1.0],
curv_type: "test".to_string(),
hemisphere: Hemisphere::Left,
n_faces: 0,
};
let (min, max) = curv.range();
assert!((min - (-0.5)).abs() < 1e-6);
assert!((max - 1.0).abs() < 1e-6);
}
#[test]
fn test_curvature_normalize() {
let curv = Curvature {
values: vec![0.0, 0.5, 1.0],
curv_type: "test".to_string(),
hemisphere: Hemisphere::Left,
n_faces: 0,
};
let normalized = curv.normalize();
assert!((normalized[0] - 0.0).abs() < 1e-6);
assert!((normalized[1] - 0.5).abs() < 1e-6);
assert!((normalized[2] - 1.0).abs() < 1e-6);
}
#[test]
fn test_curvature_filename() {
assert_eq!(curvature_filename(&Hemisphere::Left, "curv"), "lh.curv");
assert_eq!(curvature_filename(&Hemisphere::Right, "sulc"), "rh.sulc");
}
}