@@ -734,25 +734,23 @@ def get_bvals_bvecs(self):
734
734
n_slices , n_vols = self .get_data_shape ()[- 2 :]
735
735
bvals = self .image_defs ['diffusion_b_factor' ][reorder ].reshape (
736
736
(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 ))
740
739
bvals = bvals [0 ]
741
740
if 'diffusion' not in self .image_defs .dtype .names :
742
741
return bvals , None
743
742
bvecs = self .image_defs ['diffusion' ][reorder ].reshape (
744
743
(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 ))
748
746
bvecs = bvecs [0 ]
749
747
# rotate bvecs to match stored image orientation
750
748
permute_to_psl = ACQ_TO_PSL [self .get_slice_orientation ()]
751
749
bvecs = apply_affine (np .linalg .inv (permute_to_psl ), bvecs )
752
750
return bvals , bvecs
753
751
754
752
def get_def (self , name ):
755
- """ Return a single image definition field. """
753
+ """Return a single image definition field (or None if missing) """
756
754
idef = self .image_defs
757
755
return idef [name ] if name in idef .dtype .names else None
758
756
@@ -1008,33 +1006,48 @@ def get_rec_shape(self):
1008
1006
inplane_shape = tuple (self ._get_unique_image_prop ('recon resolution' ))
1009
1007
return inplane_shape + (len (self .image_defs ),)
1010
1008
1011
- def _strict_sort_keys (self ):
1009
+ def _strict_sort_order (self ):
1012
1010
""" Determine the sort order based on several image definition fields.
1013
1011
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
1018
1023
1019
1024
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' ]
1031
1025
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:
1036
1028
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 :
1038
1051
bvals = self .get_def ('diffusion b value number' )
1039
1052
if bvals is None :
1040
1053
bvals = self .get_def ('diffusion_b_factor' )
@@ -1047,30 +1060,34 @@ def _strict_sort_keys(self):
1047
1060
else :
1048
1061
diffusion_keys = ()
1049
1062
1050
- # Define the desired sort order (last key is highest precedence)
1063
+ # initial sort (last key is highest precedence)
1051
1064
keys = (slice_nos , echos , phases ) + \
1052
1065
diffusion_keys + asl_keys + (dynamics , image_type )
1053
-
1054
1066
initial_sort_order = np .lexsort (keys )
1067
+
1068
+ # sequentially number the volumes based on the initial sort
1055
1069
vol_nos = vol_numbers (slice_nos [initial_sort_order ])
1070
+ # identify truncated volumes
1056
1071
is_full = vol_is_full (slice_nos [initial_sort_order ],
1057
1072
self .general_info ['max_slices' ])
1058
1073
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 ))]
1069
1076
1077
+ def _lax_sort_order (self ):
1078
+ """
1070
1079
Sorts by (fast to slow): slice number, volume number.
1071
1080
1072
1081
We calculate volume number by looking for repeating slice numbers (see
1073
1082
: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.
1074
1091
1075
1092
If the recording is truncated, the returned indices take care of
1076
1093
discarding any slice indices from incomplete volumes.
@@ -1088,13 +1105,10 @@ def get_sorted_slice_indices(self):
1088
1105
``self.image_defs``.
1089
1106
"""
1090
1107
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 ()
1094
1109
else :
1095
- keys = self ._strict_sort_keys ()
1110
+ sort_order = self ._strict_sort_order ()
1096
1111
1097
- sort_order = np .lexsort (keys )
1098
1112
# Figure out how many we need to remove from the end, and trim them.
1099
1113
# Based on our sorting, they should always be last.
1100
1114
n_used = np .prod (self .get_data_shape ()[2 :])
0 commit comments