Project website · API documentation
wsi-rs is a Rust library for reading whole-slide images.
A whole-slide image is a scan of an entire microscope slide, usually a tissue sample in pathology. One scan holds billions of pixels, far too many to load at once. Scanners store it as a pyramid: the full-resolution image plus several smaller copies, each cut into tiles and compressed, usually as JPEG or JPEG 2000. Every scanner vendor has its own file format.
wsi-rs reads those files. You ask for a rectangle of pixels at a zoom level. wsi-rs finds the tiles that cover it, decodes them, stitches them together and gives you an image. The API is the same for every format.
OpenSlide is the C library most pathology software uses for this. wsi-rs does the same job, and:
- Is memory-safe. The library denies
unsafecode everywhere except one module that calls Apple's Metal API. JPEG, JPEG 2000 and JPEG XR are decoded by Rust codecs (J2K and JXR), not libjpeg or OpenJPEG. - Handles bad files. Every size, count and offset read from a file is checked before use, and memory is capped, so a corrupt or malicious file should get an error rather than a crash or a runaway allocation. The parsers are fuzzed. Report any file that gets past these checks as described in SECURITY.md.
- Reports decode failures. If a tile can't be decoded correctly, you get an error rather than a black or half-drawn tile.
- Can decode on the GPU. With the
metalorcudafeature, JPEG 2000 tiles decode on the GPU when that is faster on your hardware. - Works with existing OpenSlide programs. The included OpenSlide-compatible C library lets software built on OpenSlide use wsi-rs without code changes.
CI compares wsi-rs output with OpenSlide on a public corpus of real slides.
DICOM metadata uses dicom-rs. CZI reading is implemented
in this repository; LZW, DEFLATE, and Zstd decoding use the Frames
j2k-tilecodec crate. ICC profiles are presented unmodified, with their source
and provenance, through the dataset; wsi-rs does not apply them. PNG/BMP decoding, public RGB/RGBA display
buffers, and image saving use the image crate. Display methods retain their
image::RgbImage and image::RgbaImage return types and .save() support.
cargo add wsi-rswsi-rs needs Rust 1.99 or newer and a 64-bit x86 or ARM target.
Version 0.8.1 is a GitHub source release with an exact Git pin for the J2K CUDA packet-header fix. It is not on crates.io. To use this version:
[dependencies]
wsi-rs = { git = "https://github.com/frames-sg/wsi-rs", tag = "v0.8.1" }For Python access to metadata and compressed tile bytes, see the experimental
reader binding. Pixel decoding and tensors use the
separate j2k Python extension.
use wsi_rs::{RegionRequest, Slide};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let slide = Slide::open("sample.svs")?;
// The pyramid of the first image, from full resolution down.
let levels = &slide.dataset().scenes[0].series[0].levels;
for (index, level) in levels.iter().enumerate() {
println!(
"level {index}: {} x {} pixels, downsample {}",
level.dimensions.0, level.dimensions.1, level.downsample
);
}
// A 1024 x 1024 region from the top-left corner of level 0.
let region = RegionRequest::builder(0usize, 0usize, 0u32)
.origin_px((0, 0))
.size_px((1024, 1024))
.build()?;
slide.read_region_rgba(®ion)?.save("region.png")?;
// The photo of the slide label, if the scanner stored one.
if slide.dataset().associated_images.contains_key("label") {
slide.read_associated("label")?.to_rgba()?.save("label.png")?;
}
// Scanner metadata, using OpenSlide's property names.
if let Some(mpp) = slide.dataset().properties.get("openslide.mpp-x") {
println!("{mpp} microns per pixel");
}
Ok(())
}Slide::open reads the file's metadata. slide.dataset() describes it:
- Scenes. Most files hold one scanned area. Some formats hold several separate areas; each one is a scene.
- Series. The image data of a scene. Nearly every scene has exactly one.
- Levels. The pyramid. Level 0 is full resolution. Each level's
downsamplesays how much smaller it is: 4.0 means a quarter of the width and a quarter of the height. - Associated images. Small extra pictures stored with the scan, such as
label,macro(a photo of the whole glass slide) andthumbnail. - Properties. Scanner metadata as text. Common values use OpenSlide's
names (
openslide.mpp-x,openslide.objective-power,openslide.vendor). Vendor values keep a vendor prefix (aperio.AppMag).
Ways to read pixels:
| Method | Returns |
|---|---|
read_region_rgba, read_region |
Any rectangle on any level, in that level's pixel coordinates. |
read_tile, read_tiles |
Tiles exactly as the file stores them. |
read_display_tile |
Tiles on a regular grid of a size you choose, whatever the file uses. Useful for viewers. |
read_associated |
An associated image by name. |
| Format | Files |
|---|---|
| Aperio SVS | .svs, .tif |
| Hamamatsu NDPI | .ndpi |
| Hamamatsu VMS and VMU | .vms, .vmu with their companion files |
| Leica SCN | .scn |
| Roche Ventana BIF | .bif, .tif |
| Philips TIFF | .tiff |
| Trestle | .tif |
| ARGOS | .avs |
| Huron | .tif |
| 3DHISTECH MIRAX | .mrxs with its data folder |
| Olympus VSI | .vsi with its .ets files |
| Zeiss CZI and ZVI | .czi, .zvi |
| DICOM whole-slide images | .dcm files or a folder of them |
| OME-TIFF | .ome.tif, .ome.tiff |
| Other TIFF | .tif, .tiff |
| Raw JPEG 2000 | .j2k, .j2c |
| wsi-rs pre-decoded cache | .svcache (see below) |
Sakura is not supported: there is no public sample file to test against.
- Zeiss CZI: single-plane RGB brightfield scans, stored uncompressed, as JPEG or as JPEG XR. Fluorescence channels, z-stacks and other pixel types return an error. Multiple scenes are combined onto one canvas.
- OME-TIFF: single-file OME-TIFF with tiled planes. Each plane's SubIFDs
are its pyramid levels. Planes can be 8-bit RGB, or 8-bit, 16-bit or float
channels, with Z planes and time points. Images named
label,macroorthumbnailbecome associated images; other images become scenes. Multi-file datasets,.companion.omemetadata and stripped planes return an error. - Other TIFF: any tiled TIFF. A TIFF stored in strips opens when its largest image is 8-bit grayscale or RGB, uncompressed or compressed with LZW, Deflate, Zstd or JPEG; its other images become associated images. A read decodes only the strips its tiles cover, so the whole image may exceed the decoded-image limit. Strip images with 16-bit samples, a color palette, CMYK or alpha return an error. JPEG XR tiles must be 8-bit gray or RGB without alpha.
- DICOM: unsigned 8-bit images and images stored in 16-bit words. Native,
RLE, lossless JPEG, JPEG 2000 and HTJ2K frames keep 9- through 16-bit samples
as
Uint16; JPEG Extended and progressive JPEG frames keep their 12-bit precision. Higher-bit compressed frames decode on the CPU. - Hamamatsu VMU: samples are 12-bit and come back as 16-bit samples with all bits kept. Test files are generated from OpenSlide's NGR layout description; no real VMU scan is in the test corpus.
To display higher-bit samples as 8-bit RGBA, use read_region_rgba_windowed
with an explicit range, such as DisplayWindow::new(0.0, 4095.0)? for 12-bit
data.
Each slide caches decoded tiles in 128 MiB by default: 64 MiB for decoded
tiles, 32 MiB for display tiles and 32 MiB for format-specific data. Set your
own sizes with SlideOpenOptions::with_cache_config.
Every slide also has hard limits that protect against bad files:
| Limit | Default |
|---|---|
| All metadata in a slide | 128 MiB |
| One metadata value | 16 MiB |
| Tile index | 128 MiB |
| One compressed tile or frame | 128 MiB |
| One decoded tile or image | 128 MiB |
| One region | 33,554,432 pixels (128 MiB as RGBA) |
| Scratch memory per operation | 384 MiB |
| Memory in use per slide at once | 512 MiB |
Large batches are split and processed in order. A request fails only when a
single tile, image or region is over a limit. To change the limits, pass a
SlideLimits to SlideOpenOptions::with_limits.
| Feature | Hardware |
|---|---|
metal |
Apple GPUs on macOS |
cuda |
NVIDIA GPUs |
Both features accelerate JPEG 2000 and HTJ2K tiles. The normal read methods still return tiles in CPU memory. Behind the scenes, wsi-rs times the GPU and the CPU on the first few reads of each kind of tile and uses the GPU only if it is at least 15% faster. If the GPU fails, the read uses the CPU. To turn this off, open the slide with:
SlideOpenOptions::default().with_decode_execution_options(
DecodeExecutionOptions::default().with_acceleration(DecodeAcceleration::CpuOnly),
)To keep decoded tiles in GPU memory, call read_tile_metal, read_tiles_metal,
read_tile_cuda or read_tiles_cuda. These only handle JPEG 2000 and HTJ2K
tiles and return an error for anything else. They never fall back to the CPU.
Call download_cpu() on the result to copy a tile to CPU memory.
Decoding JPEG and JPEG 2000 is most of the work of reading a slide. A
.svcache file stores every tile of every level already decoded (compressed
with zstd), plus the associated images, so reads skip JPEG and JPEG 2000
decoding.
Build one next to the slide:
cargo run --release --bin svcache -- build sample.svsThis writes sample.svs.svcache. To use it, open the slide with
SlideOpenOptions::with_svcache_policy:
| Policy | Behavior |
|---|---|
SvcachePolicy::Off (default) |
Ignore cache files. |
SvcachePolicy::PreferFresh |
Use a cache file that matches the slide; otherwise read the slide. |
SvcachePolicy::RequireFresh |
Use a cache file that matches the slide; otherwise return an error. |
wsi-rs looks for the cache file next to the slide and in
~/.cache/wsi-rs/svcache/. Checking that a cache file matches its slide
reads the whole slide file and hashes it with SHA-256, which takes a while for
large slides.
wsi-rs logs through tracing. It installs no
subscriber; your application chooses where logs go. At debug level it logs
cache hits and misses and decode timings:
RUST_LOG=wsi_rs=debug your-appwsi-rs-openslide-shim builds a shared
library with the same C functions as OpenSlide. Programs that load OpenSlide
can load it instead and read slides through wsi-rs.
cargo xtask validate # format, lint, tests, docs
cargo xtask bench-check # build release performance and OpenSlide shim binaries
cargo xtask fuzz-check # check that every fuzz target compiles
cargo xtask rc-preflight # every release checkCommon tests run once. OpenSlide adds only oracle tests to the quick run;
its real-slide comparisons run through cargo xtask parity-corpus-test.
On macOS, the library suite and Metal surface check also run with Metal enabled.
docs/architecture.md explains how the code is organized and what the release checks need. Dependency rules are in SUPPLY_CHAIN.md.
Generated correctness regressions, including cooperative Metal decoding, run in normal tests under their relevant platform and feature. Real-slide checks and controlled timing experiments use the explicit commands below. Missing required inputs or hardware fail the run. The commands list the selected ignored tests first and reject a selection that matches no tests.
cargo xtask parity-corpus-test covers the public manifest plus DICOM folder
opening, Olympus VSI reading, and selected SVS/NDPI/DICOM color checks.
Color checks require every selected sample; they are ignored by quick tests.
WSI_RS_PARITY_ALIASES limits it to the
manifest aliases it lists, separated by commas, semicolons or whitespace; unset
or empty selects the whole corpus. When Olympus or ZVI is selected, it also runs
independent native-pixel probes. Generate those probes with the existing
SlideIO 2.9.0 environment after fetching the two samples:
scripts/parity-corpus-fetch.sh olympus-vsi-001 zeiss-zvi-001
python scripts/prepare-vendor-reference.py --output target/vendor-reference
WSI_RS_VENDOR_REFERENCE_ROOT=target/vendor-reference cargo xtask conformance-test vendorThe generator resolves these two sources from WSI_RS_PARITY_CORPUS_CACHE,
with explicit --vsi and --zvi overrides available. Corpus CI and source
release preflight create an isolated pinned SlideIO environment and run the
comparison. ZVI references must cover all three native U16 channels. No source
file is changed by generation.
cargo xtask conformance-test suite |
Required input and checks |
|---|---|
czi |
WSI_RS_ZEISS_CZI_PATH for Zeiss-5-Uncompressed.czi and WSI_RS_CZI_JXR_PATH for Zeiss-5-JXR.czi; five canvas/cache regressions and the JPEG XR readback case. |
vendor |
WSI_RS_VENDOR_REFERENCE_ROOT from the generator; independent VSI RGB and ZVI U16 pixels. |
zvi-variants |
WSI_RS_ZVI_ROOT containing the six filenames listed in tests/zeiss_zvi.rs; channels, Z planes, mosaic geometry and thumbnail decoding. |
vmu |
Independent libopenslide; the task builds the shim and points WSI_RS_VMU_SHIM_LIBRARY at it. Generated RGB12 geometry and ABI pixel parity. |
openslide |
Independent libopenslide, WSI_RS_OPENSLIDE_COMPARE_PATHS, and WSI_RS_APERIO_JPEG_RGB_PATH; region and reduced-level RGBA comparisons. |
metal-color |
macOS Metal; exhaustive YCbCr8 parity for both addressing kernels. |
metal-edges |
macOS Metal and WSI_RS_METAL_EDGE_PATHS; each source must have a regular eligible edge tile. |
local-htj2k |
macOS Metal and WSI_RS_LOCAL_HTJ2K_DICOM; real HTJ2K resident pixel parity. |
legacy-slides |
WSI_RS_NDPI_WRAPPED_FIXTURE (Hamamatsu-1.ndpi), WSI_RS_LEICA_MULTI_SCENE_FIXTURE (Leica-3.scn), WSI_RS_LEICA_FLUORESCENCE_FIXTURE (Leica-Fluorescence-1.scn), and WSI_RS_DICOM_SPLIT_FIXTURE (3DHISTECH-2 split-level folder); retained format regressions using explicit paths. |
ome-tiff |
WSI_RS_OME_TIFF_FIXTURE naming a single-file OME-TIFF; SubIFD pyramid levels and full- and lowest-resolution tiles of every channel. |
Use cargo xtask perf-experiment <test-name> for the following retained
workloads. This builds the selected target in release mode with route telemetry
and runs only the named experiment. Use separate output files for each run and
compare captures only under controlled corpus, cache and thread conditions.
These timing runs are outside ordinary correctness counts.
| Experiment name | Required environment |
|---|---|
metal_window_performance, metal_cooperative_performance |
macOS Metal, WSI_RS_PREPARE_PATH, WSI_RS_PREPARE_OUTPUT |
prepared_cpu_performance, pooled_cpu_scheduling_performance |
WSI_RS_PREPARE_PATH, WSI_RS_PREPARE_OUTPUT |
ycbcr_selected_u32_stays_within_five_percent_of_reference |
macOS Metal; controlled five-run address-width gate |
composition_performance |
WSI_RS_COMPOSITION_OUTPUT |
native_decode_matrix |
WSI_RS_DECODE_PATH, WSI_RS_DECODE_OUTPUT; optional CPU/automatic/device modes documented in the test |
native_decode_revisits, native_decode_fresh_readers |
WSI_RS_DECODE_STEADY_INPUT, WSI_RS_DECODE_OUTPUT |
panning_matrix |
WSI_RS_PAN_PATH, WSI_RS_PAN_OUTPUT |
warm_region_revisits |
WSI_RS_PAN_WARM_INPUT, WSI_RS_PAN_WARM_OUTPUT |
czi_reader_workloads |
WSI_RS_CZI_JXR_PATH, WSI_RS_CZI_PERF_OUTPUT |
local_htj2k_dicom_level_preparation_meets_interactive_budget |
macOS build and WSI_RS_LOCAL_HTJ2K_DICOM; controlled 75 ms preparation check |
The historical two-tile scheduling comparison and fixture-export test were removed. Production batch regressions and the maintained workloads above cover their relevant behavior.
Report vulnerabilities privately through GitHub. See SECURITY.md.
Dual-licensed under MIT or Apache-2.0, at your option.