@@ -49,12 +49,13 @@ def is_dicom_file_or_directory(path: PathLike) -> bool:
4949def extract_affine_from_dicom (dicom_slices : list ) -> np .ndarray :
5050 """
5151 Extract the affine transformation matrix from DICOM header information.
52+ Converts from DICOM LPS (Left-Posterior-Superior) to NIfTI RAS.
5253
5354 Args:
5455 dicom_slices: List of tuples (instance_number, pixel_array, dicom_dataset)
5556
5657 Returns:
57- 4x4 affine transformation matrix mapping voxel coordinates to world coordinates
58+ 4x4 affine transformation matrix mapping voxel coordinates to RAS world coordinates
5859
5960 Raises:
6061 RuntimeError: If required DICOM tags are missing
@@ -71,47 +72,42 @@ def extract_affine_from_dicom(dicom_slices: list) -> np.ndarray:
7172 # ImagePositionPatient (0020,0032): position of the upper-left voxel
7273 position = np .array (first_ds .ImagePositionPatient , dtype = float )
7374
74- # PixelSpacing (0028,0030): spacing between pixels [row_spacing, col_spacing]
75- pixel_spacing = np .array (first_ds .PixelSpacing , dtype = float )
76- row_spacing = pixel_spacing [0 ]
77- col_spacing = pixel_spacing [1 ]
75+ # PixelSpacing is [row_spacing, col_spacing], so map to dy, dx
76+ dy , dx = np .array (first_ds .PixelSpacing , dtype = float )
7877
7978 except AttributeError as e :
8079 raise RuntimeError (
8180 f"Missing required DICOM tag for affine calculation: { e } "
8281 ) from e
8382
84- # calculate slice direction as cross product of row and column directions
83+ # Compute Z direction and spacing (handling potential gantry tilt)
8584 slice_cosine = np .cross (row_cosine , col_cosine )
8685
8786 # calculate slice spacing
8887 if len (dicom_slices ) > 1 :
8988 # calculate from the distance between first two slices
9089 first_pos = np .array (dicom_slices [0 ][2 ].ImagePositionPatient , dtype = float )
9190 second_pos = np .array (dicom_slices [1 ][2 ].ImagePositionPatient , dtype = float )
92- slice_spacing = np .linalg . norm (second_pos - first_pos )
91+ dz = np .dot (second_pos - first_pos , slice_cosine )
9392 else :
9493 # single slice - try to get from SliceThickness or default to 1.0
95- slice_spacing = float (getattr (first_ds , 'SliceThickness' , 1.0 ))
94+ dz = float (getattr (first_ds , 'SliceThickness' , 1.0 ))
9695
97- # construct the affine matrix
98- # The affine maps from voxel indices (i, j, k) to physical coordinates (x, y, z)
96+ # Construct affine in DICOM LPS space
9997 affine = np .eye (4 )
100- affine [:3 , 0 ] = row_cosine * row_spacing
101- affine [:3 , 1 ] = col_cosine * col_spacing
102- affine [:3 , 2 ] = slice_cosine * slice_spacing
98+ affine [:3 , 0 ] = row_cosine * dx
99+ affine [:3 , 1 ] = col_cosine * dy
100+ affine [:3 , 2 ] = slice_cosine * dz
103101 affine [:3 , 3 ] = position
104102
105- return affine
103+ # Convert LPS to RAS by flipping X and Y axes
104+ return np .diag ([- 1 , - 1 , 1 , 1 ]) @ affine
106105
107106
108107def convert_dicom_to_nifti (input_path : PathLike , output_filepath : PathLike ) -> None :
109108 """
110109 Convert DICOM file(s) to NIfTI format using pydicom and nibabel.
111110
112- The affine transformation matrix is extracted from DICOM headers to properly
113- map voxel coordinates to scanner coordinates in the NIfTI output.
114-
115111 Args:
116112 input_path: Path to either a DICOM file or directory containing DICOM files
117113 output_filepath: Path where the output NIfTI file should be saved
@@ -136,8 +132,8 @@ def convert_dicom_to_nifti(input_path: PathLike, output_filepath: PathLike) -> N
136132 for dcm_file in dicom_files :
137133 try :
138134 ds = pydicom .dcmread (dcm_file )
139- # store instance number, pixel array, and dataset for affine extraction
140- slices .append ((ds .get ('InstanceNumber' , 0 ), ds .pixel_array , ds ))
135+ # Transpose to swap (Row, Col) -> (X, Y) for NIfTI
136+ slices .append ((ds .get ('InstanceNumber' , 0 ), ds .pixel_array . T , ds ))
141137 except Exception :
142138 # skip files that aren't valid dicom
143139 continue
@@ -155,8 +151,7 @@ def convert_dicom_to_nifti(input_path: PathLike, output_filepath: PathLike) -> N
155151 # extract affine from DICOM headers
156152 affine = extract_affine_from_dicom (slices )
157153
158- nifti_img = nib .Nifti1Image (volume , affine )
159- nib .save (nifti_img , str (output_filepath ))
154+ nib .save (nib .Nifti1Image (volume , affine ), str (output_filepath ))
160155
161156 except Exception as e :
162157 raise RuntimeError (f"DICOM to NIfTI conversion failed: { e } " ) from e
0 commit comments