Slide Pyramid

Gigapixel images, read lazily

A six-gigapixel image that opens instantly, because you never read most of it.

Below is one microscope slide: 24 × 16 mm of glass scanned at a quarter of a micrometre per pixel. That's 96,000 × 64,000 pixels. Decoded all at once it would take 18 GB of memory. Instead it's stored as a pyramid of small tiles at ten resolutions, and the viewer fetches only the tiles on screen, at the coarsest resolution that still looks sharp. Pan and zoom, and watch how little of the file you touch.

The slide is synthetic. Every tile is computed from a seed the moment it's requested, standing in for a read from storage, so the "file" costs nothing to host. Tile sizes are estimates for JPEG at typical quality. Nothing leaves your browser.

The viewer

Drag to pan · scroll or pinch to zoom · double-click to zoom in

Overview

The pyramid

Each level halves the one below. Level 0, full resolution, holds three quarters of all the tiles; every other level together adds only a third on top. The highlighted row is the level the viewer is reading now.

Levelµm per pixelPixelsTilesShare of fileEst. sizeYou've read

From one slide to a training set

A model can't take six gigapixels in one bite, so the slide gets cut into patches. First, find the tissue: three quarters of the glass is empty, and every empty patch you keep is wasted compute and noise in training. The usual first pass is a threshold on a low-resolution thumbnail, picked automatically with Otsu's method. Then lay a grid over it and keep the patches that are mostly tissue.

kept patch kept, but it's glass or ink

Score each pixel by
Patch size

Pixel scores

The output is a table, not a folder of images

Writing every patch out as a PNG copies the slide again, badly compressed. The better output is a manifest: where each patch is, in the pyramid's own coordinates, plus whatever you need to filter on. The training job reads the pixels straight from the pyramid when it needs them.

patches

The table it lands in

CREATE TABLE pathology.patches (
  slide_id     STRING NOT NULL,
  patch_id     STRING NOT NULL,
  level        INT64,     -- pyramid level to read
  x            INT64,     -- level-0 pixel coordinates
  y            INT64,
  size_px      INT64,
  tissue_frac  FLOAT64,
  mask_version STRING,    -- which detector made it
  created_at   TIMESTAMP
)
PARTITION BY DATE(created_at)
CLUSTER BY slide_id;

-- A training split is a query, not a copy:
SELECT p.* FROM pathology.patches p
JOIN pathology.slides s USING (slide_id)
WHERE p.tissue_frac >= 0.5
  AND s.qc_status = 'pass';

Now do that for a whole lab

One slide is a curiosity. A lab scans every slide it makes, every day, and keeps them for years. Move the sliders to see what that does to storage, ingest bandwidth and patch counts.

Scan resolution

How this maps to a real build

Here's what each piece on the page would be in a production pipeline on Google Cloud.

What's simulated

PieceOn this pageIn a real system
The slideProcedural noise shaped like H&E-stained tissue, colored with the Beer-Lambert lawA scanned glass slide, 0.5 to 3 GB on disk
Reading a tileComputing it, which takes about as long as a fast network fetchA ranged read from object storage or a tile server, then a JPEG decode
File sizeEstimated per tile: about 2 KB of glass, about 23 KB of tissueWhatever the scanner's encoder produced; measured, not estimated
Tissue detectionOtsu on saturation or darkness, plus a color rule for blue-green inkThe same first pass, often followed by a small segmentation model that also catches folds, bubbles and blur
"Junk" and "missed"Known exactly, because the slide is generated from known fieldsUnknown without annotation, which is why a sample of masks gets reviewed by eye

The engine is separate from the page and has its own tests. Read it in slide-pyramid/, and the tissue generator in shared/.