1use 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#[derive(Debug, Clone, PartialEq)]
14pub struct DecodedFrame {
15 pub metadata: FrameMetadata,
17 pub pixels: Vec<i16>,
19}
20
21#[derive(Debug, Error)]
23pub enum DicomDecodeError {
24 #[error(transparent)]
26 Dicom(#[from] DcmError),
27 #[error(transparent)]
29 Metadata(#[from] MetadataError),
30 #[error(
32 "unsupported samples per pixel {samples_per_pixel}; only grayscale images are supported"
33 )]
34 UnsupportedSamplesPerPixel {
35 samples_per_pixel: u16,
37 },
38 #[error("unsupported BitsAllocated={bits_allocated} for volumetric decoding")]
40 UnsupportedBitsAllocated {
41 bits_allocated: u16,
43 },
44 #[error(
46 "pixel data length {actual} does not contain {frames} frame(s) of {bytes_per_frame} bytes"
47 )]
48 NativePixelLengthMismatch {
49 actual: usize,
51 frames: u32,
53 bytes_per_frame: usize,
55 },
56 #[error("missing pixel data element")]
58 MissingPixelData,
59 #[error("decoded frame {frame_index} has {actual} pixels, expected {expected}")]
61 PixelCountMismatch {
62 frame_index: u32,
64 expected: usize,
66 actual: usize,
68 },
69 #[error("expected a single frame, found {actual_frames}")]
71 ExpectedSingleFrame {
72 actual_frames: usize,
74 },
75}
76
77pub 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
83pub 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}