diff --git a/src/error.rs b/src/error.rs index 0fdbc22..7ad5f00 100644 --- a/src/error.rs +++ b/src/error.rs @@ -57,9 +57,10 @@ quick_error! { cause(err) description(err.description()) } - /// Raw data and buffer length are incompatible - IncompatibleLength { + /// Raw data buffer length and volume dimensions are incompatible + IncompatibleLength(got: usize, expected: usize) { description("The buffer length and the header dimensions are incompatible.") + display("The buffer length ({}) and header dimensions ({} elements) are incompatible", got, expected) } /// Description length must be lower than or equal to 80 bytes IncorrectDescriptionLength(len: usize) { diff --git a/src/extension.rs b/src/extension.rs index f03fcfb..88b9f3e 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -14,7 +14,13 @@ pub struct Extender([u8; 4]); impl Extender { /// Fetch the extender code from the given source, while expecting it to exist. - pub fn from_stream(mut source: S) -> Result { + #[deprecated(since = "0.8.0", note = "use `from_reader` instead")] + pub fn from_stream(source: S) -> Result { + Self::from_reader(source) + } + + /// Fetch the extender code from the given source, while expecting it to exist. + pub fn from_reader(mut source: S) -> Result { let mut extension = [0u8; 4]; source.read_exact(&mut extension)?; Ok(extension.into()) @@ -24,7 +30,16 @@ impl Extender { /// being possible to not be available. /// Returns `None` if the source reaches EoF prematurely. /// Any other I/O error is delegated to a `NiftiError`. - pub fn from_stream_optional(mut source: S) -> Result> { + #[deprecated(since = "0.8.0", note = "use `from_reader_optional` instead")] + pub fn from_stream_optional(source: S) -> Result> { + Self::from_reader_optional(source) + } + + /// Fetch the extender code from the given source, while + /// being possible to not be available. + /// Returns `None` if the source reaches EoF prematurely. + /// Any other I/O error is delegated to a `NiftiError`. + pub fn from_reader_optional(mut source: S) -> Result> { let mut extension = [0u8; 4]; match source.read_exact(&mut extension) { Ok(()) => Ok(Some(extension.into())), @@ -131,7 +146,21 @@ impl<'a> IntoIterator for &'a ExtensionSequence { impl ExtensionSequence { /// Read a sequence of extensions from a source, up until `len` bytes. + #[deprecated(since = "0.8.0", note = "use `from_reader` instead")] pub fn from_stream( + extender: Extender, + source: ByteOrdered, + len: usize, + ) -> Result + where + S: Read, + E: Endian, + { + Self::from_reader(extender, source, len) + } + + /// Read a sequence of extensions from a source, up until `len` bytes. + pub fn from_reader( extender: Extender, mut source: ByteOrdered, len: usize, diff --git a/src/header.rs b/src/header.rs index b63a62e..8359f72 100644 --- a/src/header.rs +++ b/src/header.rs @@ -18,7 +18,7 @@ use std::io::{BufReader, Read}; use std::ops::Deref; use std::path::Path; use typedef::*; -use util::is_gz_file; +use util::{is_gz_file, validate_dim, validate_dimensionality}; /// Magic code for NIFTI-1 header files (extention ".hdr[.gz]"). pub const MAGIC_CODE_NI1: &'static [u8; 4] = b"ni1\0"; @@ -226,16 +226,27 @@ impl NiftiHeader { let gz = is_gz_file(&path); let file = BufReader::new(File::open(path)?); if gz { - NiftiHeader::from_stream(GzDecoder::new(file)) + NiftiHeader::from_reader(GzDecoder::new(file)) } else { - NiftiHeader::from_stream(file) + NiftiHeader::from_reader(file) } } /// Read a NIfTI-1 header, along with its byte order, from the given byte stream. /// It is assumed that the input is currently at the start of the /// NIFTI header. + #[deprecated(since = "0.8.0", note = "use `from_reader` instead")] pub fn from_stream(input: S) -> Result { + Self::from_reader(input) + } + + /// Read a NIfTI-1 header, along with its byte order, from the given byte stream. + /// It is assumed that the input is currently at the start of the + /// NIFTI header. + pub fn from_reader(input: S) -> Result + where + S: Read, + { parse_header_1(input) } @@ -248,12 +259,7 @@ impl NiftiHeader { /// `NiftiError::InconsistentDim` if `dim[0]` does not represent a valid /// dimensionality, or any of the real dimensions are zero. pub fn dim(&self) -> Result<&[u16]> { - let ndim = self.dimensionality()?; - let o = &self.dim[1..ndim + 1]; - if let Some(i) = o.into_iter().position(|&x| x == 0) { - return Err(NiftiError::InconsistentDim(i as u8, self.dim[i])); - } - Ok(o) + validate_dim(&self.dim) } /// Retrieve and validate the number of dimensions of the volume. This is @@ -263,11 +269,8 @@ impl NiftiHeader { /// /// `NiftiError::` if `dim[0]` does not represent a valid dimensionality /// (it must be positive and not higher than 7). - pub fn dimensionality(&self) -> Result { - if self.dim[0] == 0 || self.dim[0] > 7 { - return Err(NiftiError::InconsistentDim(0, self.dim[0])); - } - Ok(usize::from(self.dim[0])) + pub fn dimensionality(&self) -> Result { + validate_dimensionality(&self.dim) } /// Get the data type as a validated enum. diff --git a/src/lib.rs b/src/lib.rs index 05c612f..18c306d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,22 @@ //! # } //! ``` //! +//! An additional volume API is also available for reading large volumes slice +//! by slice. +//! +//! ```no_run +//! # use nifti::{NiftiObject, StreamedNiftiObject}; +//! # use nifti::error::NiftiError; +//! let obj = StreamedNiftiObject::from_file("minimal.nii.gz")?; +//! +//! let volume = obj.into_volume(); +//! for slice in volume { +//! let slice = slice?; +//! // manipulate slice here +//! } +//! # Ok::<(), NiftiError>(()) +//! ``` +//! #![deny(missing_debug_implementations)] #![warn(missing_docs, unused_extern_crates, trivial_casts, unused_results)] #![recursion_limit = "128"] @@ -73,10 +89,10 @@ pub mod typedef; mod util; pub use error::{NiftiError, Result}; -pub use object::{NiftiObject, InMemNiftiObject}; +pub use object::{NiftiObject, InMemNiftiObject, StreamedNiftiObject}; pub use extension::{Extender, Extension, ExtensionSequence}; pub use header::{NiftiHeader, NiftiHeaderBuilder}; -pub use volume::{NiftiVolume, InMemNiftiVolume, Sliceable}; +pub use volume::{NiftiVolume, RandomAccessNiftiVolume, InMemNiftiVolume, StreamedNiftiVolume, Sliceable}; pub use volume::element::DataElement; #[cfg(feature = "ndarray_volumes")] pub use volume::ndarray::IntoNdArray; pub use typedef::{NiftiType, Unit, Intent, XForm, SliceOrder}; diff --git a/src/object.rs b/src/object.rs index 97efd6d..ce64446 100644 --- a/src/object.rs +++ b/src/object.rs @@ -10,9 +10,10 @@ use header::MAGIC_CODE_NI1; use std::fs::File; use std::io::{self, BufReader, Read}; use std::path::Path; -use util::{into_img_file_gz, is_gz_file}; +use util::{into_img_file_gz, is_gz_file, open_file_maybe_gz}; use volume::inmem::InMemNiftiVolume; -use volume::NiftiVolume; +use volume::streamed::StreamedNiftiVolume; +use volume::{FromSource, FromSourceOptions, NiftiVolume}; /// Trait type for all possible implementations of /// owning NIFTI-1 objects. Objects contain a NIFTI header, @@ -38,14 +39,44 @@ pub trait NiftiObject { fn into_volume(self) -> Self::Volume; } -/// Data type for a NIFTI object that is fully contained in memory. +/// Generic data type for a NIfTI object. #[derive(Debug, PartialEq, Clone)] -pub struct InMemNiftiObject { +pub struct GenericNiftiObject { header: NiftiHeader, extensions: ExtensionSequence, - volume: InMemNiftiVolume, + volume: V, } +impl NiftiObject for GenericNiftiObject +where + V: NiftiVolume, +{ + type Volume = V; + + fn header(&self) -> &NiftiHeader { + &self.header + } + + fn header_mut(&mut self) -> &mut NiftiHeader { + &mut self.header + } + + fn extensions(&self) -> &ExtensionSequence { + &self.extensions + } + + fn volume(&self) -> &Self::Volume { + &self.volume + } + + fn into_volume(self) -> Self::Volume { + self.volume + } +} + +/// A NIfTI object containing an in-memory volume. +pub type InMemNiftiObject = GenericNiftiObject; + impl InMemNiftiObject { /// Retrieve the full contents of a NIFTI object. /// The given file system path is used as reference. @@ -56,56 +87,249 @@ impl InMemNiftiObject { /// # Example /// /// ```no_run - /// use nifti::InMemNiftiObject; - /// # use nifti::error::Result; + /// use nifti::{NiftiObject, InMemNiftiObject}; /// - /// # fn run() -> Result<()> { /// let obj = InMemNiftiObject::from_file("minimal.nii.gz")?; - /// # Ok(()) - /// # } + /// # Ok::<(), nifti::NiftiError>(()) /// ``` - pub fn from_file>(path: P) -> Result { + pub fn from_file>(path: P) -> Result { let gz = is_gz_file(&path); let file = BufReader::new(File::open(&path)?); if gz { - Self::from_file_2(path, GzDecoder::new(file)) + Self::from_file_2(path, GzDecoder::new(file), Default::default()) } else { - Self::from_file_2(path, file) + Self::from_file_2(path, file, Default::default()) } } - fn from_file_2, S>(path: P, mut stream: S) -> Result + /// Retrieve a NIFTI object as separate header and volume files. + /// This method is useful when file names are not conventional for a + /// NIFTI file pair. + pub fn from_file_pair(hdr_path: P, vol_path: Q) -> Result where - S: Read, + P: AsRef, + Q: AsRef, { - let header = NiftiHeader::from_stream(&mut stream)?; + let gz = is_gz_file(&hdr_path); + + let file = BufReader::new(File::open(&hdr_path)?); + if gz { + Self::from_file_pair_2(GzDecoder::new(file), vol_path, Default::default()) + } else { + Self::from_file_pair_2(file, vol_path, Default::default()) + } + } + + /// Retrieve an in-memory NIfTI object from a stream of data. + #[deprecated(note = "use `from_reader` instead")] + pub fn new_from_stream(&self, source: R) -> Result { + Self::from_reader(source) + } +} + +/// A NIfTI object containing a [streamed volume]. +/// +/// [streamed volume]: ../volume/streamed/index.html +pub type StreamedNiftiObject = GenericNiftiObject>; + +impl StreamedNiftiObject> { + /// Retrieve the NIfTI object and prepare the volume for streamed reading. + /// The given file system path is used as reference. + /// If the file only contains the header, this method will + /// look for the corresponding file with the extension ".img", + /// or ".img.gz" if the former wasn't found. + /// + /// # Example + /// + /// ```no_run + /// use nifti::{NiftiObject, StreamedNiftiObject}; + /// + /// let obj = StreamedNiftiObject::from_file("minimal.nii.gz")?; + /// + /// let volume = obj.into_volume(); + /// for slice in volume { + /// let slice = slice?; + /// // manipulate slice here + /// } + /// # Ok::<(), nifti::NiftiError>(()) + /// ``` + pub fn from_file>(path: P) -> Result { + let gz = is_gz_file(&path); + + let file = BufReader::new(File::open(&path)?); + if gz { + Self::from_file_2_erased(path, GzDecoder::new(file), None) + } else { + Self::from_file_2_erased(path, file, None) + } + } + + /// Retrieve the NIfTI object and prepare the volume for streamed reading, + /// using `slice_rank` as the dimensionality of each slice. + /// The given file system path is used as reference. + /// If the file only contains the header, this method will + /// look for the corresponding file with the extension ".img", + /// or ".img.gz" if the former wasn't found. + pub fn from_file_rank>(path: P, slice_rank: u16) -> Result { + let gz = is_gz_file(&path); + + let file = BufReader::new(File::open(&path)?); + if gz { + Self::from_file_2_erased(path, GzDecoder::new(file), Some(slice_rank)) + } else { + Self::from_file_2_erased(path, file, Some(slice_rank)) + } + } + + /// Retrieve a NIfTI object as separate header and volume files, for + /// streamed volume reading. This method is useful when file names are not + /// conventional for a NIfTI file pair. + /// + /// # Example + /// + /// ```no_run + /// use nifti::{NiftiObject, StreamedNiftiObject}; + /// + /// let obj = StreamedNiftiObject::from_file_pair("abc.hdr", "abc.img.gz")?; + /// + /// let volume = obj.into_volume(); + /// for slice in volume { + /// let slice = slice?; + /// // manipulate slice here + /// } + /// # Ok::<(), nifti::NiftiError>(()) + /// ``` + pub fn from_file_pair(hdr_path: P, vol_path: Q) -> Result + where + P: AsRef, + Q: AsRef, + { + let gz = is_gz_file(&hdr_path); + + let file = BufReader::new(File::open(&hdr_path)?); + if gz { + Self::from_file_pair_2_erased(GzDecoder::new(file), vol_path, Default::default()) + } else { + Self::from_file_pair_2_erased(file, vol_path, Default::default()) + } + } + + /// Retrieve a NIfTI object as separate header and volume files, for + /// streamed volume reading, using `slice_rank` as the dimensionality of + /// each slice. This method is useful when file names are not conventional + /// for a NIfTI file pair. + pub fn from_file_pair_rank(hdr_path: P, vol_path: Q, slice_rank: u16) -> Result + where + P: AsRef, + Q: AsRef, + { + let gz = is_gz_file(&hdr_path); + + let file = BufReader::new(File::open(&hdr_path)?); + if gz { + Self::from_file_pair_2_erased::<_, _>(GzDecoder::new(file), vol_path, Some(slice_rank)) + } else { + Self::from_file_pair_2_erased::<_, _>(file, vol_path, Some(slice_rank)) + } + } +} + +impl GenericNiftiObject { + /// Construct a NIfTI object from a data reader, first by fetching the + /// header, the extensions, and then the volume. + /// + /// # Errors + /// + /// - `NiftiError::NoVolumeData` if the source only contains (or claims to contain) + /// a header. + pub fn from_reader(mut source: R) -> Result + where + R: Read, + V: FromSource, + { + let header = NiftiHeader::from_reader(&mut source)?; + if &header.magic == MAGIC_CODE_NI1 { + return Err(NiftiError::NoVolumeData); + } + let extender = Extender::from_reader(&mut source)?; + + let (volume, extensions) = GenericNiftiObject::from_reader_with_extensions( + source, + &header, + extender, + Default::default(), + )?; + + Ok(GenericNiftiObject { + header, + extensions, + volume, + }) + } + + /// Read a NIFTI volume, and extensions, from a data reader. The header, + /// extender code and expected byte order of the volume's data must be + /// known in advance. + fn from_reader_with_extensions( + mut source: R, + header: &NiftiHeader, + extender: Extender, + options: ::Options, + ) -> Result<(V, ExtensionSequence)> + where + R: Read, + V: FromSource, + { + // fetch extensions + let len = header.vox_offset as usize; + let len = if len < 352 { 0 } else { len - 352 }; + + let ext = { + let source = ByteOrdered::runtime(&mut source, header.endianness); + ExtensionSequence::from_reader(extender, source, len)? + }; + + // fetch volume (rest of file) + Ok((V::from_reader(source, &header, options)?, ext)) + } + + fn from_file_2( + path: P, + mut stream: R, + options: ::Options, + ) -> Result + where + P: AsRef, + R: Read, + V: FromSource, + V: FromSource>, + V: FromSource>>, + { + let header = NiftiHeader::from_reader(&mut stream)?; let (volume, ext) = if &header.magic == MAGIC_CODE_NI1 { // extensions and volume are in another file // extender is optional - let extender = Extender::from_stream_optional(&mut stream)?.unwrap_or_default(); + let extender = Extender::from_reader_optional(&mut stream)?.unwrap_or_default(); // look for corresponding img file let img_path = path.as_ref().to_path_buf(); let mut img_path_gz = into_img_file_gz(img_path); - InMemNiftiVolume::from_file_with_extensions(&img_path_gz, &header, extender) + Self::from_file_with_extensions(&img_path_gz, &header, extender, options.clone()) .or_else(|e| { match e { NiftiError::Io(ref io_e) if io_e.kind() == io::ErrorKind::NotFound => { // try .img file instead (remove .gz extension) let has_ext = img_path_gz.set_extension(""); debug_assert!(has_ext); - InMemNiftiVolume::from_file_with_extensions( - img_path_gz, - &header, - extender, - ) + Self::from_file_with_extensions(img_path_gz, &header, extender, options) } e => Err(e), } - }).map_err(|e| { + }) + .map_err(|e| { if let NiftiError::Io(io_e) = e { NiftiError::MissingVolumeFile(io_e) } else { @@ -115,112 +339,180 @@ impl InMemNiftiObject { } else { // extensions and volume are in the same source - let extender = Extender::from_stream(&mut stream)?; + let extender = Extender::from_reader(&mut stream)?; let len = header.vox_offset as usize; let len = if len < 352 { 0 } else { len - 352 }; let ext = { let mut stream = ByteOrdered::runtime(&mut stream, header.endianness); - ExtensionSequence::from_stream(extender, stream, len)? + ExtensionSequence::from_reader(extender, stream, len)? }; - let volume = InMemNiftiVolume::from_stream(stream, &header)?; + let volume = FromSource::from_reader(stream, &header, options)?; (volume, ext) }; - Ok(InMemNiftiObject { + Ok(GenericNiftiObject { header, extensions: ext, volume, }) } - /// Retrieve a NIFTI object as separate header and volume files. - /// This method is useful when file names are not conventional for a - /// NIFTI file pair. - pub fn from_file_pair(hdr_path: P, vol_path: Q) -> Result + fn from_file_2_erased, S: 'static>( + path: P, + mut stream: S, + options: ::Options, + ) -> Result where - P: AsRef, - Q: AsRef, + S: Read, + V: FromSource>, { - let gz = is_gz_file(&hdr_path); + let header = NiftiHeader::from_reader(&mut stream)?; + let (volume, ext) = if &header.magic == MAGIC_CODE_NI1 { + // extensions and volume are in another file - let file = BufReader::new(File::open(&hdr_path)?); - if gz { - Self::from_file_pair_2(GzDecoder::new(file), vol_path) + // extender is optional + let extender = Extender::from_reader_optional(&mut stream)?.unwrap_or_default(); + + // look for corresponding img file + let img_path = path.as_ref().to_path_buf(); + let mut img_path_gz = into_img_file_gz(img_path); + + Self::from_file_with_extensions_erased(&img_path_gz, &header, extender, options.clone()) + .or_else(|e| { + match e { + NiftiError::Io(ref io_e) if io_e.kind() == io::ErrorKind::NotFound => { + // try .img file instead (remove .gz extension) + let has_ext = img_path_gz.set_extension(""); + debug_assert!(has_ext); + Self::from_file_with_extensions_erased( + img_path_gz, + &header, + extender, + options, + ) + } + e => Err(e), + } + }) + .map_err(|e| { + if let NiftiError::Io(io_e) = e { + NiftiError::MissingVolumeFile(io_e) + } else { + e + } + })? } else { - Self::from_file_pair_2(file, vol_path) - } + // extensions and volume are in the same source + + let extender = Extender::from_reader(&mut stream)?; + let len = header.vox_offset as usize; + let len = if len < 352 { 0 } else { len - 352 }; + + let ext = { + let mut stream = ByteOrdered::runtime(&mut stream, header.endianness); + ExtensionSequence::from_reader(extender, stream, len)? + }; + + let stream = Box::from(stream); + let volume = FromSource::>::from_reader(stream, &header, options)?; + + (volume, ext) + }; + + Ok(GenericNiftiObject { + header, + extensions: ext, + volume, + }) } - fn from_file_pair_2(mut hdr_stream: S, vol_path: Q) -> Result + fn from_file_pair_2( + mut hdr_stream: S, + vol_path: Q, + options: ::Options, + ) -> Result where S: Read, Q: AsRef, + V: FromSource>, + V: FromSource>>, { - let header = NiftiHeader::from_stream(&mut hdr_stream)?; - let extender = Extender::from_stream_optional(hdr_stream)?.unwrap_or_default(); + let header = NiftiHeader::from_reader(&mut hdr_stream)?; + let extender = Extender::from_reader_optional(hdr_stream)?.unwrap_or_default(); let (volume, extensions) = - InMemNiftiVolume::from_file_with_extensions(vol_path, &header, extender)?; + Self::from_file_with_extensions(vol_path, &header, extender, options)?; - Ok(InMemNiftiObject { + Ok(GenericNiftiObject { header, extensions, volume, }) } - /// Retrieve a NIFTI object from a stream of data. - /// - /// # Errors - /// - /// - `NiftiError::NoVolumeData` if the source only contains (or claims to contain) - /// a header. - pub fn new_from_stream(&self, mut source: R) -> Result { - let header = NiftiHeader::from_stream(&mut source)?; - if &header.magic == MAGIC_CODE_NI1 { - return Err(NiftiError::NoVolumeData); - } - let len = header.vox_offset as usize; - let len = if len < 352 { 0 } else { len - 352 }; - let extender = Extender::from_stream(&mut source)?; - - let ext = { - let source = ByteOrdered::runtime(&mut source, header.endianness); - ExtensionSequence::from_stream(extender, source, len)? - }; - - let volume = InMemNiftiVolume::from_stream(source, &header)?; + fn from_file_pair_2_erased( + mut hdr_stream: S, + vol_path: Q, + options: ::Options, + ) -> Result + where + S: Read, + Q: AsRef, + V: FromSource>, + { + let header = NiftiHeader::from_reader(&mut hdr_stream)?; + let extender = Extender::from_reader_optional(hdr_stream)?.unwrap_or_default(); + let (volume, extensions) = + Self::from_file_with_extensions_erased(vol_path, &header, extender, options)?; - Ok(InMemNiftiObject { + Ok(GenericNiftiObject { header, - extensions: ext, + extensions, volume, }) } -} - -impl NiftiObject for InMemNiftiObject { - type Volume = InMemNiftiVolume; - fn header(&self) -> &NiftiHeader { - &self.header - } - - fn header_mut(&mut self) -> &mut NiftiHeader { - &mut self.header - } - - fn extensions(&self) -> &ExtensionSequence { - &self.extensions - } + /// Read a NIFTI volume, along with the extensions, from an image file. NIFTI-1 volume + /// files usually have the extension ".img" or ".img.gz". In the latter case, the file + /// is automatically decoded as a Gzip stream. + fn from_file_with_extensions

