pub mod header;
mod import;
mod qform;
mod read;
mod write;
use ndarray::Array2;
use serde_json::{json, Value};
pub use header::{Element, Header, Version, VoxOffset};
pub use import::{from_nifti, grid_axes, import_seg_nifti, read_nifti, read_stored, StoredNifti};
pub use read::Voxels;
pub use write::{
embed_plane, for_export, nifti_target, sidecar_path, to_nifti, write_field, write_nifti, ExportOptions,
WriteOptions, NIFTI_SUFFIXES,
};
use crate::array::NdArray;
use crate::json::{num, repr_str};
use crate::labels::LabelSet;
use crate::sample::writer::Annotated;
use crate::{Error, Result};
pub const COORD_SYSTEMS: [&str; 2] = crate::geometry::grid::COORD_SYSTEMS;
pub const FOURTH_AXES: [&str; 3] = ["auto", "time", "channel"];
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub struct ReadOptions {
pub coord_system: String,
pub transpose: bool,
pub fourth_axis: String,
pub assume_geometry: bool,
pub plane_as_volume: bool,
}
impl Default for ReadOptions {
fn default() -> Self {
ReadOptions {
coord_system: "LPS".into(),
transpose: true,
fourth_axis: "auto".into(),
assume_geometry: false,
plane_as_volume: false,
}
}
}
impl ReadOptions {
pub fn with_coord_system(mut self, value: impl Into<String>) -> Self {
self.coord_system = value.into();
self
}
pub fn with_transpose(mut self, value: bool) -> Self {
self.transpose = value;
self
}
pub fn with_fourth_axis(mut self, value: impl Into<String>) -> Self {
self.fourth_axis = value.into();
self
}
pub fn with_assume_geometry(mut self, value: bool) -> Self {
self.assume_geometry = value;
self
}
pub fn with_plane_as_volume(mut self, value: bool) -> Self {
self.plane_as_volume = value;
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct ImportOptions {
pub masks: Vec<(String, std::path::PathBuf)>,
pub sample_id: Option<String>,
pub subject_id: Option<String>,
pub modalities: Vec<(String, String)>,
pub coord_system: String,
pub transpose: bool,
pub fourth_axis: String,
pub value_units: Vec<(String, String)>,
pub codec: String,
pub annotated_classes: Annotated,
pub assume_geometry: bool,
pub plane_as_volume: bool,
pub label_set: Option<LabelSet>,
}
impl Default for ImportOptions {
fn default() -> Self {
let read = ReadOptions::default();
ImportOptions {
masks: Vec::new(),
sample_id: None,
subject_id: None,
modalities: Vec::new(),
coord_system: read.coord_system,
transpose: read.transpose,
fourth_axis: read.fourth_axis,
value_units: Vec::new(),
codec: "balanced".into(),
annotated_classes: Annotated::AllGiven,
assume_geometry: false,
plane_as_volume: false,
label_set: None,
}
}
}
impl ImportOptions {
pub fn with_masks(mut self, masks: Vec<(String, std::path::PathBuf)>) -> Self {
self.masks = masks;
self
}
pub fn with_sample_id(mut self, value: impl Into<String>) -> Self {
self.sample_id = Some(value.into());
self
}
pub fn with_subject_id(mut self, value: impl Into<String>) -> Self {
self.subject_id = Some(value.into());
self
}
pub fn with_modalities(mut self, value: Vec<(String, String)>) -> Self {
self.modalities = value;
self
}
pub fn with_coord_system(mut self, value: impl Into<String>) -> Self {
self.coord_system = value.into();
self
}
pub fn with_transpose(mut self, value: bool) -> Self {
self.transpose = value;
self
}
pub fn with_fourth_axis(mut self, value: impl Into<String>) -> Self {
self.fourth_axis = value.into();
self
}
pub fn with_value_units(mut self, value: Vec<(String, String)>) -> Self {
self.value_units = value;
self
}
pub fn with_codec(mut self, value: impl Into<String>) -> Self {
self.codec = value.into();
self
}
pub fn with_annotated_classes(mut self, value: Annotated) -> Self {
self.annotated_classes = value;
self
}
pub fn with_assume_geometry(mut self, value: bool) -> Self {
self.assume_geometry = value;
self
}
pub fn with_plane_as_volume(mut self, value: bool) -> Self {
self.plane_as_volume = value;
self
}
pub fn with_label_set(mut self, value: LabelSet) -> Self {
self.label_set = Some(value);
self
}
fn read(&self) -> ReadOptions {
ReadOptions {
coord_system: self.coord_system.clone(),
transpose: self.transpose,
fourth_axis: self.fourth_axis.clone(),
assume_geometry: self.assume_geometry,
plane_as_volume: self.plane_as_volume,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct SegOptions {
pub ann_id: String,
pub grid: Option<String>,
pub coord_system: Option<String>,
pub transpose: bool,
pub annotated_classes: Annotated,
pub assume_aligned: bool,
pub assume_geometry: bool,
pub plane_as_volume: bool,
}
impl Default for SegOptions {
fn default() -> Self {
SegOptions {
ann_id: "seg".into(),
grid: None,
coord_system: None,
transpose: true,
annotated_classes: Annotated::AllGiven,
assume_aligned: false,
assume_geometry: false,
plane_as_volume: false,
}
}
}
impl SegOptions {
pub fn with_ann_id(mut self, value: impl Into<String>) -> Self {
self.ann_id = value.into();
self
}
pub fn with_grid(mut self, value: impl Into<String>) -> Self {
self.grid = Some(value.into());
self
}
pub fn with_coord_system(mut self, value: impl Into<String>) -> Self {
self.coord_system = Some(value.into());
self
}
pub fn with_transpose(mut self, value: bool) -> Self {
self.transpose = value;
self
}
pub fn with_annotated_classes(mut self, value: Annotated) -> Self {
self.annotated_classes = value;
self
}
pub fn with_assume_aligned(mut self, value: bool) -> Self {
self.assume_aligned = value;
self
}
pub fn with_assume_geometry(mut self, value: bool) -> Self {
self.assume_geometry = value;
self
}
pub fn with_plane_as_volume(mut self, value: bool) -> Self {
self.plane_as_volume = value;
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub struct GeometryNote {
pub kind: String,
pub message: String,
pub detail: Value,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub struct Plane {
pub z: f64,
pub normal: Vec<f64>,
pub thickness: f64,
pub units: String,
}
impl Plane {
fn to_json(&self) -> Value {
json!({
"z": num(self.z),
"normal": self.normal.iter().map(|v| num(*v)).collect::<Vec<_>>(),
"thickness": num(self.thickness),
"units": self.units,
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub struct Geometry {
pub notes: Vec<GeometryNote>,
pub spacing: Vec<f64>,
pub origin: Vec<f64>,
pub direction: Vec<Vec<f64>>,
pub coord_system: String,
pub shape: Vec<usize>,
pub axis_order: Vec<usize>,
pub time_values: Option<Vec<f64>>,
pub time_units: Option<String>,
pub time_measured: bool,
pub leading_kind: String,
pub leading_stated: bool,
pub b_values: Option<Vec<f64>>,
pub channel_field: Option<String>,
pub channel_values: Option<Vec<f64>>,
pub stated_by: Option<String>,
pub squeezed: Vec<usize>,
pub units: String,
pub dtype: String,
pub rescale: Option<(f64, f64)>,
pub plane_as_volume: Option<Plane>,
pub descrip: String,
}
fn floats(values: &[f64]) -> Value {
Value::Array(values.iter().map(|v| num(*v)).collect())
}
impl Geometry {
pub fn to_json(&self) -> Value {
let opt = |v: &Option<Vec<f64>>| v.as_deref().map_or(Value::Null, floats);
json!({
"notes": self.notes.iter().map(|n| json!([n.kind, n.message, n.detail])).collect::<Vec<_>>(),
"spacing": floats(&self.spacing),
"origin": floats(&self.origin),
"direction": self.direction.iter().map(|r| floats(r)).collect::<Vec<_>>(),
"coord_system": self.coord_system,
"shape": self.shape,
"axis_order": self.axis_order,
"time_values": opt(&self.time_values),
"time_units": self.time_units,
"time_measured": self.time_measured,
"leading_kind": self.leading_kind,
"leading_stated": self.leading_stated,
"b_values": opt(&self.b_values),
"channel_field": self.channel_field,
"channel_values": opt(&self.channel_values),
"stated_by": self.stated_by,
"squeezed": self.squeezed,
"units": self.units,
"dtype": self.dtype,
"rescale": self.rescale.map_or(Value::Null, |(s, i)| json!([num(s), num(i)])),
"plane_as_volume": self.plane_as_volume.as_ref().map_or(Value::Null, Plane::to_json),
"header": {"descrip": self.descrip, "scl_slope": null, "scl_inter": null},
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub struct NiftiRead {
pub voxels: Voxels,
pub geometry: Geometry,
}
impl Voxels {
fn swapped(&self) -> bool {
self.element.has_byte_order() && self.big_endian != header::NATIVE_BIG
}
pub fn numpy_str(&self) -> String {
if self.swapped() {
format!("{}{}", if self.big_endian { '>' } else { '<' }, self.element.numpy_code())
} else {
self.element.numpy_name().to_string()
}
}
pub fn numpy_descr(&self) -> String {
let order = if !self.element.has_byte_order() {
'|'
} else if self.big_endian {
'>'
} else {
'<'
};
format!("{order}{}", self.element.numpy_code())
}
fn units<const N: usize>(&self) -> impl Iterator<Item = [u8; N]> + '_ {
let swap = self.swapped();
self.bytes.chunks_exact(N).map(move |c| {
let mut unit: [u8; N] = c.try_into().expect("a whole unit");
if swap {
unit.reverse();
}
unit
})
}
pub fn into_ndarray(self) -> Result<NdArray> {
use header::Element as E;
let shape = ndarray::IxDyn(&self.shape);
macro_rules! typed {
($t:ty, $variant:ident) => {{
let values: Vec<$t> = self.units().map(<$t>::from_ne_bytes).collect();
NdArray::$variant(ndarray::ArrayD::from_shape_vec(shape, values)?)
}};
}
Ok(match self.element {
E::U8 => NdArray::U8(ndarray::ArrayD::from_shape_vec(shape, self.bytes)?),
E::I8 => typed!(i8, I8),
E::I16 => typed!(i16, I16),
E::U16 => typed!(u16, U16),
E::I32 => typed!(i32, I32),
E::U32 => typed!(u32, U32),
E::I64 => typed!(i64, I64),
E::U64 => typed!(u64, U64),
E::F32 => typed!(f32, F32),
E::F64 => typed!(f64, F64),
E::C64 | E::C128 | E::Rgb24 | E::Rgba32 => {
return Err(Error::Type(format!(
"arrays of dtype {} cannot be stored; use a boolean, integer or floating dtype",
self.numpy_str()
)))
}
})
}
fn parts(&self) -> Box<dyn Iterator<Item = (f64, f64)> + '_> {
use header::Element as E;
let real = |v: f64| (v, 0.0);
match self.element {
E::U8 => Box::new(self.bytes.iter().map(move |b| real(f64::from(*b)))),
E::I8 => Box::new(self.bytes.iter().map(move |b| real(f64::from(*b as i8)))),
E::I16 => Box::new(self.units().map(move |u| real(f64::from(i16::from_ne_bytes(u))))),
E::U16 => Box::new(self.units().map(move |u| real(f64::from(u16::from_ne_bytes(u))))),
E::I32 => Box::new(self.units().map(move |u| real(f64::from(i32::from_ne_bytes(u))))),
E::U32 => Box::new(self.units().map(move |u| real(f64::from(u32::from_ne_bytes(u))))),
E::I64 => Box::new(self.units().map(move |u| real(i64::from_ne_bytes(u) as f64))),
E::U64 => Box::new(self.units().map(move |u| real(u64::from_ne_bytes(u) as f64))),
E::F32 => Box::new(self.units().map(move |u| real(f64::from(f32::from_ne_bytes(u))))),
E::F64 => Box::new(self.units().map(move |u| real(f64::from_ne_bytes(u)))),
E::C64 => {
let mut it = self.units().map(|u| f64::from(f32::from_ne_bytes(u)));
Box::new(std::iter::from_fn(move || Some((it.next()?, it.next()?))))
}
E::C128 => {
let mut it = self.units().map(f64::from_ne_bytes);
Box::new(std::iter::from_fn(move || Some((it.next()?, it.next()?))))
}
E::Rgb24 | E::Rgba32 => Box::new(
self.bytes
.chunks_exact(self.element.itemsize())
.map(move |c| real(f64::from(u8::from(c.iter().any(|b| *b != 0))))),
),
}
}
pub fn nonzero(&self, rescale: Option<(f64, f64)>) -> Result<ndarray::ArrayD<bool>> {
let set: Vec<bool> = match rescale {
None => self.parts().map(|(re, im)| re != 0.0 || im != 0.0).collect(),
Some((slope, inter)) => {
self.parts().map(|(re, im)| re * slope + inter != 0.0 || im * slope != 0.0).collect()
}
};
Ok(ndarray::ArrayD::from_shape_vec(ndarray::IxDyn(&self.shape), set)?)
}
}
pub fn convert_world(affine: &Array2<f64>, source: &str, target: &str) -> Result<Array2<f64>> {
for name in [source, target] {
if !COORD_SYSTEMS.contains(&name) {
return Err(Error::invalid(format!(
"unknown coordinate system {}; expected one of {}",
repr_str(name),
crate::json::repr_list(&COORD_SYSTEMS)
)));
}
}
if source == target {
return Ok(affine.clone());
}
let (rows, cols) = affine.dim();
if !((rows, cols) == (3, 3) || (rows, cols) == (4, 4)) {
return Err(Error::invalid(format!(
"RAS↔LPS conversion is defined for 2-D and 3-D affines; got ({rows}, {cols})"
)));
}
let flip: Vec<f64> = (0..rows).map(|r| if r < 2 { -1.0 } else { 1.0 }).collect();
let mut out = Array2::zeros((rows, cols));
for r in 0..rows {
for c in 0..cols {
let mut acc = 0.0;
for (k, sign) in flip.iter().enumerate() {
let d = if k == r { *sign } else { 0.0 };
acc += d * affine[[k, c]];
}
out[[r, c]] = acc;
}
}
Ok(out)
}
pub(crate) fn float_list(values: &[f64]) -> String {
let items: Vec<String> = values.iter().map(|v| crate::json::py_float(*v)).collect();
format!("[{}]", items.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
fn voxels(element: Element, big_endian: bool, bytes: Vec<u8>) -> Voxels {
let n = bytes.len() / element.itemsize();
Voxels { element, big_endian, shape: vec![n], bytes }
}
#[test]
fn s02_5_voxels_decode_in_either_byte_order() {
let values = [1i16, -2, 300];
for big in [false, true] {
let bytes = values.iter().flat_map(|v| if big { v.to_be_bytes() } else { v.to_le_bytes() }).collect();
match voxels(Element::I16, big, bytes).into_ndarray().unwrap() {
NdArray::I16(a) => assert_eq!(a.as_slice().unwrap(), &values),
other => panic!("{other:?}"),
}
}
}
#[test]
fn s02_5_a_scaled_complex_mask_keeps_its_imaginary_voxels() {
let parts = [0f32, 0.0, 0.0, 1.0, 1.0, 0.0];
for big in [false, true] {
let bytes = parts.iter().flat_map(|v| if big { v.to_be_bytes() } else { v.to_le_bytes() }).collect();
let v = voxels(Element::C64, big, bytes);
for rescale in [None, Some((2.0, 0.0))] {
assert_eq!(v.nonzero(rescale).unwrap().as_slice().unwrap(), &[false, true, true], "{rescale:?}");
}
assert_eq!(v.nonzero(Some((1.0, -1.0))).unwrap().as_slice().unwrap(), &[true, true, false]);
}
}
}