Skip to content

Commit d0ae90d

Browse files
authored
Merge pull request #2 from dmoore247/htj2k_support
Multi-frame DICOM file showed "1/Infinity" in OHIF MPR-axial viewport while working fine in stack view.
2 parents a0e0732 + 83bbd0e commit d0ae90d

File tree

1 file changed

+45
-27
lines changed

1 file changed

+45
-27
lines changed

monailabel/datastore/utils/convert_htj2k.py

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

Comments
 (0)