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

Encoding to JPEG transfer syntaxes #303

Merged
merged 10 commits into from
Jul 23, 2023
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