use crate::error::{IoError, IoResult};
use crate::ismrmrd::header::IsmrmrdHeader;
use hdf5_metno::{File, H5Type};
use ndarray::{Array2, Array3, Array4};
use num_complex::Complex32;
use std::path::Path;
use tracing::info;
#[derive(Debug, Clone, Copy, H5Type)]
#[repr(C)]
struct Cf32 {
r: f32,
i: f32,
}
impl From<Cf32> for Complex32 {
#[inline]
fn from(c: Cf32) -> Self {
Complex32::new(c.r, c.i)
}
}
fn read_complex_flat(file: &File, path: &str) -> IoResult<(Vec<Complex32>, Vec<usize>)> {
let ds = file.dataset(path)?;
let shape = ds.shape();
let raw: Vec<Cf32> = ds.read_raw()?;
let data: Vec<Complex32> = raw.into_iter().map(Into::into).collect();
Ok((data, shape))
}
fn read_f32_flat(file: &File, path: &str) -> IoResult<(Vec<f32>, Vec<usize>)> {
let ds = file.dataset(path)?;
let shape = ds.shape();
let data: Vec<f32> = ds.read_raw()?;
Ok((data, shape))
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct FastmriMeta {
pub header: IsmrmrdHeader,
pub acquisition: String,
pub patient_id: String,
pub n_slices: usize,
pub n_coils: usize,
pub n_ky: usize,
pub n_kx: usize,
pub recon_y: usize,
pub recon_x: usize,
}
pub struct FastmriFile {
file: File,
pub meta: FastmriMeta,
}
impl FastmriFile {
pub fn open<P: AsRef<Path>>(path: P) -> IoResult<Self> {
let path = path.as_ref();
let file = File::open(path)?;
let xml_ds = file.dataset("ismrmrd_header")?;
let xml_str = xml_ds
.read_scalar::<hdf5_metno::types::VarLenUnicode>()
.map(|s| s.as_str().to_string())
.or_else(|_| {
xml_ds
.read_scalar::<hdf5_metno::types::VarLenAscii>()
.map(|s| s.as_str().to_string())
})
.map_err(|_| IoError::MissingField("ismrmrd_header"))?;
let header = IsmrmrdHeader::parse(&xml_str)?;
let kshape = file.dataset("kspace")?.shape();
if kshape.len() != 4 {
return Err(IoError::Unsupported(format!(
"kspace has {} dims (expected 4: [slices, coils, ky, kx])",
kshape.len()
)));
}
let (n_slices, n_coils, n_ky, n_kx) = (kshape[0], kshape[1], kshape[2], kshape[3]);
let rshape = file.dataset("reconstruction_rss")?.shape();
if rshape.len() != 3 {
return Err(IoError::Unsupported(format!(
"reconstruction_rss has {} dims (expected 3: [slices, y, x])",
rshape.len()
)));
}
if rshape[0] != n_slices {
return Err(IoError::Inconsistent(format!(
"kspace has {} slices but reconstruction_rss has {}",
n_slices, rshape[0]
)));
}
let (recon_y, recon_x) = (rshape[1], rshape[2]);
let attr_str = |key: &str| -> String {
file.attr(key)
.and_then(|a| a.read_scalar::<hdf5_metno::types::VarLenUnicode>())
.map(|s| s.as_str().to_string())
.unwrap_or_default()
};
let acquisition = attr_str("acquisition");
let patient_id = attr_str("patient_id");
info!(
"Opened {} -- {} slices, {} coils, ky={}, kx={}, recon={}x{}, acq={:?}",
path.display(),
n_slices,
n_coils,
n_ky,
n_kx,
recon_y,
recon_x,
acquisition,
);
Ok(FastmriFile {
file,
meta: FastmriMeta {
header,
acquisition,
patient_id,
n_slices,
n_coils,
n_ky,
n_kx,
recon_y,
recon_x,
},
})
}
pub fn read_kspace(&self) -> IoResult<Array4<Complex32>> {
let m = &self.meta;
let (data, _) = read_complex_flat(&self.file, "kspace")?;
let (ns, nc, nky, nkx) = (m.n_slices, m.n_coils, m.n_ky, m.n_kx);
let coil_stride = nky * nkx;
let slice_stride = nc * coil_stride;
let mut out = Array4::<Complex32>::zeros((nc, ns, nky, nkx));
for s in 0..ns {
for c in 0..nc {
let src_off = s * slice_stride + c * coil_stride;
out.slice_mut(ndarray::s![c, s, .., ..])
.iter_mut()
.zip(&data[src_off..src_off + coil_stride])
.for_each(|(dst, src)| *dst = *src);
}
}
Ok(out)
}
pub fn read_kspace_slice(&self, slice_idx: usize) -> IoResult<Array3<Complex32>> {
let m = &self.meta;
if slice_idx >= m.n_slices {
return Err(IoError::Inconsistent(format!(
"slice {} out of range (file has {} slices)",
slice_idx, m.n_slices
)));
}
let (data, _) = read_complex_flat(&self.file, "kspace")?;
let coil_stride = m.n_ky * m.n_kx;
let slice_stride = m.n_coils * coil_stride;
let slice_base = slice_idx * slice_stride;
let mut out = Array3::<Complex32>::zeros((m.n_coils, m.n_ky, m.n_kx));
for c in 0..m.n_coils {
let src_off = slice_base + c * coil_stride;
out.slice_mut(ndarray::s![c, .., ..])
.iter_mut()
.zip(&data[src_off..src_off + coil_stride])
.for_each(|(dst, src)| *dst = *src);
}
Ok(out)
}
pub fn read_reconstruction_rss(&self) -> IoResult<Array3<f32>> {
let m = &self.meta;
let (data, _) = read_f32_flat(&self.file, "reconstruction_rss")?;
Array3::from_shape_vec((m.n_slices, m.recon_y, m.recon_x), data)
.map_err(|e| IoError::Inconsistent(e.to_string()))
}
pub fn read_reconstruction_rss_slice(&self, slice_idx: usize) -> IoResult<Array2<f32>> {
let m = &self.meta;
if slice_idx >= m.n_slices {
return Err(IoError::Inconsistent(format!(
"slice {} out of range (file has {} slices)",
slice_idx, m.n_slices
)));
}
let (data, _) = read_f32_flat(&self.file, "reconstruction_rss")?;
let stride = m.recon_y * m.recon_x;
let off = slice_idx * stride;
Array2::from_shape_vec((m.recon_y, m.recon_x), data[off..off + stride].to_vec())
.map_err(|e| IoError::Inconsistent(e.to_string()))
}
pub fn is_brain(&self) -> bool {
self.meta.acquisition.to_ascii_uppercase().contains("AX")
}
}