( + path: P, + header: &NiftiHeader, + extender: Extender, + options: ::Options, + ) -> Result<(V, ExtensionSequence)> + where + P: AsRef, + V: FromSource>, + V: FromSource>>, + { + let path = path.as_ref(); + let gz = is_gz_file(path); + let stream = BufReader::new(File::open(path)?); - fn volume(&self) -> &Self::Volume { - &self.volume + if gz { + Self::from_reader_with_extensions(GzDecoder::new(stream), &header, extender, options) + } else { + Self::from_reader_with_extensions(stream, &header, extender, options) + } } - fn into_volume(self) -> Self::Volume { - self.volume + /// Read a NIFTI volume, along with the extensions, from an image file. NIFTI-1 volume + /// files usually have the extension ".img" or ".img.gz". In the latter case, the file + /// is automatically decoded as a Gzip stream. + fn from_file_with_extensions_erased

( + path: P, + header: &NiftiHeader, + extender: Extender, + options: ::Options, + ) -> Result<(V, ExtensionSequence)> + where + P: AsRef, + V: FromSource>, + { + let reader = open_file_maybe_gz(path)?; + Self::from_reader_with_extensions(reader, &header, extender, options) } } diff --git a/src/typedef.rs b/src/typedef.rs index cac4701..b58154f 100644 --- a/src/typedef.rs +++ b/src/typedef.rs @@ -4,12 +4,12 @@ //! reading voxel values). However, primitive integer values can be //! converted to these types and vice-versa. -use volume::element::{DataElement, LinearTransform}; +use byteordered::{Endian, Endianness}; use error::{NiftiError, Result}; +use num_traits::AsPrimitive; use std::io::Read; use std::ops::{Add, Mul}; -use byteordered::{Endian, Endianness}; -use num_traits::AsPrimitive; +use volume::element::{DataElement, LinearTransform}; /// Data type for representing a NIFTI value type in a volume. /// Methods for reading values of that type from a source are also included. @@ -110,43 +110,83 @@ impl NiftiType { match self { NiftiType::Uint8 => { let raw = u8::from_raw(source, endianness)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } NiftiType::Int8 => { let raw = i8::from_raw(source, endianness)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } NiftiType::Uint16 => { let raw = endianness.read_u16(source)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } NiftiType::Int16 => { let raw = endianness.read_i16(source)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } NiftiType::Uint32 => { let raw = endianness.read_u32(source)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } NiftiType::Int32 => { let raw = endianness.read_i32(source)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } NiftiType::Uint64 => { let raw = endianness.read_u64(source)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } NiftiType::Int64 => { let raw = endianness.read_i64(source)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } NiftiType::Float32 => { let raw = endianness.read_f32(source)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } NiftiType::Float64 => { let raw = endianness.read_f64(source)?; - Ok(::Transform::linear_transform(raw.as_(), slope, inter)) + Ok(::Transform::linear_transform( + raw.as_(), + slope, + inter, + )) } // TODO add support for more data types _ => Err(NiftiError::UnsupportedDataType(self)), diff --git a/src/util.rs b/src/util.rs index d9c5ef4..40ec194 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,10 +1,15 @@ //! Private utility module use std::borrow::Cow; -use std::io::{Read, Seek}; +use std::fs::File; +use std::io::{BufReader, Read, Result as IoResult, Seek}; use std::mem; use std::path::{Path, PathBuf}; use byteordered::Endian; +use flate2::read::GzDecoder; use safe_transmute::{transmute_vec, TriviallyTransmutable}; +use super::typedef::NiftiType; +use super::error::NiftiError; + use error::Result; use NiftiHeader; @@ -64,14 +69,53 @@ where } } +/// Validate a raw volume dimensions array, returning a slice of the concrete +/// dimensions. +/// +/// # Error +/// +/// Errors if `dim[0]` is outside the accepted rank boundaries or +/// one of the used dimensions is not positive. +pub fn validate_dim(raw_dim: &[u16; 8]) -> Result<&[u16]> { + let ndim = validate_dimensionality(raw_dim)?; + let o = &raw_dim[1..=ndim]; + if let Some(i) = o.iter().position(|&x| x == 0) { + return Err(NiftiError::InconsistentDim(i as u8, raw_dim[i])); + } + Ok(o) +} + +/// Validate a raw N-dimensional index or shape, returning its rank. +/// +/// # Error +/// +/// Errors if `raw_dim[0]` is outside the accepted rank boundaries: 0 or +/// larger than 7. +pub fn validate_dimensionality(raw_dim: &[u16; 8]) -> Result { + if raw_dim[0] == 0 || raw_dim[0] > 7 { + return Err(NiftiError::InconsistentDim(0, raw_dim[0])); + } + Ok(usize::from(raw_dim[0])) +} + pub fn nb_bytes_for_data(header: &NiftiHeader) -> Result { - let resolution: usize = header.dim()? - .iter() - .map(|d| *d as usize) - .product(); + let resolution = nb_values_for_dims(header.dim()?); Ok(resolution * header.bitpix as usize / 8) } +pub fn nb_values_for_dims(dim: &[u16]) -> usize { + dim + .iter() + .cloned() + .map(usize::from) + .product::() +} + +pub fn nb_bytes_for_dim_datatype(dim: &[u16], datatype: NiftiType) -> usize { + let resolution = nb_values_for_dims(dim); + resolution * datatype.size_of() +} + #[cfg(feature = "ndarray_volumes")] pub fn is_hdr_file

(path: P) -> bool where @@ -110,13 +154,44 @@ pub fn into_img_file_gz(mut path: PathBuf) -> PathBuf { path.with_extension("img.gz") } +/// Open a file for reading, which might be Gzip compressed based on whether +/// the extension ends with ".gz". +pub fn open_file_maybe_gz

(path: P) -> IoResult> +where + P: AsRef, +{ + let path = path.as_ref(); + let file = BufReader::new(File::open(path)?); + if is_gz_file(path) { + Ok(Box::from(GzDecoder::new(file))) + } else { + Ok(Box::from(file)) + } +} + #[cfg(test)] mod tests { - use super::into_img_file_gz; - use super::is_gz_file; + use super::{into_img_file_gz, is_gz_file, nb_bytes_for_dim_datatype}; #[cfg(feature = "ndarray_volumes")] use super::is_hdr_file; use std::path::PathBuf; + use typedef::NiftiType; + + #[test] + fn test_nbytes() { + assert_eq!( + nb_bytes_for_dim_datatype(&[2, 3, 2], NiftiType::Uint8), + 12 + ); + assert_eq!( + nb_bytes_for_dim_datatype(&[2, 3], NiftiType::Uint8), + 6 + ); + assert_eq!( + nb_bytes_for_dim_datatype(&[2, 3], NiftiType::Uint16), + 12 + ); + } #[test] fn filenames() { diff --git a/src/volume/inmem.rs b/src/volume/inmem.rs index 8a09aa7..2698825 100644 --- a/src/volume/inmem.rs +++ b/src/volume/inmem.rs @@ -1,10 +1,10 @@ //! Module holding an in-memory implementation of a NIfTI volume. +use super::shape::Dim; use super::util::coords_to_index; -use super::NiftiVolume; -use byteordered::{ByteOrdered, Endianness}; +use super::{NiftiVolume, RandomAccessNiftiVolume}; +use byteordered::Endianness; use error::{NiftiError, Result}; -use extension::{Extender, ExtensionSequence}; use flate2::bufread::GzDecoder; use header::NiftiHeader; use num_traits::{AsPrimitive, Num}; @@ -13,8 +13,9 @@ use std::io::{BufReader, Read}; use std::ops::{Add, Mul}; use std::path::Path; use typedef::NiftiType; -use util::nb_bytes_for_data; +use util::{nb_bytes_for_data, nb_bytes_for_dim_datatype}; use volume::element::DataElement; +use volume::{FromSource, FromSourceOptions}; #[cfg(feature = "ndarray_volumes")] use ndarray::{Array, Ix, IxDyn, ShapeBuilder}; @@ -33,7 +34,7 @@ use volume::ndarray::IntoNdArray; /// #[derive(Debug, PartialEq, Clone)] pub struct InMemNiftiVolume { - dim: [u16; 8], + dim: Dim, datatype: NiftiType, scl_slope: f32, scl_inter: f32, @@ -45,13 +46,14 @@ impl InMemNiftiVolume { /// Build an InMemNiftiVolume from a header and a buffer. The buffer length and the dimensions /// declared in the header are expected to fit. pub fn from_raw_data(header: &NiftiHeader, raw_data: Vec) -> Result { - if nb_bytes_for_data(header)? != raw_data.len() { - return Err(NiftiError::IncompatibleLength); + let nbytes = nb_bytes_for_data(header)?; + if nbytes != raw_data.len() { + return Err(NiftiError::IncompatibleLength(raw_data.len(), nbytes)); } let datatype = header.data_type()?; Ok(InMemNiftiVolume { - dim: header.dim, + dim: Dim::new(header.dim)?, datatype, scl_slope: header.scl_slope, scl_inter: header.scl_inter, @@ -60,17 +62,55 @@ impl InMemNiftiVolume { }) } + /// Build an InMemNiftiVolume from its raw set of attributes. The raw data + /// is assumed to contain exactly enough bytes to contain the data elements + /// of the volume in F-major order, with the byte order specified in + /// `endianness`, as specified by the volume shape in `raw_dim` and data + /// type in `datatype`. + pub fn from_raw_fields( + raw_dim: [u16; 8], + datatype: NiftiType, + scl_slope: f32, + scl_inter: f32, + raw_data: Vec, + endianness: Endianness, + ) -> Result { + let dim = Dim::new(raw_dim)?; + let nbytes = nb_bytes_for_dim_datatype(dim.as_ref(), datatype); + if nbytes != raw_data.len() { + return Err(NiftiError::IncompatibleLength(raw_data.len(), nbytes)); + } + + Ok(InMemNiftiVolume { + dim, + datatype, + scl_slope, + scl_inter, + raw_data, + endianness, + }) + } + + /// Read a NIFTI volume from a stream of data. The header and expected byte order + /// of the volume's data must be known in advance. It it also expected that the + /// following bytes represent the first voxels of the volume (and not part of the + /// extensions). + #[deprecated(since = "0.8.0", note = "use `from_reader` instead")] + pub fn from_stream(source: R, header: &NiftiHeader) -> Result { + Self::from_reader(source, header) + } + /// Read a NIFTI volume from a stream of data. The header and expected byte order /// of the volume's data must be known in advance. It it also expected that the /// following bytes represent the first voxels of the volume (and not part of the /// extensions). - pub fn from_stream(mut source: R, header: &NiftiHeader) -> Result { + pub fn from_reader(mut source: R, header: &NiftiHeader) -> Result { let mut raw_data = vec![0u8; nb_bytes_for_data(header)?]; source.read_exact(&mut raw_data)?; let datatype = header.data_type()?; Ok(InMemNiftiVolume { - dim: header.dim, + dim: Dim::new(header.dim)?, datatype, scl_slope: header.scl_slope, scl_inter: header.scl_inter, @@ -79,30 +119,6 @@ impl InMemNiftiVolume { }) } - /// Read a NIFTI volume, and extensions, from a stream of data. The header, - /// extender code and expected byte order of the volume's data must be - /// known in advance. - pub fn from_stream_with_extensions( - mut source: R, - header: &NiftiHeader, - extender: Extender, - ) -> Result<(Self, ExtensionSequence)> - where - R: Read, - { - // fetch extensions - let len = header.vox_offset as usize; - let len = if len < 352 { 0 } else { len - 352 }; - - let ext = { - let source = ByteOrdered::runtime(&mut source, header.endianness); - ExtensionSequence::from_stream::<_, _>(extender, source, len)? - }; - - // fetch volume (rest of file) - Ok((Self::from_stream(source, &header)?, ext)) - } - /// Read a NIFTI volume from an image file. NIFTI-1 volume files usually have the /// extension ".img" or ".img.gz". In the latter case, the file is automatically /// decoded as a Gzip stream. @@ -114,34 +130,9 @@ impl InMemNiftiVolume { .unwrap_or(false); let file = BufReader::new(File::open(path)?); if gz { - InMemNiftiVolume::from_stream(GzDecoder::new(file), &header) + InMemNiftiVolume::from_reader(GzDecoder::new(file), &header) } else { - InMemNiftiVolume::from_stream(file, &header) - } - } - - /// Read a NIFTI volume, along with the extensions, from an image file. NIFTI-1 volume - /// files usually have the extension ".img" or ".img.gz". In the latter case, the file - /// is automatically decoded as a Gzip stream. - pub fn from_file_with_extensions

( - path: P, - header: &NiftiHeader, - extender: Extender, - ) -> Result<(Self, ExtensionSequence)> - where - P: AsRef, - { - let gz = path - .as_ref() - .extension() - .map(|a| a.to_string_lossy() == "gz") - .unwrap_or(false); - let stream = BufReader::new(File::open(path)?); - - if gz { - InMemNiftiVolume::from_stream_with_extensions(GzDecoder::new(stream), &header, extender) - } else { - InMemNiftiVolume::from_stream_with_extensions(stream, &header, extender) + InMemNiftiVolume::from_reader(file, &header) } } @@ -233,6 +224,19 @@ impl InMemNiftiVolume { } } +impl FromSourceOptions for InMemNiftiVolume { + type Options = (); +} + +impl FromSource for InMemNiftiVolume +where + R: Read, +{ + fn from_reader(reader: R, header: &NiftiHeader, (): Self::Options) -> Result { + InMemNiftiVolume::from_reader(reader, header) + } +} + #[cfg(feature = "ndarray_volumes")] impl IntoNdArray for InMemNiftiVolume { /// Consume the volume into an ndarray. @@ -307,33 +311,23 @@ impl<'a> NiftiVolume for &'a InMemNiftiVolume { fn data_type(&self) -> NiftiType { (**self).data_type() } - - fn get_f32(&self, coords: &[u16]) -> Result { - (**self).get_f32(coords) - } - - fn get_f64(&self, coords: &[u16]) -> Result { - (**self).get_f64(coords) - } - - fn get_u8(&self, coords: &[u16]) -> Result { - (**self).get_u8(coords) - } } impl NiftiVolume for InMemNiftiVolume { fn dim(&self) -> &[u16] { - &self.dim[1..self.dimensionality() + 1] + self.dim.as_ref() } fn dimensionality(&self) -> usize { - self.dim[0] as usize + self.dim.rank() } fn data_type(&self) -> NiftiType { self.datatype } +} +impl RandomAccessNiftiVolume for InMemNiftiVolume { fn get_f32(&self, coords: &[u16]) -> Result { self.get_prim(coords) } @@ -375,18 +369,61 @@ impl NiftiVolume for InMemNiftiVolume { } } +impl<'a> RandomAccessNiftiVolume for &'a InMemNiftiVolume { + fn get_f32(&self, coords: &[u16]) -> Result { + (**self).get_f32(coords) + } + + fn get_f64(&self, coords: &[u16]) -> Result { + (**self).get_f64(coords) + } + + fn get_u8(&self, coords: &[u16]) -> Result { + (**self).get_u8(coords) + } + + fn get_i8(&self, coords: &[u16]) -> Result { + (**self).get_i8(coords) + } + + fn get_u16(&self, coords: &[u16]) -> Result { + (**self).get_u16(coords) + } + + fn get_i16(&self, coords: &[u16]) -> Result { + (**self).get_i16(coords) + } + + fn get_u32(&self, coords: &[u16]) -> Result { + (**self).get_u32(coords) + } + + fn get_i32(&self, coords: &[u16]) -> Result { + (**self).get_i32(coords) + } + + fn get_u64(&self, coords: &[u16]) -> Result { + (**self).get_u64(coords) + } + + fn get_i64(&self, coords: &[u16]) -> Result { + (**self).get_i64(coords) + } +} + #[cfg(test)] mod tests { use super::*; use byteordered::Endianness; use typedef::NiftiType; + use volume::shape::Dim; use volume::Sliceable; #[test] fn test_u8_inmem_volume() { let data: Vec = (0..64).map(|x| x * 2).collect(); let vol = InMemNiftiVolume { - dim: [3, 4, 4, 4, 0, 0, 0, 0], + dim: Dim::new([3, 4, 4, 4, 0, 0, 0, 0]).unwrap(), datatype: NiftiType::Uint8, scl_slope: 1., scl_inter: -5., @@ -410,7 +447,7 @@ mod tests { fn test_u8_inmem_volume_slice() { let data: Vec = (0..64).map(|x| x * 2).collect(); let vol = InMemNiftiVolume { - dim: [3, 4, 4, 4, 0, 0, 0, 0], + dim: Dim::new([3, 4, 4, 4, 0, 0, 0, 0]).unwrap(), datatype: NiftiType::Uint8, scl_slope: 1., scl_inter: -5., diff --git a/src/volume/mod.rs b/src/volume/mod.rs index 51a5d22..cf25fb0 100644 --- a/src/volume/mod.rs +++ b/src/volume/mod.rs @@ -5,19 +5,23 @@ //! In order to do so, you must add the `ndarray_volumes` feature //! to this crate. -pub mod inmem; pub mod element; +pub mod inmem; +pub mod shape; +pub mod streamed; pub use self::inmem::*; +pub use self::streamed::StreamedNiftiVolume; mod util; use error::{NiftiError, Result}; +use header::NiftiHeader; +use std::io::Read; use typedef::NiftiType; #[cfg(feature = "ndarray_volumes")] pub mod ndarray; -/// Public API for NIFTI volume data, exposed as a multi-dimensional -/// voxel array. +/// Public API for a NIFTI volume. /// /// This API is currently experimental and will likely be subjected to /// various changes and additions in future versions. @@ -34,6 +38,15 @@ pub trait NiftiVolume { self.dim().len() } + /// Get this volume's data type. + fn data_type(&self) -> NiftiType; +} + +/// Public API for a NIFTI volume with full random access to data. +/// +/// This API is currently experimental and will likely be subjected to +/// various changes and additions in future versions. +pub trait RandomAccessNiftiVolume: NiftiVolume { /// Fetch a single voxel's value in the given voxel index coordinates /// as a double precision floating point value. /// All necessary conversions and transformations are made @@ -47,9 +60,6 @@ pub trait NiftiVolume { /// volume's boundaries. fn get_f64(&self, coords: &[u16]) -> Result; - /// Get this volume's data type. - fn data_type(&self) -> NiftiType; - /// Fetch a single voxel's value in the given voxel index coordinates /// as a single precision floating point value. /// All necessary conversions and transformations are made @@ -63,8 +73,7 @@ pub trait NiftiVolume { /// volume's boundaries. #[inline] fn get_f32(&self, coords: &[u16]) -> Result { - self.get_f64(coords) - .map(|v| v as f32) + self.get_f64(coords).map(|v| v as f32) } /// Fetch a single voxel's value in the given voxel index coordinates @@ -80,8 +89,7 @@ pub trait NiftiVolume { /// volume's boundaries. #[inline] fn get_u8(&self, coords: &[u16]) -> Result { - self.get_f64(coords) - .map(|v| v as u8) + self.get_f64(coords).map(|v| v as u8) } /// Fetch a single voxel's value in the given voxel index coordinates @@ -97,8 +105,7 @@ pub trait NiftiVolume { /// volume's boundaries. #[inline] fn get_i8(&self, coords: &[u16]) -> Result { - self.get_f64(coords) - .map(|v| v as i8) + self.get_f64(coords).map(|v| v as i8) } /// Fetch a single voxel's value in the given voxel index coordinates @@ -114,8 +121,7 @@ pub trait NiftiVolume { /// volume's boundaries. #[inline] fn get_u16(&self, coords: &[u16]) -> Result { - self.get_f64(coords) - .map(|v| v as u16) + self.get_f64(coords).map(|v| v as u16) } /// Fetch a single voxel's value in the given voxel index coordinates @@ -131,8 +137,7 @@ pub trait NiftiVolume { /// volume's boundaries. #[inline] fn get_i16(&self, coords: &[u16]) -> Result { - self.get_f64(coords) - .map(|v| v as i16) + self.get_f64(coords).map(|v| v as i16) } /// Fetch a single voxel's value in the given voxel index coordinates @@ -148,8 +153,7 @@ pub trait NiftiVolume { /// volume's boundaries. #[inline] fn get_u32(&self, coords: &[u16]) -> Result { - self.get_f64(coords) - .map(|v| v as u32) + self.get_f64(coords).map(|v| v as u32) } /// Fetch a single voxel's value in the given voxel index coordinates @@ -165,8 +169,7 @@ pub trait NiftiVolume { /// volume's boundaries. #[inline] fn get_i32(&self, coords: &[u16]) -> Result { - self.get_f64(coords) - .map(|v| v as i32) + self.get_f64(coords).map(|v| v as i32) } /// Fetch a single voxel's value in the given voxel index coordinates @@ -182,8 +185,7 @@ pub trait NiftiVolume { /// volume's boundaries. #[inline] fn get_u64(&self, coords: &[u16]) -> Result { - self.get_f64(coords) - .map(|v| v as u64) + self.get_f64(coords).map(|v| v as u64) } /// Fetch a single voxel's value in the given voxel index coordinates @@ -199,12 +201,11 @@ pub trait NiftiVolume { /// volume's boundaries. #[inline] fn get_i64(&self, coords: &[u16]) -> Result { - self.get_f64(coords) - .map(|v| v as i64) + self.get_f64(coords).map(|v| v as i64) } } -/// Interface for a volume that can be sliced. +/// Interface for a volume that can be sliced at an arbitrary position. pub trait Sliceable { /// The type of the resulting slice, which is also a volume. type Slice: NiftiVolume; @@ -214,9 +215,30 @@ pub trait Sliceable { fn get_slice(&self, axis: u16, index: u16) -> Result; } +/// Interface for specifying the type for the set of options that are relevent +/// for constructing a volume instance from a data source. The separation +/// between this type and `FromSource` is important because the options are +/// invariant with respect to the reader type `R`. +pub trait FromSourceOptions { + /// Set of additional options required (or useful) for constructing a volume. + type Options: Clone + Default; +} + +/// Interface for constructing a volume instance from the given data source. +pub trait FromSource: FromSourceOptions + Sized { + /// Read a NIfTI volume from a stream of raw voxel data. The header and + /// expected byte order of the volume's data must be known in advance. It + /// is also expected that the following bytes represent the first voxels of + /// the volume (and not part of the extensions). + fn from_reader(reader: R, header: &NiftiHeader, options: Self::Options) -> Result + where + R: Read; +} + /// A view over a single slice of another volume. -/// Slices are usually created by calling the `get_slice` method (see `Sliceable`). -/// This implementation is generic and delegates most operations to the underlying volume. +/// Slices are usually created by calling the `get_slice` method on another +/// volume with random access to voxels (see `Sliceable`). This implementation +/// is generic and delegates most operations to the underlying volume. #[derive(Debug, Clone)] pub struct SliceView { volume: T, @@ -265,6 +287,21 @@ where &self.dim } + #[inline] + fn dimensionality(&self) -> usize { + self.dim.len() + } + + #[inline] + fn data_type(&self) -> NiftiType { + self.volume.data_type() + } +} + +impl RandomAccessNiftiVolume for SliceView +where + V: RandomAccessNiftiVolume, +{ fn get_f32(&self, coords: &[u16]) -> Result { let mut coords = Vec::from(coords); coords.insert(self.axis as usize, self.index); @@ -324,10 +361,4 @@ where coords.insert(self.axis as usize, self.index); self.volume.get_i64(&coords) } - - /// Get this volume's data type. - #[inline] - fn data_type(&self) -> NiftiType { - self.volume.data_type() - } } diff --git a/src/volume/shape.rs b/src/volume/shape.rs new file mode 100644 index 0000000..fe5ed54 --- /dev/null +++ b/src/volume/shape.rs @@ -0,0 +1,296 @@ +//! Shape and N-dimensional index constructs. +//! +//! The NIfTI-1 format has a hard dimensionality limit of 7. This is +//! specified in the `dim` field as an array of 8 integers where the +//! first element represents the number of dimensions. In order +//! to make dimensions and indices easier to manipulate, the types +//! [`Dim`] and [`Idx`] are provided here. +//! +//! [`Dim`]: ./struct.Dim.html +//! [`Idx`]: ./struct.Idx.html +use error::{NiftiError, Result}; +use util::{validate_dim, validate_dimensionality}; + +/// A validated N-dimensional index in the NIfTI format. +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] +#[repr(transparent)] +pub struct Idx( + /// dimensions starting at 1, dim[0] is the dimensionality + [u16; 8], +); + +impl Idx { + /// Validate and create a new index from the raw data field. + /// + /// # Example + /// + /// ``` + /// # use nifti::volume::shape::Idx; + /// let idx = Idx::new([3, 1, 2, 5, 0, 0, 0, 0])?; + /// assert_eq!(idx.as_ref(), &[1, 2, 5]); + /// # Ok::<(), nifti::NiftiError>(()) + /// ``` + pub fn new(idx: [u16; 8]) -> Result { + let _ = validate_dimensionality(&idx)?; + Ok(Idx(idx)) + } + + /// Create a new index without validating it. + /// + /// # Safety + /// + /// The program may misbehave severely if the raw `idx` field is not + /// consistent. The first element, `idx[0]`, must be a valid rank between 1 + /// and 7. + pub unsafe fn new_unchecked(idx: [u16; 8]) -> Self { + Idx(idx) + } + + /// Create a new N-dimensional index using the given slice as the concrete + /// shape (`idx[0]` is not the rank but the actual width-wide position of + /// the index). + /// + /// # Example + /// + /// ``` + /// # use nifti::volume::shape::Idx; + /// let idx = Idx::from_slice(&[1, 2, 5])?; + /// assert_eq!(idx.as_ref(), &[1, 2, 5]); + /// # Ok::<(), nifti::NiftiError>(()) + /// ``` + pub fn from_slice(idx: &[u16]) -> Result { + if idx.len() == 0 || idx.len() > 7 { + return Err(NiftiError::InconsistentDim(0, idx.len() as u16)); + } + let mut raw = [0; 8]; + raw[0] = idx.len() as u16; + for (i, d) in idx.iter().enumerate() { + raw[i + 1] = *d; + } + Ok(Idx(raw)) + } + + /// Retrieve a reference to the raw field + pub fn raw(&self) -> &[u16; 8] { + &self.0 + } + + /// Retrieve the rank of this index (dimensionality) + pub fn rank(&self) -> usize { + usize::from(self.0[0]) + } +} + +impl AsRef<[u16]> for Idx { + fn as_ref(&self) -> &[u16] { + &self.0[1..=self.rank()] + } +} + +impl AsMut<[u16]> for Idx { + fn as_mut(&mut self) -> &mut [u16] { + let rank = self.rank(); + &mut self.0[1..=rank] + } +} + +/// A validated NIfTI volume shape. +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] +#[repr(transparent)] +pub struct Dim(Idx); + +impl Dim { + /// Validate and create a new volume shape. + /// + /// # Example + /// + /// ``` + /// # use nifti::volume::shape::Dim; + /// let dim = Dim::new([3, 64, 32, 16, 0, 0, 0, 0])?; + /// assert_eq!(dim.as_ref(), &[64, 32, 16]); + /// # Ok::<(), nifti::NiftiError>(()) + /// ``` + pub fn new(dim: [u16; 8]) -> Result { + let _ = validate_dim(&dim)?; + Ok(Dim(Idx(dim))) + } + + /// Create a new volume shape without validating it. + /// + /// # Safety + /// + /// The program may misbehave severely if the raw `dim` field is not + /// consistent. The first element, `dim[0]`, must be a valid rank between + /// 1 and 7, and the valid dimensions in `dim[0..rank]` must be positive. + pub unsafe fn new_unchecked(dim: [u16; 8]) -> Self { + Dim(Idx(dim)) + } + + /// Create a new volume shape using the given slice as the concrete + /// shape (`dim[0]` is not the rank but the actual width of the volume). + /// + /// # Example + /// + /// ``` + /// # use nifti::volume::shape::Dim; + /// let dim = Dim::from_slice(&[64, 32, 16])?; + /// assert_eq!(dim.as_ref(), &[64, 32, 16]); + /// # Ok::<(), nifti::NiftiError>(()) + /// ``` + pub fn from_slice(dim: &[u16]) -> Result { + if dim.len() == 0 || dim.len() > 7 { + return Err(NiftiError::InconsistentDim(0, dim.len() as u16)); + } + let mut raw = [0; 8]; + raw[0] = dim.len() as u16; + for (i, d) in dim.iter().enumerate() { + raw[i + 1] = *d; + } + let _ = validate_dim(&raw)?; + Ok(Dim(Idx(raw))) + } + + /// Retrieve a reference to the raw dim field + pub fn raw(&self) -> &[u16; 8] { + self.0.raw() + } + + /// Retrieve the rank of this shape (dimensionality) + pub fn rank(&self) -> usize { + self.0.rank() + } + + /// Calculate the number of elements in this shape + pub fn element_count(&self) -> usize { + self.as_ref().iter().cloned().map(usize::from).product() + } + + /// Split the dimensions into two parts at the given axis. The first `Dim` + /// will cover the first axes up to `axis`, excluding `axis` itself. + /// + /// # Panic + /// + /// Panics if `axis` is not between 0 and `self.rank()`. + pub fn split(&self, axis: u16) -> (Dim, Dim) { + let axis = usize::from(axis); + assert!(axis <= self.rank()); + let (l, r) = self.as_ref().split_at(axis); + (Dim::from_slice(l).unwrap(), Dim::from_slice(r).unwrap()) + } + + /// Provide an iterator traversing through all possible indices of a + /// hypothetical volume with this shape. + pub fn index_iter(&self) -> DimIter { + DimIter::new(*self) + } +} + +impl AsRef<[u16]> for Dim { + fn as_ref(&self) -> &[u16] { + self.0.as_ref() + } +} + +/// An iterator of all indices in a multi-dimensional volume. +/// +/// Traversal is in standard NIfTI volume order (column major). +#[derive(Debug, Clone)] +pub struct DimIter { + shape: Dim, + state: DimIterState, +} + +#[derive(Debug, Copy, Clone)] +enum DimIterState { + First, + Middle(Idx), + Fused, +} + +impl DimIter { + fn new(shape: Dim) -> Self { + DimIter { + shape, + state: DimIterState::First, + } + } +} + +impl Iterator for DimIter { + type Item = Idx; + + fn next(&mut self) -> Option { + let (out, next_state) = match &mut self.state { + DimIterState::First => { + let out = Idx([self.shape.rank() as u16, 0, 0, 0, 0, 0, 0, 0]); + dbg!((Some(out), DimIterState::Middle(out))) + } + DimIterState::Fused => dbg!((None, DimIterState::Fused)), + DimIterState::Middle(mut current) => { + let mut good = false; + for (c, s) in Iterator::zip(current.as_mut().iter_mut(), self.shape.as_ref().iter()) + { + if *c < *s - 1 { + *c += 1; + good = true; + break; + } + *c = 0; + } + if good { + dbg!((Some(current), DimIterState::Middle(current))) + } else { + dbg!((None, DimIterState::Fused)) + } + } + }; + self.state = next_state; + out + } +} + +#[cfg(test)] +mod tests { + use super::{Dim, Idx}; + + #[test] + fn test_dim() { + let raw_dim = [3, 256, 256, 100, 0, 0, 0, 0]; + let dim = Dim::new(raw_dim).unwrap(); + assert_eq!(dim.as_ref(), &[256, 256, 100]); + assert_eq!(dim.element_count(), 6553600); + } + + #[test] + fn test_dim_iter() { + let raw_dim = [2, 3, 4, 0, 0, 0, 0, 0]; + let dim = Dim::new(raw_dim).unwrap(); + assert_eq!(dim.as_ref(), &[3, 4]); + assert_eq!(dim.element_count(), 12); + + let idx: Vec<_> = dim.index_iter().take(13).collect(); + assert_eq!(idx.len(), dim.element_count()); + for (i, (got, expected)) in Iterator::zip( + idx.into_iter(), + vec![ + Idx::from_slice(&[0, 0]).unwrap(), + Idx::from_slice(&[1, 0]).unwrap(), + Idx::from_slice(&[2, 0]).unwrap(), + Idx::from_slice(&[0, 1]).unwrap(), + Idx::from_slice(&[1, 1]).unwrap(), + Idx::from_slice(&[2, 1]).unwrap(), + Idx::from_slice(&[0, 2]).unwrap(), + Idx::from_slice(&[1, 2]).unwrap(), + Idx::from_slice(&[2, 2]).unwrap(), + Idx::from_slice(&[0, 3]).unwrap(), + Idx::from_slice(&[1, 3]).unwrap(), + Idx::from_slice(&[2, 3]).unwrap(), + ] + .into_iter(), + ) + .enumerate() + { + assert_eq!(got, expected, "#{} not ok", i); + } + } +} diff --git a/src/volume/streamed.rs b/src/volume/streamed.rs new file mode 100644 index 0000000..2d5558a --- /dev/null +++ b/src/volume/streamed.rs @@ -0,0 +1,533 @@ +//! Streamed interface of a NIfTI volume and implementation. +//! +//! This API provides slice-by-slice reading of volumes, thus lowering +//! memory requirements and better supporting the manipulation of +//! large volumes. +//! +//! Since volumes are physically persisted in column major order, each slice +//! will cover the full range of the first axes of the volume and traverse +//! the rightmost axes in each iteration. As an example, a 3D volume of +//! dimensions `[256, 256, 128]`, assuming 2D slices, will produce slices of +//! dimensions `[256, 256]`, starting at the slice `[.., .., 0]` and ending +//! at the slice `[.., .., 127]`. +//! +//! Slices may also have an arbitrary rank (dimensionality), as long as it +//! is smaller than the original volume's rank. A good default is the +//! original volume shape minus 1 (`R - 1`). +//! +//! # Examples +//! +//! Obtain a [`StreamedNiftiVolume`], usually from loading a +//! [`StreamedNiftiObject`]. When holding a streamed volume, one can use the +//! [`Iterator` API] to iteratively fetch data from the byte source, making +//! in-memory sub-volumes each time. +//! +//! ```no_run +//! # use nifti::{StreamedNiftiVolume, InMemNiftiVolume}; +//! # fn get_volume() -> StreamedNiftiVolume> { unimplemented!() } +//! let volume: StreamedNiftiVolume<_> = get_volume(); +//! for slice in volume { +//! let slice: InMemNiftiVolume = slice?; +//! // use slice +//! } +//! # Ok::<(), nifti::NiftiError>(()) +//! ``` +//! +//! For additional efficiency, a streamed iterator method is provided, which +//! enables the reuse of the same raw data vector for the slices. +//! +//! ```no_run +//! # use nifti::{StreamedNiftiVolume, InMemNiftiVolume}; +//! # fn get_volume() -> StreamedNiftiVolume> { unimplemented!() } +//! let mut volume: StreamedNiftiVolume<_> = get_volume(); +//! +//! let mut buffer = Vec::new(); // or with the expected capacity +//! while let Some(slice) = volume.next_inline(buffer) { +//! let slice: InMemNiftiVolume = slice?; +//! // use slice, then recover raw data vector +//! buffer = slice.into_raw_data(); +//! } +//! # Ok::<(), nifti::NiftiError>(()) +//! ``` +//! +//! [`StreamedNiftiVolume`]: ./struct.StreamedNiftiVolume.html +//! [`StreamedNiftiObject`]: ../../object/type.StreamedNiftiObject.html +//! [`Iterator` API]: https://doc.rust-lang.org/std/iter/trait.Iterator.html +//! + +use super::inmem::InMemNiftiVolume; +use super::shape::{Dim, Idx}; +use super::{FromSource, FromSourceOptions, NiftiVolume}; +use byteordered::Endianness; +use error::Result; +use header::NiftiHeader; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::Path; +use typedef::NiftiType; +use util::nb_bytes_for_dim_datatype; + +/// A NIfTI-1 volume instance that is read slice by slice from a byte stream. +/// +/// See the [module-level documentation] for more details. +/// +/// [module-level documentation]: ./index.html +#[derive(Debug)] +pub struct StreamedNiftiVolume { + source: R, + dim: Dim, + slice_dim: Dim, + datatype: NiftiType, + scl_slope: f32, + scl_inter: f32, + endianness: Endianness, + slices_read: usize, + slices_left: usize, +} + +impl StreamedNiftiVolume> { + /// Read a NIFTI volume from an uncompressed file. The header and expected + /// byte order of the volume's data must be known in advance. It is also + /// expected that the file starts with the first voxels of the volume, not + /// with the extensions. + pub fn from_file

