wsi-dicom-annotations 0.2.1

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

use crate::annotations::context::DicomAnnotationContext;
use crate::annotations::dicom_dataset::{optional_string, required_u16, required_u32};
use crate::annotations::seg::{
    BinarySegmentationFrame, MAX_SEGMENTATION_FRAMES, MAX_SEGMENTATION_PIXELS,
};
use crate::{Error, Result};

pub(super) struct FrameLayout {
    pub(super) rows: u16,
    pub(super) columns: u16,
    pub(super) frame_count: usize,
    pub(super) pixels_per_frame: usize,
    pub(super) pixel_count: usize,
}

impl FrameLayout {
    pub(super) fn read(object: &InMemDicomObject) -> Result<Self> {
        let rows = required_u16(object, tags::ROWS)?;
        let columns = required_u16(object, tags::COLUMNS)?;
        if rows == 0 || columns == 0 {
            return Err(Error::InvalidInput(
                "SEG Rows and Columns must be positive".into(),
            ));
        }
        let frame_count = object
            .get(tags::NUMBER_OF_FRAMES)
            .and_then(|element| element.to_int::<usize>().ok())
            .filter(|count| (1..=MAX_SEGMENTATION_FRAMES).contains(count))
            .ok_or_else(|| {
                Error::InvalidInput("SEG frame count is outside the supported range".into())
            })?;
        let pixels_per_frame = usize::from(rows) * usize::from(columns);
        let pixel_count = pixels_per_frame
            .checked_mul(frame_count)
            .ok_or_else(|| Error::InvalidInput("SEG pixel count overflows".into()))?;
        enforce_pixel_budget(pixel_count)?;
        Ok(Self {
            rows,
            columns,
            frame_count,
            pixels_per_frame,
            pixel_count,
        })
    }
}

pub(super) fn enforce_pixel_budget(pixel_count: usize) -> Result<()> {
    if pixel_count > MAX_SEGMENTATION_PIXELS {
        return Err(Error::InvalidInput(format!(
            "SEG contains {pixel_count} pixels, exceeding the {MAX_SEGMENTATION_PIXELS}-pixel import limit"
        )));
    }
    Ok(())
}

fn read_frame_position(
    item: &InMemDicomObject,
    source: &DicomAnnotationContext,
    columns: u16,
    rows: u16,
    segment_number: u16,
) -> Result<(u16, u32, u32)> {
    let plane = item
        .get(tags::PLANE_POSITION_SLIDE_SEQUENCE)
        .and_then(|element| element.items())
        .and_then(|items| items.first())
        .ok_or_else(|| Error::InvalidInput("SEG frame has no slide position".into()))?;
    let col_position = plane
        .get(tags::COLUMN_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX)
        .and_then(|element| element.to_int::<u32>().ok())
        .ok_or_else(|| Error::InvalidInput("SEG frame has invalid column position".into()))?;
    let row_position = plane
        .get(tags::ROW_POSITION_IN_TOTAL_IMAGE_PIXEL_MATRIX)
        .and_then(|element| element.to_int::<u32>().ok())
        .ok_or_else(|| Error::InvalidInput("SEG frame has invalid row position".into()))?;
    if col_position == 0 || row_position == 0 {
        return Err(Error::InvalidInput(
            "SEG frame positions are one-based and cannot be zero".into(),
        ));
    }
    if !(col_position - 1).is_multiple_of(u32::from(columns))
        || !(row_position - 1).is_multiple_of(u32::from(rows))
    {
        return Err(Error::Unsupported(
            "SEG frame positions must align with frame dimensions".into(),
        ));
    }
    let tile_col = (col_position - 1) / u32::from(columns);
    let tile_row = (row_position - 1) / u32::from(rows);
    let (matrix_width, matrix_height) = source.total_pixel_matrix_dimensions();
    if tile_col * u32::from(columns) >= matrix_width || tile_row * u32::from(rows) >= matrix_height
    {
        return Err(Error::InvalidInput(
            "SEG frame position is outside the open WSI Total Pixel Matrix".into(),
        ));
    }
    Ok((segment_number, tile_col, tile_row))
}

