spacr.layers¶
A napari-style layer model — images, labels, points and shapes in one world.
A viewer that draws overlays into the image pixels — outlines in the live preview, tracks in the timelapse preview, brush strokes in Make Masks — returns one RGB array, in which an overlay cannot be hidden, faded, recoloured or moved below another object.
This module is the model for a viewer in which each of these is a separate layer.
It is deliberately pure numpy with no Qt anywhere, for the same reasons
spacr.selection is:
it can be tested without a display, and the compositing rules — which layer wins, what opacity means, where a point lands — are exactly the rules that need testing;
the same stack can be rendered headless into a figure or a report;
and five later features (ROI shapes honoured by Measure, a counting points layer, a label brush, orthogonal views, a comparison grid) are written against this, not against a widget, so none of them needs a running
QApplicationto have a unit test.
spacr.qt.layer_viewer is the Qt view over it.
The world, and why it is not optional¶
Layers do not agree on pixels. A labels mask may be at full resolution while a downsampled preview is not; a points layer of centroids is in continuous coordinates and has no grid at all; and confocal z-stacks in this codebase are routinely anisotropic — 0.65 µm in x and y, 2 µm in z is an ordinary spaCR stack.
So a layer never says “row 40, column 12”. It says “this data axis is z, one
step along it is 2 µm, and element 0 sits at 0 µm” — a Spacing — and
everything downstream happens in world units. A Canvas is a window
onto that world (an origin, a step and a size, over two named world axes), and
rendering is: for every canvas pixel, ask each layer what is at that world
coordinate.
Ignoring spacing produces no visible error: an overlay drawn a slice out of
register still looks like a plausible image, and any number measured from it is
wrong without a warning. For this reason a zero scale raises, and a stack raises
LayerError when µm layers and px layers are mixed instead of drawing them
on top of each other.
Order¶
stack[0] is the BOTTOM layer and is drawn first, like a stack of acetates
and like napari’s own layer list. Moving a layer up moves it towards the front.
Blending¶
Every layer contributes coverage (where it draws, 0–1) and opacity (a
0–1 weight for the whole layer). All five modes combine them as alpha =
coverage * opacity, except Blending.OPAQUE, which first sets every
non-zero coverage to 1 so that soft edges cover fully; this is the only mode
that treats the two differently.
Attributes¶
Set to anything but |
|
The True 3D view's wgpu backend keys mapped to wgpu |
Exceptions¶
Raised for an invalid layer, spacing, canvas, stack or colour argument. |
Classes¶
How a layer combines with what is already on the canvas. |
|
The world window a render fills: an origin, a step and a size. |
|
Several canvases held on the same world window. |
|
A named ramp from a value in 0–1 to a colour. |
|
Which field a labels layer segments, in the schema's own key columns. |
|
|
|
Intensity data — one channel or many, each with its own LUT. |
|
An integer segmentation mask. 0 is background and draws as nothing. |
|
Base class for stack layers: a name, a |
|
A change notification that a |
|
An ordered list of layers sharing one world. |
|
The three planes of one volume, laid out so that they line up. |
|
Points in the world — centroids, counted objects, clicked markers. |
|
How finely one 3-D frame is drawn. |
|
One drawn region: a polygon, rectangle, ellipse, line or path. |
|
Drawn regions of interest; |
|
Where a layer's array elements sit in the world. |
|
How a mask surface reflects light in a 3-D view (Blinn-Phong). |
|
Where the light and the viewer are, for shading surfaces along rays. |
|
A magnifying cube in a volume, for a 3-D view's magnifier. |
|
Parallel rays through a volume, one per output pixel. |
|
|
Functions¶
|
Resolve |
|
A volume small enough to turn interactively, and how it was reduced. |
|
Why a 3-D view must not draw on the GPU here, or |
|
A stable, vivid colour for integer |
|
|
|
Turn the Render scale slider into what one frame is drawn from. |
|
Draw 3-D image and labels layers along parallel rays, as napari does. |
|
Light surface points of known colour and normal by one finish. |
|
The lighting numbers of one finish. |
|
Outward unit normals of a mask's surface at points on it. |
|
Coerce anything that names a colour into |
|
The integer stride along each axis that fits a volume in a budget. |
|
A fresh wgpu adapter of one backend, the best kind first. |
|
The graphics adapters wgpu can draw the True 3D view with. |
Module Contents¶
- exception spacr.layers.LayerError[source]¶
Bases:
ValueErrorRaised for an invalid layer, spacing, canvas, stack or colour argument.
Raised rather than repaired. Every case this covers — a zero voxel size, a stack mixing µm with px, a points array of the wrong width — produces a picture that still looks right while being out of register, which is the failure mode that reaches a figure.
Initialize self. See help(type(self)) for accurate signature.
- class spacr.layers.Blending[source]¶
How a layer combines with what is already on the canvas.
Five modes, all of which honour
opacitythe same way —alpha = coverage * opacity— exceptOPAQUE, which first sets every non-zero coverage to 1, so a soft-edged layer fully covers the layers below it wherever it has any coverage. Opacity still scales an opaque layer; “opaque” describes what it does to the layers below it where it has something, not whether it can be faded.- static apply(dst: numpy.ndarray, src: numpy.ndarray, coverage: numpy.ndarray, opacity: float, mode: str) Tuple[numpy.ndarray, numpy.ndarray][source]¶
Composite
srcoverdst; returns(rgb, alpha).- Parameters:
dst – the canvas so far,
(H, W, 3)float in 0–1.src – this layer’s colour,
(H, W, 3)float in 0–1.coverage – where this layer has something,
(H, W)in 0–1.opacity – the layer’s opacity, 0–1.
mode – one of
Blending.ALL.
- Returns:
the new canvas and the alpha that was actually used, so a caller can accumulate the composite’s own coverage.
- static check(mode: str) str[source]¶
Normalise and validate a blending name.
- Parameters:
mode – Blending-mode name. Leading and trailing whitespace and letter case are normalised before validation against
Blending.ALL.- Raises:
LayerError – on an unknown mode — a typo that silently fell back to translucent would be a compositing bug nobody could see.
- class spacr.layers.Canvas[source]¶
The world window a render fills: an origin, a step and a size.
Rows run along
axes[0]and columns alongaxes[1]; every other world axis is pinned bydepth. Naming the plane rather than assuming(y, x)is what makes an orthogonal view a differentCanvasover the same stack rather than a different renderer:Canvas.covering(stack, height=512, axes=("z", "x"), depth={"y": 40.0})
- Parameters:
origin – world coordinate of the centre of pixel
(0, 0).step – world units per canvas pixel, along rows and columns. This is the zoom: halve it to zoom in.
shape –
(height, width)in canvas pixels.axes – which world axes rows and columns run along.
depth – world coordinate for the axes not in the plane — the slice being shown. Axes absent from it are read as 0.
- __post_init__() None[source]¶
Coerce and validate the canvas geometry, then freeze it.
Two dimensions exactly, two distinct axes, a positive shape and a non-zero finite step. Each is refused rather than repaired: a canvas whose rows and columns name the same axis has no meaning to fall back to.
- at_depth(**coords: float) Canvas[source]¶
The same window at a different slice:
canvas.at_depth(z=12.0).
- column_world() numpy.ndarray[source]¶
World coordinate of every column centre,
(width,).
- classmethod covering(source: Any, *, height: int | None = None, width: int | None = None, axes: Tuple[str, str] = ('y', 'x'), depth: Mapping[str, float] | None = None, margin: float = 0.0) Canvas[source]¶
A canvas showing all of
source— a stack, a layer, or an extent.Exactly one of
height/widthfixes the resolution; the other follows from the world aspect ratio, so an anisotropic stack viewed in("z", "x")is not squashed. Giving both is allowed and stretches.- Parameters:
source – a
LayerStack, aLayer, or a{axis: (low, high)}extent mapping.margin – fraction of the extent to add on every side.
- classmethod for_grid(spacing: Spacing, shape: Sequence[int], *, axes: Tuple[str, str] = ('y', 'x'), depth: Mapping[str, float] | None = None) Canvas[source]¶
The canvas that samples
spacing’s own grid, one pixel per element.The identity render: a layer drawn onto
Canvas.for_grid(layer.spacing, layer.shape)comes back at its native resolution and alignment, which is what a shapes-to-mask conversion needs (seeShapesLayer.mask()).- Parameters:
spacing – the grid to sample. Origin, step and
unitsare all taken from it, so the canvas reports the layer’s world units instead of the"px"a bareCanvasdefaults to. Pixel(0, 0)is centred exactly on element 0 — no half-step inset, unlikecovering().shape – the layer’s whole array shape, in the spacing’s axis order. It is indexed by each plane axis’s position in
spacing.axes, not by 0 and 1, so a three-axis spacing wants three entries even though only two are read; a two-entry shape against a 3-D spacing raisesIndexError. Entries for axes outside the plane are ignored, non-integers are truncated, and a zero or negative in-plane size raisesLayerError.axes – which two spacing axes rows and columns run along. Any pair the spacing names is allowed, in any order —
("x", "y")transposes the result. An axis the spacing does not have, or the same axis twice, raisesLayerError.depth – world coordinate for the axes outside the plane. When omitted, each out-of-plane axis is pinned at the spacing’s own origin, so a translated volume starts on its first plane. Passing a mapping selects those world coordinates explicitly; entries naming an in-plane axis are kept but never consulted.
- panned(d_row: float, d_column: float) Canvas[source]¶
A canvas moved by
(d_row, d_column)canvas pixels.- Parameters:
d_row – Signed displacement in canvas-row pixels.
d_column – Signed displacement in canvas-column pixels.
- pixel_at(world: Mapping[str, float]) Tuple[float, float][source]¶
The (fractional) canvas pixel a world point falls on.
The inverse of
world_at(), and the reason a points layer and a labels layer at the same world coordinate land on the same pixel: they are both put through this, not through their own array indices.- Parameters:
world – Mapping containing world coordinates for both axes of this canvas plane.
- resized(height: int, width: int) Canvas[source]¶
The same world window at a different pixel size.
The world span is held, not the step, so a widget resize shows the same field of view rather than more of the sample.
- Parameters:
height – New canvas height in pixels, coerced to at least one.
width – New canvas width in pixels, coerced to at least one.
- row_world() numpy.ndarray[source]¶
World coordinate of every row centre,
(height,).
- world_at(row: float, column: float) Dict[str, float][source]¶
The world point under canvas pixel
(row, column).Includes the pinned
depthaxes, so the result is a complete world position a layer can be asked about.- Parameters:
row – Fractional canvas-row coordinate.
column – Fractional canvas-column coordinate.
- zoomed(factor: float, centre: Tuple[float, float] | None = None) Canvas[source]¶
A canvas
factor× closer, aboutcentre(canvas pixels).factor > 1zooms in. The default centre is the middle of the view, which is what a keyboard zoom means; a wheel zoom passes the cursor.- Parameters:
factor – Positive finite magnification factor; values above one zoom in and values below one zoom out.
- class spacr.layers.CanvasLink(canvases: Mapping[str, Canvas] | None = None)[source]¶
Several canvases held on the same world window.
The model under a comparison grid: four panels showing four channels of one field, or the same field at four timepoints, or the same well under four conditions. Panning one pans all of them and zooming one zooms all of them, so a difference between two panels reflects a difference in the data rather than a different field of view.
What is shared is the world window — the origin and the step — and NOT the pixel shape, because the panels are different widgets with different sizes. Sharing the shape would make the grid’s own layout part of the state; this way a panel that is 10 px narrower shows 10 px less of the same view at the same magnification, which is what a grid of unequal cells should do.
- Parameters:
canvases – the panels to start with, as
{key: canvas}. Added one at a time throughadd(), so each is adopted onto the shared world window as it arrives rather than the first one winning.
A panel can opt out with
unlock(), for the ordinary case of wanting to look closely at one of them without losing the others’ place.Link a set of named canvases so they can move together.
- add(key: str, canvas: Canvas, *, locked: bool = True) Canvas[source]¶
Put a panel in the link, adopting the shared window if there is one.
A panel added to a link that has already been panned starts where the others are, not where it was built — otherwise adding a fifth channel to a grid the user has zoomed into shows the whole field in one cell.
- Parameters:
key – Unique panel identifier, coerced to a string.
canvas – Canvas that defines this panel’s plane, size and world window.
- at_depth(*, key: str | None = None, **coords: float) None[source]¶
Move every locked panel to another slice:
link.at_depth(z=12.0).
- is_locked(key: str) bool[source]¶
Whether a panel follows the others.
- Parameters:
key – Identifier of an existing panel.
- lock(key: str) None[source]¶
Make a panel follow the others again, moving it there now.
- Parameters:
key – Identifier of the panel to align and lock.
- pan(d_row: float, d_column: float, *, key: str | None = None) None[source]¶
Pan by a number of the DRIVING panel’s pixels.
Converted to world before it is handed to the others, so a grid whose panels are at different zooms (one unlocked, then relocked) still moves by the same distance across the sample rather than the same number of pixels.
- Parameters:
d_row – Signed row displacement in pixels of the driving panel.
d_column – Signed column displacement in pixels of the driving panel.
- remove(key: str) Canvas[source]¶
Take a panel out and return its canvas.
- Parameters:
key – Identifier of an existing panel.
- reset(source: Any, **kwargs: Any) None[source]¶
Fit every locked panel to
sourceagain, keeping its pixel size.- Parameters:
source – Layer stack, layer or extent mapping accepted by
Canvas.covering().
- resize(key: str, height: int, width: int) None[source]¶
Set the pixel shape of one panel only, after its widget resized.
The step is held, not the span — the opposite of
Canvas.resized(). In a linked grid the shared quantity is the magnification: a cell that is 10 px narrower than its neighbour shows 10 px less of the sample at the same scale, so panels of different sizes stay at the same magnification and remain comparable.- Parameters:
key – Identifier of the existing panel whose widget resized.
height – New panel height in pixels, coerced to at least one.
width – New panel width in pixels, coerced to at least one.
- set(key: str, canvas: Canvas) None[source]¶
Replace one panel’s canvas, and bring the locked ones with it.
- Parameters:
key – Identifier of the existing panel to replace.
canvas – Replacement canvas whose world window locked peers adopt.
- subscribe(listener: Callable[[str], None]) Callable[[str], None][source]¶
Register a listener called with the key of each panel that changes.
Held by strong reference, exactly like
LayerStack.subscribe(): a view that subscribes mustunsubscribe()when it closes.- Parameters:
listener – Callable receiving the identifier of the panel that changed; it is retained by strong reference.
- unlock(key: str) None[source]¶
Let a panel be moved on its own.
- Parameters:
key – Identifier of the panel to detach from linked movement.
- unsubscribe(listener: Callable[[str], None]) bool[source]¶
Remove a listener; return
Trueif this call removed it.- Parameters:
listener – Previously subscribed callback to remove.
- zoom(factor: float, *, key: str | None = None, centre: Tuple[float, float] | None = None) None[source]¶
Zoom
key(or every panel) byfactor, aboutcentre.The world point under
centrein the panel being driven is what the other panels are zoomed about too — not their own middles, or a wheel over one cell would slide the rest of the grid sideways.- Parameters:
factor – Positive finite magnification factor passed to each affected canvas.
- class spacr.layers.Colormap(name: str, colors: Sequence[Any], stops: Sequence[float] | None = None)[source]¶
A named ramp from a value in 0–1 to a colour.
Linear interpolation between
colorsatstops. Two stops covers every microscopy channel LUT there is (black → the channel’s colour), and more stops covers the perceptual maps without pulling matplotlib into a module that has to stay importable in a worker.- Parameters:
name – Display and lookup name stored with the colour ramp; it is coerced to a string.
colors – Ordered sequence of at least two colour specifications accepted by
to_rgba(); the ramp interpolates them in order.stops – where each colour sits, in 0..1. Defaults to spacing them evenly. One stop per colour, ascending – an out-of-order stop would make the ramp non-monotonic and is refused rather than sorted, since sorting would silently give a different ramp than the caller wrote.
- Raises:
LayerError – with fewer than two colours, or when
stopsdoes not matchcolorsone for one.
Build a colormap from at least two colours.
Fewer than two is refused: one colour is not a map, and interpolating between a colour and nothing has no answer.
- __eq__(other: Any) bool[source]¶
Equal when the name AND the colours and stops match.
The name alone is not enough – two maps can be renamed to the same thing – and the arrays alone are not either, because the name is what a saved setting refers to.
- map(values: Any) numpy.ndarray[source]¶
Map
values(any shape, 0–1, clipped) to(..., 3)float32.- Parameters:
values – Numeric scalar or array-like values to interpolate; values outside 0–1 are clipped to the nearest end of the ramp.
- property colors: numpy.ndarray[source]¶
The ramp’s colours,
(K, 3)float32 — a copy.
- property stops: numpy.ndarray[source]¶
Where each colour sits in 0–1 — a copy.
- class spacr.layers.FieldKey[source]¶
Which field a labels layer segments, in the schema’s own key columns.
A labels layer on its own knows that object 17 is object 17. Only the field it came from turns that into
plate1_A_1_1_17— the key the UMAP, the plate view and the annotation grid all already use. Keys are built by handing a frame tospacr.selection.object_keys()rather than joining strings here, so this cannot drift from the identity the rest of the codebase agrees on.- Parameters:
values – the field key columns and their values — everything in
OBJECT_KEY_COLUMNSexcept the object label (and the timepoint too, whentimelapse).timelapse – key each frame of an object separately, which requires a
timeidinvalues.object_type – which mask this layer segments (
'cell','nucleus', …). Without it the keys are untyped, so clicking the nucleus labelled 1 publishes the same string as clicking the pathogen labelled 1 in the same field, and the two views the click was supposed to link land on whichever of the two their table happened to hold first. UseNonefor a mask loaded from a file that does not record its object type.
- classmethod columns(*, timelapse: bool = False) Tuple[str, ...][source]¶
The key columns a field key of this flavour needs, label excluded.
- frame(labels: Iterable[int])[source]¶
A one-column-per-key-column frame for
labels, in their order.- Parameters:
labels – Iterable of integer-like object labels to attach to this field identity.
- classmethod from_row(row: Mapping[str, Any], *, timelapse: bool = False, object_type: str | None = None) FieldKey[source]¶
Take the key columns out of a measurement row (or any mapping).
Anything else on the row is dropped — a field key is an identity, and carrying a measurement along with it would make two keys for the same field compare unequal.
- Parameters:
row – Measurement-row mapping from which the required identity columns are extracted.
- object_key(label: int) str[source]¶
The one object key for
label.- Parameters:
label – Integer-like object label to encode for this field.
- object_keys(labels: Iterable[int])[source]¶
pandas.Indexof object keys forlabels, in order.- Parameters:
labels – Iterable of integer-like object labels to encode.
- class spacr.layers.GLVolumeRenderer[source]¶
render_volume(), ray-marched on the GPU through OpenGL (VisPy gloo).The same rays (
VolumeRays), the same layer model, renderings, finishes and lens, so a frame drawn here matches the CPU’s to within 8-bit rounding and picks the same objects. Each fragment walks one ray; image layers add up, labels layers are laid over them, and a last pass writes the object nearest the viewer under each pixel for picking.The caller owns the OpenGL context and makes it current around every call; this class creates nothing until the first
render(). A volume is uploaded once and kept, keyed by its memory, for as long as every frame draws it; a frame frees the textures it did not use. A mask edited in place must therefore be handed over as a new array.Hold no GPU object until the first frame.
- limits() Dict[str, Any][source]¶
The context’s renderer name and its largest 3-D texture side.
- Returns:
{"renderer": str, "version": str, "max_3d": int}.
- render(layers: Iterable[Layer], rays: VolumeRays, *, highlight: int = 0, voxel_size: Sequence[float] | None = None, lens: VolumeLens | None = None, size: Tuple[int, int] | None = None) Tuple[numpy.ndarray, numpy.ndarray][source]¶
Draw one frame; the same picture as
render_volume().- Parameters:
layers – the layers, bottom first.
rays – where to sample, in the layers’ array indices.
highlight – an object drawn lighter.
voxel_size – physical voxel length along each array axis.
lens – a magnifier cube, or
None.size –
(rows, columns)of the picture returned, scaled up from the rays’ grid on the GPU;Nonekeeps the rays’ grid.
- Returns:
(rgb, picked):uint8colour ofsizeand the object nearest the viewer under each ray (int64, the rays’ grid).- Raises:
LayerError – as
render_volume().
- class spacr.layers.ImageLayer(data: Any, *, name: str = 'image', channel_axis: int | None = None, colormaps: Sequence[Any] | None = None, contrast_limits: Any | None = None, channel_names: Sequence[str] | None = None, channel_visible: Sequence[bool] | None = None, **kwargs: Any)[source]¶
Bases:
LayerIntensity data — one channel or many, each with its own LUT.
- Parameters:
name – layer name. Default
"image".channel_names – a label per channel, for a channel list or a legend. Defaults to positional names – and naming them is what lets a reader tell which channel is the nucleus without counting.
Channels are composited additively within the layer, which is what makes a two-channel field read as one picture: a green nucleus channel and a magenta pathogen channel overlap as white rather than as whichever channel happened to be second. Across layers the user picks the blending.
- Parameters:
data – the array. The spatial axes are every axis except
channel_axis, in order.channel_axis – which axis holds channels, if any. Negative indices work. A
(H, W, 3)RGB stack ischannel_axis=-1.colormaps – one per channel; defaults walk
DEFAULT_CHANNEL_COLORMAPS(a single-channel image gets grey).contrast_limits –
(low, high)per channel in data units.Nonemeans theDEFAULT_PERCENTILESstretch, computed once and remembered so that panning does not change the brightness — a per-view stretch is how two crops of the same field end up looking like different exposures.channel_visible – per-channel visibility, for turning one stain off without splitting the layer.
Build an image layer.
- Parameters:
data – the pixel array, channels included.
kwargs – name, spacing, colormaps and the rest of the layer contract.
- auto_contrast(percentiles: Tuple[float, float] = DEFAULT_PERCENTILES) None[source]¶
Recompute every channel’s limits from the data.
- channel_data(channel: int) numpy.ndarray[source]¶
The spatial array for one channel (a view, not a copy).
- Parameters:
channel – Zero-based channel index accepted by normal sequence indexing.
- channel_is_visible(channel: int) bool[source]¶
Whether one channel participates in rendering.
- Parameters:
channel – Zero-based channel index accepted by normal sequence indexing.
- contrast_limits(channel: int = 0) Tuple[float, float][source]¶
The limits in use for
channel, computing the default if needed.
- set_channel_visible(channel: int, visible: bool) None[source]¶
Set whether one channel participates in rendering.
- Parameters:
channel – Zero-based channel index accepted by normal sequence indexing.
visible – Truth value for the channel’s new visibility state.
- set_colormap(value: Any, channel: int = 0) None[source]¶
Give one channel a new LUT.
- Parameters:
value – Colormap or colour specification accepted by
colormap().
- set_contrast_limits(low: float, high: float, channel: int = 0) None[source]¶
Set one channel’s limits.
- Parameters:
low – Lower intensity bound in the channel’s data units.
high – Upper intensity bound in the channel’s data units; it must be greater than
low.
- Raises:
LayerError – if they do not increase — an inverted pair renders a black image, which reads as “no signal” rather than as a bad setting.
- world_extent() Dict[str, Tuple[float, float]][source]¶
The world box this layer occupies, per named axis.
What lets the viewer frame a stack whose layers have different shapes and spacings without any of them knowing about the others.
- Returns:
{axis: (low, high)}.
- property channel_names: Tuple[str, ...][source]¶
What each channel is called, in channel order.
- Returns:
one name per channel.
- property colormaps: Tuple[Colormap, ...][source]¶
One colormap per channel, in channel order.
A tuple copy, so a caller cannot reorder this layer’s colormaps by mutating what it was handed.
- Returns:
the colormaps.
- property data: numpy.ndarray[source]¶
The pixel array, channels included.
- Returns:
the array as given; not a copy.
- class spacr.layers.LabelsLayer(data: Any, *, name: str = 'labels', field: FieldKey | None = None, seed: int = 0, **kwargs: Any)[source]¶
Bases:
LayerAn integer segmentation mask. 0 is background and draws as nothing.
- Parameters:
name – layer name. Default
"labels".seed – seeds the label-to-colour mapping. The same seed gives the same object the same colour across sessions and across figures, which is what makes two screenshots of one field comparable.
Colours come from
label_color(), so an object keeps its colour between sessions and between this viewer and the live preview.- Parameters:
data – Two- or three-dimensional array-like segmentation labels; values must be integers or exactly integral numbers.
field – the
FieldKeythis mask segments. Optional, but without it a click can only say “label 17” — with it, the click can publishplate1_A_1_1_17to every other view.
Build a labels layer.
- Parameters:
data – the label array; 0 is background.
kwargs – name, spacing, field and the rest of the layer contract.
- brush_index(world: Mapping[str, float], *, radius: float = 0.0) Tuple[numpy.ndarray, ...][source]¶
The elements a world-space ball brush covers, as index arrays.
The brush is a ball in WORLD space, so on an anisotropic stack it covers fewer z-slices than y-rows — which is what the user means by a 5 µm brush. A brush measured in array elements would be a 5-slice cylinder, i.e. 10 µm deep and 3.25 µm wide on an ordinary spaCR stack.
Split out of
paint()so a caller that has to record what it changed —spacr.curation.MaskCuration, which keeps an undo history and an audit trail — asks the layer for the geometry instead of re-deriving the anisotropy rule and getting it subtly different. There is one ball, and it is defined here.- Parameters:
world – Mapping from axis names to the brush centre in world coordinates; omitted spacing axes are evaluated at zero.
- Returns:
one integer array per axis, ready to index
datawith. Empty arrays when the brush falls entirely off the grid, which indexes to nothing rather than raising.
- label_at_world(world: Mapping[str, float]) int[source]¶
The label under a world point, or 0 for background / outside.
- Parameters:
world – Mapping from axis names to a point in world coordinates.
- labels() numpy.ndarray[source]¶
Every non-zero label present, sorted.
- object_key_at_world(world: Mapping[str, float]) str | None[source]¶
The measurement-table key of the object under a world point.
Nonefor background, or when this layer was not told which field it segments. This is the whole reason a labels layer carries aFieldKey: the string this returns is the same string the UMAP, the plate view and the annotation grid use, so publishing it throughspacr.qt.linked_selectionhighlights the same cell everywhere.- Parameters:
world – Mapping from axis names to the world point to identify.
- object_keys(labels: Iterable[int] | None = None)[source]¶
Keys for
labels(default: every label present), in order.
- paint(world: Mapping[str, float], label: int, *, radius: float = 0.0) int[source]¶
Set every element within
radiusworld units of a point.brush_index()owns the ball; this is that plus the write.- Parameters:
world – Mapping from axis names to the brush centre in world coordinates.
label – Integer-like label to paint.
- Returns:
how many elements changed.
- set_labels_at(index: Sequence[numpy.ndarray], label: int) int[source]¶
Set the elements
indexnames tolabel; how many changed.The write half of
brush_index(), and the ONE place a labels layer’s data is edited element-wise — so exactly one call notifies. A caller doing its own arithmetic ondatawould mutate the array without telling the canvas, and the picture would come right only at the next unrelated repaint.- Parameters:
index – One integer index array per data axis, suitable for NumPy advanced indexing.
label – Integer-like label to write at the selected elements.
- world_extent() Dict[str, Tuple[float, float]][source]¶
The world box this layer occupies, per named axis.
What lets the viewer frame a stack whose layers have different shapes and spacings without any of them knowing about the others.
- Returns:
{axis: (low, high)}.
- property data: numpy.ndarray[source]¶
one integer per pixel, 0 for background.
- Returns:
the array as given; not a copy.
- Type:
The label array
- class spacr.layers.Layer(*, name: str, spacing: Spacing | None = None, visible: bool = True, opacity: float = 1.0, blending: str = Blending.TRANSLUCENT, metadata: Mapping[str, Any] | None = None)[source]¶
Base class for stack layers: a name, a
Spacingand display properties.Subclasses implement
_draw(); everything shared — visibility, opacity, blending, spacing, notification — lives here.Display properties are plain attributes with setters that notify the stack, so a view repaints because the model changed rather than because the widget that changed it remembered to ask.
- Parameters:
name – Non-blank layer identifier; names are uniquified when the layer enters a
LayerStack.spacing – physical size of one pixel along each spatial axis. Defaults to isotropic. It is what makes a measurement in microns mean the same thing on two microscopes, so it must have exactly
ndimaxes.visible – whether the layer draws at all. Default
True.opacity – 0.0 transparent to 1.0 opaque. Default
1.0.blending – how the layer combines with what is under it, from
Blending. Default translucent.metadata – arbitrary values carried with the layer. Copied, so the caller’s mapping is not held; spaCR itself reads nothing from it.
- Raises:
LayerError – when the name is blank, the opacity is not a finite number, the blending is unknown, or
spacinghas the wrong number of axes for this layer.
Set the layer’s name, opacity and visibility, validated.
- render(canvas: Canvas) Tuple[numpy.ndarray, numpy.ndarray][source]¶
Draw onto
canvas; returns(rgb, coverage).rgbis(H, W, 3)float32 in 0–1 andcoverageis(H, W)float32 in 0–1 marking where this layer draws anything. Opacity and blending are NOT applied here — the stack applies them, so a layer’s own render is the same whatever it is composited with.- Parameters:
canvas – World window and output grid on which to draw the layer.
- to_data(world: Mapping[str, float]) Tuple[float, ...][source]¶
{axis: world}→ fractional data index.- Parameters:
world – Mapping from world-axis names to coordinates; omitted spacing axes are evaluated at world coordinate zero.
- to_world(index: Sequence[float]) Dict[str, float][source]¶
Data index →
{axis: world}.- Parameters:
index – Fractional data coordinate in this layer’s spacing-axis order.
- abstract world_extent() Dict[str, Tuple[float, float]][source]¶
The world box this layer occupies, keyed by axis.
- property axes: Tuple[str, ...][source]¶
The world axes this layer’s data is indexed by, outermost first.
Axes are named rather than positional, so two layers with different dimensionality agree on what “z” means; this lets a 2D mask be drawn over a 3D stack.
- Returns:
the axis names.
- property blending: str[source]¶
How this layer’s pixels combine with what is under it.
- Returns:
the blending mode’s name.
- property shape: Tuple[int, ...][source]¶
The layer’s spatial shape in elements,
()when it has no grid.
- property spacing: Spacing[source]¶
Where this layer’s elements sit in the world.
- Returns:
the
Spacing.
- property stack: LayerStack | None[source]¶
The stack this layer is in, or
None.
- class spacr.layers.LayerEvent[source]¶
A change notification that a
LayerStacksends to its listeners.- Parameters:
kind –
"inserted","removed","moved","renamed","changed"(a display property),"data"(the array itself) or"selected".layer – the layer it happened to, or
Nonefor stack-wide events.index – where it is (or was) in the stack, or
-1.detail – the property name for
"changed", else free text.
- class spacr.layers.LayerStack(layers: Iterable[Layer] | None = None, *, units: str | None = None)[source]¶
An ordered list of layers sharing one world.
- Parameters:
layers – the layers to start with, bottom first. Added through
add(), so each is renamed to be unique and adopted as it arrives.units – the name of the world unit –
"um","px"– shown on scale bars and axis labels. It LABELS the spacing rather than converting it: it is what the numbers inSpacingalready mean, so setting it does not rescale anything.
stack[0]is the bottom and is drawn first. Everything a viewer does to the list — add, remove, reorder, rename, select — happens here and is announced throughsubscribe(), so the widget is a view over this rather than the other way round.The stack owns two invariants the layers cannot enforce alone:
Names are unique. A duplicate name means “hide the mask” hides the wrong one. Colliding names are suffixed rather than refused, because a user adding a second mask should get one, not an error dialog.
Units agree. A layer measured in µm and a layer measured in pixels cannot be composited: the numbers would line up and the picture would not. Mixing them raises rather than drawing something plausible.
Build a stack, optionally seeded with layers.
- Parameters:
layers – the layers to start with.
- __contains__(item: Any) bool[source]¶
Whether a layer or a layer name is in the stack.
- Parameters:
item – a layer, or its name.
- Returns:
True when present.
- __getitem__(key: Any) Any[source]¶
One layer, by name or by position.
- Parameters:
key – the layer’s name, or its index.
- Returns:
the layer.
- add_image(data: Any, **kwargs: Any) ImageLayer[source]¶
Construct and append an image layer.
- Parameters:
data – Array-like intensity data passed to
ImageLayer.
- add_labels(data: Any, **kwargs: Any) LabelsLayer[source]¶
Construct and append a labels layer.
- Parameters:
data – Array-like segmentation labels passed to
LabelsLayer.
- add_points(data: Any = None, **kwargs: Any) PointsLayer[source]¶
Build a points layer and append it to this stack.
- Parameters:
data – an
(n, ndim)array of data coordinates, or None.kwargs – passed to
PointsLayer.
- Returns:
the layer that was added.
- add_shapes(shapes: Iterable[Shape] | None = None, **kwargs: Any) ShapesLayer[source]¶
Build a shapes layer and append it to this stack.
- Parameters:
shapes – the shapes to start with, or None for an empty layer.
kwargs – passed to
ShapesLayer.
- Returns:
the layer that was added.
- append(layer: Layer) Layer[source]¶
Put
layeron top.- Parameters:
layer – Layer instance to attach to this stack.
- canvas(**kwargs: Any) Canvas[source]¶
A
Canvasshowing all of this stack — seeCanvas.covering().
- get(layer: LayerLike) Layer[source]¶
The layer named by an object, a name or an index.
- Parameters:
layer – Layer object, unique layer name or integer stack index to resolve.
- index(layer: LayerLike) int[source]¶
Where
layersits, by object, name or index.- Parameters:
layer – Layer object, unique layer name or integer stack index to resolve.
- Raises:
LayerError – if it is not in this stack.
-1would be a valid index from the top and would silently affect the wrong layer.
- insert(index: int, layer: Layer) Layer[source]¶
Put
layeratindex(0 is the bottom).- Parameters:
index – Requested insertion position, clamped between zero and the current stack length.
layer – Layer instance to attach to this stack.
- lower_layer(layer: LayerLike) int[source]¶
Move a layer one position towards the back; return its new index.
- Parameters:
layer – Layer object, unique layer name or integer stack index to lower.
- move(source: LayerLike, destination: int) int[source]¶
Move a layer to
destination; returns where it ended up.The z-order control.
destinationis clamped, so “move to the top” can be spelled with any large number.- Parameters:
source – Layer object, unique layer name or integer stack index to move.
destination – Requested destination index, clamped to the stack after the source has been removed.
- pick(canvas: Canvas, row: float, column: float) Tuple[Layer | None, Dict[str, float], Any][source]¶
What is under a canvas pixel:
(layer, world, value).Walks from the TOP down and stops at the first visible layer with something there, which is what a click means — the thing you can see.
valueis the label for a labels layer, the point index for a points layer, the shape index for a shapes layer, andNoneotherwise.- Parameters:
canvas – Canvas whose displayed stack is being queried.
row – Fractional canvas-row coordinate of the query pixel.
column – Fractional canvas-column coordinate of the query pixel.
- raise_layer(layer: LayerLike) int[source]¶
Move a layer one position towards the front; return its new index.
- Parameters:
layer – Layer object, unique layer name or integer stack index to raise.
- remove(layer: LayerLike) Layer[source]¶
Take a layer out and return it.
- Parameters:
layer – Layer object, unique layer name or integer stack index to remove.
- rename(layer: LayerLike, name: str) str[source]¶
Rename a layer, uniquifying if needed; returns the name it got.
- Parameters:
layer – Layer object, unique layer name or integer stack index to rename.
name – Requested non-blank name; a numeric suffix is added if it collides with another layer.
- render(canvas: Canvas) numpy.ndarray[source]¶
Composite every visible layer;
(H, W, 3)float32 in 0–1.- Parameters:
canvas – World window and output grid on which to composite the stack.
- render_rgba(canvas: Canvas) numpy.ndarray[source]¶
render()plus the composite’s own coverage,(H, W, 4).The alpha channel is accumulated source-over whatever the layers’ blending modes were: it records where the stack drew anything, which is what a caller compositing this onto a page background needs.
- Parameters:
canvas – World window and output grid on which to composite the stack.
- render_uint8(canvas: Canvas) numpy.ndarray[source]¶
render()as(H, W, 3)uint8 — what a QImage wants.- Parameters:
canvas – World window and output grid on which to composite the stack.
- select(layer: LayerLike | None) Layer | None[source]¶
Select a layer (or
None); returns it.- Parameters:
layer – Layer object, unique layer name, integer stack index or
Noneto clear selection.
- subscribe(listener: Listener) Listener[source]¶
Register a listener called with every
LayerEvent; return it.Listeners are held by strong reference and are NOT weak: a view that subscribes must
unsubscribe()when it closes, exactly asspacr.qt.linked_selection.LinkedViewmust unlink.- Parameters:
listener – Callable receiving each
LayerEvent; it is retained by strong reference.
- to_bottom(layer: LayerLike) int[source]¶
Move one layer to the back of the stack.
- Parameters:
layer – Layer object, unique layer name or integer stack index to move.
- to_top(layer: LayerLike) int[source]¶
Move one layer to the front of the stack.
- Parameters:
layer – Layer object, unique layer name or integer stack index to move.
- unsubscribe(listener: Listener) bool[source]¶
Remove a listener; return
Trueif this call removed it.- Parameters:
listener – Previously subscribed event callback to remove.
- world_extent() Dict[str, Tuple[float, float]][source]¶
The union of every layer’s world box, keyed by axis.
Empty points and shapes layers are skipped, so a layer with nothing in it yet does not extend the box to the origin and zoom the view out.
- property selected: Layer | None[source]¶
The layer the layer-list has highlighted — what edits apply to.
- class spacr.layers.OrthoViews[source]¶
The three planes of one volume, laid out so that they line up.
A z-stack seen only from above hides the thing a z-stack is acquired for. The three canvases here are the top view and the two side views through a crosshair, arranged the way ImageJ’s orthogonal views are:
xy— the top view, rowsyand columnsx, at depthz;zx— BELOW it, rowszand columnsx, so it shares its columns with the top view;yz— BESIDE it, rowsyand columnsz, so it shares its rows with the top view.
Every panel uses one world-units-per-pixel scale. That is the whole difficulty, and it is why this is a class rather than three
Canvas.covering()calls. Confocal stacks in this codebase are routinely 0.65 µm in xy and 2 µm in z; a side view drawn one pixel per slice is three times too thin, and it does not look wrong — it looks like a slightly flat cell. Nuclear sphericity, colocalisation depth and every 3-D shape feature read off such a picture are wrong by a factor nobody sees. Here the side views are \(z_{span}/step\) pixels tall, so a 10-slice 2 µm stack is 31 px of a 0.65 µm/px view, not 10.Build one with
covering()and move the crosshair withat().- Parameters:
xy – Top-view canvas whose rows and columns span the second and third volume axes.
zx – Lower side-view canvas whose rows span depth and whose columns align with
xy.yz – Side-view canvas whose rows align with
xyand whose columns span depth.point – Mapping from world-axis names to the shared crosshair coordinates.
- at(**coords: float) OrthoViews[source]¶
The same views with the crosshair moved:
views.at(z=12.0).Each panel is re-pinned at the axes it does not span, so moving z changes what the top view shows and nothing else — which is what makes a z slider one call rather than three.
- at_pixel(panel: str, row: float, column: float) OrthoViews[source]¶
Move the crosshair to the world point under a pixel of one panel.
Clicking the side view to move the top view’s slice, which is the interaction an orthogonal view is for.
- Parameters:
panel – Panel key:
"xy","zx"or"yz".row – Fractional row coordinate within the selected panel.
column – Fractional column coordinate within the selected panel.
- clamped(**coords: float) OrthoViews[source]¶
at(), with each coordinate held inside the volume.What a slider wants: dragging past the last slice shows the last slice, not an empty canvas that looks like a failed load.
- classmethod covering(source: Any, *, width: int = 512, point: Mapping[str, float] | None = None, axes: Tuple[str, str, str] = DEFAULT_AXES, margin: float = 0.0) OrthoViews[source]¶
Three canvases showing all of
source, crossing atpoint.- Parameters:
source – a
LayerStack, aLayeror a{axis: (low, high)}extent mapping.width – the top view’s width in pixels. Every other panel size follows from it and from the world extents, so that the layout lines up and nothing is squashed.
point – where the crosshair starts,
{axis: world}. Defaults to the middle of the volume, which is the slice a user opening a stack wants to see first — slice 0 of a confocal stack is usually empty.axes – the three world axes, depth first.
margin – fraction of the extent to add on every side.
- Raises:
LayerError – if the source does not span all three axes; a 2-D source raises rather than being drawn as a single plane.
- n_slices(axis: str) int[source]¶
How many slices the slider on
axishas.- Parameters:
axis – World axis whose extent and voxel step define the count.
- render(stack: LayerStack) Dict[str, numpy.ndarray][source]¶
{panel: (H, W, 3)}— the same stack drawn on all three planes.- Parameters:
stack – Layer stack to render through each orthogonal canvas.
- resized(width: int) OrthoViews[source]¶
The same crosshair at a different pixel width, panels still aligned.
- Parameters:
width – New top-panel width in pixels; derived panel dimensions remain aligned to the same world scale.
- slider(axis: str) Tuple[float, float, float][source]¶
(low, high, step)in WORLD units for a slice slider.The step is the source’s own voxel size along that axis, so dragging the slider moves one slice at a time rather than an arbitrary fraction of the volume — and a µm-calibrated stack’s slider is labelled in µm, which is the number a user can check against the acquisition settings.
- Parameters:
axis – World axis for which to return bounds and voxel step.
- zoomed(factor: float) OrthoViews[source]¶
Every panel
factortimes closer, about the crosshair.All three together, and about the crosshair rather than about each panel’s own middle, so the panels still line up afterwards.
- Parameters:
factor – Positive finite magnification factor applied to every panel; values above one zoom in.
- class spacr.layers.PointsLayer(data: Any = None, *, name: str = 'points', ndim: int = 2, size: Any = 10.0, face_color: Any = 'yellow', border_color: Any = 'black', border_width: float = 0.0, properties: Mapping[str, Any] | None = None, **kwargs: Any)[source]¶
Bases:
LayerPoints in the world — centroids, counted objects, clicked markers.
- Parameters:
name – layer name. Default
"points".ndim – how many axes a point has. Used only when
datais empty; otherwise the data decides, so an explicitndimcannot contradict the points that were passed.face_color – fill colour, as any
to_rgba()specification.border_color – outline colour.
border_width – outline width in world units.
0draws no outline, which is the default.
Coordinates are stored in DATA units with the layer’s own spacing, exactly like an image, so a points layer built from a centroid table in pixel coordinates lines up with the mask those centroids came from without the caller converting anything. Use
add_world()/worldwhen you have world coordinates already.- Parameters:
data –
(N, ndim)array of data coordinates, in the spacing’s axis order.size – point DIAMETER in world units — scalar or one per point. World, not pixels: a 5 µm marker is 5 µm at every zoom, and on an anisotropic stack it is a sphere rather than an ellipsoid.
properties –
{name: (N,) array}per-point columns, such as a category for each counted point. Each must have one value per point.
Build a points layer.
- Parameters:
data – an
(n, ndim)array of data coordinates, or None.kwargs – name, spacing, colours and the rest of the layer contract.
- add(point: Sequence[float], *, size: float | None = None, **properties: Any) int[source]¶
Append one point in DATA coordinates; returns its index.
- Parameters:
point – Data-coordinate sequence with one value per layer axis.
- add_world(world: Mapping[str, float], **kwargs: Any) int[source]¶
Append one point given as
{axis: world}.- Parameters:
world – Mapping from axis names to world coordinates; omitted layer axes are evaluated at world coordinate zero.
- nearest(world: Mapping[str, float]) int | None[source]¶
Index of the point whose disc contains
world, nearest first.- Parameters:
world – Mapping from axis names to the query point in world coordinates; omitted layer axes are evaluated at zero.
- remove(index: int) None[source]¶
Drop one point.
- Parameters:
index – Zero-based index of the point to remove; negative indices are rejected.
- set_size(value: Any) None[source]¶
Set every point’s diameter, or one per point.
- Parameters:
value – Scalar diameter applied to every point, or an array-like sequence with one world-unit diameter per point.
- world_extent() Dict[str, Tuple[float, float]][source]¶
The world box these points occupy, INCLUDING their drawn size.
Half a point’s size is added on each side, because a point centred on the edge of the data is still drawn past it – and a viewer that framed the centres would clip every point on the boundary.
An empty layer reports a zero box rather than an infinite one.
- Returns:
{axis: (low, high)}.
- property border_color: Tuple[float, float, float, float][source]¶
The outline colour every point is drawn with.
- Returns:
RGBA, each component 0 to 1.
- property data: numpy.ndarray[source]¶
The points in data coordinates,
(N, ndim).
- property face_color: Tuple[float, float, float, float][source]¶
The fill colour every point is drawn with.
- Returns:
RGBA, each component 0 to 1.
- property size: numpy.ndarray[source]¶
Per-point diameters in world units.
- property world: numpy.ndarray[source]¶
The points in world coordinates,
(N, ndim).
- class spacr.layers.RenderScale[source]¶
How finely one 3-D frame is drawn.
- Parameters:
percent – the share of full resolution, 1 to 100, along each axis.
max_voxels – the most voxels the frame is drawn from (see
volume_steps()), orNonefor every voxel.pixel – display pixels per rendered pixel along each side.
sample – voxels between two samples along a line of sight.
- class spacr.layers.Shape[source]¶
One drawn region: a polygon, rectangle, ellipse, line or path.
Compared by identity (
eq=False): a generated__eq__would compare the vertex arrays elementwise and raise “truth value of an array is ambiguous” from anything as ordinary asshape in layer.shapes.- Parameters:
kind –
"polygon","rectangle","ellipse","line"or"path". The first three enclose an area and can be turned into a mask; the last two are open and only have an outline.data –
(M, ndim)vertices in DATA coordinates, in the layer’s axis order. A rectangle and an ellipse are stored as their four bounding corners.name – what the ROI is called, so a mask can be attributed.
- __post_init__() None[source]¶
Normalise the kind and refuse one this layer cannot draw.
An unknown kind raises
LayerErrorat construction rather than at paint time, because a shape with an unknown kind draws nothing and the fault would otherwise appear to be in the renderer.
- class spacr.layers.ShapesLayer(shapes: Iterable[Shape] | None = None, *, name: str = 'shapes', ndim: int = 2, **kwargs: Any)[source]¶
Bases:
LayerDrawn regions of interest;
spacr.roiuses them to limit Measure to a region.- Parameters:
shapes – the
Shapeobjects to start with. Every one must have the same number of axes – a layer holding a 2-D and a 3-D shape could not be rasterised into one mask.name – layer name. Default
"shapes".ndim – axes per shape, used only when
shapesis empty; the shapes decide otherwise.
- Raises:
LayerError – when the shapes disagree about how many axes they have.
Shapes are geometry, not pixels: they are stored as vertices in data coordinates and rasterised on demand, so the same ROI can be turned into a mask for a full-resolution mask layer and for a downsampled preview and mean the same region in both.
mask()is that conversion.Build a shapes layer.
- Parameters:
shapes – the shapes to start with, or None for an empty layer.
kwargs – name, spacing and the rest of the layer contract.
- add(shape: Shape) int[source]¶
Append a shape; returns its index.
- Parameters:
shape –
Shapewith the same dimensionality as this layer.
- add_ellipse(corner_a: Any, corner_b: Any, **kwargs: Any) int[source]¶
Append an axis-aligned ellipse inside an opposite-corner box.
- Parameters:
corner_a – First bounding-box corner in data coordinates, one value per axis.
corner_b – Opposite bounding-box corner in data coordinates, one value per axis.
- add_path(vertices: Any, **kwargs: Any) int[source]¶
Append an open path.
- Parameters:
vertices – Array-like vertices in data coordinates, shaped
(M, ndim)with at least two rows.
- add_polygon(vertices: Any, **kwargs: Any) int[source]¶
Append a closed polygon.
- Parameters:
vertices – Array-like vertices in data coordinates, shaped
(M, ndim)with at least three rows.
- add_rectangle(corner_a: Any, corner_b: Any, **kwargs: Any) int[source]¶
Append an axis-aligned rectangle from opposite corners.
- Parameters:
corner_a – First corner in data coordinates, one value per axis.
corner_b – Opposite corner in data coordinates, one value per axis.
- mask(canvas: Canvas, indices: Iterable[int] | None = None) numpy.ndarray[source]¶
Rasterise the enclosed shapes onto
canvas;(H, W)bool.The conversion Measure needs: hand it
Canvas.for_grid(mask_layer.spacing, mask_layer.shape)and the ROI comes back on that layer’s own grid, whatever grid it was drawn on.Boundaries are HALF-OPEN: a pixel centre exactly on the low edge of a rectangle is inside it and one exactly on the high edge is not. That is the even-odd rule’s own convention, and it is the one that matters — two ROIs sharing an edge partition the pixels between them instead of both claiming the seam, so an object on the boundary is counted once.
Open shapes (lines, paths) enclose nothing and contribute nothing.
- Parameters:
canvas – World plane and pixel grid on which to rasterise the closed shapes.
- remove(index: int) Shape[source]¶
Drop a shape and return it.
- Parameters:
index – Integer index of the shape to remove; normal Python negative indexing is accepted.
- world_extent() Dict[str, Tuple[float, float]][source]¶
The world box every shape’s vertices fall within.
An empty layer reports a zero box rather than an infinite one, so a viewer framing the stack is not dragged to infinity by a layer that has nothing in it yet.
- Returns:
{axis: (low, high)}.
- class spacr.layers.Spacing[source]¶
Where a layer’s array elements sit in the world.
world = translate + scale * index, per axis, with axes named so that two layers can agree on what “z” means without agreeing on array shape.- Parameters:
scale – world size of one element along each axis — the voxel size. Non-zero and finite, per axis: a zero would collapse the axis and every world query on it would answer with element 0, which draws a plausible picture of the wrong slice.
translate – world coordinate of element 0 along each axis. Defaults to zeros. This is the crop offset — a tile cut out of a mosaic keeps its place in the mosaic by carrying it here.
axes – axis names, outermost first. Defaults to
("z", "y", "x")truncated tolen(scale).units – what one world unit is. Compared by name when layers are stacked, because “0.65” means nothing until you know it is µm.
Anisotropy is the normal case, not the exception:
Spacing.from_map({"z": 2.0, "y": 0.65, "x": 0.65}, units="um")
- __post_init__() None[source]¶
Coerce and validate the voxel size, then freeze it.
A zero or non-finite scale raises
LayerErrorrather than being clamped. Such a scale collapses an axis – every world coordinate on it resolves to element 0 – and the overlay is then drawn a slice out of register with no visible sign of the error.
- axis_index(axis: str) int[source]¶
Position of
axisin this spacing.- Parameters:
axis – Axis name to locate in
axes.- Raises:
LayerError – if this spacing has no such axis.
- data_from_map(world: Mapping[str, float]) Tuple[float, ...][source]¶
Fractional data index from a
{axis: world}mapping.Axes the mapping does not mention are taken to be 0 in world units, which is what a 2-D click on a 3-D layer means once the viewer has supplied the slice it is showing.
- Parameters:
world – Mapping from axis names to world coordinates. Unnamed spacing axes are evaluated at world coordinate zero.
- extent(shape: Sequence[int]) Dict[str, Tuple[float, float]][source]¶
World bounding box of an array of
shape, keyed by axis.Measured to the OUTER EDGE of the end elements (half a voxel beyond the first and last centres), because that is the region the data actually covers — a canvas fitted to element centres clips half a voxel off every side, which at 2 µm z-steps is a visible slab.
- Parameters:
shape – Array shape in
axesorder, with one size per spacing axis.
- classmethod from_map(sizes: Mapping[str, float], *, origin: Mapping[str, float] | None = None, units: str = 'px') Spacing[source]¶
Build from
{"z": 2.0, "y": 0.65, "x": 0.65}.Order is the mapping’s own, which for a dict literal is the order it was written — the same order the array’s axes are in. Naming the axes at the call site is the point:
(2.0, 0.65, 0.65)on its own has been read backwards before.- Parameters:
sizes – Mapping from axis name to non-zero finite voxel size; mapping iteration order becomes the array-axis order.
- has_axis(axis: str) bool[source]¶
Whether this spacing contains
axis.- Parameters:
axis – Axis name to test after coercion to a string.
- classmethod isotropic(ndim: int = 2, step: float = 1.0, units: str = 'px') Spacing[source]¶
Equal spacing on every axis — the pixel-grid default.
- rescaled(**sizes: float) Spacing[source]¶
A copy with some axes’ voxel sizes replaced:
sp.rescaled(z=1.5).
- to_data(world: Sequence[float]) Tuple[float, ...][source]¶
World coordinate → (fractional) data index, per axis.
- Parameters:
world – World coordinate in
axesorder, with one entry per spacing axis.
- to_world(index: Sequence[float]) Tuple[float, ...][source]¶
Data index → world coordinate, per axis.
- Parameters:
index – Fractional data coordinate in
axesorder, with one entry per spacing axis.
- class spacr.layers.SurfaceFinish[source]¶
How a mask surface reflects light in a 3-D view (Blinn-Phong).
A surface point of base colour
cwith unit normalnis drawn asc * (ambient + diffuse * max(n.l, 0)) + specular * max(n.h, 0) ** shininess * mix(white, c, tint), wherelpoints at the light andhhalfway between the light and the viewer. The GPU and the CPU renderers use the same numbers.- Parameters:
name – the finish’s key in
SURFACE_FINISHES.ambient – light every point gets, facing the light or not.
diffuse – weight of the light falling on the surface.
specular – weight of the highlight; 0 has none.
shininess – how small and sharp the highlight is.
tint – 0 for a white highlight (plastic), 1 for a highlight in the object’s own colour (metal).
- class spacr.layers.SurfaceLight[source]¶
Where the light and the viewer are, for shading surfaces along rays.
Every vector is a unit vector in physical units, one component per array axis of the volume (its storage order). The light is a head light a little above and left of the viewer, so it turns with the camera.
- Parameters:
view – towards the viewer.
light – towards the light.
half – halfway between
lightandview.size – the physical length of one voxel along each array axis.
- classmethod for_rays(rays: VolumeRays, voxel_size: Sequence[float] | None = None) SurfaceLight[source]¶
The light for one camera.
- Parameters:
rays – the camera’s rays, in array indices.
voxel_size – physical length of one voxel along each array axis;
Nonefor cubic voxels of length 1.
- Returns:
the
SurfaceLight.
- class spacr.layers.VolumeLens[source]¶
A magnifying cube in a volume, for a 3-D view’s magnifier.
Inside the cube the volume is drawn
zoomtimes larger about the cube’s centre; outside it, everything is dimmed todimof its brightness. Positions are array indices, as forVolumeRays.- Parameters:
centre – the cube’s centre.
half – half the cube’s side along each array axis.
zoom – how many times larger the inside is drawn (at least 1).
dim – brightness kept outside the cube, 0 to 1.
- contains(points: Any) numpy.ndarray[source]¶
Whether points lie inside the cube.
- Parameters:
points –
(axes, ...)array indices.- Returns:
a boolean array of the points’ shape without the axes.
- corners() numpy.ndarray[source]¶
The cube’s eight corners.
- Returns:
(8, axes)array indices.
- covers(rays: VolumeRays) numpy.ndarray[source]¶
Which rays pass through the cube between their first and last sample.
- Parameters:
rays – the camera’s rays.
- Returns:
(rows, columns)booleans;Falsepixels are dimmed all the way along.
- warp(points: Any) Tuple[numpy.ndarray, numpy.ndarray][source]¶
Where each point samples the volume, and how bright it is drawn.
- Parameters:
points –
(axes, ...)array indices.- Returns:
(where, weight): inside the cube a point samplescentre + (point - centre) / zoomat weight 1; outside it samples itself at weightdim.
- class spacr.layers.VolumeRays[source]¶
Parallel rays through a volume, one per output pixel.
Positions are continuous array indices of the volume being drawn (voxel
iis centred on indexi). The samplekof the ray through output rowrand columnclies atorigin + c * across + r * down + k * depth; sample 0 is nearest the viewer.- Parameters:
origin – the first sample of the top-left ray.
across – the step from one output column to the next.
down – the step from one output row to the next.
depth – the step between samples, away from the viewer.
shape –
(rows, columns, samples).
- positions(first: int, last: int) numpy.ndarray[source]¶
Every sample of the rays through output rows
firsttolast.- Parameters:
first – the first row.
last – one past the last row.
- Returns:
an
(axes, rays, samples)array of indices.
- ray(row: float, column: float) VolumeRays[source]¶
The single ray through one output pixel, as its own 1 x 1 grid.
Rendering it draws (and picks) exactly what that pixel of the full grid shows, so a click can be answered without drawing the frame.
- Parameters:
row – the output row (may be fractional).
column – the output column (may be fractional).
- Returns:
a
VolumeRaysof shape(1, 1, samples).
- class spacr.layers.WGPUVolumeRenderer(adapter: Any)[source]¶
render_volume(), ray-marched on the GPU through wgpu (WGSL).The third renderer of the same frame: Vulkan, Metal or Direct3D 12 by the adapter it is given. The same rays (
VolumeRays), renderings, finishes and lens asrender_volume()andGLVolumeRenderer; one compute invocation walks one ray. Volumes are read withtextureLoadand interpolated in the shader exactly asscipy.ndimage.map_coordinates()does (order=1, edges repeated), image layers add up, labels layers are laid over them in stack order, and each labels layer writes the object nearest the viewer under each ray for picking. The frame is drawn into a storage buffer, scaled to the picture’s size and read back, so it lands under the same matplotlib axes as the CPU’s.A volume is uploaded once and kept, keyed by its memory, for as long as every frame draws it, as
GLVolumeRendererdoes.- Parameters:
adapter – a wgpu adapter (
wgpu_adapter()).
Ask the adapter for a device with its own largest limits.
- limits() Dict[str, Any][source]¶
The adapter’s name, its backend and its largest 3-D texture side.
- Returns:
{"renderer": str, "version": str, "max_3d": int, "adapter_type": str}.
- render(layers: Iterable[Layer], rays: VolumeRays, *, highlight: int = 0, voxel_size: Sequence[float] | None = None, lens: VolumeLens | None = None, size: Tuple[int, int] | None = None) Tuple[numpy.ndarray, numpy.ndarray][source]¶
Draw one frame; the same picture as
render_volume().- Parameters:
layers – the layers, bottom first.
rays – where to sample, in the layers’ array indices.
highlight – an object drawn lighter.
voxel_size – physical voxel length along each array axis.
lens – a magnifier cube, or
None.size –
(rows, columns)of the picture returned, scaled up from the rays’ grid on the GPU;Nonekeeps the rays’ grid.
- Returns:
(rgb, picked):uint8colour ofsizeand the object nearest the viewer under each ray (int64, the rays’ grid).- Raises:
LayerError – as
render_volume().
- spacr.layers.colormap(spec: Any) Colormap[source]¶
Resolve
specto aColormap.Takes a
Colormap(returned unchanged), a name inCOLORMAPS, a colour (anyto_rgba()form) which becomes a black→colour ramp, or — as a last resort — a matplotlib colormap name.Matplotlib is tried last and lazily. This module must stay importable where matplotlib is not installed and cheap to import in a worker, so it is never a hard dependency; but if a user types
"viridis"and matplotlib is there, refusing would be pedantry.- Parameters:
spec – Existing
Colormap, built-in or matplotlib colormap name, or a colour specification from which to build a black-to-colour ramp.- Raises:
LayerError – if
specnames nothing.
- spacr.layers.downsample_volume(array: Any, max_voxels: int | None) Tuple[numpy.ndarray, Tuple[int, ...]][source]¶
A volume small enough to turn interactively, and how it was reduced.
Every
step-th voxel is kept along each axis (seevolume_steps()), so voxeljof the result is voxelj * stepof the input. Labels keep their exact IDs and an image its exact values; nothing is averaged or interpolated.- Parameters:
array – the volume.
max_voxels – the most voxels the result may hold, or
Noneto keep every voxel.
- Returns:
(volume, steps); the input itself when nothing is removed.
- spacr.layers.gpu_render_refusal() str[source]¶
Why a 3-D view must not draw on the GPU here, or
"".- Returns:
"SPACR_NO_GPU_RENDER is set"(GPU_RENDER_OFF_ENV, any value but0or empty),"CUDA_VISIBLE_DEVICES is empty"(set to nothing), or""when the GPU may be used.
- spacr.layers.label_color(label: int, *, seed: int = 0) Tuple[float, float, float][source]¶
A stable, vivid colour for integer
label.Golden-ratio hue spacing with a deterministic saturation/value jitter, so adjacent labels are far apart in hue and the same object keeps its colour across zooms, re-renders and sessions. Label 0 is background and is black (callers draw it transparent).
The construction is deliberately the same one
spacr.qt.widgets.live_preview._random_outline_palette()uses for outlines, so a mask has the same object colours in both viewers. It is reimplemented rather than imported because that module imports PySide6 and this one must not — see the module docstring.- Parameters:
label – Integer-like object label. Label 0 is reserved for the black background colour.
- spacr.layers.label_colors(labels: Any, *, seed: int = 0) numpy.ndarray[source]¶
label_color()over an array, returning(..., 3)float32.Built by looking up the unique labels once rather than per pixel: a 2048×2048 mask holds a few hundred objects and tens of millions of pixels.
- Parameters:
labels – Array-like integer labels; the output preserves their shape and appends one RGB axis.
- spacr.layers.render_scale(percent: Any, voxels: int, *, dragging: bool = False, gpu: bool = False, gpu_max_voxels: int | None = GPU_MAX_VOXELS) RenderScale[source]¶
Turn the Render scale slider into what one frame is drawn from.
percentis the share of full resolution along each axis: at 50 % every second voxel along X, Y and Z (an eighth of the voxels) is drawn, into a picture of half the display’s width and height. While the view is dragged the frame is drawn coarser still and then again at the slider’s scale once it stops: the CPU draws from at mostRENDER_DRAG_VOXELSvoxels into pixels of at least 4 display pixels; the GPU keeps its voxels (so nothing is uploaded again) and draws pixels of at least 2.- Parameters:
percent – the slider, 1 to 100 (clipped).
voxels – how many voxels the full volume holds.
dragging – whether the view is moving.
gpu – whether the GPU draws the frame.
gpu_max_voxels – the most voxels one GPU texture may hold.
- Returns:
the
RenderScale.
- spacr.layers.render_volume(layers: Iterable[Layer], rays: VolumeRays, *, highlight: int = 0, cancelled: Callable[[], bool] | None = None, chunk: int = 300000, voxel_size: Sequence[float] | None = None, lens: VolumeLens | None = None) Tuple[numpy.ndarray, numpy.ndarray] | None[source]¶
Draw 3-D image and labels layers along parallel rays, as napari does.
Image layers are added together, each through its colormap, between its contrast limits, times its opacity; each is drawn by the
VOLUME_RENDERINGSmode inlayer.metadata["rendering"](mipwhen absent). Labels layers are then laid over them in stack order, each object in itslabel_color(), by theLABEL_RENDERINGSmode (surfacewhen absent) and the layer’s opacity. A surface is lit by theSURFACE_FINISHESentry inlayer.metadata["finish"](mattewhen absent), with normals from the mask itself (surface_normals()). Hidden layers draw nothing. Every layer must have the volume’s shape; image layers draw their first channel.GLVolumeRendererdraws the same picture on the GPU.The rays are walked a few rows at a time, so a caller on a worker thread can stop a render that is no longer wanted.
- Parameters:
layers – the layers, bottom first.
rays – where to sample, in the layers’ array indices.
highlight – an object drawn lighter, as a chosen object is.
cancelled – asked between chunks; returning
Trueabandons the render.chunk – about how many samples one step holds.
voxel_size – physical length of one voxel along each array axis, for lighting;
Nonefor cubic voxels.lens – a magnifier cube (
VolumeLens), orNone.
- Returns:
(rgb, picked):rows x columns x 3float32 colour between 0 and 1, and the object nearest the viewer under each pixel (0 for none), orNonewhen cancelled.- Raises:
LayerError – for a rendering or finish no layer of that kind has, or a layer whose shape differs from the others’.
- spacr.layers.shade_surface(colour: numpy.ndarray, normal: numpy.ndarray, light: SurfaceLight, finish: SurfaceFinish) numpy.ndarray[source]¶
Light surface points of known colour and normal by one finish.
- Parameters:
colour –
(..., 3)base colours, 0 to 1.normal –
(..., axes)unit normals facing the viewer.light – the camera’s
SurfaceLight.finish – the
SurfaceFinish.
- Returns:
(..., 3)lit colour (not clipped).
- spacr.layers.surface_finish(name: Any) SurfaceFinish[source]¶
The lighting numbers of one finish.
- Parameters:
name – a key of
SURFACE_FINISHES;Noneismatte.- Returns:
the
SurfaceFinish.- Raises:
LayerError – for a name that is not a finish.
- spacr.layers.surface_normals(occupancy: numpy.ndarray, points: numpy.ndarray, light: SurfaceLight) numpy.ndarray[source]¶
Outward unit normals of a mask’s surface at points on it.
Central differences, one voxel each way, of the trilinearly sampled occupancy (1 inside an object, 0 outside), scaled to physical units and turned to face the viewer. Where the occupancy is flat the normal is the direction to the viewer.
- Parameters:
occupancy –
float32occupancy volume.points –
(axes, n)array indices.light – the camera’s
SurfaceLight.
- Returns:
(n, axes)normals.
- spacr.layers.to_rgba(colour: Any, *, alpha: float | None = None) Tuple[float, float, float, float][source]¶
Coerce anything that names a colour into
(r, g, b, a)in 0–1.Accepts a name from
_NAMED_COLORS,#rgb/#rrggbb/#rrggbbaa, a 3- or 4-tuple in 0–1, or a 3- or 4-tuple in 0–255 (any component above 1 switches the whole tuple to the 0–255 reading, which is the only unambiguous rule —(1, 1, 1)is white either way).- Parameters:
colour – named, hexadecimal or three-/four-component colour value to normalise; numeric components may use either the 0–1 or 0–255 scale.
alpha – overrides whatever the colour carried.
- Raises:
LayerError – on anything else.
- spacr.layers.volume_steps(shape: Sequence[int], max_voxels: int | None) Tuple[int, ...][source]¶
The integer stride along each axis that fits a volume in a budget.
The longest strided axis is halved in resolution first, so a thin stack (40 sections of 1,000 x 1,000) keeps its depth while its width is reduced.
- Parameters:
shape – the volume’s shape.
max_voxels – the most voxels the strided volume may hold, or
Nonefor no limit.
- Returns:
one stride per axis, each at least 1.
- spacr.layers.wgpu_adapter(backend: str, allow_software: bool = True) Any[source]¶
A fresh wgpu adapter of one backend, the best kind first.
A discrete GPU is preferred to an integrated one, and a software rasteriser (
adapter_typeCPU) comes last, or not at all.- Parameters:
backend – a key of
WGPU_BACKENDS.allow_software – whether a software rasteriser is accepted.
- Returns:
the adapter, ready for one device.
- Raises:
RuntimeError – with a one-line reason when there is none.
- spacr.layers.wgpu_adapters(refresh: bool = False) Tuple[Dict[str, str], ...][source]¶
The graphics adapters wgpu can draw the True 3D view with.
wgpu is imported the first time this is asked, never before, so a computer without it pays nothing; it is then remembered.
- Parameters:
refresh – look again rather than use what was found before.
- Returns:
one
{"backend", "name", "adapter_type", "description"}per Vulkan, Metal or Direct3D 12 adapter; empty without wgpu.
Nested helpers¶
- OrthoViews.covering._canvas(pair, shape)¶
Return a shared-scale
Canvasforpairandshape.spacr/layers.py:1144
- SurfaceLight.for_rays.unit(vector: np.ndarray) np.ndarray¶
vectorscaled to length 1 (a zero vector is left alone).spacr/layers.py:4276