Skip to content

Commit fa28de8

Browse files
committed
What Was Fixed
Removed top-level ImagePositionPatient (line ~1102) Was causing OHIF to use same position for all frames → spacing[2] = 0 Removed top-level ImageOrientationPatient (line ~1108) Was interfering with functional groups parsing Added SOPClassUID setting (line ~1115) Now sets 1.2.840.10008.5.1.4.1.1.2.1 (Enhanced CT Image Storage) Removed per-frame PlaneOrientationSequence (line ~1163) Was triggering wrong parsing logic in OHIF Now only in SharedFunctionalGroupsSequence Updated logging messages Reflects actual OHIF requirements and warnings
1 parent a0e0732 commit fa28de8

File tree

1 file changed

+32
-21
lines changed

1 file changed

+32
-21
lines changed

monailabel/datastore/utils/convert_htj2k.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)