wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use dicom_core::{Tag, VR};
use dicom_dictionary_std::tags;
use dicom_object::InMemDicomObject;

use crate::{Error, Result};

use super::dicom_dataset::{
    optional_string, put_optional_text, put_text, required_string, sequence, sequence_items,
};
use super::model::{
    AlgorithmIdentification, DiagnosticDisposition, DiagnosticSeverity, DicomCode,
    DicomCodeQualifiers, DicomCodeValueKind, InteroperabilityDiagnostic,
};

pub(crate) fn code_item(code: &DicomCode) -> InMemDicomObject {
    let mut item = InMemDicomObject::new_empty();
    let (value_tag, value_vr) = match code.value_kind() {
        DicomCodeValueKind::Short => (tags::CODE_VALUE, VR::SH),
        DicomCodeValueKind::Long => (tags::LONG_CODE_VALUE, VR::UC),
        DicomCodeValueKind::Urn => (tags::URN_CODE_VALUE, VR::UR),
    };
    put_text(&mut item, value_tag, value_vr, code.value());
    if !code.scheme().is_empty() {
        put_text(
            &mut item,
            tags::CODING_SCHEME_DESIGNATOR,
            VR::SH,
            code.scheme(),
        );
    }
    put_optional_text(
        &mut item,
        tags::CODING_SCHEME_VERSION,
        VR::SH,
        code.coding_scheme_version(),
    );
    put_text(&mut item, tags::CODE_MEANING, VR::LO, code.meaning());
    put_optional_text(
        &mut item,
        tags::CONTEXT_IDENTIFIER,
        VR::CS,
        code.context_identifier(),
    );
    put_optional_text(&mut item, tags::CONTEXT_UID, VR::UI, code.context_uid());
    put_optional_text(
        &mut item,
        tags::MAPPING_RESOURCE,
        VR::CS,
        code.mapping_resource(),
    );
    put_optional_text(
        &mut item,
        tags::MAPPING_RESOURCE_UID,
        VR::UI,
        code.mapping_resource_uid(),
    );
    put_optional_text(
        &mut item,
        tags::CONTEXT_GROUP_VERSION,
        VR::DT,
        code.context_group_version(),
    );
    put_optional_text(
        &mut item,
        tags::CONTEXT_GROUP_LOCAL_VERSION,
        VR::DT,
        code.context_group_local_version(),
    );
    if let Some(extension) = code.context_group_extension() {
        put_text(
            &mut item,
            tags::CONTEXT_GROUP_EXTENSION_FLAG,
            VR::CS,
            if extension { "Y" } else { "N" },
        );
    }
    put_optional_text(
        &mut item,
        tags::CONTEXT_GROUP_EXTENSION_CREATOR_UID,
        VR::UI,
        code.context_group_extension_creator_uid(),
    );
    item
}

pub(crate) fn read_code_at(
    object: &InMemDicomObject,
    tag: Tag,
    path: &str,
    diagnostics: &mut Vec<InteroperabilityDiagnostic>,
) -> Result<DicomCode> {
    let items = sequence_items(object, tag)?;
    if items.len() != 1 {
        return Err(Error::InvalidInput(format!(
            "code sequence {tag} must contain exactly one item"
        )));
    }
    read_code_item(&items[0], path, diagnostics)
}

pub(crate) fn read_codes_at(
    object: &InMemDicomObject,
    tag: Tag,
    path: &str,
    diagnostics: &mut Vec<InteroperabilityDiagnostic>,
) -> Result<Vec<DicomCode>> {
    let Some(element) = object.get(tag) else {
        return Ok(Vec::new());
    };
    let items = element
        .items()
        .ok_or_else(|| Error::InvalidInput(format!("{path} is present but is not a sequence")))?;
    items
        .iter()
        .enumerate()
        .map(|(index, item)| read_code_item(item, &format!("{path}[{index}]"), diagnostics))
        .collect()
}

