wsi_dicom_annotations/annotations/pathology_geojson/
mod.rs1mod conversion;
2mod documents;
3mod feature;
4mod geometry;
5mod mapping;
6mod preview;
7mod sr_conversion;
8
9use crate::{Error, Result};
10use sha2::{Digest, Sha256};
11
12use super::context::DicomAnnotationContext;
13use super::model::InteroperabilityDiagnostic;
14use super::semantic_digest::{finish, update_text};
15pub use documents::{PathologyDicomDocuments, PathologyDicomTarget, PathologyDocumentWriteError};
16use feature::{parse_feature_collection, ProfiledFeature};
17use mapping::{Mapping, StructuredReportMapping};
18pub use preview::{
19 PathologyPreview, PathologyPreviewFeature, PathologyPreviewGeometry, PathologyPreviewPolygon,
20};
21
22const MAX_GEOJSON_BYTES: usize = 64 * 1024 * 1024;
23const MAX_MAPPING_BYTES: usize = 4 * 1024 * 1024;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PathologyCoordinateSpace {
27 Level0Pixels,
28 SourcePixels,
29 SlideMillimeters,
30}
31
32impl PathologyCoordinateSpace {
33 pub(crate) const fn label(self) -> &'static str {
34 match self {
35 Self::Level0Pixels => "level0-pixels",
36 Self::SourcePixels => "source-pixels",
37 Self::SlideMillimeters => "slide-mm",
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum PathologyGeometryKind {
44 Point,
45 MultiPoint,
46 LineString,
47 MultiLineString,
48 Polygon,
49 MultiPolygon,
50}
51
52#[derive(Debug)]
53pub struct PathologyAnnotationSet {
54 source: DicomAnnotationContext,
55 features: Vec<ProfiledFeature>,
56 diagnostics: Vec<InteroperabilityDiagnostic>,
57 sr_mapping: Option<StructuredReportMapping>,
58}
59
60impl PathologyAnnotationSet {
61 pub fn from_json(
67 geojson: &[u8],
68 mapping: &[u8],
69 source: &DicomAnnotationContext,
70 canonical_source: &DicomAnnotationContext,
71 coordinate_space: PathologyCoordinateSpace,
72 allow_lossy: bool,
73 ) -> Result<Self> {
74 if geojson.len() > MAX_GEOJSON_BYTES {
75 return Err(Error::InvalidInput(format!(
76 "GeoJSON input exceeds the {MAX_GEOJSON_BYTES}-byte limit"
77 )));
78 }
79 if mapping.len() > MAX_MAPPING_BYTES {
80 return Err(Error::InvalidInput(format!(
81 "mapping profile exceeds the {MAX_MAPPING_BYTES}-byte limit"
82 )));
83 }
84 validate_coordinate_contexts(source, canonical_source, coordinate_space)?;
85 let mapping = Mapping::from_json(mapping)?;
86 let (features, diagnostics) = parse_feature_collection(
87 geojson,
88 &mapping,
89 source,
90 canonical_source,
91 coordinate_space,
92 allow_lossy,
93 )?;
94 Ok(Self {
95 source: source.clone(),
96 features,
97 diagnostics,
98 sr_mapping: mapping.sr,
99 })
100 }
101
102 #[must_use]
103 pub fn feature_count(&self) -> usize {
104 self.features.len()
105 }
106
107 #[must_use]
108 pub fn feature_geometry_kind(&self, index: usize) -> Option<PathologyGeometryKind> {
109 self.features.get(index).map(|feature| feature.kind)
110 }
111
112 #[must_use]
113 pub fn feature_tracking_id(&self, index: usize) -> Option<&str> {
114 self.features
115 .get(index)
116 .map(|feature| feature.tracking_id.as_str())
117 }
118
119 #[must_use]
120 pub fn feature_tracking_uid(&self, index: usize) -> Option<&str> {
121 self.features
122 .get(index)
123 .map(|feature| feature.tracking_uid.as_str())
124 }
125
126 #[must_use]
127 pub fn diagnostics(&self) -> &[InteroperabilityDiagnostic] {
128 &self.diagnostics
129 }
130
131 #[must_use]
136 pub fn into_preview(self) -> PathologyPreview {
137 PathologyPreview::from_owned_features(self.features)
138 }
139
140 #[must_use]
142 pub fn semantic_sha256(&self) -> String {
143 let mut digest = Sha256::new();
144 update_text(&mut digest, "pathology-geojson-semantics-v1");
145 update_text(&mut digest, self.source.sop_instance_uid());
146 for feature in &self.features {
147 feature.update_semantic_digest(&mut digest);
148 }
149 digest.update([u8::from(self.sr_mapping.is_some())]);
150 if let Some(mapping) = &self.sr_mapping {
151 mapping.update_digest(&mut digest);
152 }
153 finish(digest)
154 }
155}
156
157fn validate_coordinate_contexts(
158 source: &DicomAnnotationContext,
159 canonical_source: &DicomAnnotationContext,
160 coordinate_space: PathologyCoordinateSpace,
161) -> Result<()> {
162 if coordinate_space != PathologyCoordinateSpace::Level0Pixels {
163 return Ok(());
164 }
165 source.require_shared_slide_frame(canonical_source)
166}