Skip to content

Commit d872094

Browse files
author
dPys
committed
[ENH] Add docstring and first half of unit tests
1 parent a888d73 commit d872094

File tree

2 files changed

+197
-54
lines changed

2 files changed

+197
-54
lines changed

dmriprep/data/tests/dwi_b0.nii.gz

128 Bytes
Binary file not shown.

dmriprep/utils/images.py

Lines changed: 197 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
1+
import os
12
import numpy as np
23
import nibabel as nb
34
from nipype.utils.filemanip import fname_presuffix
45

56

6-
def extract_b0(in_file, b0_ixs, newpath=None):
7-
"""Extract the *b0* volumes from a DWI dataset."""
8-
out_file = fname_presuffix(in_file, suffix="_b0", newpath=newpath)
7+
def extract_b0(in_file, b0_ixs, out_path=None):
8+
"""
9+
Extract the *b0* volumes from a DWI dataset.
10+
11+
Parameters
12+
----------
13+
in_file : str
14+
DWI NIfTI file.
15+
b0_ixs : list
16+
List of B0 indices in `in_file`.
17+
out_path : str
18+
Optionally specify an output path.
19+
20+
Returns
21+
-------
22+
out_path : str
23+
4D NIfTI file consisting of B0's.
24+
25+
Examples
26+
--------
27+
>>> os.chdir(tmpdir)
28+
>>> b0_ixs = np.where(np.loadtxt(str(data_dir / 'bval')) <= 50)[0].tolist()[:2]
29+
>>> in_file = str(data_dir / 'dwi.nii.gz')
30+
>>> out_path = extract_b0(in_file, b0_ixs)
31+
>>> assert os.path.isfile(out_path)
32+
"""
33+
if out_path is None:
34+
out_path = fname_presuffix(
35+
in_file, suffix="_b0"
36+
)
37+
else:
38+
out_path = fname_presuffix(in_file, suffix="_b0",
39+
newpath=out_path)
940

1041
img = nb.load(in_file)
1142
data = img.get_fdata(dtype="float32")
@@ -16,13 +47,43 @@ def extract_b0(in_file, b0_ixs, newpath=None):
1647
hdr.set_data_shape(b0.shape)
1748
hdr.set_xyzt_units("mm")
1849
hdr.set_data_dtype(np.float32)
19-
nb.Nifti1Image(b0, img.affine, hdr).to_filename(out_file)
20-
return out_file
50+
nb.Nifti1Image(b0, img.affine, hdr).to_filename(out_path)
51+
return out_path
2152

2253

23-
def rescale_b0(in_file, mask_file, newpath=None):
24-
"""Rescale the input volumes using the median signal intensity."""
25-
out_file = fname_presuffix(in_file, suffix="_rescaled_b0", newpath=newpath)
54+
def rescale_b0(in_file, mask_file, out_path=None):
55+
"""
56+
Rescale the input volumes using the median signal intensity.
57+
58+
Parameters
59+
----------
60+
in_file : str
61+
A NIfTI file consisting of one or more B0's.
62+
mask_file : str
63+
A B0 mask NIFTI file.
64+
out_path : str
65+
Optionally specify an output path.
66+
67+
Returns
68+
-------
69+
out_path : str
70+
A rescaled B0 NIFTI file.
71+
72+
Examples
73+
--------
74+
>>> os.chdir(tmpdir)
75+
>>> mask_file = str(data_dir / 'dwi_mask.nii.gz')
76+
>>> in_file = str(data_dir / 'dwi_b0.nii.gz')
77+
>>> out_path = rescale_b0(in_file, mask_file)
78+
>>> assert os.path.isfile(out_path)
79+
"""
80+
if out_path is None:
81+
out_path = fname_presuffix(
82+
in_file, suffix="_rescaled_b0"
83+
)
84+
else:
85+
out_path = fname_presuffix(in_file, suffix="_rescaled_b0",
86+
newpath=out_path)
2687

2788
img = nb.load(in_file)
2889
if img.dataobj.ndim == 3:
@@ -35,31 +96,79 @@ def rescale_b0(in_file, mask_file, newpath=None):
3596
median_signal = np.median(data[mask_data > 0, ...], axis=0)
3697
rescaled_data = 1000 * data / median_signal
3798
hdr = img.header.copy()
38-
nb.Nifti1Image(rescaled_data, img.affine, hdr).to_filename(out_file)
39-
return out_file
99+
nb.Nifti1Image(rescaled_data, img.affine, hdr).to_filename(out_path)
100+
return out_path
40101

