spacr.layers¶
A napari-style layer model — images, labels, points and shapes in one world.
Every viewer spaCR has ever shipped draws one thing. The live preview draws an image with outlines burnt into it; the timelapse preview draws a frame with tracks burnt into it; Make Masks draws an image with a brush burnt into it. “Burnt in” is the problem: the mask is not a thing you can hide, fade, recolour or put underneath something else — it is pixels that were already RGB by the time the widget saw them.
This module is the model underneath a viewer where those are separate objects.
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.
Treating spacing as decoration is a silent error, not a cosmetic one: an overlay drawn a slice out of register still looks like a plausible image, and the number that comes out of it is wrong with no warning. Hence the guards here are loud — a zero scale raises, and a stack refuses to mix µm layers with px layers rather than quietly 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 has something to say) and
opacity (how loudly). All five modes combine them as alpha = coverage *
opacity, except Blending.OPAQUE, which first hardens coverage to
all-or-nothing — that is what makes it a curtain rather than a veil, and it is
the only place the two are treated differently.
Exceptions¶
A layer, spacing or stack that cannot mean what it was asked to mean. |
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. |
|
One thing in the stack: a name, a place in the world, and how to draw it. |
|
Something changed. What a view listens for instead of polling. |
|
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. |
|
One drawn region: a polygon, rectangle, ellipse, line or path. |
|
Drawn regions of interest — the layer Measure will later read. |
|
Where a layer's array elements sit in the world. |
Functions¶
|
Resolve |
|
A stable, vivid colour for integer |
|
|
|
Coerce anything that names a colour into |
Module Contents¶
- exception spacr.layers.LayerError[source]¶
Bases:
ValueErrorA layer, spacing or stack that cannot mean what it was asked to mean.
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 hardens coverage to 0-or-1 first so that a soft-edged layer becomes a curtain. 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, zooming one zooms all of them, and the point is that a difference between two panels is a difference in the data rather than a difference in where they happen to be looking.
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]¶
A panel’s widget changed size. Only that panel’s shape changes.
The STEP is held, not the span — the opposite of
Canvas.resized(), and deliberately. In a linked grid the shared quantity is the magnification: a cell that is 10 px narrower than its neighbour must show 10 px less of the sample at the same scale, or the two pictures are at different magnifications and the comparison the grid exists for is not one.- 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]¶
Be told, with the key that moved, whenever a panel 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]¶
Stop being told.
Trueif this call is what 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.Noneis honest for a mask loaded from a file that does not say what it is.
- 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.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]¶
One thing in the stack: a name, a place in the world, and how to draw it.
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 saying where this layer has anything to say. 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.
NAMED, NOT POSITIONAL, so two layers with different dimensionality can still agree on what “z” means – which is what lets a 2D mask sit over a 3D stack without either of them guessing.
- 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]¶
Something changed. What a view listens for instead of polling.
- 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 world 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]¶
One step towards the back.
- 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]¶
One step towards the front.
- 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]¶
Be told when anything changes. Returns
listener, for unsubscribing.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]¶
Stop being told.
Trueif this call is what 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 layers (a points layer with no points yet) do not drag the extent to the origin — an empty layer occupies nothing, and letting it vote would zoom the view out to include a point nobody has placed.
- 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 — an orthogonal view of a 2-D field is not a view, it is a mistake, and silently drawing one plane would hide it.
- 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 because an outline on a centroid marker costs more than it says.
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}rider columns — what the counting item hangs its categories off.
Build a points layer.
- Parameters:
data – an
(n, ndim)array of world 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.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.
REFUSED AT CONSTRUCTION rather than at paint time, because a shape with an unknown kind draws nothing and the reader would look for the fault 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 — the layer Measure will later read.
- 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 IS REFUSED HERE rather than tolerated. It 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 SYMPTOM, which is why this raises rather than clamping.
- 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.
- 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.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.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.
Nested helpers¶
- OrthoViews.covering._canvas(pair, shape)¶
Return a shared-scale
Canvasforpairandshape.spacr/layers.py:1090