Skip to content

Paint

Paint controllers apply label edits to data stores, with undo/redo history and write buffering for multiscale data.

Controllers

cellier.paint.AbstractPaintController

Bases: ABC

Minimal shared base for paint controllers.

Owns: EventBus subscriptions, :class:CommandHistory, :class:ActiveStroke accumulation, and camera-controller locking on the bound canvas.

Concrete subclasses override the _read_old_values, _write_values, _on_stroke_completed, commit, and abort hooks to plug in storage-specific behaviour.

Parameters:

Name Type Description Default
cellier_controller CellierController
required
visual_id UUID
required
scene_id UUID
required
canvas_id UUID

Mouse subscriptions are scoped to this canvas; its camera controller is disabled for the session duration.

required
data_store_id UUID

Recorded on every :class:PaintStrokeCommand for history attribution.

required
brush_value int
1
brush_radius_voxels float
2.0
history_depth int
100

brush_value property writable

brush_value: int

Scalar value written to every painted voxel.

brush_radius_voxels property writable

brush_radius_voxels: float

Brush radius in voxel units.

undo

undo() -> None

Undo the most recent stroke.

redo

redo() -> None

Redo the most recently undone stroke.

commit abstractmethod

commit() -> None

End the session, persisting all painted data.

abort abstractmethod

abort() -> None

End the session, reverting all painted data.

cellier.paint.SyncPaintController

Bases: AbstractPaintController

Paint controller for :class:ImageMemoryStore and :class:LabelMemoryStore.

Directly and synchronously mutates the backing numpy array on every brush application, then calls :meth:CellierController.reslice_scene so the display updates immediately. Suitable for testing and in-memory annotation workflows.

Parameters:

Name Type Description Default
cellier_controller CellierController
required
visual_id UUID
required
scene_id UUID
required
canvas_id UUID
required
data_store ImageMemoryStore | LabelMemoryStore

The store whose .data array is painted directly.

required
displayed_axes tuple[int, int]

The two data-array axes currently displayed in 2D for the bound canvas, in (row_axis, col_axis) order.

required
brush_value int
1
brush_radius_voxels float
2.0
history_depth int
100

brush_value property writable

brush_value: int

Scalar value written to every painted voxel.

brush_radius_voxels property writable

brush_radius_voxels: float

Brush radius in voxel units.

undo

undo() -> None

Undo the most recent stroke.

redo

redo() -> None

Redo the most recently undone stroke.

commit

commit() -> None

End the session. Data is already in the array; just teardown.

abort

abort() -> None

Revert all strokes by replaying the undo stack in reverse.

cellier.paint.MultiscalePaintController

Bases: AbstractPaintController

Paint controller for OME-Zarr / multiscale-zarr data stores.

Stages all writes in a tensorstore transaction (RAM-only) until the user commits or aborts the session. Visible feedback is provided by writing directly into the GPU paint cache; the shader composites paint over the base sample so painted voxels are visible immediately without a slicer round-trip.

Parameters:

Name Type Description Default
cellier_controller CellierController
required
visual_id UUID
required
scene_id UUID
required
canvas_id UUID
required
data_store Any

Either OMEZarrImageDataStore or MultiscaleZarrDataStore. Must expose _ts_stores: list[ts.TensorStore] and level_shapes / level_transforms.

required
visual_block_size int

Tile / brick side length used by the visual's render config. Must match the MultiscaleImageRenderConfig.block_size of the rendered visual.

required
displayed_axes tuple[int, ...]

The two data-array axes currently displayed in 2D for the bound canvas, in (row_axis, col_axis) order. 3D paint feedback is a follow-up; passing a 3-tuple raises NotImplementedError.

required
brush_value int
1
brush_radius_voxels float
2.0
history_depth int
100
autosave_interval_s float | None

If set, a QTimer fires every this many seconds to flush staged paint to disk, rebuild the pyramid, and reset the GPU paint textures. None disables autosave.

None

brush_value property writable

brush_value: int

Scalar value written to every painted voxel.

brush_radius_voxels property writable

brush_radius_voxels: float

Brush radius in voxel units.

autosave_count property

autosave_count: int

Number of autosaves completed this session.

last_autosave_time property

last_autosave_time: datetime | None

Datetime of the most recent autosave, or None if none yet.

undo

undo() -> None

Undo the most recent stroke.

redo

redo() -> None

Redo the most recently undone stroke.

commit

commit() -> None

Flush staged paint + rebuild pyramid, clear GPU, repopulate cache.

abort

abort() -> None

Discard staged paint and clear the GPU paint textures.

History

cellier.paint.ActiveStroke

Accumulator that builds a :class:PaintStrokeCommand during a drag.

record is called once per brush application. Successive calls concatenate their voxel arrays. finalise constructs the immutable :class:PaintStrokeCommand. After finalise, no further record calls are permitted.

gesture_id property

gesture_id: UUID | None

The gesture this stroke belongs to, if known.

record

record(voxel_indices: ndarray, old_values: ndarray, new_values: ndarray) -> None

Append the voxels painted in one brush application.

finalise

finalise() -> PaintStrokeCommand

Return the completed command. Do not call record after this.

Successive record calls during a drag often overlap. For each unique voxel we keep the first old_values seen — that is the true pre-stroke value. Without this dedup, an undo replays stale "old" values captured after earlier in-stroke writes, leaving residue. new_values are also taken from the first occurrence (within one stroke they are constant anyway).

cellier.paint.CommandHistory

Bounded deque of :class:PaintStrokeCommand with undo/redo pointers.