pub(super) fn read_frame_positions(
    object: &InMemDicomObject,
    source: &DicomAnnotationContext,
    layout: &FrameLayout,
    channel_segment_numbers: Option<&[u16]>,
    missing_message: &str,
) -> Result<Vec<(u16, u32, u32)>> {
    let FrameLayout {
        columns,
        rows,
        frame_count,
        ..
    } = *layout;
    if let Some(frame_items) = object
        .get(tags::PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE)
        .and_then(|element| element.items())
    {
        if frame_items.len() != frame_count {
            return Err(Error::InvalidInput(format!(
                "SEG declares {frame_count} frames but has {} per-frame items",
                frame_items.len()
            )));
        }
        let shared = object
            .get(tags::SHARED_FUNCTIONAL_GROUPS_SEQUENCE)
            .and_then(|element| element.items())
            .and_then(|items| items.first());
        return frame_items
            .iter()
            .map(|item| {
                let segment_number = if let Some(segment_numbers) = channel_segment_numbers {
                    let identification = item
                        .get(tags::SEGMENT_IDENTIFICATION_SEQUENCE)
                        .or_else(|| shared?.get(tags::SEGMENT_IDENTIFICATION_SEQUENCE))
                        .and_then(|element| element.items())
                        .filter(|items| items.len() == 1)
                        .and_then(|items| items.first())
                        .ok_or_else(|| {
                            Error::InvalidInput(
                                "SEG frame requires one Segment Identification item in its per-frame or shared functional groups".into(),
                            )
                        })?;
                    let number = required_u16(identification, tags::REFERENCED_SEGMENT_NUMBER)?;
                    if !segment_numbers.contains(&number) {
                        return Err(Error::InvalidInput(format!(
                            "SEG frame references unknown Segment Number {number}"
                        )));
                    }
                    number
                } else {
                    // LABELMAP segment identities are encoded in the pixel values.
                    1
                };
                read_frame_position(item, source, columns, rows, segment_number)
            })
            .collect();
    }
    if optional_string(object, tags::DIMENSION_ORGANIZATION_TYPE).as_deref() != Some("TILED_FULL") {
        return Err(Error::InvalidInput(missing_message.into()));
    }

    let total_columns = required_u32(object, tags::TOTAL_PIXEL_MATRIX_COLUMNS)?;
    let total_rows = required_u32(object, tags::TOTAL_PIXEL_MATRIX_ROWS)?;
    if total_columns == 0 || total_rows == 0 {
        return Err(Error::InvalidInput(
            "TILED_FULL SEG Total Pixel Matrix dimensions must be positive".into(),
        ));
    }
    let tile_columns = total_columns.div_ceil(u32::from(columns));
    let tile_rows = total_rows.div_ceil(u32::from(rows));
    let tile_columns_usize = usize::try_from(tile_columns)
        .map_err(|_| Error::InvalidInput("TILED_FULL SEG tile count overflows".into()))?;
    let tiles_per_channel = usize::try_from(
        tile_columns
            .checked_mul(tile_rows)
            .ok_or_else(|| Error::InvalidInput("TILED_FULL SEG tile count overflows".into()))?,
    )
    .map_err(|_| Error::InvalidInput("TILED_FULL SEG tile count overflows".into()))?;
    if tiles_per_channel == 0 || !frame_count.is_multiple_of(tiles_per_channel) {
        return Err(Error::InvalidInput(
            "TILED_FULL SEG frame count does not match its dense tile grid".into(),
        ));
    }
    let channel_count = frame_count / tiles_per_channel;
    if let Some(segment_numbers) = channel_segment_numbers {
        if segment_numbers.len() != channel_count {
            return Err(Error::InvalidInput(format!(
                "TILED_FULL SEG has {channel_count} frame channels but {} non-background segments",
                segment_numbers.len()
            )));
        }
    } else if channel_count != 1 {
        return Err(Error::InvalidInput(
            "TILED_FULL LABELMAP must have one dense frame channel".into(),
        ));
    }

    (0..frame_count)
        .map(|frame_index| {
            let channel_index = frame_index / tiles_per_channel;
            let tile_index = frame_index % tiles_per_channel;
            let tile_col = u32::try_from(tile_index % tile_columns_usize)
                .map_err(|_| Error::InvalidInput("SEG tile column overflows".into()))?;
            let tile_row = u32::try_from(tile_index / tile_columns_usize)
                .map_err(|_| Error::InvalidInput("SEG tile row overflows".into()))?;
            let segment_number =
                channel_segment_numbers.map_or(1, |numbers| numbers[channel_index]);
            let (matrix_width, matrix_height) = source.total_pixel_matrix_dimensions();
            if tile_col * u32::from(columns) >= matrix_width
                || tile_row * u32::from(rows) >= matrix_height
            {
                return Err(Error::InvalidInput(
                    "TILED_FULL SEG frame position is outside the open WSI Total Pixel Matrix"
                        .into(),
                ));
            }
            Ok((segment_number, tile_col, tile_row))
        })
        .collect()
}

