Formats and URLs¶
Every dataset is served through every frontend simultaneously. Given a dataset named
em on http://localhost:8000:
| frontend | Neuroglancer source URL | notes |
|---|---|---|
n5 |
n5://http://localhost:8000/em/n5 |
multiscale group, s0..sN; gzip or raw chunks |
zarr |
zarr2://http://localhost:8000/em/zarr |
Zarr v2 + OME-NGFF 0.4 multiscales; consolidated .zmetadata too |
zarr3 |
zarr3://http://localhost:8000/em/zarr3 |
Zarr v3 + OME-NGFF 0.5 in group attributes |
precomputed |
precomputed://http://localhost:8000/em/precomputed |
raw encoding, 3-D or 4-D (c,z,y,x) only; HTTP gzip when accepted |
mesh |
precomputed://http://localhost:8000/em/mesh |
a surface of the dataset as Neuroglancer meshes, segment 1, each fragment meshed when fetched, at one resolution or several (finer where you zoom); see Meshes |
A cache-busting token may be inserted after the name: /em/@{digest}/zarr3. The API always
hands out this form; the plain form always serves the current pipeline.
Chunk paths¶
| frontend | metadata | chunk key |
|---|---|---|
n5 |
attributes.json, s0/attributes.json |
s0/x/y/z (x first) |
zarr |
.zgroup, .zattrs, s0/.zarray |
s0/z/y/x or s0/z.y.x (both accepted) |
zarr3 |
zarr.json, s0/zarr.json |
s0/c/z/y/x |
precomputed |
info |
s0/x0-x1_y0-y1_z0-z1, must align to chunk grid |
Internally arrays are numpy C order (z, y, x). N5 and precomputed list axes x-first, so
their metadata and keys are reversed relative to zarr.
The group and each level also answer as directories, for clients that browse a store
rather than name its keys: a path ending in /, or a group or level path asked for with
HTML first in Accept (as browsers and Java's HTTP client send), gets a listing in the
form Python's http.server writes, with the metadata keys and one sN/ per level, never
the chunks. That is how Fiji's N5 viewer finds the levels (n5's HTTP access lists them;
checked with n5 4.0.1 and n5-zarr 2.0.1 for N5, zarr v2 and v3). Clients that ask for
*/*, as Neuroglancer, tensorstore and zarr-python do, get the metadata at those paths as
before. Precomputed has no directories.
| client | reads | checked |
|---|---|---|
| Neuroglancer | all four formats | the docs' demos |
| Fiji / BigDataViewer (n5-universe) | N5, zarr v2, zarr v3, and browses the levels | n5's HTTP access reading every format and listing the levels; the Fiji application itself not run |
| zarr-python, dask, napari's zarr reader | zarr v2, v3 | zarr-python 3.4 and dask reading a pipeline over HTTP |
| tensorstore | all four formats | tests/test_clients.py, against a running server |
| webKnossos | zarr v2, v3, N5, precomputed | not run; it asks for byte ranges only of sharded data, which the server does not produce |
Compression¶
Zarr2Frontend, Zarr3Frontend accept compressor="blosc" | "zstd" | "gzip" | "none",
blosc (zstd with byte shuffle) by default: on a 64×256×256 chunk it encodes float32 30
times faster than gzip and uint8 about 30 times faster, at the same ratio, and every
zarr reader listed above reads it except Fiji and BigDataViewer without the native Blosc
library (--compressor gzip for them). N5Frontend accepts gzip or raw. Precomputed raw is uncompressed by definition, so
the server applies Content-Encoding: gzip when the client accepts it.
Edge chunks¶
Zarr requires full-size chunks, so edge chunks are zero-padded (by the browser engine too). N5 and precomputed encode the clipped size.
GeoZarr, for map clients (browser engine)¶
The browser engine serves each view twice: as OME-Zarr 0.5 for Neuroglancer
(virtual/<page>/<view>/) and, for a view read from a georeferenced source (a GeoTIFF), as
GeoZarr for map clients such as OpenLayers (virtual/<page>/geo/<view>/). The GeoZarr
group carries the zarr-conventions multiscales (one layout entry per level, its
spatial:shape), proj: (proj:code, the projection the page names: a map client must
know it, as the Moon page registers the lunar south polar stereographic) and spatial:
(spatial:dimensions [y, x], spatial:bbox the pixels' corners) attributes; level <i>
is a group holding the view as its one band, <i>/<view>, a 2-D y, x array (the view's
one z plane; a map reads 2-D bands). Chunks are the same chunks, computed once whichever
layout asks. The Python server has no GeoZarr frontend yet. The engine also serves each
view as zarr v2 with OME-Zarr 0.4 (virtual/<page>/zarr2/<view>/), the same chunks, for
readers that predate zarr v3's final spec: GDAL 3.8, as the fires demo runs it in the page
(gdal3.js), opens a level as a zarr array and translates it to a GeoTIFF.
Meshes, computed when fetched¶
The mesh frontend serves a dataset's surface in Neuroglancer's precomputed mesh formats,
for a segmentation layer whose source is precomputed://.../<name>/mesh, segment 1.
One resolution (the default): the legacy format. info
({"@type": "neuroglancer_legacy_mesh"}), the manifest 1:0 listing one fragment per chunk
of one level, and each fragment 1:0:i_j_k, meshed when it is fetched (uint32 vertex
count, float32 x, y, z per vertex in nanometres, uint32 triangle corners). A fragment is
its chunk plus one voxel on its high sides, so neighbouring fragments meet exactly (the
surface is watertight across them), and closed at the array's edges. Neuroglancer fetches
every fragment of the manifest, so the level bounds the work, and zooming in shows nothing
finer.
Several resolutions (lods above 1): the multi-resolution format, whose meshes get finer
where the viewer zooms in. Level of detail i is the pyramid level level - lods + 1 + i,
its octree nodes that level's chunks, so each finer level splits a node in eight. info
(neuroglancer_multilod_draco, 16-bit positions), the index 1.index (per level of detail
its nodes and their sizes; each level's voxel size is its scale, the offset of its origin
its vertex offset) and the fragments' file 1, which Neuroglancer reads by HTTP Range
requests, one node at a time, for the nodes in view at the detail it wants (a segmentation
layer's meshRenderScale: larger is coarser). The format wants every fragment's size before
any is fetched, which on-demand meshing cannot know, so every fragment is padded to one size
(128 KB; Draco decoders read what they need and ignore the rest, and the server gzips them,
so a padded fragment costs a few hundred bytes to a few tens of kilobytes on the wire). A
node's mesh is marching cubes on each of its octants apart (no triangle may cross one, so
Neuroglancer can show part of a coarse node while its children load), its vertices integers
across the node, coarsened past 100,000 triangles, and encoded with Draco (DracoPy, in the
mesh extra; the browser engine uses Draco's own WebAssembly build). Only nodes within two
voxels of the coarsest level's surface are listed, which the index is made from when it is
first asked for (the whole coarsest level is read once): a finer level's surface lies near
the coarser one's, and a listed node that turns out empty costs only its fetch. Four levels
of detail of the Mandelbulb from its 256³ level list about 47,000 nodes, a 750 KB index;
more than two million are refused.
The spec's mesh (CLI --mesh) says what is meshed: kind: surface (default) is the
boundary of the voxels at or above threshold (128), by marching cubes (scikit-image, the
mesh extra); kind: terrain is an elevation model (y, x, or z, y, x with one z) as
two triangles per cell, its elevation times exaggeration as height, cells with a NaN
corner left out (one resolution only). level picks the level, the coarsest with lods
(default: the finest whose longest side is at most 512 voxels), and lods the levels of
detail. The browser engine serves the same at virtual/<page>/<view>/mesh, its workers
running the same module (chunkmirage.meshes); for the multi-resolution index it masks the
coarsest level's eighths in its workers at once (each with a border, for its surface band),
and cuts the coarsest nodes' fragments from that mask (the Python server does the same), so
the coarsest level is computed once. The index has to be made from the whole coarsest level
before Neuroglancer can ask for any fragment: the Mandelbulb's appears after about 16 s in a
browser, against 8 s for its single-resolution mesh.
chunkmirage serve 'synthetic://mandelbulb?shape=268435456,268435456,268435456&voxel_size=1&unit=nm' \
--mesh threshold=255,level=20,lods=4 --chunk 32,32,32 --python-viewer
Data types¶
Precomputed supports uint8, uint16, uint32, uint64, float32 only; add a cast op
for anything else. Precomputed volumes are typed by what their values are (ArrayInfo.kind,
which ops declare): labels and masks segmentation, images image; where nothing says,
uint32/uint64 are segmentation and others image. Override with
PrecomputedFrontend(volume_type=...).
Stored arrays (zarr, N5, precomputed, HDF5) say nothing of what their values are, so their
kind is guessed from the dtype: integers of 32 bits or more are labels, booleans masks, the
rest unknown. Labels and masks are resampled by nearest voxel and downsampled by their most
common value, never averaged, so no ids are invented between two segments. A spec's kind
(open_source(..., kind=...) in Python) overrides the guess: "kind": "image" for a uint32
image, "label" for ids stored as uint16.
Sources¶
Synthetic (procedural) sources¶
synthetic://<kind>?shape=z,y,x&chunk=64,64,64&levels=N&seed=0&voxel_size=8&unit=nm
generates data on the fly from voxel coordinates. Nothing is stored, so the volume can be
as large as you like, and each scale level is the same function sampled at a coarser
spacing, so the pyramid is exact (s1[z,y,x] == s0[2z,2y,2x]). Kinds: blobs (Gaussian
blobs), shells (hollow spheres, membrane-like), noise (fractal value noise), julia
(a 3-D slice of a quaternion Julia set), mandelbulb (the power-8 Mandelbulb, to zoom
into); combine with +, e.g. blobs+noise. Useful for demos and for stress-testing
pipelines without I/O. Generation is vectorised numpy, so the server's threadpool runs it
on all cores, and the browser engine's Pyodide workers run the same module for a view
whose source is synthetic:// (nothing is read).
mandelbulb spans [-1.25, 1.25] on every axis in float64 from integer indices, so an
array 2^28 voxels across (21 levels, 10^25 voxels) still resolves its finest voxels. Its
value is 4 × the smooth escape iteration (255 inside), the same at every level, so a
colour stays a colour as the viewer changes level; and it is the one kind whose levels are
not exactly the same function: a level iterates 10 + 3 log2(256 / voxels across) times,
more the finer it is, as fractal zoomers do, so zooming in resolves new detail.
Scene sources (OME-Zarr 0.6 transformations)¶
scene://<group>?image=<image path>&target=<image path or coordinate system>
serves one image of an OME-Zarr 0.6 scene resampled onto another grid through the scene's coordinate transformations, including displacement fields. No viewer applies those yet: Neuroglancer refuses to load an image whose transformation is a displacement field, and napari and vizarr skip them. Served this way, every viewer and every zarr reader sees the registered volume, and nothing is written. Each output chunk is computed on its own: its voxel centres go through the transformation chain into the source image, the exact bounding box of the results is read at the matching resolution level, and the values are interpolated. Ops can follow as with any source.
chunkmirage serve "scene:///data/fly_brains.zarr?image=FCWB&target=JRC2018F"
examples/fly_brain_registration.py builds that scene from the public RFC-5 example (two
Drosophila brain templates, a displacement field plus affine between them) and serves the
registered template next to the fixed one.
| parameter | default | meaning |
|---|---|---|
image |
the group itself, if it is a multiscale image | image to resample, relative to the group |
target |
the scene's first coordinate system, else the image's own | an image path (its grid and levels are used) or a coordinate system name |
interpolation |
linear; nearest for label images |
nearest keeps label ids exact |
inverse |
exact |
approx also walks displacement fields backwards, estimating their inverse by fixed-point iteration |
shape, voxel_size, translation, levels, chunk |
the target image's grid, else the source image's | explicit output grid, e.g. for a scene coordinate system with no image: spatial axes, C order, target units; one value applies to every axis |
field_cache_gb |
0.0625 |
decoded-chunk cache per displacement field; raise it for fields stored in large chunks so each is decoded once (see below) |
large_field_chunks |
0 |
1 accepts fields stored in chunks over 256 MiB, decoding them one at a time (see below) |
Finding the transformation. Coordinate systems (the scene's own and each image's) are
the nodes of a graph and transformations are its edges. The shortest path from the target
to the image's intrinsic system is used. An edge is walked backwards only if it has a
closed-form inverse (scale, translation, affine, rotation, mapAxis, and sequences or
byDimension of those) or is a bijection. Registration tools usually store fixed →
moving, which is the direction resampling needs, so this is rarely a limit; otherwise
inverse=approx estimates the inverse of displacement fields. Where a field folds there
is no unique inverse, and those voxels are left empty rather than filled wrongly.
Details.
- Transformation types: identity, scale, translation, affine, rotation, mapAxis,
projectAxis, sequence, displacements, coordinates, bijection, byDimension; parameters
inline or stored under
path. - Fields: 0.6 multiscale field groups (vector axis first), and the draft layout (a bare
array with the vector axis last, as BigWarp exports). Outside its grid a field takes the
nearest edge value.
cubicfield interpolation is read as linear. - Time and channel axes of the image pass through unchanged; transformations act on the spatial axes. The output is the image's leading axes plus the target's spatial grid.
- Each output level reads the coarsest source level that is still at least as fine as one output voxel, measured through the transformation at the grid centre, so zoomed-out views read small levels.
- Voxels that map outside the image are 0.
- Field chunking decides speed and memory. Each output chunk reads the small window of the
field it needs, but a store decodes whole chunks: a field saved as a few huge chunks (for
example one full-z, full-x band per writer job) costs a full-chunk decode per output
chunk, and a viewer's parallel requests start several at once. A 113 GB field in 6.6 GiB
chunks exhausted a 93 GB workstation this way. So fields whose chunks exceed 256 MiB are
refused, with the numbers, unless
large_field_chunks=1; then they are decoded one at a time, andfield_cache_gbabove one chunk decodes each only once. Better: store fields in chunks of about 64³, or sharded with small inner chunks. - One output region may read at most 2 GiB of the source image; a transformation that
spreads a chunk over more than that is refused rather than allocated. Use smaller output
chunks (
chunk=) if you hit it. - Also read: the 0.6 drafts' string
input/outputreferences,multiscalesobjects and snake_caseinput_axes. - The RFC-5 transformation conformance
suite runs in the tests
(
tests/data/rfc5_conformance).
The cache key of each level folds in the whole transformation chain, so editing the
scene's metadata and re-PUTting the dataset gives new URLs. A field rewritten in place
under the same path keeps its key; restart or clear the cache after doing that.
Warp sources (procedural deformations)¶
warp://<image>?field=swirl&angle=90&radius=200¢re=z,y,x&plane=y,x
warp://<image>?field=swirls&count=8&seed=0&angle=90&radius=200
serves <image> (anything above or below, including synthetic:// URLs) twisted on its
own grid by swirls computed from coordinates, and with &show=field the displacement
itself: a (c, z, y, x) float32 volume whose three components are the z, y and x
displacement in the image's units. Nothing is stored, not even the field, so any
parameter can change at any time: PUT a new URL and the viewer refetches what is on
screen. The resampling is the same as for scene sources, and each chunk reads only the
compact region its rotated voxels come from, however large the displacement.
swirl is one twist about an axis along a spatial axis (z for the default plane=y,x),
fading with distance from that axis, so it is a column through the volume. swirls
(3-D images) are count balls of twist at random places, each about its own axis in a
random direction and fading with distance from its centre, so they move points along z
too. Their displacements add; where they overlap the sum is smooth but no longer a pure
rotation. The same seed gives the same swirls.
With &frames=N the swirl grows along a new leading t axis, from none at frame 0 to
angle at the last, so a viewer animates it by playing t (Neuroglancer's playback)
instead of loading new URLs: each frame is its own chunks, computed when first shown and
cached from then on. examples/swirl_demo.py shows the original, the swirled image and
the field side by side and plays the frames.
| parameter | default | meaning |
|---|---|---|
field |
(required) | swirl or swirls |
angle |
90 |
twist on the axis, degrees; it fades as exp(-(r/radius)²) with distance r from the axis (swirl) or the centre (swirls). Each of the swirls turns by ½ to 1 × angle, either way |
radius |
swirl: a quarter of the smaller in-plane extent; swirls: a fifth of the smallest extent |
fall-off distance, image units; each of the swirls gets ½ to 1 × radius |
centre |
the volume centre | image units, C order: a point on the axis (swirl), the middle of the box the centres fall in (swirls) |
plane |
the last two axes, e.g. y,x |
swirl: the two axes that turn |
count |
8 |
swirls: how many |
seed |
0 |
swirls: random seed for their centres, axes, radii and angles |
spread |
half the volume | swirls: half-size of the box around centre their centres fall in; one value or z,y,x |
show |
image |
field serves the displacement instead |
frames |
none | frames on a leading t axis; frame i twists by angle·i/(frames−1). An image's own t axis must hold one time point, which the frames replace |
chunk |
the image's | output chunk shape of the spatial axes, C order; thin chunks such as 8,128,128 compute less for a view of one plane |
interpolation |
linear; nearest for uint32/uint64 |
as for scene sources |
The parameters follow the last ?, so the image URL may carry its own query. To show the
field as RGB, its components must be a Neuroglancer shader channel dimension (c^, read
with getDataValue(0..2)). The precomputed frontend makes them one, but holds only
(c, z, y, x); with frames, serve zarr and rename the channel dimension c' to c^
(Viewer.rename_dimensions, as the demo does).
Register sources (deformable registration on a GPU)¶
register://<moving>?fixed=<fixed>&affine=<matrix.npy>
register://<moving>?fixed=<fixed>&affine=<matrix.npy>&show=pair
serves <moving> registered onto <fixed>'s grid, at every level of <fixed> and with
every channel of <moving>. Opening the source solves for a smooth displacement field u
in the fixed image's space such that the moving image sampled at affine(p + u(p)) looks
like the fixed image at p. The solve reads a few coarse levels of both images, fits
their local normalized cross-correlation with a penalty on the field's gradient, and runs
Adam from coarse levels to fine ones, on a GPU when there is one (PyTorch:
pip install chunkmirage[gpu]; otherwise the CPU, slowly). Left out of the fit are
windows with no contrast in either image, and fixed voxels whose window the moving image
does not cover under the affine (a moving image with a smaller field of view): there is
nothing to match, and fitting them would drag the moving image's edge over whatever lies
beyond it, so the field there follows from its smoothness. The field lives on a control grid a few voxels
apart, so it is small (megabytes for an organ) and stays in memory. Every output chunk is
then the moving image resampled through it by the scene sources' resampler, so full
resolution is served without anything being written.
u has the convention registration pipelines write to disk (a displacement in fixed
space, applied before the fixed-to-moving affine, e.g. bigstream's), so it can be
compared with theirs directly (show=field), and the moving image's other channels come
out registered too. On a 1231×14124×5452 EASI-FISH gut round (an RTX 2080 Ti, the affine
from a keypoint fit, scored at level 3, finer than the solve): levels 6 and 5 solve in
2.4 s and beat a bigstream registration on local correlation and on the overlap of bright
voxels; levels 6 to 4, the default, take 15 s and match a cluster block-matching
registration's local correlation, 0.02 below it on overlap, its field within a median
13 µm of theirs.
Solved fields are remembered per process (the last 8), so opening the same URL again (a
downstream op edit, or another show) does not solve again, while changing a solver
parameter does. The solve runs in whatever opens the source (server start-up, or the
PUT that sets it), plus about 10 s the first time to import PyTorch from a network
file system.
With refine=n, the n levels below the solved ones are fitted where they are looked
at, not up front. Each gets its own control lattice, grid voxels apart, in blocks of
block voxels (64×128×128 by default, whatever the output's chunks), and a block is
fitted when a chunk it covers is first requested: from the
solved field over the block plus halo voxels of context, against the fixed and moving
voxels those reach, coarse to fine over halved copies of them (down to about the solved
level's resolution) for that level's iterations per copy; then it is kept in the
server's chunk cache with the computed chunks, so --cache-gb bounds it and
DELETE /api/cache clears it (1 GiB of its own when opened from Python without a cache). Every block starts from the solved field, so no level waits on
another's blocks. The finest fitted level's field serves every level below it. So the fit's
detail grows with the zoom and its cost with what is viewed rather than with the volume,
and whatever asks for chunks drives it: Neuroglancer, Fiji, webKnossos, a dask array.
Blocks are fitted separately, each over its context too, so neighbouring blocks overlap,
and they are blended there, as bigstream blends its blocks: a lattice point's value is the
average of the blocks whose fits cover it, each weighted 1 over its own block and less the
further into its context, so the field turns from one block's fit to the next's across the
overlap. Without it, neighbouring fits that disagree made the field step at their shared
edge: on the EASI-FISH pair below, by up to 0.9 µm per voxel, three times the steepest 1%
inside a block, enough to shift structures by a few voxels at a seam. Blended, the field
changes no faster across block edges than inside a block (0.17 to 0.21 µm per voxel at the
99th percentile, against 0.18 to 0.24 inside). A block's fit still depends only on the
solved field and the images, so the result does not depend on which blocks were fitted
first. The price is a ring of blocks: a chunk at a block's edge needs the block on the
other side too (zooming the browser page into the EASI-FISH pair, 66 blocks instead of 45,
and the view complete in 115 s instead of 98 s). On the tests' swirled volume the block
fit and a whole-image solve of the same levels agree to 2×10⁻³ in correlation with the
fixed image.
The window matters most at the fine levels. Two rounds of one EASI-FISH fly brain sit in a public bucket that allows any origin (3.4 G voxels per channel, 0.23 µm), so this runs from anywhere:
E=https://janelia-data-examples.s3.amazonaws.com/fly-efish/NP31_R2_20240119
chunkmirage serve "register://$E/NP31_R2_2_1_SS00090_FMRFa_546_Proc_647_1x_Central.zarr/0?fixed=$E/NP31_R2_1_1_SS00090_Spab_546_Nplp1_647_1x_Central.zarr/0&affine=auto&refine=3&iterations=100,40,40,40&window=15,31,31,31&show=pair" --https
affine=auto finds in 2 s how round 2 was remounted: turned over (z and x reversed, a half
turn about y) and tilted about 22° in the z-y plane. Level 3 (the default, 54 M voxels)
solves in 20 s on an RTX 2080 Ti, and levels 2 to 0 are fitted as you zoom. Whether round 2
is instead a mirror image (z alone reversed, as a stack taken the other way round would be)
the images cannot say: the brain is nearly symmetric, and a tilted mirror, given as the
affine, correlates as well (0.826 against 0.822), full-resolution chunks splitting between
the two. The half turn needs the smaller field afterwards (a median 0.2 µm against 0.5 µm),
so the search's rotation is kept; whoever knows the round was a mirror gives its affine.
Three full-resolution chunks in the tissue correlate with the fixed image at 0.75, 0.64 and
0.86 through the level-3 field, and at 0.85, 0.77 and 0.87 through blocks fitted with the
command above. Measured with blocks of 128³ voxels, the window matters most: 7 gives 0.79,
0.69 and 0.86, 15 gives 0.83, 0.75 and 0.87, 31 gives 0.86, 0.79 and 0.87 (a wider window
helps the level-3 solve too: 15 there alone gives 0.82, 0.73 and 0.86), while a larger halo
or more iterations changed nothing. Deeper blocks fit better, having room to go coarse to
fine: with the command's windows, blocks of 16×128×128 voxels reach 0.83, 0.75 and 0.87,
64×128×128 (the default) 0.85, 0.77 and 0.87, and 128³ 0.86, 0.79 and 0.87. The first
chunk of a region takes about 20 s, mostly its blocks (with the neighbours its
interpolation touches), then about 10 s a chunk, and blocks are kept, so a region fills in
faster the longer it is looked at.
affine=auto finds the starting affine from the images (the coarsest solved level of
each, halved to a few hundred thousand voxels): their intensity moments are matched, centre
to centre and principal axis to principal axis, the best-correlated of the orientations
that do not mirror the image is kept, and the 12
numbers are then fitted by gradient ascent
on the normalized cross-correlation at two resolutions, as the browser page does. The
moments assume both images show the same whole object; a crop of one needs an affine given.
A mirror image is not searched for: on a nearly symmetric specimen a mirror correlates as
well as the right rotation while swapping left and right (on the fly templates exactly as
well, 0.872, and 226 µm from the published affine), and a fit cannot reach one from a
rotation (it would pass through a matrix that flattens the volume). An affine with a
negative determinant, such as z reversed, is used as given. A found affine is
remembered per image pair, like the solves.
show=pair serves a (c, z, y, x) volume whose two channels are the fixed image's
matched channel and the registered moving image's, so one Neuroglancer shader can compare
them on the viewer's GPU (overlay, checkerboard, fade, difference) with no refetch. The
source carries that shader, so the link chunkmirage serve prints, the REST API's
neuroglancer routes and the python viewer show the pair in colour without being asked:
fixed magenta, registered green, white where they agree, with contrast from the coarsest
solved level and a mode control for the other three comparisons. show=field likewise
comes as a heat map of how far each point moved (a direction box colours by direction).
examples/register_demo.py shows that next to the affine alone (iterations=0), and
re-solves when you type new settings, the viewer keeping its camera.
| parameter | default | meaning |
|---|---|---|
fixed |
(required) | the fixed image: anything open_source reads, with the same spatial units as <moving>; percent-encode it if it has a query |
affine |
identity | fixed-to-moving affine in physical units, C order: a .npy or text file with a 4×4 or 3×4 matrix, or its 12 or 16 values inline, row by row; auto finds one from the images |
fixed_channel, moving_channel |
0 |
the channel each image is matched on, for images with a c axis |
levels |
from the coarsest with ≥ 16 voxels on every axis to the finest with ≤ 2²⁵ | fixed-image levels to solve on, coarse to fine, e.g. 6,5,4; each is matched with the moving level nearest its voxel size |
iterations |
100 |
Adam steps per level: one value, one per level, or one per level with the refined ones; 0 leaves the affine alone (no GPU needed) |
refine |
0 |
levels below the solved ones fitted block by block where they are viewed: refine=3 with levels=6,5,4 fits levels 3, 2 and 1 |
halo |
8 |
voxels of context on every side of a block being fitted: how far beyond it the fit looks |
block |
64,128,128 |
voxels per refined block, C order, independent of the output's chunks; deeper blocks fit better (coarse to fine within them), thinner ones answer a single slice sooner |
smooth |
1 |
weight of the penalty on the field's gradient |
grid |
4 |
control-point spacing, voxels of each level |
window |
7 |
correlation window, voxels (odd): one value, one per level, or one per level with the refined ones; fine levels gain from a wider one (see below) |
show |
image |
pair (fixed and registered as two channels) or field (u, components first, physical units) |
frames |
none | a leading t axis of that many snapshots of the solve, from the affine alone to the final field: play t to watch it converge. The moving image's own t axis must hold one time point, which the frames replace |
chunk |
the fixed image's | output chunk shape of the spatial axes, C order |
interpolation |
linear; nearest for uint32/uint64 |
as for scene sources |
device |
auto |
auto is the GPU with the most free memory, else the CPU; or cpu, cuda:1, ... |
The parameters follow the last ?, so the moving image's URL may carry its own query (a
warp:// URL, say, which is how the tests check that a known swirl is undone). They are
one Pydantic model, RegisterParams, whose JSON Schema (chunkmirage schema) the browser
engine's types and form defaults are generated from: the
browser page solves the
same spec on the viewer's GPU and shows the chunkmirage serve 'register://…' command for
its settings, and fed the same affine and levels the two fields agree to 0.01 µm (median)
on the fly templates, whose field moves tissue by 7 µm (median). It fits blocks on demand
(refine) the same way, on the viewer's GPU, so the EASI-FISH pair above runs from a link
with nothing installed:
register.html with the EASI-FISH rounds
(it shows the two rounds; Register solves).
Stack sources (several images as one array's channels)¶
stack://<image>|<image>[|<image>...]
serves images that share a grid as the channels of one (c, z, y, x) array ((c, y, x) for
images in y and x, such as GeoTIFF bands): channel 0 is
the first image's first channel, channel 1 the second's, and so on. The images must agree
level by level on shape, voxel size, translation and units (the stack has as many levels as
the image with the fewest), and each is anything open_source reads, so a stack can hold a
stored volume next to a synthetic:// or register:// one. Nothing is copied: a stack chunk
reads the same box of each image, through that image's own cache.
A stack exists for ops that need two images at once. Such an op takes the channel axis and
returns an array without it; the pipeline reads every channel and pads only the spatial axes
by the op's halo, and --chunk may then give just the three spatial values. The first such
op is contacts: the voxels within a distance of both structures.
On OpenOrganelle's published organelle predictions, mirrored back into the EM's frame (see
below), followed by label to colour and size-filter the sites:
P=https://janelia-cosem-datasets.s3.amazonaws.com/jrc_hela-2/jrc_hela-2.n5/labels
chunkmirage serve "stack://flip://$P/mito_pred?axes=y|flip://$P/er_pred?axes=y" \
--op contacts:distance=12 --op label:min_size=50 --chunk 16,128,128 --python-viewer
examples/contact_sites.py serves the same with the EM underneath and the two predictions
tinted, opens where the two organelles touch most, and takes new settings at a prompt.
Only the chunks on screen are computed, out of 122 gigavoxels of cell, and a change of
distance recomputes just those, from predictions the first pass left in the cache. The
distance is in nanometres and each level counts it in its own voxels, so a contact means
the same at every zoom (radius, in voxels, would reach proportionally further on a
zoomed-out level, as op parameters in voxels do).
Flip sources (images stored the other way round)¶
flip://<image>?axes=y[,x,...]
serves <image> mirrored along the named axes, every level in place: voxel i of a
flipped axis of length n is voxel n − 1 − i of the image, and shape, voxel size,
translation and chunks stay the image's. It repairs data whose orientation its metadata
gets wrong: OpenOrganelle's N5 organelle predictions of jrc_hela-2 (labels/*_pred) are
upside down in y relative to its EM, zarr and N5 alike (with the flip, every predicted
mitochondrion voxel of a coarse slice lies on cell; without it a fifth do), though nothing
in their attributes says so. The levels must share their centre, as OME-Zarr and COSEM
pyramids whose extents halve evenly do, since a level mirrored about its own centre would
otherwise drift from the others; flip:// refuses a pyramid whose levels share their
corner instead (as synthetic:// levels do).
Stitch sources (tiles stitched by interest points and RANSAC)¶
stitch://<BigStitcher project.xml>?channel=0&model=translation&epsilon=5&threshold=0.005
stitches the tiles of one channel of a BigStitcher project when opened, as BigStitcher's
interest-point registration does, then serves them fused, region by region: no fused copy
is written. Each overlap of two tiles' stage positions, grown by margin, is read at the
tiles' level level, and its bright blobs found in both tiles: local maxima of a
difference of Gaussians (sigma, and 2^(1/4) times it, scaled to the same physical size on
every axis) at least threshold of the region's intensity range, located to a fraction of
a voxel. A point's descriptor is the offsets to neighbors of its neighbors + redundancy
nearest neighbours (every such subset), which a shift leaves unchanged; two points match
when their descriptors are the closest pair both ways and significance times closer than
the next candidate. RANSAC then draws iterations minimal samples of the matches, keeps the
model (translation, rigid or affine) that most matches agree with to within
epsilon, refits it to them until they settle, and keeps the pair if it has min_inliers
inliers and min_inlier_ratio of its matches. Finally every tile's correction is fitted to
all the kept matches at once (each round refits each tile to where its neighbours put their
ends of its matches), one tile of each linked group held still.
The fused volume has a level for each level every tile has, the first tile's voxel size
there, covering every placed tile. A region of it is each tile that reaches it, sampled
trilinearly where its placement puts it, averaged with weights that fall off as a half
cosine over blend at the tiles' edges in y and x, so the seams do not show. The project's
images must be OME-Zarr (BigStitcher-Spark's bdv.multimg.zarr loader); a tile's stage
position is its view transforms but the outermost ones named "Stitching Transform" (a
stitching done before), and the log says how far the result lies from that one. The steps
are functions of arrays in chunkmirage.stitching (numpy and scipy, so the ops extra), the
same code the browser's stitch page runs in Pyodide.
chunkmirage serve 'stitch://https://janelia-bigstitcher-spark.s3.amazonaws.com/Stitching/dataset.xml' --python-viewer
On BigStitcher-Spark's example (a larval fly CNS in 2 × 3 tiles), the defaults keep 8 of its 11 overlaps and put the tiles 0.4 µm (RMS) from BigStitcher's own phase-correlation stitching; opening takes about 9 s, most of it reading the overlaps.
GeoTIFF sources (cloud-optimized GeoTIFFs)¶
https://.../site.tif (s3://, gs:// or a local path too; .tif or .tiff)
The tiled GeoTIFFs most geospatial and planetary rasters are published as, cloud-optimized
ones (COGs) above all, read where they are looked at: the header once (a few range
requests), then each tile a read covers, fetched by range, decoded by tifffile and kept in a
256 MB cache. The pages of decreasing size inside a COG (its overviews) are the levels, so
a zoomed-out view reads the small ones; masks are skipped. One sample per pixel; the axes
are y, x in the projection's units (metres, unitless for a geographic raster in degrees),
y counting down the image as rows do (minus the northing), voxel (0, 0) at the tiepoint
(ModelTiepoint, ModelPixelScale). A float raster's GDAL_NODATA reads as NaN. Strip
TIFFs (untiled) are refused: every read would fetch whole rows. Needs the tiff extra
(tensorstore's own tiff driver reads only uint8 images). The browser engine reads them
with geotiff.js, the same way, as z, y, x with one z.
chunkmirage serve 'https://astrogeo-ard.s3.us-west-2.amazonaws.com/moon/lro/lola/barker_south_pole_dems/Site01/Site01.tif' \
--op hillshade:azimuth=135,altitude=10 --chunk 256,256 --python-viewer
Stored sources¶
Sources are detected by content, not extension: zarr.json → zarr v3, .zarray → zarr v2,
attributes.json → N5, info → precomputed. A group's levels are the paths in its
multiscales[0].datasets (OME-NGFF, COSEM N5), in that order; without that, s0, s1, ...
are probed. N5 and precomputed data are transposed to C order (z, y, x) on read.
Remote stores are read the way a public dataset most likely lets one in, with credentials
only when a public read is refused. s3:// is read anonymously first, then with AWS's
default credentials (environment, ~/.aws, instance metadata), since S3 refuses even a
public read signed with a stale or foreign key. gs:// is read with Google's default
credentials (anonymously without any), then through the bucket's public
https://storage.googleapis.com URL, for credentials that are there but broken. Which way
worked is remembered per bucket. Metadata reads give up after about five seconds of
retries rather than tensorstore's default of many minutes, so a mistyped host fails fast.
A zarr v2 compressor with members tensorstore rejects, such as the checksum newer
numcodecs writes for zstd, is read without them.
Voxel size, translation, units and axes are read per level, first match wins:
| Format | Metadata, in order of precedence |
|---|---|
| zarr v2/v3 | parent OME-NGFF multiscales entry whose path is this array, composed with the multiscale-level coordinateTransformations if present (0.4/0.5; in 0.6 those lead to other coordinate systems and are applied only through a scene:// source, and axes come from the intrinsic coordinate system); else, for an array xarray wrote (geo, climate and solar data), its dimension names (_ARRAY_DIMENSIONS, or zarr v3 dimension_names) as the axes and the 1-D coordinate array named after each dimension as its spacing and origin, where evenly spaced and increasing (days since … and other CF time units become seconds, degrees unitless); else the array's own resolution/voxel_size, offset, units, axis_names (funlib) or transform (COSEM), C order |
| N5 | the array's transform (COSEM, C order); the parent's multiscales[].datasets[].transform for this path; pixelResolution/resolution (array, else group) × downsamplingFactors, plus offset, x-first |
| precomputed | resolution (nm) and voxel_offset (the first voxel's corner, in voxels) |
| HDF5 | resolution/voxel_size and offset attributes, C order |
A zarr array with CF packing attributes (scale_factor, add_offset) is read as float32
in its units, stored × scale_factor + add_offset, and its _FillValue (else the array's
fill value) as NaN: NASA's MUR sea temperature, stored as int16 hundredths of a degree
offset from 298.15 K, reads as kelvin with land NaN. Arrays without those attributes are
read as stored. Data whose axes are not named z, y, x (time, lat, lon) keeps its own
names: the OME frontends type a time axis as time (by name or a time unit), and the
viewer helpers show its last three axes as x, y and z.
Unitless z, y, x axes are served as nanometres by every frontend (left without a unit,
Neuroglancer would read OME axes as metres). Neuroglancer counts zoom
(crossSectionScale) in the smallest scale among the viewer's dimensions, whatever their
units, so beside a time axis in seconds a zoom of 1 is not one pixel per voxel:
chunkmirage.neuroglancer.cross_section_scale(dims, axis, voxels_per_pixel) converts.
offset/translate are in world units. A translation is where voxel 0's centre is, as in
OME-Zarr, COSEM's transform and CF coordinates. Formats that give voxel 0's corner are
moved half a voxel along their spatial axes on reading: precomputed's voxel_offset, and
funlib's offset (0 when only a resolution is given), which is how Neuroglancer and
funlib place them. N5 levels with pixelResolution and downsamplingFactors and no
offset follow BigDataViewer: a downsampled voxel's centre is the centre of the block it
covers. Served as precomputed, voxel_offset is the translation less half a voxel,
rounded to whole voxels, so a precomputed source served again keeps its offset. Served as
OME-Zarr, the translation is written as is, and Neuroglancer moves it back half a voxel to
the corner. So a volume shows in the same place whichever format serves it, except that
precomputed cannot hold half-voxel offsets. Anything in the spec (voxel_size, units,
axes, translation) overrides what was read. The spec's select pins non-spatial axes
to one index each, {"c": 1, "t": 0} (--select c=1,t=0), so the ops see one channel of
one time point as a z, y, x volume; only that channel is read. HDF5 uses file.h5::/dataset and needs
the hdf5 extra.
Your own schemes (sources from other packages)¶
A package adds a URL scheme of its own the way it adds an op: an opener that takes the URL
and returns a MultiscaleSource (a list of levels, each a Source with an info and a
read(box)), listed under the chunkmirage.sources entry point with the scheme as its
name. open_source and so chunkmirage serve then open myscheme://... like the built-in
schemes, and pipelines, caching and every frontend work on it unchanged. An opener is passed
cache_bytes (tensorstore's pool) and cache (the pipeline's chunk cache) if it takes
them. chunkmirage.sources.register_source(scheme, opener) does the same in a running
process, and schemes() lists what is known. The built-in schemes cannot be replaced.
[project.entry-points."chunkmirage.sources"]
myscheme = "mypackage.sources:open_myscheme"
A source suits work that reads the data itself rather than a padded block: a model wrapper that opens its own input, a format with its own reader, data computed from coordinates.