1111
1212import dicom2nifti
1313import dicom2nifti .exceptions
14+ import nibabel as nib
1415import numpy as np
1516import pydicom
1617from dicom2nifti import convert_dicom
@@ -87,6 +88,74 @@ def _generate_bids_path(
8788 return fname .file ["json" ], fname
8889
8990
91+ def dicom_to_nifti_multiframe (ds , nii_path ):
92+ pixel_array = ds .pixel_array
93+ if len (pixel_array .shape ) != 3 and len (pixel_array .shape ) != 4 :
94+ raise ValueError (f"Expected a shape with 3 colums not { len (pixel_array .shape )} ; { pixel_array .shape = } " )
95+ n_frames = pixel_array .shape [0 ]
96+
97+ # Pixel spacing (mm)
98+ if hasattr (ds , "PixelSpacing" ):
99+ dy , dx = (float (v ) for v in ds .PixelSpacing )
100+ # Image orientation (row and column direction cosines)
101+ orientation = [float (v ) for v in ds .ImageOrientationPatient ]
102+ row_cosines = np .array (orientation [0 :3 ])
103+ col_cosines = np .array (orientation [3 :6 ])
104+ # Normal vector (slice direction)
105+ slice_cosines = np .cross (row_cosines , col_cosines )
106+
107+ # Image position (origin of first slice)
108+ origin = np .array ([float (v ) for v in ds .ImagePositionPatient ])
109+ # Slice spacing - robust: Abstand zwischen Slice 0 und 1
110+ if n_frames > 1 :
111+ pos1 = np .array ([float (v ) for v in ds .PerFrameFunctionalGroupsSequence [0 ].PlanePositionSequence [0 ].ImagePositionPatient ])
112+ pos2 = np .array ([float (v ) for v in ds .PerFrameFunctionalGroupsSequence [1 ].PlanePositionSequence [0 ].ImagePositionPatient ])
113+ dz = np .linalg .norm (pos2 - pos1 )
114+ else :
115+ dz = float (getattr (ds , "SpacingBetweenSlices" , ds .SliceThickness ))
116+
117+ # Affine bauen
118+ affine = np .eye (4 )
119+ affine [0 :3 , 0 ] = row_cosines * dx
120+ affine [0 :3 , 1 ] = col_cosines * dy
121+ affine [0 :3 , 2 ] = slice_cosines * dz
122+ affine [0 :3 , 3 ] = origin
123+ nii = nib .Nifti1Image (np .transpose (pixel_array , (2 , 1 , 0 )), affine )
124+
125+ elif hasattr (ds , "ImagerPixelSpacing" ):
126+ dy , dx = (float (v ) for v in ds .ImagerPixelSpacing )
127+ # Einfaches affine (nur 2D + Zeit, keine Lage im Patientenraum)
128+ affine = np .eye (4 )
129+ affine [0 , 0 ] = - dx
130+ affine [1 , 1 ] = - dy
131+ nii = nib .Nifti1Image (np .transpose (pixel_array , (2 , 1 , 0 )), affine )
132+
133+ else :
134+ if hasattr (ds , "RelatedSeriesSequence" ):
135+ raise NotImplementedError ("RelatedSeriesSequence Affine lookup not implemented" )
136+ raise NotImplementedError ("No spatial metadata found" )
137+ ### Some could be solved by looking up the "RelatedSeriesSequence"
138+ # "RelatedSeriesSequence": [
139+ # {
140+ # "StudyInstanceUID": "1.2.276.0.38.1.1.1.7712.20250929100319.54200288",
141+ # "SeriesInstanceUID": "1.3.46.670589.7.8.1.6.1403526999.1.9608.1759142950287.2",
142+ # "PurposeOfReferenceCodeSequence": []
143+ # }
144+ # ],
145+ # --- No geometry info (e.g. RGB screen captures or video frames) ---
146+ print ("⚠️ No spatial metadata found — assuming pixel size = 1mm and identity orientation." )
147+ affine = np .eye (4 )
148+ affine [0 , 0 ] = 1.0
149+ affine [1 , 1 ] = 1.0
150+ affine [2 , 2 ] = 1.0
151+ nii = nib .Nifti1Image (pixel_array , affine )
152+
153+ # Reihenfolge anpassen: Nibabel erwartet (X,Y,Z)
154+ nib .save (nii , nii_path )
155+
156+ return nii_path
157+
158+
90159def _convert_to_nifti (dicom_out_path , nii_path ):
91160 """
92161 Convert DICOM files to NIfTI format and handle common conversion errors.
@@ -105,15 +174,25 @@ def _convert_to_nifti(dicom_out_path, nii_path):
105174 """
106175 try :
107176 if isinstance (dicom_out_path , list ):
177+ try :
178+ if len (dicom_out_path ) == 1 :
179+ ds = dicom_out_path [0 ]
180+ if hasattr (ds , "pixel_array" ) and len (ds .pixel_array .shape ) >= 2 :
181+ dicom_to_nifti_multiframe (ds , nii_path )
182+
183+ return True
184+ except Exception as e :
185+ logger .on_debug ("Multi-Frame DICOM did not work:" , e )
108186 convert_dicom .dicom_array_to_nifti (dicom_out_path , nii_path , True )
109187 else :
110188 # func_timeout(10, dicom2nifti.dicom_series_to_nifti, (dicom_out_path, nii_path, True))
111189 dicom2nifti .dicom_series_to_nifti (dicom_out_path , nii_path , True )
112190 logger .print ("Save " , nii_path , Log_Type .SAVE )
113191 except dicom2nifti .exceptions .ConversionValidationError as e :
114192 if e .args [0 ] in ["NON_IMAGING_DICOM_FILES" ]:
193+ s = f"dicom_array_to_nifti len={ len (dicom_out_path )} " if isinstance (dicom_out_path , list ) else "dicom_series_to_nifti"
115194 Path (str (nii_path ).replace (".nii.gz" , ".json" )).unlink (missing_ok = True )
116- logger .on_debug (f"Not exportable '{ Path (nii_path ).name } ':" , e .args [0 ])
195+ logger .on_debug (f"Not exportable '{ Path (nii_path ).name } ':" , e .args [0 ], s )
117196 return False
118197 for key , reason in [
119198 ("validate_orthogonal" , "NON_CUBICAL_IMAGE/GANTRY_TILT" ),
@@ -311,7 +390,7 @@ def _read_dicom_files(dicom_out_path: Path) -> tuple[dict[str, list[FileDataset]
311390 path = Path (_paths )
312391 if path .is_file ():
313392 try :
314- dcm_data = pydicom .dcmread (path , defer_size = "1 KB" , force = True )
393+ dcm_data = pydicom .dcmread (path , defer_size = "1 KB" , force = True ) # , stop_before_pixels=True
315394 try :
316395 typ = (
317396 str (dcm_data .get_item ((0x0008 , 0x0008 )).value )
@@ -324,7 +403,6 @@ def _read_dicom_files(dicom_out_path: Path) -> tuple[dict[str, list[FileDataset]
324403 except Exception :
325404 typ = ""
326405 key1 = str (dcm_data .SeriesInstanceUID )
327-
328406 key = f"{ key1 } _{ typ } "
329407 if not hasattr (dcm_data , "ImageOrientationPatient" ):
330408 key += "_" + dcm_data .get ("SOPInstanceUID" , 0 )
@@ -437,6 +515,7 @@ def extract_dicom_folder(
437515 validate_slicecount = True ,
438516 validate_orientation = True ,
439517 validate_orthogonal = False ,
518+ validate_slice_increment = True ,
440519 n_cpu : int | None = 1 ,
441520 override_subject_name : Callable [[dict , Path ], str ] | None = None ,
442521 skip_localizer = True ,
@@ -463,7 +542,8 @@ def extract_dicom_folder(
463542 convert_dicom .settings .disable_validate_orientation ()
464543 if not validate_orthogonal :
465544 convert_dicom .settings .disable_validate_orthogonal ()
466-
545+ if not validate_slice_increment :
546+ convert_dicom .settings .disable_validate_slice_increment ()
467547 outs = {}
468548
469549 for p in _find_all_files (dicom_folder ):
@@ -512,6 +592,8 @@ def process_series(key, files, parts):
512592 try :
513593 key2 , out = process_series (key , files , parts )
514594 outs [key2 ] = out
595+ except NotImplementedError as e :
596+ logger .on_warning ("NotImplementedError:" , e )
515597 except Exception :
516598 logger .print_error ()
517599
@@ -523,6 +605,10 @@ def process_series(key, files, parts):
523605
524606
525607if __name__ == "__main__" :
608+ for p in Path ("/DATA/NAS/datasets_source/brain/dsa" ).iterdir ():
609+ extract_dicom_folder (p , Path ("/DATA/NAS/datasets_source/brain/" , "dataset-DSA" ), False , False , validate_slice_increment = False )
610+
611+ sys .exit ()
526612 # s = "/home/robert/Downloads/bein/dataset-oberschenkel/rawdata/sub-1-3-46-670589-11-2889201787-2305829596-303261238-2367429497/mr/sub-1-3-46-670589-11-2889201787-2305829596-303261238-2367429497_sequ-406_mr.nii.gz"
527613 # nii2 = NII.load(s, False)
528614 # print(nii2.affine, nii2.orientation)
0 commit comments