Skip to main content

wsi_dicom_annotations/annotations/
seg.rs

1mod conversion;
2mod document;
3mod raster;
4mod read;
5mod runs;
6mod write;
7
8#[cfg(test)]
9#[path = "seg/performance_tests.rs"]
10mod performance_tests;
11#[cfg(test)]
12#[path = "seg/run_tests.rs"]
13mod run_tests;
14#[cfg(test)]
15#[path = "seg/vectorization_tests.rs"]
16mod vectorization_tests;
17
18use crate::{Error, Result};
19
20use super::context::DicomAnnotationContext;
21use super::derived_object::DerivedObjectProducer;
22use super::model::AnnotationGroup;
23use super::model::{
24    validate_polygon, validate_text, AlgorithmIdentification, DicomCode, FindingSemantics,
25    GenerationType, InteroperabilityDiagnostic, Point2,
26};
27
28const MAX_SEG_FILE_BYTES: u64 = 1024 * 1024 * 1024;
29const MAX_SEGMENTATION_FRAMES: usize = 65_536;
30const MAX_SEGMENTATION_PIXELS: usize = 512 * 1024 * 1024;
31const MAX_VECTORIZED_RUNS: usize = 1_000_000;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum SegmentationKind {
35    Binary,
36    LabelMap,
37    Fractional,
38}
39
40/// Controls whether SEG-to-ANN projection may discard nonrepresentable semantics.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum SegToAnnConversionPolicy {
43    RejectLoss,
44    AllowLoss,
45}
46
47/// Editable ANN groups projected from a SEG, together with typed loss diagnostics.
48#[derive(Debug, Clone, PartialEq)]
49pub struct VectorizedAnnotations {
50    groups: Vec<AnnotationGroup>,
51    diagnostics: Vec<InteroperabilityDiagnostic>,
52}
53
54impl VectorizedAnnotations {
55    #[must_use]
56    pub fn groups(&self) -> &[AnnotationGroup] {
57        &self.groups
58    }
59
60    #[must_use]
61    pub fn diagnostics(&self) -> &[InteroperabilityDiagnostic] {
62        &self.diagnostics
63    }
64
65    #[must_use]
66    pub fn into_groups(self) -> Vec<AnnotationGroup> {
67        self.groups
68    }
69}
70
71#[derive(Debug, Clone, PartialEq)]
72pub struct SegmentationSegment {
73    source_segment_number: Option<u16>,
74    label: String,
75    description: String,
76    finding: FindingSemantics,
77    tracking_id: Option<String>,
78    tracking_uid: Option<String>,
79    outer_polygons: Vec<Vec<Point2>>,
80    exclusion_polygons: Vec<Vec<Point2>>,
81    // `Some` preserves GeoJSON MultiPolygon ownership: each entry contains
82    // the holes belonging only to the outer polygon at the same index.
83    component_holes: Option<Vec<Vec<Vec<Point2>>>>,
84}
85
86impl SegmentationSegment {
87    #[allow(clippy::too_many_arguments)]
88    pub fn new(
89        label: impl Into<String>,
90        category: DicomCode,
91        property_type: DicomCode,
92        recommended_display_cielab: [u16; 3],
93        outer_polygons: Vec<Vec<Point2>>,
94        exclusion_polygons: Vec<Vec<Point2>>,
95    ) -> Result<Self> {
96        let label = label.into();
97        if label.trim().is_empty() || label.len() > 64 || label.contains(['\\', '\0']) {
98            return Err(Error::InvalidInput(
99                "segment label must be 1..=64 bytes and contain no DICOM separator or NUL".into(),
100            ));
101        }
102        if outer_polygons.is_empty() {
103            return Err(Error::InvalidInput(
104                "a binary segment needs at least one outer polygon".into(),
105            ));
106        }
107        for polygon in outer_polygons.iter().chain(&exclusion_polygons) {
108            validate_polygon(polygon)?;
109        }
110        Ok(Self {
111            source_segment_number: None,
112            label,
113            description: String::new(),
114            finding: FindingSemantics::manual(category, property_type, recommended_display_cielab),
115            tracking_id: None,
116            tracking_uid: None,
117            outer_polygons,
118            exclusion_polygons,
119            component_holes: None,
120        })
121    }
122
123    pub fn with_component_holes(mut self, component_holes: Vec<Vec<Vec<Point2>>>) -> Result<Self> {
124        if component_holes.len() != self.outer_polygons.len() {
125            return Err(Error::InvalidInput(
126                "component hole groups must match the outer polygon count".into(),
127            ));
128        }
129        for hole in component_holes.iter().flatten() {
130            validate_polygon(hole)?;
131        }
132        self.exclusion_polygons = component_holes.iter().flatten().cloned().collect();
133        self.component_holes = Some(component_holes);
134        Ok(self)
135    }
136
137    pub fn with_description(mut self, description: impl Into<String>) -> Result<Self> {
138        let description = description.into();
139        if description.len() > 1_024 || description.contains('\0') {
140            return Err(Error::InvalidInput(
141                "segment description exceeds 1024 bytes or contains NUL".into(),
142            ));
143        }
144        self.description = description;
145        Ok(self)
146    }
147
148    pub fn with_generation(
149        mut self,
150        generation_type: GenerationType,
151        algorithms: Vec<AlgorithmIdentification>,
152    ) -> Result<Self> {
153        self.finding = self.finding.with_generation(generation_type, algorithms)?;
154        Ok(self)
155    }
156
157    #[must_use]
158    pub fn with_property_type_modifiers(mut self, modifiers: Vec<DicomCode>) -> Self {
159        self.finding = self.finding.with_property_type_modifiers(modifiers);
160        self
161    }
162
163    pub fn with_tracking(
164        mut self,
165        tracking_id: impl Into<String>,
166        tracking_uid: impl Into<String>,
167    ) -> Result<Self> {
168        let tracking_id = tracking_id.into();
169        let tracking_uid = tracking_uid.into();
170        validate_text("tracking ID", &tracking_id, 10_240)?;
171        validate_text("tracking UID", &tracking_uid, 64)?;
172        self.tracking_id = Some(tracking_id);
173        self.tracking_uid = Some(tracking_uid);
174        Ok(self)
175    }
176
177    #[must_use]
178    pub fn with_anatomic_regions(mut self, regions: Vec<DicomCode>) -> Self {
179        self.finding = self.finding.with_anatomic_regions(regions);
180        self
181    }
182
183    #[must_use]
184    pub fn with_primary_anatomic_structures(mut self, structures: Vec<DicomCode>) -> Self {
185        self.finding = self.finding.with_primary_anatomic_structures(structures);
186        self
187    }
188
189    pub(crate) fn with_finding_semantics(mut self, finding: FindingSemantics) -> Result<Self> {
190        finding.validate()?;
191        self.finding = finding;
192        Ok(self)
193    }
194
195    #[must_use]
196    pub fn label(&self) -> &str {
197        &self.label
198    }
199
200    #[must_use]
201    pub fn description(&self) -> &str {
202        &self.description
203    }
204
205    #[must_use]
206    pub const fn generation_type(&self) -> GenerationType {
207        self.finding.generation_type()
208    }
209
210    #[must_use]
211    pub fn algorithms(&self) -> &[AlgorithmIdentification] {
212        self.finding.algorithms()
213    }
214
215    #[must_use]
216    pub fn source_segment_number(&self) -> Option<u16> {
217        self.source_segment_number
218    }
219
220    #[must_use]
221    pub fn category(&self) -> &DicomCode {
222        self.finding.category()
223    }
224
225    #[must_use]
226    pub fn property_type(&self) -> &DicomCode {
227        self.finding.property_type()
228    }
229
230    #[must_use]
231    pub fn property_type_modifiers(&self) -> &[DicomCode] {
232        self.finding.property_type_modifiers()
233    }
234
235    #[must_use]
236    pub fn tracking_id(&self) -> Option<&str> {
237        self.tracking_id.as_deref()
238    }
239
240    #[must_use]
241    pub fn tracking_uid(&self) -> Option<&str> {
242        self.tracking_uid.as_deref()
243    }
244
245    #[must_use]
246    pub fn anatomic_regions(&self) -> &[DicomCode] {
247        self.finding.anatomic_regions()
248    }
249
250    #[must_use]
251    pub fn primary_anatomic_structures(&self) -> &[DicomCode] {
252        self.finding.primary_anatomic_structures()
253    }
254
255    #[must_use]
256    pub const fn recommended_display_cielab(&self) -> [u16; 3] {
257        self.finding.recommended_display_cielab()
258    }
259
260    #[must_use]
261    pub fn outer_polygons(&self) -> &[Vec<Point2>] {
262        &self.outer_polygons
263    }
264
265    #[must_use]
266    pub fn exclusion_polygons(&self) -> &[Vec<Point2>] {
267        &self.exclusion_polygons
268    }
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct BinarySegmentationFrame {
273    segment_number: u16,
274    tile_col: u32,
275    tile_row: u32,
276    width: u16,
277    height: u16,
278    mask: Vec<bool>,
279}
280
281impl BinarySegmentationFrame {
282    #[must_use]
283    pub const fn segment_number(&self) -> u16 {
284        self.segment_number
285    }
286
287    #[must_use]
288    pub const fn tile_col(&self) -> u32 {
289        self.tile_col
290    }
291
292    #[must_use]
293    pub const fn tile_row(&self) -> u32 {
294        self.tile_row
295    }
296
297    #[must_use]
298    pub const fn dimensions(&self) -> (u16, u16) {
299        (self.width, self.height)
300    }
301
302    #[must_use]
303    pub fn mask(&self) -> &[bool] {
304        &self.mask
305    }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct FractionalSegmentationFrame {
310    segment_number: u16,
311    tile_col: u32,
312    tile_row: u32,
313    width: u16,
314    height: u16,
315    maximum_fractional_value: u16,
316    values: Vec<u16>,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct BinaryMaskRun {
321    segment_number: u16,
322    row: u32,
323    column_start: u32,
324    length: u32,
325}
326
327impl BinaryMaskRun {
328    #[must_use]
329    pub const fn segment_number(&self) -> u16 {
330        self.segment_number
331    }
332
333    #[must_use]
334    pub const fn row(&self) -> u32 {
335        self.row
336    }
337
338    #[must_use]
339    pub const fn column_start(&self) -> u32 {
340        self.column_start
341    }
342
343    #[must_use]
344    pub const fn length(&self) -> u32 {
345        self.length
346    }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct FractionalMaskRun {
351    segment_number: u16,
352    row: u32,
353    column_start: u32,
354    maximum_fractional_value: u16,
355    values: Vec<u16>,
356}
357
358impl FractionalMaskRun {
359    #[must_use]
360    pub const fn segment_number(&self) -> u16 {
361        self.segment_number
362    }
363
364    #[must_use]
365    pub const fn row(&self) -> u32 {
366        self.row
367    }
368
369    #[must_use]
370    pub const fn column_start(&self) -> u32 {
371        self.column_start
372    }
373
374    #[must_use]
375    pub const fn maximum_fractional_value(&self) -> u16 {
376        self.maximum_fractional_value
377    }
378
379    #[must_use]
380    pub fn values(&self) -> &[u16] {
381        &self.values
382    }
383}
384
385impl FractionalSegmentationFrame {
386    #[must_use]
387    pub const fn segment_number(&self) -> u16 {
388        self.segment_number
389    }
390
391    #[must_use]
392    pub const fn tile_col(&self) -> u32 {
393        self.tile_col
394    }
395
396    #[must_use]
397    pub const fn tile_row(&self) -> u32 {
398        self.tile_row
399    }
400
401    #[must_use]
402    pub const fn dimensions(&self) -> (u16, u16) {
403        (self.width, self.height)
404    }
405
406    #[must_use]
407    pub const fn maximum_fractional_value(&self) -> u16 {
408        self.maximum_fractional_value
409    }
410
411    #[must_use]
412    pub fn values(&self) -> &[u16] {
413        &self.values
414    }
415}
416
417#[derive(Debug, Clone, PartialEq)]
418pub struct SegmentationDocument {
419    source: DicomAnnotationContext,
420    kind: SegmentationKind,
421    sop_instance_uid: String,
422    series_instance_uid: String,
423    content_label: String,
424    content_description: String,
425    content_creator_name: Option<String>,
426    producer: DerivedObjectProducer,
427    segments: Vec<SegmentationSegment>,
428    imported_binary_frames: Option<Vec<BinarySegmentationFrame>>,
429    imported_fractional_frames: Option<Vec<FractionalSegmentationFrame>>,
430    diagnostics: Vec<InteroperabilityDiagnostic>,
431}