Skip to content

Conversation

@jantonguirao
Copy link

@jantonguirao jantonguirao commented Oct 17, 2025

Add HTJ2K DICOM support and upgrade to pydicom 3.0

Key Changes:

  • Upgrade to pydicom 3.0.0 for HTJ2K support
  • Replace pydicom-seg with highdicom (pydicom-seg unmaintained)
  • Add NvDicomReader for GPU-accelerated DICOM decoding with nvidia-nvimgcodec-cu{XX}
  • Add transcode_dicom_to_htj2k function to convert utils, to batch transcode a directory of DICOMs to Hight Throughput JPEG2000 (lossless)
  • Add transcode_dicom_to_htj2k and convert_single_frame_dicom_series_to_multiframe functions to convert utils:
    • transcode_dicom_to_htj2k: Batch transcode a directory of DICOMs to High Throughput JPEG2000 (lossless)
    • convert_single_frame_dicom_series_to_multiframe: Combine single-frame DICOM series into multi-frame files (optionally with HTJ2K compression)

NvDicomReader Features:

  • HTJ2K transfer syntax support (1.2.840.10008.1.2.4.{201, 202, 203}
  • Supports accelerated JPEG and JPEG2000 as well
  • Batch decoding for DICOM series
  • Proper spatial slice ordering and affine matrix calculation
  • Configurable layouts (NumPy-like D,H,W or ITK-like W,H,D)
  • Fallback to pydicom/SimpleITK when nvimgcodec unavailable

DICOM SEG Improvements:

  • Migrate to highdicom for DICOM SEG creation
  • Preserve ITK/dcmqi fallback path

Optional Dependencies:

  • nvidia-nvimgcodec and dcmqi are optional
  • Runtime checks with clear installation instructions

Testing:

  • Comprehensive NvDicomReader tests (HTJ2K decoding, consistency, metadata)
  • DICOM ↔ NIfTI conversion tests for original and HTJ2K files
  • Automatic HTJ2K test data generation

@jantonguirao jantonguirao changed the title [Draft] Refactor DICOM/NIfTI conversion with highdicom and optional dependencies [Draft] Add HTJ2K DICOM support and upgrade to pydicom 3.0 Oct 17, 2025
@jantonguirao jantonguirao changed the title [Draft] Add HTJ2K DICOM support and upgrade to pydicom 3.0 [Draft] Add HTJ2K DICOM support Oct 17, 2025
@jantonguirao jantonguirao force-pushed the htj2k_support branch 7 times, most recently from abd51e4 to a3a54b3 Compare October 17, 2025 15:40
Key Changes:
- Upgrade to pydicom 3.0.0 for HTJ2K support
- Replace pydicom-seg with highdicom (pydicom-seg unmaintained)
- Add NvDicomReader for GPU-accelerated DICOM decoding with nvidia-nvimgcodec

NvDicomReader Features:
- HTJ2K transfer syntax support (1.2.840.10008.1.2.4.201/202/203)
- Batch decoding optimization for HTJ2K series
- Proper spatial slice ordering and affine matrix calculation
- Configurable layouts (NumPy D,H,W or ITK W,H,D)
- Fallback to pydicom/SimpleITK when nvimgcodec unavailable

DICOM SEG Improvements:
- Migrate to highdicom for DICOM SEG creation
- Memory-efficient processing with stop_before_pixels
- Support up to 65,535 segments (uint16)
- Preserve ITK/dcmqi fallback path

Optional Dependencies:
- nvidia-nvimgcodec and dcmqi are now optional
- Runtime checks with clear installation instructions

Testing:
- Comprehensive NvDicomReader tests (HTJ2K decoding, consistency, metadata)
- DICOM ↔ NIfTI conversion tests for original and HTJ2K files
- Automatic HTJ2K test data generation

Signed-off-by: Joaquin Anton Guirao <[email protected]>
@jantonguirao jantonguirao force-pushed the htj2k_support branch 2 times, most recently from 3736a2b to 119f000 Compare October 17, 2025 17:01
@SachidanandAlle
Copy link
Collaborator

Looks good to me.. however better if all E2E is tested and verified for existing use cases to make sure new changes does break.

Thank you for trying to improve monai label.

Signed-off-by: Joaquin Anton Guirao <[email protected]>
…ch processing for large directories

Signed-off-by: Joaquin Anton Guirao <[email protected]>
… to different series and run monailabel

Signed-off-by: Joaquin Anton Guirao <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Oct 23, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dmoore247
Copy link

dmoore247 commented Oct 24, 2025

@jantonguirao
The PR should handle multi-frame DICOM files that are (now) compressed.
The compression utility create a (Basic or Extended) offset table (An array of integer byte offsets to mark the start of each (compressed) frame). The offset table is stored into the DICOM header.

The frame offsets are used for progressive decoding, download and rendering.

References: https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.4.html

@jantonguirao
Copy link
Author

@jantonguirao The PR should handle multi-frame DICOM files that are (now) compressed. The compression utility create a (Basic or Extended) offset table (An array of integer byte offsets to mark the start of each (compressed) frame). The offset table is stored into the DICOM header.

The frame offsets are used for progressive decoding, download and rendering.

References: https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.4.html

@dmoore247 Currently looking into that. I will let you know once I have something ready for evaluation.

Signed-off-by: Joaquin Anton Guirao <[email protected]>
This commit fixes two critical issues with segmentation display:
1. Segmentations appearing misaligned/misplaced in multi-frame volumes
2. Segmentations misaligned when switching back to previously segmented series

Files modified:
- MonaiLabelPanel.tsx: Core segmentation logic
- PointPrompts.tsx: Removed obsolete method calls

Key changes:

- Use series-specific segmentation IDs (seg-{SeriesUID}) instead of hardcoded '1'
  * Prevents conflicts when working with multiple series
  * Each series maintains its own independent segmentation

- Defer segmentation creation until first inference run
  * Prevents conflicts with default segmentation ID
  * Creates segmentation per-series on demand

- Add origin correction: adapt segmentation to image volume origin
  * Simple approach: copy image volume origin to segmentation
  * No complex camera adjustments or offset calculations
  * Segmentation follows image volume's coordinate system

- Detect series switches and reapply origin correction
  * Subscribe to viewport grid ACTIVE_VIEWPORT_ID_CHANGED event
  * Automatically corrects alignment when switching to existing segmentations
  * Handles both tab changes and thumbnail clicks

- Simplify segmentation creation on demand
  * Single 500ms retry instead of complex 50-attempt retry mechanism
  * Cleaner error handling

Impact:
- Removed 548 lines of complex retry/tracking/correction logic
- Added 136 lines of focused, essential functionality
- Net reduction: 412 lines (41% smaller)
- More maintainable and robust

The solution is elegant: instead of trying to fix the image volume's origin
and adjust cameras accordingly, we simply make the segmentation adapt to
whatever coordinate system the image volume is using. This eliminates all
the complexity around camera position management and origin offset calculations.

Signed-off-by: Joaquin Anton Guirao <[email protected]>
@jantonguirao jantonguirao force-pushed the htj2k_support branch 2 times, most recently from 1854d7e to 0a3fd79 Compare October 28, 2025 10:25
…ation validation

This commit adds extensive test coverage for multi-frame HTJ2K DICOM handling
and improves segmentation output validation across different DICOM formats.

Test Improvements - test_dicom_segmentation.py:
- Add _load_segmentation_array() helper for consistent segmentation loading
- Add _compare_segmentations() helper using Dice coefficient and pixel accuracy
- Refactor test_04 to test_04_compare_all_formats for comprehensive cross-format comparison
  * Compares Standard DICOM, HTJ2K, and Multi-frame HTJ2K outputs
  * Validates all formats produce highly similar segmentations (Dice > 0.95)
- Improve test_05_compare_dicom_vs_nifti with actual segmentation comparison logic
- Update test_06_multiframe_htj2k_inference with corrected test data path
- Remove redundant tests (test_07, test_08, test_09) - functionality consolidated in test_04

Multi-frame HTJ2K Tests - test_convert.py:
- Add HTJ2K_TRANSFER_SYNTAXES constant for explicit transfer syntax validation
- Add test_transcode_dicom_to_htj2k_multiframe_metadata()
  * Validates all DICOM metadata preservation (ImagePositionPatient, ImageOrientationPatient, etc.)
  * Verifies per-frame functional groups match original files
  * Checks frame ordering and spatial attributes
- Add test_transcode_dicom_to_htj2k_multiframe_lossless()
  * Validates pixel-perfect lossless compression
  * Verifies all frames match original pixel data
- Add test_transcode_dicom_to_htj2k_multiframe_nifti_consistency()
  * Ensures multi-frame HTJ2K produces identical NIfTI output as original series
- Update all transfer syntax checks to use HTJ2K_TRANSFER_SYNTAXES constant
  * Replaces .startswith("1.2.840.10008.1.2.4.20") with explicit UID list
  * Covers all three HTJ2K variants (lossless, RPCL, lossy)

Code Cleanup:
- Revert debug logging in monailabel/endpoints/infer.py
- Add HTJ2K transfer syntax documentation in convert.py

All tests pass successfully, validating that:
1. Segmentation outputs are consistent across all DICOM formats
2. Multi-frame HTJ2K transcoding preserves all metadata correctly
3. Multi-frame HTJ2K compression is lossless
4. Multi-frame HTJ2K produces identical results to single-frame series

Signed-off-by: Joaquin Anton Guirao <[email protected]>
- Extract helper functions for frame extraction and validation
  - _extract_frames_from_compressed: Extract frames from encapsulated DICOM
    (now defaults to 1 frame for single-frame images without NumberOfFrames tag)
  - _extract_frames_from_uncompressed: Extract frames from pixel arrays
  - _validate_frames: Check for None values in decoded/encoded frames
  - _find_dicom_files: Recursively find DICOM files with proper sorting

- Add PhotometricInterpretation update from YBR to RGB
  - Prevents double color space conversion by DICOM readers
  - Updates metadata to match actual RGB pixel data after nvimgcodec decoding

- Add fancy_upsampling=1 option to nvimgcodec decoder

- Add comprehensive test coverage using pydicom built-in examples:
  - test_transcode_multiframe_jpeg_ybr_to_htj2k:
    30-frame JPEG with YBR_FULL_422 color space, verifies color space
    conversion and PhotometricInterpretation update (max_diff: 4.0, atol=5)
  - test_transcode_ct_example_to_htj2k:
    Uncompressed CT grayscale (MONOCHROME2), verifies lossless transcoding
  - test_transcode_mr_example_to_htj2k:
    Uncompressed MR grayscale (MONOCHROME2), verifies lossless transcoding
  - test_transcode_rgb_color_example_to_htj2k:
    Uncompressed RGB color image, verifies PhotometricInterpretation
    preservation and lossless transcoding
  - test_transcode_jpeg2k_example_to_htj2k:
    JPEG 2000 with YBR_RCT (reversible color transform), verifies
    PhotometricInterpretation update and perfect lossless conversion
    (max_diff: 0.0)
…retations.

Group frames per PhotometricInterpretation before sending them to decode.

Signed-off-by: Joaquin Anton Guirao <[email protected]>
This commit adds support for all five JPEG2000 progression orders in HTJ2K
encoding, allowing users to optimize compression for different use cases:

- LRCP: Layer-Resolution-Component-Position (quality scalability)
- RLCP: Resolution-Layer-Component-Position (resolution scalability)
- RPCL: Resolution-Position-Component-Layer (progressive by resolution, default)
- PCRL: Position-Component-Resolution-Layer (progressive by spatial area)
- CPRL: Component-Position-Resolution-Layer (component scalability)

Changes:
- Extended _setup_htj2k_encode_params() to accept progression_order parameter
  with validation against supported values
- Added proper Transfer Syntax UID mapping for each progression order
  (1.2.840.10008.1.2.4.201 for LRCP/RLCP/PCRL/CPRL,
   1.2.840.10008.1.2.4.202 for RPCL)
- Changed bitstream type from JP2 to J2K format
- Updated transcode_dicom_to_htj2k() to expose progression_order parameter
- Added comprehensive test suite covering all progression
  orders with various DICOM configurations

This enables better control over HTJ2K encoding characteristics based on
specific deployment requirements (streaming, quality, resolution scalability).

Signed-off-by: Joaquin Anton Guirao <[email protected]>
Introduces a skip_transfer_syntaxes parameter to transcode_dicom_to_htj2k()
that allows skipping transcoding for files already in desired formats.
Files with specified transfer syntaxes are copied directly to output,
avoiding unnecessary re-encoding of already-compressed formats.

Default skip list includes:
- HTJ2K transfer syntaxes (to avoid re-encoding)
- Lossy JPEG 2000 (1.2.840.10008.1.2.4.91)
- Lossy JPEG formats (1.2.840.10008.1.2.4.50, 1.2.840.10008.1.2.4.51)

Also simplifies Basic Offset Table conditional logic and adds comprehensive
unit tests covering skip behavior, statistics tracking,
and edge cases.

Signed-off-by: Joaquin Anton Guirao <[email protected]>
Signed-off-by: Joaquin Anton Guirao <[email protected]>
- transcode_dicom_to_htj2k now accepts file_loader (Iterable) instead of input_dir/output_dir
- Add DicomFileLoader class for simple file discovery and batching
- DicomFileLoader preserves directory structure in output paths
- Support for PyTorch DataLoader and any custom iterable
- Add proper error handling for files without PixelData in both nvimgcodec and pydicom paths
- Files causing exceptions during frame extraction are now properly skipped
- Add test demonstrating PyTorch DataLoader compatibility

Signed-off-by: Joaquin Anton Guirao <[email protected]>
pre-commit-ci bot and others added 7 commits November 17, 2025 12:58
Removed top-level ImagePositionPatient (line ~1102)
Was causing OHIF to use same position for all frames → spacing[2] = 0
Removed top-level ImageOrientationPatient (line ~1108)
Was interfering with functional groups parsing
Added SOPClassUID setting (line ~1115)
Now sets 1.2.840.10008.5.1.4.1.1.2.1 (Enhanced CT Image Storage)
Removed per-frame PlaneOrientationSequence (line ~1163)
Was triggering wrong parsing logic in OHIF
Now only in SharedFunctionalGroupsSequence
Updated logging messages
Reflects actual OHIF requirements and warnings
Remove top-level ImagePositionPatient (prevents 1/Infinity)
Keep top-level ImageOrientationPatient (enables MPR button)
Remove per-frame PlaneOrientationSequence (prevents wrong parsing)
Set correct SOPClassUID (Enhanced CT)
✅ PlanePositionSequence added to every frame (with default if missing)
✅ PlaneOrientationSequence added to SharedFunctionalGroupsSequence (with standard axial if missing)
Both are MANDATORY for Enhanced CT multi-frame files to enable MPR in OHIF.
Now regenerate your multi-frame files with the updated script and the MPR button should be active
Multi-frame DICOM file showed "1/Infinity" in OHIF MPR-axial viewport while working fine in stack view.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants