11
11
import shutil
12
12
import sys
13
13
from types import ModuleType
14
- from typing import TYPE_CHECKING , Any , List , Optional , cast
14
+ from typing import TYPE_CHECKING , Any , List , Optional , Tuple , cast
15
15
16
16
import filelock
17
17
from nipype import Node
@@ -849,6 +849,64 @@ def nipype_convert(
849
849
return eg , prov_file
850
850
851
851
852
+ def filter_partial_volumes (
853
+ nii_files : list [str ],
854
+ bids_files : list [str ],
855
+ bids_metas : list [dict [str , Any ]],
856
+ ) -> Tuple [list [str ] | str , list [str ] | str , list [Any ] | Any ]:
857
+ """filter interrupted 4D scans volumes with missing slices on XA: see dcm2niix #742
858
+
859
+ Parameters
860
+ ----------
861
+ nii_files : list[str]
862
+ converted nifti filepaths
863
+ bids_files: list[str]
864
+ converted BIDS json filepaths
865
+ bids_metas : list[dict[str, Any]]
866
+ list of metadata dict loaded from BIDS json files
867
+
868
+ Returns
869
+ -------
870
+ nii_files
871
+ filtered niftis
872
+ bids_files
873
+ filtered BIDS jsons
874
+ bids_metas
875
+ filtered BIDS metadata
876
+
877
+ """
878
+ # dcm2niix sets metadata "RawImage": false and "SeriesNumber" += 1000 to mark partial volumes
879
+ # https://github.com/rordenlab/dcm2niix/blob/f6d7a0018d9d268ed1d084faafdedfadcbbb830b/console/nii_dicom.cpp#L8434-L8437
880
+ # following that logic until https://github.com/rordenlab/dcm2niix/issues/972 is addressed
881
+ partial_volumes = [
882
+ not metadata .get ("RawImage" , True )
883
+ and metadata .get ("SeriesNumber" , 0 ) > 1000
884
+ and "syngo MR XA" in metadata .get ("SoftwareVersions" , "" )
885
+ for metadata in bids_metas
886
+ ]
887
+ no_partial_volumes = not any (partial_volumes ) or all (partial_volumes )
888
+
889
+ if no_partial_volumes :
890
+ return nii_files , bids_files , bids_metas
891
+ else :
892
+ new_nii_files , new_bids_files , new_bids_metas = [], [], []
893
+ for fl , bids_file , bids_meta , is_pv in zip (
894
+ nii_files , bids_files , bids_metas , partial_volumes
895
+ ):
896
+ if is_pv :
897
+ # remove partial volume
898
+ os .remove (fl )
899
+ os .remove (bids_file )
900
+ lgr .warning (f"dropped { fl } partial volume from interrupted series" )
901
+ else :
902
+ new_nii_files .append (fl )
903
+ new_bids_files .append (bids_file )
904
+ new_bids_metas .append (bids_meta )
905
+ if len (new_nii_files ) == 1 :
906
+ return new_nii_files [0 ], new_bids_files [0 ], new_bids_metas [0 ]
907
+ return new_nii_files , new_bids_files , new_bids_metas
908
+
909
+
852
910
def save_converted_files (
853
911
res : Node ,
854
912
item_dicoms : list [str ],
@@ -885,6 +943,7 @@ def save_converted_files(
885
943
886
944
bids_outfiles : list [str ] = []
887
945
res_files = res .outputs .converted_files
946
+ bids_files = res .outputs .bids
888
947
889
948
if not len (res_files ):
890
949
lgr .debug ("DICOMs {} were not converted" .format (item_dicoms ))
@@ -925,6 +984,21 @@ def rename_files() -> None:
925
984
# we should provide specific handling for fmap,
926
985
# dwi etc which might spit out multiple files
927
986
987
+ # Also copy BIDS files although they might need to
988
+ # be merged/postprocessed later
989
+ bids_files = (
990
+ sorted (bids_files )
991
+ if len (bids_files ) == len (res_files )
992
+ else [None ] * len (res_files )
993
+ )
994
+ # preload since will be used in multiple spots
995
+ bids_metas = [load_json (b ) for b in bids_files if b ]
996
+
997
+ res_files , bids_files , bids_metas = filter_partial_volumes (
998
+ res_files , bids_files , bids_metas
999
+ )
1000
+
1001
+ if isinstance (res_files , list ):
928
1002
suffixes = (
929
1003
[str (i + 1 ) for i in range (len (res_files ))]
930
1004
if (bids_options is not None )
@@ -941,16 +1015,6 @@ def rename_files() -> None:
941
1015
)
942
1016
suffixes = [str (- i - 1 ) for i in range (len (res_files ))]
943
1017
944
- # Also copy BIDS files although they might need to
945
- # be merged/postprocessed later
946
- bids_files = (
947
- sorted (res .outputs .bids )
948
- if len (res .outputs .bids ) == len (res_files )
949
- else [None ] * len (res_files )
950
- )
951
- # preload since will be used in multiple spots
952
- bids_metas = [load_json (b ) for b in bids_files if b ]
953
-
954
1018
### Do we have a multi-echo series? ###
955
1019
# Some Siemens sequences (e.g. CMRR's MB-EPI) set the label 'TE1',
956
1020
# 'TE2', etc. in the 'ImageType' field. However, other seqs do not
@@ -1041,9 +1105,9 @@ def rename_files() -> None:
1041
1105
else :
1042
1106
outname = "{}.{}" .format (prefix , outtype )
1043
1107
safe_movefile (res_files , outname , overwrite )
1044
- if isdefined ( res . outputs . bids ) :
1108
+ if bids_files :
1045
1109
try :
1046
- safe_movefile (res . outputs . bids , outname_bids , overwrite )
1110
+ safe_movefile (bids_files , outname_bids , overwrite )
1047
1111
bids_outfiles .append (outname_bids )
1048
1112
except TypeError : ##catch lists
1049
1113
raise TypeError ("Multiple BIDS sidecars detected." )
0 commit comments