Skip to content

Commit eac41a3

Browse files
committed
MAINT: simplify logic in _strict_sort_order and improve the docstring
1 parent 9e98d6e commit eac41a3

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,25 +734,23 @@ 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
if 'diffusion' not in self.image_defs.dtype.names:
742741
return bvals, None
743742
bvecs = self.image_defs['diffusion'][reorder].reshape(
744743
(n_slices, n_vols, 3), order='F')
745-
if not self.strict_sort:
746-
# All 3 values of bvecs should be same within volume
747-
assert not np.any(np.diff(bvecs, axis=0))
744+
# All 3 values of bvecs should be same within volume
745+
assert not np.any(np.diff(bvecs, axis=0))
748746
bvecs = bvecs[0]
749747
# rotate bvecs to match stored image orientation
750748
permute_to_psl = ACQ_TO_PSL[self.get_slice_orientation()]
751749
bvecs = apply_affine(np.linalg.inv(permute_to_psl), bvecs)
752750
return bvals, bvecs
753751

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

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

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

1050-
# Define the desired sort order (last key is highest precedence)
1063+
# initial sort (last key is highest precedence)
10511064
keys = (slice_nos, echos, phases) + \
10521065
diffusion_keys + asl_keys + (dynamics, image_type)
1053-
10541066
initial_sort_order = np.lexsort(keys)
1067+
1068+
# sequentially number the volumes based on the initial sort
10551069
vol_nos = vol_numbers(slice_nos[initial_sort_order])
1070+
# identify truncated volumes
10561071
is_full = vol_is_full(slice_nos[initial_sort_order],
10571072
self.general_info['max_slices'])
10581073

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

1077+
def _lax_sort_order(self):
1078+
"""
10701079
Sorts by (fast to slow): slice number, volume number.
10711080
10721081
We calculate volume number by looking for repeating slice numbers (see
10731082
:func:`vol_numbers`).
1083+
"""
1084+
slice_nos = self.image_defs['slice number']
1085+
is_full = vol_is_full(slice_nos, self.general_info['max_slices'])
1086+
keys = (slice_nos, vol_numbers(slice_nos), np.logical_not(is_full))
1087+
return np.lexsort(keys)
1088+
1089+
def get_sorted_slice_indices(self):
1090+
"""Return indices to sort (and maybe discard) slices in REC file.
10741091
10751092
If the recording is truncated, the returned indices take care of
10761093
discarding any slice indices from incomplete volumes.
@@ -1088,13 +1105,10 @@ def get_sorted_slice_indices(self):
10881105
``self.image_defs``.
10891106
"""
10901107
if not self.strict_sort:
1091-
slice_nos = self.image_defs['slice number']
1092-
is_full = vol_is_full(slice_nos, self.general_info['max_slices'])
1093-
keys = (slice_nos, vol_numbers(slice_nos), np.logical_not(is_full))
1108+
sort_order = self._lax_sort_order()
10941109
else:
1095-
keys = self._strict_sort_keys()
1110+
sort_order = self._strict_sort_order()
10961111

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

0 commit comments

Comments
 (0)