Skip to content

Overlay persistence v1 ​

V1 remains supported. The current V2 extension adds real packed polygons, ordinary GeoJSON, raster modes and region bounds; use the developer layer guide for current capabilities.

Implemented experimental interchange for scored points, scalar rasters, and the existing synthetic renderer recipes. This is a small first format, not a universal model-output schema. Annotations retain their existing format; overlay bytes never enter annotation exports.

Location and ownership ​

text
sample.svs.microscope/
  slide.json
  projects/default/                 # or a stable collection project ID
    annotations.json
    layers/
      overlays.json
      overlays.previous.json
      assets/
        <sha256>.npy

overlays.json belongs to one workspace ID and one project namespace. Projects reference the slide; they do not duplicate its image. Default and collection namespaces have independent stacks/assets. Removing a layer or project membership never deletes files. Assets referenced by previous saves are retained; there is no garbage collector yet. Move the image and sidecar together.

Stack manifest ​

json
{
  "format": "microscope-overlays",
  "version": 1,
  "workspaceId": "matching-slide-manifest-id",
  "project": "default",
  "coordinateSystem": "level-0-pixels",
  "layers": []
}

Layers are listed bottom to top, below editable ROIs. Each entry has a stable id, renderer, visible, opacity, palette (viridis or magma), color (six-digit hex), lineWidth, radius, binSize, threshold, valueMax, raster (mask or score), and source. Renderer names are presentation settings; source.kind defines the data meaning. Revision counters and renderer error messages are runtime-only. The index retains all settings, including those unused by its current renderer.

Source forms:

  • {"kind":"synthetic","recipe":1}: deterministic fixture generator from overlay-previews.ts, using the saved slide's dimensions, 6,000 points and up to 2,000 fixture polygons. All 12 approved renderers are supported. Recipe 1 must not change silently; a changed generator needs a new recipe version. These are clearly labeled synthetic, never model results.
  • {"kind":"points","name":"cells.npy","path":"assets/<sha256>.npy","sha256":"<64 lowercase hex digits>","byteLength":140,"shape":[1,3]}: one scored point per row, columns x, y, score. Coordinates are top-left level-0 pixels, x rightward and y downward, inside [0,width] × [0,height]. Scores are finite and in [0,1]. Row index + 1 is the displayed object number; there are no stable object IDs or arbitrary metadata columns yet. Point presentation may be ScatterplotLayer, HeatmapLayer, GridLayer, or ContourLayer. Aggregations count points, independent of the supplied score; Scatterplot colors use the score. Heatmap scale defaults are illustrative and require adjustment for actual density.
  • The same binary reference with kind: "raster" and shape: [height,width]: a scalar score map spanning the entire slide. Row-major raster origin is top-left. Pixel centers map to (column+.5)*slideWidth/rasterWidth, (row+.5)*slideHeight/rasterHeight. Scores are finite in [0,1], zero is a visible score, and there is no nodata value. BitmapLayer and TileLayer use nearest sampling and a fixed 0–1 palette. No crop offsets, rotations, categorical class masks, pyramids, or physical-unit transforms are encoded in this version.

Binary references cannot escape assets/; names are labels (maximum 200 UTF-16 code units), never paths. An asset filename is the SHA-256 of its full NPY bytes, checked on opening and saving. This full asset checksum is distinct from the slide's sampled-content fingerprint. Duplicate references reuse bytes within a namespace.

NPY subset and bounds ​

Use NumPy's NPY format, version 1.0 or 2.0, two-dimensional C-order little-endian float32 (<f4). Writers emit version 1.0. Object arrays/pickles, structured types, float64, Fortran order, and big-endian files are rejected. Shape and byte count must match exactly, with no trailing bytes. Header parsing is bounded to 4 KiB and never executes Python code.

  • Maximum 64 stack entries and 256 KiB manifest.
  • Maximum 1,000,000 point rows.
  • Maximum 8,388,608 float32 values per array (32 MiB payload plus at most 4 KiB header overhead).
  • Maximum 128 MiB of distinct referenced NPY files in one active stack.
  • Empty arrays are unsupported; remove a layer to represent no data.

These bounds are memory/sanity limits, not claims of smooth interaction at every bound. The selected slide eagerly loads and validates its referenced arrays, including hidden layers. Project rosters load only small metadata. Python stack discovery is lazy; read_array and save read/verify binary payloads. TileLayer computes visible display tiles from an in-memory array, not an on-disk chunked raster. Very large arrays still need a separate chunked/pyramidal format; Arrow/Zarr remain candidates for richer tables/geometries and arrays.

