wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use super::*;
use crate::annotations::dicom_dataset::sequence;
use dicom_core::value::PrimitiveValue;
use dicom_core::{dicom_value, DataElement, VR};
use dicom_object::{FileMetaTableBuilder, InMemDicomObject};

fn empty_file_object() -> DefaultDicomObject {
    InMemDicomObject::new_empty()
        .with_meta(
            FileMetaTableBuilder::new()
                .media_storage_sop_class_uid(uids::VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE)
                .media_storage_sop_instance_uid("2.25.999")
                .transfer_syntax(uids::EXPLICIT_VR_LITTLE_ENDIAN),
        )
        .expect("test file metadata should be valid")
}

#[test]
fn context_projects_level_zero_slide_and_tiled_full_frame_coordinates() {
    let directory = tempfile::tempdir().expect("temporary directory should be created");
    let source = directory.path().join("source.dcm");
    crate::test_support::write_source_wsi(&source, 16, 12, 4, 4);
    let context = DicomAnnotationContext::from_source(&source)
        .expect("synthetic VL WSI context should parse");

    assert_eq!(context.source_path(), source);
    assert_eq!(
        context.sop_class_uid(),
        uids::VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE
    );
    assert_eq!(context.sop_instance_uid(), "1.2.826.0.1.3680043.10.777.101");
    assert_eq!(
        context.series_instance_uid(),
        "1.2.826.0.1.3680043.10.777.102"
    );
    assert_eq!(
        context.study_instance_uid(),
        "1.2.826.0.1.3680043.10.777.103"
    );
    assert_eq!(
        context.frame_of_reference_uid(),
        Some("1.2.826.0.1.3680043.10.777.104")
    );
    assert_eq!(context.total_pixel_matrix_dimensions(), (16, 12));
    assert_eq!(context.tile_dimensions(), (4, 4));
    assert_eq!(context.pixel_spacing(), Some([0.00025, 0.00025]));
    assert_eq!(context.slice_thickness(), Some(0.001));
    assert_eq!(
        context.image_orientation_slide(),
        Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0])
    );
    assert_eq!(context.total_pixel_matrix_origin(), Some([0.0, 0.0, 0.0]));

    let slide = context
        .pixel_to_slide_coordinate(4.0, 8.0)
        .expect("in-bounds pixel should project to slide coordinates");
    assert_eq!([slide.x, slide.y, slide.z], [0.001, 0.002, 0.0]);
    assert_eq!(
        context
            .slide_coordinate_to_pixel3(slide.x, slide.y, slide.z)
            .expect("slide coordinate should invert"),
        Point2::new(4.0, 8.0)
    );
    assert_eq!(
        context
            .slide_coordinate_to_pixel(slide.x, slide.y)
            .expect("planar slide coordinate should invert"),
        Point2::new(4.0, 8.0)
    );
    assert_eq!(
        context
            .frame_coordinate_to_total_pixel(2, 1.5, 2.5)
            .expect("second tiled-full frame should project"),
        Point2::new(5.5, 2.5)
    );
    assert!(context.source_metadata().is_ok());
    assert!(context.require_shared_slide_frame(&context).is_ok());

    assert!(context.pixel_to_slide_coordinate(f64::NAN, 0.0).is_err());
    assert!(context.pixel_to_slide_coordinate(17.0, 0.0).is_err());
    assert!(context
        .slide_coordinate_to_pixel3(f64::INFINITY, 0.0, 0.0)
        .is_err());
    assert!(context
        .frame_coordinate_to_total_pixel(0, 0.0, 0.0)
        .is_err());
    assert!(context
        .frame_coordinate_to_total_pixel(99, 0.0, 0.0)
        .is_err());

    let mut different_study = context.clone();
    different_study.study_instance_uid = "2.25.1".into();
    assert!(context
        .require_shared_slide_frame(&different_study)
        .is_err());
    let mut missing_frame = context.clone();
    missing_frame.frame_of_reference_uid = None;
    assert!(context.require_shared_slide_frame(&missing_frame).is_err());
}

