Skip to content

Commit

Permalink
Merge pull request #303 from Almeida-a/new/jpeg-encoding
Browse files Browse the repository at this point in the history
Encoding to JPEG transfer syntaxes
  • Loading branch information
Enet4 authored Jul 23, 2023
2 parents 74048f4 + fa0316b commit affdbc6
Show file tree
Hide file tree
Showing 6 changed files with 541 additions and 31 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

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

9 changes: 8 additions & 1 deletion transfer-syntax-registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ inventory-registry = ['dicom-encoding/inventory-registry']
# natively implemented image encodings
native = ["jpeg", "rle"]
# native JPEG support
jpeg = ["jpeg-decoder"]
jpeg = ["jpeg-decoder", "jpeg-encoder"]
# native RLE lossless support
rle = []
rayon = ["jpeg-decoder?/rayon"]
Expand All @@ -34,5 +34,12 @@ tracing = "0.1.34"
version = "0.3.0"
optional = true

[dependencies.jpeg-encoder]
version = "0.5.1"
optional = true

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

[dev-dependencies]
dicom-test-files = "0.2.1"
159 changes: 137 additions & 22 deletions transfer-syntax-registry/src/adapters/jpeg.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
//! Support for JPG image decoding.
use dicom_core::ops::{AttributeAction, AttributeOp};
use dicom_core::Tag;
use dicom_encoding::adapters::{
decode_error, encode_error, DecodeResult, EncodeOptions, EncodeResult, PixelDataObject,
PixelDataReader, PixelDataWriter,
};
use dicom_encoding::snafu::prelude::*;
use dicom_encoding::adapters::{DecodeResult, PixelDataObject, decode_error, PixelDataReader};
use jpeg_decoder::Decoder;
use jpeg_encoder::ColorType;
use std::io::Cursor;