pub(in crate::annotations::seg) fn validate_source_reference(
    object: &InMemDicomObject,
    source: &DicomAnnotationContext,
) -> Result<()> {
    if optional_string(object, tags::STUDY_INSTANCE_UID).as_deref()
        != Some(source.study_instance_uid())
    {
        return Err(Error::InvalidInput(
            "SEG Study Instance UID does not match the open WSI".into(),
        ));
    }
    if let (Some(expected), Some(actual)) = (
        source.frame_of_reference_uid(),
        optional_string(object, tags::FRAME_OF_REFERENCE_UID),
    ) {
        if actual != expected {
            return Err(Error::InvalidInput(
                "SEG Frame of Reference UID does not match the open WSI".into(),
            ));
        }
    }
    let referenced = sequence_references_source(
        object,
        tags::REFERENCED_SERIES_SEQUENCE,
        tags::REFERENCED_INSTANCE_SEQUENCE,
        source,
    ) || functional_groups_reference_source(
        object,
        tags::SHARED_FUNCTIONAL_GROUPS_SEQUENCE,
        source,
    ) || functional_groups_reference_source(
        object,
        tags::PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE,
        source,
    );
    if !referenced {
        return Err(Error::InvalidInput(format!(
            "SEG does not reference the open WSI SOP Instance {}",
            source.sop_instance_uid()
        )));
    }
    Ok(())
}

fn sequence_references_source(
    object: &InMemDicomObject,
    outer_tag: Tag,
    inner_tag: Tag,
    source: &DicomAnnotationContext,
) -> bool {
    object
        .get(outer_tag)
        .and_then(|element| element.items())
        .into_iter()
        .flatten()
        .flat_map(|item| {
            item.get(inner_tag)
                .and_then(|element| element.items())
                .unwrap_or_default()
        })
        .any(|reference| sop_reference_matches(reference, source))
}

fn functional_groups_reference_source(
    object: &InMemDicomObject,
    functional_groups_tag: Tag,
    source: &DicomAnnotationContext,
) -> bool {
    object
        .get(functional_groups_tag)
        .and_then(|element| element.items())
        .into_iter()
        .flatten()
        .flat_map(|functional_group| {
            functional_group
                .get(tags::DERIVATION_IMAGE_SEQUENCE)
                .and_then(|element| element.items())
                .unwrap_or_default()
        })
        .flat_map(|derivation| {
            derivation
                .get(tags::SOURCE_IMAGE_SEQUENCE)
                .and_then(|element| element.items())
                .unwrap_or_default()
        })
        .any(|reference| sop_reference_matches(reference, source))
}

fn sop_reference_matches(reference: &InMemDicomObject, source: &DicomAnnotationContext) -> bool {
    optional_string(reference, tags::REFERENCED_SOP_INSTANCE_UID).as_deref()
        == Some(source.sop_instance_uid())
        && optional_string(reference, tags::REFERENCED_SOP_CLASS_UID).as_deref()
            == Some(source.sop_class_uid())
}

pub(in crate::annotations::seg) fn segments_overlap(frames: &[BinarySegmentationFrame]) -> bool {
    for (index, frame) in frames.iter().enumerate() {
        if frames[index + 1..].iter().any(|other| {
            frame.segment_number != other.segment_number
                && frame.tile_col == other.tile_col
                && frame.tile_row == other.tile_row
                && frame.mask.iter().zip(&other.mask).any(|(a, b)| *a && *b)
        }) {
            return true;
        }
    }
    false
}