(path: P, header: &NiftiHeader) -> Result + where + P: AsRef, + { + let file = BufReader::new(File::open(path)?); + Self::from_reader(file, header) + } +} + +impl StreamedNiftiVolume +where + R: Read, +{ + /// Read a NIFTI volume from a stream of raw voxel data. The header and + /// expected byte order of the volume's data must be known in advance. It + /// is also expected that the following bytes represent the first voxels of + /// the volume (and not part of the extensions). + /// + /// By default, the slice's rank is the original volume's rank minus 1. + pub fn from_reader(source: R, header: &NiftiHeader) -> Result { + let dim = Dim::new(header.dim)?; + let slice_rank = dim.rank() - 1; + StreamedNiftiVolume::from_reader_rank(source, header, slice_rank as u16) + } + + /// Read a NIFTI volume from a stream of data. The header and expected byte order + /// of the volume's data must be known in advance. It it also expected that the + /// following bytes represent the first voxels of the volume (and not part of the + /// extensions). + /// + /// The slice rank defines how many dimensions each slice should have. + pub fn from_reader_rank(source: R, header: &NiftiHeader, slice_rank: u16) -> Result { + // TODO recoverable error if #dim == 0 + let dim = Dim::new(header.dim)?; // check dim consistency + let datatype = header.data_type()?; + let slice_dim = calculate_slice_dims(&dim, slice_rank); + let slices_left = calculate_total_slices(&dim, slice_rank); + Ok(StreamedNiftiVolume { + source, + dim, + slice_dim, + datatype, + scl_slope: header.scl_slope, + scl_inter: header.scl_inter, + endianness: header.endianness, + slices_read: 0, + slices_left, + }) + } + + /// Retrieve the full volume shape. + pub fn dim(&self) -> &[u16] { + self.dim.as_ref() + } + + /// Retrieve the shape of the slices. + pub fn slice_dim(&self) -> &[u16] { + self.slice_dim.as_ref() + } + + /// Retrieve the number of slices already read + pub fn slices_read(&self) -> usize { + self.slices_read + } + + /// Retrieve the number of slices left + pub fn slices_left(&self) -> usize { + self.slices_left + } + + /// Read a volume slice from the data source, producing an in-memory + /// sub-volume. + pub fn read_slice(&mut self) -> Result { + self.read_slice_inline(Vec::new()) + } + + /// Read a volume slice from the data source, producing an in-memory + /// sub-volume. This method reuses the given `buffer` to avoid + /// reallocations. Any data that the buffer previously had is + /// discarded. + pub fn read_slice_inline(&mut self, buffer: Vec) -> Result { + let mut raw_data = buffer; + raw_data.resize( + nb_bytes_for_dim_datatype(self.slice_dim(), self.datatype), + 0, + ); + self.source.read_exact(&mut raw_data)?; + + self.slices_read += 1; + self.slices_left = self.slices_left.saturating_sub(1); + InMemNiftiVolume::from_raw_fields( + *self.slice_dim.raw(), + self.datatype, + self.scl_slope, + self.scl_inter, + raw_data, + self.endianness, + ) + } + + /// Fetch the next slice while reusing a raw data buffer. This is the + /// streaming iterator equivalent of `Iterator::next`. Once the output + /// volume has been used, the method [`into_raw_data`] can be used to + /// recover the vector for the subsequent iteration. + /// + /// [`into_raw_data`]: ../inmem/struct.InMemNiftiVolume.html#method.into_raw_data + pub fn next_inline(&mut self, buffer: Vec) -> Option> { + if self.slices_left == 0 { + return None; + } + Some(self.read_slice_inline(buffer)) + } + + /// Adapt the streamed volume to produce slice indices alongside the produced + /// slices. + /// + /// # Example + /// + /// ```no_run + /// # use nifti::{StreamedNiftiVolume, InMemNiftiVolume}; + /// # use nifti::volume::shape::Idx; + /// # fn get_volume() -> StreamedNiftiVolume> { unimplemented!() } + /// let mut volume = get_volume(); + /// for slice_pair in volume.indexed() { + /// let (idx, slice): (Idx, InMemNiftiVolume) = slice_pair?; + /// // use idx and slice + /// } + /// # Ok::<(), nifti::NiftiError>(()) + /// ``` + pub fn indexed<'a>(&'a mut self) -> impl Iterator> + 'a { + let (_, r) = self.dim.split(self.slice_dim.rank() as u16); + self.zip(r.index_iter()) + .map(|(vol_result, idx)| vol_result.map(|v| (idx, v))) + } +} + +impl FromSourceOptions for StreamedNiftiVolume { + type Options = Option; +} + +impl FromSource for StreamedNiftiVolume +where + R: Read, +{ + fn from_reader(reader: R, header: &NiftiHeader, options: Self::Options) -> Result { + if let Some(slice_rank) = options { + StreamedNiftiVolume::from_reader_rank(reader, header, slice_rank) + } else { + StreamedNiftiVolume::from_reader(reader, header) + } + } +} + +impl<'a, R> NiftiVolume for &'a StreamedNiftiVolume { + fn dim(&self) -> &[u16] { + (**self).dim() + } + + fn dimensionality(&self) -> usize { + (**self).dimensionality() + } + + fn data_type(&self) -> NiftiType { + (**self).data_type() + } +} + +impl NiftiVolume for StreamedNiftiVolume { + fn dim(&self) -> &[u16] { + self.dim.as_ref() + } + + fn dimensionality(&self) -> usize { + self.dim.rank() + } + + fn data_type(&self) -> NiftiType { + self.datatype + } +} + +/** + * The iterator pattern in a streamed NIfTI volume calls the method + * [`read_slice`] on `next` unless all slices have already been read from the + * volume. + * + * [`read_slice`](./struct.StreamedNiftiVolume.html#method.read_slice) + */ +impl std::iter::Iterator for StreamedNiftiVolume +where + R: Read, +{ + type Item = Result; + + fn next(&mut self) -> Option { + if self.slices_left == 0 { + return None; + } + Some(self.read_slice()) + } +} + +fn calculate_slice_dims(dim: &Dim, slice_rank: u16) -> Dim { + assert!(dim.rank() > 0); + assert!(usize::from(slice_rank) < dim.rank()); + let mut raw_dim = *dim.raw(); + raw_dim[0] = slice_rank; + Dim::new(raw_dim).unwrap() +} + +fn calculate_total_slices(dim: &Dim, slice_rank: u16) -> usize { + assert!(usize::from(slice_rank) < dim.rank()); + let (_, r) = dim.split(slice_rank); + r.element_count() +} + +#[cfg(test)] +mod tests { + + use super::super::{NiftiVolume, RandomAccessNiftiVolume}; + use super::StreamedNiftiVolume; + use byteordered::Endianness; + use typedef::NiftiType; + use NiftiHeader; + + #[test] + fn test_streamed_base() { + let volume_data = &[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23]; + let header = NiftiHeader { + dim: [3, 2, 3, 2, 0, 0, 0, 0], + datatype: NiftiType::Uint8 as i16, + scl_slope: 1., + scl_inter: 0., + endianness: Endianness::native(), + ..NiftiHeader::default() + }; + + let mut volume = StreamedNiftiVolume::from_reader(&volume_data[..], &header).unwrap(); + + assert_eq!(volume.dim(), &[2, 3, 2]); + assert_eq!(volume.slice_dim(), &[2, 3]); + assert_eq!(volume.slices_read(), 0); + + { + let slice = volume + .next() + .expect("1st slice should exist") + .expect("should not fail to construct the volume"); + + assert_eq!(slice.dim(), &[2, 3]); + assert_eq!(slice.data_type(), NiftiType::Uint8); + assert_eq!(slice.raw_data(), &[1, 3, 5, 7, 9, 11]); + } + { + let slice = volume + .next() + .expect("2nd slice should exist") + .expect("should not fail to construct the volume"); + + assert_eq!(slice.dim(), &[2, 3]); + assert_eq!(slice.data_type(), NiftiType::Uint8); + assert_eq!(slice.raw_data(), &[13, 15, 17, 19, 21, 23]); + } + assert!(volume.next().is_none()); + } + + #[test] + fn test_streamed_indexed() { + let volume_data = &[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23]; + let header = NiftiHeader { + dim: [3, 2, 3, 2, 0, 0, 0, 0], + datatype: NiftiType::Uint8 as i16, + scl_slope: 1., + scl_inter: 0., + endianness: Endianness::native(), + ..NiftiHeader::default() + }; + + let mut volume = StreamedNiftiVolume::from_reader(&volume_data[..], &header).unwrap(); + + assert_eq!(volume.dim(), &[2, 3, 2]); + assert_eq!(volume.slice_dim(), &[2, 3]); + assert_eq!(volume.slices_read(), 0); + + let mut volume = volume.indexed(); + { + let (idx, slice) = volume + .next() + .expect("1st slice should exist") + .expect("should not fail to construct the volume"); + + assert_eq!(slice.dim(), &[2, 3]); + assert_eq!(idx.as_ref(), &[0]); + assert_eq!(slice.data_type(), NiftiType::Uint8); + assert_eq!(slice.raw_data(), &[1, 3, 5, 7, 9, 11]); + } + { + let (idx, slice) = volume + .next() + .expect("2nd slice should exist") + .expect("should not fail to construct the volume"); + + assert_eq!(slice.dim(), &[2, 3]); + assert_eq!(idx.as_ref(), &[1]); + assert_eq!(slice.data_type(), NiftiType::Uint8); + assert_eq!(slice.raw_data(), &[13, 15, 17, 19, 21, 23]); + } + assert!(volume.next().is_none()); + } + + #[test] + fn test_streamed_inline() { + let volume_data = &[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23]; + let header = NiftiHeader { + dim: [3, 2, 3, 2, 0, 0, 0, 0], + datatype: NiftiType::Uint8 as i16, + scl_slope: 1., + scl_inter: 0., + endianness: Endianness::native(), + ..NiftiHeader::default() + }; + + let mut volume = StreamedNiftiVolume::from_reader(&volume_data[..], &header).unwrap(); + + assert_eq!(volume.dim(), &[2, 3, 2]); + assert_eq!(volume.slice_dim(), &[2, 3]); + assert_eq!(volume.slices_read(), 0); + let buf = Vec::with_capacity(6); + let buf = { + let slice = volume + .next_inline(buf) + .expect("1st slice should exist") + .expect("should not fail to construct the volume"); + + assert_eq!(slice.dim(), &[2, 3]); + assert_eq!(slice.data_type(), NiftiType::Uint8); + assert_eq!(slice.raw_data(), &[1, 3, 5, 7, 9, 11]); + slice.into_raw_data() + }; + { + let slice = volume + .next_inline(buf) + .expect("2nd slice should exist") + .expect("should not fail to construct the volume"); + + assert_eq!(slice.dim(), &[2, 3]); + assert_eq!(slice.data_type(), NiftiType::Uint8); + assert_eq!(slice.raw_data(), &[13, 15, 17, 19, 21, 23]); + } + assert!(volume.next().is_none()); + } + + #[test] + fn test_streamed_ranked() { + let volume_data = &[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23]; + let header = NiftiHeader { + dim: [4, 2, 3, 2, 1, 0, 0, 0], + datatype: NiftiType::Uint8 as i16, + scl_slope: 1., + scl_inter: 0., + endianness: Endianness::native(), + ..NiftiHeader::default() + }; + + let mut volume = + StreamedNiftiVolume::from_reader_rank(&volume_data[..], &header, 2).unwrap(); + + assert_eq!(volume.dim(), &[2, 3, 2, 1]); + assert_eq!(volume.slice_dim(), &[2, 3]); + assert_eq!(volume.slices_read(), 0); + + { + let slice = volume + .next() + .expect("1st slice should exist") + .expect("should not fail to construct the volume"); + + assert_eq!(slice.dim(), &[2, 3]); + assert_eq!(slice.data_type(), NiftiType::Uint8); + assert_eq!(slice.raw_data(), &[1, 3, 5, 7, 9, 11]); + } + { + let slice = volume + .next() + .expect("2nd slice should exist") + .expect("should not fail to construct the volume"); + + assert_eq!(slice.dim(), &[2, 3]); + assert_eq!(slice.data_type(), NiftiType::Uint8); + assert_eq!(slice.raw_data(), &[13, 15, 17, 19, 21, 23]); + } + assert!(volume.next().is_none()); + } + + #[test] + fn test_streamed_lesser_rank() { + let volume_data = &[ + 1, 0, 3, 0, 5, 0, 7, 0, 9, 0, 11, 0, 13, 0, 15, 0, 17, 0, 19, 0, 21, 0, 23, 0, + ]; + let header = NiftiHeader { + dim: [3, 2, 3, 2, 0, 0, 0, 0], + datatype: NiftiType::Uint16 as i16, + scl_slope: 1., + scl_inter: 0., + endianness: Endianness::Little, + ..NiftiHeader::default() + }; + + let mut volume = + StreamedNiftiVolume::from_reader_rank(&volume_data[..], &header, 1).unwrap(); + + assert_eq!(volume.dim(), &[2, 3, 2]); + assert_eq!(volume.slice_dim(), &[2]); + assert_eq!(volume.slices_read(), 0); + + for (i, raw_data) in [ + &[1, 0, 3, 0], + &[5, 0, 7, 0], + &[9, 0, 11, 0], + &[13, 0, 15, 0], + &[17, 0, 19, 0], + &[21, 0, 23, 0], + ] + .iter() + .enumerate() + { + let slice = volume + .next() + .unwrap_or_else(|| panic!("{}st slice should exist", i)) + .expect("should not fail to construct the volume"); + + assert_eq!(slice.dim(), volume.slice_dim()); + assert_eq!(slice.data_type(), NiftiType::Uint16); + assert_eq!(slice.raw_data(), &raw_data[..]); + assert_eq!(slice.get_u16(&[0]).unwrap(), u16::from(raw_data[0])); + assert_eq!(slice.get_u16(&[1]).unwrap(), u16::from(raw_data[2])); + } + + assert!(volume.next().is_none()); + } +} diff --git a/src/volume/util.rs b/src/volume/util.rs index 7c464d2..50c1cbe 100644 --- a/src/volume/util.rs +++ b/src/volume/util.rs @@ -26,7 +26,8 @@ pub fn coords_to_index(coords: &[u16], dim: &[u16]) -> Result { let mut crds = coords.into_iter(); let start = *crds.next_back().unwrap() as usize; - let index = crds.zip(dim) + let index = crds + .zip(dim) .rev() .fold(start, |a, b| a * *b.1 as usize + *b.0 as usize); diff --git a/src/writer.rs b/src/writer.rs index 9f404df..3f71244 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -295,7 +295,7 @@ where fn write_data(mut writer: ByteOrdered, data: ArrayBase) -> Result<()> where S: Data, - A: Clone + TriviallyTransmutable, + A: TriviallyTransmutable, D: Dimension + RemoveAxis, W: Write, E: Endian + Copy, diff --git a/tests/header.rs b/tests/header.rs index 2274da2..d65870f 100644 --- a/tests/header.rs +++ b/tests/header.rs @@ -81,7 +81,7 @@ fn minimal_nii() { const FILE_NAME: &str = "resources/minimal.nii"; let file = File::open(FILE_NAME).unwrap(); - let header = NiftiHeader::from_stream(file).unwrap(); + let header = NiftiHeader::from_reader(file).unwrap(); assert_eq!(header, minimal_hdr); assert_eq!(header.endianness, Endianness::Big); diff --git a/tests/object.rs b/tests/object.rs index a42b56b..c28dded 100644 --- a/tests/object.rs +++ b/tests/object.rs @@ -5,7 +5,10 @@ extern crate nifti; #[macro_use] extern crate pretty_assertions; -use nifti::{Endianness, InMemNiftiObject, NiftiHeader, NiftiObject, NiftiType, NiftiVolume, XForm}; +use nifti::{ + Endianness, InMemNiftiObject, StreamedNiftiObject, NiftiHeader, NiftiObject, NiftiType, + NiftiVolume, RandomAccessNiftiVolume, XForm, +}; #[test] fn minimal_nii_gz() { @@ -31,6 +34,30 @@ fn minimal_nii_gz() { assert_eq!(volume.dim(), [64, 64, 10].as_ref()); } +#[test] +fn streamed_minimal_nii_gz() { + let minimal_hdr = NiftiHeader { + sizeof_hdr: 348, + dim: [3, 64, 64, 10, 0, 0, 0, 0], + datatype: 2, + bitpix: 8, + pixdim: [0., 3., 3., 3., 0., 0., 0., 0.], + vox_offset: 352., + scl_slope: 0., + scl_inter: 0., + magic: *b"n+1\0", + endianness: Endianness::Big, + ..Default::default() + }; + + const FILE_NAME: &str = "resources/minimal.nii.gz"; + let obj = StreamedNiftiObject::from_file(FILE_NAME).unwrap(); + assert_eq!(obj.header(), &minimal_hdr); + let volume = obj.volume(); + assert_eq!(volume.data_type(), NiftiType::Uint8); + assert_eq!(volume.dim(), [64, 64, 10].as_ref()); +} + #[test] fn minimal_nii() { let minimal_hdr = NiftiHeader { @@ -55,6 +82,30 @@ fn minimal_nii() { assert_eq!(volume.dim(), [64, 64, 10].as_ref()); } +#[test] +fn streamed_minimal_nii() { + let minimal_hdr = NiftiHeader { + sizeof_hdr: 348, + dim: [3, 64, 64, 10, 0, 0, 0, 0], + datatype: 2, + bitpix: 8, + pixdim: [0., 3., 3., 3., 0., 0., 0., 0.], + vox_offset: 352., + scl_slope: 0., + scl_inter: 0., + magic: *b"n+1\0", + endianness: Endianness::Big, + ..Default::default() + }; + + const FILE_NAME: &str = "resources/minimal.nii"; + let obj = StreamedNiftiObject::from_file(FILE_NAME).unwrap(); + assert_eq!(obj.header(), &minimal_hdr); + let volume = obj.volume(); + assert_eq!(volume.data_type(), NiftiType::Uint8); + assert_eq!(volume.dim(), [64, 64, 10].as_ref()); +} + #[test] fn minimal_by_hdr() { let minimal_hdr = NiftiHeader { @@ -79,6 +130,30 @@ fn minimal_by_hdr() { assert_eq!(volume.dim(), [64, 64, 10].as_ref()); } +#[test] +fn streamed_minimal_by_hdr() { + let minimal_hdr = NiftiHeader { + sizeof_hdr: 348, + dim: [3, 64, 64, 10, 0, 0, 0, 0], + datatype: 2, + bitpix: 8, + pixdim: [0., 3., 3., 3., 0., 0., 0., 0.], + vox_offset: 0., + scl_slope: 0., + scl_inter: 0., + magic: *b"ni1\0", + endianness: Endianness::Big, + ..Default::default() + }; + + const FILE_NAME: &str = "resources/minimal.hdr"; + let obj = StreamedNiftiObject::from_file(FILE_NAME).unwrap(); + assert_eq!(obj.header(), &minimal_hdr); + let volume = obj.volume(); + assert_eq!(volume.data_type(), NiftiType::Uint8); + assert_eq!(volume.dim(), [64, 64, 10].as_ref()); +} + #[test] fn minimal_by_hdr_and_img_gz() { let minimal_hdr = NiftiHeader { @@ -191,6 +266,54 @@ fn f32_nii_gz() { assert_eq!(volume.get_f32(&[0, 8, 5]).unwrap(), 0.8); } +#[test] +fn streamed_f32_nii_gz() { + let f32_hdr = NiftiHeader { + sizeof_hdr: 348, + dim: [3, 11, 11, 11, 1, 1, 1, 1], + datatype: 16, + bitpix: 32, + pixdim: [1., 1., 1., 1., 1., 1., 1., 1.], + vox_offset: 352., + scl_slope: 1., + scl_inter: 0., + srow_x: [1., 0., 0., 0.], + srow_y: [0., 1., 0., 0.], + srow_z: [0., 0., 1., 0.], + sform_code: 2, + magic: *b"n+1\0", + endianness: Endianness::Little, + ..Default::default() + }; + + const FILE_NAME: &str = "resources/f32.nii.gz"; + let obj = StreamedNiftiObject::from_file(FILE_NAME).unwrap(); + assert_eq!(obj.header(), &f32_hdr); + + assert_eq!(obj.header().sform().unwrap(), XForm::AlignedAnat); + + let volume = obj.into_volume(); + assert_eq!(volume.data_type(), NiftiType::Float32); + assert_eq!(volume.dim(), [11, 11, 11].as_ref()); + assert_eq!(volume.slice_dim(), &[11, 11]); + + let slices: Vec<_> = volume + .map(|r| r.expect("slice construction should not fail")) + .collect(); + + for slice in &slices { + assert_eq!(slice.data_type(), NiftiType::Float32); + assert_eq!(slice.dim(), &[11, 11]); + } + + assert_eq!(slices[5].get_f32(&[5, 5]).unwrap(), 1.); + assert_eq!(slices[0].get_f32(&[5, 0]).unwrap(), 0.); + assert_eq!(slices[0].get_f32(&[0, 5]).unwrap(), 0.); + assert_eq!(slices[5].get_f32(&[0, 0]).unwrap(), 0.); + assert_eq!(slices[4].get_f32(&[5, 0]).unwrap(), 0.4); + assert_eq!(slices[5].get_f32(&[0, 8]).unwrap(), 0.8); +} + #[test] fn bad_file_1() { let _ = InMemNiftiObject::from_file("resources/fuzz_artifacts/crash-1.nii"); diff --git a/tests/volume.rs b/tests/volume.rs index 0a7f488..f5e0ca0 100644 --- a/tests/volume.rs +++ b/tests/volume.rs @@ -13,7 +13,7 @@ extern crate num_traits; #[cfg(feature = "ndarray_volumes")] extern crate safe_transmute; -use nifti::{Endianness, InMemNiftiVolume, NiftiHeader, NiftiVolume}; +use nifti::{Endianness, InMemNiftiVolume, NiftiHeader, NiftiVolume, RandomAccessNiftiVolume}; #[test] fn minimal_img_gz() { @@ -43,8 +43,7 @@ fn minimal_img_gz() { let coords = [i, j, k]; let got_value = volume.get_f32(&coords).unwrap(); assert_eq!( - expected_value, - got_value, + expected_value, got_value, "bad value at coords {:?}", &coords ); @@ -57,8 +56,8 @@ fn minimal_img_gz() { mod ndarray_volumes { use std::fmt; use std::ops::{Add, Mul}; - use nifti::{DataElement, Endianness, InMemNiftiObject, InMemNiftiVolume, - NiftiHeader, NiftiObject, NiftiVolume, NiftiType, IntoNdArray}; + use nifti::{DataElement, Endianness, InMemNiftiObject, InMemNiftiVolume, IntoNdArray, + NiftiHeader, NiftiObject, NiftiType, NiftiVolume, StreamedNiftiObject}; use ndarray::{Array, Axis, IxDyn, ShapeBuilder}; use num_traits::AsPrimitive; @@ -182,6 +181,33 @@ mod ndarray_volumes { assert_ulps_eq!(volume[[5, 5, 5]], 1.0_f32 as f64); } + #[test] + fn streamed_f32_nii_gz_ndarray_f64() { + const FILE_NAME: &str = "resources/f32.nii.gz"; + let volume = StreamedNiftiObject::from_file(FILE_NAME) + .unwrap() + .into_volume(); + assert_eq!(volume.data_type(), NiftiType::Float32); + + let slices: Vec<_> = volume + .map(|r| r.expect("slice construction should not fail")) + .map(|slice| slice.into_ndarray::().unwrap()) + .collect(); + + for slice in &slices { + assert_eq!(slice.shape(), &[11, 11]); + } + assert!(slices[4].iter().any(|v| *v != 0.)); + assert!(slices[5].iter().any(|v| *v != 0.)); + + assert_ulps_eq!(slices[0][[5, 0]], 0.0); + assert_ulps_eq!(slices[0][[0, 5]], 0.0); + assert_ulps_eq!(slices[5][[0, 0]], 0.0); + assert_ulps_eq!(slices[4][[5, 0]], 0.4_f32 as f64); + assert_ulps_eq!(slices[5][[0, 8]], 0.8_f32 as f64); + assert_ulps_eq!(slices[5][[5, 5]], 1.0_f32 as f64); + } + #[test] fn test_i8() { const FILE_NAME: &str = "resources/27/int8.nii"; @@ -242,8 +268,7 @@ mod ndarray_volumes { test_all(FILE_NAME, NiftiType::Float64); } - fn test_all(path: &str, dtype: NiftiType) - { + fn test_all(path: &str, dtype: NiftiType) { test_types::(path, dtype); test_types::(path, dtype); test_types::(path, dtype); @@ -257,23 +282,23 @@ mod ndarray_volumes { } fn test_types(path: &str, dtype: NiftiType) - where - T: fmt::Debug, - T: Add, - T: Mul, - T: DataElement, - T: PartialEq, - u8: AsPrimitive, - i8: AsPrimitive, - u16: AsPrimitive, - i16: AsPrimitive, - u32: AsPrimitive, - i32: AsPrimitive, - u64: AsPrimitive, - i64: AsPrimitive, - f32: AsPrimitive, - f64: AsPrimitive, - usize: AsPrimitive, + where + T: fmt::Debug, + T: Add, + T: Mul, + T: DataElement, + T: PartialEq, + u8: AsPrimitive, + i8: AsPrimitive, + u16: AsPrimitive, + i16: AsPrimitive, + u32: AsPrimitive, + i32: AsPrimitive, + u64: AsPrimitive, + i64: AsPrimitive, + f32: AsPrimitive, + f64: AsPrimitive, + usize: AsPrimitive, { let volume = InMemNiftiObject::from_file(path) .expect("Can't read input file.")