#[test]
fn coordinate_projection_reports_each_missing_source_geometry_attribute() {
    let directory = tempfile::tempdir().unwrap();
    let source = directory.path().join("source.dcm");
    crate::test_support::write_source_wsi(&source, 16, 12, 4, 4);
    let context = DicomAnnotationContext::from_source(&source).unwrap();

    let mut missing_origin = context.clone();
    missing_origin.total_pixel_matrix_origin = None;
    assert!(missing_origin.slide_coordinate_to_pixel(0.0, 0.0).is_err());
    assert!(missing_origin
        .slide_coordinate_to_pixel3(0.0, 0.0, 0.0)
        .is_err());
    assert!(missing_origin.pixel_to_slide_coordinate(0.0, 0.0).is_err());

    let mut missing_orientation = context.clone();
    missing_orientation.image_orientation_slide = None;
    assert!(missing_orientation
        .slide_coordinate_to_pixel3(0.0, 0.0, 0.0)
        .is_err());
    assert!(missing_orientation
        .pixel_to_slide_coordinate(0.0, 0.0)
        .is_err());

    let mut missing_spacing = context.clone();
    missing_spacing.pixel_spacing = None;
    assert!(missing_spacing
        .slide_coordinate_to_pixel3(0.0, 0.0, 0.0)
        .is_err());
    assert!(missing_spacing.pixel_to_slide_coordinate(0.0, 0.0).is_err());

    let mut missing_frames = context.clone();
    missing_frames.number_of_frames = None;
    assert!(missing_frames
        .frame_coordinate_to_total_pixel(1, 0.0, 0.0)
        .is_err());

    let mut missing_sparse_origin = context;
    missing_sparse_origin.dimension_organization_type = Some("TILED_SPARSE".into());
    missing_sparse_origin.sparse_frame_origins = None;
    assert!(missing_sparse_origin
        .frame_coordinate_to_total_pixel(1, 0.0, 0.0)
        .is_err());
}

#[test]
fn context_uses_shared_pixel_measures_when_top_level_values_are_absent() {
    let directory = tempfile::tempdir().unwrap();
    let source = directory.path().join("source.dcm");
    crate::test_support::write_source_wsi(&source, 16, 12, 4, 4);
    let mut object = dicom_object::open_file(&source).unwrap();
    object.take(tags::PIXEL_SPACING).unwrap();
    object.take(tags::SLICE_THICKNESS).unwrap();

    let mut measures = InMemDicomObject::new_empty();
    measures.put(DataElement::new(
        tags::PIXEL_SPACING,
        VR::DS,
        dicom_value!(F64, [0.5, 0.25]),
    ));
    measures.put(DataElement::new(tags::SLICE_THICKNESS, VR::DS, "0.01"));
    let mut shared = InMemDicomObject::new_empty();
    shared.put(sequence(tags::PIXEL_MEASURES_SEQUENCE, vec![measures]));
    object.put(sequence(
        tags::SHARED_FUNCTIONAL_GROUPS_SEQUENCE,
        vec![shared],
    ));
    object.write_to_file(&source).unwrap();

    let context = DicomAnnotationContext::from_source(&source).unwrap();
    assert_eq!(context.pixel_spacing(), Some([0.5, 0.25]));
    assert_eq!(context.slice_thickness(), Some(0.01));
}

#[test]
fn context_retains_declared_optical_path_identifiers() {
    let directory = tempfile::tempdir().unwrap();
    let source = directory.path().join("source.dcm");
    crate::test_support::write_source_wsi(&source, 16, 12, 4, 4);
    let mut object = dicom_object::open_file(&source).unwrap();
    let paths = ["BRIGHTFIELD", "FLUORESCENCE"]
        .into_iter()
        .map(|identifier| {
            let mut item = InMemDicomObject::new_empty();
            item.put(DataElement::new(
                tags::OPTICAL_PATH_IDENTIFIER,
                VR::SH,
                identifier,
            ));
            item
        })
        .collect();
    object.put(sequence(tags::OPTICAL_PATH_SEQUENCE, paths));
    object.put(DataElement::new(
        tags::NUMBER_OF_OPTICAL_PATHS,
        VR::UL,
        PrimitiveValue::from(2_u32),
    ));
    object.write_to_file(&source).unwrap();

    let context = DicomAnnotationContext::from_source(&source).unwrap();
    assert_eq!(
        context.optical_path_identifiers(),
        &["BRIGHTFIELD", "FLUORESCENCE"]
    );
}

