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

Decode single frame using GDCM #422

Merged
merged 5 commits into from
Nov 20, 2023
Merged
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
206 changes: 194 additions & 12 deletions pixeldata/src/gdcm.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Decode pixel data using GDCM when the default features are enabled.

use crate::*;
use dicom_dictionary_std::tags;
use dicom_encoding::adapters::DecodeError;
use dicom_encoding::transfer_syntax::TransferSyntaxIndex;
use dicom_transfer_syntax_registry::TransferSyntaxRegistry;
Expand All @@ -18,28 +19,31 @@ where
use super::attribute::*;

let pixel_data = pixel_data(self).context(GetAttributeSnafu)?;

let cols = cols(self).context(GetAttributeSnafu)?;
let rows = rows(self).context(GetAttributeSnafu)?;

let photometric_interpretation =
photometric_interpretation(self).context(GetAttributeSnafu)?;
let pi_type = GDCMPhotometricInterpretation::from_str(photometric_interpretation.as_str())
.map_err(|_| {
UnsupportedPhotometricInterpretationSnafu {
pi: photometric_interpretation.clone(),
}
.build()
})?;
let pi_type = match photometric_interpretation {
PhotometricInterpretation::PaletteColor => GDCMPhotometricInterpretation::PALETTE_COLOR,
_ => GDCMPhotometricInterpretation::from_str(photometric_interpretation.as_str())
.map_err(|_| {
UnsupportedPhotometricInterpretationSnafu {
pi: photometric_interpretation.clone(),
}
.build()
})?,
};

