def fse(pd, r1, r2=None, receive=None, gfactor=None, te=0.02, tr=5, sigma=None, device=None): """Simulate data generated by a (simplified) Fast Spin-Echo (FSE) sequence. Tissue parameters ----------------- pd : tensor_like Proton density r1 : tensor_like Longitudinal relaxation rate, in 1/sec r2 : tensor_like, optional Transverse relaxation rate, in 1/sec. Fields ------ receive : tensor_like, optional Receive B1 field gfactor : tensor_like, optional G-factor map. If provided and `sigma` is not `None`, the g-factor map is used to sample non-stationary noise. Sequence parameters ------------------- te : float, default=3e-3 Echo time, in sec tr : float default=2.3 Repetition time, in sec. Noise ----- sigma : float, optional Standard-deviation of the sampled noise (no sampling if `None`) Returns ------- sim : tensor Simulated FSE image """ pd, r1, r2, receive, gfactor \ = utils.to_max_backend(pd, r1, r2, receive, gfactor) pd, r1, r2, receive, gfactor \ = utils.to(pd, r1, r2, receive, gfactor, device=device) if receive is not None: pd = pd * receive del receive e1 = r1.mul(tr).neg_().exp_() e2 = r2.mul(te).neg_().exp_() signal = pd * (1 - e1) * e2 # noise signal = add_noise(signal, std=sigma) return signal
def mp2rage_old(pd, r1, r2s=None, transmit=None, receive=None, gfactor=None, tr=6.25, ti1=0.8, ti2=2.2, tx=None, te=None, fa=(4, 5), n=160, eff=0.96, sigma=None, device=None, return_combined=True): """Simulate data generated by a (simplified) MP2RAGE sequence. The defaults are parameters used at 3T in the original MP2RAGE paper. However, I don't get a nice image with these parameters when applied to maps obtained at 3T with the hmri toolbox. Here are (unrealistic) parameters that seem to give a decent contrast: tr=6.25, ti1=1.4, ti2=4.5, tx=5.8e-3, fa=(4, 5), n=160, eff=0.96 Tissue parameters ----------------- pd : tensor_like Proton density r1 : tensor_like Longitudinal relaxation rate, in 1/sec r2s : tensor_like, optional Transverse relaxation rate, in 1/sec. If not provided, T2*-bias is not included. Fields ------ transmit : tensor_like, optional Transmit B1 field receive : tensor_like, optional Receive B1 field gfactor : tensor_like, optional G-factor map. If provided and `sigma` is not `None`, the g-factor map is used to sample non-stationary noise. Sequence parameters ------------------- tr : float default=6.25 Full Repetition time, in sec. (Time between two inversion pulses) ti1 : float, default=0.8 First inversion time, in sec. (Time between inversion pulse and middle of the first echo train) ti2 : float, default=2.2 Second inversion time, in sec. (Time between inversion pulse and middle of the second echo train) tx : float, default=te*2 or 5.8e-3 Excitation repetition time, in sec. (Time between two excitation pulses within the echo train) te : float, default=minitr/2 Echo time, in sec. fa : float or (float, float), default=(4, 5) Flip angle of the first and second acquisition block, in deg If only one value, it is shared between the blocks. n : int, default=160 Number of excitation pulses (= phase encoding steps) per train. eff : float, default=0.96 Efficiency of the inversion pulse. Noise ----- sigma : float, optional Standard-deviation of the sampled Rician noise (no sampling if `None`) Returns ------- mp2rage : tensor, if return_combined is True Simulated MP2RAGE image image1 : tensor, if return_combined is False Image at first inversion time image2 : tensor, if return_combined is False Image at second inversion time References ---------- ..[1] "MP2RAGE, a self bias-field corrected sequence for improved segmentation and T1-mapping at high field." Marques JP, Kober T, Krueger G, van der Zwaag W, Van de Moortele PF, Gruetter R. Neuroimage. 2010 Jan 15;49(2):1271-81. doi: 10.1016/j.neuroimage.2009.10.002 """ pd, r1, r2s, transmit, receive, gfactor \ = utils.to_max_backend(pd, r1, r2s, transmit, receive, gfactor) pd, r1, r2s, transmit, receive, gfactor \ = utils.to(pd, r1, r2s, transmit, receive, gfactor, device=device) backend = utils.backend(pd) if tx is None and te is None: tx = 5.8e-3 tx = tx or 2 * te # Time between excitation pulses te = te or tx / 2 # Echo time fa1, fa2 = py.make_list(fa, 2) fa1 = fa1 * constants.pi / 180 # Flip angle of first GRE block fa2 = fa2 * constants.pi / 180 # Flip angle of second GRE block n = n or min(pd.shape) # Number of readouts (PE steps) per loop tr1 = n * tx # First GRE block tr2 = n * tx # Second GRE block tp = ti1 - tr1 / 2 # Preparation time tw = ti2 - tr2 / 2 - ti1 - tr1 / 2 # Wait time between GRE blocks td = tr - ti2 - tr2 / 2 # Recovery time m = n // 2 # Middle of echo train if transmit is not None: fa1 = transmit * fa1 fa2 = transmit * fa2 del transmit fa1 = torch.as_tensor(fa1, **backend) fa2 = torch.as_tensor(fa2, **backend) # precompute exponential terms ex = r1.mul(-tx).exp() ep = r1.mul(-tp).exp() ew = r1.mul(-tw).exp() ed = r1.mul(-td).exp() e1 = r1.mul(-tr).exp() c1 = fa1.cos() c2 = fa2.cos() # steady state mss = (1 - ep) * (c1 * ex).pow(n) mss = mss + (1 - ex) * (1 - (c1 * ex).pow(n)) / (1 - c1 * ex) mss = mss * ew + (1 - ew) mss = mss * (c2 * ex).pow(n) mss = mss + (1 - ex) * (1 - (c2 * ex).pow(n)) / (1 - c2 * ex) mss = mss * ed + (1 - ed) mss = mss * pd / (1 + eff * (c1 * c2).pow(n) * e1) # IR components mi1 = -eff * mss * ep / pd + (1 - ep) mi1 = mi1 * (c1 * ex).pow(m - 1) mi1 = mi1 + (1 - ex) * (1 - (c1 * ex).pow(m - 1)) / (1 - c1 * ex) mi1 = mi1 * fa1.sin() mi1 = mi1.abs() mi2 = (mss / pd - (1 - ed)) / (ed * (c2 * ex).pow(m)) mi2 = mi2 + (1 - ex) * (1 - (c2 * ex).pow(-m)) / (1 - c2 * ex) mi2 = mi2 * fa2.sin() mi2 = mi2.abs() if return_combined and not sigma: m = (mi1 * mi2) / (mi1.square() + mi2.square()) m = torch.where(~torch.isfinite(m), m.new_zeros([]), m) return m # Common component (pd, B1-, R2*) if receive is not None: pd = pd * receive del receive mi1 = mi1 * pd mi2 = mi2 * pd if r2s is not None: e2 = r2s.mul(-te).exp_() mi1 = mi1 * e2 mi2 = mi2 * e2 del r2s # noise mi1 = add_noise(mi1, std=sigma, gfactor=gfactor) mi2 = add_noise(mi2, std=sigma, gfactor=gfactor) if return_combined: m = (mi1 * mi2) / (mi1.square() + mi2.square()) m = torch.where(~torch.isfinite(m), m.new_zeros([]), m) return m else: mi1 = torch.where(~torch.isfinite(mi1), mi1.new_zeros([]), mi1) mi2 = torch.where(~torch.isfinite(mi2), mi2.new_zeros([]), mi2) return mi1, mi2
def mp2rage(pd, r1, r2s=None, transmit=None, receive=None, gfactor=None, tr=6.25, ti1=0.8, ti2=2.2, tx=None, te=None, fa=(4, 5), n=160, eff=0.96, sigma=None, device=None, return_combined=True): """Simulate data generated by a (simplified) MP2RAGE sequence. The defaults are parameters used at 3T in the original MP2RAGE paper. However, I don't get a nice image with these parameters when applied to maps obtained at 3T with the hmri toolbox. Here are (unrealistic) parameters that seem to give a decent contrast: tr=6.25, ti1=1.4, ti2=4.5, tx=5.8e-3, fa=(4, 5), n=160, eff=0.96 Tissue parameters ----------------- pd : tensor_like Proton density r1 : tensor_like Longitudinal relaxation rate, in 1/sec r2s : tensor_like, optional Transverse relaxation rate, in 1/sec. If not provided, T2*-bias is not included. Fields ------ transmit : tensor_like, optional Transmit B1 field receive : tensor_like, optional Receive B1 field gfactor : tensor_like, optional G-factor map. If provided and `sigma` is not `None`, the g-factor map is used to sample non-stationary noise. Sequence parameters ------------------- tr : float default=6.25 Full Repetition time, in sec. (Time between two inversion pulses) ti1 : float, default=0.8 First inversion time, in sec. (Time between inversion pulse and middle of the first echo train) ti2 : float, default=2.2 Second inversion time, in sec. (Time between inversion pulse and middle of the second echo train) tx : float, default=te*2 or 5.8e-3 Excitation repetition time, in sec. (Time between two excitation pulses within the echo train) te : float, default=tx/2 Echo time, in sec. fa : float or (float, float), default=(4, 5) Flip angle of the first and second acquisition block, in deg If only one value, it is shared between the blocks. n : int, default=160 Number of excitation pulses (= phase encoding steps) per train. eff : float, default=0.96 Efficiency of the inversion pulse. Noise ----- sigma : float, optional Standard-deviation of the sampled Rician noise (no sampling if `None`) Returns ------- mp2rage : tensor, if return_combined is True Simulated MP2RAGE image image1 : tensor, if return_combined is False Image at first inversion time image2 : tensor, if return_combined is False Image at second inversion time References ---------- ..[1] "MP2RAGE, a self bias-field corrected sequence for improved segmentation and T1-mapping at high field." Marques JP, Kober T, Krueger G, van der Zwaag W, Van de Moortele PF, Gruetter R. Neuroimage. 2010 Jan 15;49(2):1271-81. doi: 10.1016/j.neuroimage.2009.10.002 """ pd, r1, r2s, transmit, receive, gfactor \ = utils.to_max_backend(pd, r1, r2s, transmit, receive, gfactor) pd, r1, r2s, transmit, receive, gfactor \ = utils.to(pd, r1, r2s, transmit, receive, gfactor, device=device) if tx is None and te is None: tx = 5.8e-3 tx = tx or 2 * te # Time between excitation pulses te = te or tx / 2 # Echo time fa1, fa2 = py.make_list(fa, 2) fa1 = fa1 * constants.pi / 180 # Flip angle of first GRE block fa2 = fa2 * constants.pi / 180 # Flip angle of second GRE block n = n or min(pd.shape) # Number of readouts (PE steps) per loop tr1 = n * tx # First GRE block tr2 = n * tx # Second GRE block tp = ti1 - tr1 / 2 # Preparation time tw = ti2 - tr2 / 2 - ti1 - tr1 / 2 # Wait time between GRE blocks td = tr - ti2 - tr2 / 2 # Recovery time if return_combined and not sigma: m = mp2rage_nonoise(pd, r1, tx, tp, tw, td, tr, fa1, fa2, n, eff, transmit) m = torch.where(~torch.isfinite(m), m.new_zeros([1]), m) return m mi1, mi2 = mp2rage_uncombined(pd, r1, r2s, tx, tp, tw, td, tr, te, fa1, fa2, n, eff, transmit, receive) # noise mi1 = add_noise(mi1, std=sigma, gfactor=gfactor) mi2 = add_noise(mi2, std=sigma, gfactor=gfactor) if return_combined: m = mp2rage_from_ir(mi1, mi2) m = torch.where(~torch.isfinite(m), m.new_zeros([1]), m) return m else: mi1 = torch.where(~torch.isfinite(mi1), mi1.new_zeros([]), mi1) mi2 = torch.where(~torch.isfinite(mi2), mi2.new_zeros([]), mi2) return mi1, mi2
def inpaint(*inputs, missing='nan', output=None, device=None, verbose=1, max_iter_rls=10, max_iter_cg=32, tol_rls=1e-5, tol_cg=1e-5): """Inpaint missing values by minimizing Joint Total Variation. Parameters ---------- *inputs : str or tensor or (tensor, tensor) Either a path to a volume file or a tuple `(dat, affine)`, where the first element contains the volume data and the second contains the orientation matrix. missing : 'nan' or scalar or callable, default='nan' Mask of the missing data. If a scalar, all voxels with that value are considered missing. If a function, it should return the mask of missing values when applied to the multi-channel data. Else, non-finite values are assumed missing. output : [sequence of] str, optional Output filename(s). If the input is not a path, the unstacked data is not written on disk by default. If the input is a path, the default output filename is '{dir}/{base}.pool{ext}', where `dir`, `base` and `ext` are the directory, base name and extension of the input file, `i` is the coordinate (starting at 1) of the slice. verbose : int, default=1 device : torch.device, optional max_iter_rls : int, default=10 max_iter_cg : int, default=32 tol_rls : float, default=1e-5 tol_cg : float, default=1e-5 Returns ------- *output : str or (tensor, tensor) If the input is a path, the output path is returned. Else, the pooled data and orientation matrix are returned. """ # Preprocess dirs = [] bases = [] exts = [] fnames = [] nchannels = [] dat = [] aff = None for i, inp in enumerate(inputs): is_file = isinstance(inp, str) if is_file: fname = inp dir, base, ext = py.fileparts(fname) fnames.append(inp) dirs.append(dir) bases.append(base) exts.append(ext) f = io.volumes.map(fname) if aff is None: aff = f.affine f = ensure_4d(f) dat.append(f.fdata(device=device)) else: fnames.append(None) dirs.append('') bases.append(f'{i+1}') exts.append('.nii.gz') if isinstance(inp, (list, tuple)): if aff is None: dat1, aff = inp else: dat1, _ = inp else: dat1 = inp dat.append(torch.as_tensor(dat1, device=device)) del dat1 nchannels.append(dat[-1].shape[-1]) dat = utils.to(*dat, dtype=torch.float, device=utils.max_device(dat)) if not torch.is_tensor(dat): dat = torch.cat(dat, dim=-1) dat = utils.movedim(dat, -1, 0) # (channels, *spatial) # Set missing data if missing != 'nan': if not callable(missing): missingval = utils.make_vector(missing, dtype=dat.dtype, device=dat.device) missing = lambda x: utils.isin(x, missingval) dat[missing(dat)] = nan dat[~torch.isfinite(dat)] = nan # Do it if aff is not None: vx = spatial.voxel_size(aff) else: vx = 1 dat = do_inpaint(dat, voxel_size=vx, verbose=verbose, max_iter_rls=max_iter_rls, tol_rls=tol_rls, max_iter_cg=max_iter_cg, tol_cg=tol_cg) # Postprocess dat = utils.movedim(dat, 0, -1) dat = dat.split(nchannels, dim=-1) output = py.make_list(output, len(dat)) for i in range(len(dat)): if fnames[i] and not output[i]: output[i] = '{dir}{sep}{base}.inpaint{ext}' if output[i]: if fnames[i]: output[i] = output[i].format(dir=dirs[i] or '.', base=bases[i], ext=exts[i], sep=os.path.sep) io.volumes.save(dat[i], output[i], like=fnames[i], affine=aff) else: output[i] = output[i].format(sep=os.path.sep) io.volumes.save(dat[i], output[i], affine=aff) dat = [ output[i] if fnames[i] else (dat[i], aff) if aff is not None else dat[i] for i in range(len(dat)) ] if len(dat) == 1: dat = dat[0] return dat
def spgr(pd, r1, r2s=None, mt=None, transmit=None, receive=None, gfactor=None, te=0, tr=25e-3, fa=20, sigma=None, device=None): """Simulate data generated by a Spoiled Gradient-Echo (SPGR/FLASH) sequence. Tissue parameters ----------------- pd : tensor_like Proton density r1 : tensor_like Longitudinal relaxation rate, in 1/sec r2s : tensor_like, optional Transverse relaxation rate, in 1/sec. Mandatory if any `te > 0`. mt : tensor_like, optional MTsat. Mandatory if any `mtpulse == True`. Fields ------ transmit : tensor_like, optional Transmit B1 field receive : tensor_like, optional Receive B1 field gfactor : tensor_like, optional G-factor map. If provided and `sigma` is not `None`, the g-factor map is used to sample non-stationary noise. Sequence parameters ------------------- te : float, default=0 Echo time, in sec tr : float default=2.5e-3 Repetition time, in sec fa : float, default=20 Flip angle, in deg Noise ----- sigma : float, optional Standard-deviation of the sampled Rician noise (no sampling if `None`) Returns ------- sim : tensor Simulated SPGR image """ pd, r1, r2s, mt, transmit, receive, gfactor \ = utils.to_max_backend(pd, r1, r2s, mt, transmit, receive, gfactor) pd, r1, r2s, mt, transmit, receive, gfactor \ = utils.to(pd, r1, r2s, mt, transmit, receive, gfactor, device=device) backend = utils.backend(pd) fa = fa * constants.pi / 180. if transmit is not None: fa = fa * transmit del transmit fa = torch.as_tensor(fa, **backend) if receive is not None: pd = pd * receive del receive pd = pd * fa.sin() fa = fa.cos() e1, r1 = r1.mul(tr).neg_().exp(), None signal = pd * (1 - e1) if mt is not None: omt = mt.neg().add_(1) signal *= omt signal /= (1 - fa * omt * e1) del omt else: signal /= (1 - fa * e1) if r2s is not None: e2, r2s = r2s.mul(te).neg_().exp(), None signal *= e2 del e2 # noise signal = add_noise(signal, std=sigma) return signal
def mprage(pd, r1, r2s=None, transmit=None, receive=None, gfactor=None, tr=2.3, ti=0.9, tx=None, te=None, fa=9, n=160, eff=0.96, sigma=None, device=None): """Simulate data generated by a (simplified) MP-RAGE sequence. Default parameters mimic the ADNI-3 protocol on 3T Siemens scanners. Our Implementation is based on the MP2RAGE paper, where the sequence is stripped from the second GRE readout block. Tissue parameters ----------------- pd : tensor_like Proton density r1 : tensor_like Longitudinal relaxation rate, in 1/sec r2s : tensor_like, optional Transverse relaxation rate, in 1/sec. If not provided, T2*-bias is not included. Fields ------ transmit : tensor_like, optional Transmit B1 field receive : tensor_like, optional Receive B1 field gfactor : tensor_like, optional G-factor map. If provided and `sigma` is not `None`, the g-factor map is used to sample non-stationary noise. Sequence parameters ------------------- tr : float default=2.3 Repetition time, in sec. (Time between two inversion pulses) ti : float, default=0.9 Inversion time, in sec. (Time between inversion pulse and middle of the echo train) tx : float, default=2*te or 6e-3 Excitation repetition time, in sec (Time between two excitation pulses within the echo train) te : float, default=tx/2 Echo time, in sec fa : float, default=9 Flip angle, in deg n : int, default=160 Number of excitation pulses (= phase encoding steps) per train. eff : float, default=0.96 Efficiency of the inversion pulse. Noise ----- sigma : float, optional Standard-deviation of the sampled Rician noise (no sampling if `None`) Returns ------- sim : tensor Simulated MPRAGE image References ---------- ..[1] "MP2RAGE, a self bias-field corrected sequence for improved segmentation and T1-mapping at high field." Marques JP, Kober T, Krueger G, van der Zwaag W, Van de Moortele PF, Gruetter R. Neuroimage. 2010 Jan 15;49(2):1271-81. doi: 10.1016/j.neuroimage.2009.10.002 """ pd, r1, r2s, transmit, receive, gfactor \ = utils.to_max_backend(pd, r1, r2s, transmit, receive, gfactor) pd, r1, r2s, transmit, receive, gfactor \ = utils.to(pd, r1, r2s, transmit, receive, gfactor, device=device) backend = utils.backend(pd) if tx is None and te is None: tx = 6e-3 tx = tx or 2 * te # Time between excitation pulses te = te or tx / 2 # Echo time fa = fa * constants.pi / 180 # Flip angle of GRE block n = n or min(pd.shape) # Number of readouts (PE steps) per loop tr1 = n * tx # GRE block tp = ti - tr1 / 2 # Preparation time td = tr - ti - tr1 / 2 # Recovery time m = n // 2 # Middle of echo train if transmit is not None: fa = transmit * fa del transmit fa = torch.as_tensor(fa, **backend) # precompute exponential terms ex = r1.mul(-tx).exp() ep = r1.mul(-tp).exp() ed = r1.mul(-td).exp() e1 = r1.mul(-tr).exp() c = fa.cos() # steady state s = (1 - ep) * (c * ex).pow(n) s = s + (1 - ex) * (1 - (c * ex).pow(n)) / (1 - c * ex) s = s * ed + (1 - ed) s = s * pd / (1 + eff * c.pow(n) * e1) # IR component s = -eff * s * ep / pd + (1 - ep) s = s * (c * ex).pow(m - 1) s = s + (1 - ex) * (1 - (c * ex).pow(m - 1)) / (1 - c * ex) s = s * fa.sin() s = s.abs() # Modulation (PD, B1-, R2*) if receive is not None: pd = pd * receive del receive s = s * pd if r2s is not None: e2 = r2s.mul(-te).exp_() s = s * e2 del r2s # noise s = add_noise(s, std=sigma, gfactor=gfactor) return s