Skip to main content

dicomview_core/
dicom_decode.rs

1//! DICOM parsing, decompression, and modality-space voxel decoding.
2
3use crate::metadata::{extract_frame_metadata, FrameMetadata, MetadataError};
4use dicom_toolkit_codec::decode_pixel_data;
5use dicom_toolkit_core::error::DcmError;
6use dicom_toolkit_data::{io::DicomReader, DataSet, FileFormat, PixelData, Value};
7use dicom_toolkit_dict::tags;
8use dicom_toolkit_image::{pixel, ModalityLut, PixelRepresentation};
9use std::io::Cursor;
10use thiserror::Error;
11
12/// One decoded grayscale image frame plus its extracted metadata.
13#[derive(Debug, Clone, PartialEq)]
14pub struct DecodedFrame {
15    /// Geometry and decoding metadata for this frame.
16    pub metadata: FrameMetadata,
17    /// Pixel values after decompression and modality LUT application.
18    pub pixels: Vec<i16>,
19}
20
21/// Errors raised while decoding DICOM bytes into voxels.
22#[derive(Debug, Error)]
23pub enum DicomDecodeError {
24    /// The DICOM payload could not be parsed or decompressed.
25    #[error(transparent)]
26    Dicom(#[from] DcmError),
27    /// Frame metadata could not be extracted.
28    #[error(transparent)]
29    Metadata(#[from] MetadataError),
30    /// The image uses an unsupported sample layout for volumetric decoding.
31    #[error(
32        "unsupported samples per pixel {samples_per_pixel}; only grayscale images are supported"
33    )]
34    UnsupportedSamplesPerPixel {
35        /// Unsupported sample count.
36        samples_per_pixel: u16,
37    },
38    /// The image uses a pixel storage layout that is not yet supported.
39    #[error("unsupported BitsAllocated={bits_allocated} for volumetric decoding")]
40    UnsupportedBitsAllocated {
41        /// Unsupported bit depth.
42        bits_allocated: u16,
43    },
44    /// The frame count does not match the available native bytes.
45    #[error(
46        "pixel data length {actual} does not contain {frames} frame(s) of {bytes_per_frame} bytes"
47    )]
48    NativePixelLengthMismatch {
49        /// Raw pixel byte length found in the dataset.
50        actual: usize,
51        /// Number of frames expected from metadata.
52        frames: u32,
53        /// Bytes required per frame.
54        bytes_per_frame: usize,
55    },
56    /// No pixel data element was present in the dataset.
57    #[error("missing pixel data element")]
58    MissingPixelData,
59    /// The decoded pixel count does not match the frame dimensions.
60    #[error("decoded frame {frame_index} has {actual} pixels, expected {expected}")]
61    PixelCountMismatch {
62        /// Zero-based frame index.
63        frame_index: u32,
64        /// Expected grayscale voxel count.
65        expected: usize,
66        /// Actual number of decoded values.
67        actual: usize,
68    },
69    /// A single-frame decode helper was used for a multi-frame payload.
70    #[error("expected a single frame, found {actual_frames}")]
71    ExpectedSingleFrame {
72        /// Number of decoded frames in the source object.
73        actual_frames: usize,
74    },
75}
76
77/// Decodes every frame contained in a DICOM Part 10 payload.
78pub fn decode_dicom(bytes: &[u8]) -> Result<Vec<DecodedFrame>, DicomDecodeError> {
79    let file = DicomReader::new(Cursor::new(bytes)).read_file()?;
80    decode_file(file)
81}
82
83/// Decodes a DICOM payload and requires that it contain exactly one frame.
84pub fn decode_dicom_frame(bytes: &[u8]) -> Result<DecodedFrame, DicomDecodeError> {
85    let mut frames = decode_dicom(bytes)?;
86    if frames.len() == 1 {
87        Ok(frames.remove(0))
88    } else {
89        Err(DicomDecodeError::ExpectedSingleFrame {
90            actual_frames: frames.len(),
91        })
92    }
93}
94
95fn decode_file(file: FileFormat) -> Result<Vec<DecodedFrame>, DicomDecodeError> {
96    let base_metadata =
97        extract_frame_metadata(&file.dataset, file.meta.transfer_syntax_uid.clone(), 0)?;
98    if base_metadata.samples_per_pixel != 1 {
99        return Err(DicomDecodeError::UnsupportedSamplesPerPixel {
100            samples_per_pixel: base_metadata.samples_per_pixel,
101        });
102    }
103
104    let raw_frames = extract_raw_frames(
105        &file.dataset,
106        &base_metadata,
107        &file.meta.transfer_syntax_uid,
108    )?;
109    let mut decoded_frames = Vec::with_capacity(raw_frames.len());
110    for (frame_index, raw_frame) in raw_frames.iter().enumerate() {
111        let mut metadata = extract_frame_metadata(
112            &file.dataset,
113            file.meta.transfer_syntax_uid.clone(),
114            frame_index as u32,
115        )?;
116        metadata.frame_index = frame_index as u32;
117        let pixels = decode_modality_voxels(
118            &metadata,
119            raw_frame,
120            metadata.rows as usize * metadata.columns as usize,
121        )?;
122        decoded_frames.push(DecodedFrame { metadata, pixels });
123    }
124    Ok(decoded_frames)
125}
126
127fn extract_raw_frames(
128    dataset: &DataSet,
129    metadata: &FrameMetadata,
130    transfer_syntax_uid: &str,
131) -> Result<Vec<Vec<u8>>, DicomDecodeError> {
132    let bytes_per_sample = (metadata.bits_allocated as usize).div_ceil(8);
133    let bytes_per_frame = (metadata.rows as usize)
134        * (metadata.columns as usize)
135        * (metadata.samples_per_pixel as usize)
136        * bytes_per_sample;
137    let number_of_frames = metadata.number_of_frames.max(1);
138
139    let pixel_data = dataset
140        .find_element(tags::PIXEL_DATA)
141        .map_err(|_| DicomDecodeError::MissingPixelData)?;
142
143    match &pixel_data.value {
144        Value::PixelData(PixelData::Native { bytes }) | Value::U8(bytes) => {
145            if bytes.len() != bytes_per_frame * number_of_frames as usize {
146                return Err(DicomDecodeError::NativePixelLengthMismatch {
147                    actual: bytes.len(),
148                    frames: number_of_frames,
149                    bytes_per_frame,
150                });
151            }
152            Ok(bytes
153                .chunks_exact(bytes_per_frame)
154                .map(|chunk| chunk.to_vec())
155                .collect())
156        }
157        Value::PixelData(pixel_data @ PixelData::Encapsulated { .. }) => pixel_data
158            .encapsulated_frames(number_of_frames)?
159            .into_iter()
160            .map(|compressed| {
161                decode_pixel_data(
162                    transfer_syntax_uid,
163                    &compressed,
164                    metadata.rows,
165                    metadata.columns,
166                    metadata.bits_allocated,
167                    metadata.samples_per_pixel,
168                )
169                .map_err(DicomDecodeError::from)
170            })
171            .collect(),
172        _ => Err(DicomDecodeError::MissingPixelData),
173    }
174}
175
176fn decode_modality_voxels(
177    metadata: &FrameMetadata,
178    pixel_data: &[u8],
179    expected_len: usize,
180) -> Result<Vec<i16>, DicomDecodeError> {
181    let modality_lut = ModalityLut::new(metadata.rescale_intercept, metadata.rescale_slope);
182
183    let values = match (metadata.bits_allocated, metadata.pixel_representation) {
184        (8, _) => modality_lut.apply_to_frame_u8(pixel_data),
185        (16, PixelRepresentation::Unsigned) => {
186            let pixels = pixel::decode_u16_le(pixel_data);
187            let pixels = pixel::mask_u16(&pixels, metadata.bits_stored, metadata.high_bit);
188            modality_lut.apply_to_frame_u16(&pixels)
189        }
190        (16, PixelRepresentation::Signed) => {
191            let pixels = pixel::decode_i16_le(pixel_data);
192            let pixels = pixel::mask_i16(&pixels, metadata.bits_stored, metadata.high_bit);
193            modality_lut.apply_to_frame_i16(&pixels)
194        }
195        (bits_allocated, _) => {
196            return Err(DicomDecodeError::UnsupportedBitsAllocated { bits_allocated });
197        }
198    };
199
200    if values.len() < expected_len {
201        return Err(DicomDecodeError::PixelCountMismatch {
202            frame_index: metadata.frame_index,
203            expected: expected_len,
204            actual: values.len(),
205        });
206    }
207
208    Ok(values
209        .into_iter()
210        .take(expected_len)
211        .map(|value| value.round().clamp(i16::MIN as f64, i16::MAX as f64) as i16)
212        .collect())
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use dicom_toolkit_data::{Element, PixelData, Value};
219    use dicom_toolkit_dict::Vr;
220
221    fn encode_dataset(ds: DataSet) -> Vec<u8> {
222        let ff = FileFormat::from_dataset("1.2.840.10008.5.1.4.1.1.2", "1.2.3", ds);
223        let mut buf = Vec::new();
224        dicom_toolkit_data::DicomWriter::new(&mut buf)
225            .write_file(&ff)
226            .expect("encode");
227        buf
228    }
229
230    fn single_frame_dataset() -> DataSet {
231        let mut ds = DataSet::new();
232        ds.set_u16(tags::ROWS, 2);
233        ds.set_u16(tags::COLUMNS, 2);
234        ds.set_u16(tags::SAMPLES_PER_PIXEL, 1);
235        ds.set_u16(tags::BITS_ALLOCATED, 16);
236        ds.set_u16(tags::BITS_STORED, 16);
237        ds.set_u16(tags::HIGH_BIT, 15);
238        ds.set_u16(tags::PIXEL_REPRESENTATION, 1);
239        ds.set_string(tags::PHOTOMETRIC_INTERPRETATION, Vr::CS, "MONOCHROME2");
240        ds.set_string(tags::IMAGE_POSITION_PATIENT, Vr::DS, "0\\0\\5");
241        ds.set_string(tags::IMAGE_ORIENTATION_PATIENT, Vr::DS, "1\\0\\0\\0\\1\\0");
242        ds.set_string(crate::metadata::PIXEL_SPACING, Vr::DS, "0.5\\0.5");
243        ds.set_f64(tags::RESCALE_INTERCEPT, -1024.0);
244        ds.set_f64(tags::RESCALE_SLOPE, 1.0);
245        ds.insert(Element::new(
246            tags::PIXEL_DATA,
247            Vr::OW,
248            Value::PixelData(PixelData::Native {
249                bytes: bytemuck::cast_slice(&[100i16, 200, 300, 400]).to_vec(),
250            }),
251        ));
252        ds
253    }
254
255    #[test]
256    fn decodes_single_frame_native_pixels() {
257        let frames = decode_dicom(&encode_dataset(single_frame_dataset())).expect("decode");
258        assert_eq!(frames.len(), 1);
259        assert_eq!(frames[0].pixels, vec![-924, -824, -724, -624]);
260        assert_eq!(
261            frames[0].metadata.image_position,
262            Some(glam::DVec3::new(0.0, 0.0, 5.0))
263        );
264    }
265
266    #[test]
267    fn rejects_rgb_images_for_volume_decode() {
268        let mut ds = single_frame_dataset();
269        ds.set_u16(tags::SAMPLES_PER_PIXEL, 3);
270        ds.insert(Element::new(
271            tags::PIXEL_DATA,
272            Vr::OB,
273            Value::PixelData(PixelData::Native {
274                bytes: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
275            }),
276        ));
277
278        let err = decode_dicom(&encode_dataset(ds)).unwrap_err();
279        assert!(matches!(
280            err,
281            DicomDecodeError::UnsupportedSamplesPerPixel {
282                samples_per_pixel: 3
283            }
284        ));
285    }
286}