Skip to content

Commit

Permalink
Merge pull request Enet4#534 from dougyau/jpegls-decode
Browse files Browse the repository at this point in the history
Add support for JPEG-LS decoding
  • Loading branch information
Enet4 authored Aug 31, 2024
2 parents de7dc58 + e593b97 commit 4f2c0d8
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 7 deletions.
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

0 comments on commit 4f2c0d8

Please sign in to comment.