Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JPEG-LS decoding #534

Merged
merged 18 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ jobs:
- run: cargo test --features image,ndarray,sop-class,rle,cli
# test dicom-pixeldata with openjp2
- run: cargo test -p dicom-pixeldata --features openjp2
# test dicom-pixeldata with openjpeg-sys
- run: cargo test -p dicom-pixeldata --features openjpeg-sys
# test dicom-pixeldata with openjpeg-sys and charls
- run: cargo test -p dicom-pixeldata --features openjpeg-sys,charls
# test dicom-pixeldata with gdcm-rs
- run: cargo test -p dicom-pixeldata --features gdcm
# test dicom-pixeldata without default features
Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pixeldata/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ rle = ["dicom-transfer-syntax-registry/rle"]
openjpeg-sys = ["dicom-transfer-syntax-registry/openjpeg-sys"]
# JPEG 2000 decoding via Rust port of OpenJPEG
openjp2 = ["dicom-transfer-syntax-registry/openjp2"]
# JpegLS via CharLS
charls = ["dicom-transfer-syntax-registry/charls"]

# replace pixel data decoding to use GDCM
gdcm = ["gdcm-rs"]
Expand Down
13 changes: 8 additions & 5 deletions pixeldata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2714,11 +2714,12 @@ mod tests {
case("pydicom/JPEG2000.dcm", 1)
)]
//
// jpeg-ls encoding not supported
#[should_panic(expected = "UnsupportedTransferSyntax { ts: \"1.2.840.10008.1.2.4.80\"")]
#[case("pydicom/emri_small_jpeg_ls_lossless.dcm", 10)]
#[should_panic(expected = "UnsupportedTransferSyntax { ts: \"1.2.840.10008.1.2.4.80\"")]
#[case("pydicom/MR_small_jpeg_ls_lossless.dcm", 1)]
// jpeg-ls encoding
#[cfg_attr(
feature = "charls",
case("pydicom/emri_small_jpeg_ls_lossless.dcm", 10)
)]
#[cfg_attr(feature = "charls", case("pydicom/MR_small_jpeg_ls_lossless.dcm", 1))]
//
// sample precision of 12 not supported yet
#[should_panic(expected = "Unsupported(SamplePrecision(12))")]
Expand Down Expand Up @@ -2775,6 +2776,8 @@ mod tests {
#[case("pydicom/SC_rgb_rle_2frame.dcm", 0)]
#[case("pydicom/SC_rgb_rle_2frame.dcm", 1)]
#[case("pydicom/JPEG2000_UNC.dcm", 0)]
#[cfg_attr(feature = "charls", case("pydicom/emri_small_jpeg_ls_lossless.dcm", 5))]
#[cfg_attr(feature = "charls", case("pydicom/MR_small_jpeg_ls_lossless.dcm", 0))]
fn test_decode_pixel_data_individual_frames(#[case] value: &str, #[case] frame: u32) {
use crate::PixelDecoder as _;
use std::path::Path;
Expand Down
8 changes: 8 additions & 0 deletions transfer-syntax-registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ simd = ["jpeg-encoder?/simd"]
# conflicts with `openjp2`
openjpeg-sys = ["dep:jpeg2k", "jpeg2k/openjpeg-sys"]

# jpeg LS support via charls bindings
charls = ["dep:charls"]

# build OpenJPEG with multithreading,
# implies "rayon"
openjpeg-sys-threads = ["rayon", "jpeg2k?/threads"]
Expand All @@ -59,6 +62,11 @@ optional = true
version = "0.6"
optional = true

[dependencies.charls]
version = "0.3"
optional = true
features = ["static"]

[package.metadata.docs.rs]
features = ["native"]

Expand Down
95 changes: 95 additions & 0 deletions transfer-syntax-registry/src/adapters/jpegls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Support for JPEG-LS image decoding.

use charls::CharLS;
use dicom_encoding::adapters::{decode_error, DecodeResult, PixelDataObject, PixelDataReader};
use dicom_encoding::snafu::prelude::*;
use std::borrow::Cow;

/// Pixel data adapter for JPEG-LS transfer syntax.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct JpegLSAdapter;

impl PixelDataReader for JpegLSAdapter {
/// Decode a single frame in JPEG-LS from a DICOM object.
fn decode_frame(
&self,
src: &dyn PixelDataObject,
frame: u32,
dst: &mut Vec<u8>,
) -> DecodeResult<()> {
let bits_allocated = src
.bits_allocated()
.context(decode_error::MissingAttributeSnafu {
name: "BitsAllocated",
})?;

ensure_whatever!(
bits_allocated == 8 || bits_allocated == 16,
"BitsAllocated other than 8 or 16 is not supported"
);

let nr_frames = src.number_of_frames().unwrap_or(1) as usize;

ensure!(
nr_frames > frame as usize,
decode_error::FrameRangeOutOfBoundsSnafu
);

let raw = src
.raw_pixel_data()
.whatever_context("Expected to have raw pixel data available")?;

let frame_data = if raw.fragments.len() == 1 || raw.fragments.len() == nr_frames {
// assuming 1:1 frame-to-fragment mapping
Cow::Borrowed(
raw.fragments
.get(frame as usize)
.with_whatever_context(|| {
format!("Missing fragment #{} for the frame requested", frame)
})?,
)
} else {
// Some embedded JPEGs might span multiple fragments.
// In this case we look up the basic offset table
// and gather all of the frame's fragments in a single vector.
// Note: not the most efficient way to do this,
// consider optimizing later with byte chunk readers
let base_offset = raw.offset_table.get(frame as usize).copied();
let base_offset = if frame == 0 {
base_offset.unwrap_or(0) as usize
} else {
base_offset
.with_whatever_context(|| format!("Missing offset for frame #{}", frame))?
as usize
};
let next_offset = raw.offset_table.get(frame as usize + 1);

let mut offset = 0;
let mut fragments = Vec::new();
for fragment in &raw.fragments {
// include it
if offset >= base_offset {
fragments.extend_from_slice(fragment);
}
offset += fragment.len() + 8;
if let Some(&next_offset) = next_offset {
if offset >= next_offset as usize {
// next fragment is for the next frame
break;
}
}
}

Cow::Owned(fragments)
};

let mut decoded = CharLS::default()
.decode(&frame_data)
.map_err(|error| error.to_string())
.with_whatever_context(|error| error.to_string())?;

dst.append(&mut decoded);

Ok(())
}
}
7 changes: 7 additions & 0 deletions transfer-syntax-registry/src/adapters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
pub mod jpeg;
#[cfg(any(feature = "openjp2", feature = "openjpeg-sys"))]
pub mod jpeg2k;
#[cfg(feature = "charls")]
pub mod jpegls;
#[cfg(feature = "rle")]
pub mod rle_lossless;

Expand All @@ -46,3 +48,8 @@ pub mod jpeg2k {}
/// Enable the `rle` feature to use this module.
#[cfg(not(feature = "rle"))]
pub mod rle {}

/// **Note:** This module is a stub.
/// Enable the `charls` feature to use this module.
#[cfg(not(feature = "charls"))]
pub mod jpegls {}
32 changes: 32 additions & 0 deletions transfer-syntax-registry/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ use dicom_encoding::NeverPixelAdapter;
use crate::adapters::jpeg::JpegAdapter;
#[cfg(any(feature = "openjp2", feature = "openjpeg-sys"))]
use crate::adapters::jpeg2k::Jpeg2000Adapter;
#[cfg(feature = "charls")]
use crate::adapters::jpegls::JpegLSAdapter;
#[cfg(feature = "rle")]
use crate::adapters::rle_lossless::RleLosslessAdapter;

Expand Down Expand Up @@ -262,12 +264,42 @@ pub const JPEG_2000_PART2_MULTI_COMPONENT_IMAGE_COMPRESSION: Ts = create_ts_stub

// --- partially supported transfer syntaxes, pixel data encapsulation not supported ---

/// An alias for a transfer syntax specifier with [`JpegLSAdapter`]
#[cfg(feature = "charls")]
type JpegLSTs<R = JpegLSAdapter, W = NeverPixelAdapter> = TransferSyntax<NeverAdapter, R, W>;

/// Create JPEG-LS TransferSyntax
#[cfg(feature = "charls")]
const fn create_ts_jpegls(uid: &'static str, name: &'static str) -> JpegLSTs {
TransferSyntax::new_ele(
uid,
name,
Codec::EncapsulatedPixelData(Some(JpegLSAdapter), None),
)
}

/// **Decoder Implementation:** JPEG-LS Lossless Image Compression
#[cfg(feature = "charls")]
pub const JPEG_LS_LOSSLESS_IMAGE_COMPRESSION: JpegLSTs = create_ts_jpegls(
"1.2.840.10008.1.2.4.80",
"JPEG-LS Lossless Image Compression",
);

/// **Stub descriptor:** JPEG-LS Lossless Image Compression
#[cfg(not(feature = "charls"))]
pub const JPEG_LS_LOSSLESS_IMAGE_COMPRESSION: Ts = create_ts_stub(
"1.2.840.10008.1.2.4.80",
"JPEG-LS Lossless Image Compression",
);
/// **Decoder Implementation:** JPEG-LS Lossy (Near-Lossless) Image Compression
#[cfg(feature = "charls")]
pub const JPEG_LS_LOSSY_IMAGE_COMPRESSION: JpegLSTs = create_ts_jpegls(
"1.2.840.10008.1.2.4.81",
"JPEG-LS Lossy (Near-Lossless) Image Compression",
);

/// **Stub descriptor:** JPEG-LS Lossy (Near-Lossless) Image Compression
#[cfg(not(feature = "charls"))]
pub const JPEG_LS_LOSSY_IMAGE_COMPRESSION: Ts = create_ts_stub(
"1.2.840.10008.1.2.4.81",
"JPEG-LS Lossy (Near-Lossless) Image Compression",
Expand Down
6 changes: 6 additions & 0 deletions transfer-syntax-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
//! | JPEG Extended (Process 2 & 4) | Cargo feature `jpeg` | x |
//! | JPEG Lossless, Non-Hierarchical (Process 14) | Cargo feature `jpeg` | x |
//! | JPEG Lossless, Non-Hierarchical, First-Order Prediction (Process 14 [Selection Value 1]) | Cargo feature `jpeg` | x |
//! | JPEG-LS Lossless | Cargo feature `charls` | x |
//! | JPEG-LS Lossy (Near-Lossless) | Cargo feature `charls` | x |
//! | JPEG 2000 (Lossless Only) | Cargo feature `openjp2` or `openjpeg-sys` | x |
//! | JPEG 2000 | Cargo feature `openjp2` or `openjpeg-sys` | x |
//! | JPEG 2000 Part 2 Multi-component Image Compression (Lossless Only) | Cargo feature `openjp2` or `openjpeg-sys` | x |
Expand All @@ -68,6 +70,10 @@
//! However, a native implementation might not always be available,
//! or alternative implementations may be preferred:
//!
//! - `charls` provides support for JPEG-LS
//! by linking to the CharLS reference implementation,
//! which is written in C++.
//! No alternative JPEG-LS implementations are available at the moment.
//! - `openjpeg-sys` provides a binding to the OpenJPEG reference implementation,
//! which is written in C and is statically linked.
//! It may offer better performance than the pure Rust implementation,
Expand Down
Loading