wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use std::path::Path;

use dicom_core::value::PrimitiveValue;
use dicom_core::{DataElement, VR};
use dicom_dictionary_std::{tags, uids};
use dicom_object::InMemDicomObject;

use crate::{Error, Result};

use super::super::coded_content::{code_item, write_algorithm};
use super::super::derived_object::{
    add_common_instance_reference, build_common_object_from_source, copy_or_empty,
    series_reference_item, sop_reference_item,
};
use super::super::dicom_dataset::{dicom_now, put_optional_text, put_text, sequence};
use super::super::dicom_file::{atomic_write_dicom, ensure_sidecar_destination};
use super::super::model::{AnnotationGroup, AnnotationMeasurement};
use super::geometry_codec::encoded_geometry;
use super::AnnotationDocument;

pub(super) fn write_ann(
    document: &AnnotationDocument,
    path: &Path,
    allow_lossy: bool,
) -> Result<()> {
    ensure_sidecar_destination(path, document.source.source_path())?;
    document.validate()?;
    document.ensure_roundtrip_safe(allow_lossy)?;
    let source_metadata = document.source.source_metadata()?;
    let preserve_volume_identity = document.coordinate_type == "2D"
        && document.pixel_origin_interpretation.as_deref() == Some("VOLUME");
    if preserve_volume_identity {
        document
            .source
            .validate_volume_source_identity(&source_metadata)?;
    }
    let mut object = build_common_object_from_source(
        &source_metadata,
        &document.source,
        uids::MICROSCOPY_BULK_SIMPLE_ANNOTATIONS_STORAGE,
        &document.sop_instance_uid,
        &document.series_instance_uid,
        "ANN",
        &document.producer,
    );
    let (date, time) = dicom_now();
    put_text(&mut object, tags::CONTENT_DATE, VR::DA, &date);
    put_text(&mut object, tags::CONTENT_TIME, VR::TM, &time);
    put_text(
        &mut object,
        tags::CONTENT_LABEL,
        VR::CS,
        &document.content_label,
    );
    put_text(
        &mut object,
        tags::CONTENT_DESCRIPTION,
        VR::LO,
        &document.content_description,
    );
    put_optional_text(
        &mut object,
        tags::CONTENT_CREATOR_NAME,
        VR::PN,
        document.content_creator_name.as_deref(),
    );
    put_text(
        &mut object,
        tags::ANNOTATION_COORDINATE_TYPE,
        VR::CS,
        &document.coordinate_type,
    );
    if document.coordinate_type == "2D" {
        put_text(
            &mut object,
            tags::PIXEL_ORIGIN_INTERPRETATION,
            VR::CS,
            document
                .pixel_origin_interpretation
                .as_deref()
                .ok_or_else(|| {
                    Error::InvalidInput("2D ANN has no Pixel Origin Interpretation".into())
                })?,
        );
        if preserve_volume_identity {
            if document.volume_frame_of_reference_present {
                put_optional_text(
                    &mut object,
                    tags::FRAME_OF_REFERENCE_UID,
                    VR::UI,
                    document.source.frame_of_reference_uid(),
                );
            } else {
                object.take(tags::FRAME_OF_REFERENCE_UID);
            }
            if document.volume_container_identifier_present {
                put_optional_text(
                    &mut object,
                    tags::CONTAINER_IDENTIFIER,
                    VR::LO,
                    document.source.container_identifier(),
                );
            } else {
                object.take(tags::CONTAINER_IDENTIFIER);
            }
        }
    } else if let Some(frame_of_reference_uid) = document.source.frame_of_reference_uid() {
        put_text(
            &mut object,
            tags::FRAME_OF_REFERENCE_UID,
            VR::UI,
            frame_of_reference_uid,
        );
        copy_or_empty(
            &source_metadata,
            &mut object,
            tags::POSITION_REFERENCE_INDICATOR,
            VR::LO,
        );
    } else {
        return Err(Error::InvalidInput(
            "3D ANN source has no Frame of Reference UID".into(),
        ));
    }
    if document.references_source_image {
        let mut reference = sop_reference_item(
            document.source.sop_class_uid(),
            document.source.sop_instance_uid(),
        );
        if let Some(frame) = document.referenced_frame_number {
            put_text(
                &mut reference,
                tags::REFERENCED_FRAME_NUMBER,
                VR::IS,
                &frame.to_string(),
            );
        }
        object.put(sequence(tags::REFERENCED_IMAGE_SEQUENCE, vec![reference]));
    }
    let groups = document
        .groups
        .iter()
        .enumerate()
        .map(|(index, group)| write_group(index, group, document.coordinate_type == "3D"))
        .collect::<Result<Vec<_>>>()?;
    object.put(sequence(tags::ANNOTATION_GROUP_SEQUENCE, groups));
    if document.references_source_image {
        add_common_instance_reference(&mut object, &document.source);
    }

    if let Some(predecessor_uid) = &document.predecessor_sop_instance_uid {
        let mut referenced_series = Vec::new();
        if document.references_source_image {
            referenced_series.push(series_reference_item(
                document.source.series_instance_uid(),
                document.source.sop_class_uid(),
                document.source.sop_instance_uid(),
            ));
        }
        referenced_series.push(series_reference_item(
            &document.series_instance_uid,
            uids::MICROSCOPY_BULK_SIMPLE_ANNOTATIONS_STORAGE,
            predecessor_uid,
        ));
        object.put(sequence(
            tags::REFERENCED_SERIES_SEQUENCE,
            referenced_series,
        ));
    }
    atomic_write_dicom(
        path,
        object,
        uids::MICROSCOPY_BULK_SIMPLE_ANNOTATIONS_STORAGE,
        &document.sop_instance_uid,
    )
}

