Skip to content

Commit 4bba783

Browse files
committed
Add implementation
1 parent d6046d9 commit 4bba783

File tree

1 file changed

+195
-0
lines changed

1 file changed

+195
-0
lines changed

nipype_generate_fieldmaps.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from itertools import chain
5+
from pathlib import Path
6+
from typing import Any
7+
8+
import nibabel as nib
9+
from nipype import Function
10+
from nipype import IdentityInterface
11+
from nipype import Merge
12+
from nipype import Node
13+
from nipype import Workflow
14+
from nipype.interfaces import fsl
15+
16+
__version__ = "0.1.0"
17+
18+
19+
INPUT_FIELDS = [
20+
"se_epi_pe1_file",
21+
"se_epi_pe2_file",
22+
"se_epi_sidecar_pe1_file",
23+
"se_epi_sidecar_pe2_file",
24+
]
25+
OUTPUT_FIELDS = [
26+
"acq_params_file",
27+
"corrected_se_epi_file",
28+
"fmap_hz_file",
29+
"fmap_rads_file",
30+
"fmap_mag_file",
31+
"fmap_mag_brain_file",
32+
"fmap_mag_brain_mask_file",
33+
]
34+
35+
36+
def create_prepare_fieldmaps_wf(name: str = "prepare_fieldmaps_wf") -> Workflow:
37+
38+
wf = Workflow(name=name)
39+
40+
inputnode = Node(IdentityInterface(fields=INPUT_FIELDS), name="inputnode")
41+
42+
# pre-concatenation (need images in a list)
43+
listify_se_epi_files = Node(Merge(numinputs=2), name="listify_se_epi_files")
44+
wf.connect(inputnode, "se_epi_pe1_file", listify_se_epi_files, "in1")
45+
wf.connect(inputnode, "se_epi_pe2_file", listify_se_epi_files, "in2")
46+
47+
# merge the acquisitions (volumes) into a single nii image
48+
merge_se_epi_files = Node(
49+
fsl.Merge(dimension="t", merged_file="merged.nii.gz"),
50+
name="merge_se_epi_files",
51+
)
52+
wf.connect(listify_se_epi_files, "out", merge_se_epi_files, "in_files")
53+
54+
# create the acquisition parameter file (--datain)
55+
acq_params = Node(
56+
Function(
57+
input_names=[
58+
"merged_se_epi_file",
59+
"sidecar_pe1_file",
60+
"sidecar_pe2_file",
61+
"out_file",
62+
],
63+
output_names=["out_file"],
64+
function=_create_acq_param_file_fi,
65+
),
66+
name="acq_params",
67+
)
68+
wf.connect(merge_se_epi_files, "merged_file", acq_params, "merged_se_epi_file")
69+
wf.connect(inputnode, "se_epi_sidecar_pe1_file", acq_params, "sidecar_pe1_file")
70+
wf.connect(inputnode, "se_epi_sidecar_pe2_file", acq_params, "sidecar_pe2_file")
71+
72+
# estimate the fieldmaps via FSL's TOPUP
73+
topup = Node(
74+
fsl.TOPUP(out_field="fmap.nii.gz", out_corrected="corrected.nii.gz"),
75+
name="topup",
76+
)
77+
wf.connect(merge_se_epi_files, "merged_file", topup, "in_file")
78+
wf.connect(acq_params, "out_file", topup, "encoding_file")
79+
80+
# convert the estimate field to rad/s
81+
fmap_rads = Node(
82+
fsl.ImageMaths(op_string="-mul 6.28", out_file="fmap_rads.nii.gz"),
83+
name="fmap_rads",
84+
)
85+
wf.connect(topup, "out_field", fmap_rads, "in_file")
86+
87+
# compute a magnitude image from the corrected Spin Echo EPI volumes
88+
fmap_mag = Node(
89+
fsl.ImageMaths(op_string="-Tmean", out_file="fmap_mag.nii.gz"),
90+
name="fmap_mag",
91+
)
92+
wf.connect(topup, "out_corrected", fmap_mag, "in_file")
93+
94+
# extract the mean brain + mask from the magnitude image
95+
fmap_mag_brain = Node(
96+
fsl.BET(frac=0.5, out_file="fmap_mag_brain.nii.gz", mask=True),
97+
name="fmap_mag_brain",
98+
)
99+
wf.connect(fmap_mag, "out_file", fmap_mag_brain, "in_file")
100+
101+
# To the outside world!
102+
outputnode = Node(IdentityInterface(fields=OUTPUT_FIELDS), name="outputnode")
103+
wf.connect(acq_params, "out_file", outputnode, "acq_params_file")
104+
wf.connect(topup, "out_corrected", outputnode, "corrected_se_epi_file")
105+
wf.connect(topup, "out_field", outputnode, "fmap_hz_file")
106+
wf.connect(fmap_rads, "out_file", outputnode, "fmap_rads_file")
107+
wf.connect(fmap_mag, "out_file", outputnode, "fmap_mag_file")
108+
wf.connect(fmap_mag_brain, "out_file", outputnode, "fmap_mag_brain_file")
109+
wf.connect(fmap_mag_brain, "mask_file", outputnode, "fmap_mag_brain_mask_file")
110+
111+
return wf
112+
113+
114+
def _create_acq_param_file_fi( # fi = [F]unction [I]nterface
115+
merged_se_epi_file,
116+
sidecar_pe1_file,
117+
sidecar_pe2_file,
118+
out_file=None,
119+
):
120+
from nipype_generate_fieldmaps import create_acq_param_file
121+
122+
return create_acq_param_file(
123+
merged_se_epi_file,
124+
sidecar_pe1_file,
125+
sidecar_pe2_file,
126+
out_file,
127+
)
128+
129+
130+
def create_acq_param_file(
131+
merged_se_epi_file: str | Path,
132+
sidecar_pe1_file: str | Path,
133+
sidecar_pe2_file: str | Path,
134+
out_file: str | Path | None = None,
135+
) -> Path:
136+
# load JSON sidecars
137+
sidecar_pe1 = json.loads(Path(sidecar_pe1_file).read_text())
138+
sidecar_pe2 = json.loads(Path(sidecar_pe2_file).read_text())
139+
140+
# total readout times
141+
trt_pe1 = get_total_readout_time(sidecar_pe1)
142+
trt_pe2 = get_total_readout_time(sidecar_pe2)
143+
144+
# phase encoding unit vectors
145+
pe1_vec = get_phase_encoding_vec(sidecar_pe1)
146+
pe2_vec = get_phase_encoding_vec(sidecar_pe2)
147+
148+
# extract the number of volumes in the merged fieldmaps nii image
149+
img: nib.Nifti1Image = nib.load(str(merged_se_epi_file))
150+
n_total_vols: int = img.header["dim"][4] # type: ignore
151+
152+
# format the lines that we'll write to the acq param file
153+
line_pe1 = " ".join(map(str, chain(pe1_vec, [trt_pe1])))
154+
lines_pe1 = [line_pe1] * (n_total_vols // 2)
155+
line_pe2 = " ".join(map(str, chain(pe2_vec, [trt_pe2])))
156+
lines_pe2 = [line_pe2] * (n_total_vols // 2)
157+
158+
# create the acq param file
159+
content = "\n".join(chain(lines_pe1, lines_pe2)) + "\n"
160+
acq_param_file = Path(out_file) if out_file else Path.cwd() / "acq_params.txt"
161+
acq_param_file.write_text(content)
162+
163+
return acq_param_file
164+
165+
166+
def get_total_readout_time(sidecar: dict[str, Any]) -> float:
167+
# extract or derive the total readout time, see:
168+
# - https://bids-specification.readthedocs.io/en/v1.6.0/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#in-plane-spatial-encoding # noqa: E501
169+
# - https://lcni.uoregon.edu/kb-articles/kb-0003
170+
if "TotalReadoutTime" in sidecar:
171+
# can we extract it?
172+
total_readout_time: float = sidecar["TotalReadoutTime"]
173+
elif "EffectiveEchoSpacing" in sidecar and "ReconMatrixPE" in sidecar:
174+
# can we compute it?
175+
effective_echo_spacing: float = sidecar["EffectiveEchoSpacing"]
176+
recon_matrix_pe: float = sidecar["ReconMatrixPE"]
177+
total_readout_time = (recon_matrix_pe - 1) * effective_echo_spacing
178+
else:
179+
msg = "Could not extract or derive Total Readout Time from fieldmap sidecar."
180+
raise RuntimeError(msg)
181+
182+
return total_readout_time
183+
184+
185+
def get_phase_encoding_vec(sidecar: dict[str, Any]) -> tuple[int, int, int]:
186+
uvecs: dict[str, tuple[int, int, int]] = { # unit vectors
187+
"i": (1, 0, 0),
188+
"j": (0, 1, 0),
189+
"k": (0, 0, 1),
190+
"i-": (-1, 0, 0),
191+
"j-": (0, -1, 0),
192+
"k-": (0, 0, -1),
193+
}
194+
pe: str = sidecar["PhaseEncodingDirection"]
195+
return uvecs[pe]

0 commit comments

Comments
 (0)