41102

42-
def median(in_file, newpath=None):
43-
"""Average a 4D dataset across the last dimension using median."""
44-
out_file = fname_presuffix(in_file, suffix="_b0ref", newpath=newpath)
103+
def median(in_file, out_path=None):
104+
"""
105+
Summarize a 4D dataset across the last dimension using median.
106+
107+
Parameters
108+
----------
109+
in_file : str
110+
A NIfTI file consisting of one or more B0's.
111+
out_path : str
112+
Optionally specify an output path for `out_path`.
113+
114+
Returns
115+
-------
116+
out_path : str
117+
A 3D B0 NIFTI file.
118+
119+
Examples
120+
--------
121+
>>> os.chdir(tmpdir)
122+
>>> in_file = str(data_dir / 'dwi_b0.nii.gz')
123+
>>> out_path = median(in_file)
124+
>>> assert os.path.isfile(out_path)
125+
"""
126+
if out_path is None:
127+
out_path = fname_presuffix(
128+
in_file, suffix="_b0ref"
129+
)
130+
else:
131+
out_path = fname_presuffix(in_file, suffix="_b0ref",
132+
newpath=out_path)
45133

46134
img = nb.load(in_file)
47135
if img.dataobj.ndim == 3:
48136
return in_file
49137
if img.shape[-1] == 1:
50-
nb.squeeze_image(img).to_filename(out_file)
51-
return out_file
138+
nb.squeeze_image(img).to_filename(out_path)
139+
return out_path
52140

53141
median_data = np.median(img.get_fdata(), axis=-1)
54142

55143
hdr = img.header.copy()
56144
hdr.set_xyzt_units("mm")
57-
nb.Nifti1Image(median_data, img.affine, hdr).to_filename(out_file)
58-
return out_file
145+
nb.Nifti1Image(median_data, img.affine, hdr).to_filename(out_path)
146+
return out_path
59147

60148

61149
def average_images(images, out_path=None):
62-
"""Average a 4D dataset across the last dimension using mean."""
150+
"""
151+
Average a 4D dataset across the last dimension using mean.
152+
153+
Parameters
154+
----------
155+
images : str
156+
A list of NIFTI file paths.
157+
out_path : str
158+
Optionally specify an output path for `out_path`.
159+
160+
Returns
161+
-------
162+
out_path : str
163+
A 3D NIFTI file averaged along the 4th dimension.
164+
165+
Examples
166+
--------
167+
>>> os.chdir(tmpdir)
168+
>>> in_file = str(data_dir / 'dwi_b0.nii.gz')
169+
>>> out_path = average_images(in_file)
170+
>>> assert os.path.isfile(out_path)
171+
"""
63172
from nilearn.image import mean_img
64173

65174
average_img = mean_img([nb.load(img) for img in images])
@@ -71,19 +180,39 @@ def average_images(images, out_path=None):
71180
return out_path
72181

73182

74-
def quick_load_images(image_list, dtype=np.float32):
75-
"""Load 3D volumes from a list of file paths into a 4D array."""
76-
return nb.concat_images([nb.load(fname) for fname in image_list]).get_fdata(dtype=dtype)
183+
def get_list_data(file_list, dtype=np.float32):
184+
"""
185+
Load 3D volumes from a list of file paths into a 4D array.
186+
187+
Parameters
188+
----------
189+
file_list : str
190+
A list of file paths to 3D NIFTI images.
191+
"""
192+
return nb.concat_images([nb.load(fname) for fname in file_list]).get_fdata(dtype=dtype)
77193

78194

79-
def match_transforms(dwi_files, transforms, b0_indices):
195+
def match_transforms(dwi_files, transforms, b0_ixs):
80196
"""
81197
Arrange the order of a list of transforms.
82-
198+
83199
This is a helper function for :abbr:`EMC (Eddy-currents and Motion Correction)`.
84200
Sorts the input list of affine transforms to correspond with that of
85201
each individual dwi volume file, accounting for the indices of :math:`b = 0` volumes.
86-
202+
203+
Parameters
204+
----------
205+
dwi_files : list
206+
A list of file paths to 3D diffusion-weighted NIFTI volumes.
207+
transforms : list
208+
A list of ndarrays.
209+
b0_ixs : list
210+
List of B0 indices.
211+
212+
Returns
213+
-------
214+
out_path : str
215+
A 3D NIFTI file averaged along the 4th dimension.
87216
"""
88217
num_dwis = len(dwi_files)
89218
num_transforms = len(transforms)
@@ -92,21 +221,33 @@ def match_transforms(dwi_files, transforms, b0_indices):
92221
return transforms
93222