Only the undo stack is bounded by max_depth; the redo stack is unbounded because it is cleared by push before it can grow indefinitely.

can_undo property

can_undo: bool

True if there is at least one command on the undo stack.

can_redo property

can_redo: bool

True if there is at least one command on the redo stack.

push

push(command: PaintStrokeCommand) -> None

Push a new command; clears the redo stack.

undo

undo() -> PaintStrokeCommand | None

Pop from the undo stack and push to the redo stack.

redo

redo() -> PaintStrokeCommand | None

Pop from the redo stack and push to the undo stack.

cellier.paint.PaintStrokeCommand dataclass

Immutable record of one completed brush stroke.

Attributes:

Name Type Description
visual_id UUID
data_store_id UUID
voxel_indices ndarray

Shape (N, ndim), int64. Each row is one voxel index in data-array order (e.g. (z, y, x) for a 3-D store).

old_values ndarray

Shape (N,), float32. Value at each voxel before the stroke.

new_values ndarray

Shape (N,), float32. Value written by the stroke.

Write buffers & layers

cellier.paint.WriteBuffer

Bases: Protocol

Protocol for staged voxel writes against a level-0 store.

Concrete implementations stage writes in RAM, allow read-your-writes visibility, and flush or discard atomically.

Implementations must be safe to call from the Qt main thread; the paint controller never invokes them from a background thread.

stage

stage(voxel_indices: ndarray, values: ndarray) -> None

Stage a vector of point writes.

Parameters:

Name Type Description Default
voxel_indices ndarray

Shape (N, ndim) int64. Each row is one level-0 voxel index in data-array axis order.

required
values ndarray

Shape (N,). Values to write. Must be castable to the underlying store dtype.

required

read_staged

read_staged(voxel_indices: ndarray) -> ndarray

Return the current values at the given voxel indices.

With read-your-writes semantics, this returns staged values for voxels that have been written through stage, falling back to the underlying store for unwritten voxels.

Returns shape (N,) in the underlying store's native dtype.

commit

commit() -> None

Flush all staged writes to durable storage. Blocks until done.

abort

abort() -> None

Discard all staged writes. Returns the buffer to a fresh state.

cellier.paint.TensorStoreWriteBuffer

:class:WriteBuffer backed by a :class:tensorstore.Transaction.

Wraps a single TensorStore handle (typically the level-0 store of a multiscale data store) in an isolated transaction. All writes are coalesced in tensorstore's in-memory write buffer until commit or abort is called.

Parameters:

Name Type Description Default
store TensorStore

The tensorstore handle to paint against. This must be the level-0 store of the multiscale data store.

required
Notes

The transaction is created in isolated mode (the default) which is what we want: paints are not visible outside the transaction until commit.

After commit or abort the buffer is one-shot — calling stage again will fail since the transaction has been finalised. The paint controller wraps lifecycle management around this.

transaction property

transaction: Transaction | None

The active transaction, or None after commit/abort.

transactional_store property

transactional_store: TensorStore | None

The store wrapped in this buffer's transaction (read-your-writes).

__init__

__init__(store: TensorStore, transaction: Transaction | None = None) -> None

Construct.

Parameters:

Name Type Description Default
store TensorStore

Untransacted level-0 store handle.

required
transaction Transaction | None

Optional pre-built transaction. When provided, the same transaction can be shared with other consumers (e.g. the data store's read path) so they observe staged writes. When None, a fresh transaction is created.

None

cellier.paint.WriteLayer

Sparse in-memory tracker of dirty level-0 bricks for a paint session.

Records which bricks have been touched; the painted voxel data itself lives in the :class:WriteBuffer. N-dim agnostic — used unchanged for 2-D and 3-D paint sessions.

Parameters:

Name Type Description Default
data_store_id

UUID of the painted store. Recorded but unused by WriteLayer itself; useful for logs and assertions.

required
block_size int

Side length of one level-0 brick in voxels. Must match the render_config.block_size of the visual being painted, since the paint controller relies on WriteLayer and the renderer agreeing on brick boundaries.

required

mark_dirty

mark_dirty(key: BrickKey) -> None

Mark a single brick as dirty.

is_dirty

is_dirty(key: BrickKey) -> bool

Return True if key is currently marked dirty.

dirty_keys

dirty_keys() -> set[BrickKey]

Return a copy of the current dirty-brick set.

clear

clear() -> None

Drop all dirty-brick entries. Called at session end.

voxel_to_brick_key

voxel_to_brick_key(voxel_idx: ndarray) -> BrickKey

Map a level-0 voxel index to its enclosing level-0 :class:BrickKey.

Parameters:

Name Type Description Default
voxel_idx ndarray

Shape (ndim,), integer. In data-array axis order (e.g. (z, y, x) for a 3-D store).

required

Returns:

Type Description
BrickKey

level=0 and grid_coords[i] = voxel_idx[i] // block_size.

voxels_to_brick_keys

voxels_to_brick_keys(voxel_indices: ndarray) -> set[BrickKey]

Vectorised :meth:voxel_to_brick_key over an (N, ndim) array.

Returns the unique set of bricks the voxels fall into.

cellier.paint.BrickKey

Bases: NamedTuple

Identifier for a level-k brick by its grid coordinates.

Parameters:

Name Type Description Default
level int

LOD level. WriteLayer only tracks level == 0.

required
grid_coords tuple[int, ...]

One grid index per spatial axis, in data-array axis order. Length matches the painted store's ndim (2 for 2-D, 3 for 3-D).

required