@@ -618,7 +618,8 @@ def __getitem__(self, slicer):
618
618
class PARRECHeader (SpatialHeader ):
619
619
"""PAR/REC header"""
620
620
621
- def __init__ (self , info , image_defs , permit_truncated = False ):
621
+ def __init__ (self , info , image_defs , permit_truncated = False ,
622
+ strict_sort = False ):
622
623
"""
623
624
Parameters
624
625
----------
@@ -631,10 +632,16 @@ def __init__(self, info, image_defs, permit_truncated=False):
631
632
permit_truncated : bool, optional
632
633
If True, a warning is emitted instead of an error when a truncated
633
634
recording is detected.
635
+ strict_sort : bool, optional, keyword-only
636
+ If True, a larger number of header fields are used while sorting
637
+ the REC data array. This may produce a different sort order than
638
+ `strict_sort=False`, where volumes are sorted by the order in which
639
+ the slices appear in the .PAR file.
634
640
"""
635
641
self .general_info = info .copy ()
636
642
self .image_defs = image_defs .copy ()
637
643
self .permit_truncated = permit_truncated
644
+ self .strict_sort = strict_sort
638
645
_truncation_checks (info , image_defs , permit_truncated )
639
646
# charge with basic properties to be able to use base class
640
647
# functionality
@@ -660,14 +667,16 @@ def from_header(klass, header=None):
660
667
'non-PARREC header.' )
661
668
662
669
@classmethod
663
- def from_fileobj (klass , fileobj , permit_truncated = False ):
670
+ def from_fileobj (klass , fileobj , permit_truncated = False ,
671
+ strict_sort = False ):
664
672
info , image_defs = parse_PAR_header (fileobj )
665
- return klass (info , image_defs , permit_truncated )
673
+ return klass (info , image_defs , permit_truncated , strict_sort )
666
674
667
675
def copy (self ):
668
676
return PARRECHeader (deepcopy (self .general_info ),
669
677
self .image_defs .copy (),
670
- self .permit_truncated )
678
+ self .permit_truncated ,
679
+ self .strict_sort )
671
680
672
681
def as_analyze_map (self ):
673
682
"""Convert PAR parameters to NIFTI1 format"""
@@ -740,6 +749,11 @@ def get_bvals_bvecs(self):
740
749
bvecs = apply_affine (np .linalg .inv (permute_to_psl ), bvecs )
741
750
return bvals , bvecs
742
751
752
+ def get_def (self , name ):
753
+ """Return a single image definition field (or None if missing) """
754
+ idef = self .image_defs
755
+ return idef [name ] if name in idef .dtype .names else None
756
+
743
757
def _get_unique_image_prop (self , name ):
744
758
""" Scan image definitions and return unique value of a property.
745
759
@@ -992,31 +1006,113 @@ def get_rec_shape(self):
992
1006
inplane_shape = tuple (self ._get_unique_image_prop ('recon resolution' ))
993
1007
return inplane_shape + (len (self .image_defs ),)
994
1008
995
- def get_sorted_slice_indices (self ):
996
- """Return indices to sort (and maybe discard) slices in REC file.
1009
+ def _strict_sort_order (self ):
1010
+ """ Determine the sort order based on several image definition fields.
1011
+
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
1023
+
1024
+ Data sorting is done in two stages:
1025
+
1026
+ 1. an initial sort using the keys described above
1027
+ 2. a resort after generating two additional sort keys:
1028
+
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 :
1051
+ bvals = self .get_def ('diffusion b value number' )
1052
+ if bvals is None :
1053
+ bvals = self .get_def ('diffusion_b_factor' )
1054
+ bvecs = self .get_def ('gradient orientation number' )
1055
+ if bvecs is None :
1056
+ # no b-vectors available
1057
+ diffusion_keys = (bvals , )
1058
+ else :
1059
+ diffusion_keys = (bvecs , bvals )
1060
+ else :
1061
+ diffusion_keys = ()
1062
+
1063
+ # initial sort (last key is highest precedence)
1064
+ keys = (slice_nos , echos , phases ) + \
1065
+ diffusion_keys + asl_keys + (dynamics , image_type )
1066
+ initial_sort_order = np .lexsort (keys )
1067
+
1068
+ # sequentially number the volumes based on the initial sort
1069
+ vol_nos = vol_numbers (slice_nos [initial_sort_order ])
1070
+ # identify truncated volumes
1071
+ is_full = vol_is_full (slice_nos [initial_sort_order ],
1072
+ self .general_info ['max_slices' ])
1073
+
1074
+ # second stage of sorting
1075
+ return initial_sort_order [np .lexsort ((vol_nos , is_full ))]
997
1076
1077
+ def _lax_sort_order (self ):
1078
+ """
998
1079
Sorts by (fast to slow): slice number, volume number.
999
1080
1000
1081
We calculate volume number by looking for repeating slice numbers (see
1001
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.
1002
1091
1003
1092
If the recording is truncated, the returned indices take care of
1004
1093
discarding any slice indices from incomplete volumes.
1005
1094
1095
+ If `self.strict_sort` is True, a more complicated sorting based on
1096
+ multiple fields from the .PAR file is used. This may produce a
1097
+ different sort order than `strict_sort=False`, where volumes are sorted
1098
+ by the order in which the slices appear in the .PAR file.
1099
+
1006
1100
Returns
1007
1101
-------
1008
1102
slice_indices : list
1009
1103
List for indexing into the last (third) dimension of the REC data
1010
1104
array, and (equivalently) the only dimension of
1011
1105
``self.image_defs``.
1012
1106
"""
1013
- slice_nos = self .image_defs ['slice number' ]
1014
- is_full = vol_is_full (slice_nos , self .general_info ['max_slices' ])
1015
- keys = (slice_nos , vol_numbers (slice_nos ), np .logical_not (is_full ))
1016
- # Figure out how many we need to remove from the end, and trim them
1017
- # Based on our sorting, they should always be last
1107
+ if not self .strict_sort :
1108
+ sort_order = self ._lax_sort_order ()
1109
+ else :
1110
+ sort_order = self ._strict_sort_order ()
1111
+
1112
+ # Figure out how many we need to remove from the end, and trim them.
1113
+ # Based on our sorting, they should always be last.
1018
1114
n_used = np .prod (self .get_data_shape ()[2 :])
1019
- return np . lexsort ( keys ) [:n_used ]
1115
+ return sort_order [:n_used ]
1020
1116
1021
1117
1022
1118
class PARRECImage (SpatialImage ):
@@ -1033,7 +1129,7 @@ class PARRECImage(SpatialImage):
1033
1129
@classmethod
1034
1130
@kw_only_meth (1 )
1035
1131
def from_file_map (klass , file_map , mmap = True , permit_truncated = False ,
1036
- scaling = 'dv' ):
1132
+ scaling = 'dv' , strict_sort = False ):
1037
1133
""" Create PARREC image from file map `file_map`
1038
1134
1039
1135
Parameters
@@ -1054,11 +1150,17 @@ def from_file_map(klass, file_map, mmap=True, permit_truncated=False,
1054
1150
scaling : {'dv', 'fp'}, optional, keyword-only
1055
1151
Scaling method to apply to data (see
1056
1152
:meth:`PARRECHeader.get_data_scaling`).
1153
+ strict_sort : bool, optional, keyword-only
1154
+ If True, a larger number of header fields are used while sorting
1155
+ the REC data array. This may produce a different sort order than
1156
+ `strict_sort=False`, where volumes are sorted by the order in which
1157
+ the slices appear in the .PAR file.
1057
1158
"""
1058
1159
with file_map ['header' ].get_prepare_fileobj ('rt' ) as hdr_fobj :
1059
1160
hdr = klass .header_class .from_fileobj (
1060
1161
hdr_fobj ,
1061
- permit_truncated = permit_truncated )
1162
+ permit_truncated = permit_truncated ,
1163
+ strict_sort = strict_sort )
1062
1164
rec_fobj = file_map ['image' ].get_prepare_fileobj ()
1063
1165
data = klass .ImageArrayProxy (rec_fobj , hdr ,
1064
1166
mmap = mmap , scaling = scaling )
@@ -1068,7 +1170,7 @@ def from_file_map(klass, file_map, mmap=True, permit_truncated=False,
1068
1170
@classmethod
1069
1171
@kw_only_meth (1 )
1070
1172
def from_filename (klass , filename , mmap = True , permit_truncated = False ,
1071
- scaling = 'dv' ):
1173
+ scaling = 'dv' , strict_sort = False ):
1072
1174
""" Create PARREC image from filename `filename`
1073
1175
1074
1176
Parameters
@@ -1088,12 +1190,18 @@ def from_filename(klass, filename, mmap=True, permit_truncated=False,
1088
1190
scaling : {'dv', 'fp'}, optional, keyword-only
1089
1191
Scaling method to apply to data (see
1090
1192
:meth:`PARRECHeader.get_data_scaling`).
1193
+ strict_sort : bool, optional, keyword-only
1194
+ If True, a larger number of header fields are used while sorting
1195
+ the REC data array. This may produce a different sort order than
1196
+ `strict_sort=False`, where volumes are sorted by the order in which
1197
+ the slices appear in the .PAR file.
1091
1198
"""
1092
1199
file_map = klass .filespec_to_file_map (filename )
1093
1200
return klass .from_file_map (file_map ,
1094
1201
mmap = mmap ,
1095
1202
permit_truncated = permit_truncated ,
1096
- scaling = scaling )
1203
+ scaling = scaling ,
1204
+ strict_sort = strict_sort )
1097
1205
1098
1206
load = from_filename
1099
1207
0 commit comments