Skip to content
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
12 changes: 12 additions & 0 deletions pixeldata/src/bin/dicom-transcode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ struct TargetTransferSyntax {
#[cfg(feature = "jpegxl")]
#[clap(long = "jpeg-xl")]
jpeg_xl: bool,

/// Transcode to Deflated Image Frame
#[cfg(feature = "deflate")]
#[clap(long = "deflated-image-frame")]
deflated_image_frame: bool,
}

impl TargetTransferSyntax {
Expand All @@ -109,6 +114,8 @@ impl TargetTransferSyntax {
jpeg_xl_lossless: false,
#[cfg(feature = "jpegxl")]
jpeg_xl: false,
#[cfg(feature = "deflate")]
deflated_image_frame: false,
} => snafu::whatever!("No target transfer syntax specified"),
// explicit VR little endian
TargetTransferSyntax {
Expand Down Expand Up @@ -158,6 +165,11 @@ impl TargetTransferSyntax {
TargetTransferSyntax { jpeg_xl: true, .. } => TransferSyntaxRegistry
.get(uids::JPEGXL)
.whatever_context("Missing specifier for JPEG XL"),
// Deflated Image Frame Compression
#[cfg(feature = "deflate")]
TargetTransferSyntax { deflated_image_frame: true, .. } => TransferSyntaxRegistry
.get(uids::DEFLATED_IMAGE_FRAME_COMPRESSION)
.whatever_context("Missing specifier for Deflated Image Frame Compression"),
TargetTransferSyntax { ts: Some(ts), .. } => TransferSyntaxRegistry
.get(ts)
.whatever_context("Unknown transfer syntax"),
Expand Down
145 changes: 145 additions & 0 deletions transfer-syntax-registry/src/adapters/deflated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//! Support for deflated image frame compression via pixel data adapter.

use dicom_core::{
ops::{AttributeAction, AttributeOp},
PrimitiveValue, Tag,
};
use dicom_encoding::{
adapters::{
DecodeResult, EncodeOptions, EncodeResult, PixelDataObject, PixelDataReader, PixelDataWriter, decode_error, encode_error
},
snafu::{OptionExt, ResultExt},
};

use flate2::{Compression, read::DeflateDecoder, write::DeflateEncoder};

/// Adapter for [Deflated Image Frame Compression][1]
/// [1]: https://dicom.nema.org/medical/dicom/2025c/output/chtml/part05/sect_10.20.html
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DeflatedImageFrameAdapter;

impl PixelDataReader for DeflatedImageFrameAdapter {
fn decode(&self, src: &dyn PixelDataObject, dst: &mut Vec<u8>) -> DecodeResult<()> {
// decod each fragment into the output vector
let pixeldata = src
.raw_pixel_data()
.context(decode_error::MissingAttributeSnafu { name: "Pixel Data" })?;

if pixeldata.fragments.is_empty() {
return Ok(());
}

// set up decoder with the first frame
let mut it = pixeldata.fragments.iter();
let mut decoder = DeflateDecoder::new(
&it.next().expect("fragments should not be empty")[..]
);
std::io::copy(&mut decoder, dst)
.whatever_context("failed to deflate frame")?;
for fragment in it {
decoder.reset(&fragment[..]);
std::io::copy(&mut decoder, dst)
.whatever_context("failed to deflate frame")?;
}

Ok(())
}

fn decode_frame(
&self,
src: &dyn PixelDataObject,
frame: u32,
dst: &mut Vec<u8>,
) -> DecodeResult<()> {

// just copy the specific fragment into the output vector
let pixeldata = src
.raw_pixel_data()
.context(decode_error::MissingAttributeSnafu { name: "Pixel Data" })?;

let fragment = pixeldata
.fragments
.get(frame as usize)
.context(decode_error::FrameRangeOutOfBoundsSnafu)?;

let mut decoder = DeflateDecoder::new(&fragment[..]);
std::io::copy(&mut decoder, dst)
.whatever_context("failed to deflate frame")?;

Ok(())
}
}

impl PixelDataWriter for DeflatedImageFrameAdapter {
fn encode_frame(
&self,
src: &dyn PixelDataObject,
frame: u32,
options: EncodeOptions,
dst: &mut Vec<u8>,
) -> EncodeResult<Vec<AttributeOp>> {
use std::io::Write;

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 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")?;

let len_before = dst.len();

// Deflate the data to the output
let compression = match options.effort {
None => Compression::default(),
Some(0) | Some(1) => Compression::fast(),
// map 0..=100 to 0..=9
Some(e) => Compression::new((e.min(100) / 11) as u32),
};
let mut encoder = DeflateEncoder::new(&mut *dst, compression);
encoder.write_all(frame_data)
.whatever_context("failed to encode deflated data")?;
encoder.finish()
.whatever_context("failed to finish deflated data encoding")?;

if dst.len() % 2 == 1 {
// add null byte to maintain even length
dst.push(0);
}

let fragment_len = dst.len() - len_before;

// provide attribute changes
Ok(vec![
// Encapsulated Pixel Data Value Total Length
AttributeOp::new(
Tag(0x7FE0, 0x0003),
AttributeAction::Set(PrimitiveValue::from(fragment_len as u64)),
),
])
}
}
2 changes: 2 additions & 0 deletions transfer-syntax-registry/src/adapters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub mod jpegls;
pub mod jpegxl;
#[cfg(feature = "rle")]
pub mod rle_lossless;
#[cfg(feature = "deflate")]
pub mod deflated;

pub mod uncompressed;

Expand Down
2 changes: 1 addition & 1 deletion transfer-syntax-registry/src/adapters/uncompressed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl PixelDataWriter for UncompressedAdapter {
.get(frame_size * frame as usize..frame_size * (frame as usize + 1))
.whatever_context("Frame index out of bounds")?;

// Copy the the data to the output
// Copy the data to the output
dst.extend_from_slice(frame_data);

// provide attribute changes
Expand Down
17 changes: 17 additions & 0 deletions transfer-syntax-registry/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ use crate::adapters::jpegxl::{JpegXlAdapter, JpegXlLosslessEncoder};
#[cfg(feature = "rle")]
use crate::adapters::rle_lossless::RleLosslessAdapter;
#[cfg(feature = "deflate")]
use crate::adapters::deflated::DeflatedImageFrameAdapter;
#[cfg(feature = "deflate")]
use crate::deflate::FlateAdapter;

// -- the three base transfer syntaxes, fully supported --
Expand Down Expand Up @@ -446,6 +448,21 @@ pub const JPEG_LS_LOSSY_IMAGE_COMPRESSION: Ts = create_ts_stub(
"JPEG-LS Lossy (Near-Lossless) Image Compression",
);

/// **Stub descriptor:** Deflated Image Frame Compression
#[cfg(not(feature = "deflate"))]
pub const DEFLATED_IMAGE_FRAME_COMPRESSION: Ts = create_ts_stub(
"1.2.840.10008.1.2.8.1",
"Deflated Image Frame Compression",
);

/// **Implemented:** Deflated Image Frame Compression
#[cfg(feature = "deflate")]
pub const DEFLATED_IMAGE_FRAME_COMPRESSION: TransferSyntax<NeverAdapter, DeflatedImageFrameAdapter, DeflatedImageFrameAdapter> = TransferSyntax::new_ele(
"1.2.840.10008.1.2.8.1",
"Deflated Image Frame Compression",
Codec::EncapsulatedPixelData(Some(DeflatedImageFrameAdapter), Some(DeflatedImageFrameAdapter)),
);

/// **Stub descriptor:** JPIP Referenced
pub const JPIP_REFERENCED: Ts = create_ts_stub("1.2.840.10008.1.2.4.94", "JPIP Referenced");

Expand Down
10 changes: 7 additions & 3 deletions transfer-syntax-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@
//! | JPEG XL Recompression | Cargo feature `jpegxl` | x |
//! | JPEG XL | Cargo feature `jpegxl` | ✓ |
//! | RLE Lossless | Cargo feature `rle` | x |
//! | Deflated Image Frame | Cargo feature `deflate` | ✓ |
//!
//! Cargo features behind `native` (`jpeg`, `rle`) are added by default.
//! Cargo features behind `native` (`jpeg`, `rle`, `deflate`)
//! are added by default in the [`dicom-pixeldata` crate][dicom-pixeldata].
//! They provide implementations that are written in pure Rust
//! and are likely available in all supported platforms without issues.
//! Additional codecs are opt-in by enabling Cargo features,
Expand Down Expand Up @@ -104,6 +106,7 @@
//! if using the inventory-based registry.
//!
//! [inventory]: https://docs.rs/inventory/0.3.15/inventory
//! [dicom-pixeldata]: https://docs.rs/dicom-pixeldata

use dicom_encoding::transfer_syntax::{AdapterFreeTransferSyntax as Ts, Codec};
use lazy_static::lazy_static;
Expand Down Expand Up @@ -236,11 +239,11 @@ lazy_static! {

static ref REGISTRY: TransferSyntaxRegistryImpl = {
let mut registry = TransferSyntaxRegistryImpl {
m: HashMap::with_capacity(32),
m: HashMap::with_capacity(64),
};

use self::entries::*;
let built_in_ts: [TransferSyntax; 45] = [
let built_in_ts: [TransferSyntax; 46] = [
IMPLICIT_VR_LITTLE_ENDIAN.erased(),
EXPLICIT_VR_LITTLE_ENDIAN.erased(),
EXPLICIT_VR_BIG_ENDIAN.erased(),
Expand Down Expand Up @@ -286,6 +289,7 @@ lazy_static! {
HEVC_H265_MAIN_PROFILE.erased(),
HEVC_H265_MAIN_10_PROFILE.erased(),
RLE_LOSSLESS.erased(),
DEFLATED_IMAGE_FRAME_COMPRESSION.erased(),
SMPTE_ST_2110_20_UNCOMPRESSED_PROGRESSIVE.erased(),
SMPTE_ST_2110_20_UNCOMPRESSED_INTERLACED.erased(),
SMPTE_ST_2110_30_PCM.erased(),
Expand Down
102 changes: 102 additions & 0 deletions transfer-syntax-registry/tests/deflated_frame.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! Test suite for Deflated Image Frame Compression pixel data reading and writing
#![cfg(feature = "deflate")]

mod adapters;

use adapters::TestDataObject;
use dicom_core::value::PixelFragmentSequence;
use dicom_encoding::{
adapters::{EncodeOptions, PixelDataReader, PixelDataWriter},
Codec,
};
use dicom_transfer_syntax_registry::entries::DEFLATED_IMAGE_FRAME_COMPRESSION;

/// writing to Deflated Image Frame Compression and back
/// should yield exactly the same pixel data
#[test]
fn write_and_read_deflated_frames() {
let rows: u16 = 128;
let columns: u16 = 256;

// build some random RGB image
let mut samples = vec![0; rows as usize * columns as usize * 3];

// use linear congruence to make RGB noise
let mut seed = 0xcfcf_acab_u32;
let mut gen_sample = || {
let r = 4_294_967_291_u32;
let b = 67291_u32;
seed = seed.wrapping_mul(r).wrapping_add(b);
// grab a portion from the seed
(seed >> 7) as u8
};

let slab = 8;
for y in (0..rows as usize).step_by(slab) {
let scan_r = gen_sample();
let scan_g = gen_sample();
let scan_b = gen_sample();

for x in 0..columns as usize {
for k in 0..slab {
let offset = ((y + k) * columns as usize + x) * 3;
samples[offset] = scan_r;
samples[offset + 1] = scan_g;
samples[offset + 2] = scan_b;
}
}
}

// create test object of native encoding
let obj = TestDataObject {
// Explicit VR Little Endian
ts_uid: "1.2.840.10008.1.2.1".to_string(),
rows,
columns,
bits_allocated: 8,
bits_stored: 8,
samples_per_pixel: 3,
photometric_interpretation: "RGB",
number_of_frames: 1,
flat_pixel_data: Some(samples.clone()),
pixel_data_sequence: None,
};

// fetch adapters for Deflated Image Frame Compression

let Codec::EncapsulatedPixelData(Some(reader), Some(writer)) = DEFLATED_IMAGE_FRAME_COMPRESSION.codec() else {
panic!("Deflated Image Frame Compression pixel data adapters not found")
};

let mut encoded = vec![];

let _ops = writer
.encode_frame(&obj, 0, EncodeOptions::default(), &mut encoded)
.expect("Deflated Image Frame encoding failed");

// instantiate new object representing the compressed version

let obj = TestDataObject {
// Deflated Image Frame Compression
ts_uid: "1.2.840.10008.1.2.8.1".to_string(),
rows,
columns,
bits_allocated: 8,
bits_stored: 8,
samples_per_pixel: 3,
photometric_interpretation: "RGB",
number_of_frames: 1,
flat_pixel_data: None,
pixel_data_sequence: Some(PixelFragmentSequence::new(vec![], vec![encoded])),
};

// decode frame
let mut decoded = vec![];

reader
.decode_frame(&obj, 0, &mut decoded)
.expect("Deflated Image Frame decoding failed");

// compare pixels, lossless encoding should yield exactly the same data
assert_eq!(samples, decoded, "pixel data mismatch");
}