Skip to content

Ops reference

Run chunkmirage ops or GET /api/ops for the live list with JSON schemas. This page must list every registered op; tests/test_docs.py enforces it.

Every op but downsample (the pipeline's own, for the pyramid of an op bound to one resolution) and python lives in chunkmirage_contrib, shipped in the same wheel and found through the chunkmirage.ops entry point like any other package's: in Python, from chunkmirage_contrib.ops import Threshold (why).

Ops that make masks (threshold, morphology, contacts) and labels (label, spots) say so (output_kind, or the kind their output_info sets), and viewers show what they make as segmentations.

Every parameter carries a description (pydantic Field(description=...)) that the control page shows under the control and GET /api/ops returns in the JSON schema. Add one to any new op; tests/test_docs.py requires it.

op parameter default meaning
threshold low 0 lower bound (inclusive), in the source's intensity units
high none upper bound (exclusive); leave empty for no upper bound
value 1 label written for passing voxels; output is uint8
cast dtype uint8 target numpy dtype name
clip true clip to the integer range first to avoid wrap-around
scale factor 1.0 multiply (contrast); output float32
offset 0.0 then add (brightness)
gaussian sigma 1.0 blur width in voxels; output float32; halo = ceil(sigma × truncate)
truncate 3.0 kernel radius in sigmas; rarely changed
uniform size 3 edge of the averaging cube in voxels; output float32; halo = size // 2 + 1
dog sigma 2.0 difference of Gaussians: enhances blobs of about this size; halo = ceil(3 × sigma × ratio)
ratio 1.6 larger blur = sigma × ratio
gain 4.0 scales the difference into 0..255 (output uint8, 128 = zero)
diff axis 0 change along one axis: each voxel minus the one lag steps before it on axis (0: time in a t, y, x series); output float32; halo = lag on that axis only; the first lag steps compare against the first
lag 1 how many steps back to compare with
gradient axes last three rate of change along each axis listed (counted from the first: [1, 2] for latitude and longitude of a time, lat, lon series), per unit of the axes (the level's voxel size): central differences, one channel each on a new leading c axis; output float32; halo = 1 + ceil(3 × sigma) on those axes, and only the interior is returned (a valid convolution)
sigma 0 smoothing first, in voxels along those axes (a Gaussian derivative), so noise a voxel or two across does not point every voxel its own way; 0 = none
downsample factor 2, 2, 2 voxels per output voxel along each of the last axes; the grid changes with it (voxels factor times bigger, the shape divided and rounded up, each voxel's position the centre of its block); how a pipeline makes the coarser levels of an op with an input voxel size
mode auto mean of each block (integers rounded), mode (its most common value), or auto: mode for labels and masks, mean otherwise
morphology operation open open, close, erode, dilate on a mask (input > 0); output uint8
radius 2 spherical structuring element radius in voxels; halo = 2 × radius + 1
label min_size 0 connected components of a mask, output uint32 segment ids; drop components smaller than this (halo 4 when set, so the size filter sees more of each object near a chunk's edge; else 0)
connectivity 1 1 = 6-connected, 2 = 18, 3 = 26
spots sigma 1.0 bright diffraction-limited spots (single mRNAs in smFISH, EASI-FISH): difference of Gaussians, local maxima above threshold, each drawn as a small ball whose uint32 id comes from its position, so it is the same whichever chunk finds it; spot size in y-x voxels; needs a z, y, x volume (select a channel); halo per axis: ceil(4.8 × sigma) + separation + radius in y and x, the same in z with sigma_z and the two distances scaled by sigma_z / sigma
sigma_z 0.6 spot size in z voxels
threshold 10 least difference-of-Gaussians response, image intensity units
separation 2 spots closer than this (y-x voxels) are one
radius 1 ball drawn per spot, y-x voxels (0 marks one voxel)
slope z_factor 1.0 slope of an elevation model in degrees (0 flat, 90 a cliff), on the last two axes, from each level's pixel spacing (for_level); output float32; halo 1; elevation units per unit of the spacing (1 when both are metres)
hillshade azimuth 315 shaded relief, the terrain lit by a distant sun from this direction (degrees clockwise from the top of the image), local illumination only (no cast shadows); output uint8, 1 unlit to 255 facing the sun, 0 where the elevation is NaN; halo 1
altitude 45 the sun's height above the horizon, degrees
z_factor 1.0 as for slope; above 1 exaggerates relief
contacts radius 3.0 contact sites between the first two channels of a stack:// source: voxels within this many voxels (Euclidean) of both structures; output uint8 mask; halo = ceil(radius) + 1
distance none the reach in the data's units (nm) instead: each level counts it in its own voxels, so a contact means the same at every zoom; halo planned on the finest level
a_low 128 values at or above this in the first channel are the first structure (128 for a uint8 probability map, 1 for a segmentation)
b_low 128 the same for the second channel
normalized_difference pair 0, 1 (a - b) / (a + b) of two channels of a stack:// source, float32: the indices remote sensing reads plants, water and burn scars from (NDVI, NDWI, NBR); NaN where a band is zero or less
minus none two more channels whose index is subtracted from the first pair's: a change between dates (burn severity, dNBR, is the NBR before minus the NBR after)
offset 0 added to every channel first, to make reflectances of stored numbers
nodata none a stored value meaning no data: the index there is NaN
floor 0 where a pair sums to less than this, its index is noise (water) and NaN
python code a template Python of your own defining apply(block, ...), with #halo, #output and #uicontrol lines; see below. A server runs it only when started with --allow-code
params none values of the code's #uicontrol sliders, over their defaults

label numbers components per chunk (salted by chunk position so ids never collide). An object spanning chunks therefore gets one colour per chunk. That is the honest per-chunk preview of a global operation; see FAQ.

Ops that need to know where a block sits override apply_at(block, box) instead of apply(block); label uses it for the salt, spots for its ids.

examples/fish_spots.py runs spots on both FISH channels of a public EASI-FISH round of a whole fly central brain (3.4 gigavoxels per channel), next to the raw channels, with the threshold editable at a prompt. Only the chunks on screen are searched, and a chunk's spots are identical to those found in one pass over a larger block.

Ops over several images take the channels of a stack:// source and return an array without the channel axis, as contacts does (its output_info drops the axis). The pipeline reads every channel and pads only the spatial axes by the halo; ops after it in the same stage see the plain spatial block.

None of the built-ins cache their output by default (cache=False); a pipeline turns it on for one op with "cache": true in its spec (caching). Everything except threshold, cast, scale, diff, downsample, normalized_difference, slope and hillshade needs the ops extra (scipy); python needs it when its code imports scipy.

slope on NASA's 5 m south-pole elevation of the Moon (the ridge between Shackleton and de Gerlache craters, LOLA, Barker et al.) equals the slope USGS publishes with it, to 0.0° (median and 95th percentile over a 512 × 512 region at full resolution); the gallery's Moon card draws ground under a chosen slope over it, and the relief lit by a sun you move.

diff along time is the view a hurricane's cold wake or a solar flare shows up in: each day's sea temperature minus the day before's (examples/hurricane_wakes.py, and the gallery's hurricanes card), each 6-minute image of the sun minus the one before (examples/solar_flares.py, the running difference solar physicists use).

Your own Python: the python op

The python op carries its code in the pipeline spec, so a pipeline can do something no op does without a plugin package: edit the code and the view is computed again, as Neuroglancer's shaders are edited live on its pixels. The control page and the browser demos show it as a code box.

#halo 1
#output uint32 label
#uicontrol float low slider(min=0, max=255, default=128)
def apply(block, low):
    from scipy import ndimage
    return ndimage.label(block > low)[0]

apply gets the block, padded by the halo, and the values of the sliders by name, and returns the block computed (or its interior, shaved by the halo on every side). numpy is np. A parameter named box gets where the block sits, as apply_at does. The result is cast to the output dtype. What it prints shows under the code in the browser demos, by level and chunk, and in a server's log. Three kinds of line, starting at the first column (an indented comment inside apply is not one), read as text so a pipeline is planned without running the code:

line says default
#halo 2, #halo 0 4 4 voxels of context needed on each side (up to 1024): one for every axis, or one per axis for the last axes 0
#output uint32 label the dtype returned (an integer or float numpy dtype), and/or whether it is an image, label or mask the input's
#uicontrol float low slider(min=0, max=255, step=1, default=128) a slider, in Neuroglancer's shader syntax: float or int slider (min and max required), or bool checkbox(default=true); apply takes a parameter of the same name default: min, or false

The spec gives the sliders' values as params, so moving one changes the parameters, not the code: {"op": "python", "code": "...", "params": {"low": 140}}. A value for a slider the code no longer has is dropped.

How a slider reaches the code. The # lines are comments to Python itself, and apply is an ordinary function: outside chunkmirage, apply(volume, sigma=1, low=300, min_size=500) runs it on any numpy array. chunkmirage reads the #uicontrol lines to draw the sliders (the browser demos and the control page draw the same ones), keeps each slider's value in the op's params, and calls apply(block, **params) on every chunk; a moved slider is a new spec, so the view is computed again. Nothing in the code belongs to a viewer or a page: from Python, Pipeline.from_spec({"source": ..., "ops": [{"op": "python", "code": code, "params": {"low": 400}}]}) runs it the same way. Both are part of the op's digest, so an edit is a new URL and viewers fetch the result afresh. Mistakes (a syntax error, an unknown dtype, a slider apply does not take or a parameter of apply with neither a slider nor a default, a slider whose default is outside its range or whose name the op has itself, such as name or halo) are refused when the pipeline is built, naming the line.

Where it runs. In the browser engine, always: the code runs in the visitor's own Pyodide workers, which load what it imports (numpy, scipy, scikit-image and the rest of Pyodide's packages; not torch, cupy or tensorstore). On a server, the code runs as the server's user, with its files, so chunkmirage serve refuses specs with python ops sent to /api unless started with --allow-code, which generates a token unless given --token (CLI). Pipelines built in your own process (Pipeline, --op) run it regardless.

As a plugin. chunkmirage.ops.python_op(name, code) makes an op class of the same code whose sliders are its parameters (pydantic fields, with their bounds), for the chunkmirage.ops entry point; the code boxes' "copy as plugin" gives such a module.

Written for you. In the browser demos, Ask a model to write it under the code box is a chat that writes the code from a request, with your own key for Claude, OpenAI, Gemini and others, or Ollama on your computer; see Interactivity.

Your own op

An op of your own is a small class in your own package, and behaves as the built-in ops do once it is installed: chunkmirage ops and GET /api/ops list it, --op invert:top=200 and specs sent to /api take it, and the control page gives it sliders from its parameters' bounds and descriptions.

from chunkmirage.ops import Op, register
from pydantic import Field

@register
class Invert(Op):
    """Each voxel as `top` minus it."""   # what `chunkmirage ops` shows
    name = "invert"
    halo = 0                              # voxels of context read on every side
    top: float = Field(255, ge=0, description="The value a zero becomes")

    def apply(self, block):
        return self.top - block
[project.entry-points."chunkmirage.ops"]
invert = "mypackage.ops:Invert"

apply gets each block padded by halo and returns it computed, as the python op's does; output_dtype, output_kind or output_info say what it makes when that differs from its input, apply_at is told where the block sits, input_voxel_size runs it at one resolution (a model trained at 8 nm), for_level adapts it to each level's voxel size, overlap has each block also compute a margin beyond its chunk, blended with its neighbours' where they meet (a fit per block, a model's tiles), and cache = True keeps its output for cheaper ops after it. The whole contract, with what a returned block may look like, is on the pipelines page; a template with a test is in Contributing, and the API a plugin may rely on is listed there. examples/model_at_one_resolution.py is a complete one: a model's stand-in with its own input resolution and two output channels.

Three ways to get there, from least to most work: code in the python op's box, which needs nothing installed and runs in the browser demos too; the same code as a plugin, from Copy as plugin or python_op, with its sliders as parameters; a class as above, for a halo that depends on parameters, several channels, a resolution of its own or a GPU. Plugin ops run on a server or in Python: the browser demos run chunkmirage's own ops and the code box, not other packages.

CLI syntax

--op threshold:low=120,high=200
--op '{"op": "gaussian", "sigma": 2}'
--op gaussian:sigma=2,cache=true
--op python:code=@slice.py
--op '{"op": "python", "code": "@slice.py", "params": {"low": 30}}'

Values are parsed as JSON where possible, else strings. Repeat --op to chain. A python op's code=@file is read from that file, on the command line only (a spec sent to a server reads no file). The demos that start with Python show their command this way, and save the file it reads (Save slice.py): the code as you have it, its sliders' defaults set to where you left them. That is how a result found in a demo reaches Fiji or napari: run the command, then open the server's /processed/n5 in Fiji or /processed/zarr in napari (what a demo computes exists only in its tab).