Initial commit
This commit is contained in:
@@ -0,0 +1,872 @@
|
||||
//! NIfTI header parsing for NIfTI-1 and NIfTI-2 formats.
|
||||
//!
|
||||
//! NIfTI-1 header is 348 bytes, NIfTI-2 header is 540 bytes.
|
||||
//! Both formats store 3D/4D volumetric data with spatial metadata.
|
||||
|
||||
use crate::error::{MedicalError, MedicalResult};
|
||||
use byteorder::{ByteOrder, LittleEndian, ReadBytesExt};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
/// NIfTI data type codes
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i16)]
|
||||
pub enum NiftiDataType {
|
||||
/// Unknown data type
|
||||
Unknown = 0,
|
||||
/// Binary (1 bit per voxel)
|
||||
Binary = 1,
|
||||
/// Unsigned 8-bit integer
|
||||
UInt8 = 2,
|
||||
/// Signed 16-bit integer
|
||||
Int16 = 4,
|
||||
/// Signed 32-bit integer
|
||||
Int32 = 8,
|
||||
/// 32-bit floating point
|
||||
Float32 = 16,
|
||||
/// 64-bit complex (2x float32)
|
||||
Complex64 = 32,
|
||||
/// 64-bit floating point
|
||||
Float64 = 64,
|
||||
/// RGB (3x uint8)
|
||||
Rgb24 = 128,
|
||||
/// Signed 8-bit integer
|
||||
Int8 = 256,
|
||||
/// Unsigned 16-bit integer
|
||||
UInt16 = 512,
|
||||
/// Unsigned 32-bit integer
|
||||
UInt32 = 768,
|
||||
/// Signed 64-bit integer
|
||||
Int64 = 1024,
|
||||
/// Unsigned 64-bit integer
|
||||
UInt64 = 1280,
|
||||
/// 128-bit floating point
|
||||
Float128 = 1536,
|
||||
/// 128-bit complex (2x float64)
|
||||
Complex128 = 1792,
|
||||
/// 256-bit complex (2x float128)
|
||||
Complex256 = 2048,
|
||||
/// RGBA (4x uint8)
|
||||
Rgba32 = 2304,
|
||||
}
|
||||
|
||||
impl NiftiDataType {
|
||||
/// Create from raw code
|
||||
pub fn from_code(code: i16) -> Option<Self> {
|
||||
match code {
|
||||
0 => Some(Self::Unknown),
|
||||
1 => Some(Self::Binary),
|
||||
2 => Some(Self::UInt8),
|
||||
4 => Some(Self::Int16),
|
||||
8 => Some(Self::Int32),
|
||||
16 => Some(Self::Float32),
|
||||
32 => Some(Self::Complex64),
|
||||
64 => Some(Self::Float64),
|
||||
128 => Some(Self::Rgb24),
|
||||
256 => Some(Self::Int8),
|
||||
512 => Some(Self::UInt16),
|
||||
768 => Some(Self::UInt32),
|
||||
1024 => Some(Self::Int64),
|
||||
1280 => Some(Self::UInt64),
|
||||
1536 => Some(Self::Float128),
|
||||
1792 => Some(Self::Complex128),
|
||||
2048 => Some(Self::Complex256),
|
||||
2304 => Some(Self::Rgba32),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of bytes per voxel
|
||||
pub fn bytes_per_voxel(&self) -> usize {
|
||||
match self {
|
||||
Self::Unknown => 0,
|
||||
Self::Binary => 1, // Stored as bytes, 1 bit per voxel logically
|
||||
Self::UInt8 | Self::Int8 => 1,
|
||||
Self::Int16 | Self::UInt16 => 2,
|
||||
Self::Int32 | Self::UInt32 | Self::Float32 => 4,
|
||||
Self::Float64 | Self::Int64 | Self::UInt64 | Self::Complex64 => 8,
|
||||
Self::Rgb24 => 3,
|
||||
Self::Rgba32 => 4,
|
||||
Self::Float128 | Self::Complex128 => 16,
|
||||
Self::Complex256 => 32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a human-readable name
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Unknown => "unknown",
|
||||
Self::Binary => "binary",
|
||||
Self::UInt8 => "uint8",
|
||||
Self::Int8 => "int8",
|
||||
Self::Int16 => "int16",
|
||||
Self::UInt16 => "uint16",
|
||||
Self::Int32 => "int32",
|
||||
Self::UInt32 => "uint32",
|
||||
Self::Int64 => "int64",
|
||||
Self::UInt64 => "uint64",
|
||||
Self::Float32 => "float32",
|
||||
Self::Float64 => "float64",
|
||||
Self::Float128 => "float128",
|
||||
Self::Complex64 => "complex64",
|
||||
Self::Complex128 => "complex128",
|
||||
Self::Complex256 => "complex256",
|
||||
Self::Rgb24 => "rgb24",
|
||||
Self::Rgba32 => "rgba32",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform code for sform/qform
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[repr(i16)]
|
||||
pub enum TransformCode {
|
||||
/// Unknown coordinate system
|
||||
#[default]
|
||||
Unknown = 0,
|
||||
/// Scanner-based anatomical coordinates
|
||||
ScannerAnat = 1,
|
||||
/// Coordinates aligned to another file
|
||||
AlignedAnat = 2,
|
||||
/// Talairach space
|
||||
Talairach = 3,
|
||||
/// MNI-152 space
|
||||
Mni152 = 4,
|
||||
/// Template-other space
|
||||
TemplateOther = 5,
|
||||
}
|
||||
|
||||
impl TransformCode {
|
||||
/// Create from raw code
|
||||
pub fn from_code(code: i16) -> Self {
|
||||
match code {
|
||||
1 => Self::ScannerAnat,
|
||||
2 => Self::AlignedAnat,
|
||||
3 => Self::Talairach,
|
||||
4 => Self::Mni152,
|
||||
5 => Self::TemplateOther,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Units for spatial dimensions
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum SpatialUnits {
|
||||
/// Unknown units
|
||||
#[default]
|
||||
Unknown,
|
||||
/// Meters
|
||||
Meters,
|
||||
/// Millimeters
|
||||
Millimeters,
|
||||
/// Micrometers
|
||||
Micrometers,
|
||||
}
|
||||
|
||||
impl SpatialUnits {
|
||||
/// Create from xyzt_units field (lower 3 bits)
|
||||
pub fn from_code(code: u8) -> Self {
|
||||
match code & 0x07 {
|
||||
1 => Self::Meters,
|
||||
2 => Self::Millimeters,
|
||||
3 => Self::Micrometers,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get conversion factor to millimeters
|
||||
pub fn to_mm_factor(&self) -> f64 {
|
||||
match self {
|
||||
Self::Unknown => 1.0,
|
||||
Self::Meters => 1000.0,
|
||||
Self::Millimeters => 1.0,
|
||||
Self::Micrometers => 0.001,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Units for temporal dimension
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum TemporalUnits {
|
||||
/// Unknown units
|
||||
#[default]
|
||||
Unknown,
|
||||
/// Seconds
|
||||
Seconds,
|
||||
/// Milliseconds
|
||||
Milliseconds,
|
||||
/// Microseconds
|
||||
Microseconds,
|
||||
/// Hertz
|
||||
Hertz,
|
||||
/// Parts per million
|
||||
Ppm,
|
||||
/// Radians per second
|
||||
Rads,
|
||||
}
|
||||
|
||||
impl TemporalUnits {
|
||||
/// Create from xyzt_units field (bits 3-5)
|
||||
pub fn from_code(code: u8) -> Self {
|
||||
match (code >> 3) & 0x07 {
|
||||
1 => Self::Seconds,
|
||||
2 => Self::Milliseconds,
|
||||
3 => Self::Microseconds,
|
||||
4 => Self::Hertz,
|
||||
5 => Self::Ppm,
|
||||
6 => Self::Rads,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// NIfTI header (unified for NIfTI-1 and NIfTI-2)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NiftiHeader {
|
||||
/// Header size (348 for NIfTI-1, 540 for NIfTI-2)
|
||||
pub sizeof_hdr: i32,
|
||||
|
||||
/// Data type
|
||||
pub datatype: NiftiDataType,
|
||||
|
||||
/// Bits per voxel
|
||||
pub bitpix: i16,
|
||||
|
||||
/// Dimensions: [ndim, dim1, dim2, dim3, dim4, dim5, dim6, dim7]
|
||||
pub dim: [i64; 8],
|
||||
|
||||
/// Intent parameters (for statistical data)
|
||||
pub intent_p1: f64,
|
||||
/// Intent parameter 2
|
||||
pub intent_p2: f64,
|
||||
/// Intent parameter 3
|
||||
pub intent_p3: f64,
|
||||
/// Intent code
|
||||
pub intent_code: i16,
|
||||
|
||||
/// Voxel dimensions (spacing): [_, pixdim1, pixdim2, pixdim3, ...]
|
||||
pub pixdim: [f64; 8],
|
||||
|
||||
/// Offset to voxel data in file
|
||||
pub vox_offset: i64,
|
||||
|
||||
/// Data scaling: slope
|
||||
pub scl_slope: f64,
|
||||
|
||||
/// Data scaling: intercept
|
||||
pub scl_inter: f64,
|
||||
|
||||
/// Slice timing order code
|
||||
pub slice_code: u8,
|
||||
|
||||
/// Units for xyzt dimensions
|
||||
pub xyzt_units: u8,
|
||||
|
||||
/// Maximum value in data (informational)
|
||||
pub cal_max: f64,
|
||||
|
||||
/// Minimum value in data (informational)
|
||||
pub cal_min: f64,
|
||||
|
||||
/// Slice duration
|
||||
pub slice_duration: f64,
|
||||
|
||||
/// Time axis shift
|
||||
pub toffset: f64,
|
||||
|
||||
/// First slice index
|
||||
pub slice_start: i64,
|
||||
|
||||
/// Last slice index
|
||||
pub slice_end: i64,
|
||||
|
||||
/// Description string (max 80 chars)
|
||||
pub descrip: String,
|
||||
|
||||
/// Auxiliary filename
|
||||
pub aux_file: String,
|
||||
|
||||
/// QForm transform code
|
||||
pub qform_code: TransformCode,
|
||||
|
||||
/// SForm transform code
|
||||
pub sform_code: TransformCode,
|
||||
|
||||
/// Quaternion parameters for qform
|
||||
pub quatern_b: f64,
|
||||
/// Quaternion c component
|
||||
pub quatern_c: f64,
|
||||
/// Quaternion d component
|
||||
pub quatern_d: f64,
|
||||
/// Quaternion offset x
|
||||
pub qoffset_x: f64,
|
||||
/// Quaternion offset y
|
||||
pub qoffset_y: f64,
|
||||
/// Quaternion offset z
|
||||
pub qoffset_z: f64,
|
||||
|
||||
/// Affine matrix rows for sform
|
||||
pub srow_x: [f64; 4],
|
||||
/// Affine matrix row y
|
||||
pub srow_y: [f64; 4],
|
||||
/// Affine matrix row z
|
||||
pub srow_z: [f64; 4],
|
||||
|
||||
/// Intent name
|
||||
pub intent_name: String,
|
||||
|
||||
/// Magic bytes (determines NIfTI-1 vs NIfTI-2)
|
||||
pub magic: [u8; 8],
|
||||
|
||||
/// Whether this is NIfTI-2 format
|
||||
pub is_nifti2: bool,
|
||||
|
||||
/// Byte order (true = little endian)
|
||||
pub little_endian: bool,
|
||||
}
|
||||
|
||||
impl Default for NiftiHeader {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sizeof_hdr: 348,
|
||||
datatype: NiftiDataType::Float32,
|
||||
bitpix: 32,
|
||||
dim: [3, 1, 1, 1, 1, 1, 1, 1],
|
||||
intent_p1: 0.0,
|
||||
intent_p2: 0.0,
|
||||
intent_p3: 0.0,
|
||||
intent_code: 0,
|
||||
pixdim: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
|
||||
vox_offset: 352,
|
||||
scl_slope: 1.0,
|
||||
scl_inter: 0.0,
|
||||
slice_code: 0,
|
||||
xyzt_units: 2, // mm
|
||||
cal_max: 0.0,
|
||||
cal_min: 0.0,
|
||||
slice_duration: 0.0,
|
||||
toffset: 0.0,
|
||||
slice_start: 0,
|
||||
slice_end: 0,
|
||||
descrip: String::new(),
|
||||
aux_file: String::new(),
|
||||
qform_code: TransformCode::Unknown,
|
||||
sform_code: TransformCode::Unknown,
|
||||
quatern_b: 0.0,
|
||||
quatern_c: 0.0,
|
||||
quatern_d: 0.0,
|
||||
qoffset_x: 0.0,
|
||||
qoffset_y: 0.0,
|
||||
qoffset_z: 0.0,
|
||||
srow_x: [1.0, 0.0, 0.0, 0.0],
|
||||
srow_y: [0.0, 1.0, 0.0, 0.0],
|
||||
srow_z: [0.0, 0.0, 1.0, 0.0],
|
||||
intent_name: String::new(),
|
||||
magic: *b"n+1\0\0\0\0\0",
|
||||
is_nifti2: false,
|
||||
little_endian: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NiftiHeader {
|
||||
/// Get spatial dimensions (x, y, z)
|
||||
pub fn shape(&self) -> (usize, usize, usize) {
|
||||
(
|
||||
self.dim[1] as usize,
|
||||
self.dim[2] as usize,
|
||||
self.dim[3] as usize,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get voxel spacing (dx, dy, dz) in mm
|
||||
pub fn spacing(&self) -> (f64, f64, f64) {
|
||||
let factor = SpatialUnits::from_code(self.xyzt_units).to_mm_factor();
|
||||
(
|
||||
self.pixdim[1].abs() * factor,
|
||||
self.pixdim[2].abs() * factor,
|
||||
self.pixdim[3].abs() * factor,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the number of dimensions
|
||||
pub fn ndim(&self) -> usize {
|
||||
self.dim[0] as usize
|
||||
}
|
||||
|
||||
/// Get total number of voxels
|
||||
pub fn num_voxels(&self) -> usize {
|
||||
let ndim = self.ndim();
|
||||
let mut total = 1usize;
|
||||
for i in 1..=ndim {
|
||||
total *= self.dim[i] as usize;
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// Get total data size in bytes
|
||||
pub fn data_size(&self) -> usize {
|
||||
self.num_voxels() * self.datatype.bytes_per_voxel()
|
||||
}
|
||||
|
||||
/// Get the 4x4 affine transformation matrix.
|
||||
/// Prefers sform if available, falls back to qform, then identity.
|
||||
pub fn affine(&self) -> [[f64; 4]; 4] {
|
||||
if self.sform_code != TransformCode::Unknown {
|
||||
// Use sform
|
||||
[self.srow_x, self.srow_y, self.srow_z, [0.0, 0.0, 0.0, 1.0]]
|
||||
} else if self.qform_code != TransformCode::Unknown {
|
||||
// Use qform (quaternion to rotation matrix)
|
||||
self.qform_to_affine()
|
||||
} else {
|
||||
// Identity with spacing
|
||||
let (dx, dy, dz) = self.spacing();
|
||||
[
|
||||
[dx, 0.0, 0.0, 0.0],
|
||||
[0.0, dy, 0.0, 0.0],
|
||||
[0.0, 0.0, dz, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert quaternion (qform) to affine matrix
|
||||
fn qform_to_affine(&self) -> [[f64; 4]; 4] {
|
||||
let b = self.quatern_b;
|
||||
let c = self.quatern_c;
|
||||
let d = self.quatern_d;
|
||||
|
||||
// Compute a (quaternion w component)
|
||||
let a = (1.0 - b * b - c * c - d * d).max(0.0).sqrt();
|
||||
|
||||
// Rotation matrix from quaternion
|
||||
let r11 = a * a + b * b - c * c - d * d;
|
||||
let r12 = 2.0 * (b * c - a * d);
|
||||
let r13 = 2.0 * (b * d + a * c);
|
||||
let r21 = 2.0 * (b * c + a * d);
|
||||
let r22 = a * a + c * c - b * b - d * d;
|
||||
let r23 = 2.0 * (c * d - a * b);
|
||||
let r31 = 2.0 * (b * d - a * c);
|
||||
let r32 = 2.0 * (c * d + a * b);
|
||||
let r33 = a * a + d * d - b * b - c * c;
|
||||
|
||||
// Apply scaling (pixdim)
|
||||
let (dx, dy, dz) = self.spacing();
|
||||
|
||||
// Handle qfac (sign of pixdim[0] determines handedness)
|
||||
let qfac = if self.pixdim[0] < 0.0 { -1.0 } else { 1.0 };
|
||||
|
||||
[
|
||||
[r11 * dx, r12 * dy, r13 * dz * qfac, self.qoffset_x],
|
||||
[r21 * dx, r22 * dy, r23 * dz * qfac, self.qoffset_y],
|
||||
[r31 * dx, r32 * dy, r33 * dz * qfac, self.qoffset_z],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
]
|
||||
}
|
||||
|
||||
/// Get origin (translation from affine)
|
||||
pub fn origin(&self) -> (f64, f64, f64) {
|
||||
let affine = self.affine();
|
||||
(affine[0][3], affine[1][3], affine[2][3])
|
||||
}
|
||||
|
||||
/// Read NIfTI header from a reader
|
||||
pub fn read<R: Read + Seek>(reader: &mut R) -> MedicalResult<Self> {
|
||||
// Read first 4 bytes to determine header size
|
||||
let sizeof_hdr = reader.read_i32::<LittleEndian>()?;
|
||||
|
||||
// Check if we need to swap bytes
|
||||
let (sizeof_hdr, little_endian) = if sizeof_hdr == 348 || sizeof_hdr == 540 {
|
||||
(sizeof_hdr, true)
|
||||
} else {
|
||||
let swapped = sizeof_hdr.swap_bytes();
|
||||
if swapped == 348 || swapped == 540 {
|
||||
(swapped, false)
|
||||
} else {
|
||||
return Err(MedicalError::Nifti(format!(
|
||||
"Invalid NIfTI header size: expected 348 or 540, got {}",
|
||||
sizeof_hdr
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// Seek back to start
|
||||
reader.seek(SeekFrom::Start(0))?;
|
||||
|
||||
if sizeof_hdr == 348 {
|
||||
Self::read_nifti1(reader, little_endian)
|
||||
} else {
|
||||
Self::read_nifti2(reader, little_endian)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read NIfTI-1 header (348 bytes)
|
||||
fn read_nifti1<R: Read>(reader: &mut R, little_endian: bool) -> MedicalResult<Self> {
|
||||
let mut buf = [0u8; 348];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let read_i16 = |offset: usize| -> i16 {
|
||||
if little_endian {
|
||||
LittleEndian::read_i16(&buf[offset..])
|
||||
} else {
|
||||
byteorder::BigEndian::read_i16(&buf[offset..])
|
||||
}
|
||||
};
|
||||
|
||||
let read_i32 = |offset: usize| -> i32 {
|
||||
if little_endian {
|
||||
LittleEndian::read_i32(&buf[offset..])
|
||||
} else {
|
||||
byteorder::BigEndian::read_i32(&buf[offset..])
|
||||
}
|
||||
};
|
||||
|
||||
let read_f32 = |offset: usize| -> f32 {
|
||||
if little_endian {
|
||||
LittleEndian::read_f32(&buf[offset..])
|
||||
} else {
|
||||
byteorder::BigEndian::read_f32(&buf[offset..])
|
||||
}
|
||||
};
|
||||
|
||||
let sizeof_hdr = read_i32(0);
|
||||
|
||||
// dim array at offset 40 (8 x i16)
|
||||
let mut dim = [0i64; 8];
|
||||
for i in 0..8 {
|
||||
dim[i] = read_i16(40 + i * 2) as i64;
|
||||
}
|
||||
|
||||
// intent_p1, p2, p3 at offsets 56, 60, 64
|
||||
let intent_p1 = read_f32(56) as f64;
|
||||
let intent_p2 = read_f32(60) as f64;
|
||||
let intent_p3 = read_f32(64) as f64;
|
||||
|
||||
// intent_code at 68
|
||||
let intent_code = read_i16(68);
|
||||
|
||||
// datatype at 70
|
||||
let datatype_code = read_i16(70);
|
||||
let datatype = NiftiDataType::from_code(datatype_code).ok_or_else(|| {
|
||||
MedicalError::Nifti(format!(
|
||||
"Unsupported NIfTI data type code: {}",
|
||||
datatype_code
|
||||
))
|
||||
})?;
|
||||
|
||||
// bitpix at 72
|
||||
let bitpix = read_i16(72);
|
||||
|
||||
// slice_start at 74
|
||||
let slice_start = read_i16(74) as i64;
|
||||
|
||||
// pixdim at 76 (8 x f32)
|
||||
let mut pixdim = [0.0f64; 8];
|
||||
for i in 0..8 {
|
||||
pixdim[i] = read_f32(76 + i * 4) as f64;
|
||||
}
|
||||
|
||||
// vox_offset at 108
|
||||
let vox_offset = read_f32(108) as i64;
|
||||
|
||||
// scl_slope at 112, scl_inter at 116
|
||||
let scl_slope = read_f32(112) as f64;
|
||||
let scl_inter = read_f32(116) as f64;
|
||||
|
||||
// slice_end at 120
|
||||
let slice_end = read_i16(120) as i64;
|
||||
|
||||
// slice_code at 122
|
||||
let slice_code = buf[122];
|
||||
|
||||
// xyzt_units at 123
|
||||
let xyzt_units = buf[123];
|
||||
|
||||
// cal_max at 124, cal_min at 128
|
||||
let cal_max = read_f32(124) as f64;
|
||||
let cal_min = read_f32(128) as f64;
|
||||
|
||||
// slice_duration at 132
|
||||
let slice_duration = read_f32(132) as f64;
|
||||
|
||||
// toffset at 136
|
||||
let toffset = read_f32(136) as f64;
|
||||
|
||||
// descrip at 148 (80 bytes)
|
||||
let descrip = String::from_utf8_lossy(&buf[148..228])
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
|
||||
// aux_file at 228 (24 bytes)
|
||||
let aux_file = String::from_utf8_lossy(&buf[228..252])
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
|
||||
// qform_code at 252, sform_code at 254
|
||||
let qform_code = TransformCode::from_code(read_i16(252));
|
||||
let sform_code = TransformCode::from_code(read_i16(254));
|
||||
|
||||
// Quaternion at 256-279
|
||||
let quatern_b = read_f32(256) as f64;
|
||||
let quatern_c = read_f32(260) as f64;
|
||||
let quatern_d = read_f32(264) as f64;
|
||||
let qoffset_x = read_f32(268) as f64;
|
||||
let qoffset_y = read_f32(272) as f64;
|
||||
let qoffset_z = read_f32(276) as f64;
|
||||
|
||||
// Sform rows at 280-327
|
||||
let mut srow_x = [0.0f64; 4];
|
||||
let mut srow_y = [0.0f64; 4];
|
||||
let mut srow_z = [0.0f64; 4];
|
||||
for i in 0..4 {
|
||||
srow_x[i] = read_f32(280 + i * 4) as f64;
|
||||
srow_y[i] = read_f32(296 + i * 4) as f64;
|
||||
srow_z[i] = read_f32(312 + i * 4) as f64;
|
||||
}
|
||||
|
||||
// intent_name at 328 (16 bytes)
|
||||
let intent_name = String::from_utf8_lossy(&buf[328..344])
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
|
||||
// magic at 344 (4 bytes for NIfTI-1)
|
||||
let mut magic = [0u8; 8];
|
||||
magic[..4].copy_from_slice(&buf[344..348]);
|
||||
|
||||
// Validate magic
|
||||
if &magic[..3] != b"n+1" && &magic[..3] != b"ni1" {
|
||||
return Err(MedicalError::Nifti(format!(
|
||||
"Invalid NIfTI magic bytes: expected 'n+1' or 'ni1', got {:?}",
|
||||
&magic[..4]
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
sizeof_hdr,
|
||||
datatype,
|
||||
bitpix,
|
||||
dim,
|
||||
intent_p1,
|
||||
intent_p2,
|
||||
intent_p3,
|
||||
intent_code,
|
||||
pixdim,
|
||||
vox_offset,
|
||||
scl_slope,
|
||||
scl_inter,
|
||||
slice_code,
|
||||
xyzt_units,
|
||||
cal_max,
|
||||
cal_min,
|
||||
slice_duration,
|
||||
toffset,
|
||||
slice_start,
|
||||
slice_end,
|
||||
descrip,
|
||||
aux_file,
|
||||
qform_code,
|
||||
sform_code,
|
||||
quatern_b,
|
||||
quatern_c,
|
||||
quatern_d,
|
||||
qoffset_x,
|
||||
qoffset_y,
|
||||
qoffset_z,
|
||||
srow_x,
|
||||
srow_y,
|
||||
srow_z,
|
||||
intent_name,
|
||||
magic,
|
||||
is_nifti2: false,
|
||||
little_endian,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read NIfTI-2 header (540 bytes)
|
||||
fn read_nifti2<R: Read>(reader: &mut R, little_endian: bool) -> MedicalResult<Self> {
|
||||
let mut buf = [0u8; 540];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let read_i16 = |offset: usize| -> i16 {
|
||||
if little_endian {
|
||||
LittleEndian::read_i16(&buf[offset..])
|
||||
} else {
|
||||
byteorder::BigEndian::read_i16(&buf[offset..])
|
||||
}
|
||||
};
|
||||
|
||||
let read_i32 = |offset: usize| -> i32 {
|
||||
if little_endian {
|
||||
LittleEndian::read_i32(&buf[offset..])
|
||||
} else {
|
||||
byteorder::BigEndian::read_i32(&buf[offset..])
|
||||
}
|
||||
};
|
||||
|
||||
let read_i64 = |offset: usize| -> i64 {
|
||||
if little_endian {
|
||||
LittleEndian::read_i64(&buf[offset..])
|
||||
} else {
|
||||
byteorder::BigEndian::read_i64(&buf[offset..])
|
||||
}
|
||||
};
|
||||
|
||||
let read_f64 = |offset: usize| -> f64 {
|
||||
if little_endian {
|
||||
LittleEndian::read_f64(&buf[offset..])
|
||||
} else {
|
||||
byteorder::BigEndian::read_f64(&buf[offset..])
|
||||
}
|
||||
};
|
||||
|
||||
let sizeof_hdr = read_i32(0);
|
||||
|
||||
// magic at 4 (8 bytes for NIfTI-2)
|
||||
let mut magic = [0u8; 8];
|
||||
magic.copy_from_slice(&buf[4..12]);
|
||||
|
||||
// Validate magic
|
||||
if &magic[..3] != b"n+2" && &magic[..3] != b"ni2" {
|
||||
return Err(MedicalError::Nifti(format!(
|
||||
"Invalid NIfTI-2 magic bytes: expected 'n+2' or 'ni2', got {:?}",
|
||||
&magic
|
||||
)));
|
||||
}
|
||||
|
||||
// datatype at 12
|
||||
let datatype_code = read_i16(12);
|
||||
let datatype = NiftiDataType::from_code(datatype_code).ok_or_else(|| {
|
||||
MedicalError::Nifti(format!(
|
||||
"Unsupported NIfTI data type code: {}",
|
||||
datatype_code
|
||||
))
|
||||
})?;
|
||||
|
||||
// bitpix at 14
|
||||
let bitpix = read_i16(14);
|
||||
|
||||
// dim at 16 (8 x i64)
|
||||
let mut dim = [0i64; 8];
|
||||
for i in 0..8 {
|
||||
dim[i] = read_i64(16 + i * 8);
|
||||
}
|
||||
|
||||
// intent_p1, p2, p3 at 80, 88, 96
|
||||
let intent_p1 = read_f64(80);
|
||||
let intent_p2 = read_f64(88);
|
||||
let intent_p3 = read_f64(96);
|
||||
|
||||
// pixdim at 104 (8 x f64)
|
||||
let mut pixdim = [0.0f64; 8];
|
||||
for i in 0..8 {
|
||||
pixdim[i] = read_f64(104 + i * 8);
|
||||
}
|
||||
|
||||
// vox_offset at 168
|
||||
let vox_offset = read_i64(168);
|
||||
|
||||
// scl_slope at 176, scl_inter at 184
|
||||
let scl_slope = read_f64(176);
|
||||
let scl_inter = read_f64(184);
|
||||
|
||||
// cal_max at 192, cal_min at 200
|
||||
let cal_max = read_f64(192);
|
||||
let cal_min = read_f64(200);
|
||||
|
||||
// slice_duration at 208
|
||||
let slice_duration = read_f64(208);
|
||||
|
||||
// toffset at 216
|
||||
let toffset = read_f64(216);
|
||||
|
||||
// slice_start at 224, slice_end at 232
|
||||
let slice_start = read_i64(224);
|
||||
let slice_end = read_i64(232);
|
||||
|
||||
// descrip at 240 (80 bytes)
|
||||
let descrip = String::from_utf8_lossy(&buf[240..320])
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
|
||||
// aux_file at 320 (24 bytes)
|
||||
let aux_file = String::from_utf8_lossy(&buf[320..344])
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
|
||||
// qform_code at 344, sform_code at 348
|
||||
let qform_code = TransformCode::from_code(read_i32(344) as i16);
|
||||
let sform_code = TransformCode::from_code(read_i32(348) as i16);
|
||||
|
||||
// Quaternion at 352-399
|
||||
let quatern_b = read_f64(352);
|
||||
let quatern_c = read_f64(360);
|
||||
let quatern_d = read_f64(368);
|
||||
let qoffset_x = read_f64(376);
|
||||
let qoffset_y = read_f64(384);
|
||||
let qoffset_z = read_f64(392);
|
||||
|
||||
// Sform rows at 400-495
|
||||
let mut srow_x = [0.0f64; 4];
|
||||
let mut srow_y = [0.0f64; 4];
|
||||
let mut srow_z = [0.0f64; 4];
|
||||
for i in 0..4 {
|
||||
srow_x[i] = read_f64(400 + i * 8);
|
||||
srow_y[i] = read_f64(432 + i * 8);
|
||||
srow_z[i] = read_f64(464 + i * 8);
|
||||
}
|
||||
|
||||
// slice_code at 496
|
||||
let slice_code = buf[496];
|
||||
|
||||
// xyzt_units at 497
|
||||
let xyzt_units = buf[497];
|
||||
|
||||
// intent_code at 500
|
||||
let intent_code = read_i32(500) as i16;
|
||||
|
||||
// intent_name at 504 (16 bytes)
|
||||
let intent_name = String::from_utf8_lossy(&buf[504..520])
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
sizeof_hdr,
|
||||
datatype,
|
||||
bitpix,
|
||||
dim,
|
||||
intent_p1,
|
||||
intent_p2,
|
||||
intent_p3,
|
||||
intent_code,
|
||||
pixdim,
|
||||
vox_offset,
|
||||
scl_slope,
|
||||
scl_inter,
|
||||
slice_code,
|
||||
xyzt_units,
|
||||
cal_max,
|
||||
cal_min,
|
||||
slice_duration,
|
||||
toffset,
|
||||
slice_start,
|
||||
slice_end,
|
||||
descrip,
|
||||
aux_file,
|
||||
qform_code,
|
||||
sform_code,
|
||||
quatern_b,
|
||||
quatern_c,
|
||||
quatern_d,
|
||||
qoffset_x,
|
||||
qoffset_y,
|
||||
qoffset_z,
|
||||
srow_x,
|
||||
srow_y,
|
||||
srow_z,
|
||||
intent_name,
|
||||
magic,
|
||||
is_nifti2: true,
|
||||
little_endian,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! NIfTI (Neuroimaging Informatics Technology Initiative) file format support.
|
||||
//!
|
||||
//! This module provides readers and writers for NIfTI-1 and NIfTI-2 formats,
|
||||
//! which are standard file formats for storing neuroimaging data.
|
||||
//!
|
||||
//! # Supported formats
|
||||
//!
|
||||
//! - `.nii` - Uncompressed NIfTI-1 single file
|
||||
//! - `.nii.gz` - Gzip-compressed NIfTI-1 single file
|
||||
//! - NIfTI-2 format (read support)
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use rtx_medical_core::nifti::{read_nifti, write_nifti, NiftiDataType};
|
||||
//!
|
||||
//! // Read a NIfTI file
|
||||
//! let volume = read_nifti("brain.nii.gz")?;
|
||||
//! println!("Shape: {:?}", volume.shape());
|
||||
//! println!("Spacing: {:?}", volume.spacing());
|
||||
//! println!("Origin: {:?}", volume.origin());
|
||||
//!
|
||||
//! // Process the volume...
|
||||
//!
|
||||
//! // Write back to a new file
|
||||
//! write_nifti(
|
||||
//! &volume.data,
|
||||
//! volume.shape(),
|
||||
//! volume.spacing(),
|
||||
//! &volume.affine(),
|
||||
//! "output.nii.gz",
|
||||
//! NiftiDataType::Float32
|
||||
//! )?;
|
||||
//! ```
|
||||
|
||||
pub mod header;
|
||||
pub mod reader;
|
||||
pub mod transform;
|
||||
pub mod writer;
|
||||
|
||||
// Re-exports for convenience
|
||||
pub use header::{NiftiDataType, NiftiHeader, SpatialUnits, TemporalUnits, TransformCode};
|
||||
pub use reader::{NiftiVolume, read_nifti, read_nifti_header};
|
||||
pub use transform::{
|
||||
Affine4, approx_equal, compose, from_flat, from_matrix4, from_spacing_origin, get_origin,
|
||||
get_rotation, get_spacing, identity, inverse, scaling, to_flat, to_matrix4, transform_point,
|
||||
transform_vector, translation, voxel_to_world, world_to_voxel,
|
||||
};
|
||||
pub use writer::write_nifti;
|
||||
@@ -0,0 +1,317 @@
|
||||
//! NIfTI file reader supporting .nii and .nii.gz formats.
|
||||
|
||||
use crate::error::{MedicalError, MedicalResult};
|
||||
use crate::nifti::header::{NiftiDataType, NiftiHeader};
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
use flate2::read::GzDecoder;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
||||
/// NIfTI volume data structure returned by the reader.
|
||||
///
|
||||
/// Contains raw voxel data as f64 and complete header metadata.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NiftiVolume {
|
||||
/// Voxel data in row-major order (z varies slowest, x varies fastest)
|
||||
pub data: Vec<f64>,
|
||||
/// NIfTI header with all metadata
|
||||
pub header: NiftiHeader,
|
||||
}
|
||||
|
||||
impl NiftiVolume {
|
||||
/// Get the volume shape (x, y, z)
|
||||
pub fn shape(&self) -> (usize, usize, usize) {
|
||||
self.header.shape()
|
||||
}
|
||||
|
||||
/// Get voxel spacing in mm (dx, dy, dz)
|
||||
pub fn spacing(&self) -> (f64, f64, f64) {
|
||||
self.header.spacing()
|
||||
}
|
||||
|
||||
/// Get origin in world coordinates (ox, oy, oz)
|
||||
pub fn origin(&self) -> (f64, f64, f64) {
|
||||
self.header.origin()
|
||||
}
|
||||
|
||||
/// Get the 4x4 affine transformation matrix
|
||||
pub fn affine(&self) -> [[f64; 4]; 4] {
|
||||
self.header.affine()
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a NIfTI file from disk.
|
||||
///
|
||||
/// Supports both .nii and .nii.gz files. The data is returned as a `NiftiVolume`
|
||||
/// with the voxel data converted to f64.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `path` - Path to the NIfTI file
|
||||
///
|
||||
/// # Returns
|
||||
/// A `NiftiVolume` containing the image data and metadata
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use rtx_medical_core::nifti::read_nifti;
|
||||
///
|
||||
/// let volume = read_nifti("brain.nii.gz")?;
|
||||
/// println!("Shape: {:?}", volume.shape());
|
||||
/// println!("Spacing: {:?}", volume.spacing());
|
||||
/// ```
|
||||
pub fn read_nifti<P: AsRef<Path>>(path: P) -> MedicalResult<NiftiVolume> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !path.exists() {
|
||||
return Err(MedicalError::Nifti(format!(
|
||||
"File not found: {}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let path_str = path.to_string_lossy().to_lowercase();
|
||||
let is_gzipped = path_str.ends_with(".nii.gz") || path_str.ends_with(".gz");
|
||||
|
||||
if is_gzipped {
|
||||
read_nifti_gz(path)
|
||||
} else if path_str.ends_with(".nii") {
|
||||
read_nifti_uncompressed(path)
|
||||
} else {
|
||||
Err(MedicalError::InvalidFormat(format!(
|
||||
"Invalid file extension: expected .nii or .nii.gz, got {:?}",
|
||||
path.extension()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a gzip-compressed NIfTI file (.nii.gz)
|
||||
fn read_nifti_gz<P: AsRef<Path>>(path: P) -> MedicalResult<NiftiVolume> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let mut decoder = GzDecoder::new(reader);
|
||||
|
||||
// Read entire decompressed content into memory
|
||||
let mut data = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| MedicalError::Nifti(format!("Failed to decompress gzip: {}", e)))?;
|
||||
|
||||
// Parse from memory buffer
|
||||
let mut cursor = Cursor::new(data);
|
||||
read_nifti_from_reader(&mut cursor)
|
||||
}
|
||||
|
||||
/// Read an uncompressed NIfTI file (.nii)
|
||||
fn read_nifti_uncompressed<P: AsRef<Path>>(path: P) -> MedicalResult<NiftiVolume> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
read_nifti_from_reader(&mut reader)
|
||||
}
|
||||
|
||||
/// Read NIfTI from any reader that implements Read + Seek
|
||||
fn read_nifti_from_reader<R: Read + Seek>(reader: &mut R) -> MedicalResult<NiftiVolume> {
|
||||
// Read header
|
||||
let header = NiftiHeader::read(reader)?;
|
||||
|
||||
// Seek to voxel data
|
||||
reader.seek(SeekFrom::Start(header.vox_offset as u64))?;
|
||||
|
||||
// Read voxel data
|
||||
let data_size = header.data_size();
|
||||
let mut raw_data = vec![0u8; data_size];
|
||||
reader.read_exact(&mut raw_data)?;
|
||||
|
||||
// Convert to f64 based on data type
|
||||
let data = convert_to_f64(&raw_data, &header)?;
|
||||
|
||||
// Apply scaling if needed
|
||||
let data = if header.scl_slope != 0.0 && (header.scl_slope != 1.0 || header.scl_inter != 0.0) {
|
||||
data.iter()
|
||||
.map(|&v| v * header.scl_slope + header.scl_inter)
|
||||
.collect()
|
||||
} else {
|
||||
data
|
||||
};
|
||||
|
||||
Ok(NiftiVolume { data, header })
|
||||
}
|
||||
|
||||
/// Convert raw bytes to f64 based on NIfTI data type
|
||||
fn convert_to_f64(raw: &[u8], header: &NiftiHeader) -> MedicalResult<Vec<f64>> {
|
||||
let num_voxels = header.num_voxels();
|
||||
let mut result = Vec::with_capacity(num_voxels);
|
||||
let little_endian = header.little_endian;
|
||||
|
||||
match header.datatype {
|
||||
NiftiDataType::UInt8 => {
|
||||
for &byte in raw.iter().take(num_voxels) {
|
||||
result.push(byte as f64);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Int8 => {
|
||||
for &byte in raw.iter().take(num_voxels) {
|
||||
result.push((byte as i8) as f64);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Int16 => {
|
||||
for chunk in raw.chunks_exact(2).take(num_voxels) {
|
||||
let val = if little_endian {
|
||||
LittleEndian::read_i16(chunk)
|
||||
} else {
|
||||
byteorder::BigEndian::read_i16(chunk)
|
||||
};
|
||||
result.push(val as f64);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::UInt16 => {
|
||||
for chunk in raw.chunks_exact(2).take(num_voxels) {
|
||||
let val = if little_endian {
|
||||
LittleEndian::read_u16(chunk)
|
||||
} else {
|
||||
byteorder::BigEndian::read_u16(chunk)
|
||||
};
|
||||
result.push(val as f64);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Int32 => {
|
||||
for chunk in raw.chunks_exact(4).take(num_voxels) {
|
||||
let val = if little_endian {
|
||||
LittleEndian::read_i32(chunk)
|
||||
} else {
|
||||
byteorder::BigEndian::read_i32(chunk)
|
||||
};
|
||||
result.push(val as f64);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::UInt32 => {
|
||||
for chunk in raw.chunks_exact(4).take(num_voxels) {
|
||||
let val = if little_endian {
|
||||
LittleEndian::read_u32(chunk)
|
||||
} else {
|
||||
byteorder::BigEndian::read_u32(chunk)
|
||||
};
|
||||
result.push(val as f64);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Float32 => {
|
||||
for chunk in raw.chunks_exact(4).take(num_voxels) {
|
||||
let val = if little_endian {
|
||||
LittleEndian::read_f32(chunk)
|
||||
} else {
|
||||
byteorder::BigEndian::read_f32(chunk)
|
||||
};
|
||||
result.push(val as f64);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Float64 => {
|
||||
for chunk in raw.chunks_exact(8).take(num_voxels) {
|
||||
let val = if little_endian {
|
||||
LittleEndian::read_f64(chunk)
|
||||
} else {
|
||||
byteorder::BigEndian::read_f64(chunk)
|
||||
};
|
||||
result.push(val);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Int64 => {
|
||||
for chunk in raw.chunks_exact(8).take(num_voxels) {
|
||||
let val = if little_endian {
|
||||
LittleEndian::read_i64(chunk)
|
||||
} else {
|
||||
byteorder::BigEndian::read_i64(chunk)
|
||||
};
|
||||
result.push(val as f64);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::UInt64 => {
|
||||
for chunk in raw.chunks_exact(8).take(num_voxels) {
|
||||
let val = if little_endian {
|
||||
LittleEndian::read_u64(chunk)
|
||||
} else {
|
||||
byteorder::BigEndian::read_u64(chunk)
|
||||
};
|
||||
result.push(val as f64);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(MedicalError::Nifti(format!(
|
||||
"Unsupported NIfTI data type: {}",
|
||||
header.datatype.name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Read only the NIfTI header without loading voxel data.
|
||||
///
|
||||
/// Useful for quickly checking image metadata without loading the full volume.
|
||||
pub fn read_nifti_header<P: AsRef<Path>>(path: P) -> MedicalResult<NiftiHeader> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !path.exists() {
|
||||
return Err(MedicalError::Nifti(format!(
|
||||
"File not found: {}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let path_str = path.to_string_lossy().to_lowercase();
|
||||
let is_gzipped = path_str.ends_with(".nii.gz") || path_str.ends_with(".gz");
|
||||
|
||||
if is_gzipped {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let mut decoder = GzDecoder::new(reader);
|
||||
|
||||
// Only read enough for the header (540 bytes covers both NIfTI-1 and NIfTI-2)
|
||||
let mut data = vec![0u8; 540];
|
||||
decoder
|
||||
.read_exact(&mut data)
|
||||
.map_err(|e| MedicalError::Nifti(format!("Failed to decompress header: {}", e)))?;
|
||||
|
||||
let mut cursor = Cursor::new(data);
|
||||
NiftiHeader::read(&mut cursor)
|
||||
} else {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
NiftiHeader::read(&mut reader)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_gzip() {
|
||||
assert!("test.nii.gz".to_lowercase().ends_with(".nii.gz"));
|
||||
assert!("TEST.NII.GZ".to_lowercase().ends_with(".nii.gz"));
|
||||
assert!(!"test.nii".to_lowercase().ends_with(".nii.gz"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nifti_volume_shape() {
|
||||
let header = NiftiHeader {
|
||||
dim: [3, 64, 64, 32, 1, 1, 1, 1],
|
||||
..Default::default()
|
||||
};
|
||||
let volume = NiftiVolume {
|
||||
data: vec![0.0; 64 * 64 * 32],
|
||||
header,
|
||||
};
|
||||
assert_eq!(volume.shape(), (64, 64, 32));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
//! Affine transformation utilities for medical imaging.
|
||||
//!
|
||||
//! Provides functions for working with 4x4 affine transformation matrices
|
||||
//! used in NIfTI files to map voxel coordinates to world coordinates.
|
||||
|
||||
use nalgebra::{Matrix4, Point3, Vector3};
|
||||
|
||||
/// A 4x4 affine transformation matrix.
|
||||
pub type Affine4 = [[f64; 4]; 4];
|
||||
|
||||
/// Convert array-based affine to nalgebra Matrix4
|
||||
pub fn to_matrix4(affine: &Affine4) -> Matrix4<f64> {
|
||||
Matrix4::from_row_slice(&[
|
||||
affine[0][0],
|
||||
affine[0][1],
|
||||
affine[0][2],
|
||||
affine[0][3],
|
||||
affine[1][0],
|
||||
affine[1][1],
|
||||
affine[1][2],
|
||||
affine[1][3],
|
||||
affine[2][0],
|
||||
affine[2][1],
|
||||
affine[2][2],
|
||||
affine[2][3],
|
||||
affine[3][0],
|
||||
affine[3][1],
|
||||
affine[3][2],
|
||||
affine[3][3],
|
||||
])
|
||||
}
|
||||
|
||||
/// Convert nalgebra Matrix4 to array-based affine
|
||||
pub fn from_matrix4(mat: &Matrix4<f64>) -> Affine4 {
|
||||
[
|
||||
[mat[(0, 0)], mat[(0, 1)], mat[(0, 2)], mat[(0, 3)]],
|
||||
[mat[(1, 0)], mat[(1, 1)], mat[(1, 2)], mat[(1, 3)]],
|
||||
[mat[(2, 0)], mat[(2, 1)], mat[(2, 2)], mat[(2, 3)]],
|
||||
[mat[(3, 0)], mat[(3, 1)], mat[(3, 2)], mat[(3, 3)]],
|
||||
]
|
||||
}
|
||||
|
||||
/// Create an identity affine transformation
|
||||
pub fn identity() -> Affine4 {
|
||||
[
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
]
|
||||
}
|
||||
|
||||
/// Create a scaling affine transformation
|
||||
pub fn scaling(sx: f64, sy: f64, sz: f64) -> Affine4 {
|
||||
[
|
||||
[sx, 0.0, 0.0, 0.0],
|
||||
[0.0, sy, 0.0, 0.0],
|
||||
[0.0, 0.0, sz, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
]
|
||||
}
|
||||
|
||||
/// Create a translation affine transformation
|
||||
pub fn translation(tx: f64, ty: f64, tz: f64) -> Affine4 {
|
||||
[
|
||||
[1.0, 0.0, 0.0, tx],
|
||||
[0.0, 1.0, 0.0, ty],
|
||||
[0.0, 0.0, 1.0, tz],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
]
|
||||
}
|
||||
|
||||
/// Create an affine from spacing and origin
|
||||
pub fn from_spacing_origin(spacing: [f64; 3], origin: [f64; 3]) -> Affine4 {
|
||||
[
|
||||
[spacing[0], 0.0, 0.0, origin[0]],
|
||||
[0.0, spacing[1], 0.0, origin[1]],
|
||||
[0.0, 0.0, spacing[2], origin[2]],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
]
|
||||
}
|
||||
|
||||
/// Compose two affine transformations: result = a * b
|
||||
pub fn compose(a: &Affine4, b: &Affine4) -> Affine4 {
|
||||
let ma = to_matrix4(a);
|
||||
let mb = to_matrix4(b);
|
||||
from_matrix4(&(ma * mb))
|
||||
}
|
||||
|
||||
/// Invert an affine transformation
|
||||
pub fn inverse(affine: &Affine4) -> Option<Affine4> {
|
||||
let mat = to_matrix4(affine);
|
||||
mat.try_inverse().map(|inv| from_matrix4(&inv))
|
||||
}
|
||||
|
||||
/// Transform a 3D point using an affine matrix
|
||||
pub fn transform_point(affine: &Affine4, point: [f64; 3]) -> [f64; 3] {
|
||||
let mat = to_matrix4(affine);
|
||||
let p = Point3::new(point[0], point[1], point[2]);
|
||||
let transformed = mat.transform_point(&p);
|
||||
[transformed.x, transformed.y, transformed.z]
|
||||
}
|
||||
|
||||
/// Transform a 3D vector using an affine matrix (ignores translation)
|
||||
pub fn transform_vector(affine: &Affine4, vector: [f64; 3]) -> [f64; 3] {
|
||||
let mat = to_matrix4(affine);
|
||||
let v = Vector3::new(vector[0], vector[1], vector[2]);
|
||||
// Extract rotation/scaling part (upper-left 3x3)
|
||||
let rotated = mat.fixed_view::<3, 3>(0, 0) * v;
|
||||
[rotated.x, rotated.y, rotated.z]
|
||||
}
|
||||
|
||||
/// Convert voxel indices to world coordinates
|
||||
pub fn voxel_to_world(affine: &Affine4, voxel: [f64; 3]) -> [f64; 3] {
|
||||
transform_point(affine, voxel)
|
||||
}
|
||||
|
||||
/// Convert world coordinates to voxel indices
|
||||
pub fn world_to_voxel(affine: &Affine4, world: [f64; 3]) -> Option<[f64; 3]> {
|
||||
inverse(affine).map(|inv| transform_point(&inv, world))
|
||||
}
|
||||
|
||||
/// Extract the origin (translation component) from an affine
|
||||
pub fn get_origin(affine: &Affine4) -> [f64; 3] {
|
||||
[affine[0][3], affine[1][3], affine[2][3]]
|
||||
}
|
||||
|
||||
/// Extract the voxel spacing from an affine (assuming no shear)
|
||||
pub fn get_spacing(affine: &Affine4) -> [f64; 3] {
|
||||
[
|
||||
(affine[0][0].powi(2) + affine[1][0].powi(2) + affine[2][0].powi(2)).sqrt(),
|
||||
(affine[0][1].powi(2) + affine[1][1].powi(2) + affine[2][1].powi(2)).sqrt(),
|
||||
(affine[0][2].powi(2) + affine[1][2].powi(2) + affine[2][2].powi(2)).sqrt(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Extract the rotation matrix from an affine (normalized)
|
||||
pub fn get_rotation(affine: &Affine4) -> [[f64; 3]; 3] {
|
||||
let spacing = get_spacing(affine);
|
||||
[
|
||||
[
|
||||
affine[0][0] / spacing[0],
|
||||
affine[0][1] / spacing[1],
|
||||
affine[0][2] / spacing[2],
|
||||
],
|
||||
[
|
||||
affine[1][0] / spacing[0],
|
||||
affine[1][1] / spacing[1],
|
||||
affine[1][2] / spacing[2],
|
||||
],
|
||||
[
|
||||
affine[2][0] / spacing[0],
|
||||
affine[2][1] / spacing[1],
|
||||
affine[2][2] / spacing[2],
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
/// Check if two affines are approximately equal
|
||||
pub fn approx_equal(a: &Affine4, b: &Affine4, epsilon: f64) -> bool {
|
||||
for i in 0..4 {
|
||||
for j in 0..4 {
|
||||
if (a[i][j] - b[i][j]).abs() > epsilon {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Convert affine to a flat row-major array (16 elements)
|
||||
pub fn to_flat(affine: &Affine4) -> [f64; 16] {
|
||||
[
|
||||
affine[0][0],
|
||||
affine[0][1],
|
||||
affine[0][2],
|
||||
affine[0][3],
|
||||
affine[1][0],
|
||||
affine[1][1],
|
||||
affine[1][2],
|
||||
affine[1][3],
|
||||
affine[2][0],
|
||||
affine[2][1],
|
||||
affine[2][2],
|
||||
affine[2][3],
|
||||
affine[3][0],
|
||||
affine[3][1],
|
||||
affine[3][2],
|
||||
affine[3][3],
|
||||
]
|
||||
}
|
||||
|
||||
/// Create affine from a flat row-major array
|
||||
pub fn from_flat(flat: &[f64; 16]) -> Affine4 {
|
||||
[
|
||||
[flat[0], flat[1], flat[2], flat[3]],
|
||||
[flat[4], flat[5], flat[6], flat[7]],
|
||||
[flat[8], flat[9], flat[10], flat[11]],
|
||||
[flat[12], flat[13], flat[14], flat[15]],
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_identity() {
|
||||
let id = identity();
|
||||
let point = [1.0, 2.0, 3.0];
|
||||
let result = transform_point(&id, point);
|
||||
assert_eq!(result, point);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translation() {
|
||||
let trans = translation(10.0, 20.0, 30.0);
|
||||
let point = [1.0, 2.0, 3.0];
|
||||
let result = transform_point(&trans, point);
|
||||
assert_eq!(result, [11.0, 22.0, 33.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scaling() {
|
||||
let scale = scaling(2.0, 3.0, 4.0);
|
||||
let point = [1.0, 1.0, 1.0];
|
||||
let result = transform_point(&scale, point);
|
||||
assert_eq!(result, [2.0, 3.0, 4.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose() {
|
||||
let scale = scaling(2.0, 2.0, 2.0);
|
||||
let trans = translation(1.0, 1.0, 1.0);
|
||||
let composed = compose(&trans, &scale); // First scale, then translate
|
||||
let point = [1.0, 1.0, 1.0];
|
||||
let result = transform_point(&composed, point);
|
||||
assert_eq!(result, [3.0, 3.0, 3.0]); // 1*2 + 1 = 3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inverse() {
|
||||
let trans = translation(10.0, 20.0, 30.0);
|
||||
let inv = inverse(&trans).unwrap();
|
||||
let composed = compose(&trans, &inv);
|
||||
assert!(approx_equal(&composed, &identity(), 1e-10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_spacing() {
|
||||
let affine = scaling(1.5, 2.0, 0.5);
|
||||
let spacing = get_spacing(&affine);
|
||||
assert!((spacing[0] - 1.5).abs() < 1e-10);
|
||||
assert!((spacing[1] - 2.0).abs() < 1e-10);
|
||||
assert!((spacing[2] - 0.5).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_voxel_to_world() {
|
||||
let affine = from_spacing_origin([2.0, 2.0, 2.0], [10.0, 20.0, 30.0]);
|
||||
let world = voxel_to_world(&affine, [5.0, 5.0, 5.0]);
|
||||
assert_eq!(world, [20.0, 30.0, 40.0]); // 5*2 + 10 = 20, etc.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_world_to_voxel() {
|
||||
let affine = from_spacing_origin([2.0, 2.0, 2.0], [10.0, 20.0, 30.0]);
|
||||
let voxel = world_to_voxel(&affine, [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_get_origin() {
|
||||
let affine = from_spacing_origin([1.0, 1.0, 1.0], [5.0, 10.0, 15.0]);
|
||||
let origin = get_origin(&affine);
|
||||
assert_eq!(origin, [5.0, 10.0, 15.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_spacing_origin() {
|
||||
let spacing = [2.0, 3.0, 4.0];
|
||||
let origin = [10.0, 20.0, 30.0];
|
||||
let affine = from_spacing_origin(spacing, origin);
|
||||
|
||||
assert_eq!(affine[0][0], 2.0);
|
||||
assert_eq!(affine[1][1], 3.0);
|
||||
assert_eq!(affine[2][2], 4.0);
|
||||
assert_eq!(affine[0][3], 10.0);
|
||||
assert_eq!(affine[1][3], 20.0);
|
||||
assert_eq!(affine[2][3], 30.0);
|
||||
assert_eq!(affine[3][3], 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_vector() {
|
||||
let scale = scaling(2.0, 3.0, 4.0);
|
||||
let vector = [1.0, 1.0, 1.0];
|
||||
let result = transform_vector(&scale, vector);
|
||||
assert_eq!(result, [2.0, 3.0, 4.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_vector_ignores_translation() {
|
||||
let trans = translation(10.0, 20.0, 30.0);
|
||||
let vector = [1.0, 2.0, 3.0];
|
||||
let result = transform_vector(&trans, vector);
|
||||
assert_eq!(result, vector); // Translation should not affect vectors
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_approx_equal_identical() {
|
||||
let a = identity();
|
||||
let b = identity();
|
||||
assert!(approx_equal(&a, &b, 1e-10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_approx_equal_different() {
|
||||
let a = identity();
|
||||
let mut b = identity();
|
||||
b[0][0] = 2.0;
|
||||
assert!(!approx_equal(&a, &b, 1e-10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_approx_equal_within_epsilon() {
|
||||
let a = identity();
|
||||
let mut b = identity();
|
||||
b[0][0] = 1.0 + 1e-11;
|
||||
assert!(approx_equal(&a, &b, 1e-10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_flat() {
|
||||
let affine = identity();
|
||||
let flat = to_flat(&affine);
|
||||
assert_eq!(flat[0], 1.0);
|
||||
assert_eq!(flat[5], 1.0);
|
||||
assert_eq!(flat[10], 1.0);
|
||||
assert_eq!(flat[15], 1.0);
|
||||
assert_eq!(flat[1], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flat() {
|
||||
let flat = [
|
||||
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
|
||||
];
|
||||
let affine = from_flat(&flat);
|
||||
assert!(approx_equal(&affine, &identity(), 1e-10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flat_round_trip() {
|
||||
let original = from_spacing_origin([2.0, 3.0, 4.0], [10.0, 20.0, 30.0]);
|
||||
let flat = to_flat(&original);
|
||||
let restored = from_flat(&flat);
|
||||
assert!(approx_equal(&original, &restored, 1e-10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_rotation() {
|
||||
let affine = scaling(2.0, 3.0, 4.0);
|
||||
let rotation = get_rotation(&affine);
|
||||
|
||||
// For pure scaling, rotation should be identity
|
||||
assert!((rotation[0][0] - 1.0).abs() < 1e-10);
|
||||
assert!((rotation[1][1] - 1.0).abs() < 1e-10);
|
||||
assert!((rotation[2][2] - 1.0).abs() < 1e-10);
|
||||
assert!((rotation[0][1]).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matrix4_conversion_round_trip() {
|
||||
let original = from_spacing_origin([1.5, 2.0, 2.5], [5.0, 10.0, 15.0]);
|
||||
let matrix = to_matrix4(&original);
|
||||
let restored = from_matrix4(&matrix);
|
||||
assert!(approx_equal(&original, &restored, 1e-10));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
//! NIfTI file writer supporting .nii and .nii.gz formats.
|
||||
|
||||
use crate::error::{MedicalError, MedicalResult};
|
||||
use crate::nifti::header::{NiftiDataType, NiftiHeader, SpatialUnits, TransformCode};
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use flate2::Compression;
|
||||
use flate2::write::GzEncoder;
|
||||
use std::fs::File;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::Path;
|
||||
|
||||
/// Write voxel data to a NIfTI file.
|
||||
///
|
||||
/// The output format is determined by the file extension:
|
||||
/// - `.nii` - uncompressed NIfTI-1
|
||||
/// - `.nii.gz` - gzip-compressed NIfTI-1
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - Voxel data in row-major order (z varies slowest, x varies fastest)
|
||||
/// * `shape` - Volume dimensions (x, y, z)
|
||||
/// * `spacing` - Voxel spacing in mm (dx, dy, dz)
|
||||
/// * `affine` - 4x4 affine transformation matrix
|
||||
/// * `path` - Output file path
|
||||
/// * `datatype` - Output data type (e.g., Float32, Int16)
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use rtx_medical_core::nifti::{write_nifti, NiftiDataType};
|
||||
///
|
||||
/// let data = vec![0.0; 64 * 64 * 64];
|
||||
/// let shape = (64, 64, 64);
|
||||
/// let spacing = (1.0, 1.0, 1.0);
|
||||
/// let affine = [[1.0, 0.0, 0.0, 0.0],
|
||||
/// [0.0, 1.0, 0.0, 0.0],
|
||||
/// [0.0, 0.0, 1.0, 0.0],
|
||||
/// [0.0, 0.0, 0.0, 1.0]];
|
||||
/// write_nifti(&data, shape, spacing, &affine, "output.nii.gz", NiftiDataType::Float32)?;
|
||||
/// ```
|
||||
pub fn write_nifti<P: AsRef<Path>>(
|
||||
data: &[f64],
|
||||
shape: (usize, usize, usize),
|
||||
spacing: (f64, f64, f64),
|
||||
affine: &[[f64; 4]; 4],
|
||||
path: P,
|
||||
datatype: NiftiDataType,
|
||||
) -> MedicalResult<()> {
|
||||
let path = path.as_ref();
|
||||
let path_str = path.to_string_lossy().to_lowercase();
|
||||
|
||||
let is_gzipped = path_str.ends_with(".nii.gz") || path_str.ends_with(".gz");
|
||||
|
||||
// Build header
|
||||
let header = build_header(shape, spacing, affine, datatype);
|
||||
|
||||
// Validate data length
|
||||
let expected_len = shape.0 * shape.1 * shape.2;
|
||||
if data.len() != expected_len {
|
||||
return Err(MedicalError::DimensionMismatch {
|
||||
expected: format!("{} voxels", expected_len),
|
||||
got: format!("{} voxels", data.len()),
|
||||
});
|
||||
}
|
||||
|
||||
// Convert data to target type
|
||||
let raw_data = convert_from_f64(data, datatype)?;
|
||||
|
||||
if is_gzipped {
|
||||
write_nifti_gz(path, &header, &raw_data)
|
||||
} else {
|
||||
write_nifti_uncompressed(path, &header, &raw_data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a gzip-compressed NIfTI file
|
||||
fn write_nifti_gz<P: AsRef<Path>>(path: P, header: &NiftiHeader, data: &[u8]) -> MedicalResult<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let mut encoder = GzEncoder::new(writer, Compression::default());
|
||||
|
||||
write_header(&mut encoder, header)?;
|
||||
encoder.write_all(data)?;
|
||||
encoder
|
||||
.finish()
|
||||
.map_err(|e| MedicalError::Nifti(format!("Failed to finish gzip compression: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write an uncompressed NIfTI file
|
||||
fn write_nifti_uncompressed<P: AsRef<Path>>(
|
||||
path: P,
|
||||
header: &NiftiHeader,
|
||||
data: &[u8],
|
||||
) -> MedicalResult<()> {
|
||||
let file = File::create(path)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
write_header(&mut writer, header)?;
|
||||
writer.write_all(data)?;
|
||||
writer.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build a NIfTI-1 header from volume metadata
|
||||
fn build_header(
|
||||
shape: (usize, usize, usize),
|
||||
spacing: (f64, f64, f64),
|
||||
affine: &[[f64; 4]; 4],
|
||||
datatype: NiftiDataType,
|
||||
) -> NiftiHeader {
|
||||
NiftiHeader {
|
||||
sizeof_hdr: 348,
|
||||
datatype,
|
||||
bitpix: (datatype.bytes_per_voxel() * 8) as i16,
|
||||
dim: [
|
||||
3,
|
||||
shape.0 as i64,
|
||||
shape.1 as i64,
|
||||
shape.2 as i64,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
],
|
||||
intent_p1: 0.0,
|
||||
intent_p2: 0.0,
|
||||
intent_p3: 0.0,
|
||||
intent_code: 0,
|
||||
pixdim: [1.0, spacing.0, spacing.1, spacing.2, 1.0, 1.0, 1.0, 1.0],
|
||||
vox_offset: 352,
|
||||
scl_slope: 1.0,
|
||||
scl_inter: 0.0,
|
||||
slice_code: 0,
|
||||
xyzt_units: SpatialUnits::Millimeters as u8,
|
||||
cal_max: 0.0,
|
||||
cal_min: 0.0,
|
||||
slice_duration: 0.0,
|
||||
toffset: 0.0,
|
||||
slice_start: 0,
|
||||
slice_end: 0,
|
||||
descrip: "Created by rtx-medical-core".to_string(),
|
||||
aux_file: String::new(),
|
||||
qform_code: TransformCode::Unknown,
|
||||
sform_code: TransformCode::ScannerAnat,
|
||||
quatern_b: 0.0,
|
||||
quatern_c: 0.0,
|
||||
quatern_d: 0.0,
|
||||
qoffset_x: 0.0,
|
||||
qoffset_y: 0.0,
|
||||
qoffset_z: 0.0,
|
||||
srow_x: affine[0],
|
||||
srow_y: affine[1],
|
||||
srow_z: affine[2],
|
||||
intent_name: String::new(),
|
||||
magic: *b"n+1\0\0\0\0\0",
|
||||
is_nifti2: false,
|
||||
little_endian: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write NIfTI-1 header (348 bytes + 4 bytes padding = 352 bytes)
|
||||
fn write_header<W: Write>(writer: &mut W, header: &NiftiHeader) -> MedicalResult<()> {
|
||||
// sizeof_hdr (0-3)
|
||||
writer.write_i32::<LittleEndian>(header.sizeof_hdr)?;
|
||||
|
||||
// data_type (unused, 4-13) - 10 bytes
|
||||
writer.write_all(&[0u8; 10])?;
|
||||
|
||||
// db_name (unused, 14-31) - 18 bytes
|
||||
writer.write_all(&[0u8; 18])?;
|
||||
|
||||
// extents (unused, 32-35)
|
||||
writer.write_i32::<LittleEndian>(0)?;
|
||||
|
||||
// session_error (unused, 36-37)
|
||||
writer.write_i16::<LittleEndian>(0)?;
|
||||
|
||||
// regular (unused, 38)
|
||||
writer.write_all(&[0u8; 1])?;
|
||||
|
||||
// dim_info (unused, 39)
|
||||
writer.write_all(&[0u8; 1])?;
|
||||
|
||||
// dim (40-55) - 8 x i16
|
||||
for i in 0..8 {
|
||||
writer.write_i16::<LittleEndian>(header.dim[i] as i16)?;
|
||||
}
|
||||
|
||||
// intent_p1 (56-59)
|
||||
writer.write_f32::<LittleEndian>(header.intent_p1 as f32)?;
|
||||
|
||||
// intent_p2 (60-63)
|
||||
writer.write_f32::<LittleEndian>(header.intent_p2 as f32)?;
|
||||
|
||||
// intent_p3 (64-67)
|
||||
writer.write_f32::<LittleEndian>(header.intent_p3 as f32)?;
|
||||
|
||||
// intent_code (68-69)
|
||||
writer.write_i16::<LittleEndian>(header.intent_code)?;
|
||||
|
||||
// datatype (70-71)
|
||||
writer.write_i16::<LittleEndian>(header.datatype as i16)?;
|
||||
|
||||
// bitpix (72-73)
|
||||
writer.write_i16::<LittleEndian>(header.bitpix)?;
|
||||
|
||||
// slice_start (74-75)
|
||||
writer.write_i16::<LittleEndian>(header.slice_start as i16)?;
|
||||
|
||||
// pixdim (76-107) - 8 x f32
|
||||
for i in 0..8 {
|
||||
writer.write_f32::<LittleEndian>(header.pixdim[i] as f32)?;
|
||||
}
|
||||
|
||||
// vox_offset (108-111)
|
||||
writer.write_f32::<LittleEndian>(header.vox_offset as f32)?;
|
||||
|
||||
// scl_slope (112-115)
|
||||
writer.write_f32::<LittleEndian>(header.scl_slope as f32)?;
|
||||
|
||||
// scl_inter (116-119)
|
||||
writer.write_f32::<LittleEndian>(header.scl_inter as f32)?;
|
||||
|
||||
// slice_end (120-121)
|
||||
writer.write_i16::<LittleEndian>(header.slice_end as i16)?;
|
||||
|
||||
// slice_code (122)
|
||||
writer.write_all(&[header.slice_code])?;
|
||||
|
||||
// xyzt_units (123)
|
||||
writer.write_all(&[header.xyzt_units])?;
|
||||
|
||||
// cal_max (124-127)
|
||||
writer.write_f32::<LittleEndian>(header.cal_max as f32)?;
|
||||
|
||||
// cal_min (128-131)
|
||||
writer.write_f32::<LittleEndian>(header.cal_min as f32)?;
|
||||
|
||||
// slice_duration (132-135)
|
||||
writer.write_f32::<LittleEndian>(header.slice_duration as f32)?;
|
||||
|
||||
// toffset (136-139)
|
||||
writer.write_f32::<LittleEndian>(header.toffset as f32)?;
|
||||
|
||||
// glmax (unused, 140-143)
|
||||
writer.write_i32::<LittleEndian>(0)?;
|
||||
|
||||
// glmin (unused, 144-147)
|
||||
writer.write_i32::<LittleEndian>(0)?;
|
||||
|
||||
// descrip (148-227) - 80 bytes
|
||||
let descrip_bytes = header.descrip.as_bytes();
|
||||
let mut descrip_buf = [0u8; 80];
|
||||
let len = descrip_bytes.len().min(80);
|
||||
descrip_buf[..len].copy_from_slice(&descrip_bytes[..len]);
|
||||
writer.write_all(&descrip_buf)?;
|
||||
|
||||
// aux_file (228-251) - 24 bytes
|
||||
let aux_bytes = header.aux_file.as_bytes();
|
||||
let mut aux_buf = [0u8; 24];
|
||||
let len = aux_bytes.len().min(24);
|
||||
aux_buf[..len].copy_from_slice(&aux_bytes[..len]);
|
||||
writer.write_all(&aux_buf)?;
|
||||
|
||||
// qform_code (252-253)
|
||||
writer.write_i16::<LittleEndian>(header.qform_code as i16)?;
|
||||
|
||||
// sform_code (254-255)
|
||||
writer.write_i16::<LittleEndian>(header.sform_code as i16)?;
|
||||
|
||||
// quatern_b (256-259)
|
||||
writer.write_f32::<LittleEndian>(header.quatern_b as f32)?;
|
||||
|
||||
// quatern_c (260-263)
|
||||
writer.write_f32::<LittleEndian>(header.quatern_c as f32)?;
|
||||
|
||||
// quatern_d (264-267)
|
||||
writer.write_f32::<LittleEndian>(header.quatern_d as f32)?;
|
||||
|
||||
// qoffset_x (268-271)
|
||||
writer.write_f32::<LittleEndian>(header.qoffset_x as f32)?;
|
||||
|
||||
// qoffset_y (272-275)
|
||||
writer.write_f32::<LittleEndian>(header.qoffset_y as f32)?;
|
||||
|
||||
// qoffset_z (276-279)
|
||||
writer.write_f32::<LittleEndian>(header.qoffset_z as f32)?;
|
||||
|
||||
// srow_x (280-295) - 4 x f32
|
||||
for val in &header.srow_x {
|
||||
writer.write_f32::<LittleEndian>(*val as f32)?;
|
||||
}
|
||||
|
||||
// srow_y (296-311) - 4 x f32
|
||||
for val in &header.srow_y {
|
||||
writer.write_f32::<LittleEndian>(*val as f32)?;
|
||||
}
|
||||
|
||||
// srow_z (312-327) - 4 x f32
|
||||
for val in &header.srow_z {
|
||||
writer.write_f32::<LittleEndian>(*val as f32)?;
|
||||
}
|
||||
|
||||
// intent_name (328-343) - 16 bytes
|
||||
let intent_bytes = header.intent_name.as_bytes();
|
||||
let mut intent_buf = [0u8; 16];
|
||||
let len = intent_bytes.len().min(16);
|
||||
intent_buf[..len].copy_from_slice(&intent_bytes[..len]);
|
||||
writer.write_all(&intent_buf)?;
|
||||
|
||||
// magic (344-347) - 4 bytes for NIfTI-1
|
||||
writer.write_all(&header.magic[..4])?;
|
||||
|
||||
// Padding to vox_offset (348-351) - 4 bytes
|
||||
writer.write_all(&[0u8; 4])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert f64 data to raw bytes for the target data type
|
||||
fn convert_from_f64(data: &[f64], datatype: NiftiDataType) -> MedicalResult<Vec<u8>> {
|
||||
let mut result = Vec::with_capacity(data.len() * datatype.bytes_per_voxel());
|
||||
|
||||
match datatype {
|
||||
NiftiDataType::UInt8 => {
|
||||
for &val in data {
|
||||
result.push(val.clamp(0.0, 255.0) as u8);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Int8 => {
|
||||
for &val in data {
|
||||
result.push(val.clamp(-128.0, 127.0) as i8 as u8);
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Int16 => {
|
||||
for &val in data {
|
||||
let v = val.clamp(i16::MIN as f64, i16::MAX as f64) as i16;
|
||||
result.write_i16::<LittleEndian>(v)?;
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::UInt16 => {
|
||||
for &val in data {
|
||||
let v = val.clamp(0.0, u16::MAX as f64) as u16;
|
||||
result.write_u16::<LittleEndian>(v)?;
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Int32 => {
|
||||
for &val in data {
|
||||
let v = val.clamp(i32::MIN as f64, i32::MAX as f64) as i32;
|
||||
result.write_i32::<LittleEndian>(v)?;
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::UInt32 => {
|
||||
for &val in data {
|
||||
let v = val.clamp(0.0, u32::MAX as f64) as u32;
|
||||
result.write_u32::<LittleEndian>(v)?;
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Float32 => {
|
||||
for &val in data {
|
||||
result.write_f32::<LittleEndian>(val as f32)?;
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Float64 => {
|
||||
for &val in data {
|
||||
result.write_f64::<LittleEndian>(val)?;
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::Int64 => {
|
||||
for &val in data {
|
||||
let v = val.clamp(i64::MIN as f64, i64::MAX as f64) as i64;
|
||||
result.write_i64::<LittleEndian>(v)?;
|
||||
}
|
||||
}
|
||||
|
||||
NiftiDataType::UInt64 => {
|
||||
for &val in data {
|
||||
let v = val.clamp(0.0, u64::MAX as f64) as u64;
|
||||
result.write_u64::<LittleEndian>(v)?;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(MedicalError::Nifti(format!(
|
||||
"Unsupported NIfTI data type for writing: {}",
|
||||
datatype.name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_header() {
|
||||
let shape = (64, 64, 32);
|
||||
let spacing = (1.0, 1.0, 2.0);
|
||||
let affine = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 2.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
let header = build_header(shape, spacing, &affine, NiftiDataType::Float32);
|
||||
|
||||
assert_eq!(header.sizeof_hdr, 348);
|
||||
assert_eq!(header.dim[1], 64);
|
||||
assert_eq!(header.dim[2], 64);
|
||||
assert_eq!(header.dim[3], 32);
|
||||
assert_eq!(header.datatype, NiftiDataType::Float32);
|
||||
assert_eq!(header.bitpix, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_uint8() {
|
||||
let data = vec![0.0, 127.5, 255.0, 300.0, -10.0];
|
||||
let result = convert_from_f64(&data, NiftiDataType::UInt8).unwrap();
|
||||
// 127.5 truncates to 127 (not rounded)
|
||||
assert_eq!(result, vec![0, 127, 255, 255, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_float32() {
|
||||
let data = vec![1.5, 2.7, 3.9];
|
||||
let result = convert_from_f64(&data, NiftiDataType::Float32).unwrap();
|
||||
assert_eq!(result.len(), 12); // 3 values * 4 bytes each
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user