#[test]
fn context_attribute_helpers_accept_only_exact_finite_shapes() {
    let mut object = empty_file_object();
    assert!(required_string(&object, tags::SOP_INSTANCE_UID).is_err());
    assert!(required_u32(&object, tags::NUMBER_OF_FRAMES).is_err());
    assert!(required_u16(&object, tags::ROWS).is_err());
    assert!(optional_pair(&object, tags::PIXEL_SPACING).is_none());
    assert!(optional_positive_float(&object, tags::SLICE_THICKNESS).is_none());
    assert!(optional_six(&object, tags::IMAGE_ORIENTATION_SLIDE).is_none());
    assert!(optional_origin(&object).is_none());
    assert!(optional_u32(&object, tags::NUMBER_OF_FRAMES).is_none());

    object.put(DataElement::new(tags::SOP_INSTANCE_UID, VR::UI, "2.25.10"));
    object.put(DataElement::new(
        tags::NUMBER_OF_FRAMES,
        VR::UL,
        PrimitiveValue::from(5_u32),
    ));
    object.put(DataElement::new(
        tags::ROWS,
        VR::US,
        PrimitiveValue::from(4_u16),
    ));
    object.put(DataElement::new(
        tags::PIXEL_SPACING,
        VR::DS,
        dicom_value!(F64, [0.5, 0.25]),
    ));
    object.put(DataElement::new(tags::SLICE_THICKNESS, VR::DS, "0.001"));
    object.put(DataElement::new(
        tags::IMAGE_ORIENTATION_SLIDE,
        VR::DS,
        dicom_value!(F64, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]),
    ));
    let mut origin = InMemDicomObject::new_empty();
    origin.put(DataElement::new(
        tags::X_OFFSET_IN_SLIDE_COORDINATE_SYSTEM,
        VR::DS,
        "1.5",
    ));
    origin.put(DataElement::new(
        tags::Y_OFFSET_IN_SLIDE_COORDINATE_SYSTEM,
        VR::DS,
        "2.5",
    ));
    origin.put(DataElement::new(
        tags::Z_OFFSET_IN_SLIDE_COORDINATE_SYSTEM,
        VR::DS,
        "3.5",
    ));
    object.put(sequence(
        tags::TOTAL_PIXEL_MATRIX_ORIGIN_SEQUENCE,
        vec![origin],
    ));

    assert_eq!(
        required_string(&object, tags::SOP_INSTANCE_UID).unwrap(),
        "2.25.10"
    );
    assert_eq!(required_u32(&object, tags::NUMBER_OF_FRAMES).unwrap(), 5);
    assert_eq!(required_u16(&object, tags::ROWS).unwrap(), 4);
    assert_eq!(
        optional_pair(&object, tags::PIXEL_SPACING),
        Some([0.5, 0.25])
    );
    assert_eq!(
        optional_positive_float(&object, tags::SLICE_THICKNESS),
        Some(0.001)
    );
    assert_eq!(
        optional_six(&object, tags::IMAGE_ORIENTATION_SLIDE),
        Some([1.0, 0.0, 0.0, 0.0, 1.0, 0.0])
    );
    assert_eq!(optional_origin(&object), Some([1.5, 2.5, 3.5]));
    assert_eq!(optional_u32(&object, tags::NUMBER_OF_FRAMES), Some(5));

    object.put(DataElement::new(
        tags::PIXEL_SPACING,
        VR::DS,
        dicom_value!(F64, [0.0, 0.25]),
    ));
    object.put(DataElement::new(tags::SLICE_THICKNESS, VR::DS, "-1"));
    object.put(DataElement::new(
        tags::IMAGE_ORIENTATION_SLIDE,
        VR::DS,
        dicom_value!(F64, [1.0, 0.0]),
    ));
    assert!(optional_pair(&object, tags::PIXEL_SPACING).is_none());
    assert!(optional_positive_float(&object, tags::SLICE_THICKNESS).is_none());
    assert!(optional_six(&object, tags::IMAGE_ORIENTATION_SLIDE).is_none());
}

