@@ -1099,15 +1099,21 @@ def convert_single_frame_dicom_series_to_multiframe(
10991099 setattr (output_ds , attr , default )
11001100 logger .warning (f" ⚠️ Added missing { attr } = { default } " )
11011101
1102- # Keep first frame's spatial attributes as top-level (represents volume origin)
1103- if hasattr (datasets [0 ], "ImagePositionPatient" ):
1104- output_ds .ImagePositionPatient = datasets [0 ].ImagePositionPatient
1105- logger .info (f" ✓ Top-level ImagePositionPatient: { output_ds .ImagePositionPatient } " )
1106- logger .info (f" (This is Frame[0], the FIRST slice in Z-order)" )
1107-
1108- if hasattr (datasets [0 ], "ImageOrientationPatient" ):
1109- output_ds .ImageOrientationPatient = datasets [0 ].ImageOrientationPatient
1110- logger .info (f" ✓ ImageOrientationPatient: { output_ds .ImageOrientationPatient } " )
1102+ # CRITICAL: Remove top-level ImagePositionPatient and ImageOrientationPatient
1103+ # Working files (that display correctly in OHIF MPR) have NEITHER at top level
1104+ # These should ONLY exist in functional groups for Enhanced CT
1105+
1106+ if hasattr (output_ds , "ImagePositionPatient" ):
1107+ delattr (output_ds , "ImagePositionPatient" )
1108+ logger .info (f" ✓ Removed top-level ImagePositionPatient (use per-frame only)" )
1109+
1110+ if hasattr (output_ds , "ImageOrientationPatient" ):
1111+ delattr (output_ds , "ImageOrientationPatient" )
1112+ logger .info (f" ✓ Removed top-level ImageOrientationPatient (use SharedFunctionalGroupsSequence only)" )
1113+
1114+ # CRITICAL: Set correct SOPClassUID for Enhanced multi-frame CT
1115+ output_ds .SOPClassUID = "1.2.840.10008.5.1.4.1.1.2.1" # Enhanced CT Image Storage
1116+ logger .info (f" ✓ Set SOPClassUID to Enhanced CT Image Storage" )
11111117
11121118 # Keep pixel spacing and slice thickness
11131119 if hasattr (datasets [0 ], "PixelSpacing" ):
@@ -1143,9 +1149,11 @@ def convert_single_frame_dicom_series_to_multiframe(
11431149 output_ds .SpacingBetweenSlices = spacing
11441150 logger .info (f" ✓ Added SpacingBetweenSlices: { spacing :.6f} mm" )
11451151
1146- # Add minimal PerFrameFunctionalGroupsSequence for OHIF compatibility
1147- # OHIF's cornerstone3D expects this even for simple multi-frame CT
1148- logger .info (f" Adding minimal per-frame functional groups for OHIF compatibility..." )
1152+ # Add PerFrameFunctionalGroupsSequence for OHIF/Cornerstone3D compatibility
1153+ # CRITICAL: Structure must be exactly right to avoid "1/Infinity" MPR display bug
1154+ # - Per-frame: PlanePositionSequence ONLY (unique position per frame)
1155+ # - Shared: PlaneOrientationSequence (common orientation for all frames)
1156+ logger .info (f" Adding per-frame functional groups (OHIF-compatible structure)..." )
11491157 from pydicom .dataset import Dataset as DicomDataset
11501158 from pydicom .sequence import Sequence
11511159
@@ -1154,18 +1162,23 @@ def convert_single_frame_dicom_series_to_multiframe(
11541162 frame_item = DicomDataset ()
11551163
11561164 # PlanePositionSequence - ImagePositionPatient for this frame
1157- # CRITICAL: Best defense against Cornerstone3D bugs
1165+ # This is MANDATORY for Enhanced CT multi-frame
1166+ plane_pos_item = DicomDataset ()
11581167 if hasattr (ds_frame , "ImagePositionPatient" ):
1159- plane_pos_item = DicomDataset ()
11601168 plane_pos_item .ImagePositionPatient = ds_frame .ImagePositionPatient
1161- frame_item .PlanePositionSequence = Sequence ([plane_pos_item ])
1162-
1163- # PlaneOrientationSequence - ImageOrientationPatient for this frame
1164- # CRITICAL: Best defense against Cornerstone3D bugs
1165- if hasattr (ds_frame , "ImageOrientationPatient" ):
1166- plane_orient_item = DicomDataset ()
1167- plane_orient_item .ImageOrientationPatient = ds_frame .ImageOrientationPatient
1168- frame_item .PlaneOrientationSequence = Sequence ([plane_orient_item ])
1169+ else :
1170+ # If missing, use default (0,0,frame_idx * spacing)
1171+ # This shouldn't happen for valid CT series, but ensures MPR compatibility
1172+ default_spacing = float (output_ds .SpacingBetweenSlices ) if hasattr (output_ds , 'SpacingBetweenSlices' ) else 1.0
1173+ plane_pos_item .ImagePositionPatient = [0.0 , 0.0 , frame_idx * default_spacing ]
1174+ logger .warning (f" Frame { frame_idx } missing ImagePositionPatient, using default" )
1175+ frame_item .PlanePositionSequence = Sequence ([plane_pos_item ])
1176+
1177+ # CRITICAL: Do NOT add per-frame PlaneOrientationSequence!
1178+ # PlaneOrientationSequence should ONLY be in SharedFunctionalGroupsSequence
1179+ # Having it per-frame triggers different parsing logic in OHIF/Cornerstone3D
1180+ # Result: metadata not read correctly, spacing[2] = 0
1181+ # (The orientation is shared across all frames anyway)
11691182
11701183 # FrameContentSequence - helps with frame identification
11711184 frame_content_item = DicomDataset ()
@@ -1178,17 +1191,22 @@ def convert_single_frame_dicom_series_to_multiframe(
11781191
11791192 output_ds .PerFrameFunctionalGroupsSequence = Sequence (per_frame_seq )
11801193 logger .info (f" ✓ Added PerFrameFunctionalGroupsSequence with { len (per_frame_seq )} frame items" )
1181- logger .info (f" Each frame includes: PlanePositionSequence + PlaneOrientationSequence " )
1194+ logger .info (f" Each frame includes: PlanePositionSequence only (orientation in shared) " )
11821195
11831196 # Add SharedFunctionalGroupsSequence for additional Cornerstone3D compatibility
11841197 # This defines attributes that are common to ALL frames
11851198 shared_item = DicomDataset ()
11861199
1187- # PlaneOrientationSequence - same for all frames
1200+ # PlaneOrientationSequence - MANDATORY for Enhanced CT multi-frame
1201+ shared_orient_item = DicomDataset ()
11881202 if hasattr (datasets [0 ], "ImageOrientationPatient" ):
1189- shared_orient_item = DicomDataset ()
11901203 shared_orient_item .ImageOrientationPatient = datasets [0 ].ImageOrientationPatient
1191- shared_item .PlaneOrientationSequence = Sequence ([shared_orient_item ])
1204+ else :
1205+ # If missing, use standard axial orientation
1206+ # This ensures MPR button is enabled in OHIF
1207+ shared_orient_item .ImageOrientationPatient = [1.0 , 0.0 , 0.0 , 0.0 , 1.0 , 0.0 ]
1208+ logger .warning (f" Source files missing ImageOrientationPatient, using standard axial orientation" )
1209+ shared_item .PlaneOrientationSequence = Sequence ([shared_orient_item ])
11921210
11931211 # PixelMeasuresSequence - pixel spacing and slice thickness
11941212 if hasattr (datasets [0 ], "PixelSpacing" ) or hasattr (datasets [0 ], "SliceThickness" ):
@@ -1203,7 +1221,7 @@ def convert_single_frame_dicom_series_to_multiframe(
12031221
12041222 output_ds .SharedFunctionalGroupsSequence = Sequence ([shared_item ])
12051223 logger .info (f" ✓ Added SharedFunctionalGroupsSequence (common attributes for all frames)" )
1206- logger .info (f" (Additional defense against Cornerstone3D < v2.0 bugs )" )
1224+ logger .info (f" Includes PlaneOrientationSequence (ONLY location for orientation! )" )
12071225
12081226 # Verify frame ordering
12091227 if len (per_frame_seq ) > 0 :
0 commit comments