@@ -1099,15 +1099,25 @@ 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: Do NOT add top-level ImagePositionPatient or ImageOrientationPatient!
1103+ # These tags interfere with OHIF/Cornerstone3D multi-frame parsing
1104+ # OHIF will read the top-level value for ALL frames instead of per-frame values
1105+ # Result: spacing[2] = 0 and "1/Infinity" display in MPR views
1106+
1107+ # Remove them if they exist (from template dataset)
1108+ if hasattr (output_ds , "ImagePositionPatient" ):
1109+ delattr (output_ds , "ImagePositionPatient" )
1110+ logger .info (f" ✓ Removed top-level ImagePositionPatient (use per-frame only)" )
1111+
1112+ if hasattr (output_ds , "ImageOrientationPatient" ):
1113+ delattr (output_ds , "ImageOrientationPatient" )
1114+ logger .info (f" ✓ Removed top-level ImageOrientationPatient (use SharedFunctionalGroupsSequence only)" )
1115+
1116+ # CRITICAL: Set correct SOPClassUID for Enhanced multi-frame CT
1117+ # Use Enhanced CT Image Storage (not legacy CT Image Storage)
1118+ # This tells DICOM viewers to use Enhanced multi-frame parsing logic
1119+ output_ds .SOPClassUID = "1.2.840.10008.5.1.4.1.1.2.1" # Enhanced CT Image Storage
1120+ logger .info (f" ✓ Set SOPClassUID to Enhanced CT Image Storage" )
11111121
11121122 # Keep pixel spacing and slice thickness
11131123 if hasattr (datasets [0 ], "PixelSpacing" ):
@@ -1143,9 +1153,11 @@ def convert_single_frame_dicom_series_to_multiframe(
11431153 output_ds .SpacingBetweenSlices = spacing
11441154 logger .info (f" ✓ Added SpacingBetweenSlices: { spacing :.6f} mm" )
11451155
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..." )
1156+ # Add PerFrameFunctionalGroupsSequence for OHIF/Cornerstone3D compatibility
1157+ # CRITICAL: Structure must be exactly right to avoid "1/Infinity" MPR display bug
1158+ # - Per-frame: PlanePositionSequence ONLY (unique position per frame)
1159+ # - Shared: PlaneOrientationSequence (common orientation for all frames)
1160+ logger .info (f" Adding per-frame functional groups (OHIF-compatible structure)..." )
11491161 from pydicom .dataset import Dataset as DicomDataset
11501162 from pydicom .sequence import Sequence
11511163
@@ -1154,18 +1166,17 @@ def convert_single_frame_dicom_series_to_multiframe(
11541166 frame_item = DicomDataset ()
11551167
11561168 # PlanePositionSequence - ImagePositionPatient for this frame
1157- # CRITICAL: Best defense against Cornerstone3D bugs
1169+ # This is REQUIRED - each frame needs its own position
11581170 if hasattr (ds_frame , "ImagePositionPatient" ):
11591171 plane_pos_item = DicomDataset ()
11601172 plane_pos_item .ImagePositionPatient = ds_frame .ImagePositionPatient
11611173 frame_item .PlanePositionSequence = Sequence ([plane_pos_item ])
11621174
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 ])
1175+ # CRITICAL: Do NOT add per-frame PlaneOrientationSequence!
1176+ # PlaneOrientationSequence should ONLY be in SharedFunctionalGroupsSequence
1177+ # Having it per-frame triggers different parsing logic in OHIF/Cornerstone3D
1178+ # Result: metadata not read correctly, spacing[2] = 0
1179+ # (The orientation is shared across all frames anyway)
11691180
11701181 # FrameContentSequence - helps with frame identification
11711182 frame_content_item = DicomDataset ()
@@ -1178,7 +1189,7 @@ def convert_single_frame_dicom_series_to_multiframe(
11781189
11791190 output_ds .PerFrameFunctionalGroupsSequence = Sequence (per_frame_seq )
11801191 logger .info (f" ✓ Added PerFrameFunctionalGroupsSequence with { len (per_frame_seq )} frame items" )
1181- logger .info (f" Each frame includes: PlanePositionSequence + PlaneOrientationSequence " )
1192+ logger .info (f" Each frame includes: PlanePositionSequence only (orientation in shared) " )
11821193
11831194 # Add SharedFunctionalGroupsSequence for additional Cornerstone3D compatibility
11841195 # This defines attributes that are common to ALL frames
@@ -1203,7 +1214,7 @@ def convert_single_frame_dicom_series_to_multiframe(
12031214
12041215 output_ds .SharedFunctionalGroupsSequence = Sequence ([shared_item ])
12051216 logger .info (f" ✓ Added SharedFunctionalGroupsSequence (common attributes for all frames)" )
1206- logger .info (f" (Additional defense against Cornerstone3D < v2.0 bugs )" )
1217+ logger .info (f" Includes PlaneOrientationSequence (ONLY location for orientation! )" )
12071218
12081219 # Verify frame ordering
12091220 if len (per_frame_seq ) > 0 :
0 commit comments