Skip to content

Commit f5c4b2a

Browse files
Merge pull request #238 from alexdaniel654/feature/t1_acq_orders
SE-EPI T1 Acquisition Orders
2 parents d2b78c9 + 2a866a0 commit f5c4b2a

File tree

2 files changed

+155
-40
lines changed

2 files changed

+155
-40
lines changed

ukat/mapping/t1.py

+63-11
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
class T1Model(fitting.Model):
1212
def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
13-
tss_axis=-2, molli=False, mag_corr=False, multithread=True):
13+
tss_axis=-2, acq_order='ascend', molli=False, mag_corr=False,
14+
multithread=True):
1415
"""
1516
A class containing the T1 fitting model
1617
@@ -45,6 +46,19 @@ def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
4546
would be along the TI axis and would be meaningless.
4647
If `pixel_array` is single slice (dimensions [x, y, TI]),
4748
then this should be set to None.
49+
acq_order : str or list, optional
50+
Default 'ascend'
51+
The order in which the slices were acquired. 'ascend' assumes
52+
the zeroth slice (in the tss_axis) was acquired first, 'descend'
53+
assumes the -1 slice was acquired first and the zeroth
54+
slice was acquired last. 'centric' assumes the centre slice was
55+
acquired first, then the slice above the centre, then the slice
56+
below the centre etc. In the case of an even number of slices,
57+
the centre slice is taken as the lower of the two central slices.
58+
Alternatively, a list of integers can be used to specify the
59+
acquisition order. Specifying `acq_order='centric'` and
60+
`acq_order=[4, 2, 0, 1, 3, 5]` would be equivalent for a six
61+
slice acquisition.
4862
mag_corr : bool, optional
4963
Default False
5064
If True, the data is assumed to have been magnitude corrected
@@ -60,6 +74,7 @@ def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
6074
self.parameters = parameters
6175
self.tss = tss
6276
self.tss_axis = tss_axis
77+
self.acq_order = acq_order
6378
self.molli = molli
6479

6580
if (mag_corr is False) & (np.nanmin(pixel_array) < 0):
@@ -107,7 +122,23 @@ def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
107122
self._tss_correct_ti()
108123

109124
def _tss_correct_ti(self):
110-
slices = np.indices(self.map_shape)[self.tss_axis].ravel()
125+
slices = np.indices(self.map_shape)[self.tss_axis]
126+
if self.acq_order == 'ascend':
127+
slices = slices.ravel()
128+
elif self.acq_order == 'descend':
129+
slices = np.flip(slices, axis=self.tss_axis).ravel()
130+
elif self.acq_order == 'centric':
131+
ns = self.map_shape[self.tss_axis]
132+
# Generate the acquisition order for centric ordering. e.g. for
133+
# a five slice acquisition, the order would be [4, 2, 0, 1, 3].
134+
start = ns - 2 if ns % 2 == 0 else ns - 1
135+
evens_desc = np.arange(start, -1, -2)
136+
odds_asc = np.arange(1, ns, 2)
137+
acq_ind = np.concatenate([evens_desc, odds_asc])
138+
slices = np.take(slices, acq_ind, axis=self.tss_axis).ravel()
139+
else:
140+
slices = np.take(slices, self.acq_order, axis=self.tss_axis).ravel()
141+
111142
for ind, (ti, slice) in enumerate(zip(self.x_list, slices)):
112143
self.x_list[ind] = np.array(ti) + self.tss * slice
113144

@@ -144,9 +175,9 @@ class T1:
144175
apart from TI
145176
"""
146177