fn write_group(
    index: usize,
    group: &AnnotationGroup,
    three_dimensional: bool,
) -> Result<InMemDicomObject> {
    let group_number = u16::try_from(index + 1).map_err(|_| {
        Error::InvalidInput("annotation group number exceeds DICOM US range".into())
    })?;
    let annotation_count = u32::try_from(group.geometry().annotation_count())
        .map_err(|_| Error::InvalidInput("annotation count exceeds DICOM UL range".into()))?;
    let mut item = InMemDicomObject::new_empty();
    item.put(DataElement::new(
        tags::ANNOTATION_GROUP_NUMBER,
        VR::US,
        PrimitiveValue::from(group_number),
    ));
    put_text(&mut item, tags::ANNOTATION_GROUP_UID, VR::UI, group.uid());
    put_text(
        &mut item,
        tags::ANNOTATION_GROUP_LABEL,
        VR::LO,
        group.label(),
    );
    if !group.description().is_empty() {
        put_text(
            &mut item,
            tags::ANNOTATION_GROUP_DESCRIPTION,
            VR::UT,
            group.description(),
        );
    }
    put_text(
        &mut item,
        tags::ANNOTATION_GROUP_GENERATION_TYPE,
        VR::CS,
        group.generation_type().dicom_value(),
    );
    if !group.algorithms().is_empty() {
        item.put(sequence(
            tags::ANNOTATION_GROUP_ALGORITHM_IDENTIFICATION_SEQUENCE,
            group.algorithms().iter().map(write_algorithm).collect(),
        ));
    }
    if !group.anatomic_regions().is_empty() {
        item.put(sequence(
            tags::ANATOMIC_REGION_SEQUENCE,
            group.anatomic_regions().iter().map(code_item).collect(),
        ));
    }
    if !group.primary_anatomic_structures().is_empty() {
        item.put(sequence(
            tags::PRIMARY_ANATOMIC_STRUCTURE_SEQUENCE,
            group
                .primary_anatomic_structures()
                .iter()
                .map(code_item)
                .collect(),
        ));
    }
    item.put(sequence(
        tags::ANNOTATION_PROPERTY_CATEGORY_CODE_SEQUENCE,
        vec![code_item(group.category())],
    ));
    item.put(sequence(
        tags::ANNOTATION_PROPERTY_TYPE_CODE_SEQUENCE,
        vec![code_item(group.property_type())],
    ));
    if !group.property_type_modifiers().is_empty() {
        item.put(sequence(
            tags::ANNOTATION_PROPERTY_TYPE_MODIFIER_CODE_SEQUENCE,
            group
                .property_type_modifiers()
                .iter()
                .map(code_item)
                .collect(),
        ));
    }
    item.put(DataElement::new(
        tags::NUMBER_OF_ANNOTATIONS,
        VR::UL,
        PrimitiveValue::from(annotation_count),
    ));
    put_text(
        &mut item,
        tags::GRAPHIC_TYPE,
        VR::CS,
        group.geometry().graphic_type().dicom_value(),
    );
    put_text(
        &mut item,
        tags::ANNOTATION_APPLIES_TO_ALL_OPTICAL_PATHS,
        VR::CS,
        if group.applies_to_all_optical_paths() {
            "YES"
        } else {
            "NO"
        },
    );
    if !group.applies_to_all_optical_paths() {
        put_text(
            &mut item,
            tags::REFERENCED_OPTICAL_PATH_IDENTIFIER,
            VR::SH,
            &group.referenced_optical_paths().join("\\"),
        );
    }
    if three_dimensional {
        put_text(
            &mut item,
            tags::ANNOTATION_APPLIES_TO_ALL_Z_PLANES,
            VR::CS,
            if group.applies_to_all_z_planes() {
                "YES"
            } else {
                "NO"
            },
        );
        if !group.common_z_coordinates().is_empty() {
            item.put(DataElement::new(
                tags::COMMON_Z_COORDINATE_VALUE,
                VR::FD,
                PrimitiveValue::F64(group.common_z_coordinates().to_vec().into()),
            ));
        }
    }
    item.put(DataElement::new(
        tags::RECOMMENDED_DISPLAY_CIE_LAB_VALUE,
        VR::US,
        PrimitiveValue::from(group.recommended_display_cielab()),
    ));

    let (coordinates, indices) = encoded_geometry(group.geometry())?;
    item.put(DataElement::new(
        tags::DOUBLE_POINT_COORDINATES_DATA,
        VR::OD,
        PrimitiveValue::F64(coordinates.into()),
    ));
    if let Some(indices) = indices {
        item.put(DataElement::new(
            tags::LONG_PRIMITIVE_POINT_INDEX_LIST,
            VR::OL,
            PrimitiveValue::U32(indices.into()),
        ));
    }
    if !group.measurements().is_empty() {
        item.put(sequence(
            tags::MEASUREMENTS_SEQUENCE,
            group.measurements().iter().map(write_measurement).collect(),
        ));
    }
    Ok(item)
}

fn write_measurement(measurement: &AnnotationMeasurement) -> InMemDicomObject {
    let mut item = InMemDicomObject::new_empty();
    item.put(sequence(
        tags::CONCEPT_NAME_CODE_SEQUENCE,
        vec![code_item(measurement.concept())],
    ));
    item.put(sequence(
        tags::MEASUREMENT_UNITS_CODE_SEQUENCE,
        vec![code_item(measurement.units())],
    ));
    let mut values_item = InMemDicomObject::new_empty();
    values_item.put(DataElement::new(
        tags::FLOATING_POINT_VALUES,
        VR::OF,
        PrimitiveValue::F32(
            measurement
                .values()
                .iter()
                .map(|value| *value as f32)
                .collect::<Vec<_>>()
                .into(),
        ),
    ));
    if let Some(indices) = measurement.annotation_indices() {
        values_item.put(DataElement::new(
            tags::ANNOTATION_INDEX_LIST,
            VR::OL,
            PrimitiveValue::U32(indices.to_vec().into()),
        ));
    }
    item.put(sequence(
        tags::MEASUREMENT_VALUES_SEQUENCE,
        vec![values_item],
    ));
    item
}