let transfer_syntax = &self.meta().transfer_syntax;
let registry =
TransferSyntaxRegistry
.get(&&transfer_syntax)
.get(transfer_syntax)
.context(UnknownTransferSyntaxSnafu {
ts_uid: transfer_syntax,
})?;
let ts_type = GDCMTransferSyntax::from_str(&registry.uid()).map_err(|_| {
let ts_type = GDCMTransferSyntax::from_str(registry.uid()).map_err(|_| {
UnsupportedTransferSyntaxSnafu {
ts: transfer_syntax.clone(),
}
Expand All @@ -66,7 +70,7 @@ where
};
if fragments.len() > 1 {
// Bundle fragments and decode multi-frame dicoms
let dims = [cols.into(), rows.into(), number_of_frames.into()];
let dims = [cols.into(), rows.into(), number_of_frames];
let fragments: Vec<_> = fragments.iter().map(|frag| frag.as_slice()).collect();
decode_multi_frame_compressed(
fragments.as_slice(),
Expand Down Expand Up @@ -144,6 +148,184 @@ where
window,
})
}

fn decode_pixel_data_frame(&self, frame: u32) -> Result<DecodedPixelData<'_>> {
use super::attribute::*;

let pixel_data = pixel_data(self).context(GetAttributeSnafu)?;

let cols = cols(self).context(GetAttributeSnafu)?;
let rows = rows(self).context(GetAttributeSnafu)?;

let photometric_interpretation =
photometric_interpretation(self).context(GetAttributeSnafu)?;
let pi_type = match photometric_interpretation {
PhotometricInterpretation::PaletteColor => GDCMPhotometricInterpretation::PALETTE_COLOR,
_ => GDCMPhotometricInterpretation::from_str(photometric_interpretation.as_str())
.map_err(|_| {
UnsupportedPhotometricInterpretationSnafu {
pi: photometric_interpretation.clone(),
}
.build()
})?,
};

let transfer_syntax = &self.meta().transfer_syntax;
let registry =
TransferSyntaxRegistry
.get(transfer_syntax)
.context(UnknownTransferSyntaxSnafu {
ts_uid: transfer_syntax,
})?;
let ts_type = GDCMTransferSyntax::from_str(registry.uid()).map_err(|_| {
UnsupportedTransferSyntaxSnafu {
ts: transfer_syntax.clone(),
}
.build()
})?;

let samples_per_pixel = samples_per_pixel(self).context(GetAttributeSnafu)?;
let bits_allocated = bits_allocated(self).context(GetAttributeSnafu)?;
let bits_stored = bits_stored(self).context(GetAttributeSnafu)?;
let high_bit = high_bit(self).context(GetAttributeSnafu)?;
let pixel_representation = pixel_representation(self).context(GetAttributeSnafu)?;
let planar_configuration = if let Ok(el) = self.element(tags::PLANAR_CONFIGURATION) {
el.uint16().unwrap_or(0)
} else {
0
};
let rescale_intercept = rescale_intercept(self);
let rescale_slope = rescale_slope(self);
let number_of_frames = number_of_frames(self).context(GetAttributeSnafu)?;
let voi_lut_function = voi_lut_function(self).context(GetAttributeSnafu)?;
let voi_lut_function = voi_lut_function.and_then(|v| VoiLutFunction::try_from(&*v).ok());

let decoded_pixel_data = match pixel_data.value() {
Value::PixelSequence(v) => {
let fragments = v.fragments();
let gdcm_error_mapper = |source: GDCMError| DecodeError::Custom {
message: source.to_string(),
source: Some(Box::new(source)),
};

let frame = frame as usize;
let data = if number_of_frames == 1 && fragments.len() > 1 {
fragments.iter().flat_map(|frame| frame.to_vec()).collect()
} else {
fragments[frame].to_vec()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the frame comprises more than one fragment in a multi-frame pixel data fragment sequence? I do not think that we can make this assumption.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only single frame dicoms can have more than 1 fragment. Multi frame files, must be 1 fragment per frame. At least according to the standard.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can move forward with this assumption and revisit if we find a conflicting sample case. 👍

Copy link
Owner

@Enet4 Enet4 Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a heads-up, I received clarifications from DICOM WG6, who asserted that it is allowed for frames in a multi-frame instance to span through multiple fragments each, though more recent transfer syntaxes tend to disallow this.

Still, I won't make this a blocker so that these improvements can be delivered. The affected area is only for the integration with GDCM, which hopefully the project will depend less on over time.

};

match ts_type {
GDCMTransferSyntax::ImplicitVRLittleEndian
| GDCMTransferSyntax::ExplicitVRLittleEndian => {
// This is just in case of encapsulated uncompressed data
let frame_size = cols * rows * samples_per_pixel * (bits_allocated / 8);
data.chunks_exact(frame_size as usize)
.nth(frame)
.map(|frame| frame.to_vec())
.unwrap_or_default()
}
_ => {
let buffer = [data.as_slice()];
let dims = [cols.into(), rows.into(), 1];

decode_multi_frame_compressed(
&buffer,
&dims,
pi_type,
ts_type,
samples_per_pixel,
bits_allocated,
bits_stored,
high_bit,
pixel_representation as u16,
)
.map_err(gdcm_error_mapper)
.context(DecodePixelDataSnafu)?
.to_vec()
}
}
}
Value::Primitive(p) => {
// Uncompressed data
let frame_size = cols as usize
* rows as usize
* samples_per_pixel as usize
* (bits_allocated as usize / 8);
p.to_bytes()
.chunks_exact(frame_size)
.nth(frame as usize)
.map(|frame| frame.to_vec())
.unwrap_or_default()
}
Value::Sequence(_) => InvalidPixelDataSnafu.fail()?,
};

// Convert to PlanarConfiguration::Standard
let decoded_pixel_data = if planar_configuration == 1 && samples_per_pixel == 3 {
interleave_planes(
cols as usize,
rows as usize,
bits_allocated as usize,
decoded_pixel_data,
)
} else {
decoded_pixel_data
};

let window = match (
window_center(self).context(GetAttributeSnafu)?,
window_width(self).context(GetAttributeSnafu)?,
) {
(Some(center), Some(width)) => Some(WindowLevel { center, width }),
_ => None,
};

Ok(DecodedPixelData {
data: Cow::from(decoded_pixel_data),
cols: cols.into(),
rows: rows.into(),
number_of_frames: 1,
photometric_interpretation,
samples_per_pixel,
planar_configuration: PlanarConfiguration::Standard,
bits_allocated,
bits_stored,
high_bit,
pixel_representation,
rescale_intercept,
rescale_slope,
voi_lut_function,
window,
})
}
}

fn interleave_planes(cols: usize, rows: usize, bits_allocated: usize, data: Vec<u8>) -> Vec<u8> {
let frame_size = cols * rows * (bits_allocated / 8);
let mut interleaved = Vec::with_capacity(data.len());

let mut i = 0;
while i < frame_size {
interleaved.push(data[i]);
if bits_allocated > 8 {
interleaved.push(data[i + 1])
}

interleaved.push(data[i + frame_size]);
if bits_allocated > 8 {
interleaved.push(data[i + frame_size + 1])
}

interleaved.push(data[i + frame_size * 2]);
if bits_allocated > 8 {
interleaved.push(data[i + frame_size * 2 + 1])
}

i = if bits_allocated > 8 { i + 2 } else { i + 1 };
}

interleaved
}

#[cfg(test)]
Expand Down Expand Up @@ -225,7 +407,7 @@ mod tests {
#[case("pydicom/SC_rgb_rle_2frame.dcm", 1)]
#[case("pydicom/JPEG2000.dcm", 0)]
#[case("pydicom/JPEG2000_UNC.dcm", 0)]
fn test_parse_dicom_pixel_data_individual_frames(#[case] value: &str, #[case] frame: u32) {
fn test_parse_dicom_pixel_data_individual_frames(#[case] value: &str, #[case] frame: u32) {
let test_file = dicom_test_files::path(value).unwrap();
println!("Parsing pixel data for {}", test_file.display());
let obj = open_file(test_file).unwrap();
Expand Down
Loading