@@ -169,9 +169,10 @@ def dicom_to_nifti(series_dir, is_seg=False):
169169
170170 try :
171171 from monai .transforms import LoadImage
172+
172173 from monailabel .transform .reader import NvDicomReader
173174 from monailabel .transform .writer import write_itk
174-
175+
175176 # Use NvDicomReader with LoadImage
176177 reader = NvDicomReader (reverse_indexing = True , use_nvimgcodec = True )
177178 loader = LoadImage (reader = reader , image_only = False )
@@ -512,9 +513,10 @@ def nifti_to_dicom_seg(
512513
513514
514515def itk_image_to_dicom_seg (label , series_dir , template ) -> str :
515- from monailabel .utils .others .generic import run_command
516516 import shutil
517517
518+ from monailabel .utils .others .generic import run_command
519+
518520 command = "itkimage2segimage"
519521 if not shutil .which (command ):
520522 error_msg = (
@@ -608,35 +610,35 @@ def transcode_dicom_to_htj2k(
608610) -> str :
609611 """
610612 Transcode DICOM files to HTJ2K (High Throughput JPEG 2000) lossless compression.
611-
613+
612614 HTJ2K is a faster variant of JPEG 2000 that provides better compression performance
613615 for medical imaging applications. This function uses nvidia-nvimgcodec for encoding
614616 with batch processing for improved performance. All transcoding is performed using
615617 lossless compression to preserve image quality.
616-
618+
617619 The function operates in three phases:
618620 1. Load all DICOM files and prepare pixel arrays
619621 2. Batch encode all images to HTJ2K in parallel
620622 3. Save encoded data back to DICOM files
621-
623+
622624 Args:
623625 input_dir: Path to directory containing DICOM files to transcode
624626 output_dir: Path to output directory for transcoded files. If None, creates temp directory
625627 num_resolutions: Number of resolution levels (default: 6)
626628 code_block_size: Code block size as (height, width) tuple (default: (64, 64))
627629 verify: If True, decode output to verify correctness (default: False)
628-
630+
629631 Returns:
630632 Path to output directory containing transcoded DICOM files
631-
633+
632634 Raises:
633635 ImportError: If nvidia-nvimgcodec or pydicom are not available
634636 ValueError: If input directory doesn't exist or contains no DICOM files
635-
637+
636638 Example:
637639 >>> output_dir = transcode_dicom_to_htj2k("/path/to/dicoms")
638640 >>> # Transcoded files are now in output_dir with lossless HTJ2K compression
639-
641+
640642 Note:
641643 Requires nvidia-nvimgcodec to be installed:
642644 pip install nvidia-nvimgcodec-cu{XX}[all]
@@ -645,7 +647,7 @@ def transcode_dicom_to_htj2k(
645647 import glob
646648 import shutil
647649 from pathlib import Path
648-
650+
649651 # Check for nvidia-nvimgcodec
650652 try :
651653 from nvidia import nvimgcodec
@@ -655,134 +657,136 @@ def transcode_dicom_to_htj2k(
655657 "Install it with: pip install nvidia-nvimgcodec-cu{XX}[all] "
656658 "(replace {XX} with your CUDA version, e.g., cu13)"
657659 )
658-
660+
659661 # Validate input
660662 if not os .path .exists (input_dir ):
661663 raise ValueError (f"Input directory does not exist: { input_dir } " )
662-
664+
663665 if not os .path .isdir (input_dir ):
664666 raise ValueError (f"Input path is not a directory: { input_dir } " )
665-
667+
666668 # Get all DICOM files
667669 dicom_files = []
668670 for pattern in ["*.dcm" , "*" ]:
669671 dicom_files .extend (glob .glob (os .path .join (input_dir , pattern )))
670-
672+
671673 # Filter to actual DICOM files
672674 valid_dicom_files = []
673675 for file_path in dicom_files :
674676 if os .path .isfile (file_path ):
675677 try :
676678 # Quick check if it's a DICOM file
677- with open (file_path , 'rb' ) as f :
679+ with open (file_path , "rb" ) as f :
678680 f .seek (128 )
679681 magic = f .read (4 )
680- if magic == b' DICM' :
682+ if magic == b" DICM" :
681683 valid_dicom_files .append (file_path )
682684 except Exception :
683685 continue
684-
686+
685687 if not valid_dicom_files :
686688 raise ValueError (f"No valid DICOM files found in { input_dir } " )
687-
689+
688690 logger .info (f"Found { len (valid_dicom_files )} DICOM files to transcode" )
689-
691+
690692 # Create output directory
691693 if output_dir is None :
692694 output_dir = tempfile .mkdtemp (prefix = "htj2k_" )
693695 else :
694696 os .makedirs (output_dir , exist_ok = True )
695-
697+
696698 # Create encoder and decoder instances (reused for all files)
697699 encoder = nvimgcodec .Encoder ()
698700 decoder = nvimgcodec .Decoder () if verify else None
699-
701+
700702 # HTJ2K Transfer Syntax UID - Lossless Only
701703 # 1.2.840.10008.1.2.4.201 = HTJ2K Lossless Only
702704 target_transfer_syntax = "1.2.840.10008.1.2.4.201"
703705 quality_type = nvimgcodec .QualityType .LOSSLESS
704706 logger .info ("Using lossless HTJ2K compression" )
705-
707+
706708 # Configure JPEG2K encoding parameters
707709 jpeg2k_encode_params = nvimgcodec .Jpeg2kEncodeParams ()
708710 jpeg2k_encode_params .num_resolutions = num_resolutions
709711 jpeg2k_encode_params .code_block_size = code_block_size
710712 jpeg2k_encode_params .bitstream_type = nvimgcodec .Jpeg2kBitstreamType .JP2
711713 jpeg2k_encode_params .prog_order = nvimgcodec .Jpeg2kProgOrder .LRCP
712714 jpeg2k_encode_params .ht = True # Enable High Throughput mode
713-
715+
714716 encode_params = nvimgcodec .EncodeParams (
715717 quality_type = quality_type ,
716718 jpeg2k_encode_params = jpeg2k_encode_params ,
717719 )
718-
720+
719721 start_time = time .time ()
720722 transcoded_count = 0
721723 skipped_count = 0
722724 failed_count = 0
723-
725+
724726 # Phase 1: Load all DICOM files and prepare pixel arrays for batch encoding
725727 logger .info ("Phase 1: Loading DICOM files and preparing pixel arrays..." )
726728 dicom_datasets = []
727729 pixel_arrays = []
728730 files_to_encode = []
729-
731+
730732 for i , input_file in enumerate (valid_dicom_files , 1 ):
731733 try :
732734 # Read DICOM
733735 ds = pydicom .dcmread (input_file )
734-
736+
735737 # Check if already HTJ2K
736- current_ts = getattr (ds , ' file_meta' , {}).get (' TransferSyntaxUID' , None )
737- if current_ts and str (current_ts ).startswith (' 1.2.840.10008.1.2.4.20' ):
738+ current_ts = getattr (ds , " file_meta" , {}).get (" TransferSyntaxUID" , None )
739+ if current_ts and str (current_ts ).startswith (" 1.2.840.10008.1.2.4.20" ):
738740 logger .debug (f"[{ i } /{ len (valid_dicom_files )} ] Already HTJ2K: { os .path .basename (input_file )} " )
739741 # Just copy the file
740742 output_file = os .path .join (output_dir , os .path .basename (input_file ))
741743 shutil .copy2 (input_file , output_file )
742744 skipped_count += 1
743745 continue
744-
746+
745747 # Use pydicom's pixel_array to decode the source image
746748 # This handles all transfer syntaxes automatically
747749 source_pixel_array = ds .pixel_array
748-
750+
749751 # Ensure it's a numpy array
750752 if not isinstance (source_pixel_array , np .ndarray ):
751753 source_pixel_array = np .array (source_pixel_array )
752-
754+
753755 # Add channel dimension if needed (nvimgcodec expects shape like (H, W, C))
754756 if source_pixel_array .ndim == 2 :
755757 source_pixel_array = source_pixel_array [:, :, np .newaxis ]
756-
758+
757759 # Store for batch encoding
758760 dicom_datasets .append (ds )
759761 pixel_arrays .append (source_pixel_array )
760762 files_to_encode .append (input_file )
761-
763+
762764 if i % 50 == 0 or i == len (valid_dicom_files ):
763765 logger .info (f"Loading progress: { i } /{ len (valid_dicom_files )} files loaded" )
764-
766+
765767 except Exception as e :
766768 logger .error (f"[{ i } /{ len (valid_dicom_files )} ] Error loading { os .path .basename (input_file )} : { e } " )
767769 failed_count += 1
768770 continue
769-
771+
770772 if not pixel_arrays :
771773 logger .warning ("No images to encode" )
772774 return output_dir
773-
775+
774776 # Phase 2: Batch encode all images to HTJ2K
775777 logger .info (f"Phase 2: Batch encoding { len (pixel_arrays )} images to HTJ2K..." )
776778 encode_start = time .time ()
777-
779+
778780 try :
779781 encoded_htj2k_images = encoder .encode (
780782 pixel_arrays ,
781783 codec = "jpeg2k" ,
782784 params = encode_params ,
783785 )
784786 encode_time = time .time () - encode_start
785- logger .info (f"Batch encoding completed in { encode_time :.2f} seconds ({ len (pixel_arrays )/ encode_time :.1f} images/sec)" )
787+ logger .info (
788+ f"Batch encoding completed in { encode_time :.2f} seconds ({ len (pixel_arrays )/ encode_time :.1f} images/sec)"
789+ )
786790 except Exception as e :
787791 logger .error (f"Batch encoding failed: { e } " )
788792 # Fall back to individual encoding
@@ -799,70 +803,67 @@ def transcode_dicom_to_htj2k(
799803 except Exception as e2 :
800804 logger .error (f"Failed to encode image { idx } : { e2 } " )
801805 encoded_htj2k_images .append (None )
802-
806+
803807 # Phase 3: Save encoded data back to DICOM files
804808 logger .info ("Phase 3: Saving encoded DICOM files..." )
805809 save_start = time .time ()
806-
810+
807811 for idx , (ds , encoded_data , input_file ) in enumerate (zip (dicom_datasets , encoded_htj2k_images , files_to_encode )):
808812 try :
809813 if encoded_data is None :
810814 logger .error (f"Skipping { os .path .basename (input_file )} - encoding failed" )
811815 failed_count += 1
812816 continue
813-
817+
814818 # Encapsulate encoded frames for DICOM
815819 new_encoded_frames = [bytes (encoded_data )]
816820 encapsulated_pixel_data = pydicom .encaps .encapsulate (new_encoded_frames )
817821 ds .PixelData = encapsulated_pixel_data
818-
822+
819823 # Update transfer syntax UID
820824 ds .file_meta .TransferSyntaxUID = pydicom .uid .UID (target_transfer_syntax )
821-
825+
822826 # Save to output directory
823827 output_file = os .path .join (output_dir , os .path .basename (input_file ))
824828 ds .save_as (output_file )
825-
829+
826830 # Verify if requested
827831 if verify :
828832 ds_verify = pydicom .dcmread (output_file )
829833 pixel_data = ds_verify .PixelData
830834 data_sequence = pydicom .encaps .decode_data_sequence (pixel_data )
831835 images_verify = decoder .decode (
832836 data_sequence ,
833- params = nvimgcodec .DecodeParams (
834- allow_any_depth = True ,
835- color_spec = nvimgcodec .ColorSpec .UNCHANGED
836- ),
837+ params = nvimgcodec .DecodeParams (allow_any_depth = True , color_spec = nvimgcodec .ColorSpec .UNCHANGED ),
837838 )
838839 image_verify = np .array (images_verify [0 ].cpu ()).squeeze ()
839-
840+
840841 if not np .allclose (image_verify , ds_verify .pixel_array ):
841842 logger .warning (f"Verification failed for { os .path .basename (input_file )} " )
842843 failed_count += 1
843844 continue
844-
845+
845846 transcoded_count += 1
846-
847+
847848 if (idx + 1 ) % 50 == 0 or (idx + 1 ) == len (dicom_datasets ):
848849 logger .info (f"Saving progress: { idx + 1 } /{ len (dicom_datasets )} files saved" )
849-
850+
850851 except Exception as e :
851852 logger .error (f"Error saving { os .path .basename (input_file )} : { e } " )
852853 failed_count += 1
853854 continue
854-
855+
855856 save_time = time .time () - save_start
856857 logger .info (f"Saving completed in { save_time :.2f} seconds" )
857-
858+
858859 elapsed_time = time .time () - start_time
859-
860+
860861 logger .info (f"Transcoding complete:" )
861862 logger .info (f" Total files: { len (valid_dicom_files )} " )
862863 logger .info (f" Successfully transcoded: { transcoded_count } " )
863864 logger .info (f" Already HTJ2K (copied): { skipped_count } " )
864865 logger .info (f" Failed: { failed_count } " )
865866 logger .info (f" Time elapsed: { elapsed_time :.2f} seconds" )
866867 logger .info (f" Output directory: { output_dir } " )
867-
868+
868869 return output_dir
0 commit comments