Skip to content

Commit 638fd66

Browse files
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
1 parent 119f000 commit 638fd66

File tree

5 files changed

+223
-233
lines changed

5 files changed

+223
-233
lines changed

monailabel/datastore/utils/convert.py

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

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

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)