94223
# Do sanity checks
95-
if not len(transforms) == len(b0_indices):
224+
if not len(transforms) == len(b0_ixs):
96225
raise Exception("number of transforms does not match number of b0 images")
97226

98227
# Create a list of which emc affines go with each of the split images
99228
nearest_affines = []
100229
for index in range(num_dwis):
101-
nearest_b0_num = np.argmin(np.abs(index - np.array(b0_indices)))
230+
nearest_b0_num = np.argmin(np.abs(index - np.array(b0_ixs)))
102231
this_transform = transforms[nearest_b0_num]
103232
nearest_affines.append(this_transform)
104233

105234
return nearest_affines
106235

107236

108237
def save_4d_to_3d(in_file):
109-
"""Split a 4D dataset along the last dimension into multiple 3D volumes."""
238+
"""
239+
Split a 4D dataset along the last dimension into multiple 3D volumes.
240+
241+
Parameters
242+
----------
243+
in_file : str
244+
DWI NIfTI file.
245+
246+
Returns
247+
-------
248+
out_files : list
249+
A list of file paths to 3d NIFTI images.
250+
"""
110251
files_3d = nb.four_to_three(nb.load(in_file))
111252
out_files = []
112253
for i, file_3d in enumerate(files_3d):
@@ -118,7 +259,22 @@ def save_4d_to_3d(in_file):
118259

119260

120261
def prune_b0s_from_dwis(in_files, b0_ixs):
121-
"""Remove *b0* volume files from a complete list of DWI volume files."""
262+
"""
263+
Remove *b0* volume files from a complete list of DWI volume files.
264+
265+
Parameters
266+
----------
267+
in_files : list
268+
A list of NIfTI file paths corresponding to each 3D volume of a
269+
DWI image (i.e. including B0's).
270+
b0_ixs : list
271+
List of B0 indices.
272+
273+
Returns
274+
-------
275+
out_files : list
276+
A list of file paths to 3d NIFTI images.
277+
"""
122278
if in_files[0].endswith("_warped.nii.gz"):
123279
out_files = [
124280
i
@@ -143,34 +299,21 @@ def prune_b0s_from_dwis(in_files, b0_ixs):
143299

144300

145301
def save_3d_to_4d(in_files):
146-
"""Concatenate a list of 3D volumes into a 4D output."""
302+
"""
303+
Concatenate a list of 3D volumes into a 4D output.
304+
305+
Parameters
306+
----------
307+
in_files : list
308+
A list of file paths to 3D NIFTI images.
309+
310+
Returns
311+
-------
312+
out_file : str
313+
A file path to a 4d NIFTI image of concatenated 3D volumes.
314+
"""
147315
img_4d = nb.funcs.concat_images([nb.load(img_3d) for img_3d in in_files])
148316
out_file = fname_presuffix(in_files[0], suffix="_merged")
149317
img_4d.to_filename(out_file)
150318
return out_file
151319

152-
153-
def decompose_affine(affine):
154-
"""Takes an transformation affine matrix A and determines
155-
rotations and translations."""
156-
157-
def rang(b):
158-
a = min(max(b, -1), 1)
159-
return a
160-
161-
Ry = np.arcsin(A[0, 2])
162-
# Rx = np.arcsin(A[1, 2] / np.cos(Ry))
163-
# Rz = np.arccos(A[0, 1] / np.sin(Ry))
164-
165-
if (abs(Ry) - np.pi / 2) ** 2 < 1e-9:
166-
Rx = 0
167-
Rz = np.arctan2(-rang(A[1, 0]), rang(-A[2, 0] / A[0, 2]))
168-
else:
169-
c = np.cos(Ry)
170-
Rx = np.arctan2(rang(A[1, 2] / c), rang(A[2, 2] / c))
171-
Rz = np.arctan2(rang(A[0, 1] / c), rang(A[0, 0] / c))
172-
173-
rotations = [Rx, Ry, Rz]
174-
translations = [A[0, 3], A[1, 3], A[2, 3]]
175-
176-
return rotations, translations

0 commit comments

Comments
 (0)