Skip to main content

dicomview_core/
incremental_volume.rs

1//! Progressive volume construction from arriving slices.
2
3use crate::metadata::VolumeGeometry;
4use glam::{DMat3, DVec3, UVec3};
5use thiserror::Error;
6use volren_core::{DynVolume, Volume, VolumeError, VolumeInfo};
7
8/// Errors raised while creating or updating an [`IncrementalVolume`].
9#[derive(Debug, Error, PartialEq)]
10pub enum IncrementalVolumeError {
11    /// The provided geometry had zero depth or an invalid slice shape.
12    #[error("invalid geometry dimensions: {dimensions:?}")]
13    InvalidGeometry {
14        /// Dimensions that were rejected.
15        dimensions: UVec3,
16    },
17    /// The provided slice length did not match the preallocated geometry.
18    #[error("slice {z_index} has {actual} voxels, expected {expected}")]
19    SliceLengthMismatch {
20        /// Z index that failed validation.
21        z_index: u32,
22        /// Expected number of voxels in the slice.
23        expected: usize,
24        /// Actual number of voxels supplied.
25        actual: usize,
26    },
27    /// The requested slice index is outside the preallocated depth.
28    #[error("slice index {z_index} is out of bounds for depth {depth}")]
29    SliceOutOfBounds {
30        /// Invalid slice index.
31        z_index: u32,
32        /// Preallocated volume depth.
33        depth: u32,
34    },
35    /// The internal volume buffer could not be materialized.
36    #[error(transparent)]
37    Volume(#[from] VolumeError),
38}
39
40/// A volume that can be filled one slice at a time as DICOM frames arrive.
41#[derive(Debug, Clone)]
42pub struct IncrementalVolume {
43    geometry: VolumeGeometry,
44    voxels: Vec<i16>,
45    loaded_slices: Vec<bool>,
46    loaded_count: usize,
47    scalar_range: Option<(i16, i16)>,
48}
49
50impl IncrementalVolume {
51    /// Creates an empty preallocated volume with the provided geometry.
52    pub fn new(geometry: VolumeGeometry) -> Result<Self, IncrementalVolumeError> {
53        if geometry.dimensions.x == 0 || geometry.dimensions.y == 0 || geometry.dimensions.z == 0 {
54            return Err(IncrementalVolumeError::InvalidGeometry {
55                dimensions: geometry.dimensions,
56            });
57        }
58
59        Ok(Self {
60            geometry,
61            voxels: vec![0; geometry.voxel_count()],
62            loaded_slices: vec![false; geometry.dimensions.z as usize],
63            loaded_count: 0,
64            scalar_range: None,
65        })
66    }
67
68    /// Inserts or replaces one slice at `z_index`.
69    pub fn insert_slice(
70        &mut self,
71        z_index: u32,
72        pixels: &[i16],
73    ) -> Result<(), IncrementalVolumeError> {
74        if z_index >= self.geometry.dimensions.z {
75            return Err(IncrementalVolumeError::SliceOutOfBounds {
76                z_index,
77                depth: self.geometry.dimensions.z,
78            });
79        }
80
81        let expected = self.geometry.slice_len();
82        if pixels.len() != expected {
83            return Err(IncrementalVolumeError::SliceLengthMismatch {
84                z_index,
85                expected,
86                actual: pixels.len(),
87            });
88        }
89
90        let start = z_index as usize * expected;
91        let end = start + expected;
92        self.voxels[start..end].copy_from_slice(pixels);
93
94        let was_loaded = std::mem::replace(&mut self.loaded_slices[z_index as usize], true);
95        if !was_loaded {
96            self.loaded_count += 1;
97        }
98        self.scalar_range = self.compute_scalar_range();
99        Ok(())
100    }
101
102    /// Returns the preallocated geometry for the volume.
103    #[must_use]
104    pub fn geometry(&self) -> VolumeGeometry {
105        self.geometry
106    }
107
108    /// Returns `true` when every slice has been inserted.
109    #[must_use]
110    pub fn is_complete(&self) -> bool {
111        self.loaded_count == self.loaded_slices.len()
112    }
113
114    /// Returns the number of inserted slices.
115    #[must_use]
116    pub fn loaded_count(&self) -> usize {
117        self.loaded_count
118    }
119
120    /// Returns the per-slice loaded mask.
121    #[must_use]
122    pub fn loaded_mask(&self) -> &[bool] {
123        &self.loaded_slices
124    }
125
126    /// Returns the loading progress in the range `[0.0, 1.0]`.
127    #[must_use]
128    pub fn loading_progress(&self) -> f64 {
129        self.loaded_count as f64 / self.loaded_slices.len() as f64
130    }
131
132    /// Returns the scalar range across the loaded slices.
133    #[must_use]
134    pub fn scalar_range(&self) -> Option<(i16, i16)> {
135        self.scalar_range
136    }
137
138    /// Materializes the currently loaded voxels as a typed `Volume<i16>`.
139    pub fn as_volume(&self) -> Result<Volume<i16>, IncrementalVolumeError> {
140        Ok(Volume::from_data(
141            self.voxels.clone(),
142            self.geometry.dimensions,
143            self.geometry.spacing,
144            self.geometry.origin,
145            self.geometry.direction,
146            1,
147        )?)
148    }
149
150    /// Materializes the currently loaded voxels as a type-erased `DynVolume`.
151    pub fn as_dyn_volume(&self) -> Result<DynVolume, IncrementalVolumeError> {
152        Ok(self.as_volume()?.into())
153    }
154
155    fn compute_scalar_range(&self) -> Option<(i16, i16)> {
156        let slice_len = self.geometry.slice_len();
157        let mut min_value = i16::MAX;
158        let mut max_value = i16::MIN;
159        let mut seen = false;
160
161        for (z_index, loaded) in self.loaded_slices.iter().copied().enumerate() {
162            if !loaded {
163                continue;
164            }
165            for &value in &self.voxels[z_index * slice_len..(z_index + 1) * slice_len] {
166                min_value = min_value.min(value);
167                max_value = max_value.max(value);
168                seen = true;
169            }
170        }
171
172        seen.then_some((min_value, max_value))
173    }
174}
175
176impl From<Volume<i16>> for VolumeGeometry {
177    fn from(volume: Volume<i16>) -> Self {
178        Self {
179            dimensions: volume.dimensions(),
180            spacing: volume.spacing(),
181            origin: volume.origin(),
182            direction: volume.direction(),
183        }
184    }
185}
186
187impl VolumeGeometry {
188    /// Creates geometry from raw volume components.
189    #[must_use]
190    pub fn new(dimensions: UVec3, spacing: DVec3, origin: DVec3, direction: DMat3) -> Self {
191        Self {
192            dimensions,
193            spacing,
194            origin,
195            direction,
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    fn geometry() -> VolumeGeometry {
205        VolumeGeometry::new(
206            UVec3::new(2, 2, 3),
207            DVec3::ONE,
208            DVec3::ZERO,
209            DMat3::IDENTITY,
210        )
211    }
212
213    #[test]
214    fn inserts_slices_in_any_order() {
215        let mut volume = IncrementalVolume::new(geometry()).unwrap();
216        volume.insert_slice(2, &[9, 10, 11, 12]).unwrap();
217        volume.insert_slice(0, &[1, 2, 3, 4]).unwrap();
218        volume.insert_slice(1, &[5, 6, 7, 8]).unwrap();
219
220        let typed = volume.as_volume().unwrap();
221        assert_eq!(typed.get(0, 0, 0), Some(1));
222        assert_eq!(typed.get(1, 1, 1), Some(8));
223        assert_eq!(typed.get(0, 0, 2), Some(9));
224        assert!(volume.is_complete());
225        assert_eq!(volume.scalar_range(), Some((1, 12)));
226    }
227
228    #[test]
229    fn duplicate_insert_recomputes_scalar_range_without_double_counting() {
230        let mut volume = IncrementalVolume::new(geometry()).unwrap();
231        volume.insert_slice(0, &[1, 2, 3, 4]).unwrap();
232        volume.insert_slice(0, &[10, 20, 30, 40]).unwrap();
233
234        assert_eq!(volume.loaded_count(), 1);
235        assert_eq!(volume.scalar_range(), Some((10, 40)));
236    }
237
238    #[test]
239    fn rejects_bad_slice_length() {
240        let err = IncrementalVolume::new(geometry())
241            .unwrap()
242            .insert_slice(0, &[1, 2, 3])
243            .unwrap_err();
244        assert!(matches!(
245            err,
246            IncrementalVolumeError::SliceLengthMismatch {
247                expected: 4,
248                actual: 3,
249                ..
250            }
251        ));
252    }
253
254    #[test]
255    fn rejects_out_of_bounds_z() {
256        let err = IncrementalVolume::new(geometry())
257            .unwrap()
258            .insert_slice(3, &[1, 2, 3, 4])
259            .unwrap_err();
260        assert!(matches!(
261            err,
262            IncrementalVolumeError::SliceOutOfBounds {
263                z_index: 3,
264                depth: 3
265            }
266        ));
267    }
268}