1use dicom_toolkit_data::{DataSet, Value};
4use dicom_toolkit_dict::{tags, Tag};
5use dicom_toolkit_image::PixelRepresentation;
6use glam::{DMat3, DVec3, UVec3};
7use thiserror::Error;
8
9pub const PIXEL_SPACING: Tag = Tag::new(0x0028, 0x0030);
11
12pub const SLICE_THICKNESS: Tag = Tag::new(0x0018, 0x0050);
14
15#[derive(Debug, Error, PartialEq)]
17pub enum MetadataError {
18 #[error("missing mandatory attribute: {name}")]
20 MissingAttribute {
21 name: &'static str,
23 },
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct VolumeGeometry {
29 pub dimensions: UVec3,
31 pub spacing: DVec3,
33 pub origin: DVec3,
35 pub direction: DMat3,
37}
38
39impl VolumeGeometry {
40 #[must_use]
42 pub fn voxel_count(self) -> usize {
43 (self.dimensions.x as usize) * (self.dimensions.y as usize) * (self.dimensions.z as usize)
44 }
45
46 #[must_use]
48 pub fn slice_len(self) -> usize {
49 (self.dimensions.x as usize) * (self.dimensions.y as usize)
50 }
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub struct FrameMetadata {
56 pub frame_index: u32,
58 pub rows: u16,
60 pub columns: u16,
62 pub number_of_frames: u32,
64 pub samples_per_pixel: u16,
66 pub bits_allocated: u16,
68 pub bits_stored: u16,
70 pub high_bit: u16,
72 pub pixel_representation: PixelRepresentation,
74 pub instance_number: i32,
76 pub pixel_spacing: Option<(f64, f64)>,
78 pub slice_thickness: Option<f64>,
80 pub image_position: Option<DVec3>,
82 pub image_orientation: Option<(DVec3, DVec3)>,
84 pub window_center: Option<f64>,
86 pub window_width: Option<f64>,
88 pub rescale_intercept: f64,
90 pub rescale_slope: f64,
92 pub sop_instance_uid: Option<String>,
94 pub transfer_syntax_uid: String,
96}
97
98impl FrameMetadata {
99 #[must_use]
101 pub fn direction(&self) -> DMat3 {
102 self.image_orientation
103 .map(|(row, col)| {
104 let normal = row.cross(col).normalize_or_zero();
105 if normal.length_squared() > 0.0 {
106 DMat3::from_cols(row, col, normal)
107 } else {
108 DMat3::IDENTITY
109 }
110 })
111 .unwrap_or(DMat3::IDENTITY)
112 }
113
114 #[must_use]
116 pub fn slice_normal(&self) -> Option<DVec3> {
117 self.image_orientation.and_then(|(row, col)| {
118 let normal = row.cross(col).normalize_or_zero();
119 (normal.length_squared() > 0.0).then_some(normal)
120 })
121 }
122
123 #[must_use]
125 pub fn spacing_xy(&self) -> DVec3 {
126 let (row, col) = self.pixel_spacing.unwrap_or((1.0, 1.0));
127 DVec3::new(col, row, self.slice_thickness.unwrap_or(1.0))
128 }
129
130 #[must_use]
132 pub fn voxel_count(&self) -> usize {
133 (self.rows as usize) * (self.columns as usize) * (self.samples_per_pixel as usize)
134 }
135}
136
137pub fn extract_frame_metadata(
139 dataset: &DataSet,
140 transfer_syntax_uid: impl Into<String>,
141 frame_index: u32,
142) -> Result<FrameMetadata, MetadataError> {
143 let rows = dataset
144 .get_u16(tags::ROWS)
145 .ok_or(MetadataError::MissingAttribute {
146 name: "Rows (0028,0010)",
147 })?;
148 let columns = dataset
149 .get_u16(tags::COLUMNS)
150 .ok_or(MetadataError::MissingAttribute {
151 name: "Columns (0028,0011)",
152 })?;
153 let bits_allocated =
154 dataset
155 .get_u16(tags::BITS_ALLOCATED)
156 .ok_or(MetadataError::MissingAttribute {
157 name: "BitsAllocated (0028,0100)",
158 })?;
159 let samples_per_pixel = dataset.get_u16(tags::SAMPLES_PER_PIXEL).unwrap_or(1);
160 let bits_stored = dataset.get_u16(tags::BITS_STORED).unwrap_or(bits_allocated);
161 let high_bit = dataset
162 .get_u16(tags::HIGH_BIT)
163 .unwrap_or(bits_stored.saturating_sub(1));
164 let pixel_representation = match dataset.get_u16(tags::PIXEL_REPRESENTATION).unwrap_or(0) {
165 1 => PixelRepresentation::Signed,
166 _ => PixelRepresentation::Unsigned,
167 };
168
169 Ok(FrameMetadata {
170 frame_index,
171 rows,
172 columns,
173 number_of_frames: number_of_frames(dataset),
174 samples_per_pixel,
175 bits_allocated,
176 bits_stored,
177 high_bit,
178 pixel_representation,
179 instance_number: dataset.get_i32(tags::INSTANCE_NUMBER).unwrap_or(0),
180 pixel_spacing: decimal_pair(dataset, PIXEL_SPACING),
181 slice_thickness: dataset
182 .get_f64(SLICE_THICKNESS)
183 .or_else(|| dataset.get_string(SLICE_THICKNESS).and_then(parse_decimal)),
184 image_position: decimal_vector3(dataset, tags::IMAGE_POSITION_PATIENT),
185 image_orientation: orientation_pair(dataset, tags::IMAGE_ORIENTATION_PATIENT),
186 window_center: decimal_value(dataset, tags::WINDOW_CENTER),
187 window_width: decimal_value(dataset, tags::WINDOW_WIDTH),
188 rescale_intercept: decimal_value(dataset, tags::RESCALE_INTERCEPT).unwrap_or(0.0),
189 rescale_slope: decimal_value(dataset, tags::RESCALE_SLOPE).unwrap_or(1.0),
190 sop_instance_uid: dataset
191 .get_string(tags::SOP_INSTANCE_UID)
192 .map(std::string::ToString::to_string),
193 transfer_syntax_uid: transfer_syntax_uid.into(),
194 })
195}
196
197fn number_of_frames(dataset: &DataSet) -> u32 {
198 dataset
199 .get(tags::NUMBER_OF_FRAMES)
200 .and_then(|elem| match &elem.value {
201 dicom_toolkit_data::Value::Ints(values) => {
202 values.first().copied().map(|n| n.max(1) as u32)
203 }
204 dicom_toolkit_data::Value::Strings(values) => values
205 .first()
206 .and_then(|value| value.trim().parse::<u32>().ok()),
207 dicom_toolkit_data::Value::U16(values) => values.first().copied().map(u32::from),
208 dicom_toolkit_data::Value::U32(values) => values.first().copied(),
209 _ => None,
210 })
211 .unwrap_or(1)
212}
213
214fn decimal_value(dataset: &DataSet, tag: Tag) -> Option<f64> {
215 decimal_values(dataset, tag).into_iter().next()
216}
217
218fn parse_decimal(value: &str) -> Option<f64> {
219 value.trim().parse::<f64>().ok()
220}
221
222fn decimal_values(dataset: &DataSet, tag: Tag) -> Vec<f64> {
223 let Some(element) = dataset.get(tag) else {
224 return Vec::new();
225 };
226 match &element.value {
227 Value::Decimals(values) => values.clone(),
228 Value::F64(values) => values.clone(),
229 Value::F32(values) => values.iter().map(|&value| value as f64).collect(),
230 Value::Strings(values) => values
231 .iter()
232 .flat_map(|value| value.split('\\'))
233 .filter_map(parse_decimal)
234 .collect(),
235 Value::U16(values) => values.iter().map(|&value| value as f64).collect(),
236 Value::U32(values) => values.iter().map(|&value| value as f64).collect(),
237 Value::I32(values) => values.iter().map(|&value| value as f64).collect(),
238 _ => Vec::new(),
239 }
240}
241
242fn decimal_pair(dataset: &DataSet, tag: Tag) -> Option<(f64, f64)> {
243 let parts = decimal_values(dataset, tag);
244 (parts.len() >= 2).then(|| (parts[0], parts[1]))
245}
246
247fn decimal_vector3(dataset: &DataSet, tag: Tag) -> Option<DVec3> {
248 let parts = decimal_values(dataset, tag);
249 (parts.len() >= 3).then(|| DVec3::new(parts[0], parts[1], parts[2]))
250}
251
252fn orientation_pair(dataset: &DataSet, tag: Tag) -> Option<(DVec3, DVec3)> {
253 let parts = decimal_values(dataset, tag);
254 (parts.len() >= 6).then(|| {
255 (
256 DVec3::new(parts[0], parts[1], parts[2]).normalize_or_zero(),
257 DVec3::new(parts[3], parts[4], parts[5]).normalize_or_zero(),
258 )
259 })
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use dicom_toolkit_data::DataSet;
266 use dicom_toolkit_dict::Vr;
267
268 fn dataset_with_geometry() -> DataSet {
269 let mut ds = DataSet::new();
270 ds.set_u16(tags::ROWS, 4);
271 ds.set_u16(tags::COLUMNS, 3);
272 ds.set_u16(tags::SAMPLES_PER_PIXEL, 1);
273 ds.set_u16(tags::BITS_ALLOCATED, 16);
274 ds.set_u16(tags::BITS_STORED, 12);
275 ds.set_u16(tags::HIGH_BIT, 11);
276 ds.set_u16(tags::PIXEL_REPRESENTATION, 1);
277 ds.set_string(PIXEL_SPACING, Vr::DS, "0.7\\0.8");
278 ds.set_string(tags::IMAGE_POSITION_PATIENT, Vr::DS, "1\\2\\3");
279 ds.set_string(tags::IMAGE_ORIENTATION_PATIENT, Vr::DS, "1\\0\\0\\0\\1\\0");
280 ds.set_string(tags::WINDOW_CENTER, Vr::DS, "40");
281 ds.set_string(tags::WINDOW_WIDTH, Vr::DS, "400");
282 ds.set_string(tags::SOP_INSTANCE_UID, Vr::UI, "1.2.3");
283 ds
284 }
285
286 #[test]
287 fn extracts_frame_metadata() {
288 let metadata = extract_frame_metadata(&dataset_with_geometry(), "1.2.840.10008.1.2.1", 0)
289 .expect("metadata");
290 assert_eq!(metadata.rows, 4);
291 assert_eq!(metadata.columns, 3);
292 assert_eq!(metadata.bits_allocated, 16);
293 assert_eq!(metadata.spacing_xy(), DVec3::new(0.8, 0.7, 1.0));
294 assert_eq!(metadata.image_position, Some(DVec3::new(1.0, 2.0, 3.0)));
295 assert_eq!(metadata.sop_instance_uid.as_deref(), Some("1.2.3"));
296 }
297
298 #[test]
299 fn direction_defaults_to_identity_without_orientation() {
300 let mut metadata =
301 extract_frame_metadata(&dataset_with_geometry(), "1.2.840.10008.1.2.1", 0).unwrap();
302 metadata.image_orientation = None;
303 assert_eq!(metadata.direction(), DMat3::IDENTITY);
304 assert_eq!(metadata.slice_normal(), None);
305 }
306}