Skip to content

Commit 35ddcc1

Browse files
committed
Merge pull request #427 from grlee77/parrec_labeling
MRG: add a function to retrieve .PAR data labels for the 4th dimension This was pulled out of the previously submitted #278 to make reviewing easier. This complements the functionality in the recently merged #409. The primary enhancement is a new method in the PARREC header called `get_volume_labels` that lists which .PAR fields the data stored along the 4th dimension correspond to. (i.e. all data >4D is still collapsed into 4 dimensions exactly as before, but labels can be retrieved to make the data ordering unambiguous). The script interface to parrec can write these labels out as a CSV file with one row per volume.
2 parents 1627b5c + aecbd10 commit 35ddcc1

File tree

4 files changed

+123
-3
lines changed

4 files changed

+123
-3
lines changed

bin/parrec2nii

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import numpy as np
88
import numpy.linalg as npl
99
import sys
1010
import os
11+
import csv
1112
import nibabel
1213
import nibabel.parrec as pr
1314
from nibabel.parrec import one_line
@@ -66,6 +67,18 @@ def get_opt_parser():
6667
"""The magnetic field strength of the recording, only needed
6768
for --dwell-time. The field strength must be supplied
6869
because it is not encoded in the PAR/REC format.""")))
70+
p.add_option(
71+
Option("-i", "--volume-info", action="store_true", dest="vol_info",
72+
default=False,
73+
help=one_line(
74+
"""Export .PAR volume labels corresponding to the fourth
75+
dimension of the data. The dimension info will be stored in
76+
CSV format with the first row containing dimension labels
77+
and the subsequent rows (one per volume), the corresponding
78+
indices. Only labels that vary along the 4th dimension are
79+
exported (e.g. for a single volume structural scan there
80+
are no dynamic labels and no output file will be created).
81+
""")))
6982
p.add_option(
7083
Option("--origin", action="store", dest="origin", default="scanner",
7184
help=one_line(
@@ -262,6 +275,17 @@ def proc_file(infile, opts):
262275
fid.write('%s ' % val)
263276
fid.write('\n')
264277

278+
# export data labels varying along the 4th dimensions if requested
279+
if opts.vol_info:
280+
labels = pr_img.header.get_volume_labels()
281+
if len(labels) > 0:
282+
vol_keys = list(labels.keys())
283+
with open(basefilename + '.ordering.csv', 'w') as csvfile:
284+
csvwriter = csv.writer(csvfile, delimiter=',')
285+
csvwriter.writerow(vol_keys)
286+
for vals in zip(*[labels[k] for k in vol_keys]):
287+
csvwriter.writerow(vals)
288+
265289
# write out dwell time if requested
266290
if opts.dwell_time:
267291
try:

nibabel/parrec.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
import re
9696
from io import StringIO
9797
from locale import getpreferredencoding
98+
from collections import OrderedDict
9899

99100
from .keywordonly import kw_only_meth
100101
from .spatialimages import SpatialHeader, SpatialImage
@@ -1123,6 +1124,57 @@ def get_sorted_slice_indices(self):
11231124
n_used = np.prod(self.get_data_shape()[2:])
11241125
return sort_order[:n_used]
11251126

1127+
def get_volume_labels(self):
1128+
""" Dynamic labels corresponding to the final data dimension(s).
1129+
1130+
This is useful for custom data sorting. A subset of the info in
1131+
``self.image_defs`` is returned in an order that matches the final
1132+
data dimension(s). Only labels that have more than one unique value
1133+
across the dataset will be returned.
1134+
1135+
Returns
1136+
-------
1137+
sort_info : dict
1138+
Each key corresponds to volume labels for a dynamically varying
1139+
sequence dimension. The ordering of the labels matches the volume
1140+
ordering determined via ``self.get_sorted_slice_indices``.
1141+
"""
1142+
sorted_indices = self.get_sorted_slice_indices()
1143+
image_defs = self.image_defs
1144+
1145+
# define which keys which might vary across image volumes
1146+
dynamic_keys = ['cardiac phase number',
1147+
'echo number',
1148+
'label type',
1149+
'image_type_mr',
1150+
'dynamic scan number',
1151+
'scanning sequence',
1152+
'gradient orientation number',
1153+
'diffusion b value number']
1154+
1155+
# remove dynamic keys that may not be present in older .PAR versions
1156+
dynamic_keys = [d for d in dynamic_keys if d in
1157+
image_defs.dtype.fields]
1158+
1159+
non_unique_keys = []
1160+
for key in dynamic_keys:
1161+
ndim = image_defs[key].ndim
1162+
if ndim == 1:
1163+
num_unique = len(np.unique(image_defs[key]))
1164+
else:
1165+
raise ValueError("unexpected image_defs shape > 1D")
1166+
if num_unique > 1:
1167+
non_unique_keys.append(key)
1168+
1169+
# each key in dynamic keys will be identical across slices, so use
1170+
# the value at slice 1.
1171+
sl1_indices = image_defs['slice number'][sorted_indices] == 1
1172+
1173+
sort_info = OrderedDict()
1174+
for key in non_unique_keys:
1175+
sort_info[key] = image_defs[key][sorted_indices][sl1_indices]
1176+
return sort_info
1177+
11261178

11271179
class PARRECImage(SpatialImage):
11281180
"""PAR/REC image"""

nibabel/tests/test_parrec.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,16 @@ def test_header_scaling():
191191
assert_false(np.all(fp_scaling == dv_scaling))
192192

193193

194+
def test_header_volume_labels():
195+
hdr = PARRECHeader(HDR_INFO, HDR_DEFS)
196+
# check volume labels
197+
vol_labels = hdr.get_volume_labels()
198+
assert_equal(list(vol_labels.keys()), ['dynamic scan number'])
199+
assert_array_equal(vol_labels['dynamic scan number'], [1, 2, 3])
200+
# check that output is ndarray rather than list
201+
assert_true(isinstance(vol_labels['dynamic scan number'], np.ndarray))
202+
203+
194204
def test_orientation():
195205
hdr = PARRECHeader(HDR_INFO, HDR_DEFS)
196206
assert_array_equal(HDR_DEFS['slice orientation'], 1)
@@ -285,6 +295,11 @@ def test_sorting_dual_echo_T1():
285295
# second half (volume 2) should all correspond to echo 2
286296
assert_equal(np.all(sorted_echos[n_half:] == 2), True)
287297

298+
# check volume labels
299+
vol_labels = t1_hdr.get_volume_labels()
300+
assert_equal(list(vol_labels.keys()), ['echo number'])
301+
assert_array_equal(vol_labels['echo number'], [1, 2])
302+
288303

289304
def test_sorting_multiple_echos_and_contrasts():
290305
# This .PAR file has 3 echos and 4 image types (real, imaginary, magnitude,
@@ -327,6 +342,13 @@ def test_sorting_multiple_echos_and_contrasts():
327342
assert_equal(np.all(sorted_types[ntotal//2:3*ntotal//4] == 2), True)
328343
assert_equal(np.all(sorted_types[3*ntotal//4:ntotal] == 3), True)
329344

345+
# check volume labels
346+
vol_labels = t1_hdr.get_volume_labels()
347+
assert_equal(list(vol_labels.keys()), ['echo number', 'image_type_mr'])
348+
assert_array_equal(vol_labels['echo number'], [1, 2, 3]*4)
349+
assert_array_equal(vol_labels['image_type_mr'],
350+
[0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3])
351+
330352

331353
def test_sorting_multiecho_ASL():
332354
# For this .PAR file has 3 keys corresponding to volumes:
@@ -368,6 +390,15 @@ def test_sorting_multiecho_ASL():
368390
# check that slices vary fastest
369391
assert_array_equal(sorted_slices[:nslices], np.arange(1, nslices+1))
370392

393+
# check volume labels
394+
vol_labels = asl_hdr.get_volume_labels()
395+
assert_equal(list(vol_labels.keys()),
396+
['echo number', 'label type', 'dynamic scan number'])
397+
assert_array_equal(vol_labels['dynamic scan number'], [1]*6 + [2]*6)
398+
assert_array_equal(vol_labels['label type'], [1]*3 + [2]*3 + [1]*3 + [2]*3)
399+
assert_array_equal(vol_labels['echo number'], [1, 2, 3]*4)
400+
401+
371402
def test_vol_number():
372403
# Test algorithm for calculating volume number
373404
assert_array_equal(vol_numbers([1, 3, 0]), [0, 0, 0])

nibabel/tests/test_scripts.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import os
1010
from os.path import (dirname, join as pjoin, abspath, splitext, basename,
1111
exists)
12-
import re
12+
import csv
1313
from glob import glob
1414

1515
import numpy as np
@@ -18,8 +18,7 @@
1818
from ..loadsave import load
1919
from ..orientations import flip_axis, aff2axcodes, inv_ornt_aff
2020

21-
from nose.tools import (assert_true, assert_false, assert_not_equal,
22-
assert_equal)
21+
from nose.tools import assert_true, assert_false, assert_equal
2322
from nose import SkipTest
2423

2524
from numpy.testing import assert_almost_equal
@@ -333,3 +332,17 @@ def test_parrec2nii_with_data():
333332
data_sorted = img.get_data().copy()
334333
assert_almost_equal(data[..., np.argsort(DTI_PAR_BVALS)], data_sorted)
335334
del img
335+
336+
# Writes .ordering.csv if requested
337+
run_command(['parrec2nii', '--overwrite', '--volume-info', dti_par])
338+
assert_true(exists('DTI.ordering.csv'))
339+
with open('DTI.ordering.csv', 'r') as csvfile:
340+
csvreader = csv.reader(csvfile, delimiter=',')
341+
csv_keys = csvreader.__next__() # header row
342+
nlines = 0 # count number of non-header rows
343+
for line in csvreader:
344+
nlines += 1
345+
346+
assert_equal(sorted(csv_keys), ['diffusion b value number',
347+
'gradient orientation number'])
348+
assert_equal(nlines, 8) # 8 volumes present in DTI.PAR

0 commit comments

Comments
 (0)