Skip to content

Commit 9b7a5fd

Browse files
committed
MAINT: simplify logic in _strict_sort_order and improve the docstring
1 parent 2c9f0f8 commit 9b7a5fd

File tree

1 file changed

+59
-45
lines changed

1 file changed

+59
-45
lines changed

nibabel/parrec.py

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -734,23 +734,21 @@ def get_bvals_bvecs(self):
734734
n_slices, n_vols = self.get_data_shape()[-2:]
735735
bvals = self.image_defs['diffusion_b_factor'][reorder].reshape(
736736
(n_slices, n_vols), order='F')
737-
if not self.strict_sort:
738-
# All bvals within volume should be the same
739-
assert not np.any(np.diff(bvals, axis=0))
737+
# All bvals within volume should be the same
738+
assert not np.any(np.diff(bvals, axis=0))
740739
bvals = bvals[0]
741740
bvecs = self.image_defs['diffusion'][reorder].reshape(
742741
(n_slices, n_vols, 3), order='F')
743-
if not self.strict_sort:
744-
# All 3 values of bvecs should be same within volume
745-
assert not np.any(np.diff(bvecs, axis=0))
742+
# All 3 values of bvecs should be same within volume
743+
assert not np.any(np.diff(bvecs, axis=0))
746744
bvecs = bvecs[0]
747745
# rotate bvecs to match stored image orientation
748746
permute_to_psl = ACQ_TO_PSL[self.get_slice_orientation()]
749747
bvecs = apply_affine(np.linalg.inv(permute_to_psl), bvecs)
750748
return bvals, bvecs
751749

752750
def get_def(self, name):
753-
""" Return a single image definition field. """
751+
"""Return a single image definition field (or None if missing) """
754752
idef = self.image_defs
755753
return idef[name] if name in idef.dtype.names else None
756754

@@ -1006,33 +1004,48 @@ def get_rec_shape(self):
10061004
inplane_shape = tuple(self._get_unique_image_prop('recon resolution'))
10071005
return inplane_shape + (len(self.image_defs),)
10081006