147-
def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,
148-
mask=None, parameters=2, mag_corr=False, molli=False,
149-
multithread=True, mdr=False):
178+
def __init__(self, pixel_array, inversion_list, affine, tss=0,
179+
tss_axis=-2, acq_order='ascend', mask=None, parameters=2,
180+
mag_corr=False, molli=False, multithread=True, mdr=False):
150181
"""Initialise a T1 class instance.
151182
152183
Parameters
@@ -171,6 +202,19 @@ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,
171202
would be along the TI axis and would be meaningless.
172203
If `pixel_array` is single slice (dimensions [x, y, TI]),
173204
then this should be set to None.
205+
acq_order : str or list, optional
206+
Default 'ascend'
207+
The order in which the slices were acquired. 'ascend' assumes
208+
the zeroth slice (in the tss_axis) was acquired first, 'descend'
209+
assumes the -1 slice was acquired first and the zeroth
210+
slice was acquired last. 'centric' assumes the centre slice was
211+
acquired first, then the slice above the centre, then the slice
212+
below the centre etc. In the case of an even number of slices,
213+
the centre slice is taken as the lower of the two central slices.
214+
Alternatively, a list of integers can be used to specify the
215+
acquisition order. Specifying `acq_order='centric'` and
216+
`acq_order=[4, 2, 0, 1, 3, 5]` would be equivalent for a six
217+
slice acquisition.
174218
affine : np.ndarray
175219
A matrix giving the relationship between voxel coordinates and
176220
world coordinates.
@@ -272,6 +316,15 @@ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,
272316
raise ValueError('Temporal slice spacing only supported '
273317
'along the z direction when using '
274318
'model-driven registration.')
319+
if type(acq_order) == list:
320+
assert len(acq_order) == self.shape[self.tss_axis], \
321+
'acq_order must have the same length as the number of ' \
322+
'slices in the tss_axis.'
323+
assert type(acq_order[0]) == int, \
324+
'acq_order must be a list of integers.'
325+
elif acq_order not in ['ascend', 'descend', 'centric']:
326+
raise ValueError('acq_order must be a list of integers or '
327+
'one of "ascend", "descend" or "centric".')
275328

276329
if self.molli:
277330
if self.parameters == 2:
@@ -285,7 +338,7 @@ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,
285338
# are the same.
286339
if self.tss == 0:
287340
pixel_array, deform, _, _ = mdreg.fit(
288-
self.pixel_array,
341+
np.nan_to_num(self.pixel_array),
289342
force_2d=True,
290343
verbose=1,
291344
fit_image={
@@ -325,7 +378,7 @@ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,
325378
+ self.tss * slice)
326379
(pixel_array[..., slice, :], deform[..., slice, :, :], _,
327380
_) = mdreg.fit(
328-
self.pixel_array[..., slice, :],
381+
np.nan_to_num(self.pixel_array[..., slice, :]),
329382
force_2d=True,
330383
verbose=1,
331384
fit_image={
@@ -357,8 +410,8 @@ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,
357410
# Fit Data
358411
self.fitting_model = T1Model(self.pixel_array, self.inversion_list,
359412
self.parameters, self.mask, self.tss,
360-
self.tss_axis, self.molli, self.mag_corr,
361-
self.multithread)
413+
self.tss_axis, acq_order, self.molli,
414+
self.mag_corr, self.multithread)
362415
self.mag_corr = self.fitting_model.mag_corr
363416
popt, error, r2 = fitting.fit_image(self.fitting_model)
364417
self.t1_map = popt[0]
@@ -390,8 +443,7 @@ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,
390443

391444
# Do MOLLI correction
392445
if self.molli:
393-
correction_factor = (((self.m0_map * self.eff_map) / self.m0_map)
394-
- 1)
446+
correction_factor = -(1 - self.eff_map)
395447
percentage_error = self.t1_err / self.t1_map
396448
self.t1_map = np.nan_to_num(self.t1_map * correction_factor)
397449
self.t1_err = np.nan_to_num(self.t1_map * percentage_error)

ukat/mapping/tests/test_t1.py

+92-29
Original file line numberDiff line numberDiff line change
@@ -33,37 +33,37 @@ class TestT1:
3333
1689.08502946])
3434
# The ideal signal produced by the equation M0 * (1 - 2 * exp(-t / T1))
3535
# where M0 = 5000 and T1 = 1000 acquired over three slices at 9 t
36-
# between 200 and 1000 ms + a temporal slice spacing of 10 ms
36+
# between 200 and 1000 ms + a temporal slice spacing of 100 ms
3737
correct_signal_two_param_tss = np.array([[[-3187.30753078, -2408.18220682,
3838
-1703.20046036, -1065.30659713,
3939
-488.11636094, 34.14696209,
4040
506.71035883, 934.30340259,
4141
1321.20558829],
42-
[-3105.8424597, -2334.46956224,
43-
-1636.50250136, -1004.95578812,
44-
-433.50869074, 83.55802539,
45-
551.41933777, 974.75775966,
46-
1357.81020428],
47-
[-3025.18797962, -2261.49037074,
48-
-1570.46819815, -945.2054797,
49-
-379.44437595, 132.4774404,
50-
595.68345494, 1014.80958915,
51-
1394.05059827]],
42+
[-2408.18220682, -1703.20046036,
43+
-1065.30659713, -488.11636094,
44+
34.14696209, 506.71035883,
45+
934.30340259, 1321.20558829,
46+
1671.28916302],
47+
[-1703.20046036, -1065.30659713,
48+
-488.11636094, 34.14696209,
49+
506.71035883, 934.30340259,
50+
1321.20558829, 1671.28916302,
51+
1988.05788088]],
5252
[[-3187.30753078, -2408.18220682,
5353
-1703.20046036, -1065.30659713,
5454
-488.11636094, 34.14696209,
5555
506.71035883, 934.30340259,
5656
1321.20558829],
57-
[-3105.8424597, -2334.46956224,
58-
-1636.50250136, -1004.95578812,
59-
-433.50869074, 83.55802539,
60-
551.41933777, 974.75775966,
61-
1357.81020428],
62-
[-3025.18797962, -2261.49037074,
63-
-1570.46819815, -945.2054797,
64-
-379.44437595, 132.4774404,
65-
595.68345494, 1014.80958915,
66-
1394.05059827]]
57+
[-2408.18220682, -1703.20046036,
58+
-1065.30659713, -488.11636094,
59+
34.14696209, 506.71035883,
60+
934.30340259, 1321.20558829,
61+
1671.28916302],
62+
[-1703.20046036, -1065.30659713,
63+
-488.11636094, 34.14696209,
64+
506.71035883, 934.30340259,
65+
1321.20558829, 1671.28916302,
66+
1988.05788088]]
6767
])
6868
# Make some silly data that the code won't be able to fit any values to.
6969
signal_fail_fit = np.arange(0, 9) % 2
@@ -147,20 +147,61 @@ def test_three_param_fit(self):
147147
def test_tss(self):
148148

149149
mapper = T1(self.correct_signal_two_param_tss, self.t, self.affine,
150-
tss=10, mag_corr=True)
150+
tss=100, mag_corr=True)
151151
assert mapper.shape == self.correct_signal_two_param_tss.shape[:-1]
152-
npt.assert_almost_equal(mapper.t1_map.mean(), self.t1)
153-
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0)
154-
npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1)
152+
npt.assert_almost_equal(mapper.t1_map.mean(), self.t1, 4)
153+
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0, 4)
154+
npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1, 4)
155155
npt.assert_almost_equal(mapper.r2.mean(), 1)
156156

157157
def test_tss_axis(self):
158158
signal_array = np.swapaxes(self.correct_signal_two_param_tss, 0, 1)
159-
mapper = T1(signal_array, self.t, self.affine, tss=10, tss_axis=0,
159+
mapper = T1(signal_array, self.t, self.affine, tss=100, tss_axis=0,
160160
mag_corr=True)
161-
npt.assert_almost_equal(mapper.t1_map.mean(), self.t1)
162-
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0)
163-
npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1)
161+
npt.assert_almost_equal(mapper.t1_map.mean(), self.t1, 4)
162+
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0, 4)
163+
npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1, 4)
164+
npt.assert_almost_equal(mapper.r2.mean(), 1)
165+
166+
def test_acq_order(self):
167+
# Descending order
168+
signal_array = np.tile(self.correct_signal_two_param_tss, (10, 10, 1, 1))
169+
signal_array = signal_array[:, :, ::-1, :]
170+
mapper = T1(signal_array, self.t, self.affine, tss=100,
171+
acq_order='descend', mag_corr=True)
172+
npt.assert_almost_equal(mapper.t1_map.mean(), self.t1, 4)
173+
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0, 4)
174+
npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1, 4)
175+
npt.assert_almost_equal(mapper.r2.mean(), 1)
176+
177+
# Centric order
178+
signal_array_asc = np.tile(self.correct_signal_two_param_tss,
179+
(10, 10, 1, 1))
180+
signal_array = np.zeros(signal_array_asc.shape)
181+
signal_array[:, :, 0, :] = signal_array_asc[:, :, 2, :]
182+
signal_array[:, :, 1, :] = signal_array_asc[:, :, 0, :]
183+
signal_array[:, :, 2, :] = signal_array_asc[:, :, 1, :]
184+
185+
mapper = T1(signal_array, self.t, self.affine, tss=100, tss_axis=2,
186+
acq_order='centric', mag_corr=True)
187+
npt.assert_almost_equal(mapper.t1_map.mean(), self.t1, 4)
188+
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0, 4)
189+
npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1, 4)
190+
npt.assert_almost_equal(mapper.r2.mean(), 1)
191+
192+
# Custom order
193+
signal_array_asc = np.tile(self.correct_signal_two_param_tss,
194+
(10, 10, 1, 1))
195+
signal_array = np.zeros(signal_array_asc.shape)
196+
signal_array[:, :, 1, :] = signal_array_asc[:, :, 0, :]
197+
signal_array[:, :, 0, :] = signal_array_asc[:, :, 1, :]
198+
signal_array[:, :, 2, :] = signal_array_asc[:, :, 2, :]
199+
acq_order = [1, 0, 2]
200+
mapper = T1(signal_array, self.t, self.affine, tss=100, tss_axis=2,
201+
acq_order=acq_order, mag_corr=True)
202+
npt.assert_almost_equal(mapper.t1_map.mean(), self.t1, 4)
203+
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0, 4)
204+
npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1, 4)
164205
npt.assert_almost_equal(mapper.r2.mean(), 1)
165206

166207
def test_failed_fit(self):
@@ -268,6 +309,28 @@ def test_tss_valid_axis(self):
268309
inversion_list=np.linspace(0, 2000, 10),
269310
affine=self.affine, tss=1, tss_axis=2)
270311

312+
def test_acq_order_options(self):
313+
# Invalid string
314+
with pytest.raises(ValueError):
315+
mapper = T1(pixel_array=np.zeros((5, 5, 5, 10)),
316+
inversion_list=np.linspace(0, 2000, 10),
317+
affine=self.affine, tss=1, tss_axis=2,
318+
acq_order='invalid')
319+
320+
# List length doesn't match number of slices
321+
with pytest.raises(AssertionError):
322+
mapper = T1(pixel_array=np.zeros((5, 5, 5, 4)),
323+
inversion_list=np.linspace(0, 2000, 10),
324+
affine=self.affine, tss=1, tss_axis=2,
325+
acq_order=[0, 1, 2])
326+
327+
# List type not int
328+
with pytest.raises(AssertionError):
329+
mapper = T1(pixel_array=np.zeros((5, 5, 5, 4)),
330+
inversion_list=np.linspace(0, 2000, 10),
331+
affine=self.affine, tss=1, tss_axis=2,
332+
acq_order=[0.0, 1.0, 2.0, 3.0])
333+
271334
def test_mag_corr_options(self):
272335
# Test that the mag_corr option can be set to True, False, auto is
273336
# checked more thoroughly in the next test

0 commit comments

Comments
 (0)