1mod reader;
2mod writer;
3
4use std::path::Path;
5
6use crate::{Error, Result};
7
8use super::context::DicomAnnotationContext;
9use super::derived_object::DerivedObjectProducer;
10use super::dicom_dataset::new_dicom_uid;
11use super::model::{AlgorithmIdentification, DicomCode, Point2, Point3, TrackingIdentity};
12use super::seg::SegmentationDocument;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum StructuredReportReferenceKind {
16 Point,
17 Polygon,
18 Segmentation,
19 MeasurementCoordinates,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum CoordinateGraphic {
24 Point,
25 Multipoint,
26 Polyline,
27 Polygon,
28}
29
30impl CoordinateGraphic {
31 #[must_use]
32 pub const fn dicom_value(self) -> &'static str {
33 match self {
34 Self::Point => "POINT",
35 Self::Multipoint => "MULTIPOINT",
36 Self::Polyline => "POLYLINE",
37 Self::Polygon => "POLYGON",
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub struct SpatialCoordinates {
44 pub(crate) graphic: CoordinateGraphic,
45 pub(crate) points: Vec<Point3>,
46 pub(crate) frame_of_reference_uid: String,
47}
48
49impl SpatialCoordinates {
50 #[must_use]
51 pub const fn graphic(&self) -> CoordinateGraphic {
52 self.graphic
53 }
54
55 #[must_use]
56 pub fn points(&self) -> &[Point3] {
57 &self.points
58 }
59
60 #[must_use]
61 pub fn frame_of_reference_uid(&self) -> &str {
62 &self.frame_of_reference_uid
63 }
64}
65
66#[derive(Debug, Clone, PartialEq)]
67pub(crate) struct SegmentationReference {
68 pub(crate) sop_instance_uid: String,
69 pub(crate) series_instance_uid: String,
70 pub(crate) segment_number: u16,
71 pub(crate) frame_numbers: Vec<u32>,
72}
73
74#[derive(Debug, Clone, PartialEq)]
75pub(crate) enum RegionReference {
76 Coordinates(SpatialCoordinates),
77 Segmentation(SegmentationReference),
78 MeasurementCoordinates,
79}
80
81#[derive(Debug, Clone, PartialEq)]
82pub struct StructuredReportMeasurement {
83 pub(crate) concept: DicomCode,
84 pub(crate) unit: DicomCode,
85 pub(crate) value: f64,
86 pub(crate) coordinates: Vec<SpatialCoordinates>,
87}
88
89impl StructuredReportMeasurement {
90 #[must_use]
91 pub fn concept(&self) -> &DicomCode {
92 &self.concept
93 }
94
95 #[must_use]
96 pub fn unit(&self) -> &DicomCode {
97 &self.unit
98 }
99
100 #[must_use]
101 pub const fn value(&self) -> f64 {
102 self.value
103 }
104
105 #[must_use]
106 pub fn coordinates(&self) -> &[SpatialCoordinates] {
107 &self.coordinates
108 }
109}
110
111#[derive(Debug, Clone, PartialEq)]
112pub struct StructuredReportQualitativeEvaluation {
113 pub(crate) concept: DicomCode,
114 pub(crate) value: DicomCode,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct MeasurementReportSemantics {
119 report_title: DicomCode,
120 procedures_reported: Vec<DicomCode>,
121 length_concept: DicomCode,
122 length_unit: DicomCode,
123}
124
125impl MeasurementReportSemantics {
126 pub fn new(
127 report_title: DicomCode,
128 procedures_reported: Vec<DicomCode>,
129 length_concept: DicomCode,
130 length_unit: DicomCode,
131 ) -> Result<Self> {
132 if procedures_reported.is_empty() {
133 return Err(Error::InvalidInput(
134 "measurement report semantics require at least one procedure".into(),
135 ));
136 }
137 Ok(Self {
138 report_title,
139 procedures_reported,
140 length_concept,
141 length_unit,
142 })
143 }
144
145 #[must_use]
146 pub fn pathology_v1() -> Self {
147 Self::new(
148 DicomCode::new("126000", "DCM", "Imaging Measurement Report")
149 .expect("fixed report title code is valid"),
150 vec![DicomCode::new("252416005", "SCT", "Histopathology test")
151 .expect("fixed pathology procedure code is valid")],
152 DicomCode::new("410668003", "SCT", "Length").expect("fixed length code is valid"),
153 DicomCode::new("mm", "UCUM", "mm").expect("fixed millimeter code is valid"),
154 )
155 .expect("fixed pathology measurement semantics are valid")
156 }
157
158 #[must_use]
159 pub fn report_title(&self) -> &DicomCode {
160 &self.report_title
161 }
162
163 #[must_use]
164 pub fn procedures_reported(&self) -> &[DicomCode] {
165 &self.procedures_reported
166 }
167
168 #[must_use]
169 pub fn length_concept(&self) -> &DicomCode {
170 &self.length_concept
171 }
172
173 #[must_use]
174 pub fn length_unit(&self) -> &DicomCode {
175 &self.length_unit
176 }
177}
178
179#[derive(Debug, Clone, PartialEq)]
180pub struct LinearMeasurementSpec {
181 tracking: TrackingIdentity,
182 finding_category: DicomCode,
183 finding_type: DicomCode,
184 finding_sites: Vec<DicomCode>,
185 endpoints: [Point2; 2],
186}
187
188impl LinearMeasurementSpec {
189 pub fn new(
190 tracking: TrackingIdentity,
191 finding_category: DicomCode,
192 finding_type: DicomCode,
193 start: Point2,
194 end: Point2,
195 ) -> Result<Self> {
196 if !start.x.is_finite() || !start.y.is_finite() || !end.x.is_finite() || !end.y.is_finite()
197 {
198 return Err(Error::InvalidInput(
199 "linear measurement endpoints must be finite".into(),
200 ));
201 }
202 if start == end {
203 return Err(Error::InvalidInput(
204 "linear measurement requires distinct endpoints".into(),
205 ));
206 }
207 Ok(Self {
208 tracking,
209 finding_category,
210 finding_type,
211 finding_sites: Vec::new(),
212 endpoints: [start, end],
213 })
214 }
215
216 #[must_use]
217 pub fn with_finding_sites(mut self, finding_sites: Vec<DicomCode>) -> Self {
218 self.finding_sites = finding_sites;
219 self
220 }
221
222 #[must_use]
223 pub fn tracking(&self) -> &TrackingIdentity {
224 &self.tracking
225 }
226
227 #[must_use]
228 pub fn finding_category(&self) -> &DicomCode {
229 &self.finding_category
230 }
231
232 #[must_use]
233 pub fn finding_type(&self) -> &DicomCode {
234 &self.finding_type
235 }
236
237 #[must_use]
238 pub fn finding_sites(&self) -> &[DicomCode] {
239 &self.finding_sites
240 }
241
242 #[must_use]
243 pub const fn endpoints(&self) -> [Point2; 2] {
244 self.endpoints
245 }
246}
247
248impl StructuredReportQualitativeEvaluation {
249 #[must_use]
250 pub fn concept(&self) -> &DicomCode {
251 &self.concept
252 }
253
254 #[must_use]
255 pub fn value(&self) -> &DicomCode {
256 &self.value
257 }
258}
259
260#[derive(Debug, Clone, PartialEq)]
261pub struct StructuredReportMeasurementGroup {
262 pub(crate) template_id: &'static str,
263 pub(crate) tracking_id: String,
264 pub(crate) tracking_uid: String,
265 pub(crate) finding_category: DicomCode,
266 pub(crate) finding_type: DicomCode,
267 pub(crate) algorithms: Vec<AlgorithmIdentification>,
268 pub(crate) finding_sites: Vec<DicomCode>,
269 pub(crate) reference: RegionReference,
270 pub(crate) measurements: Vec<StructuredReportMeasurement>,
271 pub(crate) qualitative_evaluations: Vec<StructuredReportQualitativeEvaluation>,
272}
273
274impl StructuredReportMeasurementGroup {
275 #[must_use]
276 pub fn tracking_id(&self) -> &str {
277 &self.tracking_id
278 }
279
280 #[must_use]
281 pub fn tracking_uid(&self) -> &str {
282 &self.tracking_uid
283 }
284
285 #[must_use]
286 pub fn finding_category(&self) -> &DicomCode {
287 &self.finding_category
288 }
289
290 #[must_use]
291 pub fn finding_type(&self) -> &DicomCode {
292 &self.finding_type
293 }
294
295 #[must_use]
296 pub fn algorithms(&self) -> &[AlgorithmIdentification] {
297 &self.algorithms
298 }
299
300 #[must_use]
301 pub fn finding_sites(&self) -> &[DicomCode] {
302 &self.finding_sites
303 }
304
305 #[must_use]
306 pub fn region_coordinates(&self) -> Option<&SpatialCoordinates> {
307 match &self.reference {
308 RegionReference::Coordinates(coordinates) => Some(coordinates),
309 RegionReference::Segmentation(_) | RegionReference::MeasurementCoordinates => None,
310 }
311 }
312
313 #[must_use]
314 pub const fn reference_kind(&self) -> StructuredReportReferenceKind {
315 match &self.reference {
316 RegionReference::Coordinates(coordinates) => match coordinates.graphic {
317 CoordinateGraphic::Point => StructuredReportReferenceKind::Point,
318 CoordinateGraphic::Polygon => StructuredReportReferenceKind::Polygon,
319 CoordinateGraphic::Multipoint | CoordinateGraphic::Polyline => {
320 StructuredReportReferenceKind::MeasurementCoordinates
321 }
322 },
323 RegionReference::Segmentation(_) => StructuredReportReferenceKind::Segmentation,
324 RegionReference::MeasurementCoordinates => {
325 StructuredReportReferenceKind::MeasurementCoordinates
326 }
327 }
328 }
329
330 #[must_use]
331 pub fn referenced_segmentation_uid(&self) -> Option<&str> {
332 match &self.reference {
333 RegionReference::Segmentation(reference) => Some(&reference.sop_instance_uid),
334 _ => None,
335 }
336 }
337
338 #[must_use]
339 pub const fn referenced_segment_number(&self) -> Option<u16> {
340 match &self.reference {
341 RegionReference::Segmentation(reference) => Some(reference.segment_number),
342 _ => None,
343 }
344 }
345
346 #[must_use]
347 pub fn measurements(&self) -> &[StructuredReportMeasurement] {
348 &self.measurements
349 }
350
351 #[must_use]
352 pub fn qualitative_evaluations(&self) -> &[StructuredReportQualitativeEvaluation] {
353 &self.qualitative_evaluations
354 }
355}
356
357#[derive(Debug, Clone, PartialEq)]
358pub struct StructuredReportDocument {
359 pub(crate) source: DicomAnnotationContext,
360 pub(crate) sop_instance_uid: String,
361 pub(crate) series_instance_uid: String,
362 pub(crate) device_observer_uid: String,
363 pub(crate) producer: DerivedObjectProducer,
364 pub(crate) report_title: DicomCode,
365 pub(crate) procedures_reported: Vec<DicomCode>,
366 pub(crate) groups: Vec<StructuredReportMeasurementGroup>,
367 pub(crate) completion_flag: String,
368 pub(crate) verification_flag: String,
369 pub(crate) preliminary_flag: String,
370}
371
372impl StructuredReportDocument {
373 pub(crate) fn new(
374 source: DicomAnnotationContext,
375 report_title: DicomCode,
376 procedures_reported: Vec<DicomCode>,
377 groups: Vec<StructuredReportMeasurementGroup>,
378 ) -> Result<Self> {
379 if procedures_reported.is_empty() || groups.is_empty() {
380 return Err(Error::InvalidInput(
381 "TID 1500 requires a procedure reported and at least one measurement group".into(),
382 ));
383 }
384 if source.frame_of_reference_uid().is_none() {
385 return Err(Error::InvalidInput(
386 "Comprehensive 3D SR coordinates require a source Frame of Reference UID".into(),
387 ));
388 }
389 Ok(Self {
390 source,
391 sop_instance_uid: new_dicom_uid(),
392 series_instance_uid: new_dicom_uid(),
393 device_observer_uid: new_dicom_uid(),
394 producer: DerivedObjectProducer::library_default(9301, "WSI measurement reports"),
395 report_title,
396 procedures_reported,
397 groups,
398 completion_flag: "COMPLETE".into(),
399 verification_flag: "UNVERIFIED".into(),
400 preliminary_flag: "PRELIMINARY".into(),
401 })
402 }
403
404 pub fn from_linear_measurements(
405 source: DicomAnnotationContext,
406 semantics: &MeasurementReportSemantics,
407 measurements: &[LinearMeasurementSpec],
408 ) -> Result<Self> {
409 if measurements.is_empty() {
410 return Err(Error::InvalidInput(
411 "measurement report requires at least one linear measurement".into(),
412 ));
413 }
414 let frame_of_reference_uid = source
415 .frame_of_reference_uid()
416 .ok_or_else(|| {
417 Error::InvalidInput(
418 "Comprehensive 3D SR coordinates require a source Frame of Reference UID"
419 .into(),
420 )
421 })?
422 .to_string();
423 let groups = measurements
424 .iter()
425 .map(|spec| {
426 let endpoints = spec.endpoints();
427 let points = endpoints
428 .into_iter()
429 .map(|point| source.pixel_to_slide_coordinate(point.x, point.y))
430 .collect::<Result<Vec<_>>>()?;
431 let delta = Point3::new(
432 points[1].x - points[0].x,
433 points[1].y - points[0].y,
434 points[1].z - points[0].z,
435 );
436 let value = (delta.x * delta.x + delta.y * delta.y + delta.z * delta.z).sqrt();
437 if !value.is_finite() || value <= 0.0 {
438 return Err(Error::InvalidInput(
439 "linear measurement has no positive physical length".into(),
440 ));
441 }
442 let coordinates = SpatialCoordinates {
443 graphic: CoordinateGraphic::Polyline,
444 points,
445 frame_of_reference_uid: frame_of_reference_uid.clone(),
446 };
447 Ok(StructuredReportMeasurementGroup {
448 template_id: "1501",
449 tracking_id: spec.tracking.id().to_string(),
450 tracking_uid: spec.tracking.uid().to_string(),
451 finding_category: spec.finding_category.clone(),
452 finding_type: spec.finding_type.clone(),
453 algorithms: Vec::new(),
454 finding_sites: spec.finding_sites.clone(),
455 reference: RegionReference::MeasurementCoordinates,
456 measurements: vec![StructuredReportMeasurement {
457 concept: semantics.length_concept.clone(),
458 unit: semantics.length_unit.clone(),
459 value,
460 coordinates: vec![coordinates],
461 }],
462 qualitative_evaluations: Vec::new(),
463 })
464 })
465 .collect::<Result<Vec<_>>>()?;
466 Self::new(
467 source,
468 semantics.report_title.clone(),
469 semantics.procedures_reported.clone(),
470 groups,
471 )
472 }
473
474 #[must_use]
476 pub fn with_producer(mut self, producer: DerivedObjectProducer) -> Self {
477 self.producer = producer;
478 self
479 }
480
481 #[must_use]
482 pub fn producer(&self) -> &DerivedObjectProducer {
483 &self.producer
484 }
485
486 #[must_use]
487 pub fn source(&self) -> &DicomAnnotationContext {
488 &self.source
489 }
490
491 pub fn read_sr(
492 path: impl AsRef<Path>,
493 source: &DicomAnnotationContext,
494 segmentation: Option<&SegmentationDocument>,
495 ) -> Result<Self> {
496 reader::read_sr(path.as_ref(), source, segmentation)
497 }
498
499 pub fn write_sr(&self, path: impl AsRef<Path>) -> Result<()> {
500 writer::write_sr(self, path.as_ref())
501 }
502
503 #[must_use]
504 pub fn sop_instance_uid(&self) -> &str {
505 &self.sop_instance_uid
506 }
507
508 #[must_use]
509 pub fn series_instance_uid(&self) -> &str {
510 &self.series_instance_uid
511 }
512
513 #[must_use]
514 pub fn groups(&self) -> &[StructuredReportMeasurementGroup] {
515 &self.groups
516 }
517
518 #[must_use]
519 pub fn report_title(&self) -> &DicomCode {
520 &self.report_title
521 }
522
523 #[must_use]
524 pub fn procedures_reported(&self) -> &[DicomCode] {
525 &self.procedures_reported
526 }
527
528 #[must_use]
529 pub fn completion_flag(&self) -> &str {
530 &self.completion_flag
531 }
532
533 #[must_use]
534 pub fn verification_flag(&self) -> &str {
535 &self.verification_flag
536 }
537
538 #[must_use]
539 pub fn preliminary_flag(&self) -> &str {
540 &self.preliminary_flag
541 }
542}