Skip to content

Commit 37ca835

Browse files
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
1 parent 7e9e7de commit 37ca835

File tree

6 files changed

+228
-236
lines changed

6 files changed

+228
-236
lines changed

monailabel/datastore/utils/convert.py

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

554557
def 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

monailabel/transform/reader.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ def _get_nvimgcodec_decoder():
5555
"""Get or create a thread-local nvimgcodec decoder singleton."""
5656
if not has_nvimgcodec:
5757
raise RuntimeError("nvimgcodec is not available. Cannot create decoder.")
58-
59-
if not hasattr(_thread_local, 'decoder') or _thread_local.decoder is None:
58+
59+
if not hasattr(_thread_local, "decoder") or _thread_local.decoder is None:
6060
_thread_local.decoder = nvimgcodec.Decoder()
6161
logger.debug(f"Initialized thread-local nvimgcodec.Decoder for thread {threading.current_thread().name}")
62-
62+
6363
return _thread_local.decoder
6464

6565

tests/integration/radiology_serverless/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,3 @@
88
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
11-

0 commit comments

Comments
 (0)