Skip to content

Commit

Permalink
Merge pull request #573 from Enet4/imp/ts-registry/jpeg-ls-encode
Browse files Browse the repository at this point in the history
[ts-registry] Add JPEG-LS pixel data encoder
  • Loading branch information
Enet4 authored Oct 23, 2024
2 parents dee9a34 + fd44db9 commit a3e6a52
Show file tree
Hide file tree
Showing 5 changed files with 458 additions and 22 deletions.
4 changes: 4 additions & 0 deletions toimage/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ fn convert_single_file(
| uids::JPEG2000_LOSSLESS => {
output.set_extension("jp2");
}
uids::JPEGLS_LOSSLESS
| uids::JPEGLS_NEAR_LOSSLESS => {
output.set_extension("jls");
}
_ => {
output.set_extension("data");
}
Expand Down
175 changes: 170 additions & 5 deletions transfer-syntax-registry/src/adapters/jpegls.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
//! Support for JPEG-LS image decoding.

use charls::CharLS;
use dicom_encoding::adapters::{decode_error, DecodeResult, PixelDataObject, PixelDataReader};
use charls::{CharLS, FrameInfo};
use dicom_core::ops::{AttributeAction, AttributeOp};
use dicom_core::Tag;
use dicom_encoding::adapters::{
decode_error, encode_error, DecodeResult, EncodeResult, PixelDataObject, PixelDataReader,
PixelDataWriter,
};
use dicom_encoding::snafu::prelude::*;
use std::borrow::Cow;

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

impl PixelDataReader for JpegLSAdapter {
/// Pixel data writer specifically for JPEG-LS lossless.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct JpegLsLosslessWriter;

impl PixelDataReader for JpegLsAdapter {
/// Decode a single frame in JPEG-LS from a DICOM object.
fn decode_frame(
&self,
Expand Down Expand Up @@ -93,3 +102,159 @@ impl PixelDataReader for JpegLSAdapter {
Ok(())
}
}

impl PixelDataWriter for JpegLsAdapter {
fn encode_frame(
&self,
src: &dyn PixelDataObject,
frame: u32,
options: dicom_encoding::adapters::EncodeOptions,
dst: &mut Vec<u8>,
) -> EncodeResult<Vec<AttributeOp>> {
let cols = src
.cols()
.context(encode_error::MissingAttributeSnafu { name: "Columns" })?;
let rows = src
.rows()
.context(encode_error::MissingAttributeSnafu { name: "Rows" })?;
let samples_per_pixel =
src.samples_per_pixel()
.context(encode_error::MissingAttributeSnafu {
name: "SamplesPerPixel",
})?;
let bits_allocated = src
.bits_allocated()
.context(encode_error::MissingAttributeSnafu {
name: "BitsAllocated",
})?;
let bits_stored = src
.bits_stored()
.context(encode_error::MissingAttributeSnafu { name: "BitsStored" })?;

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

ensure_whatever!(
bits_stored != 1,
"BitsStored of 1 is not supported"
);

let bytes_per_sample = (bits_allocated / 8) as usize;
let frame_size =
cols as usize * rows as usize * samples_per_pixel as usize * bytes_per_sample;

// identify frame data using the frame index
let pixeldata_uncompressed = &src
.raw_pixel_data()
.context(encode_error::MissingAttributeSnafu { name: "Pixel Data" })?
.fragments[0];

let frame_data = pixeldata_uncompressed
.get(frame_size * frame as usize..frame_size * (frame as usize + 1))
.whatever_context("Frame index out of bounds")?;

// Encode the data
let mut encoder = CharLS::default();

let frame_info = FrameInfo {
width: cols as u32,
height: rows as u32,
bits_per_sample: bits_stored as i32,
component_count: samples_per_pixel as i32,
};

// prefer lossless encoding by default
let mut quality = options.quality.map(|q| q.clamp(0, 100)).unwrap_or(100);

let pmi = src.photometric_interpretation();

if pmi == Some("PALETTE COLOR") {
// force lossless encoding of palette color samples
quality = 100;
}

// calculate the maximum acceptable error range
// based on the requested quality and bit depth
let near = ((1 << (bits_stored - 4)) * (100 - quality as i32) / 100).min(4096);

let compressed_data = encoder
.encode(frame_info, near, frame_data)
.whatever_context("JPEG-LS encoding failed")?;

dst.extend_from_slice(&compressed_data);

let mut changes = if near > 0 {
let compressed_frame_size = compressed_data.len();

let compression_ratio = frame_size as f64 / compressed_frame_size as f64;
let compression_ratio = format!("{:.6}", compression_ratio);

// provide attribute changes
vec![
// lossy image compression
AttributeOp::new(Tag(0x0028, 0x2110), AttributeAction::SetStr("01".into())),
// lossy image compression ratio
AttributeOp::new(
Tag(0x0028, 0x2112),
AttributeAction::PushStr(compression_ratio.into()),
),
]
} else {
vec![
// lossless image compression
AttributeOp::new(Tag(0x0028, 0x2110), AttributeAction::SetIfMissing("00".into())),
]
};

if samples_per_pixel == 1 {
// set Photometric Interpretation to MONOCHROME2
// if it was neither of the expected 1-channel formats
if pmi != Some("MONOCHROME1") && pmi != Some("MONOCHROME2") && pmi != Some("PALETTE COLOR") {
changes.push(AttributeOp::new(
Tag(0x0028, 0x0004),
AttributeAction::SetStr("MONOCHROME2".into()),
));
}
} else if samples_per_pixel == 3 {
// set Photometric Interpretation to RGB
// if it was not already set to RGB
if pmi != Some("RGB") {
changes.push(AttributeOp::new(
Tag(0x0028, 0x0004),
AttributeAction::SetStr("RGB".into()),
));
}
}

Ok(changes)
}
}

impl PixelDataWriter for JpegLsLosslessWriter {
fn encode_frame(
&self,
src: &dyn PixelDataObject,
frame: u32,
mut options: dicom_encoding::adapters::EncodeOptions,
dst: &mut Vec<u8>,
) -> EncodeResult<Vec<AttributeOp>> {
// override quality and defer to the main adapter
options.quality = Some(100);
JpegLsAdapter.encode_frame(src, frame, options, dst)
}

fn encode(
&self,
src: &dyn PixelDataObject,
options: dicom_encoding::adapters::EncodeOptions,
dst: &mut Vec<Vec<u8>>,
offset_table: &mut Vec<u32>,
) -> EncodeResult<Vec<AttributeOp>> {
// override quality and defer to the main adapter
let mut options = options;
options.quality = Some(100);
JpegLsAdapter.encode(src, options, dst, offset_table)
}
}
24 changes: 9 additions & 15 deletions transfer-syntax-registry/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ 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;
use crate::adapters::jpegls::{JpegLsAdapter, JpegLsLosslessWriter};
#[cfg(feature = "rle")]
use crate::adapters::rle_lossless::RleLosslessAdapter;

Expand Down Expand Up @@ -264,25 +264,17 @@ 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`]
/// An alias for a transfer syntax specifier with [`JpegLSAdapter`] as the decoder
/// and an arbitrary encoder (since two impls are available)
#[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),
)
}
type JpegLSTs<W> = TransferSyntax<NeverAdapter, JpegLsAdapter, W>;

/// **Decoder Implementation:** JPEG-LS Lossless Image Compression
#[cfg(feature = "charls")]
pub const JPEG_LS_LOSSLESS_IMAGE_COMPRESSION: JpegLSTs = create_ts_jpegls(
pub const JPEG_LS_LOSSLESS_IMAGE_COMPRESSION: JpegLSTs<JpegLsLosslessWriter> = TransferSyntax::new_ele(
"1.2.840.10008.1.2.4.80",
"JPEG-LS Lossless Image Compression",
Codec::EncapsulatedPixelData(Some(JpegLsAdapter), Some(JpegLsLosslessWriter)),
);

/// **Stub descriptor:** JPEG-LS Lossless Image Compression
Expand All @@ -291,11 +283,13 @@ 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(
pub const JPEG_LS_LOSSY_IMAGE_COMPRESSION: JpegLSTs<JpegLsAdapter> = TransferSyntax::new_ele(
"1.2.840.10008.1.2.4.81",
"JPEG-LS Lossy (Near-Lossless) Image Compression",
Codec::EncapsulatedPixelData(Some(JpegLsAdapter), Some(JpegLsAdapter)),
);

/// **Stub descriptor:** JPEG-LS Lossy (Near-Lossless) Image Compression
Expand Down
4 changes: 2 additions & 2 deletions transfer-syntax-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +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-LS Lossless | Cargo feature `charls` | |
//! | JPEG-LS Lossy (Near-Lossless) | Cargo feature `charls` | |
//! | 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 Down
Loading

0 comments on commit a3e6a52

Please sign in to comment.