Browser workflow ​

Open a slide, choose Layers → Import points or Import score map, and select a .npy file. Import validates before changing the current stack. The filename labels the layer. Pick objects/pixels in Select mode; switch the renderer in the selected layer properties. Synthetic renderer previews remain available separately.

Save connects the containing folder and commits annotations and overlays beside the slide. Existing saved annotations or overlays trigger a load confirmation before replacing the current session. Autosave includes committed stack additions/removals, ordering, visibility and appearance, with the existing two-second debounce, draft/navigation guards, and failure pause. Annotation Export clears only exported annotation changes; it never marks an unsaved overlay saved. Stress-test cell-count controls and camera state remain session-only.

Reopening a connected slide/project loads its stack. An invalid manifest, missing/checksum-mismatched binary, or wrong workspace/project prevents that next slide from replacing the current viewer. Errors never silently become empty overlays. Remember folder/Resume works with the same client-local folder grant. Browsers without native folder access can import/view these arrays in the session, but cannot persist the stack; annotation Import/Export remains available.

Save and failure protocol ​

There is one intended writer per namespace. Browser and Python use the same optimistic index baseline check. The slide manifest must exist and match; browser saves annotations first to establish a new workspace, while Python callers creating a slide must call slide.save() before slide.overlays.save().

  1. Validate the full stack and array bounds/values.
  2. Verify slide identity and exact current index text against the baseline.
  3. Write new immutable, fully hashed NPY assets; validate any existing assets at those addresses.
  4. Recheck the manifest/index baseline; retain the previous valid stack in overlays.previous.json.
  5. Recheck again, then commit the new overlays.json index last. Update the baseline only after success.

Native browser streams commit at close and abort on failure; Python writes temporary files, fsyncs, and replaces. Failed asset/index commits leave the previously committed index valid. New orphan files/directories may remain; failed creation can leave an empty asset that a retry can complete. A previous stack may equal the current stack after a failed index commit. There is no directory transaction, multi-process lock, atomic compare-and-swap, whole-slide transaction across annotation/overlay/project files, or guarantee against a concurrent writer after the last check. Autosave failures may occur after annotations have committed; the status keeps the overall save failed/overlay changes unsaved.

External changes pause automatic saving and require reopening. Do not overwrite asset files at their content-addressed names; use add_points/add_raster to add new data, then remove old membership. Backups retain referenced arrays. There is one previous stack, not a history archive. Python slide.recover_overlays() reviews that stack without writes; call its save() explicitly to restore it. The viewer's Review previous save still reviews annotations only; overlay recovery UI is not implemented.

Python access ​

python
import microscope_io as mio

slide = mio.open_slide('sample.svs')  # dimensions discovered from existing sidecar
layers = slide.overlays
points = layers.add_points([(100, 200, .8), (120, 210, .4)], name='Cells')
mask = layers.add_raster([0, .25, .75, 1], shape=(2, 2), name='Scores')
layers.save()                       # separate from slide.save() annotations
shape, values = layers.read_array(points['id'])  # tuple and stdlib array('f')
layers.layers[0]['opacity'] = .35
layers.save()

With NumPy (optional; not an SDK dependency):

python
import numpy as np
points = slide.overlays.add_points(np.asarray([[100, 200, .8]], dtype=np.float32))
slide.overlays.add_raster(np.asarray([[0, .25], [.75, 1]], dtype=np.float32))
slide.overlays.save()
arr = np.load(slide.overlays.array_path(points['id']), mmap_mode='r', allow_pickle=False)
# Or create standalone files to import into the viewer:
np.save('cells.npy', np.asarray([[100, 200, .8]], dtype='<f4'), allow_pickle=False)

add_npy(path, kind='points'|'raster', name=...) accepts an existing compliant file. remove(layer_id) removes only membership. Layer mutations are validated at save. Follow the same one-writer workflow as annotations: turn viewer autosave off while changing files from Python, then reopen in the viewer. Numeric score/value data is read-only in the viewer; geometry editing remains an ROI operation.

Evidence ​

TypeScript tests cover restricted NPY parsing, all synthetic recipes, immutable binary ordering, backup/removal/recovery, conflicts/failures, unsafe references, score/coordinate validation, namespace isolation, and a TypeScript → Python → TypeScript round trip. Python tests cover the same APIs, lazy metadata, explicit recovery, and write refusal. Stress testing uses synthetic arrays; no representative model output or physical-GPU benchmark is claimed.