1use crate::dicom_decode::DecodedFrame;
4use crate::metadata::{FrameMetadata, VolumeGeometry};
5use glam::{DMat3, DVec3, UVec3};
6use std::cmp::Ordering;
7use thiserror::Error;
8use volren_core::{Volume, VolumeError};
9
10#[derive(Debug, Error)]
12pub enum VolumeAssemblyError {
13 #[error("no decoded frames were provided")]
15 EmptyFrames,
16 #[error("frame {frame_index} has inconsistent geometry: {reason}")]
18 InconsistentGeometry {
19 frame_index: usize,
21 reason: String,
23 },
24 #[error("frame {frame_index} has {actual} voxels, expected {expected}")]
26 PixelCountMismatch {
27 frame_index: usize,
29 expected: usize,
31 actual: usize,
33 },
34 #[error(transparent)]
36 Volume(#[from] VolumeError),
37}
38
39pub fn assemble_volume_from_frames(
41 frames: &[DecodedFrame],
42) -> Result<Volume<i16>, VolumeAssemblyError> {
43 let geometry = derive_volume_geometry_from_frames(frames)?;
44 let sorted_frames = sort_frames_for_volume(frames);
45 let slice_len = geometry.slice_len();
46 let mut slices = Vec::with_capacity(sorted_frames.len());
47
48 for (index, frame) in sorted_frames.iter().enumerate() {
49 if frame.pixels.len() != slice_len {
50 return Err(VolumeAssemblyError::PixelCountMismatch {
51 frame_index: index,
52 expected: slice_len,
53 actual: frame.pixels.len(),
54 });
55 }
56 slices.push(frame.pixels.as_slice());
57 }
58
59 Volume::from_slices(
60 &slices,
61 geometry.dimensions.x,
62 geometry.dimensions.y,
63 geometry.spacing,
64 geometry.origin,
65 geometry.direction,
66 )
67 .map_err(VolumeAssemblyError::from)
68}
69
70pub fn derive_volume_geometry_from_frames(
72 frames: &[DecodedFrame],
73) -> Result<VolumeGeometry, VolumeAssemblyError> {
74 let first = frames.first().ok_or(VolumeAssemblyError::EmptyFrames)?;
75 validate_frame_set(frames, &first.metadata)?;
76 let sorted_frames = sort_frames_for_volume(frames);
77 let spacing = extract_spacing(&sorted_frames);
78 let origin = sorted_frames
79 .first()
80 .and_then(|frame| frame.metadata.image_position)
81 .unwrap_or(DVec3::ZERO);
82 let direction = sorted_frames
83 .first()
84 .map(|frame| frame.metadata.direction())
85 .unwrap_or(DMat3::IDENTITY);
86
87 Ok(VolumeGeometry {
88 dimensions: UVec3::new(
89 first.metadata.columns as u32,
90 first.metadata.rows as u32,
91 sorted_frames.len() as u32,
92 ),
93 spacing,
94 origin,
95 direction,
96 })
97}
98
99fn validate_frame_set(
100 frames: &[DecodedFrame],
101 first: &FrameMetadata,
102) -> Result<(), VolumeAssemblyError> {
103 for (index, frame) in frames.iter().enumerate() {
104 if frame.metadata.rows != first.rows || frame.metadata.columns != first.columns {
105 return Err(VolumeAssemblyError::InconsistentGeometry {
106 frame_index: index,
107 reason: format!(
108 "expected {}x{}, got {}x{}",
109 first.columns, first.rows, frame.metadata.columns, frame.metadata.rows
110 ),
111 });
112 }
113 if frame.metadata.samples_per_pixel != first.samples_per_pixel {
114 return Err(VolumeAssemblyError::InconsistentGeometry {
115 frame_index: index,
116 reason: format!(
117 "expected samples_per_pixel={}, got {}",
118 first.samples_per_pixel, frame.metadata.samples_per_pixel
119 ),
120 });
121 }
122 if frame.metadata.image_orientation != first.image_orientation
123 && frame.metadata.image_orientation.is_some()
124 && first.image_orientation.is_some()
125 {
126 return Err(VolumeAssemblyError::InconsistentGeometry {
127 frame_index: index,
128 reason: "image orientation differs across frames".to_string(),
129 });
130 }
131 }
132 Ok(())
133}
134
135fn sort_frames_for_volume<'a>(frames: &'a [DecodedFrame]) -> Vec<&'a DecodedFrame> {
136 let mut sorted: Vec<&DecodedFrame> = frames.iter().collect();
137 if sorted.len() <= 1 {
138 return sorted;
139 }
140
141 let Some(reference_normal) = reference_slice_normal(&sorted) else {
142 sorted.sort_by(|left, right| compare_frames_fallback(left, right));
143 return sorted;
144 };
145
146 let reference_origin = sorted
147 .iter()
148 .find_map(|frame| frame.metadata.image_position)
149 .unwrap_or(DVec3::ZERO);
150
151 sorted.sort_by(|left, right| {
152 compare_frames_by_geometry(left, right, reference_origin, reference_normal)
153 .then_with(|| compare_frames_fallback(left, right))
154 });
155 sorted
156}
157
158fn reference_slice_normal(frames: &[&DecodedFrame]) -> Option<DVec3> {
159 let candidate = frames
160 .iter()
161 .find_map(|frame| frame.metadata.slice_normal())?;
162 frames
163 .iter()
164 .all(|frame| {
165 let Some(normal) = frame.metadata.slice_normal() else {
166 return false;
167 };
168 normal.dot(candidate).abs() > 0.999
169 })
170 .then_some(candidate)
171}
172
173fn compare_frames_by_geometry(
174 left: &DecodedFrame,
175 right: &DecodedFrame,
176 reference_origin: DVec3,
177 reference_normal: DVec3,
178) -> Ordering {
179 match (left.metadata.image_position, right.metadata.image_position) {
180 (Some(left_pos), Some(right_pos)) => {
181 let left_distance = reference_normal.dot(left_pos - reference_origin);
182 let right_distance = reference_normal.dot(right_pos - reference_origin);
183 left_distance
184 .partial_cmp(&right_distance)
185 .unwrap_or(Ordering::Equal)
186 }
187 _ => compare_frames_fallback(left, right),
188 }
189}
190
191fn compare_frames_fallback(left: &DecodedFrame, right: &DecodedFrame) -> Ordering {
192 left.metadata
193 .instance_number
194 .cmp(&right.metadata.instance_number)
195 .then_with(|| {
196 left.metadata
197 .sop_instance_uid
198 .as_deref()
199 .unwrap_or("")
200 .cmp(right.metadata.sop_instance_uid.as_deref().unwrap_or(""))
201 })
202 .then_with(|| left.metadata.frame_index.cmp(&right.metadata.frame_index))
203}
204
205fn extract_spacing(frames: &[&DecodedFrame]) -> DVec3 {
206 let first = &frames[0].metadata;
207 let pixel_spacing = first.pixel_spacing.unwrap_or((1.0, 1.0));
208 let slice_spacing = if frames.len() >= 2 {
209 projected_slice_spacing(frames)
210 .or(first.slice_thickness)
211 .unwrap_or(1.0)
212 } else {
213 first.slice_thickness.unwrap_or(1.0)
214 };
215
216 DVec3::new(pixel_spacing.1, pixel_spacing.0, slice_spacing)
217}
218
219fn projected_slice_spacing(frames: &[&DecodedFrame]) -> Option<f64> {
220 let normal = reference_slice_normal(frames)?;
221 let first = frames.first()?.metadata.image_position?;
222 let second = frames.get(1)?.metadata.image_position?;
223 Some(normal.dot(second - first).abs())
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::metadata::FrameMetadata;
230 use dicom_toolkit_image::PixelRepresentation;
231 use volren_core::VolumeInfo;
232
233 fn frame(
234 instance_number: i32,
235 z: f64,
236 pixels: [i16; 4],
237 orientation: Option<(DVec3, DVec3)>,
238 ) -> DecodedFrame {
239 DecodedFrame {
240 metadata: FrameMetadata {
241 frame_index: 0,
242 rows: 2,
243 columns: 2,
244 number_of_frames: 1,
245 samples_per_pixel: 1,
246 bits_allocated: 16,
247 bits_stored: 16,
248 high_bit: 15,
249 pixel_representation: PixelRepresentation::Signed,
250 instance_number,
251 pixel_spacing: Some((0.6, 0.8)),
252 slice_thickness: Some(1.5),
253 image_position: Some(DVec3::new(0.0, 0.0, z)),
254 image_orientation: orientation,
255 window_center: None,
256 window_width: None,
257 rescale_intercept: 0.0,
258 rescale_slope: 1.0,
259 sop_instance_uid: Some(format!("1.2.3.{instance_number}")),
260 transfer_syntax_uid: "1.2.840.10008.1.2.1".to_string(),
261 },
262 pixels: pixels.to_vec(),
263 }
264 }
265
266 #[test]
267 fn assembles_sorted_volume() {
268 let orientation = Some((DVec3::X, DVec3::Y));
269 let frames = vec![
270 frame(2, 2.0, [5, 6, 7, 8], orientation),
271 frame(1, 1.0, [1, 2, 3, 4], orientation),
272 frame(3, 3.0, [9, 10, 11, 12], orientation),
273 ];
274
275 let volume = assemble_volume_from_frames(&frames).expect("volume");
276 assert_eq!(volume.dimensions(), UVec3::new(2, 2, 3));
277 assert_eq!(volume.spacing(), DVec3::new(0.8, 0.6, 1.0));
278 assert_eq!(volume.get(0, 0, 0), Some(1));
279 assert_eq!(volume.get(1, 1, 2), Some(12));
280 }
281
282 #[test]
283 fn falls_back_to_instance_number_sorting() {
284 let mut frames = vec![
285 frame(3, 0.0, [9, 10, 11, 12], None),
286 frame(1, 0.0, [1, 2, 3, 4], None),
287 frame(2, 0.0, [5, 6, 7, 8], None),
288 ];
289 for decoded in &mut frames {
290 decoded.metadata.image_position = None;
291 }
292
293 let volume = assemble_volume_from_frames(&frames).expect("volume");
294 assert_eq!(volume.get(0, 0, 0), Some(1));
295 assert_eq!(volume.get(0, 0, 1), Some(5));
296 assert_eq!(volume.get(0, 0, 2), Some(9));
297 }
298
299 #[test]
300 fn rejects_inconsistent_geometry() {
301 let orientation = Some((DVec3::X, DVec3::Y));
302 let mut frames = vec![
303 frame(1, 1.0, [1, 2, 3, 4], orientation),
304 frame(2, 2.0, [5, 6, 7, 8], orientation),
305 ];
306 frames[1].metadata.columns = 3;
307
308 let err = assemble_volume_from_frames(&frames).unwrap_err();
309 assert!(matches!(
310 err,
311 VolumeAssemblyError::InconsistentGeometry { .. }
312 ));
313 }
314}