Skip to main content

wsi_dicom_annotations/annotations/seg/
document.rs

1use 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    /// Replaces the neutral library identity with caller-owned producer metadata.
140    #[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    /// Projects binary or label-map pixels into exact pixel-edge ANN rectangles.
287    ///
288    /// Representable coded semantics are preserved. Identity that ANN cannot encode
289    /// is reported through typed diagnostics and rejected unless `AllowLoss` is used.
290    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}