wsi-dicom 0.7.0

Convert whole-slide imaging files to DICOM VL Whole Slide Microscopy
Documentation
use std::time::Instant;

use j2k::{J2kProgressionOrder, J2kToHtj2kOptions};
use j2k_core::CompressedPayloadKind;
use rayon::prelude::*;

use super::{
    ensure_consistent_pixel_profile, CodecValidation, Compression, Error, ExportMetrics,
    PixelProfile, RawCompressedTile, TransferSyntax,
};

pub(super) struct BatchOutcome {
    pub(super) codestream: Vec<u8>,
    pub(super) profile: PixelProfile,
    transcode_micros: u128,
}

#[derive(Clone)]
pub(super) struct Frame {
    data: Vec<u8>,
    profile: PixelProfile,
}

pub(super) fn transfer_syntax(transfer_syntax: TransferSyntax) -> bool {
    matches!(
        transfer_syntax,
        TransferSyntax::Htj2kLossless | TransferSyntax::Htj2kLosslessRpcl
    )
}

pub(super) fn frame(
    raw: &RawCompressedTile,
    frame_columns: u32,
    frame_rows: u32,
    transfer_syntax: TransferSyntax,
    profile: Option<PixelProfile>,
) -> Option<Frame> {
    if !self::transfer_syntax(transfer_syntax)
        || raw.width() != frame_columns
        || raw.height() != frame_rows
        || !matches!(
            raw.compression(),
            Compression::Jp2kRgb | Compression::Jp2kYcbcr
        )
    {
        return None;
    }
    Some(Frame {
        data: raw.data().to_vec(),
        profile: profile?,
    })
}

pub(super) fn encode_planned_batch(
    planned: &[super::LosslessJ2kPlannedFrame],
    transfer_syntax: TransferSyntax,
    codec_validation: CodecValidation,
) -> Result<Vec<Option<Result<BatchOutcome, Error>>>, Error> {
    let indices = planned
        .iter()
        .enumerate()
        .filter_map(|(idx, planned_frame)| planned_frame.source_j2k.is_some().then_some(idx))
        .collect::<Vec<_>>();
    let mut outcomes = (0..planned.len()).map(|_| None).collect::<Vec<_>>();
    if indices.is_empty() {
        return Ok(outcomes);
    }

    let frames = indices
        .iter()
        .map(|&idx| {
            planned[idx]
                .source_j2k
                .as_ref()
                .ok_or_else(|| Error::Encode {
                    message: "direct J2K route missing source J2K frame".into(),
                })
        })
        .collect::<Result<Vec<_>, _>>()?;
    let batch = encode_frame_refs_batch(&frames, transfer_syntax, codec_validation)?;

    for (input_idx, encoded) in batch.into_iter().enumerate() {
        let planned_idx = indices[input_idx];
        outcomes[planned_idx] = Some(encoded);
    }

    Ok(outcomes)
}

pub(super) fn record_success(
    metrics: &mut ExportMetrics,
    pixel_profile: &mut Option<PixelProfile>,
    direct: &BatchOutcome,
    mismatch_reason: &'static str,
) -> Result<(), Error> {
    ensure_consistent_pixel_profile(pixel_profile, direct.profile, mismatch_reason)?;
    metrics.record_pixel_profile(direct.profile);
    metrics.record_j2k_direct_htj2k_frame(direct.transcode_micros);
    Ok(())
}

fn encode_frame_refs_batch(
    frames: &[&Frame],
    transfer_syntax: TransferSyntax,
    codec_validation: CodecValidation,
) -> Result<Vec<Result<BatchOutcome, Error>>, Error> {
    let options = options(transfer_syntax, codec_validation)?;
    Ok(frames
        .par_iter()
        .map(|frame| {
            let started = Instant::now();
            j2k::recode_j2k_to_htj2k_lossless(&frame.data, options).map_or_else(
                |err| Err(to_wsi_error(err)),
                |recoded| {
                    Ok(BatchOutcome {
                        codestream: recoded.bytes,
                        profile: frame.profile,
                        transcode_micros: started.elapsed().as_micros(),
                    })
                },
            )
        })
        .collect())
}

fn options(
    transfer_syntax: TransferSyntax,
    codec_validation: CodecValidation,
) -> Result<J2kToHtj2kOptions, Error> {
    if !self::transfer_syntax(transfer_syntax) {
        return Err(Error::Unsupported {
            reason: "direct J2K to HTJ2K recode requires an HTJ2K lossless transfer syntax".into(),
        });
    }
    let progression = if transfer_syntax == TransferSyntax::Htj2kLosslessRpcl {
        J2kProgressionOrder::Rpcl
    } else {
        J2kProgressionOrder::Lrcp
    };
    Ok(J2kToHtj2kOptions::new(
        CompressedPayloadKind::Jpeg2000Codestream,
        progression,
        codec_validation.to_j2k_validation(),
    ))
}

