wsi_dicom_annotations/annotations/parametric_map/
document.rs1use std::path::Path;
2
3use sha2::{Digest, Sha256};
4
5use crate::{
6 DerivedObjectProducer, DicomAnnotationContext, Error, InteroperabilityDiagnostic, Result,
7};
8
9use super::super::semantic_digest::{finish, update_algorithm, update_code, update_text};
10use super::frame::{encode_frame, FrameKey, PixelPrecision};
11use super::geometry::RasterGeometry;
12use super::profile::{RasterDType, RasterOutputPrecision};
13use super::{NormalizedRaster, RasterChannelSelection, RasterDescriptor, RasterProfile};
14
15pub struct ParametricMapDocument {
16 pub(super) source: DicomAnnotationContext,
17 pub(super) profile: RasterProfile,
18 pub(super) producer: DerivedObjectProducer,
19 pub(super) raster: NormalizedRaster,
20 pub(super) selected_channels: Vec<usize>,
21 pub(super) descriptor: RasterDescriptor,
22 pub(super) geometry: RasterGeometry,
23 pub(super) precision: PixelPrecision,
24 pub(super) frame_count: u32,
25 pub(super) dense: bool,
26 pub(super) channel_ranges: Vec<ValueRange>,
27 pub(super) pixel_digest: [u8; 32],
28 semantic_digest: String,
29 diagnostics: Vec<InteroperabilityDiagnostic>,
30}
31
32#[derive(Debug, Clone, Copy)]
33pub(super) struct ValueRange {
34 pub(super) minimum: f64,
35 pub(super) maximum: f64,
36}
37
38impl ParametricMapDocument {
39 pub fn open(
40 source: DicomAnnotationContext,
41 canonical_source: DicomAnnotationContext,
42 profile: RasterProfile,
43 raster_path: impl AsRef<Path>,
44 channel_selection: RasterChannelSelection,
45 ) -> Result<Self> {
46 let selected_channels = profile.select_channels(channel_selection)?;
47 let raster_path = raster_path.as_ref().to_path_buf();
48 let raster = NormalizedRaster::open(&profile, &raster_path)?;
49 let descriptor = raster.descriptor();
50 validate_descriptor(descriptor)?;
51 let precision = output_precision(&profile);
52 let geometry = RasterGeometry::resolve(&source, &canonical_source, &profile, descriptor)?;
53 let mut document = Self {
54 source,
55 profile,
56 producer: DerivedObjectProducer::library_default(9401, "WSI parametric maps"),
57 raster,
58 selected_channels,
59 descriptor,
60 geometry,
61 precision,
62 frame_count: 0,
63 dense: false,
64 channel_ranges: Vec::new(),
65 pixel_digest: [0; 32],
66 semantic_digest: String::new(),
67 diagnostics: Vec::new(),
68 };
69 document.scan()?;
70 Ok(document)
71 }
72
73 #[must_use]
75 pub fn with_producer(mut self, producer: DerivedObjectProducer) -> Self {
76 self.producer = producer;
77 self
78 }
79
80 #[must_use]
81 pub fn producer(&self) -> &DerivedObjectProducer {
82 &self.producer
83 }
84
85 #[must_use]
86 pub fn frame_count(&self) -> u32 {
87 self.frame_count
88 }
89
90 #[must_use]
91 pub fn semantic_digest(&self) -> &str {
92 &self.semantic_digest
93 }
94
95 #[must_use]
96 pub fn selected_channel_count(&self) -> usize {
97 self.selected_channels.len()
98 }
99
100 #[must_use]
101 pub fn diagnostics(&self) -> &[InteroperabilityDiagnostic] {
102 &self.diagnostics
103 }
104
105 fn scan(&mut self) -> Result<()> {
106 let mut digest = Sha256::new();
107 let mut ranges = vec![MutableRange::default(); self.selected_channels.len()];
108 let mut frame_count = 0_u64;
109 let mut missing_frame_count = 0_u64;
110 let mut missing_sample_count = 0_u64;
111 let mut padded_sample_count = 0_u64;
112 let tiles_across = self.descriptor.width.div_ceil(self.descriptor.tile_width);
113 let tiles_down = self.descriptor.height.div_ceil(self.descriptor.tile_height);
114 for (channel_ordinal, channel) in self.selected_channels.iter().copied().enumerate() {
115 let channel_ordinal = u32::try_from(channel_ordinal + 1)
116 .map_err(|_| Error::InvalidInput("raster channel count exceeds DICOM UL".into()))?;
117 for tile_row in 0..tiles_down {
118 for tile_column in 0..tiles_across {
119 let key = FrameKey {
120 channel,
121 channel_ordinal,
122 tile_row,
123 tile_column,
124 };
125 let frame = encode_frame(&self.raster, self.descriptor, key)?;
126 validate_encoded_precision(self.precision, self.descriptor, frame.bytes.len())?;
127 missing_sample_count = missing_sample_count
128 .checked_add(frame.missing_sample_count)
129 .ok_or_else(|| {
130 Error::InvalidInput("missing raster sample count overflows".into())
131 })?;
132 padded_sample_count = padded_sample_count
133 .checked_add(frame.padded_sample_count)
134 .ok_or_else(|| {
135 Error::InvalidInput("padded raster sample count overflows".into())
136 })?;
137 if frame.all_missing {
138 missing_frame_count += 1;
139 continue;
140 }
141 digest.update(&frame.bytes);
142 ranges[channel_ordinal as usize - 1]
143 .observe(frame.finite_minimum, frame.finite_maximum);
144 frame_count += 1;
145 }
146 }
147 }
148 if frame_count == 0 {
149 return Err(Error::InvalidInput(
150 "selected raster channels contain no finite samples".into(),
151 ));
152 }
153 self.frame_count = u32::try_from(frame_count).map_err(|_| {
154 Error::InvalidInput(
155 "selected raster contains more frames than Concatenation Frame Offset Number can represent"
156 .into(),
157 )
158 })?;
159 self.dense = self.selected_channels.len() == 1 && missing_frame_count == 0;
160 self.channel_ranges = ranges
161 .into_iter()
162 .map(MutableRange::finish)
163 .collect::<Result<Vec<_>>>()?;
164 self.pixel_digest = digest.finalize().into();
165 self.semantic_digest = semantic_digest(self);
166 self.diagnostics = build_diagnostics(
167 &self.profile,
168 self.raster.normalizations(),
169 missing_sample_count,
170 padded_sample_count,
171 missing_frame_count,
172 );
173 Ok(())
174 }
175
176 pub(super) fn output_frames(&self) -> OutputFrameCursor<'_> {
177 OutputFrameCursor {
178 document: self,
179 channel_ordinal: 0,
180 tile_index: 0,
181 }
182 }
183}
184
185fn build_diagnostics(
186 profile: &RasterProfile,
187 source_normalizations: &[InteroperabilityDiagnostic],
188 missing_samples: u64,
189 padded_samples: u64,
190 omitted_frames: u64,
191) -> Vec<InteroperabilityDiagnostic> {
192 let mut diagnostics = source_normalizations.to_vec();
193 if profile.dtype.is_integer() {
194 diagnostics.push(InteroperabilityDiagnostic::normalized(
195 "RASTER_INTEGER_SCALED",
196 "$.raster.integer_scaling",
197 "integer samples were transformed with the explicitly declared slope, intercept, missing sentinel, and output precision",
198 ));
199 }
200 if missing_samples > 0 {
201 diagnostics.push(InteroperabilityDiagnostic::normalized(
202 "RASTER_NAN_CANONICALIZED",
203 "$.raster.values",
204 format!(
205 "canonicalized {missing_samples} missing samples to the selected quiet-NaN payload"
206 ),
207 ));
208 }
209 if padded_samples > 0 {
210 diagnostics.push(InteroperabilityDiagnostic::normalized(
211 "PM_EDGE_TILE_PADDED",
212 "$.raster.edge_tiles",
213 format!("padded {padded_samples} edge-tile samples with the canonical quiet NaN"),
214 ));
215 }
216 if omitted_frames > 0 {
217 diagnostics.push(InteroperabilityDiagnostic::normalized(
218 "PM_ALL_MISSING_TILES_OMITTED",
219 "$.raster.tiles",
220 format!("omitted {omitted_frames} all-missing tiles and encoded TILED_SPARSE"),
221 ));
222 }
223 diagnostics
224}
225
226pub(super) struct OutputFrameCursor<'a> {
227 document: &'a ParametricMapDocument,
228 channel_ordinal: usize,
229 tile_index: u64,
230}
231
232impl OutputFrameCursor<'_> {
233 pub(super) fn next(&mut self) -> Result<Option<super::frame::EncodedFrame>> {
234 let descriptor = self.document.descriptor;
235 let tiles_across = descriptor.width.div_ceil(descriptor.tile_width);
236 let tiles_down = descriptor.height.div_ceil(descriptor.tile_height);
237 let tiles_per_channel = u64::from(tiles_across) * u64::from(tiles_down);
238 while self.channel_ordinal < self.document.selected_channels.len() {
239 if self.tile_index == tiles_per_channel {
240 self.channel_ordinal += 1;
241 self.tile_index = 0;
242 continue;
243 }
244 let key = FrameKey {
245 channel: self.document.selected_channels[self.channel_ordinal],
246 channel_ordinal: self.channel_ordinal as u32 + 1,
247 tile_row: (self.tile_index / u64::from(tiles_across)) as u32,
248 tile_column: (self.tile_index % u64::from(tiles_across)) as u32,
249 };
250 self.tile_index += 1;
251 let frame = encode_frame(&self.document.raster, descriptor, key)?;
252 if !frame.all_missing {
253 return Ok(Some(frame));
254 }
255 }
256 Ok(None)
257 }
258}
259
260#[derive(Debug, Clone, Copy, Default)]
261struct MutableRange {
262 minimum: Option<f64>,
263 maximum: Option<f64>,
264}
265
266impl MutableRange {
267 fn observe(&mut self, minimum: Option<f64>, maximum: Option<f64>) {
268 if let Some(value) = minimum {
269 self.minimum = Some(self.minimum.map_or(value, |current| current.min(value)));
270 }
271 if let Some(value) = maximum {
272 self.maximum = Some(self.maximum.map_or(value, |current| current.max(value)));
273 }
274 }
275
276 fn finish(self) -> Result<ValueRange> {
277 match (self.minimum, self.maximum) {
278 (Some(minimum), Some(maximum)) => Ok(ValueRange { minimum, maximum }),
279 _ => Err(Error::InvalidInput(
280 "a selected raster channel contains no finite samples".into(),
281 )),
282 }
283 }
284}
285
286fn validate_descriptor(descriptor: RasterDescriptor) -> Result<()> {
287 if descriptor.tile_height == 0
288 || descriptor.tile_width == 0
289 || descriptor.tile_height > u32::from(u16::MAX)
290 || descriptor.tile_width > u32::from(u16::MAX)
291 {
292 return Err(Error::InvalidInput(
293 "PM tile rows and columns must each fit a nonzero DICOM US value".into(),
294 ));
295 }
296 Ok(())
297}
298
299fn output_precision(profile: &RasterProfile) -> PixelPrecision {
300 match profile.dtype {
301 RasterDType::Float32 => PixelPrecision::Float32,
302 RasterDType::Float64 => PixelPrecision::Float64,
303 _ => match profile
304 .integer_scaling
305 .as_ref()
306 .map(|scaling| scaling.output_precision)
307 {
308 Some(RasterOutputPrecision::Float64) => PixelPrecision::Float64,
309 Some(RasterOutputPrecision::Float32) | None => PixelPrecision::Float32,
310 },
311 }
312}
313
314fn validate_encoded_precision(
315 precision: PixelPrecision,
316 descriptor: RasterDescriptor,
317 actual: usize,
318) -> Result<()> {
319 let expected = super::frame::frame_byte_length(descriptor, precision)?;
320 if actual != expected {
321 return Err(Error::InvalidInput(format!(
322 "raster adapter produced {actual} bytes per frame, expected {expected} for the profile precision"
323 )));
324 }
325 Ok(())
326}
327
328fn semantic_digest(document: &ParametricMapDocument) -> String {
329 let mut digest = Sha256::new();
330 digest.update(b"dicom-parametric-map-v1\0");
331 update_text(&mut digest, document.source.study_instance_uid());
332 update_text(&mut digest, document.source.sop_instance_uid());
333 update_text(
334 &mut digest,
335 document.source.frame_of_reference_uid().unwrap_or(""),
336 );
337 for value in [
338 document.descriptor.height,
339 document.descriptor.width,
340 document.descriptor.tile_height,
341 document.descriptor.tile_width,
342 ] {
343 digest.update(value.to_le_bytes());
344 }
345 for value in document
346 .geometry
347 .origin
348 .into_iter()
349 .chain(document.geometry.orientation)
350 .chain(document.geometry.pixel_spacing)
351 {
352 digest.update(value.to_bits().to_le_bytes());
353 }
354 for channel in &document.selected_channels {
355 let channel = &document.profile.channels[*channel];
356 update_text(&mut digest, &channel.name);
357 update_code(&mut digest, &channel.quantity);
358 update_code(&mut digest, &channel.unit);
359 }
360 update_algorithm(&mut digest, &document.profile.algorithm);
361 digest.update(document.pixel_digest);
362 finish(digest)
363}