use crate::coil::rss_combine_4d;
use crate::crop::center_crop_3d;
use crate::fft::{ifft2_inplace, ifft3_inplace};
use crate::oversampling::OversamplingRemover;
use crate::partial_fourier::{homodyne_reconstruct, PartialFourierPlan};
use crate::phasecorr::PhaseCorrector;
use crate::prewhiten::NoisePrewhitener;
use ndarray::Array3;
use openkspace_io::error::{IoError, IoResult};
use openkspace_io::ismrmrd::IsmrmrdFile;
use tracing::info;
#[derive(Debug)]
pub struct ImageVolume {
pub data: Array3<f32>,
pub strategy: &'static str,
pub gfactor: Option<Array3<f32>>,
}
impl ImageVolume {
pub fn new(data: Array3<f32>, strategy: &'static str) -> Self {
Self {
data,
strategy,
gfactor: None,
}
}
}
pub trait ReconStrategy {
fn name(&self) -> &'static str;
fn reconstruct(&self, file: &IsmrmrdFile) -> IoResult<ImageVolume>;
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FftMode {
Auto,
TwoD,
ThreeD,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub struct IfftRss {
pub remove_oversampling: bool,
pub prewhiten: bool,
pub phase_correct: bool,
pub partial_fourier: bool,
pub fft_mode: FftMode,
pub crop_to_recon_matrix: bool,
}
impl Default for IfftRss {
fn default() -> Self {
Self {
remove_oversampling: true,
prewhiten: true,
phase_correct: true,
partial_fourier: true,
fft_mode: FftMode::Auto,
crop_to_recon_matrix: true,
}
}
}
impl ReconStrategy for IfftRss {
fn name(&self) -> &'static str {
"ifft-rss"
}
fn reconstruct(&self, file: &IsmrmrdFile) -> IoResult<ImageVolume> {
let (whitener, phase_corr) = if self.prewhiten || self.phase_correct {
let cal = file.read_calibration()?;
let w = if self.prewhiten {
NoisePrewhitener::from_noise_acqs(&cal.noise)
} else {
None
};
let pc = if self.phase_correct {
PhaseCorrector::from_phasecorr_acqs(&cal.phasecorr)
} else {
PhaseCorrector::default()
};
(w, pc)
} else {
(None, PhaseCorrector::default())
};
if whitener.is_some() {
info!("Noise pre-whitening: enabled");
}
if !phase_corr.is_empty() {
info!("Navigator phase correction: enabled");
}
let os_remover = if self.remove_oversampling {
let enc_x = file.header.encoding.encoded_matrix.x as usize;
let rec_x = file.header.encoding.recon_matrix.x as usize;
let r = OversamplingRemover::new(enc_x, rec_x);
if let Some(r) = &r {
r.log_summary();
}
r
} else {
None
};
let (mut kspace, mask) = file.read_kspace_with_mask(|acq| {
if let Some(w) = whitener.as_ref() {
w.apply(acq);
}
phase_corr.apply(acq);
if let Some(os) = os_remover.as_ref() {
os.apply(acq);
}
})?;
let three_d = match self.fft_mode {
FftMode::Auto => file.is_3d_encoding()?,
FftMode::TwoD => false,
FftMode::ThreeD => true,
};
if !three_d && self.partial_fourier {
let ky_dc = kspace.shape()[2] / 2;
if let Some(plan) = PartialFourierPlan::detect(&mask, ky_dc) {
let mut magnitude = homodyne_reconstruct(&kspace, &plan);
drop(kspace);
if self.crop_to_recon_matrix {
magnitude = self.maybe_crop(magnitude, file);
}
return Ok(ImageVolume {
data: magnitude,
strategy: self.name(),
gfactor: None,
});
}
}
info!(
"FFT mode: {}",
if three_d {
"3D (kz, ky, kx)"
} else {
"2D (ky, kx)"
}
);
if three_d {
info!("Running 3D IFFT on axes (kz=1, ky=2, kx=3)");
ifft3_inplace(&mut kspace, (1, 2, 3));
} else {
info!("Running 2D IFFT on axes (ky=2, kx=3) for all channels/slices");
ifft2_inplace(&mut kspace, (2, 3));
}
info!("RSS coil combine");
let mut magnitude = rss_combine_4d(&kspace);
drop(kspace);
if self.crop_to_recon_matrix {
magnitude = self.maybe_crop(magnitude, file);
}
Ok(ImageVolume {
data: magnitude,
strategy: self.name(),
gfactor: None,
})
}
}
impl IfftRss {
fn maybe_crop(&self, mut magnitude: Array3<f32>, file: &IsmrmrdFile) -> Array3<f32> {
let rm = &file.header.encoding.recon_matrix;
let (nz, ny, nx) = magnitude.dim();
let tz = if (rm.z as usize) >= 2 {
(rm.z as usize).min(nz)
} else {
nz
};
let ty = if rm.y as usize >= 1 {
(rm.y as usize).min(ny)
} else {
ny
};
let tx = if rm.x as usize >= 1 {
(rm.x as usize).min(nx)
} else {
nx
};
if (tz, ty, tx) != (nz, ny, nx) && tx <= nx && ty <= ny && tz <= nz {
info!(
"Cropping recon from {}x{}x{} -> {}x{}x{} (recon matrix)",
nz, ny, nx, tz, ty, tx
);
magnitude = center_crop_3d(&magnitude, (tz, ty, tx));
}
magnitude
}
}
use crate::grappa::{detect_pattern, extract_acs_slice, GrappaKernel};
use ndarray::s;
use tracing::warn;
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub struct GrappaRss {
pub remove_oversampling: bool,
pub prewhiten: bool,
pub phase_correct: bool,
pub kernel_ky: usize,
pub kernel_kx: usize,
pub ridge: f32,
pub fft_mode: FftMode,
pub crop_to_recon_matrix: bool,
}
impl Default for GrappaRss {
fn default() -> Self {
Self {
remove_oversampling: true,
prewhiten: true,
phase_correct: true,
kernel_ky: 4,
kernel_kx: 5,
ridge: 1e-3,
fft_mode: FftMode::Auto,
crop_to_recon_matrix: true,
}
}
}
impl ReconStrategy for GrappaRss {
fn name(&self) -> &'static str {
"grappa-rss"
}
fn reconstruct(&self, file: &IsmrmrdFile) -> IoResult<ImageVolume> {
let (whitener, phase_corr) = if self.prewhiten || self.phase_correct {
let cal = file.read_calibration()?;
let w = if self.prewhiten {
NoisePrewhitener::from_noise_acqs(&cal.noise)
} else {
None
};
let pc = if self.phase_correct {
PhaseCorrector::from_phasecorr_acqs(&cal.phasecorr)
} else {
PhaseCorrector::default()
};
(w, pc)
} else {
(None, PhaseCorrector::default())
};
let os_remover = if self.remove_oversampling {
let enc_x = file.header.encoding.encoded_matrix.x as usize;
let rec_x = file.header.encoding.recon_matrix.x as usize;
let r = OversamplingRemover::new(enc_x, rec_x);
if let Some(r) = &r {
r.log_summary();
}
r
} else {
None
};
let (mut kspace, mask) = file.read_kspace_with_mask(|acq| {
if let Some(w) = whitener.as_ref() {
w.apply(acq);
}
phase_corr.apply(acq);
if let Some(os) = os_remover.as_ref() {
os.apply(acq);
}
})?;
match detect_pattern(&mask) {
None => {
info!(
"GRAPPA: data appears fully sampled or pattern unsupported; \
skipping kernel synthesis"
);
}
Some(pattern) => {
info!(
"GRAPPA: R={}, ACS ky=[{}, {}) (length {})",
pattern.r,
pattern.acs_start,
pattern.acs_end,
pattern.acs_len()
);
let nz = kspace.shape()[1];
for kz in 0..nz {
let acs = extract_acs_slice(&kspace, kz, &pattern);
match GrappaKernel::calibrate(
acs.view(),
pattern.r,
self.kernel_ky,
self.kernel_kx,
self.ridge,
) {
Ok(kernel) => {
let mut slice_view = kspace.slice_mut(s![.., kz..=kz, .., ..]);
let mut slice_owned = slice_view.to_owned();
if let Err(e) = kernel.synthesize(&mut slice_owned) {
warn!("GRAPPA synthesize failed on slice {}: {}", kz, e);
} else {
slice_view.assign(&slice_owned);
}
}
Err(e) => {
warn!(
"GRAPPA calibration failed on slice {}: {} \
-- leaving this slice undersampled",
kz, e
);
}
}
}
}
}
let three_d = match self.fft_mode {
FftMode::Auto => file.is_3d_encoding()?,
FftMode::TwoD => false,
FftMode::ThreeD => true,
};
if three_d {
info!("Running 3D IFFT on axes (kz=1, ky=2, kx=3)");
ifft3_inplace(&mut kspace, (1, 2, 3));
} else {
info!("Running 2D IFFT on axes (ky=2, kx=3) for all channels/slices");
ifft2_inplace(&mut kspace, (2, 3));
}
info!("RSS coil combine");
let mut magnitude = rss_combine_4d(&kspace);
drop(kspace);
if self.crop_to_recon_matrix {
let rm = &file.header.encoding.recon_matrix;
let (nz, ny, nx) = magnitude.dim();
let tz = if (rm.z as usize) >= 2 {
(rm.z as usize).min(nz)
} else {
nz
};
let ty = if rm.y as usize >= 1 {
(rm.y as usize).min(ny)
} else {
ny
};
let tx = if rm.x as usize >= 1 {
(rm.x as usize).min(nx)
} else {
nx
};
if (tz, ty, tx) != (nz, ny, nx) && tx <= nx && ty <= ny && tz <= nz {
info!(
"Cropping recon from {}x{}x{} -> {}x{}x{} (recon matrix)",
nz, ny, nx, tz, ty, tx
);
magnitude = center_crop_3d(&magnitude, (tz, ty, tx));
}
}
Ok(ImageVolume {
data: magnitude,
strategy: self.name(),
gfactor: None,
})
}
}
use crate::espirit::espirit_sensitivity_maps;
use crate::sense::sense_unfold_1d;
use crate::sensitivity::walsh_sensitivity_maps;
use ndarray::{Array4, Axis};
use num_complex::Complex32;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SenseMapSource {
Walsh,
Espirit,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub struct SenseRss {
pub remove_oversampling: bool,
pub prewhiten: bool,
pub phase_correct: bool,
pub map_source: SenseMapSource,
pub walsh_window: usize,
pub walsh_iters: usize,
pub espirit_kernel: usize,
pub espirit_threshold: f32,
pub espirit_iters: usize,
pub ridge: f32,
pub compute_gfactor: bool,
pub fft_mode: FftMode,
pub crop_to_recon_matrix: bool,
}
impl Default for SenseRss {
fn default() -> Self {
Self {
remove_oversampling: true,
prewhiten: true,
phase_correct: true,
map_source: SenseMapSource::Walsh,
walsh_window: 3,
walsh_iters: 6,
espirit_kernel: 5,
espirit_threshold: 0.02,
espirit_iters: 30,
ridge: 1e-4,
compute_gfactor: false,
fft_mode: FftMode::Auto,
crop_to_recon_matrix: true,
}
}
}
impl ReconStrategy for SenseRss {
fn name(&self) -> &'static str {
"sense"
}
fn reconstruct(&self, file: &IsmrmrdFile) -> IoResult<ImageVolume> {
let (whitener, phase_corr) = if self.prewhiten || self.phase_correct {
let cal = file.read_calibration()?;
let w = if self.prewhiten {
NoisePrewhitener::from_noise_acqs(&cal.noise)
} else {
None
};
let pc = if self.phase_correct {
PhaseCorrector::from_phasecorr_acqs(&cal.phasecorr)
} else {
PhaseCorrector::default()
};
(w, pc)
} else {
(None, PhaseCorrector::default())
};
let os_remover = if self.remove_oversampling {
let enc_x = file.header.encoding.encoded_matrix.x as usize;
let rec_x = file.header.encoding.recon_matrix.x as usize;
let r = OversamplingRemover::new(enc_x, rec_x);
if let Some(r) = &r {
r.log_summary();
}
r
} else {
None
};
let (mut kspace, mask) = file.read_kspace_with_mask(|acq| {
if let Some(w) = whitener.as_ref() {
w.apply(acq);
}
phase_corr.apply(acq);
if let Some(os) = os_remover.as_ref() {
os.apply(acq);
}
})?;
let three_d = match self.fft_mode {
FftMode::Auto => file.is_3d_encoding()?,
FftMode::TwoD => false,
FftMode::ThreeD => true,
};
if three_d {
info!("SENSE: 3D encoding; decoupling kz via 1-D IFFT");
crate::fft::ifft1_inplace(&mut kspace, 1);
}
let pattern_opt = detect_pattern(&mask);
let Some(pattern) = pattern_opt else {
info!(
"SENSE: data appears fully sampled or pattern unsupported; \
falling back to plain IFFT+RSS"
);
ifft2_inplace(&mut kspace, (2, 3));
let mut magnitude = rss_combine_4d(&kspace);
drop(kspace);
if self.crop_to_recon_matrix {
magnitude = IfftRss::default().maybe_crop(magnitude, file);
}
return Ok(ImageVolume {
data: magnitude,
strategy: self.name(),
gfactor: None,
});
};
info!(
"SENSE: R={}, ACS ky=[{}, {}) (length {}), map_source={:?} ridge={:.1e}",
pattern.r,
pattern.acs_start,
pattern.acs_end,
pattern.acs_len(),
self.map_source,
self.ridge
);
let (nc, nz, ny, nx) = kspace.dim();
if ny % pattern.r != 0 {
warn!(
"SENSE: Ny={} not divisible by R={}; falling back to IFFT+RSS",
ny, pattern.r
);
ifft2_inplace(&mut kspace, (2, 3));
let mut magnitude = rss_combine_4d(&kspace);
drop(kspace);
if self.crop_to_recon_matrix {
magnitude = IfftRss::default().maybe_crop(magnitude, file);
}
return Ok(ImageVolume {
data: magnitude,
strategy: self.name(),
gfactor: None,
});
}
let mut output = Array3::<f32>::zeros((nz, ny, nx));
let mut gfactor_vol = if self.compute_gfactor {
Some(Array3::<f32>::zeros((nz, ny, nx)))
} else {
None
};
for kz in 0..nz {
let maps = match self.map_source {
SenseMapSource::Walsh => {
let mut acs_k = Array4::<Complex32>::zeros(kspace.raw_dim());
for c in 0..nc {
for ky in pattern.acs_start..pattern.acs_end {
for x in 0..nx {
acs_k[[c, kz, ky, x]] = kspace[[c, kz, ky, x]];
}
}
}
ifft2_inplace(&mut acs_k, (2, 3));
let low_res = acs_k.index_axis(Axis(1), kz).to_owned();
walsh_sensitivity_maps(&low_res, self.walsh_window, self.walsh_iters)
}
SenseMapSource::Espirit => {
let acs_len = pattern.acs_end - pattern.acs_start;
let mut acs_block = ndarray::Array3::<Complex32>::zeros((nc, acs_len, nx));
for c in 0..nc {
for (i, ky) in (pattern.acs_start..pattern.acs_end).enumerate() {
for x in 0..nx {
acs_block[[c, i, x]] = kspace[[c, kz, ky, x]];
}
}
}
espirit_sensitivity_maps(
&acs_block,
(ny, nx),
self.espirit_kernel,
self.espirit_threshold,
self.espirit_iters,
)
}
};
let mut aliased_k = kspace.slice(s![.., kz..=kz, .., ..]).to_owned();
ifft2_inplace(&mut aliased_k, (2, 3));
let aliased = aliased_k.index_axis(Axis(1), 0).to_owned();
let unfolded = sense_unfold_1d(&aliased, &maps, pattern.r, self.ridge)
.map_err(|e| IoError::Inconsistent(e.to_string()))?;
for y in 0..ny {
for x in 0..nx {
output[[kz, y, x]] = unfolded[[y, x]].norm();
}
}
if let Some(gv) = gfactor_vol.as_mut() {
let gslice = crate::sense::sense_gfactor_1d(&maps, pattern.r, self.ridge)
.map_err(|e| IoError::Inconsistent(e.to_string()))?;
for y in 0..ny {
for x in 0..nx {
gv[[kz, y, x]] = gslice[[y, x]];
}
}
}
}
drop(kspace);
let mut magnitude = output;
if self.crop_to_recon_matrix {
magnitude = IfftRss::default().maybe_crop(magnitude, file);
if let Some(gv) = gfactor_vol.as_mut() {
let cropped = IfftRss::default().maybe_crop(gv.clone(), file);
*gv = cropped;
}
}
Ok(ImageVolume {
data: magnitude,
strategy: self.name(),
gfactor: gfactor_vol,
})
}
}
use crate::cs::fista_cs_single_coil;
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub struct CsRss {
pub remove_oversampling: bool,
pub prewhiten: bool,
pub phase_correct: bool,
pub iters: usize,
pub lambda: f32,
pub fft_mode: FftMode,
pub crop_to_recon_matrix: bool,
}
impl Default for CsRss {
fn default() -> Self {
Self {
remove_oversampling: true,
prewhiten: true,
phase_correct: true,
iters: 60,
lambda: 0.01,
fft_mode: FftMode::Auto,
crop_to_recon_matrix: true,
}
}
}
impl ReconStrategy for CsRss {
fn name(&self) -> &'static str {
"cs"
}
fn reconstruct(&self, file: &IsmrmrdFile) -> IoResult<ImageVolume> {
let (whitener, phase_corr) = if self.prewhiten || self.phase_correct {
let cal = file.read_calibration()?;
let w = if self.prewhiten {
NoisePrewhitener::from_noise_acqs(&cal.noise)
} else {
None
};
let pc = if self.phase_correct {
PhaseCorrector::from_phasecorr_acqs(&cal.phasecorr)
} else {
PhaseCorrector::default()
};
(w, pc)
} else {
(None, PhaseCorrector::default())
};
let os_remover = if self.remove_oversampling {
let enc_x = file.header.encoding.encoded_matrix.x as usize;
let rec_x = file.header.encoding.recon_matrix.x as usize;
OversamplingRemover::new(enc_x, rec_x)
} else {
None
};
let (mut kspace, mask) = file.read_kspace_with_mask(|acq| {
if let Some(w) = whitener.as_ref() {
w.apply(acq);
}
phase_corr.apply(acq);
if let Some(os) = os_remover.as_ref() {
os.apply(acq);
}
})?;
let three_d = match self.fft_mode {
FftMode::Auto => file.is_3d_encoding()?,
FftMode::TwoD => false,
FftMode::ThreeD => true,
};
if three_d {
info!("CS: 3D encoding; decoupling kz via 1-D IFFT");
crate::fft::ifft1_inplace(&mut kspace, 1);
}
let (nc, nz, ny, nx) = kspace.dim();
if ny % 2 != 0 || nx % 2 != 0 {
info!("CS: odd spatial dims -- falling back to plain IFFT+RSS");
let mut k = kspace;
ifft2_inplace(&mut k, (2, 3));
let mut magnitude = rss_combine_4d(&k);
if self.crop_to_recon_matrix {
magnitude = IfftRss::default().maybe_crop(magnitude, file);
}
return Ok(ImageVolume {
data: magnitude,
strategy: self.name(),
gfactor: None,
});
}
info!(
"CS: iters={} lambda={:.3e} (per-coil FISTA + RSS)",
self.iters, self.lambda
);
let mut output = Array3::<f32>::zeros((nz, ny, nx));
for kz in 0..nz {
let mut mask2 = ndarray::Array2::<bool>::from_elem((ny, nx), false);
for y in 0..ny {
for x in 0..nx {
mask2[[y, x]] = mask[[kz, y, x]];
}
}
let mut coil_imgs: Vec<ndarray::Array2<Complex32>> = Vec::with_capacity(nc);
for c in 0..nc {
let mut kzf = ndarray::Array2::<Complex32>::zeros((ny, nx));
for y in 0..ny {
for x in 0..nx {
kzf[[y, x]] = kspace[[c, kz, y, x]];
}
}
let recon = fista_cs_single_coil(&kzf, &mask2, self.iters, self.lambda)
.map_err(|e| IoError::Inconsistent(e.to_string()))?;
coil_imgs.push(recon);
}
#[allow(clippy::needless_range_loop)]
for y in 0..ny {
for x in 0..nx {
let mut s = 0.0f32;
for c in 0..nc {
s += coil_imgs[c][[y, x]].norm_sqr();
}
output[[kz, y, x]] = s.sqrt();
}
}
}
let mut magnitude = output;
if self.crop_to_recon_matrix {
magnitude = IfftRss::default().maybe_crop(magnitude, file);
}
Ok(ImageVolume {
data: magnitude,
strategy: self.name(),
gfactor: None,
})
}
}