wsi_dicom_annotations/annotations/pathology_geojson/
documents.rs1use std::path::Path;
2
3use super::PathologyAnnotationSet;
4use crate::{
5 AnnotationDocument, DerivedObjectProducer, DicomAnnotationContext, Error, Result,
6 SegmentationDocument, StructuredReportDocument,
7};
8
9#[derive(Debug, thiserror::Error)]
10pub enum PathologyDocumentWriteError {
11 #[error("selected pathology DICOM target document is missing")]
12 MissingTarget,
13 #[error("could not write staged DICOM {target}: {source}")]
14 Write {
15 target: &'static str,
16 #[source]
17 source: Error,
18 },
19 #[error("could not reread staged DICOM {target}: {source}")]
20 Verification {
21 target: &'static str,
22 #[source]
23 source: Error,
24 },
25 #[error("reread {0} semantics differ from the staged document")]
26 Mismatch(&'static str),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum PathologyDicomTarget {
31 Ann,
32 Seg,
33 Sr,
34}
35
36impl PathologyDicomTarget {
37 #[must_use]
38 pub const fn label(self) -> &'static str {
39 match self {
40 Self::Ann => "ann",
41 Self::Seg => "seg",
42 Self::Sr => "sr",
43 }
44 }
45
46 #[must_use]
47 pub const fn file_name(self) -> &'static str {
48 match self {
49 Self::Ann => "ann.dcm",
50 Self::Seg => "seg.dcm",
51 Self::Sr => "sr.dcm",
52 }
53 }
54}
55
56pub struct PathologyDicomDocuments {
62 ann: Option<AnnotationDocument>,
63 seg: Option<SegmentationDocument>,
64 sr: Option<StructuredReportDocument>,
65}
66
67impl PathologyDicomDocuments {
68 pub fn build(
69 annotations: &PathologyAnnotationSet,
70 targets: &[PathologyDicomTarget],
71 ) -> Result<Self> {
72 validate_targets(targets)?;
73 let wants_ann = targets.contains(&PathologyDicomTarget::Ann);
74 let wants_seg = targets.contains(&PathologyDicomTarget::Seg);
75 let wants_sr = targets.contains(&PathologyDicomTarget::Sr);
76 let ann = wants_ann
77 .then(|| {
78 if wants_sr {
79 annotations.to_ann_with_companion_sr()
80 } else {
81 annotations.to_ann()
82 }
83 })
84 .transpose()?;
85 let seg = wants_seg
86 .then(|| annotations.to_seg(wants_sr))
87 .transpose()?;
88 let sr = wants_sr
89 .then(|| annotations.to_sr(seg.as_ref()))
90 .transpose()?;
91 Ok(Self { ann, seg, sr })
92 }
93
94 #[must_use]
96 pub fn with_producers(
97 mut self,
98 ann: DerivedObjectProducer,
99 seg: DerivedObjectProducer,
100 sr: DerivedObjectProducer,
101 ) -> Self {
102 self.ann = self.ann.take().map(|document| document.with_producer(ann));
103 self.seg = self.seg.take().map(|document| document.with_producer(seg));
104 self.sr = self.sr.take().map(|document| document.with_producer(sr));
105 self
106 }
107
108 #[must_use]
109 pub const fn ann(&self) -> Option<&AnnotationDocument> {
110 self.ann.as_ref()
111 }
112
113 #[must_use]
114 pub const fn seg(&self) -> Option<&SegmentationDocument> {
115 self.seg.as_ref()
116 }
117
118 #[must_use]
119 pub const fn sr(&self) -> Option<&StructuredReportDocument> {
120 self.sr.as_ref()
121 }
122
123 pub fn write_and_verify(
126 &self,
127 target: PathologyDicomTarget,
128 path: &Path,
129 source: &DicomAnnotationContext,
130 ) -> std::result::Result<(), PathologyDocumentWriteError> {
131 match target {
132 PathologyDicomTarget::Ann => {
133 let expected = self.ann.as_ref().ok_or_else(missing_document)?;
134 expected
135 .write_ann(path)
136 .map_err(|source| write_error("ANN", source))?;
137 let actual = AnnotationDocument::read_ann(path, source)
138 .map_err(|source| verification_error("ANN", source))?;
139 if actual.groups().len() != expected.groups().len()
140 || actual.sop_instance_uid() != expected.sop_instance_uid()
141 {
142 return Err(verification_mismatch("ANN"));
143 }
144 }
145 PathologyDicomTarget::Seg => {
146 let expected = self.seg.as_ref().ok_or_else(missing_document)?;
147 expected
148 .write_seg(path)
149 .map_err(|source| write_error("SEG", source))?;
150 let actual = SegmentationDocument::read_seg(path, source)
151 .map_err(|source| verification_error("SEG", source))?;
152 if actual.segments().len() != expected.segments().len()
153 || actual.sop_instance_uid() != expected.sop_instance_uid()
154 || actual
155 .binary_runs()
156 .map_err(|source| verification_error("SEG", source))?
157 != expected
158 .binary_runs()
159 .map_err(|source| verification_error("SEG", source))?
160 {
161 return Err(verification_mismatch("SEG"));
162 }
163 }
164 PathologyDicomTarget::Sr => {
165 let expected = self.sr.as_ref().ok_or_else(missing_document)?;
166 expected
167 .write_sr(path)
168 .map_err(|source| write_error("SR", source))?;
169 let actual = StructuredReportDocument::read_sr(path, source, self.seg.as_ref())
170 .map_err(|source| verification_error("SR", source))?;
171 if !sr_semantics_match(&actual, expected) {
172 return Err(verification_mismatch("SR"));
173 }
174 }
175 }
176 Ok(())
177 }
178}
179
180fn validate_targets(targets: &[PathologyDicomTarget]) -> Result<()> {
181 if targets.is_empty() {
182 return Err(Error::InvalidInput(
183 "pathology conversion requires at least one DICOM target".into(),
184 ));
185 }
186 if targets
187 .iter()
188 .enumerate()
189 .any(|(index, target)| targets[..index].contains(target))
190 {
191 return Err(Error::InvalidInput(
192 "pathology conversion targets must be unique".into(),
193 ));
194 }
195 Ok(())
196}
197
198fn sr_semantics_match(
199 actual: &StructuredReportDocument,
200 expected: &StructuredReportDocument,
201) -> bool {
202 actual.sop_instance_uid() == expected.sop_instance_uid()
203 && actual.series_instance_uid() == expected.series_instance_uid()
204 && actual.completion_flag() == expected.completion_flag()
205 && actual.verification_flag() == expected.verification_flag()
206 && actual.preliminary_flag() == expected.preliminary_flag()
207 && actual.groups().len() == expected.groups().len()
208 && actual
209 .groups()
210 .iter()
211 .zip(expected.groups())
212 .all(|(actual, expected)| {
213 actual.tracking_id() == expected.tracking_id()
214 && actual.tracking_uid() == expected.tracking_uid()
215 && actual.reference_kind() == expected.reference_kind()
216 && actual.referenced_segmentation_uid()
217 == expected.referenced_segmentation_uid()
218 && actual.referenced_segment_number() == expected.referenced_segment_number()
219 && actual.measurements().len() == expected.measurements().len()
220 && actual
221 .measurements()
222 .iter()
223 .zip(expected.measurements())
224 .all(|(actual, expected)| {
225 actual.concept() == expected.concept()
226 && actual.unit() == expected.unit()
227 && actual.value() == expected.value()
228 })
229 && actual.qualitative_evaluations() == expected.qualitative_evaluations()
230 })
231}
232
233fn missing_document() -> PathologyDocumentWriteError {
234 PathologyDocumentWriteError::MissingTarget
235}
236
237fn write_error(target: &'static str, source: Error) -> PathologyDocumentWriteError {
238 PathologyDocumentWriteError::Write { target, source }
239}
240
241fn verification_error(target: &'static str, source: Error) -> PathologyDocumentWriteError {
242 PathologyDocumentWriteError::Verification { target, source }
243}
244
245fn verification_mismatch(target: &'static str) -> PathologyDocumentWriteError {
246 PathologyDocumentWriteError::Mismatch(target)
247}