fn to_wsi_error(source: j2k::J2kError) -> Error {
    Error::Encode {
        message: format!("direct J2K to HTJ2K recode failed: {source}"),
    }
}

#[cfg(test)]
mod tests {
    use dicom_dictionary_std::tags;
    use j2k::J2kLosslessSamples;

    use crate::encode::encode_dicom_lossless;
    use crate::metadata::MetadataSource;
    use crate::options::{EncodeBackendPreference, ExportOptions};
    use crate::request::ExportRequest;
    use crate::test_support::{
        assert_htj2k_rpcl_codestream, dicom_fragment_payload_without_padding,
        write_tiled_jp2k_rgb_tiff,
    };

    use super::super::export_dicom;
    use super::*;

    #[test]
    fn export_htj2k_from_native_j2k_tiles_uses_direct_coefficient_recode() {
        let tmp = tempfile::tempdir().unwrap();
        let width = 64;
        let height = 64;
        let bytes: Vec<u8> = (0..width * height * 3)
            .map(|value| ((value * 37 + 11) & 0xFF) as u8)
            .collect();
        let samples =
            J2kLosslessSamples::new(&bytes, width, height, 3, 8, false).expect("valid samples");
        let codestream = encode_dicom_lossless(
            samples,
            TransferSyntax::Jpeg2000Lossless,
            EncodeBackendPreference::CpuOnly,
            CodecValidation::Disabled,
        )
        .unwrap();
        let source = tmp.path().join("source.svs");
        write_tiled_jp2k_rgb_tiff(
            &source,
            width,
            height,
            width,
            height,
            std::slice::from_ref(&codestream),
        );

        let report = export_dicom(ExportRequest {
            source_path: source,
            output_dir: tmp.path().join("out"),
            options: ExportOptions {
                tile_size: width,
                transfer_syntax: TransferSyntax::Htj2kLosslessRpcl,
                encode_backend: EncodeBackendPreference::RequireDevice,
                codec_validation: CodecValidation::Disabled,
                source_device_decode: true,
                ..ExportOptions::default()
            },
            metadata: MetadataSource::ResearchPlaceholder,
            level_filter: None,
        })
        .unwrap();

        assert_eq!(report.metrics.routes.total_frames, 1);
        assert_eq!(report.metrics.routes.j2k_direct_htj2k_frames, 1);
        assert_eq!(report.metrics.routes.j2k_passthrough_frames, 0);
        assert_eq!(report.metrics.routes.cpu_input_frames, 0);
        assert_eq!(report.metrics.routes.gpu_input_decode_frames, 0);
        assert_eq!(report.metrics.routes.gpu_encode_frames, 0);
        assert_eq!(report.metrics.routes.cpu_fallback_frames, 0);

        let object = dicom_object::open_file(&report.instances[0].path).unwrap();
        assert_eq!(
            object.meta().transfer_syntax.trim_end_matches('\0'),
            TransferSyntax::Htj2kLosslessRpcl.uid()
        );
        let fragments = object
            .element(tags::PIXEL_DATA)
            .unwrap()
            .value()
            .fragments()
            .unwrap();
        assert_eq!(fragments.len(), 1);
        let payload = dicom_fragment_payload_without_padding(&fragments[0]);
        assert_ne!(payload, codestream);
        assert_htj2k_rpcl_codestream(payload);
        assert_eq!(
            decode_j2k_frame_for_test(payload, width, height, 3, 8),
            bytes
        );
    }

    fn decode_j2k_frame_for_test(
        codestream: &[u8],
        width: u32,
        height: u32,
        components: u8,
        bits_allocated: u16,
    ) -> Vec<u8> {
        let fmt = match (components, bits_allocated) {
            (1, 8) => j2k::PixelFormat::Gray8,
            (3, 8) => j2k::PixelFormat::Rgb8,
            (1, 16) => j2k::PixelFormat::Gray16,
            (3, 16) => j2k::PixelFormat::Rgb16,
            other => panic!("unsupported frame profile: {other:?}"),
        };
        let bytes_per_sample = if bits_allocated <= 8 { 1usize } else { 2usize };
        let stride = width as usize * components as usize * bytes_per_sample;
        let mut decoder = j2k::J2kDecoder::new(codestream).unwrap();
        let mut decoded = vec![0; stride * height as usize];
        decoder.decode_into(&mut decoded, stride, fmt).unwrap();
        decoded
    }
}