use byteordered::byteorder::{BigEndian, ByteOrder, LittleEndian};
use byteordered::{ByteOrdered, Endianness};
use flate2::bufread::GzDecoder;
use flate2::Compression;
use ndarray::{Array, Dim};
use std::fmt;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use crate::config;
use crate::error::{NeuroformatsError, Result};
use crate::fs_mgh::{FsMgh, FsMghData, FsMghHeader, MRI_FLOAT, MRI_INT, MRI_SHORT, MRI_UCHAR};
use crate::util::{checked_mul_dims, validate_finite_f32_slice};
pub const NIFTI_HEADER_SIZE_IN_BYTES: usize = 348;
pub const NIFTI_HEADER_PLUS_EXT_SIZE_IN_BYTES: usize = 352;
pub const NIFTI_DEFAULT_VOX_OFFSET: f32 = 352.0;
pub const DT_UINT8: i16 = 2;
pub const DT_INT16: i16 = 4;
pub const DT_INT32: i16 = 8;
pub const DT_FLOAT32: i16 = 16;
pub const XFORM_UNKNOWN: i16 = 0;
pub const XFORM_SCANNER_ANAT: i16 = 1;
const MAGIC_SINGLE_FILE: [u8; 4] = [b'n', b'+', b'1', 0];
const MAGIC_TWO_FILE: [u8; 4] = [b'n', b'i', b'1', 0];
#[derive(Debug, Clone, Copy)]
struct VolumeScaling {
slope: f32,
inter: f32,
}
impl VolumeScaling {
fn from_header(scl_slope: f32, scl_inter: f32) -> Result<VolumeScaling> {
validate_finite_f32_slice(&[scl_slope, scl_inter], "scl_slope/scl_inter")?;
let slope = if scl_slope == 0.0 { 1.0 } else { scl_slope };
Ok(VolumeScaling {
slope,
inter: scl_inter,
})
}
fn is_identity(&self) -> bool {
self.slope == 1.0 && self.inter == 0.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Nifti1Header {
pub sizeof_hdr: i32,
pub data_type: [u8; 10],
pub db_name: [u8; 18],
pub extents: i32,
pub session_error: i16,
pub regular: u8,
pub dim_info: u8,
pub dim: [i16; 8],
pub intent_p1: f32,
pub intent_p2: f32,
pub intent_p3: f32,
pub intent_code: i16,
pub datatype: i16,
pub bitpix: i16,
pub slice_start: i16,
pub pixdim: [f32; 8],
pub vox_offset: f32,
pub scl_slope: f32,
pub scl_inter: f32,
pub slice_end: i16,
pub slice_code: u8,
pub xyzt_units: u8,
pub cal_max: f32,
pub cal_min: f32,
pub slice_duration: f32,
pub toffset: f32,
pub glmax: i32,
pub glmin: i32,
pub descrip: [u8; 80],
pub aux_file: [u8; 24],
pub qform_code: i16,
pub sform_code: i16,
pub quatern_b: f32,
pub quatern_c: f32,
pub quatern_d: f32,
pub qoffset_x: f32,
pub qoffset_y: f32,
pub qoffset_z: f32,
pub srow_x: [f32; 4],
pub srow_y: [f32; 4],
pub srow_z: [f32; 4],
pub intent_name: [u8; 16],
pub magic: [u8; 4],
}
impl Default for Nifti1Header {
fn default() -> Nifti1Header {
Nifti1Header {
sizeof_hdr: NIFTI_HEADER_SIZE_IN_BYTES as i32,
data_type: [0; 10],
db_name: [0; 18],
extents: 0,
session_error: 0,
regular: b'r',
dim_info: 0,
dim: [0; 8],
intent_p1: 0.0,
intent_p2: 0.0,
intent_p3: 0.0,
intent_code: 0,
datatype: 0,
bitpix: 0,
slice_start: 0,
pixdim: [0.0; 8],
vox_offset: NIFTI_DEFAULT_VOX_OFFSET,
scl_slope: 1.0,
scl_inter: 0.0,
slice_end: 0,
slice_code: 0,
xyzt_units: 0,
cal_max: 0.0,
cal_min: 0.0,
slice_duration: 0.0,
toffset: 0.0,
glmax: 0,
glmin: 0,
descrip: [0; 80],
aux_file: [0; 24],
qform_code: XFORM_UNKNOWN,
sform_code: XFORM_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: [0.0; 4],
srow_y: [0.0; 4],
srow_z: [0.0; 4],
intent_name: [0; 16],
magic: MAGIC_SINGLE_FILE,
}
}
}
impl Nifti1Header {
pub fn from_bytes(buf: &[u8; NIFTI_HEADER_SIZE_IN_BYTES], little_endian: bool) -> Nifti1Header {
if little_endian {
parse_header::<LittleEndian>(buf)
} else {
parse_header::<BigEndian>(buf)
}
}
pub fn to_bytes(&self, little_endian: bool) -> [u8; NIFTI_HEADER_SIZE_IN_BYTES] {
if little_endian {
serialize_header::<LittleEndian>(self)
} else {
serialize_header::<BigEndian>(self)
}
}
pub fn is_single_file_magic(&self) -> bool {
self.magic == MAGIC_SINGLE_FILE
}
pub fn is_two_file_magic(&self) -> bool {
self.magic == MAGIC_TWO_FILE
}
pub fn set_descrip(&mut self, descrip: &str) {
let bytes = descrip.as_bytes();
self.descrip = [0; 80];
let n = bytes.len().min(80);
self.descrip[..n].copy_from_slice(&bytes[..n]);
}
pub fn descrip_string(&self) -> String {
String::from_utf8_lossy(trim_nul_bytes(&self.descrip)).into_owned()
}
}
impl fmt::Display for Nifti1Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"NIfTI-1 header with dim {}, {}, {}, {} and data type {}.",
self.dim[1], self.dim[2], self.dim[3], self.dim[4], self.datatype
)
}
}
fn trim_nul_bytes(bytes: &[u8]) -> &[u8] {
let mut end = bytes.len();
while end > 0 && bytes[end - 1] == 0 {
end -= 1;
}
&bytes[..end]
}
fn parse_header<E: ByteOrder>(buf: &[u8]) -> Nifti1Header {
let mut h = Nifti1Header::default();
h.sizeof_hdr = E::read_i32(&buf[0..4]);
h.data_type.copy_from_slice(&buf[4..14]);
h.db_name.copy_from_slice(&buf[14..32]);
h.extents = E::read_i32(&buf[32..36]);
h.session_error = E::read_i16(&buf[36..38]);
h.regular = buf[38];
h.dim_info = buf[39];
for (i, item) in h.dim.iter_mut().enumerate() {
*item = E::read_i16(&buf[40 + i * 2..42 + i * 2]);
}
h.intent_p1 = E::read_f32(&buf[56..60]);
h.intent_p2 = E::read_f32(&buf[60..64]);
h.intent_p3 = E::read_f32(&buf[64..68]);
h.intent_code = E::read_i16(&buf[68..70]);
h.datatype = E::read_i16(&buf[70..72]);
h.bitpix = E::read_i16(&buf[72..74]);
h.slice_start = E::read_i16(&buf[74..76]);
for (i, item) in h.pixdim.iter_mut().enumerate() {
*item = E::read_f32(&buf[76 + i * 4..80 + i * 4]);
}
h.vox_offset = E::read_f32(&buf[108..112]);
h.scl_slope = E::read_f32(&buf[112..116]);
h.scl_inter = E::read_f32(&buf[116..120]);
h.slice_end = E::read_i16(&buf[120..122]);
h.slice_code = buf[122];
h.xyzt_units = buf[123];
h.cal_max = E::read_f32(&buf[124..128]);
h.cal_min = E::read_f32(&buf[128..132]);
h.slice_duration = E::read_f32(&buf[132..136]);
h.toffset = E::read_f32(&buf[136..140]);
h.glmax = E::read_i32(&buf[140..144]);
h.glmin = E::read_i32(&buf[144..148]);
h.descrip.copy_from_slice(&buf[148..228]);
h.aux_file.copy_from_slice(&buf[228..252]);
h.qform_code = E::read_i16(&buf[252..254]);
h.sform_code = E::read_i16(&buf[254..256]);
h.quatern_b = E::read_f32(&buf[256..260]);
h.quatern_c = E::read_f32(&buf[260..264]);
h.quatern_d = E::read_f32(&buf[264..268]);
h.qoffset_x = E::read_f32(&buf[268..272]);
h.qoffset_y = E::read_f32(&buf[272..276]);
h.qoffset_z = E::read_f32(&buf[276..280]);
for (i, item) in h.srow_x.iter_mut().enumerate() {
*item = E::read_f32(&buf[280 + i * 4..284 + i * 4]);
}
for (i, item) in h.srow_y.iter_mut().enumerate() {
*item = E::read_f32(&buf[296 + i * 4..300 + i * 4]);
}
for (i, item) in h.srow_z.iter_mut().enumerate() {
*item = E::read_f32(&buf[312 + i * 4..316 + i * 4]);
}
h.intent_name.copy_from_slice(&buf[328..344]);
h.magic.copy_from_slice(&buf[344..348]);
h
}
fn serialize_header<E: ByteOrder>(h: &Nifti1Header) -> [u8; NIFTI_HEADER_SIZE_IN_BYTES] {
let mut buf = [0u8; NIFTI_HEADER_SIZE_IN_BYTES];
E::write_i32(&mut buf[0..4], h.sizeof_hdr);
buf[4..14].copy_from_slice(&h.data_type);
buf[14..32].copy_from_slice(&h.db_name);
E::write_i32(&mut buf[32..36], h.extents);
E::write_i16(&mut buf[36..38], h.session_error);
buf[38] = h.regular;
buf[39] = h.dim_info;
for (i, item) in h.dim.iter().enumerate() {
E::write_i16(&mut buf[40 + i * 2..42 + i * 2], *item);
}
E::write_f32(&mut buf[56..60], h.intent_p1);
E::write_f32(&mut buf[60..64], h.intent_p2);
E::write_f32(&mut buf[64..68], h.intent_p3);
E::write_i16(&mut buf[68..70], h.intent_code);
E::write_i16(&mut buf[70..72], h.datatype);
E::write_i16(&mut buf[72..74], h.bitpix);
E::write_i16(&mut buf[74..76], h.slice_start);
for (i, item) in h.pixdim.iter().enumerate() {
E::write_f32(&mut buf[76 + i * 4..80 + i * 4], *item);
}
E::write_f32(&mut buf[108..112], h.vox_offset);
E::write_f32(&mut buf[112..116], h.scl_slope);
E::write_f32(&mut buf[116..120], h.scl_inter);
E::write_i16(&mut buf[120..122], h.slice_end);
buf[122] = h.slice_code;
buf[123] = h.xyzt_units;
E::write_f32(&mut buf[124..128], h.cal_max);
E::write_f32(&mut buf[128..132], h.cal_min);
E::write_f32(&mut buf[132..136], h.slice_duration);
E::write_f32(&mut buf[136..140], h.toffset);
E::write_i32(&mut buf[140..144], h.glmax);
E::write_i32(&mut buf[144..148], h.glmin);
buf[148..228].copy_from_slice(&h.descrip);
buf[228..252].copy_from_slice(&h.aux_file);
E::write_i16(&mut buf[252..254], h.qform_code);
E::write_i16(&mut buf[254..256], h.sform_code);
E::write_f32(&mut buf[256..260], h.quatern_b);
E::write_f32(&mut buf[260..264], h.quatern_c);
E::write_f32(&mut buf[264..268], h.quatern_d);
E::write_f32(&mut buf[268..272], h.qoffset_x);
E::write_f32(&mut buf[272..276], h.qoffset_y);
E::write_f32(&mut buf[276..280], h.qoffset_z);
for (i, item) in h.srow_x.iter().enumerate() {
E::write_f32(&mut buf[280 + i * 4..284 + i * 4], *item);
}
for (i, item) in h.srow_y.iter().enumerate() {
E::write_f32(&mut buf[296 + i * 4..300 + i * 4], *item);
}
for (i, item) in h.srow_z.iter().enumerate() {
E::write_f32(&mut buf[312 + i * 4..316 + i * 4], *item);
}
buf[328..344].copy_from_slice(&h.intent_name);
buf[344..348].copy_from_slice(&h.magic);
buf
}
fn detect_little_endian(buf: &[u8; NIFTI_HEADER_SIZE_IN_BYTES]) -> Result<bool> {
let le = LittleEndian::read_i32(&buf[0..4]);
let be = BigEndian::read_i32(&buf[0..4]);
if le == NIFTI_HEADER_SIZE_IN_BYTES as i32 {
Ok(true)
} else if be == NIFTI_HEADER_SIZE_IN_BYTES as i32 {
Ok(false)
} else {
Err(NeuroformatsError::InvalidNiftiFormat(format!(
"sizeof_hdr is {} (expected {}).",
le, NIFTI_HEADER_SIZE_IN_BYTES
)))
}
}
fn nifti_dtype_to_mri(nifti_dtype: i16) -> Result<i32> {
match nifti_dtype {
DT_UINT8 => Ok(MRI_UCHAR),
DT_INT16 => Ok(MRI_SHORT),
DT_INT32 => Ok(MRI_INT),
DT_FLOAT32 => Ok(MRI_FLOAT),
other => Err(NeuroformatsError::UnsupportedNiftiDataType(other)),
}
}
fn mri_dtype_to_nifti(mri_type: i32) -> Result<(i16, i16)> {
match mri_type {
MRI_UCHAR => Ok((DT_UINT8, 8)),
MRI_SHORT => Ok((DT_INT16, 16)),
MRI_INT => Ok((DT_INT32, 32)),
MRI_FLOAT => Ok((DT_FLOAT32, 32)),
other => Err(NeuroformatsError::UnsupportedNiftiDataType(other as i16)),
}
}
fn bytes_per_value(mri_type: i32) -> Result<usize> {
match mri_type {
MRI_UCHAR => Ok(1),
MRI_SHORT => Ok(2),
MRI_INT => Ok(4),
MRI_FLOAT => Ok(4),
other => Err(NeuroformatsError::UnsupportedNiftiDataType(other as i16)),
}
}
fn is_nii_gz_file<P>(path: P) -> bool
where
P: AsRef<Path>,
{
path.as_ref()
.file_name()
.map(|n| n.to_string_lossy().ends_with(".nii.gz"))
.unwrap_or(false)
}
#[derive(Debug, Clone, PartialEq)]
pub struct Nifti1 {
pub header: Nifti1Header,
pub volume: FsMgh,
}
impl Nifti1 {
pub fn from_file<P: AsRef<Path> + Copy>(path: P) -> Result<Nifti1> {
let gz = is_nii_gz_file(&path);
let file = File::open(path)?;
let buf_reader = BufReader::new(file);
if gz {
let mut input = BufReader::new(GzDecoder::new(buf_reader));
Nifti1::from_reader(&mut input)
} else {
let mut input = buf_reader;
Nifti1::from_reader(&mut input)
}
}
pub fn from_reader<S>(input: &mut S) -> Result<Nifti1>
where
S: BufRead,
{
let mut hdr_bytes = [0u8; NIFTI_HEADER_SIZE_IN_BYTES];
input
.read_exact(&mut hdr_bytes)
.map_err(|e| NeuroformatsError::Io(e))?;
let little_endian = detect_little_endian(&hdr_bytes)?;
let header = Nifti1Header::from_bytes(&hdr_bytes, little_endian);
if header.is_two_file_magic() {
return Err(NeuroformatsError::InvalidNiftiFormat(
"two-file NIfTI volumes (.hdr/.img pairs) are not supported, only single-file .nii volumes.".to_string(),
));
}
if !header.is_single_file_magic() {
return Err(NeuroformatsError::InvalidNiftiFormat(format!(
"invalid magic string {:?}. Only single-file .nii volumes are supported.",
&header.magic
)));
}
let ndim = header.dim[0];
if !(1..=7).contains(&ndim) {
return Err(NeuroformatsError::InvalidNiftiFormat(format!(
"dim[0] = {} is not in the valid range 1..=7.",
ndim
)));
}
if header.dim[1] <= 0 {
return Err(NeuroformatsError::InvalidNiftiFormat(format!(
"dim[1] = {}. Note that FreeSurfer surface data stored in NIfTI files (the FreeSurfer hack) is not supported.",
header.dim[1]
)));
}
for extra_dim in &header.dim[5..8] {
if *extra_dim > 1 {
return Err(NeuroformatsError::InvalidNiftiFormat(format!(
"more than 4 dimensions are not supported (dim = {:?}).",
header.dim
)));
}
}
let dims: [usize; 4] = [
header.dim[1] as usize,
max_1(header.dim[2]) as usize,
max_1(header.dim[3]) as usize,
max_1(header.dim[4]) as usize,
];
let mri_type = nifti_dtype_to_mri(header.datatype)?;
let bytes_per_element = bytes_per_value(mri_type)?;
let dims_i32: [i32; 4] = [
dims[0] as i32,
dims[1] as i32,
dims[2] as i32,
dims[3] as i32,
];
let num_voxels = checked_mul_dims(&dims_i32)?;
let num_bytes = num_voxels
.checked_mul(bytes_per_element)
.ok_or(NeuroformatsError::IntegerOverflow)?;
if num_bytes > config::max_bytes_per_file() {
return Err(NeuroformatsError::AllocationTooLarge);
}
if !header.vox_offset.is_finite() || header.vox_offset < NIFTI_HEADER_SIZE_IN_BYTES as f32 {
return Err(NeuroformatsError::InvalidNiftiFormat(format!(
"vox_offset = {} is invalid (must be finite and >= {}).",
header.vox_offset, NIFTI_HEADER_SIZE_IN_BYTES
)));
}
if header.vox_offset > (usize::MAX as f32 / 2.0) {
return Err(NeuroformatsError::InvalidNiftiFormat(format!(
"vox_offset = {} is unreasonably large.",
header.vox_offset
)));
}
let vox_offset = header.vox_offset as usize;
let to_skip = vox_offset - NIFTI_HEADER_SIZE_IN_BYTES;
discard_bytes(input, to_skip)?;
let scaling = VolumeScaling::from_header(header.scl_slope, header.scl_inter)?;
let mut data_bytes = vec![0u8; num_bytes];
input
.read_exact(&mut data_bytes)
.map_err(|e| NeuroformatsError::Io(e))?;
let data = decode_volume(&data_bytes, mri_type, little_endian, &scaling, &dims)?;
let mgh_header = mgh_header_from_nifti(&header, &dims)?;
let volume = FsMgh {
header: mgh_header,
data,
};
Ok(Nifti1 { header, volume })
}
pub fn from_mgh(mgh: FsMgh) -> Result<Nifti1> {
let mh = &mgh.header;
for dim in [mh.dim1len, mh.dim2len, mh.dim3len, mh.dim4len].iter() {
if *dim <= 0 || *dim > i16::MAX as i32 {
return Err(NeuroformatsError::InvalidNiftiFormat(format!(
"MGH dimension {} exceeds the NIfTI-1 int16 range (1..=32767). Cannot write as NIfTI.",
dim
)));
}
}
let mut header = Nifti1Header::default();
header.sizeof_hdr = NIFTI_HEADER_SIZE_IN_BYTES as i32;
header.dim[0] = if mh.dim4len > 1 { 4 } else { 3 };
header.dim[1] = mh.dim1len as i16;
header.dim[2] = mh.dim2len as i16;
header.dim[3] = mh.dim3len as i16;
header.dim[4] = mh.dim4len as i16;
header.dim[5] = 1;
header.dim[6] = 1;
header.dim[7] = 1;
let (datatype, bitpix) = mri_dtype_to_nifti(mh.dtype)?;
header.datatype = datatype;
header.bitpix = bitpix;
header.vox_offset = NIFTI_DEFAULT_VOX_OFFSET;
header.scl_slope = 1.0;
header.scl_inter = 0.0;
header.pixdim[0] = 1.0;
header.pixdim[1] = 1.0;
header.pixdim[2] = 1.0;
header.pixdim[3] = 1.0;
header.pixdim[4] = 1.0;
header.pixdim[5] = 1.0;
header.pixdim[6] = 1.0;
header.pixdim[7] = 1.0;
if mh.is_ras_good == 1 {
let sizes = mh.delta;
if sizes.iter().all(|s| s.is_finite() && *s > 0.0) {
header.pixdim[1] = sizes[0];
header.pixdim[2] = sizes[1];
header.pixdim[3] = sizes[2];
}
if let Some(affine) = mgh_affine_from_header(mh) {
header.sform_code = XFORM_SCANNER_ANAT;
header.srow_x = [
affine[0][0], affine[0][1], affine[0][2], affine[0][3],
];
header.srow_y = [
affine[1][0], affine[1][1], affine[1][2], affine[1][3],
];
header.srow_z = [
affine[2][0], affine[2][1], affine[2][2], affine[2][3],
];
if let Some((qfac, b, c, d, qoffset)) = affine_to_qform(&affine) {
header.qform_code = XFORM_SCANNER_ANAT;
header.pixdim[0] = qfac;
header.quatern_b = b;
header.quatern_c = c;
header.quatern_d = d;
header.qoffset_x = qoffset[0];
header.qoffset_y = qoffset[1];
header.qoffset_z = qoffset[2];
} else {
header.qform_code = XFORM_UNKNOWN;
}
} else {
header.sform_code = XFORM_UNKNOWN;
header.qform_code = XFORM_UNKNOWN;
}
}
Ok(Nifti1 { header, volume: mgh })
}
pub fn to_mgh(&self) -> FsMgh {
self.volume.clone()
}
pub fn dim(&self) -> [usize; 4] {
self.volume.dim()
}
}
impl From<Nifti1> for FsMgh {
fn from(nifti: Nifti1) -> FsMgh {
nifti.volume
}
}
fn discard_bytes<S: BufRead>(input: &mut S, num_bytes: usize) -> Result<()> {
let mut remaining = num_bytes;
let mut tmp = [0u8; 4096];
while remaining > 0 {
let n = remaining.min(tmp.len());
input
.read_exact(&mut tmp[..n])
.map_err(|e| NeuroformatsError::Io(e))?;
remaining -= n;
}
Ok(())
}
fn max_1(v: i16) -> i16 {
if v > 0 {
v
} else {
1
}
}
fn decode_volume(
data_bytes: &[u8],
mri_type: i32,
little_endian: bool,
scaling: &VolumeScaling,
dims: &[usize; 4],
) -> Result<FsMghData> {
let vol_dim = Dim([dims[0], dims[1], dims[2], dims[3]]);
let nvox = dims[0] * dims[1] * dims[2] * dims[3];
let mut data = FsMghData {
mri_uchar: None,
mri_int: None,
mri_float: None,
mri_short: None,
};
match mri_type {
MRI_UCHAR => {
let values: Vec<u8> = if scaling.is_identity() {
data_bytes.to_vec()
} else {
data_bytes
.iter()
.map(|&v| {
let scaled = (v as f32) * scaling.slope + scaling.inter;
scaled.round().clamp(0.0, 255.0) as u8
})
.collect()
};
data.mri_uchar = Some(
Array::from_shape_vec(vol_dim, values)
.map_err(|e| NeuroformatsError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?,
);
}
MRI_SHORT => {
let raw = decode_ints::<i16>(data_bytes, 2, nvox, little_endian)?;
let values: Vec<i16> = if scaling.is_identity() {
raw
} else {
raw.iter()
.map(|&v| ((v as f32) * scaling.slope + scaling.inter).round() as i16)
.collect()
};
data.mri_short = Some(
Array::from_shape_vec(vol_dim, values)
.map_err(|e| NeuroformatsError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?,
);
}
MRI_INT => {
let raw = decode_ints::<i32>(data_bytes, 4, nvox, little_endian)?;
let values: Vec<i32> = if scaling.is_identity() {
raw
} else {
raw.iter()
.map(|&v| ((v as f32) * scaling.slope + scaling.inter).round() as i32)
.collect()
};
data.mri_int = Some(
Array::from_shape_vec(vol_dim, values)
.map_err(|e| NeuroformatsError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?,
);
}
MRI_FLOAT => {
let raw = decode_floats(data_bytes, nvox, little_endian)?;
let values: Vec<f32> = if scaling.is_identity() {
raw
} else {
raw.iter().map(|&v| v * scaling.slope + scaling.inter).collect()
};
data.mri_float = Some(
Array::from_shape_vec(vol_dim, values)
.map_err(|e| NeuroformatsError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?,
);
}
other => {
return Err(NeuroformatsError::UnsupportedNiftiDataType(other as i16));
}
}
Ok(data)
}
fn decode_ints<T>(bytes: &[u8], width: usize, num_items: usize, little_endian: bool) -> Result<Vec<T>>
where
T: IntFromBytes,
{
let mut out = Vec::with_capacity(num_items);
for chunk in bytes.chunks_exact(width) {
out.push(T::from_bytes(chunk, little_endian));
}
if out.len() != num_items {
return Err(NeuroformatsError::InvalidNiftiFormat(
"voxel data section has an unexpected length.".to_string(),
));
}
Ok(out)
}
trait IntFromBytes {
fn from_bytes(bytes: &[u8], little_endian: bool) -> Self;
}
impl IntFromBytes for i16 {
fn from_bytes(bytes: &[u8], little_endian: bool) -> Self {
if little_endian {
LittleEndian::read_i16(bytes)
} else {
BigEndian::read_i16(bytes)
}
}
}
impl IntFromBytes for i32 {
fn from_bytes(bytes: &[u8], little_endian: bool) -> Self {
if little_endian {
LittleEndian::read_i32(bytes)
} else {
BigEndian::read_i32(bytes)
}
}
}
fn decode_floats(bytes: &[u8], num_items: usize, little_endian: bool) -> Result<Vec<f32>> {
let mut out = Vec::with_capacity(num_items);
for chunk in bytes.chunks_exact(4) {
if little_endian {
out.push(LittleEndian::read_f32(chunk));
} else {
out.push(BigEndian::read_f32(chunk));
}
}
if out.len() != num_items {
return Err(NeuroformatsError::InvalidNiftiFormat(
"voxel data section has an unexpected length.".to_string(),
));
}
Ok(out)
}
fn mgh_affine_from_header(mh: &FsMghHeader) -> Option<[[f32; 4]; 4]> {
if mh.is_ras_good != 1 {
return None;
}
validate_finite_f32_slice(&mh.delta, "delta").ok()?;
validate_finite_f32_slice(&mh.mdc_raw, "mdc_raw").ok()?;
validate_finite_f32_slice(&mh.p_xyz_c, "p_xyz_c").ok()?;
let delta = mh.delta;
let mdc = mh.mdc_raw;
let mut affine = [[0.0f32; 4]; 4];
for i in 0..3 {
for j in 0..3 {
affine[i][j] = delta[j] * mdc[j * 3 + i];
}
}
let c_crs = [
(mh.dim1len / 2) as f32,
(mh.dim2len / 2) as f32,
(mh.dim3len / 2) as f32,
];
for i in 0..3 {
let mut center_world = 0.0;
for j in 0..3 {
center_world += affine[i][j] * c_crs[j];
}
affine[i][3] = mh.p_xyz_c[i] - center_world;
}
affine[3][3] = 1.0;
Some(affine)
}
fn affine_to_qform(affine: &[[f32; 4]; 4]) -> Option<(f32, f32, f32, f32, [f32; 3])> {
let mut sizes = [0.0f32; 3];
let mut rot = [[0.0f32; 3]; 3];
for j in 0..3 {
let mut norm = 0.0;
for i in 0..3 {
norm += affine[i][j] * affine[i][j];
}
sizes[j] = norm.sqrt();
if !sizes[j].is_finite() || sizes[j] < f32::EPSILON {
return None;
}
for i in 0..3 {
rot[i][j] = affine[i][j] / sizes[j];
}
}
let det = determinant3x3(&rot);
let qfac = if det < 0.0 { -1.0 } else { 1.0 };
for i in 0..3 {
rot[i][2] *= qfac;
}
let (_a, b, c, d) = rotation_matrix_to_quaternion(&rot);
let qoffset = [affine[0][3], affine[1][3], affine[2][3]];
Some((qfac, b, c, d, qoffset))
}
fn determinant3x3(m: &[[f32; 3]; 3]) -> f32 {
m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2])
- m[1][0] * (m[0][1] * m[2][2] - m[2][1] * m[0][2])
+ m[2][0] * (m[0][1] * m[1][2] - m[1][1] * m[0][2])
}
fn rotation_matrix_to_quaternion(r: &[[f32; 3]; 3]) -> (f32, f32, f32, f32) {
let trace = r[0][0] + r[1][1] + r[2][2];
let (a, b, c, d);
if trace > 0.0 {
let s = (trace + 1.0).sqrt() * 2.0;
a = 0.25 * s;
b = (r[2][1] - r[1][2]) / s;
c = (r[0][2] - r[2][0]) / s;
d = (r[1][0] - r[0][1]) / s;
} else if r[0][0] > r[1][1] && r[0][0] > r[2][2] {
let s = (1.0 + r[0][0] - r[1][1] - r[2][2]).sqrt() * 2.0;
a = (r[2][1] - r[1][2]) / s;
b = 0.25 * s;
c = (r[0][1] + r[1][0]) / s;
d = (r[0][2] + r[2][0]) / s;
} else if r[1][1] > r[2][2] {
let s = (1.0 + r[1][1] - r[0][0] - r[2][2]).sqrt() * 2.0;
a = (r[0][2] - r[2][0]) / s;
b = (r[0][1] + r[1][0]) / s;
c = 0.25 * s;
d = (r[1][2] + r[2][1]) / s;
} else {
let s = (1.0 + r[2][2] - r[0][0] - r[1][1]).sqrt() * 2.0;
a = (r[1][0] - r[0][1]) / s;
b = (r[0][2] + r[2][0]) / s;
c = (r[1][2] + r[2][1]) / s;
d = 0.25 * s;
}
(a, b, c, d)
}
fn quaternion_to_rotation(a: f32, b: f32, c: f32, d: f32) -> [[f32; 3]; 3] {
[
[
a * a + b * b - c * c - d * d,
2.0 * (b * c - a * d),
2.0 * (b * d + a * c),
],
[
2.0 * (b * c + a * d),
a * a + c * c - b * b - d * d,
2.0 * (c * d - a * b),
],
[
2.0 * (b * d - a * c),
2.0 * (c * d + a * b),
a * a + d * d - b * b - c * c,
],
]
}
fn decode_transform(
header: &Nifti1Header,
dims: &[usize; 4],
) -> Result<Option<([f32; 3], [f32; 9], [f32; 3])>> {
let (linear, translation): ([[f32; 3]; 3], [f32; 3]) = if header.sform_code > 0 {
validate_finite_f32_slice(&header.srow_x, "srow_x")?;
validate_finite_f32_slice(&header.srow_y, "srow_y")?;
validate_finite_f32_slice(&header.srow_z, "srow_z")?;
let linear = [
[header.srow_x[0], header.srow_x[1], header.srow_x[2]],
[header.srow_y[0], header.srow_y[1], header.srow_y[2]],
[header.srow_z[0], header.srow_z[1], header.srow_z[2]],
];
let translation = [header.srow_x[3], header.srow_y[3], header.srow_z[3]];
(linear, translation)
} else if header.qform_code > 0 {
validate_finite_f32_slice(
&[
header.quatern_b,
header.quatern_c,
header.quatern_d,
header.qoffset_x,
header.qoffset_y,
header.qoffset_z,
],
"qform quaternion/qoffset",
)?;
validate_finite_f32_slice(&header.pixdim[0..4], "pixdim")?;
let a = (1.0 - (header.quatern_b * header.quatern_b
+ header.quatern_c * header.quatern_c
+ header.quatern_d * header.quatern_d))
.max(0.0)
.sqrt();
let qfac = if header.pixdim[0] < 0.0 { -1.0 } else { 1.0 };
let mut rot = quaternion_to_rotation(a, header.quatern_b, header.quatern_c, header.quatern_d);
for i in 0..3 {
rot[i][2] *= qfac;
}
let mut linear = [[0.0f32; 3]; 3];
for j in 0..3 {
let size = header.pixdim[j + 1].max(0.0);
for i in 0..3 {
linear[i][j] = rot[i][j] * size;
}
}
let translation = [header.qoffset_x, header.qoffset_y, header.qoffset_z];
(linear, translation)
} else {
return Ok(None);
};
let mut delta = [0.0f32; 3];
let mut direction = [[0.0f32; 3]; 3];
for j in 0..3 {
let mut norm = 0.0;
for i in 0..3 {
norm += linear[i][j] * linear[i][j];
}
delta[j] = norm.sqrt();
if delta[j] > f32::EPSILON {
for i in 0..3 {
direction[i][j] = linear[i][j] / delta[j];
}
} else {
delta[j] = 1.0;
for i in 0..3 {
direction[i][j] = if i == j { 1.0 } else { 0.0 };
}
}
}
let mut mdc_raw = [0.0f32; 9];
for j in 0..3 {
for i in 0..3 {
mdc_raw[j * 3 + i] = direction[i][j];
}
}
let c_crs = [
(dims[0] / 2) as f32,
(dims[1] / 2) as f32,
(dims[2] / 2) as f32,
];
let mut p_xyz_c = translation;
for i in 0..3 {
for j in 0..3 {
p_xyz_c[i] += linear[i][j] * c_crs[j];
}
}
Ok(Some((delta, mdc_raw, p_xyz_c)))
}
fn mgh_header_from_nifti(header: &Nifti1Header, dims: &[usize; 4]) -> Result<FsMghHeader> {
let mut mh = FsMghHeader::default();
mh.dim1len = dims[0] as i32;
mh.dim2len = dims[1] as i32;
mh.dim3len = dims[2] as i32;
mh.dim4len = dims[3] as i32;
mh.dtype = nifti_dtype_to_mri(header.datatype)?;
mh.dof = 0;
if let Some((delta, mdc_raw, p_xyz_c)) = decode_transform(header, dims)? {
mh.is_ras_good = 1;
mh.delta = delta;
mh.mdc_raw = mdc_raw;
mh.p_xyz_c = p_xyz_c;
} else {
mh.is_ras_good = 0;
}
Ok(mh)
}
pub fn read_nifti<P: AsRef<Path> + Copy>(path: P) -> Result<Nifti1> {
Nifti1::from_file(path)
}
pub fn write_nifti<P: AsRef<Path> + Copy>(path: P, nifti: &Nifti1) -> Result<()> {
let file = File::create(path)?;
let writer = BufWriter::new(&file);
if is_nii_gz_file(path) {
write_nifti_to(
flate2::write::GzEncoder::new(writer, Compression::default()),
nifti,
)
} else {
write_nifti_to(writer, nifti)
}
}
fn write_nifti_to<W>(w: W, nifti: &Nifti1) -> Result<()>
where
W: Write,
{
let header = &nifti.header;
let volume = &nifti.volume;
let mh = &volume.header;
let mri_type = nifti_dtype_to_mri(header.datatype)?;
if mri_type != mh.dtype {
return Err(NeuroformatsError::InvalidNiftiFormat(format!(
"inconsistent volume data type: header says {}, volume says {}.",
header.datatype, mh.dtype
)));
}
let expected_dims = [
max_1(header.dim[1]) as usize,
max_1(header.dim[2]) as usize,
max_1(header.dim[3]) as usize,
max_1(header.dim[4]) as usize,
];
let volume_dims = volume.dim();
if expected_dims != volume_dims {
return Err(NeuroformatsError::InvalidNiftiFormat(format!(
"inconsistent dimensions: header says {:?}, volume has {:?}.",
expected_dims, volume_dims
)));
}
let mut out = BufWriter::new(w);
let header_bytes = header.to_bytes(false);
out.write_all(&header_bytes)?;
out.write_all(&[0u8; 4])?;
let mut data_writer = ByteOrdered::runtime(&mut out, Endianness::Big);
if mri_type == MRI_UCHAR {
for v in volume.data.mri_uchar.as_ref().unwrap().iter() {
data_writer.write_u8(*v)?;
}
} else if mri_type == MRI_SHORT {
for v in volume.data.mri_short.as_ref().unwrap().iter() {
data_writer.write_i16(*v)?;
}
} else if mri_type == MRI_INT {
for v in volume.data.mri_int.as_ref().unwrap().iter() {
data_writer.write_i32(*v)?;
}
} else if mri_type == MRI_FLOAT {
for v in volume.data.mri_float.as_ref().unwrap().iter() {
data_writer.write_f32(*v)?;
}
} else {
return Err(NeuroformatsError::UnsupportedNiftiDataType(header.datatype));
}
out.flush()?;
Ok(())
}
impl fmt::Display for Nifti1 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"NIfTI-1 volume with dim {}, {}, {}, {} and data type {}.",
self.header.dim[1],
self.header.dim[2],
self.header.dim[3],
self.header.dim[4],
self.header.datatype
)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::fs_mgh::{read_mgh, write_mgh};
use approx::assert_abs_diff_eq;
use std::io::Cursor;
use tempfile::tempdir;
const NII_FILE: &str = "resources/subjects_dir/subject1/mri/brain.nii";
const MGZ_FILE: &str = "resources/subjects_dir/subject1/mri/brain.mgz";
fn assert_same_data(a: &FsMgh, b: &FsMgh) {
assert_eq!(a.header.dtype, b.header.dtype);
assert_eq!(a.dim(), b.dim());
match a.header.dtype {
MRI_UCHAR => {
let (da, db) = (a.data.mri_uchar.as_ref().unwrap(), b.data.mri_uchar.as_ref().unwrap());
assert_eq!(da, db);
}
MRI_SHORT => {
let (da, db) = (a.data.mri_short.as_ref().unwrap(), b.data.mri_short.as_ref().unwrap());
assert_eq!(da, db);
}
MRI_INT => {
let (da, db) = (a.data.mri_int.as_ref().unwrap(), b.data.mri_int.as_ref().unwrap());
assert_eq!(da, db);
}
MRI_FLOAT => {
let (da, db) = (a.data.mri_float.as_ref().unwrap(), b.data.mri_float.as_ref().unwrap());
assert_abs_diff_eq!(da, db, epsilon = 1e-5);
}
other => panic!("unexpected data type {}", other),
}
}
fn assert_f32_slice_approx(a: &[f32], b: &[f32], eps: f32) {
assert_eq!(a.len(), b.len(), "slices differ in length ({} vs {})", a.len(), b.len());
for (x, y) in a.iter().zip(b.iter()) {
assert_abs_diff_eq!(*x, *y, epsilon = eps);
}
}
fn assert_same_ras(a: &FsMgh, b: &FsMgh) {
assert_eq!(a.header.is_ras_good, b.header.is_ras_good);
if a.header.is_ras_good == 1 {
for i in 0..3 {
assert_abs_diff_eq!(a.header.delta[i], b.header.delta[i], epsilon = 1e-4);
}
for i in 0..9 {
assert_abs_diff_eq!(a.header.mdc_raw[i], b.header.mdc_raw[i], epsilon = 1e-4);
}
for i in 0..3 {
assert_abs_diff_eq!(a.header.p_xyz_c[i], b.header.p_xyz_c[i], epsilon = 1e-4);
}
let vox2ras_a = a.header.vox2ras().unwrap();
let vox2ras_b = b.header.vox2ras().unwrap();
assert_abs_diff_eq!(vox2ras_a, vox2ras_b, epsilon = 1e-3);
}
}
#[test]
fn one_can_read_the_demo_nifti_file() {
let nifti = read_nifti(NII_FILE).unwrap();
let brain = nifti.to_mgh();
assert_eq!(brain.dim(), [256, 256, 256, 1]);
assert_eq!(brain.header.dtype, MRI_UCHAR);
assert_eq!(brain.header.is_ras_good, 1);
assert_eq!(nifti.header.datatype, DT_UINT8);
assert_eq!(nifti.header.sform_code, XFORM_SCANNER_ANAT);
assert_eq!(nifti.header.qform_code, XFORM_SCANNER_ANAT);
let data = brain.data.mri_uchar.unwrap();
assert_eq!(data[[99, 99, 99, 0]], 77);
assert_eq!(data[[109, 109, 109, 0]], 71);
assert_eq!(data[[0, 0, 0, 0]], 0);
assert_eq!(data.mapv(|a| a as i32).sum(), 121035479);
}
#[test]
fn reading_nifti_and_mgh_gives_the_same_volume() {
let from_nii = read_nifti(NII_FILE).unwrap().to_mgh();
let from_mgz = read_mgh(MGZ_FILE).unwrap();
assert_same_data(&from_nii, &from_mgz);
assert_same_ras(&from_nii, &from_mgz);
}
#[test]
fn the_freesurfer_brain_nifti_matches_the_brain_mgz() {
let nifti = read_nifti(NII_FILE).unwrap();
let from_nii = nifti.to_mgh();
let from_mgz = read_mgh(MGZ_FILE).unwrap();
assert_eq!(from_nii.dim(), from_mgz.dim());
assert_eq!(from_nii.header.dtype, from_mgz.header.dtype);
assert_eq!(from_nii.header.dtype, MRI_UCHAR);
assert_eq!(from_nii.header.is_ras_good, 1);
assert_eq!(from_mgz.header.is_ras_good, 1);
let nii_data = from_nii.data.mri_uchar.as_ref().unwrap();
let mgz_data = from_mgz.data.mri_uchar.as_ref().unwrap();
assert_eq!(nii_data, mgz_data);
let expected_delta = [1.0_f32, 1.0, 1.0];
let expected_mdc_raw = [-1.0_f32, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0];
let expected_p_xyz_c = [-0.49995422_f32, 29.372742, -48.90473];
assert_f32_slice_approx(&from_nii.header.delta, &expected_delta, 1e-4);
assert_f32_slice_approx(&from_nii.header.mdc_raw, &expected_mdc_raw, 1e-4);
assert_f32_slice_approx(&from_nii.header.p_xyz_c, &expected_p_xyz_c, 1e-4);
assert_f32_slice_approx(&from_mgz.header.delta, &expected_delta, 1e-4);
assert_f32_slice_approx(&from_mgz.header.mdc_raw, &expected_mdc_raw, 1e-4);
assert_f32_slice_approx(&from_mgz.header.p_xyz_c, &expected_p_xyz_c, 1e-4);
let vox2ras_nii = from_nii.header.vox2ras().unwrap();
let vox2ras_mgz = from_mgz.header.vox2ras().unwrap();
assert_abs_diff_eq!(vox2ras_nii, vox2ras_mgz, epsilon = 1e-2);
let expected_vox2ras = [
[-1.0_f32, 0.0, 0.0, 127.5],
[0.0, 0.0, 1.0, -98.6273],
[0.0, -1.0, 0.0, 79.0953],
[0.0, 0.0, 0.0, 1.0],
];
let sform_rows = [nifti.header.srow_x, nifti.header.srow_y, nifti.header.srow_z];
for i in 0..3 {
for j in 0..4 {
assert_abs_diff_eq!(vox2ras_nii[[i, j]], expected_vox2ras[i][j], epsilon = 1e-2);
assert_abs_diff_eq!(vox2ras_nii[[i, j]], sform_rows[i][j], epsilon = 1e-2);
}
}
let sform_translation = [
nifti.header.srow_x[3],
nifti.header.srow_y[3],
nifti.header.srow_z[3],
];
assert_f32_slice_approx(&sform_translation, &[127.5_f32, -98.6273, 79.0953], 1e-2);
for i in 0..3 {
assert!(
(sform_translation[i] - from_nii.header.p_xyz_c[i]).abs() > 10.0,
"expected the voxel-(0,0,0) RAS and the center voxel RAS to differ by more than 10 mm"
);
}
}
#[test]
fn mgh_can_be_converted_to_nifti_and_back() {
let brain = read_mgh(MGZ_FILE).unwrap();
let dir = tempdir().unwrap();
let nii_path = dir.path().join("brain.nii");
let nifti = Nifti1::from_mgh(brain.clone()).unwrap();
write_nifti(&nii_path, &nifti).unwrap();
let brain2 = read_nifti(&nii_path).unwrap().to_mgh();
assert_same_data(&brain, &brain2);
assert_same_ras(&brain, &brain2);
}
#[test]
fn nifti_can_be_converted_to_mgh_and_back() {
let brain = read_nifti(NII_FILE).unwrap().to_mgh();
let dir = tempdir().unwrap();
let mgh_path = dir.path().join("brain.mgh");
write_mgh(&mgh_path, &brain).unwrap();
let brain2 = read_mgh(&mgh_path).unwrap();
assert_same_data(&brain, &brain2);
assert_same_ras(&brain, &brain2);
}
#[test]
fn writing_the_mgz_as_nifti_matches_the_freesurfer_reference() {
let brain = read_mgh(MGZ_FILE).unwrap();
let nifti = Nifti1::from_mgh(brain).unwrap();
let h = &nifti.header;
assert_eq!(h.sform_code, XFORM_SCANNER_ANAT);
assert_eq!(h.qform_code, XFORM_SCANNER_ANAT);
let expected_sform = [
[-1.0, 0.0, 0.0, 127.50005],
[0.0, 0.0, 1.0, -98.62726],
[0.0, -1.0, 0.0, 79.09527],
];
for (i, row) in expected_sform.iter().enumerate() {
let srow = match i {
0 => h.srow_x,
1 => h.srow_y,
_ => h.srow_z,
};
for (j, v) in row.iter().enumerate() {
assert_abs_diff_eq!(srow[j], *v, epsilon = 1e-3);
}
}
assert_abs_diff_eq!(h.qoffset_x, 127.50005, epsilon = 1e-3);
assert_abs_diff_eq!(h.qoffset_y, -98.62726, epsilon = 1e-3);
assert_abs_diff_eq!(h.qoffset_z, 79.09527, epsilon = 1e-3);
assert_abs_diff_eq!(h.pixdim[0], -1.0, epsilon = 1e-4);
}
#[test]
fn a_small_float_volume_can_be_written_and_reread_without_ras() {
let mut mh = FsMghHeader::default();
mh.dim1len = 2;
mh.dim2len = 3;
mh.dim3len = 4;
mh.dim4len = 1;
mh.dtype = MRI_FLOAT;
mh.is_ras_good = 0;
let values: Vec<f32> = (0..24).map(|i| i as f32 + 0.5).collect();
let data = FsMghData {
mri_float: Some(Array::from_shape_vec(Dim([2, 3, 4, 1]), values).unwrap()),
mri_uchar: None,
mri_int: None,
mri_short: None,
};
let volume = FsMgh { header: mh, data };
let nifti = Nifti1::from_mgh(volume.clone()).unwrap();
assert_eq!(nifti.header.sform_code, XFORM_UNKNOWN);
assert_eq!(nifti.header.qform_code, XFORM_UNKNOWN);
let dir = tempdir().unwrap();
let nii_path = dir.path().join("small.nii");
write_nifti(&nii_path, &nifti).unwrap();
let volume2 = read_nifti(&nii_path).unwrap().to_mgh();
assert_same_data(&volume, &volume2);
assert_eq!(volume2.header.is_ras_good, 0);
}
#[test]
fn a_qform_only_file_can_be_read() {
let mut header = Nifti1Header::default();
header.dim[0] = 3;
header.dim[1] = 2;
header.dim[2] = 3;
header.dim[3] = 4;
header.dim[4] = 1;
header.datatype = DT_FLOAT32;
header.bitpix = 32;
header.pixdim[0] = 1.0;
header.pixdim[1] = 1.0;
header.pixdim[2] = 1.0;
header.pixdim[3] = 1.0;
header.vox_offset = NIFTI_DEFAULT_VOX_OFFSET;
header.qform_code = XFORM_SCANNER_ANAT;
header.sform_code = XFORM_UNKNOWN;
header.quatern_b = 0.0;
header.quatern_c = 0.0;
header.quatern_d = (0.5_f32).sqrt();
header.qoffset_x = 10.0;
header.qoffset_y = 20.0;
header.qoffset_z = 30.0;
let values: Vec<f32> = (0..24).map(|i| i as f32).collect();
let mh = mgh_header_from_nifti(&header, &[2, 3, 4, 1]).unwrap();
let volume = FsMgh {
header: mh,
data: FsMghData {
mri_float: Some(Array::from_shape_vec(Dim([2, 3, 4, 1]), values).unwrap()),
mri_uchar: None,
mri_int: None,
mri_short: None,
},
};
let nifti = Nifti1 { header, volume };
let dir = tempdir().unwrap();
let nii_path = dir.path().join("qform.nii");
write_nifti(&nii_path, &nifti).unwrap();
let mgh = read_nifti(&nii_path).unwrap().to_mgh();
assert_eq!(mgh.dim(), [2, 3, 4, 1]);
assert_eq!(mgh.header.dtype, MRI_FLOAT);
assert_eq!(mgh.header.is_ras_good, 1);
let expected_mdc_raw = [
0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ];
for i in 0..9 {
assert_abs_diff_eq!(mgh.header.mdc_raw[i], expected_mdc_raw[i], epsilon = 1e-5);
}
assert_abs_diff_eq!(mgh.header.p_xyz_c[0], 10.0 - 1.0, epsilon = 1e-4);
assert_abs_diff_eq!(mgh.header.p_xyz_c[1], 20.0 + 1.0, epsilon = 1e-4);
assert_abs_diff_eq!(mgh.header.p_xyz_c[2], 30.0 + 2.0, epsilon = 1e-4);
}
#[test]
fn gzipped_nifti_roundtrip() {
let brain = read_mgh(MGZ_FILE).unwrap();
let dir = tempdir().unwrap();
let nii_gz_path = dir.path().join("brain.nii.gz");
let nifti = Nifti1::from_mgh(brain.clone()).unwrap();
write_nifti(&nii_gz_path, &nifti).unwrap();
let brain2 = read_nifti(&nii_gz_path).unwrap().to_mgh();
assert_same_data(&brain, &brain2);
assert_same_ras(&brain, &brain2);
}
#[test]
fn header_roundtrips_through_bytes() {
let nifti = read_nifti(NII_FILE).unwrap();
let h = &nifti.header;
for little in [true, false].iter() {
let bytes = h.to_bytes(*little);
let h2 = Nifti1Header::from_bytes(&bytes, *little);
assert_eq!(h, &h2);
}
}
#[test]
fn rejects_a_two_file_volume() {
let mut hdr_bytes = [0u8; NIFTI_HEADER_SIZE_IN_BYTES];
BigEndian::write_i32(&mut hdr_bytes[0..4], 348);
hdr_bytes[344..348].copy_from_slice(&MAGIC_TWO_FILE);
let mut cursor = Cursor::new(hdr_bytes.to_vec());
let result = Nifti1::from_reader(&mut cursor);
assert!(matches!(result, Err(NeuroformatsError::InvalidNiftiFormat(_))));
}
#[test]
fn rejects_a_bad_magic_string() {
let mut hdr_bytes = [0u8; NIFTI_HEADER_SIZE_IN_BYTES];
BigEndian::write_i32(&mut hdr_bytes[0..4], 348);
hdr_bytes[344..348].copy_from_slice(&[b'x', b'y', b'z', 0]);
let mut cursor = Cursor::new(hdr_bytes.to_vec());
let result = Nifti1::from_reader(&mut cursor);
assert!(matches!(result, Err(NeuroformatsError::InvalidNiftiFormat(_))));
}
#[test]
fn rejects_a_bad_sizeof_hdr() {
let mut hdr_bytes = [0u8; NIFTI_HEADER_SIZE_IN_BYTES];
BigEndian::write_i32(&mut hdr_bytes[0..4], 999);
let mut cursor = Cursor::new(hdr_bytes.to_vec());
let result = Nifti1::from_reader(&mut cursor);
assert!(matches!(result, Err(NeuroformatsError::InvalidNiftiFormat(_))));
}
#[test]
fn rejects_negative_dim1() {
let mut h = Nifti1Header::default();
h.dim[0] = 3;
h.dim[1] = -100;
let bytes = h.to_bytes(false);
let mut cursor = Cursor::new(bytes.to_vec());
let result = Nifti1::from_reader(&mut cursor);
assert!(matches!(result, Err(NeuroformatsError::InvalidNiftiFormat(_))));
}
#[test]
fn rejects_more_than_4_dimensions() {
let mut h = Nifti1Header::default();
h.dim[0] = 5;
h.dim[1] = 2;
h.dim[2] = 2;
h.dim[3] = 2;
h.dim[4] = 2;
h.dim[5] = 2; h.datatype = DT_UINT8;
h.bitpix = 8;
let bytes = h.to_bytes(false);
let mut cursor = Cursor::new(bytes.to_vec());
let result = Nifti1::from_reader(&mut cursor);
assert!(matches!(result, Err(NeuroformatsError::InvalidNiftiFormat(_))));
}
#[test]
fn rejects_an_unsupported_data_type() {
let mut h = Nifti1Header::default();
h.dim[0] = 3;
h.dim[1] = 2;
h.dim[2] = 2;
h.dim[3] = 2;
h.dim[4] = 1;
h.datatype = 64; h.bitpix = 64;
let mut file_bytes = h.to_bytes(false).to_vec();
file_bytes.extend_from_slice(&[0u8; 4]);
file_bytes.extend_from_slice(&[0u8; 8]);
let mut cursor = Cursor::new(file_bytes);
let result = Nifti1::from_reader(&mut cursor);
assert!(matches!(
result,
Err(NeuroformatsError::UnsupportedNiftiDataType(64))
));
}
#[test]
fn rejects_an_invalid_vox_offset() {
let mut h = Nifti1Header::default();
h.dim[0] = 3;
h.dim[1] = 2;
h.dim[2] = 2;
h.dim[3] = 2;
h.dim[4] = 1;
h.datatype = DT_UINT8;
h.bitpix = 8;
h.vox_offset = 10.0; let bytes = h.to_bytes(false);
let mut cursor = Cursor::new(bytes.to_vec());
let result = Nifti1::from_reader(&mut cursor);
assert!(matches!(result, Err(NeuroformatsError::InvalidNiftiFormat(_))));
}
#[test]
fn rejects_nan_in_sform() {
let mut h = Nifti1Header::default();
h.dim[0] = 3;
h.dim[1] = 2;
h.dim[2] = 2;
h.dim[3] = 2;
h.dim[4] = 1;
h.datatype = DT_UINT8;
h.bitpix = 8;
h.sform_code = XFORM_SCANNER_ANAT;
h.srow_x = [f32::NAN, 0.0, 0.0, 0.0];
let mut file_bytes = h.to_bytes(false).to_vec();
file_bytes.extend_from_slice(&[0u8; 4]);
file_bytes.extend_from_slice(&[0u8; 8]);
let mut cursor = Cursor::new(file_bytes);
let result = Nifti1::from_reader(&mut cursor);
assert!(matches!(result, Err(NeuroformatsError::InvalidHeaderValue(_))));
}
#[test]
fn rejects_huge_dimensions() {
let mut h = Nifti1Header::default();
h.dim[0] = 4;
h.dim[1] = 32767;
h.dim[2] = 32767;
h.dim[3] = 32767;
h.dim[4] = 1;
h.datatype = DT_UINT8;
h.bitpix = 8;
let bytes = h.to_bytes(false);
let mut cursor = Cursor::new(bytes.to_vec());
let result = Nifti1::from_reader(&mut cursor);
assert!(matches!(result, Err(NeuroformatsError::AllocationTooLarge)));
}
#[test]
fn the_vox2ras_fix_matches_freesurfer_for_anisotropic_data() {
let mut mh = FsMghHeader::default();
mh.dim1len = 10;
mh.dim2len = 10;
mh.dim3len = 10;
mh.dim4len = 1;
mh.dtype = MRI_FLOAT;
mh.is_ras_good = 1;
mh.delta = [1.0, 2.0, 3.0];
mh.mdc_raw = [-1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0];
mh.p_xyz_c = [0.0, 0.0, 0.0];
let v = mh.vox2ras().unwrap();
for j in 0..3 {
let mut norm = 0.0;
for i in 0..3 {
norm += v[[i, j]] * v[[i, j]];
}
assert_abs_diff_eq!(norm.sqrt(), mh.delta[j], epsilon = 1e-5);
}
let volume = FsMgh {
header: mh,
data: FsMghData {
mri_float: Some(Array::from_shape_vec(
Dim([10, 10, 10, 1]),
vec![0.0f32; 1000],
).unwrap()),
mri_uchar: None,
mri_int: None,
mri_short: None,
},
};
let nifti = Nifti1::from_mgh(volume.clone()).unwrap();
let dir = tempdir().unwrap();
let nii_path = dir.path().join("aniso.nii");
write_nifti(&nii_path, &nifti).unwrap();
let volume2 = read_nifti(&nii_path).unwrap().to_mgh();
assert_same_ras(&volume, &volume2);
}
}