1009-
def _strict_sort_keys(self):
1007+
def _strict_sort_order(self):
10101008
""" Determine the sort order based on several image definition fields.
10111009
1012-
If the sort keys are not unique for each volume, we calculate a volume
1013-
number by looking for repeating slice numbers
1014-
(see :func:`vol_numbers`). This may occur for diffusion scans from
1015-
older V4 .PAR format, where diffusion direction info is not stored.
1010+
The fields taken into consideration, if present, are (in order from
1011+
slowest to fastest variation after sorting):
1012+
1013+
- image_defs['image_type_mr'] # Re, Im, Mag, Phase
1014+
- image_defs['dynamic scan number'] # repetition
1015+
- image_defs['label type'] # ASL tag/control
1016+
- image_defs['diffusion b value number'] # diffusion b value
1017+
- image_defs['gradient orientation number'] # diffusion directoin
1018+
- image_defs['cardiac phase number'] # cardiac phase
1019+
- image_defs['echo number'] # echo
1020+
- image_defs['slice number'] # slice
10161021
10171022
Data sorting is done in two stages:
1018-
- run an initial sort using several keys of interest
1019-
- call `vol_is_full` to identify potentially missing volumes
1020-
and add the result to the list of sort keys
1021-
"""
1022-
# Sort based on a larger number of keys. This is more complicated
1023-
# but works for .PAR files that get missorted by the above method
1024-
slice_nos = self.image_defs['slice number']
1025-
dynamics = self.image_defs['dynamic scan number']
1026-
phases = self.image_defs['cardiac phase number']
1027-
echos = self.image_defs['echo number']
1028-
image_type = self.image_defs['image_type_mr']
10291023
1030-
# try adding keys only present in a subset of .PAR files
1031-
idefs = self.image_defs
1032-
asl_keys = (idefs['label type'], ) if 'label type' in \
1033-
idefs.dtype.names else ()
1024+
1. an initial sort using the keys described above
1025+
2. a resort after generating two additional sort keys:
10341026
1035-
if not self.general_info['diffusion'] == 0:
1027+
* a key to assign unique volume numbers to any volumes that
1028+
didn't have a unique sort based on the keys above
1029+
(see :func:`vol_numbers`).
1030+
* a sort key based on `vol_is_full` to identify truncated
1031+
volumes
1032+
1033+
A case where the initial sort may not create a unique label for each
1034+
volume is diffusion scans acquired in the older V4 .PAR format, where
1035+
diffusion direction info is not available.
1036+
"""
1037+
# sort keys present in all supported .PAR versions
1038+
idefs = self.image_defs
1039+
slice_nos = idefs['slice number']
1040+
dynamics = idefs['dynamic scan number']
1041+
phases = idefs['cardiac phase number']
1042+
echos = idefs['echo number']
1043+
image_type = idefs['image_type_mr']
1044+
1045+
# sort keys only present in a subset of .PAR files
1046+
asl_keys = ((idefs['label type'], ) if 'label type' in
1047+
idefs.dtype.names else ())
1048+
if self.general_info['diffusion'] != 0:
10361049
bvals = self.get_def('diffusion b value number')
10371050
if bvals is None:
10381051
bvals = self.get_def('diffusion_b_factor')
@@ -1045,30 +1058,34 @@ def _strict_sort_keys(self):
10451058
else:
10461059
diffusion_keys = ()
10471060

1048-
# Define the desired sort order (last key is highest precedence)
1061+
# initial sort (last key is highest precedence)
10491062
keys = (slice_nos, echos, phases) + \
10501063
diffusion_keys + asl_keys + (dynamics, image_type)
1051-
10521064
initial_sort_order = np.lexsort(keys)
1065+
1066+
# sequentially number the volumes based on the initial sort
10531067
vol_nos = vol_numbers(slice_nos[initial_sort_order])
1068+
# identify truncated volumes
10541069
is_full = vol_is_full(slice_nos[initial_sort_order],
10551070
self.general_info['max_slices'])
10561071

1057-
# have to "unsort" is_full and volumes to match the other sort keys
1058-
unsort_indices = np.argsort(initial_sort_order)
1059-
is_full = is_full[unsort_indices]
1060-
vol_nos = np.asarray(vol_nos)[unsort_indices]
1061-
1062-
# final set of sort keys
1063-
return (keys[0], vol_nos) + keys[1:] + (np.logical_not(is_full), )
1064-
1065-
def get_sorted_slice_indices(self):
1066-
"""Return indices to sort (and maybe discard) slices in REC file.
1072+
# second stage of sorting
1073+
return initial_sort_order[np.lexsort((vol_nos, is_full))]
10671074

1075+
def _lax_sort_order(self):
1076+
"""
10681077
Sorts by (fast to slow): slice number, volume number.
10691078
10701079
We calculate volume number by looking for repeating slice numbers (see
10711080
:func:`vol_numbers`).
1081+
"""
1082+
slice_nos = self.image_defs['slice number']
1083+
is_full = vol_is_full(slice_nos, self.general_info['max_slices'])
1084+
keys = (slice_nos, vol_numbers(slice_nos), np.logical_not(is_full))
1085+
return np.lexsort(keys)
1086+
1087+
def get_sorted_slice_indices(self):
1088+
"""Return indices to sort (and maybe discard) slices in REC file.
10721089
10731090
If the recording is truncated, the returned indices take care of
10741091
discarding any slice indices from incomplete volumes.
@@ -1086,13 +1103,10 @@ def get_sorted_slice_indices(self):
10861103
``self.image_defs``.
10871104
"""
10881105
if not self.strict_sort:
1089-
slice_nos = self.image_defs['slice number']
1090-
is_full = vol_is_full(slice_nos, self.general_info['max_slices'])
1091-
keys = (slice_nos, vol_numbers(slice_nos), np.logical_not(is_full))
1106+
sort_order = self._lax_sort_order()
10921107
else:
1093-
keys = self._strict_sort_keys()
1108+
sort_order = self._strict_sort_order()
10941109

1095-
sort_order = np.lexsort(keys)
10961110
# Figure out how many we need to remove from the end, and trim them.
10971111
# Based on our sorting, they should always be last.
10981112
n_used = np.prod(self.get_data_shape()[2:])

0 commit comments

Comments
 (0)