/// Pixel data adapter for JPEG-based transfer syntaxes.
Expand All @@ -15,13 +21,19 @@ impl PixelDataReader for JpegAdapter {
let cols = src
.cols()
.context(decode_error::MissingAttributeSnafu { name: "Columns" })?;
let rows = src.rows().context(decode_error::MissingAttributeSnafu { name: "Rows" })?;
let samples_per_pixel = src.samples_per_pixel().context(decode_error::MissingAttributeSnafu {
name: "SamplesPerPixel",
})?;
let bits_allocated = src.bits_allocated().context(decode_error::MissingAttributeSnafu {
name: "BitsAllocated",
})?;
let rows = src
.rows()
.context(decode_error::MissingAttributeSnafu { name: "Rows" })?;
let samples_per_pixel =
src.samples_per_pixel()
.context(decode_error::MissingAttributeSnafu {
name: "SamplesPerPixel",
})?;
let bits_allocated = src
.bits_allocated()
.context(decode_error::MissingAttributeSnafu {
name: "BitsAllocated",
})?;

if bits_allocated != 8 && bits_allocated != 16 {
whatever!("BitsAllocated other than 8 or 16 is not supported");
Expand All @@ -33,7 +45,10 @@ impl PixelDataReader for JpegAdapter {
// `stride` it the total number of bytes for each sample plane
let stride: usize = bytes_per_sample as usize * cols as usize * rows as usize;
let base_offset = dst.len();
dst.resize(base_offset + (samples_per_pixel as usize * stride) * nr_frames, 0);
dst.resize(
base_offset + (samples_per_pixel as usize * stride) * nr_frames,
0,
);

// Embedded jpegs can span multiple fragments
// Hence we collect all fragments into single vector
Expand Down Expand Up @@ -76,25 +91,40 @@ impl PixelDataReader for JpegAdapter {
}

/// Decode DICOM image data with jpeg encoding.
fn decode_frame(&self, src: &dyn PixelDataObject, frame: u32, dst: &mut Vec<u8>) -> DecodeResult<()> {
fn decode_frame(
&self,
src: &dyn PixelDataObject,
frame: u32,
dst: &mut Vec<u8>,
) -> DecodeResult<()> {
let cols = src
.cols()
.context(decode_error::MissingAttributeSnafu { name: "Columns" })?;
let rows = src.rows().context(decode_error::MissingAttributeSnafu { name: "Rows" })?;
let samples_per_pixel = src.samples_per_pixel().context(decode_error::MissingAttributeSnafu {
name: "SamplesPerPixel",
})?;
let bits_allocated = src.bits_allocated().context(decode_error::MissingAttributeSnafu {
name: "BitsAllocated",
})?;
let rows = src
.rows()
.context(decode_error::MissingAttributeSnafu { name: "Rows" })?;
let samples_per_pixel =
src.samples_per_pixel()
.context(decode_error::MissingAttributeSnafu {
name: "SamplesPerPixel",
})?;
let bits_allocated = src
.bits_allocated()
.context(decode_error::MissingAttributeSnafu {
name: "BitsAllocated",
})?;

if bits_allocated != 8 && bits_allocated != 16 {
whatever!("BitsAllocated other than 8 or 16 is not supported");
}
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);
ensure!(
nr_frames > frame as usize,
decode_error::FrameRangeOutOfBoundsSnafu
);

let bytes_per_sample = bits_allocated / 8;

Expand All @@ -106,7 +136,8 @@ impl PixelDataReader for JpegAdapter {
// Embedded jpegs can span multiple fragments
// Hence we collect all fragments into single vector
// and then just iterate a cursor for each frame
let raw_pixeldata = src.raw_pixel_data()
let raw_pixeldata = src
.raw_pixel_data()
.whatever_context("Expected to have raw pixel data available")?;
let fragment = raw_pixeldata
.fragments
Expand Down Expand Up @@ -142,3 +173,87 @@ impl PixelDataReader for JpegAdapter {
Ok(())
}
}

impl PixelDataWriter for JpegAdapter {
fn encode_frame(
&self,
src: &dyn PixelDataObject,
frame: u32,
options: 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",
})?;

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

let quality = options.quality.unwrap_or(85);

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;

let color_type = match samples_per_pixel {
1 => ColorType::Luma,
3 => ColorType::Rgb,
_ => whatever!("Unsupported samples per pixel: {}", samples_per_pixel),
};

let photometric_interpretation = match samples_per_pixel {
1 => "MONOCHROME2",
3 => "RGB",
_ => unreachable!(),
};

// record dst length before encoding to know full jpeg size
let len_before = dst.len();

// Encode the data
let frame_uncompressed = src
.fragment(frame as usize)
.context(encode_error::FrameRangeOutOfBoundsSnafu)?;
let mut encoder = jpeg_encoder::Encoder::new(&mut *dst, quality);
encoder.set_progressive(false);
encoder
.encode(&frame_uncompressed, cols, rows, color_type)
.whatever_context("JPEG encoding failed")?;

let compressed_frame_size = dst.len() - len_before;

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

// provide attribute changes
Ok(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()),
),
// Photometric interpretation
AttributeOp::new(
Tag(0x0028, 0x0004),
AttributeAction::SetStr(photometric_interpretation.into()),
),
])
}
}
25 changes: 17 additions & 8 deletions transfer-syntax-registry/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,25 +81,29 @@ pub const RLE_LOSSLESS: Ts = create_ts_stub("1.2.840.10008.1.2.5", "RLE Lossless

// JPEG encoded pixel data

/// An alias for a transfer syntax specifier with `JpegPixelAdapter`
/// (note that only decoding is supported at the moment).
/// An alias for a transfer syntax specifier with [`JpegAdapter`]
/// (supports decoding and encoding to JPEG baseline,
/// support for JPEG extended and JPEG lossless may vary).
#[cfg(feature = "jpeg")]
type JpegTs<R = JpegAdapter, W = NeverPixelAdapter> = TransferSyntax<NeverAdapter, R, W>;
type JpegTs<R = JpegAdapter, W = JpegAdapter> = TransferSyntax<NeverAdapter, R, W>;

/// Create a transfer syntax with JPEG encapsulated pixel data
#[cfg(feature = "jpeg")]
const fn create_ts_jpeg(uid: &'static str, name: &'static str) -> JpegTs {
const fn create_ts_jpeg(uid: &'static str, name: &'static str, encoder: bool) -> JpegTs {
TransferSyntax::new_ele(
uid,
name,
Codec::EncapsulatedPixelData(Some(JpegAdapter), None),
Codec::EncapsulatedPixelData(
Some(JpegAdapter),
if encoder { Some(JpegAdapter) } else { None },
),
)
}

/// **Implemented:** JPEG Baseline (Process 1): Default Transfer Syntax for Lossy JPEG 8 Bit Image Compression
#[cfg(feature = "jpeg")]
pub const JPEG_BASELINE: JpegTs =
create_ts_jpeg("1.2.840.10008.1.2.4.50", "JPEG Baseline (Process 1)");
create_ts_jpeg("1.2.840.10008.1.2.4.50", "JPEG Baseline (Process 1)", true);
/// **Implemented:** JPEG Baseline (Process 1): Default Transfer Syntax for Lossy JPEG 8 Bit Image Compression
///
/// A native implementation is available
Expand All @@ -109,8 +113,11 @@ pub const JPEG_BASELINE: Ts = create_ts_stub("1.2.840.10008.1.2.4.50", "JPEG Bas

/// **Implemented:** JPEG Extended (Process 2 & 4): Default Transfer Syntax for Lossy JPEG 12 Bit Image Compression (Process 4 only)
#[cfg(feature = "jpeg")]
pub const JPEG_EXTENDED: JpegTs =
create_ts_jpeg("1.2.840.10008.1.2.4.51", "JPEG Extended (Process 2 & 4)");
pub const JPEG_EXTENDED: JpegTs = create_ts_jpeg(
"1.2.840.10008.1.2.4.51",
"JPEG Extended (Process 2 & 4)",
false,
);
/// **Stub descriptor:** JPEG Extended (Process 2 & 4): Default Transfer Syntax for Lossy JPEG 12 Bit Image Compression (Process 4 only)
///
/// A native implementation is available
Expand All @@ -124,6 +131,7 @@ pub const JPEG_EXTENDED: Ts =
pub const JPEG_LOSSLESS_NON_HIERARCHICAL: JpegTs = create_ts_jpeg(
"1.2.840.10008.1.2.4.57",
"JPEG Lossless, Non-Hierarchical (Process 14)",
false,
);
/// **Stub descriptor:** JPEG Lossless, Non-Hierarchical (Process 14)
///
Expand All @@ -142,6 +150,7 @@ pub const JPEG_LOSSLESS_NON_HIERARCHICAL: Ts = create_ts_stub(
pub const JPEG_LOSSLESS_NON_HIERARCHICAL_FIRST_ORDER_PREDICTION: JpegTs = create_ts_jpeg(
"1.2.840.10008.1.2.4.70",
"JPEG Lossless, Non-Hierarchical, First-Order Prediction",
false,
);
/// **Stub descriptor:** JPEG Lossless, Non-Hierarchical, First-Order Prediction
/// (Process 14 [Selection Value 1]):
Expand Down
Loading

0 comments on commit affdbc6

Please sign in to comment.