fn read_code_item(
    item: &InMemDicomObject,
    path: &str,
    diagnostics: &mut Vec<InteroperabilityDiagnostic>,
) -> Result<DicomCode> {
    let candidates = [
        (tags::CODE_VALUE, DicomCodeValueKind::Short, false),
        (tags::LONG_CODE_VALUE, DicomCodeValueKind::Long, false),
        (tags::URN_CODE_VALUE, DicomCodeValueKind::Urn, false),
        (tags::EXTENDED_CODE_VALUE, DicomCodeValueKind::Long, true),
    ]
    .into_iter()
    .filter_map(|(tag, kind, normalized)| {
        optional_string(item, tag).map(|value| (value, kind, normalized))
    })
    .collect::<Vec<_>>();
    if candidates.len() != 1 {
        return Err(Error::InvalidInput(format!(
            "{path} must contain exactly one code value representation"
        )));
    }
    let (value, kind, normalized) = &candidates[0];
    if *normalized {
        diagnostics.push(InteroperabilityDiagnostic::new(
            "EXTENDED_CODE_VALUE_NORMALIZED",
            DiagnosticSeverity::Info,
            path,
            DiagnosticDisposition::Normalized,
            "retired Extended Code Value will be written as Long Code Value",
        ));
    }
    for (tag, keyword, code) in [
        (
            tags::EQUIVALENT_CODE_SEQUENCE,
            "EquivalentCodeSequence",
            "EQUIVALENT_CODE_SEQUENCE_WOULD_DROP",
        ),
        (
            tags::CONTEXT_GROUP_IDENTIFICATION_SEQUENCE,
            "ContextGroupIdentificationSequence",
            "CONTEXT_GROUP_IDENTIFICATION_WOULD_DROP",
        ),
        (
            tags::MAPPING_RESOURCE_IDENTIFICATION_SEQUENCE,
            "MappingResourceIdentificationSequence",
            "MAPPING_RESOURCE_IDENTIFICATION_WOULD_DROP",
        ),
        (
            tags::EXTENDED_CODE_MEANING,
            "ExtendedCodeMeaning",
            "EXTENDED_CODE_MEANING_WOULD_DROP",
        ),
        (
            tags::ANATOMIC_REGION_MODIFIER_SEQUENCE,
            "AnatomicRegionModifierSequence",
            "ANATOMIC_REGION_MODIFIER_WOULD_DROP",
        ),
        (
            tags::PRIMARY_ANATOMIC_STRUCTURE_MODIFIER_SEQUENCE,
            "PrimaryAnatomicStructureModifierSequence",
            "PRIMARY_ANATOMIC_STRUCTURE_MODIFIER_WOULD_DROP",
        ),
    ] {
        if item.get(tag).is_some() {
            diagnostics.push(InteroperabilityDiagnostic::new(
                code,
                DiagnosticSeverity::Warning,
                format!("{path}.{keyword}"),
                DiagnosticDisposition::WouldDrop,
                format!("{keyword} is outside the preserved semantic allowlist"),
            ));
        }
    }
    let scheme = optional_string(item, tags::CODING_SCHEME_DESIGNATOR).unwrap_or_default();
    let code = match kind {
        DicomCodeValueKind::Short => DicomCode::new(
            value.clone(),
            scheme,
            required_string(item, tags::CODE_MEANING)?,
        )?,
        DicomCodeValueKind::Long => DicomCode::new_long(
            value.clone(),
            scheme,
            required_string(item, tags::CODE_MEANING)?,
        )?,
        DicomCodeValueKind::Urn => DicomCode::new_urn(
            value.clone(),
            scheme,
            required_string(item, tags::CODE_MEANING)?,
        )?,
    };
    let extension = optional_string(item, tags::CONTEXT_GROUP_EXTENSION_FLAG)
        .map(|value| match value.as_str() {
            "Y" => Ok(true),
            "N" => Ok(false),
            _ => Err(Error::InvalidInput(format!(
                "{path}.ContextGroupExtensionFlag is not Y or N"
            ))),
        })
        .transpose()?;
    code.with_optional_qualifiers(DicomCodeQualifiers {
        coding_scheme_version: optional_string(item, tags::CODING_SCHEME_VERSION),
        context_identifier: optional_string(item, tags::CONTEXT_IDENTIFIER),
        context_uid: optional_string(item, tags::CONTEXT_UID),
        mapping_resource: optional_string(item, tags::MAPPING_RESOURCE),
        mapping_resource_uid: optional_string(item, tags::MAPPING_RESOURCE_UID),
        context_group_version: optional_string(item, tags::CONTEXT_GROUP_VERSION),
        context_group_local_version: optional_string(item, tags::CONTEXT_GROUP_LOCAL_VERSION),
        context_group_extension: extension,
        context_group_extension_creator_uid: optional_string(
            item,
            tags::CONTEXT_GROUP_EXTENSION_CREATOR_UID,
        ),
    })
}

