//! rtx-slidescope - GPU-accelerated pathology slide processing //! //! This crate provides NMF-based stain unmixing and tile pyramid generation //! for digital pathology slide analysis. //! //! # Features //! //! - **NMF Stain Unmixing**: Separate stain components (H&E, IHC-DAB) using //! non-negative matrix factorization //! - **Tile Pyramid Generation**: Create deep-zoom compatible tile pyramids //! for fast viewing of large images //! - **Optical Density Conversion**: Convert between RGB and optical density //! color spaces for accurate stain analysis //! //! # Example //! //! ```no_run //! use rtx_slidescope::{ //! SlidescopeProcessor, ProcessingConfig, //! image_io::load_image, //! }; //! use std::path::Path; //! //! # fn main() -> anyhow::Result<()> { //! // Load an image //! let image = load_image(Path::new("slide.tiff"))?; //! //! // Create processor with default config //! let config = ProcessingConfig::default(); //! let processor = SlidescopeProcessor::new(config)?; //! //! // Generate tile pyramid //! let pyramid = processor.generate_pyramid(&image)?; //! //! // Get DZI metadata for OpenSeadragon //! let dzi = pyramid.get_dzi_metadata(); //! println!("Image: {}x{}, {} levels", dzi.width, dzi.height, dzi.max_level); //! # Ok(()) //! # } //! ``` pub mod config; pub mod gpu; pub mod gpu_nmf; pub mod image_io; pub mod nmf; pub mod optical_density; pub mod pyramid; pub mod stain_vectors; // Re-exports pub use config::{ EstimationMethod, NmfProcessingConfig, ProcessingConfig, PyramidProcessingConfig, StainEstimationConfig, }; pub use gpu::{BackendType, DeviceInfo, GpuBackend, gpu_available, select_backend}; pub use gpu_nmf::GpuNmfProcessor; pub use nmf::{NmfOutput, NmfProcessor}; pub use optical_density::{od_to_rgb, rgb_to_od}; pub use pyramid::{MultiLayerPyramid, TilePyramid}; pub use stain_vectors::{ StainEstimationResult, StainEstimator, he_stain_matrix, ihc_dab_stain_matrix, }; use anyhow::Result; use image::DynamicImage; use ndarray::{Array2, Array3}; use slidescope_shared::TileLayer; use std::time::Instant; use tracing::info; /// Main processor for SlideScope operations pub struct SlidescopeProcessor { config: ProcessingConfig, } impl SlidescopeProcessor { /// Create a new processor with the given configuration pub fn new(config: ProcessingConfig) -> Result { Ok(Self { config }) } /// Create a processor with default configuration pub fn with_defaults() -> Result { Self::new(ProcessingConfig::default()) } /// Generate a tile pyramid from an image pub fn generate_pyramid(&self, image: &DynamicImage) -> Result { TilePyramid::generate(image, &self.config.pyramid) } /// Process an image with NMF stain unmixing /// /// Returns a multi-layer pyramid containing the original image and /// individual stain concentration maps. pub fn process_nmf(&self, image: &DynamicImage) -> Result { let start_time = Instant::now(); // Convert to array let rgb_array = image_io::image_to_array(image); let (height, width, _) = rgb_array.dim(); info!("Processing {}x{} image with NMF", width, height); // Convert to optical density let od_array = rgb_to_od_array(&rgb_array); // Estimate stain vectors let estimator = StainEstimator::new( self.config.stain_estimation.method, self.config.stain_estimation.background_threshold, self.config.stain_estimation.sample_size, ); let stain_result = estimator.estimate(&od_array, self.config.nmf.n_components)?; // Flatten OD to pixel matrix let od_pixels = optical_density::flatten_to_pixels(&od_array); // Perform NMF unmixing let concentrations = nmf::unmix_stains(&od_pixels, &stain_result.stain_matrix, &self.config.nmf)?; // Reshape concentrations back to images let mut stain_images = Vec::new(); for i in 0..self.config.nmf.n_components { let conc_col = concentrations.column(i); let conc_2d = Array2::from_shape_vec((height, width), conc_col.to_vec())?; stain_images.push(conc_2d); } // Create multi-layer pyramid let mut multi_pyramid = MultiLayerPyramid::new(width as u32, height as u32); // Add original image pyramid let original_pyramid = self.generate_pyramid(image)?; multi_pyramid.add_layer(TileLayer::Original, original_pyramid); // Add stain concentration pyramids let stain_colors = [ [0.4, 0.2, 0.7], // Hematoxylin-ish (purple) [0.9, 0.5, 0.5], // Eosin-ish (pink) [0.6, 0.4, 0.2], // DAB-ish (brown) ]; for (i, (conc, color)) in stain_images.iter().zip(stain_colors.iter()).enumerate() { let colored = image_io::concentration_to_colored(conc, *color)?; let dynamic = DynamicImage::ImageRgb8(colored); let pyramid = self.generate_pyramid(&dynamic)?; let layer = match i { 0 => TileLayer::Stain1, 1 => TileLayer::Stain2, 2 => TileLayer::Stain3, _ => continue, }; multi_pyramid.add_layer(layer, pyramid); } let processing_time_ms = start_time.elapsed().as_millis() as u64; info!( "NMF processing complete in {}ms, {} stain components", processing_time_ms, stain_images.len() ); Ok(NmfProcessingResult { multi_pyramid, stain_matrix: stain_result.stain_matrix, concentrations: stain_images, processing_time_ms, }) } /// Get the current configuration pub fn config(&self) -> &ProcessingConfig { &self.config } } /// Result of NMF processing pub struct NmfProcessingResult { /// Multi-layer tile pyramid with original and stain layers pub multi_pyramid: MultiLayerPyramid, /// Estimated stain matrix pub stain_matrix: Array2, /// Stain concentration maps pub concentrations: Vec>, /// Processing time in milliseconds pub processing_time_ms: u64, } /// Convert RGB array to OD array (helper) fn rgb_to_od_array(rgb: &Array3) -> Array3 { optical_density::rgb_to_od(rgb) } /// Get GPU device name if available pub fn gpu_device_name() -> Option { gpu::select_backend() .ok() .filter(|b| b.device_info().backend != gpu::BackendType::Cpu) .map(|b| b.device_info().name) } #[cfg(test)] mod tests { use super::*; use image::{Rgb, RgbImage}; fn create_test_image(width: u32, height: u32) -> DynamicImage { let mut img = RgbImage::new(width, height); for y in 0..height { for x in 0..width { // Create a gradient with some color variation let r = ((x as f32 / width as f32) * 200.0 + 50.0) as u8; let g = ((y as f32 / height as f32) * 150.0 + 100.0) as u8; let b = 180; img.put_pixel(x, y, Rgb([r, g, b])); } } DynamicImage::ImageRgb8(img) } #[test] fn test_processor_creation() { let processor = SlidescopeProcessor::with_defaults().unwrap(); assert_eq!(processor.config().nmf.n_components, 2); } #[test] fn test_pyramid_generation() { let processor = SlidescopeProcessor::with_defaults().unwrap(); let image = create_test_image(200, 150); let pyramid = processor.generate_pyramid(&image).unwrap(); assert_eq!(pyramid.width, 200); assert_eq!(pyramid.height, 150); assert!(pyramid.total_tiles() > 0); } #[test] fn test_nmf_processing() { // Use a smaller config for faster testing let mut config = ProcessingConfig::default(); config.nmf = NmfProcessingConfig::fast(); config.pyramid.tile_size = 64; let processor = SlidescopeProcessor::new(config).unwrap(); let image = create_test_image(100, 100); let result = processor.process_nmf(&image).unwrap(); // Should have original + 2 stain layers assert!(result.multi_pyramid.has_layer(TileLayer::Original)); assert!(result.multi_pyramid.has_layer(TileLayer::Stain1)); assert!(result.multi_pyramid.has_layer(TileLayer::Stain2)); // Should have 2 concentration maps assert_eq!(result.concentrations.len(), 2); assert_eq!(result.concentrations[0].shape(), &[100, 100]); } #[test] fn test_gpu_availability() { // This should not panic regardless of GPU presence let _ = gpu_available(); let _ = gpu_device_name(); } }