1
+ import argparse
2
+ import os
3
+ from pathlib import Path
4
+ import subprocess
5
+ import sys
6
+ import inquirer
7
+
8
+ def prompt_input_directory ():
9
+ return inquirer .prompt ([
10
+ inquirer .Path (
11
+ "path" ,
12
+ message = "📂 Select an input directory containing DICOM image files:" ,
13
+ path_type = inquirer .Path .DIRECTORY ,
14
+ exists = True ,
15
+ )
16
+ ])["path" ]
17
+
18
+
19
+ def prompt_output_directory (input_dir ):
20
+ # List subfolders of input_dir
21
+ subdirs = [
22
+ name for name in os .listdir (input_dir )
23
+ if os .path .isdir (os .path .join (input_dir , name ))
24
+ ]
25
+
26
+ choices = [f"./{ name } " for name in subdirs ]
27
+ choices .append ("📥 Enter a custom output path..." )
28
+
29
+ answer = inquirer .prompt ([
30
+ inquirer .List (
31
+ "choice" ,
32
+ message = f"📁 Choose an output directory for NIfTI files from:\n → { input_dir } " ,
33
+ choices = choices
34
+ )
35
+ ])["choice" ]
36
+
37
+ if answer == "📥 Enter a custom output path..." :
38
+ return inquirer .prompt ([
39
+ inquirer .Path (
40
+ "custom_path" ,
41
+ message = "📥 Enter custom output directory path:" ,
42
+ path_type = inquirer .Path .DIRECTORY ,
43
+ exists = True
44
+ )
45
+ ])["custom_path" ]
46
+ else :
47
+ return os .path .abspath (os .path .join (input_dir , answer .strip ("./" )))
48
+
49
+
50
+ def dicom_to_niix (vol_dir : Path , out_dir : Path , merge_2d : bool = False ):
51
+ """
52
+ For converting DICOM images to a (compresssed) 4d nifti image
53
+ """
54
+ os .makedirs (out_dir , exist_ok = True )
55
+
56
+ try :
57
+ res = subprocess .run (
58
+ [
59
+ "dcm2niix" ,
60
+ "-f" , "%s_%p" , # dcm2niix attempts to provide a sensible file naming scheme
61
+ "-o" , out_dir , # output directory
62
+ "-z" , "y" , #specifying compressed nii.gz file
63
+ "-m" , "y" if merge_2d else "n" , # Add merge option
64
+ # https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage
65
+ # for further configuration for general usage see page above
66
+ vol_dir # input directory
67
+ ],
68
+ capture_output = True ,
69
+ text = True ,
70
+ check = True
71
+ )
72
+
73
+ nifti_files = list (Path (out_dir ).glob ("*.nii.gz" ))
74
+ if len (nifti_files ) != 1 :
75
+ raise RuntimeError ("Only one NIfTI (.nii.gz) should be in this output directory." )
76
+
77
+ bval_files = list (out_dir .glob ("*.bval" ))
78
+ bvec_files = list (out_dir .glob ("*.bvec" ))
79
+ bval_path = str (bval_files [0 ]) if bval_files else None
80
+ bvec_path = str (bvec_files [0 ]) if bvec_files else None
81
+
82
+ if not bval_path or not bvec_path :
83
+ raise RuntimeError ("No bvec or bval files were generated." )
84
+
85
+ return nifti_files [0 ], bval_path , bvec_path
86
+
87
+ except subprocess .CalledProcessError as e :
88
+ raise RuntimeError (f"dcm2niix failed: { e .stderr } " )
89
+
90
+ def run_interactive ():
91
+
92
+ input_dirs = []
93
+ output_dirs = []
94
+
95
+ while True :
96
+ input_dir = prompt_input_directory ()
97
+ output_dir = prompt_output_directory (input_dir )
98
+
99
+ input_dirs .append (input_dir )
100
+ output_dirs .append (output_dir )
101
+
102
+ add_more = inquirer .prompt ([
103
+ inquirer .Confirm ("more" , message = "➕ Add another input/output pair?" , default = False )
104
+ ])["more" ]
105
+
106
+ if not add_more :
107
+ break
108
+
109
+ merge_answer = inquirer .prompt ([
110
+ inquirer .Confirm ("merge" , message = "🧩 Merge 2D slices into a single NIfTI (-m y)?" , default = True )
111
+ ])
112
+ merge_2d = merge_answer ["merge" ]
113
+
114
+ for in_dir , out_dir in zip (input_dirs , output_dirs ):
115
+ vol_dir = Path (in_dir )
116
+ out_path = Path (out_dir )
117
+
118
+ print (f"Converting:\n → Input: { vol_dir } \n → Output: { out_path } " )
119
+ try :
120
+ nifti , bval , bvec = dicom_to_niix (vol_dir , out_path , merge_2d )
121
+ print (f"✅ Created: { nifti } " )
122
+ except RuntimeError as err :
123
+ print (f"❌ Conversion failed: { err } " )
124
+
125
+ def run_cli (input_path : str , output_path : str ):
126
+ vol_dir = Path (input_path )
127
+ out_dir = Path (output_path )
128
+
129
+ print (f" Converting:\n → Input: { vol_dir } \n → Output: { out_dir } " )
130
+ try :
131
+ nifti , bval , bvec = dicom_to_niix (vol_dir , out_dir , merge_2d = False )
132
+ print (f" Created NIfTI: { nifti } " )
133
+
134
+ if bval and bvec :
135
+ print (" Running IVIM fitting algorithm..." )
136
+ subprocess .run ([
137
+ "python3" , "-m" , "WrapImage.nifti_wrapper" ,
138
+ str (nifti ), str (bvec ), str (bval )
139
+ ], check = True )
140
+ print (" IVIM fitting complete." )
141
+ else :
142
+ print ("⚠️ bvec/bval missing, skipping IVIM post-processing." )
143
+ except RuntimeError as err :
144
+ print (f"❌ Conversion failed: { err } " )
145
+ sys .exit (1 )
146
+
147
+ if __name__ == "__main__" :
148
+ parser = argparse .ArgumentParser (description = "DICOM to NIfTI converter with optional IVIM processing" )
149
+ parser .add_argument ("input" , nargs = "?" , help = "Path to input DICOM directory" )
150
+ parser .add_argument ("output" , nargs = "?" , help = "Path to output directory for NIfTI files" )
151
+ parser .add_argument ("-pu" , "--prompt-user" , action = "store_true" , help = "Run in interactive mode" )
152
+
153
+ args = parser .parse_args ()
154
+
155
+ if args .prompt_user :
156
+ run_interactive ()
157
+ elif args .input and args .output :
158
+ run_cli (args .input , args .output )
159
+ else :
160
+ print ("❗ You must provide input and output paths OR use --prompt-user for interactive mode." )
161
+ parser .print_help ()
162
+ sys .exit (1 )
0 commit comments