pub(crate) fn write_algorithm(algorithm: &AlgorithmIdentification) -> InMemDicomObject {
    let mut item = InMemDicomObject::new_empty();
    item.put(sequence(
        tags::ALGORITHM_FAMILY_CODE_SEQUENCE,
        vec![code_item(algorithm.family())],
    ));
    if let Some(name_code) = algorithm.name_code() {
        item.put(sequence(
            tags::ALGORITHM_NAME_CODE_SEQUENCE,
            vec![code_item(name_code)],
        ));
    }
    put_text(&mut item, tags::ALGORITHM_NAME, VR::LO, algorithm.name());
    put_text(
        &mut item,
        tags::ALGORITHM_VERSION,
        VR::LO,
        algorithm.version(),
    );
    put_optional_text(
        &mut item,
        tags::ALGORITHM_PARAMETERS,
        VR::LT,
        algorithm.parameters(),
    );
    put_optional_text(
        &mut item,
        tags::ALGORITHM_SOURCE,
        VR::LO,
        algorithm.source(),
    );
    item
}

pub(crate) fn read_algorithms(
    object: &InMemDicomObject,
    tag: Tag,
    path: &str,
    diagnostics: &mut Vec<InteroperabilityDiagnostic>,
) -> Result<Vec<AlgorithmIdentification>> {
    let Some(element) = object.get(tag) else {
        return Ok(Vec::new());
    };
    let items = element
        .items()
        .ok_or_else(|| Error::InvalidInput(format!("{path} is present but is not a sequence")))?;
    items
        .iter()
        .enumerate()
        .map(|(index, item)| {
            let path = format!("{path}[{index}]");
            let family = read_code_at(
                item,
                tags::ALGORITHM_FAMILY_CODE_SEQUENCE,
                &format!("{path}.AlgorithmFamilyCodeSequence"),
                diagnostics,
            )?;
            let mut algorithm = AlgorithmIdentification::new(
                family,
                required_string(item, tags::ALGORITHM_NAME)?,
                required_string(item, tags::ALGORITHM_VERSION)?,
            )?;
            if item.get(tags::ALGORITHM_NAME_CODE_SEQUENCE).is_some() {
                algorithm = algorithm.with_name_code(read_code_at(
                    item,
                    tags::ALGORITHM_NAME_CODE_SEQUENCE,
                    &format!("{path}.AlgorithmNameCodeSequence"),
                    diagnostics,
                )?);
            }
            if let Some(parameters) = optional_string(item, tags::ALGORITHM_PARAMETERS) {
                algorithm = algorithm.with_parameters(parameters)?;
            }
            if let Some(source) = optional_string(item, tags::ALGORITHM_SOURCE) {
                algorithm = algorithm.with_source(source)?;
            }
            Ok(algorithm)
        })
        .collect()
}