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 pixel | Pixels | Tiles | Share of file | Est. size | You'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
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.
How this maps to a real build
Here's what each piece on the page would be in a production pipeline on Google Cloud.
-
The file
Scanners write tiled, pyramidal TIFF variants (SVS, NDPI, MRXS and friends) or DICOM whole-slide images.
OpenSlideortiffslideread tiles by level and position without opening the rest. Converting to DICOM puts every vendor's format behind one standard, and a DICOM store can serve tiles over DICOMweb. -
Landing and orchestration
Scanners drop files into a bucket; each new object fires a Pub/Sub message. An Airflow (Cloud Composer) DAG, or an event-driven Cloud Run job, checks the file opens, registers the slide, and fans out the per-slide work.
-
Tissue detection and patching
An Apache Beam pipeline on Dataflow, one element per slide: read the thumbnail level, build the mask, filter ink and artifacts, emit patch coordinates. It's embarrassingly parallel, so a day's slides finish in minutes with enough workers.
-
Manifests, not copies
Patch coordinates go to a BigQuery table, partitioned by date and clustered by slide. Training splits, sampling and class balancing become SQL. The data loader reads pixels from the pyramid on the fly, with a local tile cache, instead of from millions of small files.
-
Viewing
A Deep Zoom or DICOMweb tile endpoint behind a CDN, and a viewer like OpenSeadragon doing exactly what this page does: pick a level, request visible tiles centre-first, draw a coarser level while they arrive, cancel what scrolled away.
-
What production adds
Lifecycle rules that move old slides to colder storage classes. A versioned mask detector, so every patch can be traced to the code that chose it. QC flags (blur, folds, ink) as columns, not deletions. Checksums on landing.
What's simulated
| Piece | On this page | In a real system |
|---|---|---|
| The slide | Procedural noise shaped like H&E-stained tissue, colored with the Beer-Lambert law | A scanned glass slide, 0.5 to 3 GB on disk |
| Reading a tile | Computing it, which takes about as long as a fast network fetch | A ranged read from object storage or a tile server, then a JPEG decode |
| File size | Estimated per tile: about 2 KB of glass, about 23 KB of tissue | Whatever the scanner's encoder produced; measured, not estimated |
| Tissue detection | Otsu on saturation or darkness, plus a color rule for blue-green ink | The 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 fields | Unknown 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/.