#[test]
fn shared_pixel_measures_and_sparse_frame_origins_validate_nested_sequences() {
    let mut measures = InMemDicomObject::new_empty();
    measures.put(DataElement::new(
        tags::PIXEL_SPACING,
        VR::DS,
        dicom_value!(F64, [0.5, 0.25]),
    ));
    measures.put(DataElement::new(tags::SLICE_THICKNESS, VR::DS, "0.01"));
    let mut shared = InMemDicomObject::new_empty();
    shared.put(sequence(tags::PIXEL_MEASURES_SEQUENCE, vec![measures]));
    let mut object = empty_file_object();
    object.put(sequence(
        tags::SHARED_FUNCTIONAL_GROUPS_SEQUENCE,
        vec![shared],
    ));
    assert_eq!(
        shared_pixel_measures_pair(&object, tags::PIXEL_SPACING),
        Some([0.5, 0.25])
    );
    assert_eq!(
        shared_pixel_measures_float(&object, tags::SLICE_THICKNESS),
        Some(0.01)
    );
    assert!(shared_pixel_measures(&object).is_some());
    assert!(read_sparse_frame_origins(&object, Some("TILED_FULL"))
        .unwrap()
        .is_none());
    assert!(read_sparse_frame_origins(&object, Some("TILED_SPARSE")).is_err());

    object.put(DataElement::new(tags::NUMBER_OF_FRAMES, VR::IS, "1"));
    let mut frame = InMemDicomObject::new_empty();
    object.put(sequence(
        tags::PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE,
        vec![frame.clone()],
    ));
    assert!(read_sparse_frame_origins(&object, Some("TILED_SPARSE")).is_err());

    let mut plane = InMemDicomObject::new_empty();
    plane.put(DataElement::new(
        tags::ROW_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX,
        VR::SL,
        PrimitiveValue::from(9_i32),
    ));
    frame.put(sequence(
        tags::PLANE_POSITION_SLIDE_SEQUENCE,
        vec![plane.clone()],
    ));
    object.put(sequence(
        tags::PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE,
        vec![frame.clone()],
    ));
    assert!(read_sparse_frame_origins(&object, Some("TILED_SPARSE")).is_err());

    plane.put(DataElement::new(
        tags::COLUMN_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX,
        VR::SL,
        PrimitiveValue::from(5_i32),
    ));
    plane
        .take(tags::ROW_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX)
        .unwrap();
    frame.put(sequence(
        tags::PLANE_POSITION_SLIDE_SEQUENCE,
        vec![plane.clone()],
    ));
    object.put(sequence(
        tags::PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE,
        vec![frame.clone()],
    ));
    assert!(read_sparse_frame_origins(&object, Some("TILED_SPARSE")).is_err());

    plane.put(DataElement::new(
        tags::ROW_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX,
        VR::SL,
        PrimitiveValue::from(9_i32),
    ));
    frame.put(sequence(tags::PLANE_POSITION_SLIDE_SEQUENCE, vec![plane]));
    object.put(sequence(
        tags::PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE,
        vec![frame],
    ));
    assert_eq!(
        read_sparse_frame_origins(&object, Some("TILED_SPARSE")).unwrap(),
        Some(vec![FrameOrigin { column: 5, row: 9 }])
    );
    assert_eq!(dot([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]), 32.0);
}