wsi-dicom-annotations 0.2.1

DICOM WSI ANN, SEG, SR, and Parametric Map interoperability
Documentation
use crate::{DicomAnnotationContext, Error, Result};

use super::profile::{RasterCoordinateSpace, RasterProfile};
use super::source::RasterDescriptor;

#[derive(Debug, Clone, Copy)]
pub(super) struct RasterGeometry {
    pub(super) origin: [f64; 3],
    pub(super) orientation: [f64; 6],
    pub(super) pixel_spacing: [f64; 2],
    pub(super) slice_thickness: f64,
}

impl RasterGeometry {
    pub(super) fn resolve(
        source: &DicomAnnotationContext,
        canonical_source: &DicomAnnotationContext,
        profile: &RasterProfile,
        descriptor: RasterDescriptor,
    ) -> Result<Self> {
        let base = match profile.coordinate_space {
            RasterCoordinateSpace::Level0Pixels => {
                source.require_shared_slide_frame(canonical_source)?;
                canonical_source
            }
            RasterCoordinateSpace::SourcePixels | RasterCoordinateSpace::SlideMm => source,
        };
        let orientation = base.image_orientation_slide().ok_or_else(|| {
            Error::InvalidInput(
                "raster conversion requires source Image Orientation (Slide)".into(),
            )
        })?;
        let slice_thickness = base.slice_thickness().ok_or_else(|| {
            Error::InvalidInput(
                "raster conversion requires source Slice Thickness/depth of field".into(),
            )
        })?;
        let (origin, pixel_spacing) = match profile.coordinate_space {
            RasterCoordinateSpace::Level0Pixels | RasterCoordinateSpace::SourcePixels => {
                let source_spacing = base.pixel_spacing().ok_or_else(|| {
                    Error::InvalidInput(
                        "pixel-coordinate raster conversion requires source Pixel Spacing".into(),
                    )
                })?;
                let origin =
                    base.pixel_to_slide_coordinate(profile.grid_origin.x, profile.grid_origin.y)?;
                validate_last_pixel_sample(base, profile, descriptor)?;
                (
                    [origin.x, origin.y, origin.z],
                    [
                        profile.sample_spacing.y * source_spacing[0],
                        profile.sample_spacing.x * source_spacing[1],
                    ],
                )
            }
            RasterCoordinateSpace::SlideMm => {
                let z = profile
                    .grid_origin
                    .z
                    .ok_or_else(|| Error::InvalidInput("slide-mm grid origin requires z".into()))?;
                let origin = [profile.grid_origin.x, profile.grid_origin.y, z];
                validate_slide_grid(source, origin, orientation, profile, descriptor)?;
                (origin, [profile.sample_spacing.y, profile.sample_spacing.x])
            }
        };
        Ok(Self {
            origin,
            orientation,
            pixel_spacing,
            slice_thickness,
        })
    }

    pub(super) fn frame_position(self, column_position: u32, row_position: u32) -> [f64; 3] {
        self.position_at(f64::from(column_position - 1), f64::from(row_position - 1))
    }

    pub(super) fn position_at(self, column_offset: f64, row_offset: f64) -> [f64; 3] {
        let column_distance = column_offset * self.pixel_spacing[1];
        let row_distance = row_offset * self.pixel_spacing[0];
        [
            self.origin[0]
                + column_distance * self.orientation[0]
                + row_distance * self.orientation[3],
            self.origin[1]
                + column_distance * self.orientation[1]
                + row_distance * self.orientation[4],
            self.origin[2]
                + column_distance * self.orientation[2]
                + row_distance * self.orientation[5],
        ]
    }
}

fn validate_last_pixel_sample(
    base: &DicomAnnotationContext,
    profile: &RasterProfile,
    descriptor: RasterDescriptor,
) -> Result<()> {
    let x = profile.grid_origin.x + f64::from(descriptor.width - 1) * profile.sample_spacing.x;
    let y = profile.grid_origin.y + f64::from(descriptor.height - 1) * profile.sample_spacing.y;
    base.pixel_to_slide_coordinate(x, y).map(|_| ())
}

fn validate_slide_grid(
    source: &DicomAnnotationContext,
    origin: [f64; 3],
    orientation: [f64; 6],
    profile: &RasterProfile,
    descriptor: RasterDescriptor,
) -> Result<()> {
    let column_distance = f64::from(descriptor.width - 1) * profile.sample_spacing.x;
    let row_distance = f64::from(descriptor.height - 1) * profile.sample_spacing.y;
    for (column, row) in [(0.0, 0.0), (column_distance, row_distance)] {
        source.slide_coordinate_to_pixel3(
            origin[0] + column * orientation[0] + row * orientation[3],
            origin[1] + column * orientation[1] + row * orientation[4],
            origin[2] + column * orientation[2] + row * orientation[5],
        )?;
    }
    Ok(())
}