wsi_dicom_annotations/annotations/seg/
document.rs1use std::borrow::Cow;
2use std::path::Path;
3
4use dicom_dictionary_std::{tags, uids};
5use sha2::{Digest, Sha256};
6
7use super::conversion;
8use super::raster::rasterize_segments;
9use super::read::{
10 read_binary_frames, read_fractional_frames, read_labelmap_frames, read_segments,
11 validate_source_reference,
12};
13use super::runs;
14use super::write::add_segmentation_attributes;
15use super::{
16 BinaryMaskRun, BinarySegmentationFrame, FractionalMaskRun, FractionalSegmentationFrame,
17 SegToAnnConversionPolicy, SegmentationDocument, SegmentationKind, SegmentationSegment,
18 VectorizedAnnotations, MAX_SEG_FILE_BYTES,
19};
20use crate::annotations::context::DicomAnnotationContext;
21use crate::annotations::derived_object::{
22 add_common_instance_reference, build_common_object, DerivedObjectProducer,
23};
24use crate::annotations::dicom_dataset::{new_dicom_uid, optional_string, required_string};
25use crate::annotations::dicom_file::{
26 atomic_write_dicom, enforce_file_limit, ensure_sidecar_destination,
27};
28use crate::annotations::model::InteroperabilityDiagnostic;
29use crate::{Error, Result};
30
31impl SegmentationDocument {
32 pub fn binary(
33 source: DicomAnnotationContext,
34 segments: Vec<SegmentationSegment>,
35 ) -> Result<Self> {
36 if segments.is_empty() || segments.len() > usize::from(u16::MAX) {
37 return Err(Error::InvalidInput(
38 "a binary SEG document must contain 1..=65535 segments".into(),
39 ));
40 }
41 for segment in &segments {
42 for point in segment
43 .outer_polygons()
44 .iter()
45 .chain(segment.exclusion_polygons())
46 .flatten()
47 {
48 source.validate_point(point.x, point.y)?;
49 }
50 }
51 Ok(Self {
52 source,
53 kind: SegmentationKind::Binary,
54 sop_instance_uid: new_dicom_uid(),
55 series_instance_uid: new_dicom_uid(),
56 content_label: "WSI_SEGMENTATION".into(),
57 content_description: "WSI binary segmentation".into(),
58 content_creator_name: None,
59 producer: DerivedObjectProducer::library_default(9201, "WSI segmentations"),
60 segments,
61 imported_binary_frames: None,
62 imported_fractional_frames: None,
63 diagnostics: Vec::new(),
64 })
65 }
66
67 pub fn read_seg(path: impl AsRef<Path>, source: &DicomAnnotationContext) -> Result<Self> {
68 let path = path.as_ref();
69 enforce_file_limit(path, MAX_SEG_FILE_BYTES, "SEG")?;
70 let object = dicom_object::open_file(path).map_err(|error| Error::DicomRead {
71 path: path.to_path_buf(),
72 source: Box::new(error),
73 })?;
74 let sop_class = object.meta().media_storage_sop_class_uid();
75 if sop_class != uids::SEGMENTATION_STORAGE
76 && sop_class != uids::LABEL_MAP_SEGMENTATION_STORAGE
77 {
78 return Err(Error::Unsupported(format!(
79 "{} is not a DICOM Segmentation instance",
80 path.display()
81 )));
82 }
83 validate_source_reference(&object, source)?;
84 let segmentation_type = required_string(&object, tags::SEGMENTATION_TYPE)?;
85 let kind = match segmentation_type.as_str() {
86 "BINARY" => SegmentationKind::Binary,
87 "LABELMAP" => SegmentationKind::LabelMap,
88 "FRACTIONAL" => SegmentationKind::Fractional,
89 other => {
90 return Err(Error::Unsupported(format!(
91 "SEG type {other:?} is not recognized"
92 )))
93 }
94 };
95 if !matches!(
96 object.meta().transfer_syntax(),
97 uids::IMPLICIT_VR_LITTLE_ENDIAN | uids::EXPLICIT_VR_LITTLE_ENDIAN
98 ) {
99 return Err(Error::Unsupported(
100 "compressed or big-endian SEG Pixel Data is not supported".into(),
101 ));
102 }
103 let mut diagnostics = Vec::new();
104 let segments = read_segments(
105 &object,
106 &mut diagnostics,
107 kind == SegmentationKind::LabelMap,
108 )?;
109 let (imported_binary_frames, imported_fractional_frames) = match kind {
110 SegmentationKind::Binary => {
111 (Some(read_binary_frames(&object, source, &segments)?), None)
112 }
113 SegmentationKind::LabelMap => (
114 Some(read_labelmap_frames(&object, source, &segments)?),
115 None,
116 ),
117 SegmentationKind::Fractional => (
118 None,
119 Some(read_fractional_frames(&object, source, &segments)?),
120 ),
121 };
122 Ok(Self {
123 source: source.clone(),
124 kind,
125 sop_instance_uid: required_string(&object, tags::SOP_INSTANCE_UID)?,
126 series_instance_uid: required_string(&object, tags::SERIES_INSTANCE_UID)?,
127 content_label: required_string(&object, tags::CONTENT_LABEL)?,
128 content_description: optional_string(&object, tags::CONTENT_DESCRIPTION)
129 .unwrap_or_default(),
130 content_creator_name: optional_string(&object, tags::CONTENT_CREATOR_NAME),
131 producer: DerivedObjectProducer::read(&object),
132 segments,
133 imported_binary_frames,
134 imported_fractional_frames,
135 diagnostics,
136 })
137 }
138
139 #[must_use]
141 pub fn with_producer(mut self, producer: DerivedObjectProducer) -> Self {
142 self.producer = producer;
143 self
144 }
145
146 #[must_use]
147 pub fn producer(&self) -> &DerivedObjectProducer {
148 &self.producer
149 }
150
151 pub fn rasterized_frames(&self) -> Result<Vec<BinarySegmentationFrame>> {
152 Ok(self.binary_frames()?.into_owned())
153 }
154
155 pub(super) fn binary_frames(&self) -> Result<Cow<'_, [BinarySegmentationFrame]>> {
156 if let Some(frames) = &self.imported_binary_frames {
157 return Ok(Cow::Borrowed(frames));
158 }
159 if self.kind != SegmentationKind::Binary {
160 return Err(Error::Unsupported(
161 "fractional SEG is a read-only raster overlay and cannot be vectorized as binary"
162 .into(),
163 ));
164 }
165 Ok(Cow::Owned(rasterize_segments(
166 &self.source,
167 &self.segments,
168 )?))
169 }
170
171 pub fn binary_runs(&self) -> Result<Vec<BinaryMaskRun>> {
172 runs::binary_runs(self)
173 }
174
175 pub fn fractional_runs(&self) -> Result<Vec<FractionalMaskRun>> {
176 runs::fractional_runs(self)
177 }
178
179 pub fn mask_digest(&self) -> Result<String> {
180 let mut digest = Sha256::new();
181 digest.update(b"dicom-viewer-seg-runs-v1\0");
182 let (columns, rows) = self.source.total_pixel_matrix_dimensions();
183 digest.update(columns.to_le_bytes());
184 digest.update(rows.to_le_bytes());
185 digest.update([match self.kind {
186 SegmentationKind::Binary => 0,
187 SegmentationKind::LabelMap => 1,
188 SegmentationKind::Fractional => 2,
189 }]);
190 if self.kind == SegmentationKind::Fractional {
191 for run in self.fractional_runs()? {
192 digest.update(run.segment_number.to_le_bytes());
193 digest.update(run.row.to_le_bytes());
194 digest.update(run.column_start.to_le_bytes());
195 digest.update(run.maximum_fractional_value.to_le_bytes());
196 digest.update(
197 u32::try_from(run.values.len())
198 .map_err(|_| {
199 Error::InvalidInput(
200 "fractional SEG run length exceeds DICOM range".into(),
201 )
202 })?
203 .to_le_bytes(),
204 );
205 for value in run.values {
206 digest.update(value.to_le_bytes());
207 }
208 }
209 } else {
210 for run in self.binary_runs()? {
211 digest.update(run.segment_number.to_le_bytes());
212 digest.update(run.row.to_le_bytes());
213 digest.update(run.column_start.to_le_bytes());
214 digest.update(run.length.to_le_bytes());
215 }
216 }
217 Ok(format!("{:x}", digest.finalize()))
218 }
219
220 #[must_use]
221 pub fn fractional_frames(&self) -> Option<&[FractionalSegmentationFrame]> {
222 self.imported_fractional_frames.as_deref()
223 }
224
225 pub fn write_seg(&self, path: impl AsRef<Path>) -> Result<()> {
226 self.write_seg_with_loss_policy(path, false)
227 }
228
229 pub fn write_seg_with_loss_policy(
230 &self,
231 path: impl AsRef<Path>,
232 allow_lossy: bool,
233 ) -> Result<()> {
234 if self.kind != SegmentationKind::Binary {
235 return Err(Error::Unsupported(
236 "only binary SEG export is supported".into(),
237 ));
238 }
239 if !allow_lossy {
240 let blocking = self
241 .diagnostics
242 .iter()
243 .filter(|diagnostic| diagnostic.blocks_roundtrip())
244 .map(InteroperabilityDiagnostic::code)
245 .collect::<Vec<_>>();
246 if !blocking.is_empty() {
247 return Err(Error::Unsupported(format!(
248 "SEG rewrite would lose semantics ({}); pass the explicit lossy override to continue",
249 blocking.join(", ")
250 )));
251 }
252 }
253 let path = path.as_ref();
254 ensure_sidecar_destination(path, self.source.source_path())?;
255 let frames = self.rasterized_frames()?;
256 if frames.is_empty() {
257 return Err(Error::InvalidInput(
258 "SEG contains no non-empty frames".into(),
259 ));
260 }
261 let mut object = build_common_object(
262 &self.source,
263 uids::SEGMENTATION_STORAGE,
264 &self.sop_instance_uid,
265 &self.series_instance_uid,
266 "SEG",
267 &self.producer,
268 )?;
269 add_segmentation_attributes(&mut object, self, &frames)?;
270 add_common_instance_reference(&mut object, &self.source);
271 atomic_write_dicom(
272 path,
273 object,
274 uids::SEGMENTATION_STORAGE,
275 &self.sop_instance_uid,
276 )
277 }
278
279 #[must_use]
280 pub fn revised(&self) -> Self {
281 let mut revised = self.clone();
282 revised.sop_instance_uid = new_dicom_uid();
283 revised
284 }
285
286 pub fn vectorized_annotations(
291 &self,
292 policy: SegToAnnConversionPolicy,
293 ) -> Result<VectorizedAnnotations> {
294 conversion::vectorized_annotations(self, policy)
295 }
296
297 #[must_use]
298 pub const fn kind(&self) -> SegmentationKind {
299 self.kind
300 }
301
302 #[must_use]
303 pub const fn editable(&self) -> bool {
304 !matches!(self.kind, SegmentationKind::Fractional)
305 }
306
307 #[must_use]
308 pub fn source(&self) -> &DicomAnnotationContext {
309 &self.source
310 }
311
312 #[must_use]
313 pub fn sop_instance_uid(&self) -> &str {
314 &self.sop_instance_uid
315 }
316
317 #[must_use]
318 pub fn segments(&self) -> &[SegmentationSegment] {
319 &self.segments
320 }
321
322 #[must_use]
323 pub fn series_instance_uid(&self) -> &str {
324 &self.series_instance_uid
325 }
326
327 #[must_use]
328 pub fn content_label(&self) -> &str {
329 &self.content_label
330 }
331
332 #[must_use]
333 pub fn content_description(&self) -> &str {
334 &self.content_description
335 }
336
337 #[must_use]
338 pub fn content_creator_name(&self) -> Option<&str> {
339 self.content_creator_name.as_deref()
340 }
341
342 #[must_use]
343 pub fn diagnostics(&self) -> &[InteroperabilityDiagnostic] {
344 &self.diagnostics
345 }
346
347 #[must_use]
348 pub fn segment_for_number(&self, number: u16) -> Option<&SegmentationSegment> {
349 self.segment_index_for_number(number)
350 .and_then(|index| self.segments.get(index))
351 }
352
353 fn segment_index_for_number(&self, number: u16) -> Option<usize> {
354 self.segments
355 .iter()
356 .enumerate()
357 .position(|(index, segment)| {
358 segment
359 .source_segment_number
360 .unwrap_or_else(|| u16::try_from(index + 1).unwrap_or(u16::MAX))
361 == number
362 })
363 }
364}