1010# limitations under the License.
1111
1212import os
13+ import shutil
1314import tempfile
1415import unittest
1516from pathlib import Path
@@ -786,7 +787,9 @@ def test_transcode_dicom_to_htj2k_multiframe_metadata(self):
786787 first_original = original_datasets [0 ]
787788
788789 # Check ImagePositionPatient is NOT there at top level DICOM file
789- self .assertFalse (hasattr (ds_multiframe , "ImagePositionPatient" ), "Should not have ImagePositionPatient at top level" )
790+ self .assertFalse (
791+ hasattr (ds_multiframe , "ImagePositionPatient" ), "Should not have ImagePositionPatient at top level"
792+ )
790793
791794 # Check PixelSpacing
792795 self .assertTrue (hasattr (ds_multiframe , "PixelSpacing" ), "Should have PixelSpacing" )
@@ -812,31 +815,31 @@ def test_transcode_dicom_to_htj2k_multiframe_metadata(self):
812815 # Check SOPClassUID conversion to Enhanced/Multi-frame
813816 self .assertTrue (hasattr (ds_multiframe , "SOPClassUID" ), "Should have SOPClassUID" )
814817 self .assertTrue (hasattr (first_original , "SOPClassUID" ), "Original should have SOPClassUID" )
815-
818+
816819 # Map of single-frame to enhanced/multi-frame SOPClassUIDs
817820 sopclass_map = {
818- "1.2.840.10008.5.1.4.1.1.2" : "1.2.840.10008.5.1.4.1.1.2.1" , # CT -> Enhanced CT
819- "1.2.840.10008.5.1.4.1.1.4" : "1.2.840.10008.5.1.4.1.1.4.1" , # MR -> Enhanced MR
820- "1.2.840.10008.5.1.4.1.1.6.1" : "1.2.840.10008.5.1.4.1.1.3.1" , # US -> Ultrasound Multi-frame
821+ "1.2.840.10008.5.1.4.1.1.2" : "1.2.840.10008.5.1.4.1.1.2.1" , # CT -> Enhanced CT
822+ "1.2.840.10008.5.1.4.1.1.4" : "1.2.840.10008.5.1.4.1.1.4.1" , # MR -> Enhanced MR
823+ "1.2.840.10008.5.1.4.1.1.6.1" : "1.2.840.10008.5.1.4.1.1.3.1" , # US -> Ultrasound Multi-frame
821824 }
822-
825+
823826 original_sopclass = str (first_original .SOPClassUID )
824827 multiframe_sopclass = str (ds_multiframe .SOPClassUID )
825-
828+
826829 if original_sopclass in sopclass_map :
827830 expected_sopclass = sopclass_map [original_sopclass ]
828831 self .assertEqual (
829832 multiframe_sopclass ,
830833 expected_sopclass ,
831- f"SOPClassUID should be converted from { original_sopclass } to { expected_sopclass } "
834+ f"SOPClassUID should be converted from { original_sopclass } to { expected_sopclass } " ,
832835 )
833836 print (f"✓ SOPClassUID converted: { original_sopclass } -> { multiframe_sopclass } " )
834837 else :
835838 # If not in map, should remain unchanged
836839 self .assertEqual (
837840 multiframe_sopclass ,
838841 original_sopclass ,
839- "SOPClassUID should remain unchanged if not in conversion map"
842+ "SOPClassUID should remain unchanged if not in conversion map" ,
840843 )
841844 print (f"✓ SOPClassUID unchanged: { multiframe_sopclass } " )
842845
@@ -1974,6 +1977,139 @@ def collate_paths(batch):
19741977 shutil .rmtree (input_dir , ignore_errors = True )
19751978 shutil .rmtree (output_dir , ignore_errors = True )
19761979
1980+ def test_convert_multiframe_handles_missing_pixeldata (self ):
1981+ """Test that convert_single_frame_dicom_series_to_multiframe handles datasets without PixelData."""
1982+ if not HAS_NVIMGCODEC :
1983+ self .skipTest (
1984+ "nvimgcodec not available. Install nvidia-nvimgcodec-cu{XX} matching your CUDA version (e.g., nvidia-nvimgcodec-cu13 for CUDA 13.x)"
1985+ )
1986+
1987+ # Create temporary directory with mixed DICOM files
1988+ input_dir = tempfile .mkdtemp (prefix = "test_missing_pixeldata_" )
1989+ output_dir = tempfile .mkdtemp (prefix = "test_missing_pixeldata_output_" )
1990+
1991+ try :
1992+ # Create a series with some files having PixelData and some without
1993+ study_uid = pydicom .uid .generate_uid ()
1994+ series_uid = pydicom .uid .generate_uid ()
1995+
1996+ print (f"\n Creating test series with mixed PixelData presence..." )
1997+
1998+ # Create 3 valid DICOM files with PixelData
1999+ valid_files = []
2000+ for i in range (3 ):
2001+ ds = pydicom .Dataset ()
2002+ ds .StudyInstanceUID = study_uid
2003+ ds .SeriesInstanceUID = series_uid
2004+ ds .SOPInstanceUID = pydicom .uid .generate_uid ()
2005+ ds .SOPClassUID = "1.2.840.10008.5.1.4.1.1.2" # CT Image Storage
2006+ ds .InstanceNumber = i + 1
2007+ ds .Modality = "CT"
2008+ ds .PatientName = "Test^Patient"
2009+ ds .PatientID = "12345"
2010+
2011+ # Add spatial metadata
2012+ ds .ImagePositionPatient = [0.0 , 0.0 , float (i * 2.5 )]
2013+ ds .ImageOrientationPatient = [1.0 , 0.0 , 0.0 , 0.0 , 1.0 , 0.0 ]
2014+ ds .PixelSpacing = [0.5 , 0.5 ]
2015+ ds .SliceThickness = 2.5
2016+
2017+ # Add image data
2018+ ds .Rows = 64
2019+ ds .Columns = 64
2020+ ds .SamplesPerPixel = 1
2021+ ds .PhotometricInterpretation = "MONOCHROME2"
2022+ ds .BitsAllocated = 16
2023+ ds .BitsStored = 16
2024+ ds .HighBit = 15
2025+ ds .PixelRepresentation = 0
2026+
2027+ # Create pixel data
2028+ pixel_array = np .random .randint (0 , 1000 , (64 , 64 ), dtype = np .uint16 )
2029+ ds .PixelData = pixel_array .tobytes ()
2030+
2031+ # Save file with proper file meta
2032+ ds .file_meta = pydicom .dataset .FileMetaDataset ()
2033+ ds .file_meta .FileMetaInformationVersion = b"\x00 \x01 "
2034+ ds .file_meta .TransferSyntaxUID = pydicom .uid .ExplicitVRLittleEndian
2035+ ds .file_meta .MediaStorageSOPClassUID = ds .SOPClassUID
2036+ ds .file_meta .MediaStorageSOPInstanceUID = ds .SOPInstanceUID
2037+ ds .file_meta .ImplementationClassUID = pydicom .uid .PYDICOM_IMPLEMENTATION_UID
2038+
2039+ filepath = os .path .join (input_dir , f"valid_{ i :03d} .dcm" )
2040+ # Use save_as which properly writes DICOM Part 10 format with preamble
2041+ ds .save_as (filepath , enforce_file_format = True )
2042+ valid_files .append (filepath )
2043+ print (f" Created valid file: { os .path .basename (filepath )} " )
2044+
2045+ # Create 2 DICOM files WITHOUT PixelData (like SR or metadata-only)
2046+ for i in range (2 ):
2047+ ds = pydicom .Dataset ()
2048+ ds .StudyInstanceUID = study_uid
2049+ ds .SeriesInstanceUID = series_uid
2050+ ds .SOPInstanceUID = pydicom .uid .generate_uid ()
2051+ ds .SOPClassUID = "1.2.840.10008.5.1.4.1.1.2" # CT Image Storage
2052+ ds .InstanceNumber = i + 10
2053+ ds .Modality = "CT"
2054+ ds .PatientName = "Test^Patient"
2055+ ds .PatientID = "12345"
2056+
2057+ # Add spatial metadata but NO PixelData
2058+ ds .ImagePositionPatient = [0.0 , 0.0 , float ((i + 10 ) * 2.5 )]
2059+ ds .ImageOrientationPatient = [1.0 , 0.0 , 0.0 , 0.0 , 1.0 , 0.0 ]
2060+
2061+ # Save file with proper file meta
2062+ ds .file_meta = pydicom .dataset .FileMetaDataset ()
2063+ ds .file_meta .FileMetaInformationVersion = b"\x00 \x01 "
2064+ ds .file_meta .TransferSyntaxUID = pydicom .uid .ExplicitVRLittleEndian
2065+ ds .file_meta .MediaStorageSOPClassUID = ds .SOPClassUID
2066+ ds .file_meta .MediaStorageSOPInstanceUID = ds .SOPInstanceUID
2067+ ds .file_meta .ImplementationClassUID = pydicom .uid .PYDICOM_IMPLEMENTATION_UID
2068+
2069+ filepath = os .path .join (input_dir , f"no_pixel_{ i :03d} .dcm" )
2070+ # Use save_as which properly writes DICOM Part 10 format with preamble
2071+ ds .save_as (filepath , enforce_file_format = True )
2072+ print (f" Created file without PixelData: { os .path .basename (filepath )} " )
2073+
2074+ print (f"✓ Created { len (valid_files )} valid files and 2 files without PixelData" )
2075+
2076+ # Convert to multiframe - should skip files without PixelData
2077+ result_dir = convert_single_frame_dicom_series_to_multiframe (
2078+ input_dir = input_dir ,
2079+ output_dir = output_dir ,
2080+ convert_to_htj2k = True ,
2081+ )
2082+
2083+ # Verify multiframe file was created
2084+ multiframe_files = list (Path (result_dir ).rglob ("*.dcm" ))
2085+ self .assertEqual (len (multiframe_files ), 1 , "Should create one multiframe file" )
2086+ print (f"✓ Created multiframe file: { multiframe_files [0 ]} " )
2087+
2088+ # Load and verify the multiframe file
2089+ ds_multiframe = pydicom .dcmread (str (multiframe_files [0 ]))
2090+
2091+ # Should have 3 frames (only the valid files)
2092+ self .assertTrue (hasattr (ds_multiframe , "NumberOfFrames" ), "Should have NumberOfFrames" )
2093+ num_frames = int (ds_multiframe .NumberOfFrames )
2094+ self .assertEqual (num_frames , 3 , "Should have 3 frames (files without PixelData excluded)" )
2095+ print (f"✓ NumberOfFrames: { num_frames } (correctly excluded files without PixelData)" )
2096+
2097+ # Verify PerFrameFunctionalGroupsSequence has correct number of items
2098+ self .assertTrue (
2099+ hasattr (ds_multiframe , "PerFrameFunctionalGroupsSequence" ),
2100+ "Should have PerFrameFunctionalGroupsSequence" ,
2101+ )
2102+ per_frame_seq = ds_multiframe .PerFrameFunctionalGroupsSequence
2103+ self .assertEqual (len (per_frame_seq ), 3 , "Should have 3 per-frame items" )
2104+ print (f"✓ PerFrameFunctionalGroupsSequence has { len (per_frame_seq )} items" )
2105+
2106+ print (f"✓ Test passed: Files without PixelData were correctly skipped" )
2107+
2108+ finally :
2109+ # Clean up
2110+ shutil .rmtree (input_dir , ignore_errors = True )
2111+ shutil .rmtree (output_dir , ignore_errors = True )
2112+
19772113
19782114if __name__ == "__main__" :
19792115 unittest .main ()
0 commit comments