diff --git a/Cargo.toml b/Cargo.toml index bbebe19..59b771e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,10 @@ flate2 = "1.0" num-derive = "0.3" num-traits = "0.2" quick-error = "2.0" -safe-transmute = "0.11" either = "1.6" +num-complex = {version = "0.4.3", features=["bytemuck"]} +rgb = "0.8.36" +bytemuck = {version = "1.13.1", features=["extern_crate_alloc"]} [dependencies.nalgebra] optional = true diff --git a/resources/complex/complex32.nii b/resources/complex/complex32.nii new file mode 100644 index 0000000..0f004a5 Binary files /dev/null and b/resources/complex/complex32.nii differ diff --git a/resources/complex/complex64.nii b/resources/complex/complex64.nii new file mode 100644 index 0000000..0db122f Binary files /dev/null and b/resources/complex/complex64.nii differ diff --git a/resources/rgba/4D.nii b/resources/rgba/4D.nii new file mode 100644 index 0000000..e320b80 Binary files /dev/null and b/resources/rgba/4D.nii differ diff --git a/src/error.rs b/src/error.rs index a62cc78..40d96c1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -83,6 +83,11 @@ quick_error! { display("Could not reserve {} bytes of memory for extended data", bytes) source(err) } + + /// Attempted a type conversion that is not supported by this crate + InvalidTypeConversion(from: NiftiType, to: &'static str) { + display("Invalid type conversion from {:?} to {}", from, to) + } } } diff --git a/src/typedef.rs b/src/typedef.rs index 9a9be4f..f75fe3b 100644 --- a/src/typedef.rs +++ b/src/typedef.rs @@ -5,7 +5,7 @@ //! converted to these types and vice-versa. use crate::error::{NiftiError, Result}; -use crate::volume::element::{DataElement, LinearTransform}; +use crate::volume::element::{DataElement, NiftiDataRescaler}; use byteordered::{Endian, Endianness}; use num_derive::FromPrimitive; use std::io::Read; @@ -96,11 +96,12 @@ impl NiftiType { T: Mul, T: Add, T: DataElement, + T: NiftiDataRescaler, { match self { NiftiType::Uint8 => { let raw = u8::from_raw(source, endianness)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_u8(raw), slope, inter, @@ -108,7 +109,7 @@ impl NiftiType { } NiftiType::Int8 => { let raw = i8::from_raw(source, endianness)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_i8(raw), slope, inter, @@ -116,7 +117,7 @@ impl NiftiType { } NiftiType::Uint16 => { let raw = endianness.read_u16(source)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_u16(raw), slope, inter, @@ -124,7 +125,7 @@ impl NiftiType { } NiftiType::Int16 => { let raw = endianness.read_i16(source)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_i16(raw), slope, inter, @@ -132,7 +133,7 @@ impl NiftiType { } NiftiType::Uint32 => { let raw = endianness.read_u32(source)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_u32(raw), slope, inter, @@ -140,7 +141,7 @@ impl NiftiType { } NiftiType::Int32 => { let raw = endianness.read_i32(source)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_i32(raw), slope, inter, @@ -148,7 +149,7 @@ impl NiftiType { } NiftiType::Uint64 => { let raw = endianness.read_u64(source)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_u64(raw), slope, inter, @@ -156,7 +157,7 @@ impl NiftiType { } NiftiType::Int64 => { let raw = endianness.read_i64(source)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_i64(raw), slope, inter, @@ -164,7 +165,7 @@ impl NiftiType { } NiftiType::Float32 => { let raw = endianness.read_f32(source)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_f32(raw), slope, inter, @@ -172,12 +173,13 @@ impl NiftiType { } NiftiType::Float64 => { let raw = endianness.read_f64(source)?; - Ok(::Transform::linear_transform( + Ok(::DataRescaler::nifti_rescale( T::from_f64(raw), slope, inter, )) } + // TODO(#3) add support for more data types _ => Err(NiftiError::UnsupportedDataType(self)), } diff --git a/src/util.rs b/src/util.rs index c0b2a32..88c0d91 100644 --- a/src/util.rs +++ b/src/util.rs @@ -6,28 +6,24 @@ use crate::NiftiHeader; use byteordered::Endian; use either::Either; use flate2::bufread::GzDecoder; -use safe_transmute::{transmute_vec, TriviallyTransmutable}; use std::borrow::Cow; use std::fs::File; use std::io::{BufReader, Read, Result as IoResult, Seek}; use std::mem; use std::path::{Path, PathBuf}; - /// A trait that is both Read and Seek. pub trait ReadSeek: Read + Seek {} impl ReadSeek for T {} pub fn convert_bytes_to(mut a: Vec, e: E) -> Vec where - T: TriviallyTransmutable, + T: bytemuck::Pod, E: Endian, { adapt_bytes_inline::(&mut a, e); - match transmute_vec(a) { + match bytemuck::allocation::try_cast_vec(a) { Ok(v) => v, - Err(safe_transmute::Error::IncompatibleVecTarget(e)) => e.copy(), - Err(safe_transmute::Error::Unaligned(e)) => e.copy(), - _ => unreachable!(), + Err((_, v)) => bytemuck::allocation::pod_collect_to_vec(&v), } } diff --git a/src/volume/element.rs b/src/volume/element.rs index a8e25f9..24672b2 100644 --- a/src/volume/element.rs +++ b/src/volume/element.rs @@ -3,110 +3,187 @@ //! elements. use crate::error::Result; use crate::util::convert_bytes_to; +use crate::NiftiError; use crate::NiftiType; + +use bytemuck::*; use byteordered::{ByteOrdered, Endian}; -use num_traits::cast::AsPrimitive; -use safe_transmute::transmute_vec; +use num_complex::{Complex, Complex32, Complex64}; +use rgb::*; use std::io::Read; use std::mem::align_of; -use std::ops::{Add, Mul}; - -/// Interface for linear (affine) transformations to values. Multiple -/// implementations are needed because the original type `T` may not have -/// enough precision to obtain an appropriate outcome. For example, -/// transforming a `u8` is always done through `f32`, but an `f64` is instead -/// manipulated through its own type by first converting the slope and -/// intercept arguments to `f64`. -pub trait LinearTransform { - /// Linearly transform a value with the given slope and intercept. - fn linear_transform(value: T, slope: f32, intercept: f32) -> T; - - /// Linearly transform a sequence of values with the given slope and intercept into - /// a vector. - fn linear_transform_many(value: &[T], slope: f32, intercept: f32) -> Vec { + +/// NiftiDataRescaler, a trait for rescaling data elements according to the Nifti 1.1 specification +pub trait NiftiDataRescaler { + /// Rescale a single value with the given slope and intercept. + fn nifti_rescale(value: T, slope: f32, intercept: f32) -> T; + + /// Rescale a slice of values, with the given slope and intercept. + fn nifti_rescale_many(value: &[T], slope: f32, intercept: f32) -> Vec { value .iter() - .map(|x| Self::linear_transform(*x, slope, intercept)) + .map(|x| Self::nifti_rescale(*x, slope, intercept)) .collect() } - /// Linearly transform a sequence of values inline, with the given slope and intercept. - fn linear_transform_many_inline(value: &mut [T], slope: f32, intercept: f32) { + /// Rescale a slice of values inline (inplace), with the given slope and intercept. + fn nifti_rescale_many_inline(value: &mut [T], slope: f32, intercept: f32) { for v in value.iter_mut() { - *v = Self::linear_transform(*v, slope, intercept); + *v = Self::nifti_rescale(*v, slope, intercept); } } } -/// A linear transformation in which the value is converted to `f32` for the -/// affine transformation, then converted back to the original type. Ideal for -/// small, low precision types such as `u8` and `i16`. -#[derive(Debug)] -pub struct LinearTransformViaF32; +impl NiftiDataRescaler for u8 { + fn nifti_rescale(value: u8, slope: f32, intercept: f32) -> u8 { + if slope == 0. { + value + } else { + (value as f32 * slope + intercept) as u8 + } + } +} -impl LinearTransform for LinearTransformViaF32 -where - T: 'static + Copy + DataElement, -{ - fn linear_transform(value: T, slope: f32, intercept: f32) -> T { +impl NiftiDataRescaler for i8 { + fn nifti_rescale(value: i8, slope: f32, intercept: f32) -> i8 { if slope == 0. { return value; } - T::from_f32(AsPrimitive::::as_(value) * slope + intercept) + (value as f32 * slope + intercept) as i8 } } -/// A linear transformation in which the value and parameters are converted to -/// `f64` for the affine transformation, then converted to the original type. -/// Ideal for wide integer types such as `i64`. -#[derive(Debug)] -pub struct LinearTransformViaF64; +impl NiftiDataRescaler for u16 { + fn nifti_rescale(value: u16, slope: f32, intercept: f32) -> u16 { + if slope == 0. { + return value; + } + (value as f32 * slope + intercept) as u16 + } +} -impl LinearTransform for LinearTransformViaF64 -where - T: 'static + Copy + DataElement, -{ - fn linear_transform(value: T, slope: f32, intercept: f32) -> T { +impl NiftiDataRescaler for i16 { + fn nifti_rescale(value: i16, slope: f32, intercept: f32) -> i16 { if slope == 0. { return value; } - let slope: f64 = slope.as_(); - let intercept: f64 = intercept.as_(); - T::from_f64(AsPrimitive::::as_(value) * slope + intercept) + (value as f32 * slope + intercept) as i16 } } -/// A linear transformation in which the slope and intercept parameters are -/// converted to the value's type for the affine transformation. Ideal -/// for high precision or complex number types. -#[derive(Debug)] -pub struct LinearTransformViaOriginal; +impl NiftiDataRescaler for u32 { + fn nifti_rescale(value: u32, slope: f32, intercept: f32) -> u32 { + if slope == 0. { + return value; + } + (value as f32 * slope + intercept) as u32 + } +} -impl LinearTransform for LinearTransformViaOriginal -where - T: 'static + Copy + DataElement + Mul + Add, -{ - fn linear_transform(value: T, slope: f32, intercept: f32) -> T { +impl NiftiDataRescaler for i32 { + fn nifti_rescale(value: i32, slope: f32, intercept: f32) -> i32 { + if slope == 0. { + return value; + } + (value as f32 * slope + intercept) as i32 + } +} + +impl NiftiDataRescaler for u64 { + fn nifti_rescale(value: u64, slope: f32, intercept: f32) -> u64 { + if slope == 0. { + return value; + } + (value as f64 * slope as f64 + intercept as f64) as u64 + } +} + +impl NiftiDataRescaler for i64 { + fn nifti_rescale(value: i64, slope: f32, intercept: f32) -> i64 { + if slope == 0. { + return value; + } + (value as f64 * slope as f64 + intercept as f64) as i64 + } +} + +impl NiftiDataRescaler for f32 { + fn nifti_rescale(value: f32, slope: f32, intercept: f32) -> f32 { if slope == 0. { return value; } - let slope = T::from_f32(slope); - let intercept = T::from_f32(intercept); value * slope + intercept } } +impl NiftiDataRescaler for f64 { + fn nifti_rescale(value: f64, slope: f32, intercept: f32) -> f64 { + if slope == 0. { + return value; + } + value * slope as f64 + intercept as f64 + } +} + +// Nifti 1.1 specifies that Complex valued data is scaled the same for both real and imaginary parts +impl NiftiDataRescaler for Complex32 { + fn nifti_rescale(value: Complex32, slope: f32, intercept: f32) -> Complex32 { + if slope == 0. { + return value; + } + Complex32::new(value.re * slope + intercept, value.im * slope + intercept) + } +} + +// Nifti 1.1 specifies that Complex valued data is scaled the same for both real and imaginary parts +impl NiftiDataRescaler for Complex64 { + fn nifti_rescale(value: Complex64, slope: f32, intercept: f32) -> Complex64 { + if slope == 0. { + return value; + } + Complex64::new( + value.re * slope as f64 + intercept as f64, + value.im * slope as f64 + intercept as f64, + ) + } +} + +// Nifti 1.1 specifies that RGB data must NOT be rescaled +impl NiftiDataRescaler for RGB8 { + fn nifti_rescale(value: RGB8, _slope: f32, _intercept: f32) -> RGB8 { + value + } +} + +// Nifti 1.1 specifies that RGB(A) data must NOT be rescaled +impl NiftiDataRescaler for RGBA8 { + fn nifti_rescale(value: RGBA8, _slope: f32, _intercept: f32) -> RGBA8 { + value + } +} + +/// A vessel to host the NiftiDataRescaler trait +#[derive(Debug)] +pub struct DataRescaler; + +impl NiftiDataRescaler for DataRescaler +where + T: 'static + Copy + DataElement + NiftiDataRescaler, +{ + fn nifti_rescale(value: T, slope: f32, intercept: f32) -> T { + T::nifti_rescale(value, slope, intercept) + } +} + /// Trait type for characterizing a NIfTI data element, implemented for /// primitive numeric types which are used by the crate to represent voxel /// values. -pub trait DataElement: - 'static + Sized + Copy + AsPrimitive + AsPrimitive + AsPrimitive -{ +pub trait DataElement: 'static + Sized + Copy { /// The `datatype` mapped to the type T const DATA_TYPE: NiftiType; - /// For defining how this element is linearly transformed to another. - type Transform: LinearTransform; + /// Implement rescaling for the given data type + type DataRescaler: NiftiDataRescaler; /// Read a single element from the given byte source. fn from_raw(src: R, endianness: E) -> Result @@ -115,34 +192,64 @@ pub trait DataElement: E: Endian; /// Create a single element by converting a scalar value. - fn from_u8(value: u8) -> Self; + fn from_u8(_value: u8) -> Self { + unimplemented!() + } /// Create a single element by converting a scalar value. - fn from_i8(value: i8) -> Self; + fn from_i8(_value: i8) -> Self { + unimplemented!() + } /// Create a single element by converting a scalar value. - fn from_u16(value: u16) -> Self; + fn from_u16(_value: u16) -> Self { + unimplemented!() + } /// Create a single element by converting a scalar value. - fn from_i16(value: i16) -> Self; + fn from_i16(_value: i16) -> Self { + unimplemented!() + } /// Create a single element by converting a scalar value. - fn from_u32(value: u32) -> Self; + fn from_u32(_value: u32) -> Self { + unimplemented!() + } /// Create a single element by converting a scalar value. - fn from_i32(value: i32) -> Self; + fn from_i32(_value: i32) -> Self { + unimplemented!() + } /// Create a single element by converting a scalar value. - fn from_u64(value: u64) -> Self; + fn from_u64(_value: u64) -> Self { + unimplemented!() + } /// Create a single element by converting a scalar value. - fn from_i64(value: i64) -> Self; + fn from_i64(_value: i64) -> Self { + unimplemented!() + } /// Create a single element by converting a scalar value. - fn from_f32(value: f32) -> Self; + fn from_f32(_value: f32) -> Self { + unimplemented!() + } /// Create a single element by converting a scalar value. - fn from_f64(value: f64) -> Self; + fn from_f64(_value: f64) -> Self { + unimplemented!() + } + + /// Create a single element by converting a complex value + fn from_complex32(_value: Complex32) -> Self { + unimplemented!() + } + + /// Create a single element by converting a complex value + fn from_complex64(_value: Complex64) -> Self { + unimplemented!() + } /// Transform the given data vector into a vector of data elements. fn from_raw_vec(vec: Vec, endianness: E) -> Result> @@ -156,6 +263,15 @@ pub trait DataElement: .map(|_| Self::from_raw(&mut cursor, endianness)) .collect() } + + /// Return a vector of data elements of the native type indicated in the Nifti file with runtime check + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian; } /// Mass-implement primitive conversions from scalar types @@ -203,15 +319,88 @@ macro_rules! fn_from_scalar { }; } +macro_rules! fn_cplx_from_scalar { + ($typ: ty) => { + fn from_u8(value: u8) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + + fn from_i8(value: i8) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + + fn from_u16(value: u16) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + + fn from_i16(value: i16) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + + fn from_u32(value: u32) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + + fn from_i32(value: i32) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + + fn from_u64(value: u64) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + + fn from_i64(value: i64) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + + fn from_f32(value: f32) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + + fn from_f64(value: f64) -> Self { + Complex::<$typ>::new(value as $typ, 0.) + } + }; +} + +macro_rules! fn_from_complex { + ($typ: ty) => { + fn from_complex32(value: Complex32) -> Self { + Complex::<$typ>::new(value.re as $typ, value.im as $typ) + } + + fn from_complex64(value: Complex64) -> Self { + Complex::<$typ>::new(value.re as $typ, value.im as $typ) + } + }; +} + impl DataElement for u8 { const DATA_TYPE: NiftiType = NiftiType::Uint8; - type Transform = LinearTransformViaF32; + type DataRescaler = DataRescaler; + fn from_raw_vec(vec: Vec, _: E) -> Result> where E: Endian, { Ok(vec) } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Uint8 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "u8")) + } + } + fn from_raw(src: R, _: E) -> Result where R: Read, @@ -225,13 +414,29 @@ impl DataElement for u8 { impl DataElement for i8 { const DATA_TYPE: NiftiType = NiftiType::Int8; - type Transform = LinearTransformViaF32; + type DataRescaler = DataRescaler; fn from_raw_vec(vec: Vec, _: E) -> Result> where E: Endian, { - Ok(transmute_vec(vec).unwrap()) + Ok(try_cast_vec(vec).unwrap()) + } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Int8 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "i8")) + } } + fn from_raw(src: R, _: E) -> Result where R: Read, @@ -245,13 +450,29 @@ impl DataElement for i8 { impl DataElement for u16 { const DATA_TYPE: NiftiType = NiftiType::Uint16; - type Transform = LinearTransformViaF32; + type DataRescaler = DataRescaler; fn from_raw_vec(vec: Vec, e: E) -> Result> where E: Endian, { Ok(convert_bytes_to(vec, e)) } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Uint16 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "u16")) + } + } + fn from_raw(src: R, e: E) -> Result where R: Read, @@ -265,13 +486,29 @@ impl DataElement for u16 { impl DataElement for i16 { const DATA_TYPE: NiftiType = NiftiType::Int16; - type Transform = LinearTransformViaF32; + type DataRescaler = DataRescaler; fn from_raw_vec(vec: Vec, e: E) -> Result> where E: Endian, { Ok(convert_bytes_to(vec, e)) } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Int16 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "i16")) + } + } + fn from_raw(src: R, e: E) -> Result where R: Read, @@ -285,13 +522,28 @@ impl DataElement for i16 { impl DataElement for u32 { const DATA_TYPE: NiftiType = NiftiType::Uint32; - type Transform = LinearTransformViaF32; + type DataRescaler = DataRescaler; fn from_raw_vec(vec: Vec, e: E) -> Result> where E: Endian, { Ok(convert_bytes_to(vec, e)) } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Uint32 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "u32")) + } + } fn from_raw(src: R, e: E) -> Result where R: Read, @@ -305,13 +557,29 @@ impl DataElement for u32 { impl DataElement for i32 { const DATA_TYPE: NiftiType = NiftiType::Int32; - type Transform = LinearTransformViaF32; + type DataRescaler = DataRescaler; fn from_raw_vec(vec: Vec, e: E) -> Result> where E: Endian, { Ok(convert_bytes_to(vec, e)) } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Int32 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "i32")) + } + } + fn from_raw(src: R, e: E) -> Result where R: Read, @@ -325,13 +593,29 @@ impl DataElement for i32 { impl DataElement for u64 { const DATA_TYPE: NiftiType = NiftiType::Uint64; - type Transform = LinearTransformViaF64; + type DataRescaler = DataRescaler; fn from_raw_vec(vec: Vec, e: E) -> Result> where E: Endian, { Ok(convert_bytes_to(vec, e)) } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Uint64 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "u64")) + } + } + fn from_raw(src: R, e: E) -> Result where R: Read, @@ -345,13 +629,30 @@ impl DataElement for u64 { impl DataElement for i64 { const DATA_TYPE: NiftiType = NiftiType::Int64; - type Transform = LinearTransformViaF64; + type DataRescaler = DataRescaler; + fn from_raw_vec(vec: Vec, e: E) -> Result> where E: Endian, { Ok(convert_bytes_to(vec, e)) } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Int64 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "i64")) + } + } + fn from_raw(src: R, e: E) -> Result where R: Read, @@ -365,13 +666,30 @@ impl DataElement for i64 { impl DataElement for f32 { const DATA_TYPE: NiftiType = NiftiType::Float32; - type Transform = LinearTransformViaOriginal; + type DataRescaler = DataRescaler; + fn from_raw_vec(vec: Vec, e: E) -> Result> where E: Endian, { Ok(convert_bytes_to(vec, e)) } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Float32 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "f32")) + } + } + fn from_raw(src: R, e: E) -> Result where R: Read, @@ -385,13 +703,30 @@ impl DataElement for f32 { impl DataElement for f64 { const DATA_TYPE: NiftiType = NiftiType::Float64; - type Transform = LinearTransformViaOriginal; + type DataRescaler = DataRescaler; + fn from_raw_vec(vec: Vec, e: E) -> Result> where E: Endian, { Ok(convert_bytes_to(vec, e)) } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Float64 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "f64")) + } + } + fn from_raw(src: R, e: E) -> Result where R: Read, @@ -402,3 +737,176 @@ impl DataElement for f64 { fn_from_scalar!(f64); } + +impl DataElement for Complex32 { + const DATA_TYPE: NiftiType = NiftiType::Complex64; + type DataRescaler = DataRescaler; + + fn from_raw_vec(vec: Vec, e: E) -> Result> + where + E: Endian, + { + Ok(convert_bytes_to::<[f32; 2], _>(vec, e) + .into_iter() + .map(|x| Complex32::new(x[0], x[1])) + .collect()) + } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Complex64 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "Complex32")) + } + } + + fn from_raw(mut src: R, e: E) -> Result + where + R: Read, + E: Endian, + { + let real = e.read_f32(&mut src)?; + let imag = e.read_f32(&mut src)?; + + Ok(Complex32::new(real, imag)) + } + + fn_cplx_from_scalar!(f32); + fn_from_complex!(f32); +} + +impl DataElement for Complex64 { + const DATA_TYPE: NiftiType = NiftiType::Complex128; + type DataRescaler = DataRescaler; + + fn from_raw_vec(vec: Vec, e: E) -> Result> + where + E: Endian, + { + Ok(convert_bytes_to::<[f64; 2], _>(vec, e) + .into_iter() + .map(|x| Complex64::new(x[0], x[1])) + .collect()) + } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Complex128 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "Complex64")) + } + } + + fn from_raw(mut src: R, e: E) -> Result + where + R: Read, + E: Endian, + { + let real = e.read_f64(&mut src)?; + let imag = e.read_f64(&mut src)?; + + Ok(Complex64::new(real, imag)) + } + + fn_cplx_from_scalar!(f64); + fn_from_complex!(f64); +} + +impl DataElement for RGB8 { + const DATA_TYPE: NiftiType = NiftiType::Rgb24; + type DataRescaler = DataRescaler; + + fn from_raw_vec(vec: Vec, e: E) -> Result> + where + E: Endian, + { + Ok(convert_bytes_to::<[u8; 3], _>(vec, e) + .into_iter() + .map(|x| RGB8::new(x[0], x[1], x[2])) + .collect()) + } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Rgb24 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "RGB8")) + } + } + + fn from_raw(mut src: R, _: E) -> Result + where + R: Read, + E: Endian, + { + let r = ByteOrdered::native(&mut src).read_u8()?; + let g = ByteOrdered::native(&mut src).read_u8()?; + let b = ByteOrdered::native(&mut src).read_u8()?; + + Ok(RGB8::new(r, g, b)) + } +} + +impl DataElement for RGBA8 { + const DATA_TYPE: NiftiType = NiftiType::Rgba32; + type DataRescaler = DataRescaler; + + fn from_raw_vec(vec: Vec, e: E) -> Result> + where + E: Endian, + { + Ok(convert_bytes_to::<[u8; 4], _>(vec, e) + .into_iter() + .map(|x| RGBA8::new(x[0], x[1], x[2], x[3])) + .collect()) + } + + fn from_raw_vec_validated( + vec: Vec, + endianness: E, + datatype: NiftiType, + ) -> Result> + where + E: Endian, + { + if datatype == NiftiType::Rgba32 { + Self::from_raw_vec(vec, endianness) + } else { + Err(NiftiError::InvalidTypeConversion(datatype, "RGBA8")) + } + } + + fn from_raw(mut src: R, _: E) -> Result + where + R: Read, + E: Endian, + { + let r = ByteOrdered::native(&mut src).read_u8()?; + let g = ByteOrdered::native(&mut src).read_u8()?; + let b = ByteOrdered::native(&mut src).read_u8()?; + let a = ByteOrdered::native(&mut src).read_u8()?; + + Ok(RGBA8::new(r, g, b, a)) + } +} diff --git a/src/volume/inmem.rs b/src/volume/inmem.rs index a40efbe..f807489 100644 --- a/src/volume/inmem.rs +++ b/src/volume/inmem.rs @@ -6,7 +6,7 @@ use crate::error::{NiftiError, Result}; use crate::header::NiftiHeader; use crate::typedef::NiftiType; use crate::util::{nb_bytes_for_data, nb_bytes_for_dim_datatype}; -use crate::volume::element::DataElement; +use crate::volume::element::{DataElement, NiftiDataRescaler}; use crate::volume::{FromSource, FromSourceOptions, NiftiVolume, RandomAccessNiftiVolume}; use byteordered::Endianness; use flate2::bufread::GzDecoder; @@ -28,8 +28,6 @@ macro_rules! fn_convert_and_cast { where O: DataElement, { - use crate::volume::element::LinearTransform; - let dim: Vec<_> = self.dim().iter().map(|d| *d as Ix).collect(); // cast the raw data buffer to the DataElement @@ -38,7 +36,7 @@ macro_rules! fn_convert_and_cast { // cast elements to the requested output type let mut data: Vec = data.into_iter().map($converter).collect(); // apply slope and inter before creating the final ndarray - ::Transform::linear_transform_many_inline( + ::DataRescaler::nifti_rescale_many_inline( &mut data, self.scl_slope, self.scl_inter, @@ -180,9 +178,18 @@ impl InMemNiftiVolume { &mut self.raw_data } + /// Retrieve the raw data, typed as specified in the volume's header, consuming the volume + pub fn into_nifti_typed_data(self) -> Result> + where + T: DataElement, + { + T::from_raw_vec_validated(self.raw_data, self.endianness, self.datatype) + } + fn get_prim(&self, coords: &[u16]) -> Result where T: DataElement, + T: NiftiDataRescaler, T: Num, T: Copy, T: Mul, @@ -204,6 +211,29 @@ impl InMemNiftiVolume { fn_convert_and_cast!(convert_and_cast_i64, i64, DataElement::from_i64); fn_convert_and_cast!(convert_and_cast_f32, f32, DataElement::from_f32); fn_convert_and_cast!(convert_and_cast_f64, f64, DataElement::from_f64); + + // no casting here + #[cfg(feature = "ndarray_volumes")] + fn no_cast_convert_to_ndarray(self) -> Result> + where + T: DataElement, + { + let dim: Vec<_> = self.dim().iter().map(|d| *d as Ix).collect(); + + let mut data: Vec<_> = ::from_raw_vec(self.raw_data, self.endianness)?; + // corresponding to the declared datatype + if self.datatype != NiftiType::Rgb24 + && self.datatype != NiftiType::Rgba32 + && self.scl_slope != 0.0 + { + ::DataRescaler::nifti_rescale_many_inline( + &mut data, + self.scl_slope, + self.scl_inter, + ); + } + Ok(Array::from_shape_vec(IxDyn(&dim).f(), data).expect("Inconsistent raw data size")) + } } impl FromSourceOptions for InMemNiftiVolume { @@ -238,11 +268,11 @@ impl IntoNdArray for InMemNiftiVolume { NiftiType::Float32 => self.convert_and_cast_f32::(), NiftiType::Float64 => self.convert_and_cast_f64::(), //NiftiType::Float128 => {} - //NiftiType::Complex64 => {} - //NiftiType::Complex128 => {} + NiftiType::Complex64 => self.no_cast_convert_to_ndarray::(), + NiftiType::Complex128 => self.no_cast_convert_to_ndarray::(), //NiftiType::Complex256 => {} - //NiftiType::Rgb24 => {} - //NiftiType::Rgba32 => {} + NiftiType::Rgb24 => self.no_cast_convert_to_ndarray::(), + NiftiType::Rgba32 => self.no_cast_convert_to_ndarray::(), _ => Err(NiftiError::UnsupportedDataType(self.datatype)), } } diff --git a/src/writer.rs b/src/writer.rs index 6f4bc31..336b68d 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -2,13 +2,14 @@ use std::fs::File; use std::io::{BufWriter, Write}; +use std::mem::size_of; use std::path::{Path, PathBuf}; +use bytemuck::{cast_slice, Pod}; use byteordered::{ByteOrdered, Endian}; use flate2::write::GzEncoder; use flate2::Compression; use ndarray::{ArrayBase, Axis, Data, Dimension, RemoveAxis}; -use safe_transmute::{transmute_to_bytes, TriviallyTransmutable}; use crate::{ header::{MAGIC_CODE_NI1, MAGIC_CODE_NIP1}, @@ -137,15 +138,23 @@ impl<'a> WriterOptions<'a> { self } - /// Write a nifti file (.nii or .nii.gz). - pub fn write_nifti(&self, data: &ArrayBase) -> Result<()> + /// Write a nifti file (.nii or .nii.gz) from an NdArray of any Pod type + pub fn write_nifti_with_type( + &self, + data: &ArrayBase, + datatype: NiftiType, + ) -> Result<()> where S: Data, - A: DataElement, - A: TriviallyTransmutable, + A: Pod, D: Dimension + RemoveAxis, { - let header = self.prepare_header(data, A::DATA_TYPE)?; + // do a basic size check + if size_of::() != datatype.size_of() { + return Err(crate::error::NiftiError::UnsupportedDataType(datatype)); + } + + let header = self.prepare_header(data, datatype)?; let (header_path, data_path) = self.output_paths(); // Need the transpose for fortran ordering used in nifti file format. @@ -200,65 +209,23 @@ impl<'a> WriterOptions<'a> { Ok(()) } + /// Write a nifti file (.nii or .nii.gz) from an NdArray of DataElements + pub fn write_nifti(&self, data: &ArrayBase) -> Result<()> + where + S: Data, + A: DataElement + Pod, + D: Dimension + RemoveAxis, + { + self.write_nifti_with_type(data, A::DATA_TYPE) + } + /// Write a RGB nifti file (.nii or .nii.gz). pub fn write_rgb_nifti(&self, data: &ArrayBase) -> Result<()> where S: Data, D: Dimension + RemoveAxis, { - let header = self.prepare_header(data, NiftiType::Rgb24)?; - let (header_path, data_path) = self.output_paths(); - - // Need the transpose for fortran used in nifti file format. - let data = data.t(); - - let header_file = File::create(header_path)?; - if header.vox_offset > 0.0 { - if let Some(compression_level) = self.compression { - let mut writer = ByteOrdered::runtime( - GzEncoder::new(header_file, compression_level), - header.endianness, - ); - write_header(writer.as_mut(), &header)?; - write_extensions(writer.as_mut(), self.extension_sequence.as_ref())?; - write_data::<_, u8, _, _, _, _>(writer.as_mut(), data)?; - let _ = writer.into_inner().finish()?; - } else { - let mut writer = - ByteOrdered::runtime(BufWriter::new(header_file), header.endianness); - write_header(writer.as_mut(), &header)?; - write_extensions(writer.as_mut(), self.extension_sequence.as_ref())?; - write_data::<_, u8, _, _, _, _>(writer, data)?; - } - } else { - let data_file = File::create(data_path)?; - if let Some(compression_level) = self.compression { - let mut writer = ByteOrdered::runtime( - GzEncoder::new(header_file, compression_level), - header.endianness, - ); - write_header(writer.as_mut(), &header)?; - write_extensions(writer.as_mut(), self.extension_sequence.as_ref())?; - let _ = writer.into_inner().finish()?; - - let mut writer = ByteOrdered::runtime( - GzEncoder::new(data_file, compression_level), - header.endianness, - ); - write_data::<_, u8, _, _, _, _>(writer.as_mut(), data)?; - let _ = writer.into_inner().finish()?; - } else { - let mut header_writer = - ByteOrdered::runtime(BufWriter::new(header_file), header.endianness); - write_header(header_writer.as_mut(), &header)?; - write_extensions(header_writer.as_mut(), self.extension_sequence.as_ref())?; - let data_writer = - ByteOrdered::runtime(BufWriter::new(data_file), header.endianness); - write_data::<_, u8, _, _, _, _>(data_writer, data)?; - } - } - - Ok(()) + self.write_nifti_with_type(data, NiftiType::Rgb24) } fn prepare_header( @@ -423,7 +390,7 @@ where fn write_data(mut writer: ByteOrdered, data: ArrayBase) -> Result<()> where S: Data, - A: TriviallyTransmutable, + A: Pod, D: Dimension + RemoveAxis, W: Write, E: Endian + Copy, @@ -453,7 +420,7 @@ fn write_slice( ) -> Result<()> where S: Data, - A: Clone + TriviallyTransmutable, + A: Clone + Pod, D: Dimension, W: Write, E: Endian, @@ -461,7 +428,7 @@ where let len = data.len(); let arr_data = data.into_shape(len).unwrap(); let slice = arr_data.as_slice().unwrap(); - let bytes = transmute_to_bytes(slice); + let bytes = cast_slice(slice); let (writer, endianness) = writer.into_parts(); let bytes = adapt_bytes::(bytes, endianness); writer.write_all(&bytes)?; diff --git a/tests/volume.rs b/tests/volume.rs index a046a2b..70f9df1 100644 --- a/tests/volume.rs +++ b/tests/volume.rs @@ -10,8 +10,6 @@ extern crate approx; extern crate ndarray; #[cfg(feature = "ndarray_volumes")] extern crate num_traits; -#[cfg(feature = "ndarray_volumes")] -extern crate safe_transmute; use nifti::{InMemNiftiVolume, NiftiVolume, RandomAccessNiftiVolume}; @@ -52,6 +50,8 @@ mod ndarray_volumes { DataElement, InMemNiftiVolume, IntoNdArray, NiftiObject, NiftiType, NiftiVolume, ReaderOptions, ReaderStreamedOptions, }; + use num_complex::{Complex32, Complex64}; + use rgb::{RGB8, RGBA8}; use std::fmt; use std::ops::{Add, Mul}; @@ -273,4 +273,147 @@ mod ndarray_volumes { assert_eq!(T::from_u64(idx as u64), *val); } } + + #[test] + fn test_read_rgb8() { + const FILE_NAME: &str = "resources/rgb/4D.nii"; + let volume = ReaderOptions::new() + .read_file(FILE_NAME) + .unwrap() + .into_volume(); + assert_eq!(volume.data_type(), NiftiType::Rgb24); + assert_eq!(volume.dim(), [3, 3, 3, 2].as_ref()); + + let v: Vec = volume.into_nifti_typed_data().unwrap(); + + assert_eq!(v.len(), 54); + } + + #[test] + fn test_read_rgb8_ndarray() { + const FILE_NAME: &str = "resources/rgb/4D.nii"; + let volume = ReaderOptions::new() + .read_file(FILE_NAME) + .unwrap() + .into_volume(); + assert_eq!(volume.data_type(), NiftiType::Rgb24); + assert_eq!(volume.dim(), [3, 3, 3, 2].as_ref()); + let volume = volume.into_ndarray::().unwrap(); + + assert_eq!(volume.shape(), [3, 3, 3, 2].as_ref()); + + assert_eq!(volume[[0, 0, 0, 0]], RGB8::new(55, 55, 0)); + assert_eq!(volume[[0, 0, 1, 0]], RGB8::new(55, 0, 55)); + assert_eq!(volume[[0, 1, 0, 0]], RGB8::new(0, 55, 55)); + assert_eq!(volume[[0, 0, 0, 1]], RGB8::new(55, 55, 0)); + assert_eq!(volume[[0, 1, 0, 1]], RGB8::new(55, 0, 55)); + assert_eq!(volume[[1, 0, 0, 1]], RGB8::new(0, 55, 55)); + } + + #[test] + fn test_read_rgba8() { + const FILE_NAME: &str = "resources/rgba/4D.nii"; + let volume = ReaderOptions::new() + .read_file(FILE_NAME) + .unwrap() + .into_volume(); + assert_eq!(volume.data_type(), NiftiType::Rgba32); + assert_eq!(volume.dim(), [3, 3, 3, 2].as_ref()); + + let v: Vec = volume.into_nifti_typed_data().unwrap(); + + assert_eq!(v.len(), 54); + } + + #[test] + fn test_read_rgba8_ndarray() { + const FILE_NAME: &str = "resources/rgba/4D.nii"; + let volume = ReaderOptions::new() + .read_file(FILE_NAME) + .unwrap() + .into_volume(); + assert_eq!(volume.data_type(), NiftiType::Rgba32); + assert_eq!(volume.dim(), [3, 3, 3, 2].as_ref()); + let volume = volume.into_ndarray::().unwrap(); + + assert_eq!(volume.shape(), [3, 3, 3, 2].as_ref()); + + assert_eq!(volume[[0, 0, 0, 0]], RGBA8::new(55, 55, 0, 0)); + assert_eq!(volume[[0, 0, 1, 0]], RGBA8::new(55, 0, 55, 0)); + assert_eq!(volume[[0, 1, 0, 0]], RGBA8::new(0, 55, 55, 0)); + assert_eq!(volume[[0, 0, 0, 1]], RGBA8::new(55, 55, 0, 0)); + assert_eq!(volume[[0, 1, 0, 1]], RGBA8::new(55, 0, 55, 0)); + assert_eq!(volume[[1, 0, 0, 1]], RGBA8::new(0, 55, 55, 0)); + } + + #[test] + fn test_read_complex32() { + const FILE_NAME: &str = "resources/complex/complex32.nii"; + let volume = ReaderOptions::new() + .read_file(FILE_NAME) + .unwrap() + .into_volume(); + assert_eq!(volume.data_type(), NiftiType::Complex64); + assert_eq!(volume.dim(), [3, 3].as_ref()); + let v: Vec = volume.into_nifti_typed_data().unwrap(); + + assert_eq!(v.len(), 9); + // this is a column-major storage!!! + assert_eq!(v[0], Complex32::new(1.0, 1.0)); + assert_eq!(v[1], Complex32::new(3.0, 3.0)); + assert_eq!(v[3], Complex32::new(2.0, 2.0)); + } + + #[test] + fn test_read_complex32_ndarray() { + const FILE_NAME: &str = "resources/complex/complex32.nii"; + let volume = ReaderOptions::new() + .read_file(FILE_NAME) + .unwrap() + .into_volume(); + assert_eq!(volume.data_type(), NiftiType::Complex64); + assert_eq!(volume.dim(), [3, 3].as_ref()); + let volume = volume.into_ndarray::().unwrap(); + + assert_eq!(volume.shape(), [3, 3].as_ref()); + + assert_eq!(volume[[0, 0]], Complex32::new(1.0, 1.0)); + assert_eq!(volume[[0, 1]], Complex32::new(2.0, 2.0)); + assert_eq!(volume[[1, 0]], Complex32::new(3.0, 3.0)); + } + + #[test] + fn test_read_complex64() { + const FILE_NAME: &str = "resources/complex/complex64.nii"; + let volume = ReaderOptions::new() + .read_file(FILE_NAME) + .unwrap() + .into_volume(); + assert_eq!(volume.data_type(), NiftiType::Complex128); + assert_eq!(volume.dim(), [3, 3].as_ref()); + let v: Vec = volume.into_nifti_typed_data().unwrap(); + + assert_eq!(v.len(), 9); + // this is a column-major storage!!! + assert_eq!(v[0], Complex64::new(1.0, 1.0)); + assert_eq!(v[1], Complex64::new(3.0, 3.0)); + assert_eq!(v[3], Complex64::new(2.0, 2.0)); + } + #[test] + fn test_read_complex64_ndarray() { + const FILE_NAME: &str = "resources/complex/complex64.nii"; + let volume = ReaderOptions::new() + .read_file(FILE_NAME) + .unwrap() + .into_volume(); + assert_eq!(volume.data_type(), NiftiType::Complex128); + assert_eq!(volume.dim(), [3, 3].as_ref()); + let volume = volume.into_ndarray::().unwrap(); + + assert_eq!(volume.shape(), [3, 3].as_ref()); + + assert_eq!(volume[[0, 0]], Complex64::new(1.0, 1.0)); + assert_eq!(volume[[0, 1]], Complex64::new(2.0, 2.0)); + assert_eq!(volume[[1, 0]], Complex64::new(3.0, 3.0)); + } } diff --git a/tests/writer.rs b/tests/writer.rs index b0769c2..6978bb8 100644 --- a/tests/writer.rs +++ b/tests/writer.rs @@ -21,6 +21,7 @@ mod tests { use ndarray::{ s, Array, Array1, Array2, Array3, Array4, Array5, Axis, Dimension, Ix2, IxDyn, ShapeBuilder, }; + use rgb::{RGB8, RGBA8}; use tempfile::tempdir; use nifti::{ @@ -346,7 +347,7 @@ mod tests { .write_rgb_nifti(&data) .unwrap(); - // Until we are able to read RGB images, we simply compare the bytes of the newly created + // Simply compare the bytes of the newly created // image to the bytes of the prepared 3D RGB image in ressources/rgb/. However, we need to // set the bytes of vox_offset to 0.0 and of magic to MAGIC_CODE_NI1. The data bytes should // be identical though. @@ -375,7 +376,7 @@ mod tests { .write_rgb_nifti(&data) .unwrap(); - // Until we are able to read RGB images, we simply compare the bytes of the newly created + // Simply compare the bytes of the newly created // image to the bytes of the prepared 3D RGB image in ressources/rgb/. assert_eq!( fs::read(path).unwrap(), @@ -400,7 +401,7 @@ mod tests { .write_rgb_nifti(&data) .unwrap(); - // Until we are able to read RGB images, we simply compare the bytes of the newly created + // Simply compare the bytes of the newly created // image to the bytes of the prepared 4D RGB image in ressources/rgb/. assert_eq!( fs::read(path).unwrap(), @@ -408,6 +409,156 @@ mod tests { ); } + #[test] + fn write_4d_rgb_direct() { + let mut data = Array::from_elem((3, 3, 3, 2), [0u8, 0u8, 0u8]); + data[(0, 0, 0, 0)] = [55, 55, 0]; + data[(0, 0, 1, 0)] = [55, 0, 55]; + data[(0, 1, 0, 0)] = [0, 55, 55]; + data[(0, 0, 0, 1)] = [55, 55, 0]; + data[(0, 1, 0, 1)] = [55, 0, 55]; + data[(1, 0, 0, 1)] = [0, 55, 55]; + + let path = get_temporary_path("rgb.nii"); + let header = rgb_header_gt(); + WriterOptions::new(&path) + .reference_header(&header) + .write_nifti_with_type(&data, NiftiType::Rgb24) + .unwrap(); + + // Simply compare the bytes of the newly created + // image to the bytes of the prepared 4D RGB image in ressources/rgb/. + assert_eq!( + fs::read(path).unwrap(), + fs::read("resources/rgb/4D.nii").unwrap() + ); + } + + #[test] + fn write_4d_rgba_direct() { + let mut data = Array::from_elem((3, 3, 3, 2), [0u8, 0u8, 0u8, 0u8]); + data[(0, 0, 0, 0)] = [55, 55, 0, 0]; + data[(0, 0, 1, 0)] = [55, 0, 55, 0]; + data[(0, 1, 0, 0)] = [0, 55, 55, 0]; + data[(0, 0, 0, 1)] = [55, 55, 0, 0]; + data[(0, 1, 0, 1)] = [55, 0, 55, 0]; + data[(1, 0, 0, 1)] = [0, 55, 55, 0]; + + let path = get_temporary_path("rgb.nii"); + let header = rgb_header_gt(); + WriterOptions::new(&path) + .reference_header(&header) + .write_nifti_with_type(&data, NiftiType::Rgba32) + .unwrap(); + + // Simply compare the bytes of the newly created + // image to the bytes of the prepared 4D RGBA image in ressources/rgba/. + // Verify the binary identity to the nibabel generated file + assert_eq!( + fs::read(path).unwrap(), + fs::read("resources/rgba/4D.nii").unwrap() + ); + } + + #[test] + fn write_4d_rgb_rgbtype() { + let mut data = Array::from_elem((3, 3, 3, 2), RGB8::new(0u8, 0u8, 0u8)); + + data[(0, 0, 0, 0)] = RGB8::new(55, 55, 0); + data[(0, 0, 1, 0)] = RGB8::new(55, 0, 55); + data[(0, 1, 0, 0)] = RGB8::new(0, 55, 55); + data[(0, 0, 0, 1)] = RGB8::new(55, 55, 0); + data[(0, 1, 0, 1)] = RGB8::new(55, 0, 55); + data[(1, 0, 0, 1)] = RGB8::new(0, 55, 55); + + let path = get_temporary_path("rgb.nii"); + let header = rgb_header_gt(); + WriterOptions::new(&path) + .reference_header(&header) + .write_nifti(&data) + .unwrap(); + + // Simply compare the bytes of the newly created + // image to the bytes of the prepared 4D RGB image in ressources/rgb/. + assert_eq!( + fs::read(path).unwrap(), + fs::read("resources/rgb/4D.nii").unwrap() + ); + } + + #[test] + fn write_4d_rgb_rgbatype() { + let mut data = Array::from_elem((3, 3, 3, 2), RGBA8::new(0u8, 0u8, 0u8, 0u8)); + + data[(0, 0, 0, 0)] = RGBA8::new(55, 55, 0, 0); + data[(0, 0, 1, 0)] = RGBA8::new(55, 0, 55, 0); + data[(0, 1, 0, 0)] = RGBA8::new(0, 55, 55, 0); + data[(0, 0, 0, 1)] = RGBA8::new(55, 55, 0, 0); + data[(0, 1, 0, 1)] = RGBA8::new(55, 0, 55, 0); + data[(1, 0, 0, 1)] = RGBA8::new(0, 55, 55, 0); + + let path = get_temporary_path("rgb.nii"); + let header = rgb_header_gt(); + WriterOptions::new(&path) + .reference_header(&header) + .write_nifti(&data) + .unwrap(); + + // Simply compare the bytes of the newly created + // image to the bytes of the prepared 4D RGBA image in ressources/rgba/. + // Verify the binary identity to the nibabel generated file + assert_eq!( + fs::read(path).unwrap(), + fs::read("resources/rgba/4D.nii").unwrap() + ); + } + + #[test] + fn write_2d_complex32() { + let mut data = Array::from_elem((3, 3), num_complex::Complex32::new(0.0, 0.0)); + + data[(0, 0)] = num_complex::Complex32::new(1.0, 1.0); + data[(0, 1)] = num_complex::Complex32::new(2.0, 2.0); + data[(1, 0)] = num_complex::Complex32::new(3.0, 3.0); + + let path = get_temporary_path("complex32.nii"); + let header = + generate_nifti_header([2, 3, 3, 1, 1, 1, 1, 1], 1.0, 0.0, NiftiType::Complex64); + WriterOptions::new(&path) + .reference_header(&header) + .write_nifti(&data) + .unwrap(); + + // Verify the binary identity to the nibabel generated file + assert_eq!( + fs::read(path).unwrap(), + fs::read("resources/complex/complex32.nii").unwrap() + ); + } + + #[test] + fn write_2d_complex64() { + let mut data = Array::from_elem((3, 3), num_complex::Complex64::new(0.0, 0.0)); + + data[(0, 0)] = num_complex::Complex64::new(1.0, 1.0); + data[(0, 1)] = num_complex::Complex64::new(2.0, 2.0); + data[(1, 0)] = num_complex::Complex64::new(3.0, 3.0); + + let path = get_temporary_path("complex32.nii"); + let header = + generate_nifti_header([2, 3, 3, 1, 1, 1, 1, 1], 1.0, 0.0, NiftiType::Complex128); + WriterOptions::new(&path) + .reference_header(&header) + .write_nifti(&data) + .unwrap(); + + // Verify the binary identity to the nibabel generated file + assert_eq!( + fs::read(path).unwrap(), + fs::read("resources/complex/complex64.nii").unwrap() + ); + } + #[test] fn write_extended_header() { let data: Array2 = Array2::zeros((8, 8));