Files
rustytorch/demos/rtx-slidescope/src/image_io.rs
T
2026-03-04 00:08:42 +00:00

258 lines
7.4 KiB
Rust

//! Image loading and saving utilities
//!
//! Handles loading various image formats and converting between
//! image crate types and ndarray for processing.
use anyhow::{Context, Result, bail};
use image::{DynamicImage, GenericImageView, ImageBuffer, Rgb, RgbImage};
use ndarray::Array3;
use slidescope_shared::ImageFormat;
use std::path::Path;
use tracing::info;
/// Load an image from a file path
pub fn load_image(path: &Path) -> Result<DynamicImage> {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let format = ImageFormat::from_extension(ext)
.with_context(|| format!("Unsupported image format: {}", ext))?;
if format.requires_openslide() {
bail!(
"Format {} requires OpenSlide support (enable 'openslide' feature)",
ext
);
}
let img =
image::open(path).with_context(|| format!("Failed to open image: {}", path.display()))?;
let (width, height) = img.dimensions();
info!("Loaded image: {}x{} from {}", width, height, path.display());
Ok(img)
}
/// Convert DynamicImage to ndarray Array3<u8>
pub fn image_to_array(img: &DynamicImage) -> Array3<u8> {
let rgb = img.to_rgb8();
let (width, height) = rgb.dimensions();
let mut array = Array3::zeros((height as usize, width as usize, 3));
for y in 0..height {
for x in 0..width {
let pixel = rgb.get_pixel(x, y);
array[[y as usize, x as usize, 0]] = pixel[0];
array[[y as usize, x as usize, 1]] = pixel[1];
array[[y as usize, x as usize, 2]] = pixel[2];
}
}
array
}
/// Convert ndarray Array3<u8> to RgbImage
pub fn array_to_image(array: &Array3<u8>) -> Result<RgbImage> {
let shape = array.raw_dim();
if shape[2] != 3 {
bail!("Expected 3 channels, got {}", shape[2]);
}
let height = shape[0] as u32;
let width = shape[1] as u32;
let mut img = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let r = array[[y as usize, x as usize, 0]];
let g = array[[y as usize, x as usize, 1]];
let b = array[[y as usize, x as usize, 2]];
img.put_pixel(x, y, Rgb([r, g, b]));
}
}
Ok(img)
}
/// Convert f32 array (0.0-1.0) to u8 image
pub fn array_f32_to_image(array: &Array3<f32>) -> Result<RgbImage> {
let shape = array.raw_dim();
if shape[2] != 3 {
bail!("Expected 3 channels, got {}", shape[2]);
}
let height = shape[0] as u32;
let width = shape[1] as u32;
let mut img = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let r = (array[[y as usize, x as usize, 0]].clamp(0.0, 1.0) * 255.0) as u8;
let g = (array[[y as usize, x as usize, 1]].clamp(0.0, 1.0) * 255.0) as u8;
let b = (array[[y as usize, x as usize, 2]].clamp(0.0, 1.0) * 255.0) as u8;
img.put_pixel(x, y, Rgb([r, g, b]));
}
}
Ok(img)
}
/// Save an image to a file
pub fn save_image(img: &RgbImage, path: &Path) -> Result<()> {
img.save(path)
.with_context(|| format!("Failed to save image to {}", path.display()))?;
info!("Saved image to {}", path.display());
Ok(())
}
/// Generate a thumbnail from an image
pub fn generate_thumbnail(img: &DynamicImage, max_size: u32) -> DynamicImage {
img.thumbnail(max_size, max_size)
}
/// Extract image metadata
#[derive(Debug, Clone)]
pub struct ImageMetadata {
pub width: u32,
pub height: u32,
pub format: ImageFormat,
pub color_type: String,
pub file_size_bytes: u64,
}
/// Get metadata from an image file
pub fn get_metadata(path: &Path) -> Result<ImageMetadata> {
let file_size = std::fs::metadata(path)
.with_context(|| format!("Cannot read file metadata: {}", path.display()))?
.len();
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let format = ImageFormat::from_extension(ext).unwrap_or(ImageFormat::Jpeg);
// Load just enough to get dimensions
let img = image::open(path)?;
let (width, height) = img.dimensions();
let color_type = format!("{:?}", img.color());
Ok(ImageMetadata {
width,
height,
format,
color_type,
file_size_bytes: file_size,
})
}
/// Create a grayscale concentration image from a single channel
///
/// Useful for visualizing individual stain concentrations.
pub fn concentration_to_grayscale(concentration: &ndarray::Array2<f32>) -> Result<RgbImage> {
let shape = concentration.raw_dim();
let height = shape[0] as u32;
let width = shape[1] as u32;
// Find min/max for normalization
let min_val = concentration.iter().fold(f32::INFINITY, |a, &b| a.min(b));
let max_val = concentration
.iter()
.fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let range = (max_val - min_val).max(1e-6);
let mut img = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let val = concentration[[y as usize, x as usize]];
let normalized = ((val - min_val) / range).clamp(0.0, 1.0);
let gray = (normalized * 255.0) as u8;
img.put_pixel(x, y, Rgb([gray, gray, gray]));
}
}
Ok(img)
}
/// Create a colored concentration image using a stain color
pub fn concentration_to_colored(
concentration: &ndarray::Array2<f32>,
stain_color: [f32; 3],
) -> Result<RgbImage> {
let shape = concentration.raw_dim();
let height = shape[0] as u32;
let width = shape[1] as u32;
// Find max for normalization
let max_val = concentration.iter().fold(0.0f32, |a, &b| a.max(b));
let scale = if max_val > 0.0 { 1.0 / max_val } else { 1.0 };
let mut img = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let val = concentration[[y as usize, x as usize]] * scale;
let r = (stain_color[0] * val * 255.0).clamp(0.0, 255.0) as u8;
let g = (stain_color[1] * val * 255.0).clamp(0.0, 255.0) as u8;
let b = (stain_color[2] * val * 255.0).clamp(0.0, 255.0) as u8;
img.put_pixel(x, y, Rgb([r, g, b]));
}
}
Ok(img)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_array_roundtrip() {
// Create a test image
let mut img = RgbImage::new(10, 10);
for y in 0..10 {
for x in 0..10 {
img.put_pixel(x, y, Rgb([(x * 25) as u8, (y * 25) as u8, 128]));
}
}
let dynamic = DynamicImage::ImageRgb8(img.clone());
let array = image_to_array(&dynamic);
assert_eq!(array.shape(), &[10, 10, 3]);
let back = array_to_image(&array).unwrap();
assert_eq!(back.dimensions(), (10, 10));
// Check values preserved
for y in 0..10 {
for x in 0..10 {
assert_eq!(img.get_pixel(x, y), back.get_pixel(x, y));
}
}
}
#[test]
fn test_thumbnail() {
let img = DynamicImage::ImageRgb8(RgbImage::new(1000, 800));
let thumb = generate_thumbnail(&img, 256);
let (w, h) = thumb.dimensions();
assert!(w <= 256);
assert!(h <= 256);
}
#[test]
fn test_concentration_to_grayscale() {
let concentration =
ndarray::Array2::from_shape_fn((10, 10), |(y, x)| (x + y) as f32 / 20.0);
let img = concentration_to_grayscale(&concentration).unwrap();
assert_eq!(img.dimensions(), (10, 10));
}
}