@@ -53,6 +53,7 @@ def _get_nvimgcodec_encoder():
5353 if _NVIMGCODEC_ENCODER is None :
5454 try :
5555 from nvidia import nvimgcodec
56+
5657 _NVIMGCODEC_ENCODER = nvimgcodec .Encoder ()
5758 logger .debug ("Initialized global nvimgcodec.Encoder singleton" )
5859 except ImportError :
@@ -70,6 +71,7 @@ def _get_nvimgcodec_decoder():
7071 if _NVIMGCODEC_DECODER is None :
7172 try :
7273 from nvidia import nvimgcodec
74+
7375 _NVIMGCODEC_DECODER = nvimgcodec .Decoder ()
7476 logger .debug ("Initialized global nvimgcodec.Decoder singleton" )
7577 except ImportError :
@@ -209,9 +211,10 @@ def dicom_to_nifti(series_dir, is_seg=False):
209211
210212 try :
211213 from monai .transforms import LoadImage
214+
212215 from monailabel .transform .reader import NvDicomReader
213216 from monailabel .transform .writer import write_itk
214-
217+
215218 # Use NvDicomReader with LoadImage
216219 reader = NvDicomReader (reverse_indexing = True , use_nvimgcodec = True )
217220 loader = LoadImage (reader = reader , image_only = False )
@@ -552,9 +555,10 @@ def nifti_to_dicom_seg(
552555
553556
554557def itk_image_to_dicom_seg (label , series_dir , template ) -> str :
555- from monailabel .utils .others .generic import run_command
556558 import shutil
557559
560+ from monailabel .utils .others .generic import run_command
561+
558562 command = "itkimage2segimage"
559563 if not shutil .which (command ):
560564 error_msg = (
@@ -648,35 +652,35 @@ def transcode_dicom_to_htj2k(
648652) -> str :
649653 """
650654 Transcode DICOM files to HTJ2K (High Throughput JPEG 2000) lossless compression.
651-
655+
652656 HTJ2K is a faster variant of JPEG 2000 that provides better compression performance
653657 for medical imaging applications. This function uses nvidia-nvimgcodec for encoding
654658 with batch processing for improved performance. All transcoding is performed using
655659 lossless compression to preserve image quality.
656-
660+
657661 The function operates in three phases:
658662 1. Load all DICOM files and prepare pixel arrays
659663 2. Batch encode all images to HTJ2K in parallel
660664 3. Save encoded data back to DICOM files
661-
665+
662666 Args:
663667 input_dir: Path to directory containing DICOM files to transcode
664668 output_dir: Path to output directory for transcoded files. If None, creates temp directory
665669 num_resolutions: Number of resolution levels (default: 6)
666670 code_block_size: Code block size as (height, width) tuple (default: (64, 64))
667671 verify: If True, decode output to verify correctness (default: False)
668-
672+
669673 Returns:
670674 Path to output directory containing transcoded DICOM files
671-
675+
672676 Raises:
673677 ImportError: If nvidia-nvimgcodec or pydicom are not available
674678 ValueError: If input directory doesn't exist or contains no DICOM files
675-
679+
676680 Example:
677681 >>> output_dir = transcode_dicom_to_htj2k("/path/to/dicoms")
678682 >>> # Transcoded files are now in output_dir with lossless HTJ2K compression
679-
683+
680684 Note:
681685 Requires nvidia-nvimgcodec to be installed:
682686 pip install nvidia-nvimgcodec-cu{XX}[all]
@@ -685,7 +689,7 @@ def transcode_dicom_to_htj2k(
685689 import glob
686690 import shutil
687691 from pathlib import Path
688-
692+
689693 # Check for nvidia-nvimgcodec
690694 try :
691695 from nvidia import nvimgcodec
@@ -695,134 +699,136 @@ def transcode_dicom_to_htj2k(
695699 "Install it with: pip install nvidia-nvimgcodec-cu{XX}[all] "
696700 "(replace {XX} with your CUDA version, e.g., cu13)"
697701 )
698-
702+
699703 # Validate input
700704 if not os .path .exists (input_dir ):
701705 raise ValueError (f"Input directory does not exist: { input_dir } " )
702-
706+
703707 if not os .path .isdir (input_dir ):
704708 raise ValueError (f"Input path is not a directory: { input_dir } " )
705-
709+
706710 # Get all DICOM files
707711 dicom_files = []
708712 for pattern in ["*.dcm" , "*" ]:
709713 dicom_files .extend (glob .glob (os .path .join (input_dir , pattern )))
710-
714+
711715 # Filter to actual DICOM files
712716 valid_dicom_files = []
713717 for file_path in dicom_files :
714718 if os .path .isfile (file_path ):
715719 try :
716720 # Quick check if it's a DICOM file
717- with open (file_path , 'rb' ) as f :
721+ with open (file_path , "rb" ) as f :
718722 f .seek (128 )
719723 magic = f .read (4 )
720- if magic == b' DICM' :
724+ if magic == b" DICM" :
721725 valid_dicom_files .append (file_path )
722726 except Exception :
723727 continue
724-
728+
725729 if not valid_dicom_files :
726730 raise ValueError (f"No valid DICOM files found in { input_dir } " )
727-
731+
728732 logger .info (f"Found { len (valid_dicom_files )} DICOM files to transcode" )
729-
733+
730734 # Create output directory
731735 if output_dir is None :
732736 output_dir = tempfile .mkdtemp (prefix = "htj2k_" )
733737 else :
734738 os .makedirs (output_dir , exist_ok = True )
735-
739+
736740 # Create encoder and decoder instances (reused for all files)
737741 encoder = _get_nvimgcodec_encoder ()
738742 decoder = _get_nvimgcodec_decoder () if verify else None
739-
743+
740744 # HTJ2K Transfer Syntax UID - Lossless Only
741745 # 1.2.840.10008.1.2.4.201 = HTJ2K Lossless Only
742746 target_transfer_syntax = "1.2.840.10008.1.2.4.201"
743747 quality_type = nvimgcodec .QualityType .LOSSLESS
744748 logger .info ("Using lossless HTJ2K compression" )
745-
749+
746750 # Configure JPEG2K encoding parameters
747751 jpeg2k_encode_params = nvimgcodec .Jpeg2kEncodeParams ()
748752 jpeg2k_encode_params .num_resolutions = num_resolutions
749753 jpeg2k_encode_params .code_block_size = code_block_size
750754 jpeg2k_encode_params .bitstream_type = nvimgcodec .Jpeg2kBitstreamType .JP2
751755 jpeg2k_encode_params .prog_order = nvimgcodec .Jpeg2kProgOrder .LRCP
752756 jpeg2k_encode_params .ht = True # Enable High Throughput mode
753-
757+
754758 encode_params = nvimgcodec .EncodeParams (
755759 quality_type = quality_type ,
756760 jpeg2k_encode_params = jpeg2k_encode_params ,
757761 )
758-
762+
759763 start_time = time .time ()
760764 transcoded_count = 0
761765 skipped_count = 0
762766 failed_count = 0
763-
767+
764768 # Phase 1: Load all DICOM files and prepare pixel arrays for batch encoding
765769 logger .info ("Phase 1: Loading DICOM files and preparing pixel arrays..." )
766770 dicom_datasets = []
767771 pixel_arrays = []
768772 files_to_encode = []
769-
773+
770774 for i , input_file in enumerate (valid_dicom_files , 1 ):
771775 try :
772776 # Read DICOM
773777 ds = pydicom .dcmread (input_file )
774-
778+
775779 # Check if already HTJ2K
776- current_ts = getattr (ds , ' file_meta' , {}).get (' TransferSyntaxUID' , None )
777- if current_ts and str (current_ts ).startswith (' 1.2.840.10008.1.2.4.20' ):
780+ current_ts = getattr (ds , " file_meta" , {}).get (" TransferSyntaxUID" , None )
781+ if current_ts and str (current_ts ).startswith (" 1.2.840.10008.1.2.4.20" ):
778782 logger .debug (f"[{ i } /{ len (valid_dicom_files )} ] Already HTJ2K: { os .path .basename (input_file )} " )
779783 # Just copy the file
780784 output_file = os .path .join (output_dir , os .path .basename (input_file ))
781785 shutil .copy2 (input_file , output_file )
782786 skipped_count += 1
783787 continue
784-
788+
785789 # Use pydicom's pixel_array to decode the source image
786790 # This handles all transfer syntaxes automatically
787791 source_pixel_array = ds .pixel_array
788-
792+
789793 # Ensure it's a numpy array
790794 if not isinstance (source_pixel_array , np .ndarray ):
791795 source_pixel_array = np .array (source_pixel_array )
792-
796+
793797 # Add channel dimension if needed (nvimgcodec expects shape like (H, W, C))
794798 if source_pixel_array .ndim == 2 :
795799 source_pixel_array = source_pixel_array [:, :, np .newaxis ]
796-
800+
797801 # Store for batch encoding
798802 dicom_datasets .append (ds )
799803 pixel_arrays .append (source_pixel_array )
800804 files_to_encode .append (input_file )
801-
805+
802806 if i % 50 == 0 or i == len (valid_dicom_files ):
803807 logger .info (f"Loading progress: { i } /{ len (valid_dicom_files )} files loaded" )
804-
808+
805809 except Exception as e :
806810 logger .error (f"[{ i } /{ len (valid_dicom_files )} ] Error loading { os .path .basename (input_file )} : { e } " )
807811 failed_count += 1
808812 continue
809-
813+
810814 if not pixel_arrays :
811815 logger .warning ("No images to encode" )
812816 return output_dir
813-
817+
814818 # Phase 2: Batch encode all images to HTJ2K
815819 logger .info (f"Phase 2: Batch encoding { len (pixel_arrays )} images to HTJ2K..." )
816820 encode_start = time .time ()
817-
821+
818822 try :
819823 encoded_htj2k_images = encoder .encode (
820824 pixel_arrays ,
821825 codec = "jpeg2k" ,
822826 params = encode_params ,
823827 )
824828 encode_time = time .time () - encode_start
825- logger .info (f"Batch encoding completed in { encode_time :.2f} seconds ({ len (pixel_arrays )/ encode_time :.1f} images/sec)" )
829+ logger .info (
830+ f"Batch encoding completed in { encode_time :.2f} seconds ({ len (pixel_arrays )/ encode_time :.1f} images/sec)"
831+ )
826832 except Exception as e :
827833 logger .error (f"Batch encoding failed: { e } " )
828834 # Fall back to individual encoding
@@ -839,70 +845,67 @@ def transcode_dicom_to_htj2k(
839845 except Exception as e2 :
840846 logger .error (f"Failed to encode image { idx } : { e2 } " )
841847 encoded_htj2k_images .append (None )
842-
848+
843849 # Phase 3: Save encoded data back to DICOM files
844850 logger .info ("Phase 3: Saving encoded DICOM files..." )
845851 save_start = time .time ()
846-
852+
847853 for idx , (ds , encoded_data , input_file ) in enumerate (zip (dicom_datasets , encoded_htj2k_images , files_to_encode )):
848854 try :
849855 if encoded_data is None :
850856 logger .error (f"Skipping { os .path .basename (input_file )} - encoding failed" )
851857 failed_count += 1
852858 continue
853-
859+
854860 # Encapsulate encoded frames for DICOM
855861 new_encoded_frames = [bytes (encoded_data )]
856862 encapsulated_pixel_data = pydicom .encaps .encapsulate (new_encoded_frames )
857863 ds .PixelData = encapsulated_pixel_data
858-
864+
859865 # Update transfer syntax UID
860866 ds .file_meta .TransferSyntaxUID = pydicom .uid .UID (target_transfer_syntax )
861-
867+
862868 # Save to output directory
863869 output_file = os .path .join (output_dir , os .path .basename (input_file ))
864870 ds .save_as (output_file )
865-
871+
866872 # Verify if requested
867873 if verify :
868874 ds_verify = pydicom .dcmread (output_file )
869875 pixel_data = ds_verify .PixelData
870876 data_sequence = pydicom .encaps .decode_data_sequence (pixel_data )
871877 images_verify = decoder .decode (
872878 data_sequence ,
873- params = nvimgcodec .DecodeParams (
874- allow_any_depth = True ,
875- color_spec = nvimgcodec .ColorSpec .UNCHANGED
876- ),
879+ params = nvimgcodec .DecodeParams (allow_any_depth = True , color_spec = nvimgcodec .ColorSpec .UNCHANGED ),
877880 )
878881 image_verify = np .array (images_verify [0 ].cpu ()).squeeze ()
879-
882+
880883 if not np .allclose (image_verify , ds_verify .pixel_array ):
881884 logger .warning (f"Verification failed for { os .path .basename (input_file )} " )
882885 failed_count += 1
883886 continue
884-
887+
885888 transcoded_count += 1
886-
889+
887890 if (idx + 1 ) % 50 == 0 or (idx + 1 ) == len (dicom_datasets ):
888891 logger .info (f"Saving progress: { idx + 1 } /{ len (dicom_datasets )} files saved" )
889-
892+
890893 except Exception as e :
891894 logger .error (f"Error saving { os .path .basename (input_file )} : { e } " )
892895 failed_count += 1
893896 continue
894-
897+
895898 save_time = time .time () - save_start
896899 logger .info (f"Saving completed in { save_time :.2f} seconds" )
897-
900+
898901 elapsed_time = time .time () - start_time
899-
902+
900903 logger .info (f"Transcoding complete:" )
901904 logger .info (f" Total files: { len (valid_dicom_files )} " )
902905 logger .info (f" Successfully transcoded: { transcoded_count } " )
903906 logger .info (f" Already HTJ2K (copied): { skipped_count } " )
904907 logger .info (f" Failed: { failed_count } " )
905908 logger .info (f" Time elapsed: { elapsed_time :.2f} seconds" )
906909 logger .info (f" Output directory: { output_dir } " )
907-
910+
908911 return output_dir
0 commit comments