wsi_dicom_annotations/annotations/
seg.rs1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum SegToAnnConversionPolicy {
43 RejectLoss,
44 AllowLoss,
45}
46
47#[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 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}