wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use dicom_dictionary_std::tags;

use crate::test_support::write_source_wsi;
use crate::{
    AnnotationDocument, AnnotationGroup, DerivedObjectProducer, DicomAnnotationContext, DicomCode,
    LinearMeasurementSpec, MeasurementReportSemantics, Point2, SegmentationDocument,
    SegmentationSegment, StructuredReportDocument, TrackingIdentity,
};

fn code(value: &str, meaning: &str) -> DicomCode {
    DicomCode::new(value, "99FRAMES", meaning).unwrap()
}

fn producer(series_number: i32) -> DerivedObjectProducer {
    DerivedObjectProducer::new(
        series_number,
        "Acme Pathology",
        "Acme Slide Workstation",
        "ACME-42",
        "7.3.1",
    )
    .unwrap()
    .with_series_description("Acme derived pathology objects")
    .unwrap()
}

fn assert_producer(path: &std::path::Path, expected_series: &str) {
    let object = dicom_object::open_file(path).unwrap();
    for (tag, expected) in [
        (tags::SERIES_NUMBER, expected_series),
        (tags::SERIES_DESCRIPTION, "Acme derived pathology objects"),
        (tags::MANUFACTURER, "Acme Pathology"),
        (tags::MANUFACTURER_MODEL_NAME, "Acme Slide Workstation"),
        (tags::DEVICE_SERIAL_NUMBER, "ACME-42"),
        (tags::SOFTWARE_VERSIONS, "7.3.1"),
    ] {
        assert_eq!(
            object.element(tag).unwrap().to_str().unwrap().trim(),
            expected
        );
    }
}

#[test]
fn caller_owned_producer_is_written_by_ann_seg_and_sr() {
    let directory = tempfile::tempdir().unwrap();
    let source_path = directory.path().join("source.dcm");
    write_source_wsi(&source_path, 16, 12, 4, 4);
    let source = DicomAnnotationContext::from_source(&source_path).unwrap();
    let category = code("MORPH", "Morphology");
    let property = code("TUMOR", "Tumor");

    let ann_path = directory.path().join("annotations.dcm");
    AnnotationDocument::new(
        source.clone(),
        vec![AnnotationGroup::points(
            "Cells",
            category.clone(),
            property.clone(),
            [1, 2, 3],
            vec![Point2::new(1.0, 1.0)],
        )
        .unwrap()],
    )
    .unwrap()
    .with_producer(producer(41))
    .write_ann(&ann_path)
    .unwrap();
    assert_producer(&ann_path, "41");

    let seg_path = directory.path().join("segmentation.dcm");
    SegmentationDocument::binary(
        source.clone(),
        vec![SegmentationSegment::new(
            "Tumor",
            category,
            property,
            [1, 2, 3],
            vec![vec![
                Point2::new(0.0, 0.0),
                Point2::new(4.0, 0.0),
                Point2::new(4.0, 4.0),
                Point2::new(0.0, 4.0),
            ]],
            Vec::new(),
        )
        .unwrap()],
    )
    .unwrap()
    .with_producer(producer(42))
    .write_seg(&seg_path)
    .unwrap();
    assert_producer(&seg_path, "42");

    let sr_path = directory.path().join("measurements.dcm");
    let measurement = LinearMeasurementSpec::new(
        TrackingIdentity::new("length-1", "2.25.42").unwrap(),
        code("MORPH", "Morphology"),
        code("TUMOR", "Tumor"),
        Point2::new(1.0, 1.0),
        Point2::new(3.0, 1.0),
    )
    .unwrap();
    StructuredReportDocument::from_linear_measurements(
        source,
        &MeasurementReportSemantics::pathology_v1(),
        &[measurement],
    )
    .unwrap()
    .with_producer(producer(43))
    .write_sr(&sr_path)
    .unwrap();
    assert_producer(&sr_path, "43");

    let rewritten_sr = directory.path().join("measurements-rewritten.dcm");
    StructuredReportDocument::read_sr(
        &sr_path,
        &DicomAnnotationContext::from_source(&source_path).unwrap(),
        None,
    )
    .unwrap()
    .write_sr(&rewritten_sr)
    .unwrap();
    assert_producer(&rewritten_sr, "43");
}

#[test]
fn producer_metadata_rejects_invalid_dicom_text() {
    assert!(DerivedObjectProducer::new(1, "Acme\\Other", "Model", "Serial", "1.0").is_err());
    assert!(DerivedObjectProducer::new(1, "Acme", "Model\0bad", "Serial", "1.0").is_err());
    assert!(producer(1).with_series_description("").is_err());
}

#[test]
fn generic_default_identifies_the_library_not_frames_dicom_viewer() {
    let directory = tempfile::tempdir().unwrap();
    let source_path = directory.path().join("source.dcm");
    let output_path = directory.path().join("annotations.dcm");
    write_source_wsi(&source_path, 16, 12, 4, 4);
    let source = DicomAnnotationContext::from_source(&source_path).unwrap();
    AnnotationDocument::new(
        source,
        vec![AnnotationGroup::points(
            "Cells",
            code("MORPH", "Morphology"),
            code("CELL", "Cell"),
            [1, 2, 3],
            vec![Point2::new(1.0, 1.0)],
        )
        .unwrap()],
    )
    .unwrap()
    .write_ann(&output_path)
    .unwrap();

    let object = dicom_object::open_file(output_path).unwrap();
    assert_ne!(
        object
            .element(tags::MANUFACTURER_MODEL_NAME)
            .unwrap()
            .to_str()
            .unwrap()
            .trim(),
        "DICOM Viewer"
    );
    assert_eq!(
        object
            .element(tags::MANUFACTURER_MODEL_NAME)
            .unwrap()
            .to_str()
            .unwrap()
            .trim(),
        "wsi-dicom-annotations"
    );
}