wsi-dicom-annotations 0.2.1

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

use crate::{Error, Result};

use super::super::model::{
    clockwise_polygon, validate_polygon, AnnotationGeometry, AnnotationGraphicType, Point2,
};

pub(super) fn encoded_geometry(
    geometry: &AnnotationGeometry,
) -> Result<(Vec<f64>, Option<Vec<u32>>)> {
    match geometry {
        AnnotationGeometry::Points(points) => Ok((
            points.iter().flat_map(|point| [point.x, point.y]).collect(),
            None,
        )),
        AnnotationGeometry::Polygons(polygons) => {
            let mut coordinates = Vec::new();
            let mut indices = Vec::with_capacity(polygons.len());
            for polygon in polygons {
                indices.push(u32::try_from(coordinates.len() + 1).map_err(|_| {
                    Error::InvalidInput("ANN coordinate index exceeds DICOM OL range".into())
                })?);
                coordinates.extend(clockwise_polygon(polygon).flat_map(|point| [point.x, point.y]));
            }
            Ok((coordinates, Some(indices)))
        }
        AnnotationGeometry::ReadOnly {
            coordinates,
            primitive_point_indices,
            ..
        } => Ok((
            coordinates.clone(),
            (!primitive_point_indices.is_empty()).then(|| primitive_point_indices.clone()),
        )),
    }
}

pub(super) fn validate_encoded_geometry(
    item: &InMemDicomObject,
    graphic_type: AnnotationGraphicType,
    coordinates: &[f64],
    indices: &[u32],
    dimensions: usize,
) -> Result<()> {
    let point_count = coordinates.len() / dimensions;
    let annotation_count = match graphic_type {
        AnnotationGraphicType::Point => {
            if !indices.is_empty() {
                return Err(Error::InvalidInput(
                    "ANN point groups cannot contain primitive indices".into(),
                ));
            }
            point_count
        }
        AnnotationGraphicType::Polygon | AnnotationGraphicType::Polyline => {
            if indices.is_empty()
                || indices[0] != 1
                || indices.windows(2).any(|pair| pair[0] >= pair[1])
                || indices
                    .iter()
                    .any(|index| primitive_point_offset(*index, dimensions).is_none())
            {
                return Err(Error::InvalidInput(
                    "ANN primitive indices must start at one, align to coordinate tuples, and increase strictly"
                        .into(),
                ));
            }
            for (position, index) in indices.iter().enumerate() {
                let start = primitive_point_offset(*index, dimensions)
                    .ok_or_else(|| Error::InvalidInput("invalid ANN primitive index".into()))?;
                let end = indices.get(position + 1).map_or(point_count, |next| {
                    primitive_point_offset(*next, dimensions).unwrap_or(point_count)
                });
                let minimum = if graphic_type == AnnotationGraphicType::Polygon {
                    3
                } else {
                    2
                };
                if start >= end || end > point_count || end - start < minimum {
                    return Err(Error::InvalidInput(format!(
                        "ANN {} primitive has fewer than {minimum} points or exceeds coordinate data",
                        graphic_type.dicom_value()
                    )));
                }
            }
            indices.len()
        }
        AnnotationGraphicType::Ellipse | AnnotationGraphicType::Rectangle => {
            if !indices.is_empty() || !point_count.is_multiple_of(4) {
                return Err(Error::InvalidInput(format!(
                    "ANN {} groups require four points per annotation and no primitive indices",
                    graphic_type.dicom_value()
                )));
            }
            point_count / 4
        }
    };
    let declared = item
        .get(tags::NUMBER_OF_ANNOTATIONS)
        .and_then(|element| element.to_int::<usize>().ok())
        .ok_or_else(|| {
            Error::InvalidInput("ANN group has no valid Number of Annotations".into())
        })?;
    if annotation_count == 0 || declared != annotation_count {
        return Err(Error::InvalidInput(format!(
            "ANN declares {declared} annotations but its geometry encodes {annotation_count}"
        )));
    }
    Ok(())
}

pub(super) fn primitive_point_offset(index: u32, dimensions: usize) -> Option<usize> {
    let value_offset = usize::try_from(index).ok()?.checked_sub(1)?;
    value_offset
        .is_multiple_of(dimensions)
        .then_some(value_offset / dimensions)
}

pub(super) fn decode_polygons(coordinates: &[f64], indices: &[u32]) -> Result<Vec<Vec<Point2>>> {
    if indices.is_empty() {
        return Err(Error::InvalidInput(
            "ANN polygon group has no primitive point index list".into(),
        ));
    }
    let mut polygons = Vec::with_capacity(indices.len());
    for (position, &index) in indices.iter().enumerate() {
        let start = usize::try_from(index.saturating_sub(1)).unwrap_or(usize::MAX);
        let end = indices.get(position + 1).map_or(coordinates.len(), |next| {
            usize::try_from(next.saturating_sub(1)).unwrap_or(usize::MAX)
        });
        if index == 0
            || start >= end
            || end > coordinates.len()
            || !start.is_multiple_of(2)
            || !end.is_multiple_of(2)
        {
            return Err(Error::InvalidInput(
                "ANN polygon primitive indices are invalid".into(),
            ));
        }
        let polygon = coordinates[start..end]
            .as_chunks::<2>()
            .0
            .iter()
            .map(|point| Point2::new(point[0], point[1]))
            .collect::<Vec<_>>();
        validate_polygon(&polygon)?;
        polygons.push(polygon);
    }
    Ok(polygons)
}