wsi-annotation-tests 0.1.3

DICOM Annotation Test Suite for WSI ANN, SEG, SR, and Parametric Map
Documentation
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};

const MAPPING: &[u8] = include_bytes!("../../examples/pathology-mapping-v1.json");
const DIRECT_GEOJSON: &[u8] = include_bytes!("../../examples/pathology-annotations.geojson");
const SEG_GEOJSON: &[u8] = include_bytes!("../../examples/pathology-seg-holes.geojson");
const RASTER_PROFILE: &[u8] = include_bytes!("../../examples/raster-profile-v1.json");
const CONCATENATION_PROFILE: &[u8] =
    include_bytes!("../../examples/raster-profile-concatenation-v1.json");

pub(super) struct ConversionInputs {
    pub mapping: PathBuf,
    pub direct_geojson: PathBuf,
    pub seg_geojson: PathBuf,
    pub raster_profile: PathBuf,
    pub concatenation_profile: PathBuf,
    pub raster: PathBuf,
    pub wide_raster: PathBuf,
}

pub(super) fn prepare(directory: &Path) -> Result<ConversionInputs, String> {
    fs::create_dir(directory).map_err(|error| {
        format!(
            "could not create conversion input directory {}: {error}",
            directory.display()
        )
    })?;
    let inputs = ConversionInputs {
        mapping: directory.join("pathology-mapping-v1.json"),
        direct_geojson: directory.join("pathology-annotations.geojson"),
        seg_geojson: directory.join("pathology-seg-holes.geojson"),
        raster_profile: directory.join("raster-profile-v1.json"),
        concatenation_profile: directory.join("raster-profile-concatenation-v1.json"),
        raster: directory.join("probability-float32.npy"),
        wide_raster: directory.join("probability-wide-float32.npy"),
    };
    for (path, bytes) in [
        (&inputs.mapping, MAPPING),
        (&inputs.direct_geojson, DIRECT_GEOJSON),
        (&inputs.seg_geojson, SEG_GEOJSON),
        (&inputs.raster_profile, RASTER_PROFILE),
        (&inputs.concatenation_profile, CONCATENATION_PROFILE),
    ] {
        write_new(path, bytes)?;
    }
    let mut values = (0_u16..256)
        .map(|value| f32::from(value) / 255.0)
        .collect::<Vec<_>>();
    values[5 * 16 + 5] = f32::NAN;
    write_npy_f32(&inputs.raster, &[16, 16], &values)?;
    let wide = (0_u16..513)
        .map(|value| f32::from(value) / 512.0)
        .collect::<Vec<_>>();
    write_npy_f32(&inputs.wide_raster, &[1, 513], &wide)?;
    Ok(inputs)
}

fn write_npy_f32(path: &Path, shape: &[usize], values: &[f32]) -> Result<(), String> {
    let expected = shape
        .iter()
        .try_fold(1_usize, |length, dimension| length.checked_mul(*dimension));
    if expected != Some(values.len()) {
        return Err("NPY fixture shape does not match its value count".to_owned());
    }
    let shape = shape
        .iter()
        .map(usize::to_string)
        .collect::<Vec<_>>()
        .join(", ");
    let mut header = format!("{{'descr': '<f4', 'fortran_order': False, 'shape': ({shape},), }}");
    while (10 + header.len() + 1) % 64 != 0 {
        header.push(' ');
    }
    header.push('\n');
    let header_length = u16::try_from(header.len())
        .map_err(|_| "NPY fixture header exceeds version 1 length".to_owned())?;
    let mut bytes = Vec::with_capacity(10 + header.len() + values.len() * 4);
    bytes.extend(b"\x93NUMPY\x01\x00");
    bytes.extend(header_length.to_le_bytes());
    bytes.extend(header.as_bytes());
    for value in values {
        bytes.extend(value.to_le_bytes());
    }
    write_new(path, &bytes)
}

fn write_new(path: &Path, bytes: &[u8]) -> Result<(), String> {
    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(path)
        .map_err(|error| {
            format!(
                "could not create conversion input {}: {error}",
                path.display()
            )
        })?;
    file.write_all(bytes).map_err(|error| {
        format!(
            "could not write conversion input {}: {error}",
            path.display()
        )
    })
}